1. 程式人生 > IOS開發 >iOS 13 問題解決以及蘋果登入,暗黑模式

iOS 13 問題解決以及蘋果登入,暗黑模式

本文對應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)
    
    /// 自動調整模態彈出樣式時要排除的控制器(如果未設定則使用內建)
    /// @param controllerNameArray 模態彈出的控制器名稱陣列
    + (void)ddy_ExcludeControllerNameArray:(NSArray<NSString *> *)controllerNameArray;
    
    /// 是否自動調整模態彈出全屏樣式
    /// NO:表示不自動調整,保持預設,可能全屏樣式也可能其他樣式
    /// YES:表示調整為全屏樣式
    /// 如果是排除的控制器陣列包含的控制器則預設NO
    /// 如果不在排除的控制器陣列內包含則預設YES
    @property (nonatomic,assign) BOOL ddy_AutoSetModalPresentationStyleFullScreen;
    
    @end
    
    NS_ASSUME_NONNULL_END
        
    複製程式碼

    UIViewController+DDYPresent.m下載該檔案

    #import "UIViewController+DDYPresent.h"
    #import <objc/runtime.h>
    
    @implementation UIViewController (DDYPresent)
    
    static NSArray *excludeControllerNameArray;
    
    + (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_AutoSetModalPresentationStyleFullScreen) {
                viewControllerToPresent.modalPresentationStyle = UIModalPresentationFullScreen;
            }
        }
        [self ddy_PresentViewController:viewControllerToPresent animated:flag completion:completion];
    }
    
    - (void)setDdy_AutoSetModalPresentationStyleFullScreen:(BOOL)ddy_AutoSetModalPresentationStyleFullScreen {
        objc_setAssociatedObject(self,@selector(ddy_AutoSetModalPresentationStyleFullScreen),@(ddy_AutoSetModalPresentationStyleFullScreen),OBJC_ASSOCIATION_ASSIGN);
    }
    
    - (BOOL)ddy_AutoSetModalPresentationStyleFullScreen {
        NSNumber *obj = objc_getAssociatedObject(self,@selector(ddy_AutoSetModalPresentationStyleFullScreen));
        return obj ? [obj boolValue] : ![UIViewController ddy_IsExcludeSetModalPresentationStyleFullScreen:NSStringFromClass(self.class)];
    }
    
    // MARK: - 類方法
    // MARK: 全域性設定排除的控制器
    + (void)ddy_ExcludeControllerNameArray:(NSArray<NSString *> *)controllerNameArray {
        excludeControllerNameArray = controllerNameArray;
    }
    
    // MARK: 如果沒有外部設定則使用內建的排除陣列
    + (NSArray<NSString *> *)ddy_InnerExcludeControllerNameArray {
        NSMutableArray *nameArray = [NSMutableArray array];
        [nameArray addObject:@"UIImagePickerController"];
        [nameArray addObject:@"UIAlertController"];
        [nameArray addObject:@"UIActivityViewController"];
        [nameArray addObject:@"UIDocumentInteractionController"];
        [nameArray addObject:@"SLComposeViewController"]; //  #import <Social/Social.h>
        [nameArray addObject:@"SLComposeServiceViewController"]; // #import <Social/Social.h>
        [nameArray addObject:@"UIMenuController"];
        [nameArray addObject:@"SFSafariViewController"]; // API_AVAILABLE(ios(9.0)) #import <SafariServices/SafariServices.h>
        [nameArray addObject:@"SKStoreReviewController"]; // API_AVAILABLE(ios(10.3),macos(10.14)) #import <StoreKit/StoreKit.h>
        [nameArray addObject:@"SKStoreProductViewController"]; // API_AVAILABLE(ios(6.0)) #import <StoreKit/StoreKit.h>
        return nameArray;
    }
    
    // MARK: 是否是要排除自動設定的控制器
    + (BOOL)ddy_IsExcludeSetModalPresentationStyleFullScreen:(NSString *)className {
        NSArray *nameArray = excludeControllerNameArray ?: [UIViewController ddy_InnerExcludeControllerNameArray];
        return [nameArray containsObject:className];
    }
    
    @end    
    複製程式碼
  • 獲取DeviceToken姿勢改變

    iOS13之前
    NSString *myToken = [deviceToken description];
    myToken = [myToken stringByReplacingOccurrencesOfString: @"<" withString: @""];
    myToken = [myToken stringByReplacingOccurrencesOfString: @">" withString: @""];
    myToken = [myToken stringByReplacingOccurrencesOfString: @" " withString: @""];
    複製程式碼
    iOS13之後(不建議這樣寫)

    為什麼不建議這樣寫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(過渡期仍可用,只是郵件警告,目前不影響稽核)
    • 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,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

    flutter upgrade
    複製程式碼
    • 姿勢二:

    暫時切換到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()
    }
    複製程式碼
  • iOS 13 Push後Pop回來tabbar選中文字顏色變系統藍色(OC程式碼,swift一個樣)

    • 姿勢一
    self.tabBar.tinColor = color;
    複製程式碼
    • 姿勢二
    if (@available(iOS 10.0,*)) {
    	self.tabBar.unselectedItemTintColor = color;
    }
    複製程式碼
  • 暗黑模式

  • 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],^{
            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上的位置。。。
    • 姿勢二:通用型方案
      • 想全域性更改,交換 -leftViewRectForBounds: (最好留出控制屬性以在外部隨意更改玩轉)
      • 如果採用子類,重寫 -leftViewRectForBounds:
      - (CGRect)leftViewRectForBounds:(CGRect)bounds {
          CGRect iconRect = [super leftViewRectForBounds:bounds];
          iconRect.origin.x = 3; // 可用屬性控制 
          iconRect.size.width = 6; // 可用屬性控制 
          return iconRect;
      }	
      複製程式碼
  • 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;