1. 程式人生 > IOS開發 >iOS 13原生端適配攻略

iOS 13原生端適配攻略

隨著iOS 13的釋出,公司的專案也勢必要著手適配了。現彙總一下iOS 13的各種坑

目錄

1. KVC訪問私有屬性

2. 模態彈窗ViewController 預設樣式改變

3. 黑暗模式的適配

4. LaunchImage即將廢棄

5. 新增一直使用藍芽的許可權申請

6. Sign With Apple

7. 推送Device Token適配

8. UIKit 控制元件變化

9. StatusBar新增樣式


1. KVC訪問私有屬性

 這次iOS 13系統升級,影響範圍最廣的應屬KVC訪問修改私有屬性了,直接禁止開發者獲取或直接設定私有屬性。而KVC的初衷是允許開發者通過Key名直接訪問修改物件的屬性值,為其中最典型的 UITextField

_placeholderLabelUISearchBar_searchField。 造成影響:在iOS 13下App閃退 錯誤程式碼:

// placeholderLabel私有屬性訪問
[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[textField setValue:[UIFont boldSystemFontOfSize:16] forKeyPath:@"_placeholderLabel.font"];
// searchField私有屬性訪問
UISearchBar *searchBar = [[UISearchBar alloc] init];
UITextField *searchTextField = [searchBar valueForKey:@"_searchField"
]; 複製程式碼

解決方案:  使用 NSMutableAttributedString 富文字來替代KVC訪問 UITextField_placeholderLabel

textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"placeholder" attributes:@{NSForegroundColorAttributeName: [UIColor darkGrayColor],NSFontAttributeName: [UIFont systemFontOfSize:13]}];
複製程式碼

 因此,可以為UITextFeild建立Category,專門用於處理修改placeHolder屬性提供方法

#import "UITextField+ChangePlaceholder.h"

@implementation UITextField (Change)

- (void)setPlaceholderFont:(UIFont *)font {

  [self setPlaceholderColor:nil font:font];
}

- (void)setPlaceholderColor:(UIColor *)color {

  [self setPlaceholderColor:color font:nil];
}

- (void)setPlaceholderColor:(nullable UIColor *)color font:(nullable UIFont *)font {

  if ([self checkPlaceholderEmpty]) {
      return;
  }

  NSMutableAttributedString *placeholderAttriString = [[NSMutableAttributedString alloc] initWithString:self.placeholder];
  if (color) {
      [placeholderAttriString addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0,self.placeholder.length)];
  }
  if (font) {
      [placeholderAttriString addAttribute:NSFontAttributeName value:font range:NSMakeRange(0,self.placeholder.length)];
  }

  [self setAttributedPlaceholder:placeholderAttriString];
}

- (BOOL)checkPlaceholderEmpty {
  return (self.placeholder == nil) || ([[self.placeholder stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] == 0);
}
複製程式碼

 關於 UISearchBar,可遍歷其所有子檢視,找到指定的 UITextField 型別的子檢視,再根據上述 UITextField 的通過富文字方法修改屬性。

#import "UISearchBar+ChangePrivateTextFieldSubview.h"

@implementation UISearchBar (ChangePrivateTextFieldSubview)

/// 修改SearchBar系統自帶的TextField
- (void)changeSearchTextFieldWithCompletionBlock:(void(^)(UITextField *textField))completionBlock {

    if (!completionBlock) {
        return;
    }
    UITextField *textField = [self findTextFieldWithView:self];
    if (textField) {
        completionBlock(textField);
    }
}

/// 遞迴遍歷UISearchBar的子檢視,找到UITextField
- (UITextField *)findTextFieldWithView:(UIView *)view {

    for (UIView *subview in view.subviews) {
        if ([subview isKindOfClass:[UITextField class]]) {
            return (UITextField *)subview;
        }else if (subview.subviews.count > 0) {
            return [self findTextFieldWithView:subview];
        }
    }
    return nil;
}
@end
複製程式碼

 PS:關於如何查詢自己的App專案是否使用了私有api,可以參考 iOS查詢私有API 文章


2. 模態彈窗 ViewController 預設樣式改變

 模態彈窗屬性 UIModalPresentationStyle 在 iOS 13 下預設被設定為 UIModalPresentationAutomatic新特性,展示樣式更為炫酷,同時可用下拉手勢關閉模態彈窗。 若原有模態彈出 ViewController 時都已指定模態彈窗屬性,則可以無視該改動。 若想在 iOS 13 中繼續保持原有預設模態彈窗效果。可以通過 runtime 的 Method Swizzling 方法交換來實現。

#import "UIViewController+ChangeDefaultPresentStyle.h"

@implementation UIViewController (ChangeDefaultPresentStyle)

+ (void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken,^{
        Class class = [self class];
        //替換方法
        SEL originalSelector = @selector(presentViewController:animated:completion:);
        SEL newSelector = @selector(new_presentViewController:animated:completion:);

        Method originalMethod = class_getInstanceMethod(class,originalSelector);
        Method newMethod = class_getInstanceMethod(class,newSelector);;
        BOOL didAddMethod =
        class_addMethod(class,originalSelector,method_getImplementation(newMethod),method_getTypeEncoding(newMethod));

        if (didAddMethod) {
            class_replaceMethod(class,newSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));

        } else {
            method_exchangeImplementations(originalMethod,newMethod);
        }
    });
}

