转:iOS 13 问题解决以及苹果登录,暗黑模式

本文转载自:https://juejin.im/post/5d85c3fde51d453b7779d604

本文对应github地址iOS 13 问题解决以及苹果登录,如果由于github调整导致资源找不到或细节更改,请访问github

本文直接搬砖,随便看看就行

iOS 13 (Xcode11编译时)问题解决以及苹果登录

  • KVC修改私有属性可能Crash(不是所有,不是所有,不是所有),需要用别的姿势替代。

    • UITextField的私有属性_placeholderLabel的字体颜色,

    [textField setValue:color forKeyPath:@"_placeholderLabel.textColor"];会crash。

    那么该怎么办呢?下面提供几种姿势

    姿势一:采用富文本形式
    _textField.attributedPlaceholder = [[NSMutableAttributedString alloc] initWithString:placeholder attributes:@{NSForegroundColorAttributeName : color}];
    复制代码
    
    姿势二:new方式创建一个新label(太low不建议用)
    // 必须new创建,如果alloc-init创建还是crash(两种方式差别自行google,不是BD)
    UILabel * placeholderLabel = [UILabel new];
    placeholderLabel.text = @"666";
    placeholderLabel.textColor = [UIColor blueColor];
    [_textField setValue: placeholderLabel forKey:@"_placeholderLabel"];//new创建这里并没有crash
    复制代码
    
    姿势三:Runtime
    Ivar ivar = class_getInstanceVariable([UITextField class], "_placeholderLabel");
    UILabel *placeholderLabel = object_getIvar(_textField, ivar);
    placeholderLabel.textColor = color;
    复制代码
    
    [searchBar valueForKey:@"_searchField"]; 取值崩溃
    - (UITextField *)ddy_SearchField {
        #ifdef __IPHONE_13_0
        if (@available(iOS 13.0, *)) {
            return self.searchTextField;
        }
        #endif
        return [self valueForKey:@"_searchField"];
    }
    复制代码
    

    所以修改UISearchBar占位字符可以把上面的结合使用

  • 模态弹出时 modalPresentationStyle 改变了默认值

    • 在iOS13之前的版本中, UIViewController的UIModalPresentationStyle属性默认是UIModalPresentationFullScreen,而在iOS13中变成了UIModalPresentationPageSheet。
    • 我们需要在presentViewController时,设置一下UIModalPresentationStyle,就可以达到旧的效果。
    • 如果PageSheet想控制下拉dismiss,modalInPresentation可以控制

    该分类所在github工程

    UIViewController+DDYPresent.h下载该文件

    /// 一个一个改浪费时间,适合版本迭代中逐步替换;
    /// 直接重写-modalPresentationStyle 侵入性太大,造成系统弹出也被重置,或者某个控制器想改变样式都不能,不太友好
    /// 所以用一个类方法控制全局,一个实例方法控制具体某个控制器实例样式。
    
    #import <UIKit/UIKit.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface UIViewController (DDYPresent)
    
    /// 如果以后迭代版本想全部用系统原来样式,统一返回NO即可
    /// rentrn BOOL UIImagePickerController/UIAlertController is NO,others is YES
    + (BOOL)ddy_GlobalAutoSetModalPresentationStyle;
    
    /// 具体某个控制器不想更改了(想用系统默认)设置NO
    /// return BOOL [Class ddy_GlobalAutoSetModalPresentationStyle];
    @property (nonatomic, assign) BOOL ddy_AutoSetModalPresentationStyle;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    复制代码
    

    UIViewController+DDYPresent.m下载该文件

    #import "UIViewController+DDYPresent.h"
    #import <objc/runtime.h>
    #import <StoreKit/StoreKit.h>
    复制代码
    

import <SafariServices/SafariServices.h>

@implementation UIViewController (DDYPresent)

