就在今天WWDC会议上,苹果正式推出了iOS14。对,那我们现在就来总结一下iOS13的新特性吧。

目录

新建工程

iOS13工程结构发生了两个变化,有两个代理

工程

在Xcode 11 创建的工程,运行设备选择 iOS 13.0 以下的设备,运行应用时会出现黑屏现象。
原因:
Xcode 11 默认是会创建通过 UIScene 管理多个 UIWindow 的应用,工程中除了 AppDelegate 外会多一个 SceneDelegate
AppDelegate和SceneDelegate这是iPadOS带来的新的多窗口支持的结果,并且有效地将应用程序委托的工作分成两部分。
也就是说在我们用多窗口开发iPadOS中,从iOS 13开始,您的应用代表应该:

设置应用程序期间所需的任何数据。
响应任何专注于应用的事件,例如与您共享的文件。
注册外部服务,例如推送通知。
配置您的初始场景。
相比之下,在iOS 13中的新顶级对象是一个UIWindowScene,场景代表可以处理应用程序用户界面的一个实例。因此,如果用户创建了两个显示您的应用程序的窗口,则您有两个场景,均由同一个应用程序委托支持。
这些场景旨在彼此独立工作。因此,您的应用程序不再移动到后台,而是单个场景执行 - 用户可以将一个移动到后台,同时保持另一个打开。

我们可以看下info.plist文件和工程项目文件的变化如图:
plist

这里的适配方案分为两种,不需要多窗口和兼容iOS13以下设备

方案一

如果我们不开发iPadOS多窗口APP,SceneDelegate窗口管理我们可以不需要直接删掉就好了。

删除掉info.plist中Application Scene Manifest选项,同时,文件SceneDelegate可删除可不删
相关代码注释掉
Appdelegate新增windows属性

方案二

即要用iOS 13中新的SceneDelegate,又可以在iOS 13一下的设备中完美运行。那就添加版本判断.

SceneDelegate中添加@available(iOS 13, *)
AppDelegate中同样声明window属性
AppDelegate中两个关于Scene的类也添加版本控制,oc/Swift中可以用扩展单独拎出来
切记:这种方式,AppDelegate中的有关程序的一下状态的方法,iOS 13设备是不会走的,iOS13一下的是会收到事件回调的。13以上的设备会走SceneDelegate对应的方法


kvc访问私有属性

这次iOS 13系统升级,影响范围最广的应属KVC访问修改私有属性了,直接禁止开发者获取或直接设置私有属性。而KVC的初衷是允许开发者通过Key名直接访问修改对象的属性值,为其中最典型的 UITextField 的 _placeholderLabel、UISearchBar 的 _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


模态弹窗 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

黑暗模式的适配

针对黑暗模式的推出,Apple官方推荐所有三方App尽快适配。目前并没有强制App进行黑暗模式适配。因此黑暗模式适配范围现在可采用以下三种策略:

全局关闭黑暗模式
指定页面关闭黑暗模式
全局适配黑暗模式

全局关闭黑暗模式

方案一:在项目 Info.plist 文件中,添加一条内容,Key为 User Interface Style,值类型设置为String并设置为 Light 即可。

方案二:代码强制关闭黑暗模式,将当前 window 设置为 Light 状态。

if(@available(iOS 13.0,*)){

self.window.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;

}

指定页面关闭黑暗模式

从Xcode 11、iOS 13开始,UIViewController与View新增属性 overrideUserInterfaceStyle,若设置View对象该属性为指定模式,则强制该对象以及子对象以指定模式展示,不会跟随系统模式改变。

设置 ViewController 该属性, 将会影响视图控制器的视图以及子视图控制器都采用该模式
设置 View 该属性, 将会影响视图及其所有子视图采用该模式
设置 Window 该属性, 将会影响窗口中的所有内容都采用该样式,包括根视图控制器和在该窗口中显示内容的所有控制器

全局适配黑暗模式

适配黑暗模式,主要从两方面入手:图片资源适配与颜色适配

图片资源适配

打开图片资源管理库 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官方适配链接):

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;

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 如下图,完成

launchImage

新增一直使用蓝牙的权限申请

在iOS13之前,无需权限提示窗即可直接使用蓝牙,但在iOS 13下,新增了使用蓝牙的权限申请。最近一段时间上传IPA包至App Store会收到以下提示。

blue

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

NSBluetoothAlwaysUsageDescription 这里输入使用蓝牙来做什么

Sign With Apple

 在iOS 13系统中,Apple要求提供第三方登录的App也要支持「Sign With Apple」,具体实践参考 iOS Sign With Apple实践

推送Device Token适配

在iOS 13之前,获取Device Token 是将系统返回的 NSData 类型数据通过 -(void)description; 方法直接转换成 NSString 字符串。 iOS 13之前获取结果:

low

iOS 13之后获取结果:

high

适配方案: 目的是要将系统返回 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;

}

UIKit 控件变化

主要还是参照了Apple官方的 UIKit 修改文档声明。https://developer.apple.com/documentation/ios-ipados-release-notes/ios-13-release-notes?preferredLanguage=occ

UITableView

iOS 13下设置 cell.contentView.backgroundColor 会直接影响 cell 本身 selected 与 highlighted 效果。 建议不要对 contentView.backgroundColor 修改,而对 cell 本身进行设置。

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];

}

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];

新增 Diffable DataSource

在 iOS 13下,对 UITableView 与 UICollectionView 新增了一套 Diffable DataSource API。为了更高效地更新数据源刷新列表,避免了原有粗暴的刷新方法 - (void)reloadData,以及手动调用控制列表刷新范围的api,很容易出现计算不准确造成 NSInternalInconsistencyException 而引发App crash。使用案例

StatusBar新增样式

StatusBar 新增一种样式,默认的 default 由之前的黑色字体,变为根据系统模式自动选择展示 lightContent 或者 darkContent


你远道而来这人世间,想必也是因为热爱吧 ||