- (void)new_presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion {

    viewControllerToPresent.modalPresentationStyle = UIModalPresentationFullScreen;
    [self new_presentViewController:viewControllerToPresent animated:flag completion:completion];
}

@end
複製程式碼

3. 黑暗模式的適配

 針對黑暗模式的推出,Apple官方推薦所有三方App儘快適配。目前並沒有強制App進行黑暗模式適配。因此黑暗模式適配範圍現在可採用以下三種策略:

  • 全域性關閉黑暗模式
  • 指定頁面關閉黑暗模式
  • 全域性適配黑暗模式

3.1. 全域性關閉黑暗模式

 方案一:在專案 Info.plist 檔案中,新增一條內容,Key為 User Interface Style,值型別設定為String並設定為 Light 即可。

 方案二:程式碼強制關閉黑暗模式,將當前 window 設定為 Light 狀態。

if(@available(iOS 13.0,*)){
self.window.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
}
複製程式碼

3.2 指定頁面關閉黑暗模式

 從Xcode 11、iOS 13開始,UIViewController與View新增屬性 overrideUserInterfaceStyle,若設定View物件該屬性為指定模式,則強制該物件以及子物件以指定模式展示,不會跟隨系統模式改變。

  • 設定 ViewController 該屬性, 將會影響檢視控制器的檢視以及子檢視控制器都採用該模式
  • 設定 View 該屬性, 將會影響檢視及其所有子檢視採用該模式
  • 設定 Window 該屬性, 將會影響視窗中的所有內容都採用該樣式,包括根檢視控制器和在該視窗中顯示內容的所有控制器

3.3 全域性適配黑暗模式

 適配黑暗模式,主要從兩方面入手:圖片資源適配與顏色適配

圖片資源適配

 開啟圖片資源管理庫 Assets.xcassets,選中需要適配的圖片素材item,開啟最右側的 Inspectors 工具欄,找到 Appearances 選項,並設定為 Any,Dark模式,此時會在item下增加Dark Appearance,將黑暗模式下的素材拖入即可。關於黑暗模式圖片資源的載入,與正常載入圖片方法一致。

圖片資源適配黑暗模式

顏色適配

 iOS 13開始UIColor變為動態顏色,在Light Mode與Dark Mode可以分別設定不同顏色。 若UIColor色值管理,與圖片資源一樣儲存於 Assets.xcassets 中,同樣參照上述方法適配。 若UIColor色值並沒有儲存於 Assets.xcassets 情況下,自定義動態UIColor時,在iOS 13下初始化方法增加了兩個方法

+ (UIColor *)colorWithDynamicProvider:(UIColor * (^)(UITraitCollection *))dynamicProvider API_AVAILABLE(ios(13.0),tvos(13.0)) API_UNAVAILABLE(watchos);
- (UIColor *)initWithDynamicProvider:(UIColor * (^)(UITraitCollection *))dynamicProvider API_AVAILABLE(ios(13.0),tvos(13.0)) API_UNAVAILABLE(watchos);
複製程式碼
  • 這兩個方法要求傳一個block,block會返回一個 UITraitCollection 類
  • 當系統在黑暗模式與正常模式切換時,會觸發block回撥 示例程式碼:
UIColor *dynamicColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull trainCollection) {
        if ([trainCollection userInterfaceStyle] == UIUserInterfaceStyleLight) {
            return [UIColor whiteColor];
        } else {
            return [UIColor blackColor];
        }
    }];
    
 [self.view setBackgroundColor:dynamicColor];
