iOS13適配/黑暗模式的適配/KVC訪問私有屬性/模態彈窗ViewController 默認樣式改變 /LaunchImage即將廢棄/藍牙的權限申請/推送Device Token適配/UIKit

目錄

1. KVC訪問私有屬性html

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

3. 黑暗模式的適配api

4. LaunchImage即將廢棄xcode

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

6. Sign With Appleide

7. 推送Device Token適配函數

8. UIKit 控件變化工具

9. StatusBar新增樣式字體


1. KVC訪問私有屬性

 此次iOS 13系統升級,影響範圍最廣的應屬KVC訪問修改私有屬性了,直接禁止開發者獲取或直接設置私有屬性。而KVC的初衷是容許開發者經過Key名直接訪問修改對象的屬性值,爲其中最典型的 UITextField 的 _placeholderLabelUISearchBar 的 _searchField。 形成影響:在iOS 13下App閃退 錯誤代碼:ui

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

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

 

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

相關文章
相關標籤/搜索