+ (void)changeOriginalSEL:(SEL)orignalSEL swizzledSEL:(SEL)swizzledSEL {
    Method originalMethod = class_getInstanceMethod([self class], orignalSEL);
    Method swizzledMethod = class_getInstanceMethod([self class], swizzledSEL);
    if (class_addMethod([self class], orignalSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod([self class], swizzledSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSEL = @selector(presentViewController:animated:completion:);
        SEL swizzledSEL = @selector(ddy_PresentViewController:animated:completion:);
        [self changeOriginalSEL:originalSEL swizzledSEL:swizzledSEL];
    });
}

- (void)ddy_PresentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion {
    if (@available(iOS 13.0, *)) {
        if (viewControllerToPresent.ddy_AutoSetModalPresentationStyle) {
            viewControllerToPresent.modalPresentationStyle = UIModalPresentationFullScreen;
        }
    }
    [self ddy_PresentViewController:viewControllerToPresent animated:flag completion:completion];
}

- (void)setDdy_AutoSetModalPresentationStyle:(BOOL)ddy_AutoSetModalPresentationStyle {
    objc_setAssociatedObject(self, @selector(ddy_AutoSetModalPresentationStyle), @(ddy_AutoSetModalPresentationStyle), OBJC_ASSOCIATION_ASSIGN);
}

- (BOOL)ddy_AutoSetModalPresentationStyle {
    NSNumber *obj = objc_getAssociatedObject(self, @selector(ddy_AutoSetModalPresentationStyle));
    return obj ? [obj boolValue] : [self.class ddy_GlobalAutoSetModalPresentationStyle];
}

// MARK: 排除一些系统控制器
+ (BOOL)ddy_GlobalAutoSetModalPresentationStyle {
    if ([self isKindOfClass:[UIImagePickerController class]]) {
        return NO;
    } else if ([self isKindOfClass:[UIAlertController class]]) {
        return NO;
    } else if ([self isKindOfClass:[UIActivityViewController class]]) {
        return NO;
    } else if ([self isKindOfClass:[UIDocumentInteractionController class]]) {
        return NO;
    } else if ([self isKindOfClass:[SFSafariViewController class]]) {
           return NO;
    }
#ifdef __IPHONE_10_3
    else if ([self isKindOfClass:[SKStoreReviewController class]]) {
        return NO;
    } else if ([self isKindOfClass:[SKStoreProductViewController class]]) {
        return NO;
    }
#endif
    return YES;
}

@end

获取DeviceToken姿势改变

##### iOS13之前

```
NSString *myToken = [deviceToken description];
myToken = [myToken stringByReplacingOccurrencesOfString: @"<" withString: @""];
myToken = [myToken stringByReplacingOccurrencesOfString: @">" withString: @""];
myToken = [myToken stringByReplacingOccurrencesOfString: @" " withString: @""];
```


##### iOS13之后(不建议这样写)

[为什么不建议这样写](https://link.juejin.im/?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fuikit%2Fuiapplicationdelegate%2F1622958-application%3Flanguage%3Dobjc)APNs device tokens are of variable length. Do not hard-code their size

```
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    if (![deviceToken isKindOfClass:[NSData class]]) return;
    const unsigned *tokenBytes = [deviceToken bytes];
    NSString *hexToken = [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x",
                          ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]),
                          ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]),
                          ntohl(tokenBytes[6]), ntohl(tokenBytes[7])];
    NSLog(@"deviceToken:%@",hexToken);
}

##### 推荐的写法

```
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    if (!deviceToken || ![deviceToken isKindOfClass:[NSData class]] || deviceToken.length==0) {
        return;
    }
    NSString *(^getDeviceToken)(void) = ^() {
        if (@available(iOS 13.0, *)) {
            const unsigned char *dataBuffer = (const unsigned char *)deviceToken.bytes;
            NSMutableString *myToken  = [NSMutableString stringWithCapacity:(deviceToken.length * 2)];
            for (int i = 0; i < deviceToken.length; i++) {
                [myToken appendFormat:@"%02x", dataBuffer[I]];
            }
            return (NSString *)[myToken copy];
        } else {
            NSCharacterSet *characterSet = [NSCharacterSet characterSetWithCharactersInString:@"<>"];
            NSString *myToken = [[deviceToken description] stringByTrimmingCharactersInSet:characterSet];
            return [myToken stringByReplacingOccurrencesOfString:@" " withString:@""];
        }
    };
    NSString *myToken = getDeviceToken();
    NSLog(@"%@", myToken);
}
```
  • UIWebView

    • 苹果已经从iOS13禁止UIWebView方式了,需要更换WKWebView(过渡期仍可用,只是邮件警告,目前不影响审核)
  • 即将废弃的 LaunchImage

    • 从iOS 8 苹果引入了 LaunchScreen,我们可以设置LaunchScreen来作为启动页。当然,现在你还可以使用LaunchImage来设置启动图。不过使用LaunchImage的话,要求我们必须提供各种屏幕尺寸的启动图,来适配各种设备,随着苹果设备尺寸越来越多,这种方式显然不够 Flexible。而使用 LaunchScreen的话,情况会变的很简单, LaunchScreen是支持AutoLayout+SizeClass的,所以适配各种屏幕都不在话下。
    • 从2020年4月开始,所有使⽤ iOS13 SDK的 App将必须提供 LaunchScreen,LaunchImage即将退出历史舞台。
  • MPMoviePlayerController 被禁止

    • 这个用的人应该不多了,如果是则更换姿势,如用AVPlayer
  • UITableViewCell中cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;方框问题

    • 低版本Xcode(如Xcode10)编译运行在iOS13上则会出现方框,如果用Xcode11编译则不会出现
  • 增加苹果登录(可选)

  1. 开发者网站在 Sign in with Apple 开启功能

  2. Xcode 里面 Signing & Capabilities 开启 Sign in with Apple 功能

  3. 利用 ASAuthorizationAppleIDButton

    ASAuthorizationAppleIDButton *button = [ASAuthorizationAppleIDButton buttonWithType:ASAuthorizationAppleIDButtonTypeSignIn style:ASAuthorizationAppleIDButtonStyleWhite];
    [button addTarget:self action:@selector(signInWithApple) forControlEvents:UIControlEventTouchUpInside];
    button.center = self.view.center;
    button.bounds = CGRectMake(0, 0, 40, 40); // 宽度过小就没有文字了,只剩图标
    [self.view addSubview:button];
    
  4. ASAuthorizationControllerPresentationContextProviding

    • ASAuthorizationControllerPresentationContextProviding 就一个方法,主要是告诉 ASAuthorizationController 展示在哪个 window 上
    #pragma mark - ASAuthorizationControllerPresentationContextProviding
    
    - (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0))
    {
        return self.view.window;
    }
    
  5. Authorization 发起授权登录请求

    - (void)signInWithApple API_AVAILABLE(ios(13.0)) {
        ASAuthorizationAppleIDProvider *provider = [[ASAuthorizationAppleIDProvider alloc] init];
        ASAuthorizationAppleIDRequest *request = [provider createRequest];
        request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
    
        ASAuthorizationController *vc = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]];
        vc.delegate = self;
        vc.presentationContextProvider = self;
        [vc performRequests];
    }
    
    • ASAuthorizationAppleIDProvider 这个类比较简单,头文件中可以看出,主要用于创建一个 ASAuthorizationAppleIDRequest 以及获取对应 userID 的用户授权状态。在上面的方法中我们主要是用于创建一个 ASAuthorizationAppleIDRequest ,用户授权状态的获取后面会提到。
    • 给创建的 request 设置 requestedScopes ,这是个 ASAuthorizationScope 数组,目前只有两个值,ASAuthorizationScopeFullName 和 ASAuthorizationScopeEmail ,根据需求去设置即可。
    • 然后,创建 ASAuthorizationController ,它是管理授权请求的控制器,给其设置 delegate 和 presentationContextProvider ,最后启动授权 performRequests
  6. 授权回调处理

    #pragma mark - ASAuthorizationControllerDelegate
    
    - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0))
    {
        if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]])       {
            ASAuthorizationAppleIDCredential *credential = authorization.credential;
    
            NSString *state = credential.state;
            NSString *userID = credential.user;
            NSPersonNameComponents *fullName = credential.fullName;
            NSString *email = credential.email;
            NSString *authorizationCode = [[NSString alloc] initWithData:credential.authorizationCode encoding:NSUTF8StringEncoding]; // refresh token
            NSString *identityToken = [[NSString alloc] initWithData:credential.identityToken encoding:NSUTF8StringEncoding]; // access token
            ASUserDetectionStatus realUserStatus = credential.realUserStatus;
    
            NSLog(@"state: %@", state);
            NSLog(@"userID: %@", userID);
            NSLog(@"fullName: %@", fullName);
            NSLog(@"email: %@", email);
            NSLog(@"authorizationCode: %@", authorizationCode);
            NSLog(@"identityToken: %@", identityToken);
            NSLog(@"realUserStatus: %@", @(realUserStatus));
        }
    }
    
    - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0))
    {
        NSString *errorMsg = nil;
        switch (error.code) {
            case ASAuthorizationErrorCanceled:
                errorMsg = @"用户取消了授权请求";
                break;
            case ASAuthorizationErrorFailed:
                errorMsg = @"授权请求失败";
                break;
            case ASAuthorizationErrorInvalidResponse:
                errorMsg = @"授权请求响应无效";
                break;
            case ASAuthorizationErrorNotHandled:
                errorMsg = @"未能处理授权请求";
                break;
            case ASAuthorizationErrorUnknown:
                errorMsg = @"授权请求失败未知原因";
                break;
        }
        NSLog(@"%@", errorMsg);
    }
    
    • User ID: Unique, stable, team-scoped user ID,苹果用户唯一标识符,该值在同一个开发者账号下的所有 App 下是一样的,开发者可以用该唯一标识符与自己后台系统的账号体系绑定起来。

    • Verification data: Identity token, code,验证数据,用于传给开发者后台服务器,然后开发者服务器再向苹果的身份验证服务端验证本次授权登录请求数据的有效性和真实性,详见 Sign In with Apple REST API。如果验证成功,可以根据 userIdentifier 判断账号是否已存在,若存在,则返回自己账号系统的登录态,若不存在,则创建一个新的账号,并返回对应的登录态给 App。

    • Account information: Name, verified email,苹果用户信息,包括全名、邮箱等。

    • Real user indicator: High confidence indicator that likely real user,用于判断当前登录的苹果账号是否是一个真实用户,取值有:unsupported、unknown、likelyReal。

    • 失败情况会走 authorizationController:didCompleteWithError: 这个方法,具体看代码吧

  7. 其他情况的处理

    • 用户终止 App 中使用 Sign in with Apple 功能
    • 用户在设置里注销了 AppleId

    这些情况下,App 需要获取到这些状态,然后做退出登录操作,或者重新登录。
    我们需要在 App 启动的时候,通过 getCredentialState:completion: 来获取当前用户的授权状态

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
        if (@available(iOS 13.0, *)) {
            NSString *userIdentifier = 钥匙串中取出的 userIdentifier;
            if (userIdentifier) {
                ASAuthorizationAppleIDProvider *appleIDProvider = [ASAuthorizationAppleIDProvider new];
                [appleIDProvider getCredentialStateForUserID:userIdentifier
                                                  completion:^(ASAuthorizationAppleIDProviderCredentialState credentialState,
                                                               NSError * _Nullable error)
                {
                    switch (credentialState) {
                        case ASAuthorizationAppleIDProviderCredentialAuthorized:
                            // The Apple ID credential is valid
                            break;
                        case ASAuthorizationAppleIDProviderCredentialRevoked:
                            // Apple ID Credential revoked, handle unlink
                            break;
                        case ASAuthorizationAppleIDProviderCredentialNotFound:
                            // Credential not found, show login UI
                            break;
                    }
                }];
            }
        }
    
        return YES;
    }
    

    ASAuthorizationAppleIDProviderCredentialState 解析如下:

    • ASAuthorizationAppleIDProviderCredentialAuthorized 授权状态有效;
    • ASAuthorizationAppleIDProviderCredentialRevoked 上次使用苹果账号登录的凭据已被移除,需解除绑定并重新引导用户使用苹果登录;
    • ASAuthorizationAppleIDProviderCredentialNotFound 未登录授权,直接弹出登录页面,引导用户登录
  8. 还可以通过通知方法来监听 revoked 状态,可以添加 ASAuthorizationAppleIDProviderCredentialRevokedNotification 这个通知

    - (void)observeAppleSignInState {
        if (@available(iOS 13.0, *)) {
            [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(handleSignInWithAppleStateChanged:)
                                                         name:ASAuthorizationAppleIDProviderCredentialRevokedNotification
                                                       object:nil];
        }
    }
    
    - (void)handleSignInWithAppleStateChanged:(NSNotification *)notification {
        // Sign the user out, optionally guide them to sign in again
        NSLog(@"%@", notification.userInfo);
    }
    
  9. One more thing

    苹果还把 iCloud KeyChain password 集成到了这套 API 里,我们在使用的时候,只需要在创建 request 的时候,多创建一个 ASAuthorizationPasswordRequest ,这样如果 KeyChain 里面也有登录信息的话,可以直接使用里面保存的用户名和密码进行登录。代码如下

    - (void)perfomExistingAccountSetupFlows API_AVAILABLE(ios(13.0))
    {
        ASAuthorizationAppleIDProvider *appleIDProvider = [ASAuthorizationAppleIDProvider new];
        ASAuthorizationAppleIDRequest *authAppleIDRequest = [appleIDProvider createRequest];
        ASAuthorizationPasswordRequest *passwordRequest = [[ASAuthorizationPasswordProvider new] createRequest];
    
        NSMutableArray <ASAuthorizationRequest *>* array = [NSMutableArray arrayWithCapacity:2];
        if (authAppleIDRequest) {
            [array addObject:authAppleIDRequest];
        }
        if (passwordRequest) {
            [array addObject:passwordRequest];
        }
        NSArray <ASAuthorizationRequest *>* requests = [array copy];
    
        ASAuthorizationController *authorizationController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:requests];
        authorizationController.delegate = self;
        authorizationController.presentationContextProvider = self;
        [authorizationController performRequests];
    }
    
    #pragma mark - ASAuthorizationControllerDelegate
    
    - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0))
    {
        if ([authorization.credential isKindOfClass:[ASPasswordCredential class]]) {
            ASPasswordCredential *passwordCredential = authorization.credential;
            NSString *userIdentifier = passwordCredential.user;
            NSString *password = passwordCredential.password;
    
            NSLog(@"userIdentifier: %@", userIdentifier);
            NSLog(@"password: %@", password);
        }
    }
    
  10. 官方Demo

  • Flutter1.9.1+hotfix2 Dart2.5 在iOS13真机上启动不了

    错误信息 Device doesn't support wireless sync. AMDeviceStartService(device, CFSTR("com.apple.debugserver"), &gdbfd, NULL)

    解决方案

    更新Flutter到dev或master

    flutter channel dev
    // 或下面
    // flutter channel master
    // 然后执行
    flutter doctor
    // dart2.6 flutter1.10
    
  • 获取不到wifiSSID(wifi名)

    Dear Developer,
    
    As we announced at WWDC19, we're making changes to further protect user privacy and prevent unauthorized location tracking. Starting with iOS 13, the CNCopyCurrentNetworkInfo API will no longer return valid Wi-Fi SSID and BSSID information. Instead, the information returned by default will be: 
    
    SSID: “Wi-Fi” or “WLAN” (“WLAN" will be returned for the China SKU)
    BSSID: "00:00:00:00:00:00" 
    
    If your app is using this API, we encourage you to adopt alternative approaches that don’t require Wi-Fi or network information. Valid SSID and BSSID information from CNCopyCurrentNetworkInfo will still be provided to VPN apps, apps that have used NEHotspotConfiguration to configure the current Wi-Fi network, and apps that have obtained permission to access user location through Location Services. 
    
    Test your app on the latest iOS 13 beta to make sure it works properly. If your app requires valid Wi-Fi SSID and BSSID information to function, you can do the following:
    For accessory setup apps, use the NEHotSpotConfiguration API, which now has the option to pass a prefix of the SSID hotspot your app expects to connect to.
    For other types of apps, use the CoreLocation API to request the user’s consent to access location information.
    
    Learn more by reading the updated documentation or viewing the the Advances in Networking session video from WWDC19\. You can also submit a TSI for code-level support. 
    
    Best regards,
    Apple Developer Relations
    

    苹果为了所谓隐私安全不让直接获取到wifiSSID了,然后还告知,如果是使用 NEHotspotConfiguration 的app可以获取,另外其他类型app需要用CoreLocation请求位置权限,用户同意后才可以获取。

    • 使用NEHotspotConfiguration的app
    // 连接WiFi
    NEHotspotConfiguration *config = [[NEHotspotConfiguration alloc] initWithSSID:@"wifi名" passphrase:@"密码" isWEP:NO];
    NEHotspotConfigurationManager *manager = [NEHotspotConfigurationManager sharedManager];
    [manager applyConfiguration: config completionHandler:^(NSError * _Nullable error) {
        NSLog(@"error :%@",error);
    }];
    
    // 获取wifiSSID 
    - (NSString *)wifiSSID {
        NSString *wifiSSID = nil;
    
        CFArrayRef wifiInterfaces = CNCopySupportedInterfaces();
    
        if (!wifiInterfaces) {
            return nil;
        }
    
        NSArray *interfaces = (__bridge NSArray *)wifiInterfaces;
    
        for (NSString *interfaceName in interfaces) {
            CFDictionaryRef dictRef = CNCopyCurrentNetworkInfo((__bridge CFStringRef)(interfaceName));
            if (dictRef) {
                NSDictionary *networkInfo = (__bridge NSDictionary *)dictRef;
                wifiSSID = [networkInfo objectForKey:(__bridge NSString *)kCNNetworkInfoKeySSID];
                CFRelease(dictRef);
            }
        }
        CFRelease(wifiInterfaces);
        return wifiName;
    }
    
    • 请求位置权限征求用户同意后获取wifiSSID

    推荐使用封装好的请求权限方式github.com/RainOpen/DD…

    #import <SystemConfiguration/CaptiveNetwork.h>
    
    - (void)ddy_wifiSSID:(void (^)(NSString *wifiSSID))wifiSSID {
    
        void (^callBack)(NSString *) = ^(NSString *wifiName) {
            if (wifiSSID) {
                wifiSSID(nil);
            }
        };
    
        void (^getWifiName)(void) = ^(){
            CFArrayRef wifiInterfaces = CNCopySupportedInterfaces();
            if (!wifiInterfaces) {
                callBack(nil);
                return;
            }
    
            NSString *wifiName = nil;
            for (NSString *interfaceName in (__bridge_transfer NSArray *)wifiInterfaces) {
                CFDictionaryRef dictRef = CNCopyCurrentNetworkInfo((__bridge CFStringRef)(interfaceName));
                if (dictRef) {
                    NSDictionary *networkInfo = (__bridge NSDictionary *)dictRef;
                    wifiName = [networkInfo objectForKey:(__bridge NSString *)kCNNetworkInfoKeySSID];
                    CFRelease(dictRef);
                }
            }
            CFRelease(wifiInterfaces);
            callBack(wifiName);
        };
    
        if ([CLLocationManager locationServicesEnabled]) {
            [DDYAuthManager ddy_LocationAuthType:DDYCLLocationTypeAuthorized alertShow:YES success:^{
                getWifiName();
            } fail:^(CLAuthorizationStatus authStatus) {
                NSLog(@"定位服务被拒绝,弹窗告诉无法获取wifiSSID,请到设置开启定位权限");
                callBack(nil);
            }];
        } else {
            NSLog(@"定位服务不可用");
            callBack(nil);
        }
    }
    
    • VPN旧版应用
  • Xcode10往iOS13上编译运行提示 Could not find Developer Disk Image

    1. 下载开发包
    2. 强制退出Xcode(必须退出干净)
    3. 前往"/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport"粘贴解压缩文件(以自己实际路径实际名称)
  • iOS 13 UITabBar上分割线呢操作

    原来设置分割线的方式失效了

    [[UITabBar appearance] setBackgroundImage:[UIImage new]];
    [[UITabBar appearance] setShadowImage:[UIImage new]];
    复制代码
    

    最新更改TabBar上细线方式实例,利用苹果提供的新API,为所欲为(改图片,改颜色)

    // OC
    if (@available(iOS 13, *)) {
        #ifdef __IPHONE_13_0
        UITabBarAppearance *appearance = [self.tabBar.standardAppearance copy];
        appearance.backgroundImage = [UIImage new];
        appearance.shadowImage = [UIImage imageNamed:@"Dotted_Line"];
        appearance.shadowColor = [UIColor clearColor];
        self.tabBar.standardAppearance = appearance;
        #endif
    } else {
        self.tabBar.backgroundImage = [UIImage new];
        self.tabBar.shadowImage = [UIImage imageNamed:@"Dotted_Line"];
    }
    
    // Swift
    if #available(iOS 13, *) {
        let appearance = self.tabBar.standardAppearance.copy()
        appearance.backgroundImage = UIImage()
        appearance.shadowImage = UIImage()
        appearance.shadowColor = .clear
        self.tabBar.standardAppearance = appearance
    } else {
        self.tabBar.shadowImage = UIImage()
        self.tabBar.backgroundImage = UIImage()
    }
    
  • 暗黑模式

  • library not found for -l stdc++.6.0.9
    • Xcode10开始去除了C++6.0.9

    • 如果非用不可,下载文件

    // 文件夹 1、2、3、4 中的文件分别对应复制到Xcode10中的以下4个目录中即可(Xcode11目录可能有变更)
    // 假设默认安装目录且Xcode.app命名
    
    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/
    
    /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/
    
    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/
    
    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/
    复制代码
    

    更新Xcode11目录变更

    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/
    ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 变更为 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/
    复制代码
    
    • 换个姿势试试
    1\. TARGETS–Build Phases–Link Binary With Libraries,删除6.0.9依赖,
    2\. 需要的话对应添加libc++.tdb、libstdc++.tdb
    3\. TARGETS–Build Settings–Other Linker Flags,删除 -l”stdc++.6.0.9”
    4\. 如果是第三库引用了C++6.0.9库,那就只能联系服务方修改了
    复制代码
    
  • Multiple commands produce 'xxx/Info.plist'

    • Xcode10开始变更编译系统,如果项目所在空间存在多个Info.plist则报错
    xcworkspace项目: Xcode左上角菜单栏 File –> Workspace Settings –> Build System – >Legacy Build System
    xcodeproj项目:Xcode左上角菜单栏 –> File –> Project Settings –> Build System –> Legacy Build System
    复制代码
    
  • 升级Xcode后xib报错 Failed to find or create execution context for description ...

    • 可以万能重启,还可以。。。
    sudo killall -9 com.apple.CoreSimulator.CoreSimulatorService
    
    # 将你xcode中Developer文件夹位置放进来
    sudo xcode-select -s  /Applications/Xcode.app/Contents/Developer
    
    xcrun simctl erase all
    复制代码
    
  • 友盟导致崩溃 +[_LSDefaults sharedInstance]

    // 本工地大工没实际验证。。。
    @implementation NSObject (DDYExtension)
    
    + (void)changeOriginalSEL:(SEL)orignalSEL swizzledSEL:(SEL)swizzledSEL {
        Method originalMethod = class_getInstanceMethod([self class], orignalSEL);
        Method swizzledMethod = class_getInstanceMethod([self class], swizzledSEL);
        if (class_addMethod([self class], orignalSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
            class_replaceMethod([self class], swizzledSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    }
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            SEL originalSEL = @selector(doesNotRecognizeSelector:);
            SEL swizzledSEL = @selector(ddy_doesNotRecognizeSelector:);
            [self changeOriginalSEL:originalSEL swizzledSEL:swizzledSEL];
        });
    }
    
    + (void)ddy_doesNotRecognizeSelector:(SEL)aSelector{
        // 处理 _LSDefaults 崩溃问题
        if([[self description] isEqualToString:@"_LSDefaults"] && (aSelector == @selector(sharedInstance))){
            //冷处理...
            return;
        }
        [self ddy_doesNotRecognizeSelector:aSelector];
    }
    复制代码
    
  • UITextField的leftView和rightView设置UIImageView或UIButton等被系统强(变)奸(窄)了。。。

    • 临时解决方案,交换leftView/rightView的getter、setter,
    • 然后包装一个containerView父视图,并将containerView给了相应左右视图
    • 取视图则先取出containerView,从containerView中取出真正想要的视图
    • 注意处理在containerView上的位置。。。
  • UIScrollView滚动条指示器偏移

    // 屏幕旋转可能会触发系统对滚动条的自动修正,如果没有修改需求,关闭该特性即可
    #ifdef __IPHONE_13_0
    if (@available(iOS 13.0, *)) {
       self.automaticallyAdjustsScrollIndicatorInsets = NO;
    }
    #endif
    复制代码
    
  • WKWebView 中测量页面内容高度的方式变更

    iOS 13以前 document.body.scrollHeight iOS 13开始 document.documentElement.scrollHeight

  • CBCenterManager 蓝牙使用权限

    iOS13之前不用申请权限,iOS13开始需要申请权限

    <key>NSBluetoothAlwaysUsageDescription</key> 
    <string>App想使用蓝牙,是否同意</string>`
    复制代码
    
  • Xcode10的过时问题,虫洞传送门

  • Xcode11 创建新工程在AppDelegate.m中设置window不生效

    • Xcode11把入口交给了scene(为SwiftUI多场景打基础的)
    • 想要换回AppDelegate入口
    • 打开Info.plist点击减号删除Application Scene Mainfest
    • 再删除SceneDelegate两个文件
    • 删除AppDelegate.m中UISceneSession lifecycle两个方法
    • 最后AppDelegate.h中添加 @property (strong, nonatomic) UIWindow *window;
参考
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,154评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,252评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,107评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,985评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,905评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,256评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,978评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,611评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,891评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,910评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,736评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,516评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,995评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,034评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,242评论 2 339

推荐阅读更多精彩内容