複製程式碼

 當然了,iOS 13系統也預設提供了一套基本的黑暗模式UIColor動態顏色,具體宣告如下:

@property (class,nonatomic,readonly) UIColor *systemBrownColor        API_AVAILABLE(ios(13.0),tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class,readonly) UIColor *systemIndigoColor       API_AVAILABLE(ios(13.0),readonly) UIColor *systemGray2Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,watchos);
@property (class,readonly) UIColor *systemGray3Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,readonly) UIColor *systemGray4Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,readonly) UIColor *systemGray5Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,readonly) UIColor *systemGray6Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,readonly) UIColor *labelColor              API_AVAILABLE(ios(13.0),readonly) UIColor *secondaryLabelColor     API_AVAILABLE(ios(13.0),readonly) UIColor *tertiaryLabelColor      API_AVAILABLE(ios(13.0),readonly) UIColor *quaternaryLabelColor    API_AVAILABLE(ios(13.0),readonly) UIColor *linkColor               API_AVAILABLE(ios(13.0),readonly) UIColor *placeholderTextColor    API_AVAILABLE(ios(13.0),readonly) UIColor *separatorColor          API_AVAILABLE(ios(13.0),readonly) UIColor *opaqueSeparatorColor    API_AVAILABLE(ios(13.0),readonly) UIColor *systemBackgroundColor                   API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,readonly) UIColor *secondarySystemBackgroundColor          API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,readonly) UIColor *tertiarySystemBackgroundColor           API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,readonly) UIColor *systemGroupedBackgroundColor            API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,readonly) UIColor *secondarySystemGroupedBackgroundColor   API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,readonly) UIColor *tertiarySystemGroupedBackgroundColor    API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,readonly) UIColor *systemFillColor                         API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,readonly) UIColor *secondarySystemFillColor                API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,readonly) UIColor *tertiarySystemFillColor                 API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,readonly) UIColor *quaternarySystemFillColor               API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos,watchos);
複製程式碼

監聽模式的切換

 當需要監聽系統模式發生變化並作出響應時,需要用到 ViewController 以下函式

// 注意:引數為變化前的traitCollection,改函式需要重寫
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;
 
// 判斷兩個UITraitCollection物件是否不同
- (BOOL)hasDifferentColorAppearanceComparedToTraitCollection:(UITraitCollection *)traitCollection;
複製程式碼

示例程式碼:

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
    [super traitCollectionDidChange:previousTraitCollection];
    // trait has Changed?
    if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
    // do something...
    }
    }
複製程式碼

系統模式變更,自定義重繪檢視

 當系統模式變更時,系統會通知所有的 View以及 ViewController 需要更新樣式,會觸發以下方法執行(參考Apple官方適配連結):

NSView

- (void)updateLayer;
- (void)drawRect:(NSRect)dirtyRect;
- (void)layout;
- (void)updateConstraints;
複製程式碼

UIView

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;
- (void)layoutSubviews;
- (void)drawRect:(NSRect)dirtyRect;
- (void)updateConstraints;
- (void)tintColorDidChange;
複製程式碼

UIViewController

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;
- (void)updateViewConstraints;
- (void)viewWillLayoutSubviews;
- (void)viewDidLayoutSubviews;
複製程式碼

UIPresentationController

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;
- (void)containerViewWillLayoutSubviews;
- (void)containerViewDidLayoutSubviews;
複製程式碼

4. LaunchImage即將廢棄

 使用 LaunchImage 設定啟動圖,需要提供各類螢幕尺寸的啟動圖適配,這種方式隨著各類裝置尺寸的增加,增加了額外不必要的工作量。為了解決 LaunchImage 帶來的弊端,iOS 8引入了 LaunchScreen 技術,因為支援 AutoLayout + SizeClass,所以通過 LaunchScreen 就可以簡單解決適配當下以及未來各種螢幕尺寸。 Apple官方已經發出公告,2020年4月開始,所有使用iOS 13 SDK 的App都必須提供 LaunchScreen。 建立一個 LaunchScreen 也非常簡單 (1)New Files建立一個 LaunchScreen,在建立的 ViewController 下 View 中新建一個 Image,並配置 Image 的圖片 (2)調整 Image 的 frame 為佔滿螢幕,並修改 Image 的 Autoresizing 如下圖,完成

Image 的 Autoresizing 配置

5. 新增一直使用藍芽的許可權申請

 在iOS13之前,無需許可權提示窗即可直接使用藍芽,但在iOS 13下,新增了使用藍芽的許可權申請。最近一段時間上傳IPA包至App Store會收到以下提示。

