iOS13原生端适配攻略(推荐)
随着ios 13的发布,公司的项目也势必要着手适配了。现汇总一下ios 13的各种坑
1. 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 文章
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, 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 如下图,完成
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); }
8.3. 新增 diffable datasource
在 ios 13下,对 uitableview 与 uicollectionview 新增了一套 diffable datasource api。为了更高效地更新数据源刷新列表,避免了原有粗暴的刷新方法 - (void)reloaddata,以及手动调用控制列表刷新范围的api,很容易出现计算不准确造成 nsinternalinconsistencyexception 而引发app crash。
9. statusbar新增样式
statusbar 新增一种样式,默认的 default 由之前的黑色字体,变为根据系统模式自动选择展示 lightcontent 或者 darkcontent
针对ios 13 sdk适配,后续将会持续收集并更新
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。