iOS 13原生端適配攻略

我只不過是出來寫寫代碼· 2019-10-16
本文來自 掘金 ,作者 我只不過是出來寫寫代碼

目錄

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,將黑暗模式下的素材拖入即可。關于黑暗模式圖片資源的加載,與正常加載圖片方法一致。

image.png

顏色適配

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.png

5. 新增一直使用藍牙的權限申請

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

image.png

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

NSBluetoothAlwaysUsageDescription 這里輸入使用藍牙來做什么`

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之前獲取結果:

image.png

iOS 13之后獲取結果:

image.png

適配方案: 目的是要將系統返回 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適配,后續將會持續收集并更新

广东26选5开奖结果查 28加拿大开奖结果 血流成河换三张怎么打 青海十一选五走势图表 加拿大快乐8基本走势图 手机网赚论坛 五分快三买大小的技巧 25选7一等奖多少钱 私募股权基金配资 捕鱼王截图 大连娱网棋牌大厅下载 广东十一选五任选八 湖北今日快三开奖结果 江苏7位数专家*号 搜 1分快3开奖结果1分快3豹子规律技巧 26选5今晚开奖结 意甲5月恢复训练新闻