解決方案:只需要在 Info.plist 裡增加以下條目:

<key>NSBluetoothAlwaysUsageDescription</key> 
<string>這裡輸入使用藍芽來做什麼</string>`
複製程式碼

6. Sign With Apple

 在iOS 13系統中,Apple要求提供第三方登入的App也要支援「Sign With Apple」,具體實踐參考 iOS Sign With Apple實踐


7. 推送Device Token適配

 在iOS 13之前,獲取Device Token 是將系統返回的 NSData 型別資料通過 -(void)description; 方法直接轉換成 NSString 字串。 iOS 13之前獲取結果:

iOS 13之後獲取結果:
 適配方案: 目的是要將系統返回 NSData 型別資料轉換成字串,再傳給推送服務方。-(void)description; 本身是用於為類除錯提供相關的列印資訊,嚴格來說,不應直接從該方法獲取資料並應用於正式環境中。將 NSData 轉換成 HexString,即可滿足適配需求。

- (NSString *)getHexStringForData:(NSData *)data {
    NSUInteger length = [data length];
    char *chars = (char *)[data bytes];
    NSMutableString *hexString = [[NSMutableString alloc] init];
    for (NSUInteger i = 0; i < length; i++) {
        [hexString appendString:[NSString stringWithFormat:@"%0.2hhx",chars[i]]];
    }
    return hexString;
}
複製程式碼

8. UIKit 控制元件變化

 主要還是參照了Apple官方的 UIKit 修改文件宣告。iOS 13 Release Notes

8.1. UITableView

 iOS 13下設定 cell.contentView.backgroundColor 會直接影響 cell 本身 selected 與 highlighted 效果。 建議不要對 contentView.backgroundColor 修改,而對 cell 本身進行設定。

8.2. UITabbar

Badge 文字大小變化

 iOS 13之後,Badge 字型預設由13號變為17號。 建議在初始化 TabbarController 時,顯示 Badge 的 ViewController 呼叫 setBadgeTextAttributes:forState: 方法

if (@available(iOS 13,*)) {
    [viewController.tabBarItem setBadgeTextAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:13]} forState:UIControlStateNormal];
    [viewController.tabBarItem setBadgeTextAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:13]} forState:UIControlStateSelected];
}
複製程式碼

8.2. UITabBarItem

載入gif需設定 scale 比例

NSData *data = [NSData dataWithContentsOfFile:path];
CGImageSourceRef gifSource = CGImageSourceCreateWithData(CFBridgingRetain(data),nil);
size_t gifCount = CGImageSourceGetCount(gifSource);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(gifSource,i,NULL);

//  iOS 13之前
UIImage *image = [UIImage imageWithCGImage:imageRef]
//  iOS 13之後新增scale比例(該imageView將展示該動圖效果)
UIImage *image = [UIImage imageWithCGImage:imageRef scale:image.size.width / CGRectGetWidth(imageView.frame) orientation:UIImageOrientationUp];

CGImageRelease(imageRef);
複製程式碼

無文字時圖片位置調整

 iOS 13下不需要調整 imageInsets,圖片會自動居中顯示,因此只需要針對iOS 13之前的做適配即可。

if (IOS_VERSION < 13.0) {
      viewController.tabBarItem.imageInsets = UIEdgeInsetsMake(5,-5,0);
  }
複製程式碼

TabBarItem選中顏色異常

 在 iOS 13下設定 tabbarItem 字型選中狀態的顏色,在push到其它 ViewController 再返回時,選中狀態的 tabbarItem 顏色會變成預設的藍色。

設定 tabbar 的 tintColor 屬性為原本選中狀態的顏色即可。

self.tabBar.tintColor = [UIColor redColor];
複製程式碼

8.3. 新增 Diffable DataSource

 在 iOS 13下,對 UITableView 與 UICollectionView 新增了一套 Diffable DataSource API。為了更高效地更新資料來源重新整理列表,避免了原有粗暴的重新整理方法 - (void)reloadData,以及手動呼叫控制列表重新整理範圍的api,很容易出現計算不準確造成 NSInternalInconsistencyException 而引發App crash。 api 官方連結


9. StatusBar新增樣式

 StatusBar 新增一種樣式,預設的 default 由之前的黑色字型,變為根據系統模式自動選擇展示 lightContent 或者 darkContent

針對iOS 13 SDK適配,後續將會持續收集並更新