iOS 13原生端適配攻略

隨着iOS 13的發佈,公司的項目也勢必要着手適配了。現彙總一下iOS 13的各類坑html

目錄

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閃退 錯誤代碼:ios

// 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_placeholderLabelapi

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

 所以,能夠爲UITextFeild建立Category,專門用於處理修改placeHolder屬性提供方法xcode

#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 的經過富文本方法修改屬性。bash

#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 文章app


2. 模態彈窗 ViewController 默認樣式改變

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

#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 十一、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, nonatomic, readonly) UIColor *systemIndigoColor       API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemGray2Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray3Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray4Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray5Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray6Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *labelColor              API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *secondaryLabelColor     API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *tertiaryLabelColor      API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *quaternaryLabelColor    API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *linkColor               API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *placeholderTextColor    API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *separatorColor          API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *opaqueSeparatorColor    API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemBackgroundColor                   API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemBackgroundColor          API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemBackgroundColor           API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGroupedBackgroundColor            API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemGroupedBackgroundColor   API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemGroupedBackgroundColor    API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemFillColor                         API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemFillColor                API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemFillColor                 API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, 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, 0, -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適配,後續將會持續收集並更新

相關文章
相關標籤/搜索