iOS 编程零碎要点
本文总结了我在学习 Swift 开发 iOS App 过程中的零碎知识

UIKit
UIView 的
convert(_ rect: CGRect, to view: UIView?) -> CGRect可以将receiver中的rect转换到to view的坐标系上比如
priceView的坐标为(10, 10, 20, 20),priceView.inputField的坐标为(5, 5, 10, 10), 那么:swiftvar rect1 = vc.priceView.inputField.convert(vc.priceView.inputField.bounds, to: vc.view) // (15, 15, 10, 10) var rect2 = vc.priceView.inputField.convert(CGRect.zero, to: vc.view) // (15, 15, 0, 0)UIDatePicker继承自UIControl, 其通过addTarget()方法来添加用户交互事件. 其不是UIPickerView的子类, 不可通过协议的方式进行配置或用户交互.- 设置
layer的层次可以使用layer.zPosition = Int, 数字越大层次越高, 越不被遮挡 - 设置
view的层级可以使用bringSubview(toFront view: UIView)和sendSubview(toBack view: UIView)两个方法调整层次 - 代码中但凡以
UI开头的都属于UIKit的一部分 scrollView最重要的就是contentSize属性, 设置时将其内的view上下左右全部对齐到contentSize, 然后将view的宽度对齐到frameSize就可以保证视图只能上下移动.- 将图片上方覆盖一层视图, 调整视图为黑色, 透明度为 0.2, 可以上图片上的文字标签更加易读
- Xcode 中的视图顺序为最下方在最上层.
- 每个单元格都最好设置独立的相对应的 view 文件
- 动态形态即 iOS 系统中的字体放大功能, 苹果推荐开发者开发的
APP都支持动态形态, 即字体的Text Style CALayer与UIView区别- 每个
UIView内部都有一个CALayer在背后提供内容的绘制和显示, 并且UIView的尺寸样式都由内部的Layer所提供. 两者都有树状层级结构,CALayer内部有SubLayers,UIView内部有SubViews. 但是CALayer比UIView多了个 AnchorPoint UIView显示的时候,UIView做为 Layer 的 CALayerDelegate, View 的显示内容由内部的 CALayer 的 displayCALayer是默认修改属性支持隐式动画的, 在给UIView的CALayer做动画的时候,UIView作为CALayer的代理, Layer 通过actionForLayer: forKey:向 View 请求相应的 action(动画行为)- layer 内部维护着三分
layer tree, 分别是presentLayer Tree(动画树),modelLayer Tree(模型树),Render Tree(渲染树), 在做 iOS 动画的时候, 我们修改动画的属性, 在动画的其实是CALayer的presentLayer的属性值, 而最终展示在界面上的其实是提供UIView的modelLayer - 两者最明显的区别是
UIView可以接受并处理事件, 而CALayer不可以 - 总结:
UIView负责了与人的动作交互以及对CALayer的管理,CALayer则负责了所有能让人看到的东西.
- 每个
- 如果要展示或弹出某个操作的话一定要用到
present命令 Storyboard中项目变多后会变得难以管理, 这时可以将项目中与其他视图没有关联的视图独立出来. 一是为了方便管理, 二也可以分配给专人进行设计, 互不打扰. 命令为Editor -> Refactor to Storyboard. 注: 重构之后标签的名称不可再改变! 可能是 Xcode 的 bug- 如果要在
APP中嵌入一个有良好体验的网页, 可以使用SFSafariViewController, 如果想简单方便可以使用WKWebView. UIPickerView与UITableView类似, 需要与普通控制器建立DataSource和delegateUIScrollView与UIImageView会截获touch, 传递不到父视图view上, 需要extension里面对touchesBegan方法进行重载后将其能传递到父视图view中.- 代码设置字体objc
label.font = [UIFont fontWithName:@"Helvetica-Bold" size:20];// 加粗 label.font = [UIFont fontWithName:@"Helvetica-Oblique" size:20];// 加斜 label.font = [UIFont fontWithName:@"Helvetica-BoldOblique" size:20];// 又粗又斜 UIViewController可以addChild,UIView也可以addSubview, 如果一个vc的view只添加了子vc的view, 并没有vc.addChild(subVC), 那么这个subVC的viewDidAppear等不会被触发 (viewDidLoad会被触发)设置一个
view位于屏幕的三分之一处, 可以设置屏幕的bottom与 view 的top或bottom对齐, 然后设置 multiplier. 以下图为例:
在图中的例子中, 可以理解为将 view 的
bottom与屏幕的bottom合并为一条线附着于 view 上, 然后依据 multiplier 的值设置此线在屏幕中的位置- Xcode 中的默认可以使用的字体与 macOS 的安装字体不同, Xcode 中的所有可显示的字体都在 iPhone 中默认存在
- 除了系统的字体有
systemFontWeight方法, 其他的所有自定义字体的粗细都只能通过字体的文件来区分, 比如细字体是一个字体文件, 粗字体是一个字体文件 - 导入自定义字体并使用
LaunchScreen修改图片无响应- 判断
Info.plist中launchscreen的名称是否设置正确 - 修改
LaunchScreen的图片后, 需要删除 app, 再重新安装
- 判断
UIButton的titleLabel设置: 必须使用setTitle("", for: .touchUpInside)指定状态, 然后才可以使用 titleLabel. https://www.jianshu.com/p/53dcf361236bUITableView的sectionHeaderView的背景颜色不可通过backgroundColor直接进行设置, 可以通过contentView.backgroundColor进行设置, 但是由于contentView是与 safeArea 对齐的, 超出 safeArea 区域的其他地方的背景颜色仍然是默认的白色, 因此最直接的办法是backgroundView = LineView(UIColor.red)UITableView的 cell 中有: 自身 &backgroundView&selectedBackgroundView&contentView, 他们层次的上下关系是:自身 -> backgroundView -> selectedBackgroundView -> contentView -> other view, 在布局时, 我们切记记得一定要将自定义添加的subview添加到contentView上, 因为系统对editStyle的cell进行处理时都是针对contentView进行contentView默认是根据safeArea对齐的, 因此如果添加的subview对齐了contentView, 那么就不用操心对齐到safeArea了- 在长按
UITableviewCell弹出菜单的方法中, 我们必须要设置canBecomeFirstResponder与canPerformAction, 然后在长按手势方法中声明recognizer.view?.becomeFirstResponder(), 切记不要在 cell 每次赋值时指定cell.becomeFirstResponder, 否则会发生AXError错误! - 在
UITableView的reloadRows方法执行时, 如果我们同时使用了tableView.reloadData(), 那么将会出现单元格缺失的现象, 这应该与单元格的复用有关. 相应的 iOS 13 的方法UITableViewDiffableDataSource在增量更新时如果进行tableView.reloadData()也会发生cell的断层现象 UITableView的复用机制通常, 我们会在
tableView.register(HomeTableViewCell.self, forCellReuseIdentifier: "tableCell")进行注册 cell, 之后在cellForRowAt方法中使用let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as! HomeTableViewCell来进行复用后.想象一下, 如果不进行复用, 而是直接在
cellForRowAt中返回一个我们自定义的 cell, 可不可以呢?答案是可以的, 但是这样的话每次向下滑动, 显示一个新的 cell 时, 系统就会对那一个新的 cell 进行初始化, 对旧的进行释放, 这样是极其浪费系统计算资源的. 而使用了复用机制后, 系统就可以直接调用指定标识符 (指定类型) 的 cell, 不用涉及到初始化, 大大减轻了系统负担.
复用的简单流程如下:
假设一个屏幕 (显示范围) 只能显示 tableView 的 5 个 cell
- 在添加一个 tableView 到主 view 的时候会随之创建一个复用池, 用于存放通过标识符和指定类型创建的
注册 cell - 系统在显示 tableView 的第一个 cell 时调用其代理方法
cellForRowAt, 因为代理方法中使用了dequeueReusableCell(withIdentifier: "tableCell", for: indexPath), 那么会检测当前复用池有无 cell, 没有则新建 (初始化) 一个返回, 否则从中取出并返回 - 系统在显示第 2 个 cell 的时候, 再次调用代理方法
cellForRowAt, 此再次创建 (即初始化) 一个 cell, 返回此 cell 到屏幕 重复第 3 步, 直至显示到第 7 个 cell, 此时屏幕 (显示范围) 已经被填满, 显示新的 cell 的同时对旧 cell 进行回收到复用池并从复用池拉取之前被回收的 cell 进行复用.

tableView,tableView的headerView,collectionView,collectionView的headerView都是这个套路- 在添加一个 tableView 到主 view 的时候会随之创建一个复用池, 用于存放通过标识符和指定类型创建的
UIView 的如下属性是可以有动画效果的, 其他的则不行, 比如
isHidden属性frameboundscentertransformalphabackgroundColorcontentStretch
组透明
UIView有一个叫做alpha的属性来确定视图的透明度.CALayer有一个等同的属性叫做opacity, 这两个属性都是影响子层级的. 也就是说, 如果给一个图层设置了 opacity 属性, 那它的子图层都会受此影响.iOS 常见的做法是把一个控件的
alpha值设置为 0.5(50%) 以使其看上去呈现为不可用状态. 对于独立的视图来说还不错, 但是当一个控件有子视图的时候就有点奇怪了, 下图展示了一个内嵌了UILabel的自定义UIButton; 左边是一个不透明的按钮, 右边是 50% 透明度的相同按钮. 可以注意到, 里面的标签的轮廓跟按钮的背景很不搭调.
这是由透明度的混合叠加造成的, 当显示一个 50% 透明度的图层时, 图层的每个像素都会一半显示自己的颜色, 另一半显示图层下面的颜色. 这是正常的透明度的表现. 但是如果图层包含一个同样显示 50% 透明的子图层时, 所看到的视图, 50% 来自子视图, 25% 来了图层本身的颜色, 另外的 25% 则来自背景色.
可以设置 CALayer 的一个叫做
shouldRasterize属性来实现组透明的效果, 如果它被设置为 YES, 在应用透明度之前, 图层及其子图层都会被整合成一个整体的图片, 这样就没有透明度混合的问题了swiftbutton2.layer.shouldRasterize = trueUITableView代理方法呼叫的顺序:- 首先执行
numberOfRowsInSection:方法, 返回 cell 个数为 10. - 其次执行的就是
heightForRowAtIndexPath:方法, 如上图, 此时执行该方法会将所有 cell 的高度全部返回. - 这时候就开始执行
cellForRowAtIndexPath:方法, 因为当前页面只能布局 3 条 cell, 所以该方法会被执行三次. 并且, 执行一次cellForRowAtIndexPath:方法紧接着就会执行一次heightForRowAtIndexPath:方法返回 cell 高度.
因此, 当我们从网络或者本地缓存中获取到所需数据 (array) 后, 可以直接执行代码:
self.tableView reloadData, 然后就会调用cellForRowAtIndexPath:方法和heightForRowAtIndexPath:方法.- 首先执行
UIViewanimate 的动画选项layoutSubviewsallowUserInteraction: 允许在动画执行过程中对用户交互进行反馈beginFromCurrentState: 从当前状态继续执行另一动画repeat: 让动画一直重复执行autoreverse: 配合. repeat 使用, 使动画反转并持续执行overrideInheritedDuration: 强制使用提交动画时使用的动画时长overrideInheritedCurve: 强制使用提交动画时使用的曲线overrideInheritedOptions: 强制使用提交动画时使用的选项allowAnimatedContent: 允许在动画过程中直接动态改变 view 的属性 (默认不设置此值时动画过程中使用的是此 view 的快照)showHideTransitionViews: 允许在动画过程中隐藏或显示正在进行动画的 viewcurveEaseInOut: 相当于[.curveEaseIn,.curveEaseOut]的组合, 在开始加速和在结束动画时减速curveEaseIn: 在动画开始时加速curveEaseOut: 在动画结束时减速curveLinear: 让动画保持匀速transitionFlipFromLeft: 从左边翻转transitionFlipFromRight: 从右边翻转transitionCurlUp: 卷上去transitionCurlDown: 卷下去transitionCrossDissolve: 交叉溶解transitionFlipFromTop: 从顶部翻转transitionFlipFromBottom: 从底部翻转preferredFramesPerSecond60: 每秒 60 帧preferredFramesPerSecond30: 每秒 30 帧
swiftUIView.animate(withDuration: 1, // 动画总时长 delay: 2, // 执行动画前的延时 options: [.curveEaseIn,.curveEaseOut], // 动画的属性, 可以使多个属性配合, 也可以是单个属性, 如果是 [] 的话则使用默认 animations: {print(1) }, // 闭包, 动画作用的目标 completion: nil) // 动画执行结束后的回调闭包 UIView.animate(withDuration: 1, delay: 1, usingSpringWithDamping: 0.5, // 设置弹性动画的阻尼 (范围: 0.0~1.0), 越接近 0.0 弹性越大, 反之则越小. initialSpringVelocity: 0.5, // 控制动画初始速度. options: [.curveEaseIn], animations: {print(2) }, completion: nil)
backgroundColor, alpha, isHidden, opaque 区别
hidden: 此属性为BOOL值, 用来表示UIView是否隐藏. 关于隐藏大家都知道就是让UIView不显示而已, 但是需要注意的是:- 当前
UIView的所有subview也会被隐藏, 忽略subview的hidden属性.UIView中的subview就相当于UIView的死忠小弟, 老大干什么我们就跟着老大, 同进同退, 生死与共! - 当前
UIView也会从响应链中移除. 你想你都不显示了, 就不用在响应链中接受事件了.
- 当前
alpha: 此属性为浮点类型的值, 取值范围从 0.0 到 1.0, 表示从完全透明到完全不透明, 其特性有:- 当前
UIView的alpha值会被其所有subview继承. 因此,alpha值会影响到UIView跟其所有subview. alpha具有动画效果. 当alpha为 0 时, 跟hidden为YES时效果一样, 但是alpha主要用于实现隐藏的动画效果, 在动画块中将hidden设置为YES没有动画效果.
- 当前
backgroundColor的alpha(Clear Color): 此属性为UIColor值, 而UIColor可以设置alpha的值, 其特性有:- 设置
backgroundColor的alpha值只影响当前UIView的背景, 并不会影响其所有subview. 这点是同alpha的区别,Clear Color就是backgroundColor的alpha为 0.0. alpha值会影响backgroundColor最终的alpha. 假设UIView的alpha为 0.5,backgroundColor的alpha为 0.5, 那么backgroundColor最终的alpha为 0.25(0.5 乘以 0.5).
- 设置
opaque: 此属性为 BOOL 值. 要搞清楚这个属性的作用, 就要先了解绘图系统的一些原理: 屏幕上的每个像素点都是通过 RGBA 值 (Red, Green, Blue 三原色再配上 Alpha 透明度) 表示的, 当纹理 (UIView在绘图系统中对应的表示项) 出现重叠时, GPU 会按照下面的公式计算重叠部分的像素 (这就是所谓的 合成):bashResult = Source + Destination * (1 - SourceAlpha)Result是结果RGB值,Source为处在重叠顶部纹理的 RGB 值,Destination为处在重叠底部纹理的 RGB 值. 通过公式发现: 当SourceAlpha为 1 时, 绘图系统认为下面的纹理全部被遮盖住了, Result 等于 Source, 直接省去了计算! 尤其在重叠的层数比较多的时候, 完全不同考虑底下有多少层, 直接用当前层的数据显示即可, 这样大大节省了 GPU 的工作量, 提高了效率. (多像现在一些 “美化墙”, 不管后面的环境多破烂, “美化墙” 直接遮盖住了, 什么都看不到, 不用整治改进, 省心省力). 更详细的可以读下 objc.io 中 < 绘制像素到屏幕上 > 这篇文章.那什么时候
SourceAlpha为 1 呢? 这时候就是opaque上场的时候啦! 当opaque为 YES 时,SourceAlpha为 1.opaque就是绘图系统向UIView开放的一个性能开关, 开发者根据当前UIView的情况 (这些是绘图系统不知道的, 所以绘图系统也无法优化), 将opaque设置为YES, 绘图系统会根据此值进行优化. 所以, 如果在开发时某 UIView 是不透明的, 就将opaque设置为YES, 能优化显示效率.需要注意的是:
- 当
UIView的opaque为YES时, 其alpha必须为1.0, 这样才符合opaque为 YES 的场景. 如果alpha不为 1.0, 最终的结果将是不可预料的 (unpredictable). - opaque 只对 UIView 及其 subclass 生效, 对系统提供的类 (像 UIButton, UILabel) 是没有效果的.
- 当
UIKit Scale 影响
| iPhone 8 | iPhone X | |
|---|---|---|
| UIKit Size | 375*667 | 375*812 |
| 分辨率 | 750*1334 | 1125*2436 |
| UIKit Scale factor | 2 | 3 |
| 1 像素消耗点 | 0.5 | 0.33 |
iOS 系统的针对像素的绘制采用四舍五入的方式, 比如说 22.1999, 在 iPhone 8 上 22.1999 * 2 = 44.3998, 因为 0.3998 < 0.5, 故丢弃, 故实际显示为 44 像素. 在 iPhone X 上 22.1999 * 3 = 66.60575756, 因为 0.60575756 > 0.5, 补充为 1 个像素, 故实际显示为 67 像素.
UIWindow 操作
- 通过
.isHidden来控制隐藏及显示 window?.makeKeyAndVisible()的作用是显示一个 UIWindow, 同时设置为 keyWindow, 并将其显示在同一 windowLevel 的其它任何 UIWindow 之上, 等效于:swiftwindow?.makeKey() window?.isHidden = false- window 的显示问题
- 对于
hidden的setter方法, 最终显示的以最后执行过.isHidden=false的 UIWindow 为准, 且执行.isHidden=false之前isHidden的值为true. (isHidden如果是从false改为false的不算最后改变 UIWindow 的显示状态) - 对于
makeKeyAndVisible方法, 最终显示的以最后执行过makeKeyAndVisible的 UIWindow 为准. - 对于先后分别用
makeKeyAndVisible方法和isHidden的setter方法, 还是先后分别用isHidden的setter方法和makeKeyAndVisible方法, 结局同样以最后改变显示状态的 UIWindow 为准.
- 对于
- window level 问题
- windowLevel 数值越大的显示在窗口栈的越上面
- 显示层的优先级 为:
alert>statusBar>normal - 系统给 UIWindow 默认的
windowLevel为normal
控制器
- 给定的导航流程只有一个导航控制器; 一个导航控制器可以管理多个视图控制器; 导航体系的每个视图控制器都有到导航控制器的引用.
UIPageViewController与UINavigationController都属于容器控制器.- 如果控制器被包裹在
navigationController中, 则必须在navigationController中设置状态列才有效果 为什么必须要添加
required init?(coder decoder: NSCoder)- 名称: 必要初始化器
- 上下文: 当继承了遵守
NSCoding protocol的类 (如UIView,UIViewController等) 时 - 显性添加的条件: 当在子类定义了指定初始化器或
override了父类的初始化器后, 那么必须显性实现 (其他情况下会隐性实现, 不需要我们管) fatalError含义: 默认的在必要初始化器中系统会给我们添加fatalError命令, 其含义是无条件停止执行并打印
如果是代码实现界面, 当重写或自定义了初始化器时, 系统会自动提示我们添加此必要初始化器, 按照系统的要求进行
fix即可
target 与 project 与 xcworkspace 的关系
- 一个
xcworkspace可以包含多个project - 一个
target只能对应一个product - 一个
xcworkspace编译时可以选择多个项目中的不同target, 如图中可以选择 3 个target - 一个文件可以映射到同一个
xcworkspace中的多个target中, 可以用在开发vip版本与普通版本这一需求上 Swift Package Manager是对应于一整个xcworkspace的, 即, 在同一个xcworkspace中的任意一个project中引入了第三方库, 那么在任意一个project中都可以使用

MapKit
- 前向地址编码 (
Forward Geocoding): 将文字地址转换为全球地理坐标 - 反向地理编码 (
Reverse Geocoding): 将经纬度值转回地址 iOS 10之后规定如果要使用照片库或者相机则必须要将原因列在info.plist文件中. 这样可以在APP要使用照片或相机时给用户提醒.- 如果要与图片选择器进行互动, 必须遵守
UIImagePickerControllerDelegate和UINavigationControllerDelegate
AutoLayout
constrain to margin是xcode中AutoLayout的防触摸边缘功能, 选中后自动缩进20autolayout即设置物件的长宽以及横纵坐标- 长宽: 通过物件长或宽与屏幕 view 的比值确定物件长宽
- 纵横坐标: 通过居中及物件之间相对距离确认
Content Hugging Priority(视图抗拉伸优先级): 值越小, 越先被拉伸Content Compression Resistance(抗压缩优先级): 值越小, 越先被压缩autolayout中设置百分比宽度 / 高度: 先设定等宽, 然后再属性设置面板中multuplier调整为百分比- 纯代码写视图布局时需要注意, 要手动调用
loadView方法, 而且不要调用父类的loadView方法. 纯代码和用 IB 的区别仅存在于loadView方法及其之前, 编程时需要注意的也就是loadView方法.
影响编译时间的因素
在 Build Settings -> Swift Compiler -> Custom Flags -> Other Swift Flags 中添加如下代码可查看耗时编译代码
/// <limit> 为 warning 的编译时间阈值
-Xfrontend -warn-long-function-bodies=<limit>
-Xfrontend -warn-long-expression-type-checking=<limit>
影响因素如下:
- 硬件
- 配置
- 代码书写方式
- 使用
+拼接可选字符串会极其耗时swift/* 优化前 372ms */ let finalResult = (dbWordModel?.vocabularyModel?.justSentenceExplain?? "") +"<br/>"+ (dbWordModel?.vocabularyModel?.justSentence ?? "") /* 优化后 20ms */ // let finalResult = "\(dbWordModel?.vocabularyModel?.justSentenceExplain ?? "")<br/>\(dbWordModel?.vocabularyModel?.justSentence ?? "")" - 可选值使用
??赋默认值再嵌套其他运算会极其耗时.swift/* 优化前 372 ms */ let finalResult = (dbWordModel?.vocabularyModel?.justSentenceExplain?? "") + "<br/>" + (dbWordModel?.vocabularyModel?.justSentence ?? "") /* 优化后 63 ms */ guard let dbSentenceExp = dbWordModel?.vocabularyModel?.justSentenceExplain, let dbSentence = dbWordModel?.vocabularyModel?.justSentence else {return} // let finalResult = "\(dbSentenceExp)<br/>\(dbSentence)" - 将长计算式代码拆分 最后组合计算swift
/* 优化前 736 ms */ let totalTime = (timeArray.first?.float()?.int?? 0) * 60 + (timeArray.last?.float()?.int?? 0) /* 优化后 22 ms */ let firstPart: Int = (timeArray.first?.float()?.int?? 0) let lastPart: Int = (timeArray.last?.float()?.int?? 0) let totalTime = firstPart * 60 + lastPart - 与或非和
>=,<=,==逻辑运算嵌套 Optional 会比较耗时swift/* 优化前 10420 ms */ let finalResult = (dbWordModel?.vocabularyModel?.justSentenceExplain?? "") +"<br/>"+ (dbWordModel?.vocabularyModel?.justSentence??"") /* 优化后 21 ms */ let leftValue: CGFloat = homeMainVC?.scrollview.contentOffset.y?? 0 let rightValue: CGFloat = (homeMainVC?.headHeight?? 0.0) - (homeMainVC?.ignoreTopSpeace?? 0.0) if leftValue == rightValue {...} - 手动增加类型推断会降低编译时间.swift
/* 优化前 21 ms */ let leftValue = homeMainVC?.scrollview.contentOffset.y?? 0 let rightValue = (homeMainVC?.headHeight?? 0.0) - (homeMainVC?.ignoreTopSpeace?? 0.0) /* 优化后 16 ms */ let leftValue: CGFloat = homeMainVC?.scrollview.contentOffset.y?? 0 let rightValue: CGFloat = (homeMainVC?.headHeight?? 0.0) - (homeMainVC?.ignoreTopSpeace?? 0.0)
- 使用
UDID & UUID
UDID: Unique Device Identifier, 对于已越狱了的设备, UDID 并不是唯一的. 使用 Cydia 插件 UDIDFaker, 可以为每一个应用分配不同的 UDID. 所以 UDID 作为标识唯一设备的用途已经不大了.

获取方法: 目前没有代码方式可以获取, 只能通过外部工具, 如 Xcode 或
idevice_id -hUUID: Universally Unique Identifier: 是基于 iOS 设备上面某个单个的应用程序生成的一个唯一标示, 只要用户没有完全删除应用程序, 则这个 UUID 在用户使用该应用程序的时候一直保持不变. 如果用户删除了这个应用程序, 然后再重新安装, 那么这个 UUID 已经发生了改变. UUID 会在用户删除了程序后再重装的时候发生改变, 解决的方案就是使用
UUID+KeyChain记录设备唯一标识获取方法: 代码方式
UIDevice.current.identifierForVendor
UUID().uuidString获得的并不是设备的 UUID, 而是一个随机数, 这个随机数每次产生都不一样, 而且能保证唯一
drawrect & layoutsubviews 调用时机
layoutSubviews:在以下情况下会被调用:- init 初始化不会触发
layoutSubviews. - addSubview 会触发
layoutSubviews. - 设置 view 的 Frame 会触发
layoutSubviews(frame 发生变化触发). - 滚动一个 UIScrollView 会触发
layoutSubviews. - 旋转 Screen 会触发父 UIView 上的
layoutSubviews事件. - 改变一个 UIView 大小的时候会触发其
superView上的layoutSubviews事件. - 直接调用
layoutIfNeeded
- init 初始化不会触发
drawrect:This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself.
To invalidate part of your view, and thus cause that portion to be redrawn, call the
setNeedsDisplay()orsetNeedsDisplay(_:)method instead.sizeThatFits:sizeToFit内部会调用sizeThatFit, 我们不应该重写子类的sizeToFit方法, 而是重写sizeThatFits[UITableView _heightForCell:atIndexPath:]会调用sizeThatFits, 因此 cell 自动高度的关键是sizeThatFits方法
常见问题
building for iOS simulator, but linking in object file built for iOS, for architecture arm64
虽然库中包含了 arm64, 但是表明了它是用在实际设备而非模拟器上的, 临时解决办法可以设置 EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
这是一个治标不治本的”快速疗法”, 加入这个设定可以让你编译通过并运行, 但是你需要清楚了解到这么做的弊端: 因为 arm64 被排除了, 所以在 iOS 模拟器上, 只有 x86_64 这一个架构选择. 这意味着你的整个 app 都会以 x86_64 进行编译, 然后跑在 x86_64 的模拟器上. 而在 Apple Silicon 的 mac 上, 这个模拟器其实是使用 Rosetta 2 跑起来的, 这意味着性能的大幅下降.
setValue:forKey: 原理
当一个对象调用 setValue:forKey: 方法时, 方法内部会做以下操作:
- 判断有没有指定 key 的 set 方法, 如果有
set方法, 就会调用set方法, 给该属性赋值 - 如果没有
set方法, 判断有没有跟key值相同且带有下划线的成员属性 (_key). 如果有, 直接给该成员属性进行赋值 - 如果没有成员属性
_key, 判断有没有跟key相同名称的属性. 如果有, 直接给该属性进行赋值 - 如果都没有, 就会调用
valueforUndefinedKey和setValue:forUndefinedKey:方法
感悟
- 如果一些程序中使用的静态库不支持
armv7s, 而你的工程支持armv7s时, 就会出现 “xxxx does not contain a(n) armv7s slice:xxxxx for architecture armv7s” 的编译错误, 想要解决这个问题, 有两个方法:- 如果是开源的, 能够找到源代码, 则可以用源代码重新打一个支持
armv7s的 libaray, 或者在工程中直接使用源代码, 而不是静态库. - 如果不是开源的, 要么就坐等第三方库的支持, 要么就暂时让你的工程不支持
armv7s
- 如果是开源的, 能够找到源代码, 则可以用源代码重新打一个支持
ios Deep Link: Deep Link 包含 URL Scheme 与 Universal Link 两种技术

delay deep link 是指当设备没有安装 app 时会指引到应用市场安装 app, 安装完打开 app 后仍然会定位到目标页
Universal link 本地调试
通常情况下用户会在下载完 APP 后由系统请求所关联的域名, 下载相应的 json 文件, 这个请求并不是每次下载都会触发, 也并不是每次请求都会请求最新文件 (而是一个 CDN), 因此我们在开发时需要:
- 设置为开发模式, 在 associated domain 后添加
?mode=developer - 使用 Charles 的
Map to Local功能, 直接使用本地 json 文件返回 (目的是每次请求的都是最新文件)
为了验证一个指定 url 在 app 内打开的页面效果, 我们可以将 url 拷贝至备忘录中, 然后在备忘录中点击 url
- 设置为开发模式, 在 associated domain 后添加
- 显式动画与隐式动画: 隐式动画一直存在, 如需关闭需设置; 显式动画是不存在, 如需显式 要开启 (创建). UIView 动画, 又称隐式动画, 动画后 frame 的数值发生了变化. 另一种是 CALayer 动画, 又称显示动画
持久化存储时如果要频繁写入或读取最好使用 CoreData 或其他数据库而不是使用文件以减少 I/O 次数
OS Cache: 性能最好的一层, 使用 logical I/O, 由于是储存在内存中, 所以 I/O 操作很高效 (使用 logical I/O)Disk Cache: 磁盘储存的物理映射. (使用 physical I/O)Permanent Storage: 最终用于持久化数据的介质, 对于 iOS 来说, 就是闪存 (使用 physical I/O)
缓存有以上几个层级, 对于 app 来说, 离 cpu 越近的 cache, 性能就越好, 但同时我们也希望 cache 能确实地落在磁盘中. 数据在内存当中时对于 app 而言速度是最快的, 也没有任何的 IO 开销, 但是当我们需要将数据从内存一层一层地注入到闪存时, 就需要注意 IO 开销了.

面是单单的更新
plist操作, 调用了系统的writeToFile函数, 最后再调用栈上系统为我们调用了fsync, 所以数据就会直接由OS cache层一直写入到Disk cache层, 并从OS cache层被清除, 如果在写入后我们仍然要继续使用数据, 就会失去了 OS cache 这一层的缓存, 而需要重新开启 IO 去磁盘中读取数据因此使用
plist这类文件来储存需要频繁读写的数据, 是非常不合适的UITableView的几个交互属性isDragging: 是否正在被手指拖动 (必须手指与屏幕接触, 需要滑动一小段距离才能使此值设为 true)isZooming: 当前 tableView 是否正在缩放 (放大或缩小)isFocused: 是否是当前 UIScreen 的 focusedViewisTracking: 是否被手指按住以开始一个滑动事件 (只要手指放上哪怕没有滚动也会为 true 值)isDecelerating: 是否在惯性滑动, 即手指已经离开屏幕但是 scrollView 仍然在滚动的情况. 因此本属性与isDragging不可能同时为 trueisDecendant(of: UIView): 是否是某view的subviewisZoomBouncing: 是否正在缩放的惯性动画中isExclusiveTouch: 当设置了isExclusiveTouch = true的控件 (View) 是事件的第一响应者, 那么到你的所有手指离开屏幕前, 其他的控件 (View) 是不会响应任何触摸事件的. 如果设置类别较多, 可直接设置全局UIView.appearance().isExclusiveTouch = trueisFirstResponder: 是否是第一响应者
UITableView的行高默认情况下如果我们实现了
cellForRowAtIndexPath方法, 那么如果有 500 行, 在reloadData的时候就会调用高度方法heightForRowAt500 次.最好的优化办法是如果 cell 高度都统一, 那么就直接使用
tableView.rowHeight =...来确定高度, 这样不会调用高度方法那么多次.如果我们的
tableView含有不同的 cell 高度, 那么可以使用自动行高来将高度计算推迟到滚动时发生swift// ios 10 及以下 tableView.estimatedRowHeight = 100.0 tableView.rowHeight = UITableView.automaticDimension // ios 11 及以上 tableView.rowHeight = UITableView.automaticDimension然后 cell 就会依据内部的所有空间自动计算行高
或者直接通过代理方法实现:
swiftfunc tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { }设置
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { }其实是没有意义的自动行高与非自动行高有如下区别:
- 在禁用
cell预估高度的情况下, 系统会先把所有cell实际高度先计算出来, 也就是先执行tableView:heightForRowAtIndexPath:代理方法, 接着用获取的cell实际高度总和来参与计算contentSize, 然后才显示cell的内容. 在这个过程中, 如果实际高度计算比较复杂的话, 可能会消耗更多的性能. - 在使用
cell预估高度的情况下, 系统会先执行所有cell的预估高度, 也就是先执行tableView:estimatedHeightForRowAtIndexPath:代理方法, 接着用所有cell预估高度总和来参与计算contentSize, 然后才显示cell的内容. 这时候从下往上滚动tableView, 当有新的cell出现的时候, 如果cell预估值高度减去实际高度 (实际高度根据cell中所持有控件约束计算得出) 的差值不等于 0,contentSize的高度会以这个差值来动态变化, 如果差值等于0,contentSize的高度不再变化. 在这个过程中, 由之前的所有cell实际高度一次性先计算变成了现在预估高度一次性先计算, 然后实际高度分步计算. 正如苹果官方文档所说, 减少了实际高度计算时的性能消耗, 但是这种实际高度和预估高度差值的动态变化在滑动过快时可能会产生跳跃现象, 所以此时的预估高度和真实高度越接近越好 (为了解决这种问题, 可以使用字典缓存所有的预估高度然后在代理方法中返回当前cell的高度).
- 在禁用
UIScrollView如果被设置contentOffset或者setContentOffset()的话, 会触发其scrillViewDidScroll代理方法UITableViewCell的contentView的bgColor位于self的bgColor之上UIScrollView如果没有被正确释放, 并且其代理方法中会发送 Notification 的话, 那么可能会触发各种灵异事件.- 时间戳: 格林威治时间 1970-01-01 00:00:00 起至现在的总秒数, 所以所有时区的时间戳都是一样的, 但是同样的时间戳在不同的时区会显示不同的日期时间, 因为中国是
UTC+8时区, 因此在北京时间 1970-01-01 08:00:00 的时候, 时间戳为零, 一个普通的时间戳如果放到中国时区来计算的话就会以8:00为基准, 计算差值NSDate: 网络时间, 属于Foundation(单位秒, 保留到微秒)CFAbsoluteTimeGetCurrent(): 网络时间, 属于CoreFoundation(单位秒, 保留到微秒, 默认为the reference date is 00:00:00 1 January 2001)相当于NSDate().timeIntervalSinceReferenceDatemach_absolute_time(): 内建时钟 (单位秒, 保留到纳秒), 不会因为外部时间变化而变化 (例如时区变化, 夏时制, 秒突变等), 系统重启后会被重置.CACurrentMediaTime(): 内建时钟, 属于QuartzCore(单位秒, 保留到纳秒), 不会因为外部时间变化而变化 (例如时区变化, 夏时制, 秒突变等), 系统重启后CACurrentMediaTime()会被重置.
@IBOutlet与@IBAction区别IBOutlet只是将标签等不需要与使用者互动的原件进行连接, 通过代码控制界面IBAction连接的是按钮一样的与使用者互动的原件, 通过界面控制代码
- 为什么
IBOutlet后面有!, 类别中的属性被定义后一定要被初始化 (有值), 但如果是Optional类型则可以不初始化, 使用!表示不用进行解包一定会有值 addTarget方法和@IBAction链接的意义是相同的, 都是通过用户交互事件来执行相关方法.- 事件区别:
touch up inside触发: 手指按下, 在按钮区域抬起touch up outside触发: 手指按下, 在按钮区域外抬起touch down触发: 手指按下
- 在方法中
indexPath可翻译为某路径,indexPath.row则翻译为某路径所在的行数 row属于数据,cell属于视图, 表视图控制器通过数据源和代理方法将两者关联在一起.shortcutItem快速启动菜单思路分析:- 在主页面的
viewDidload方法中创建相关的菜单动作, 然后注入到系统管理的 APP 中UIApplication.shared.shortcutItems = [shortcutItem1, shortcutItem2, shortcutItem3] - 创建一个
shortcutItem实例, 从两个地方抓取动作信息赋值给他.- 从应用启动的方法中抓取场景创建信息的方法中的快捷动作
application(_:configurationForConnecting:options:) - 从后台挂起状态快捷动作激活的方法中抓取动作信息
windowScene(_:performActionFor:completionHandler:)
- 从应用启动的方法中抓取场景创建信息的方法中的快捷动作
- 然后在
becomeActive方法中对shortcutItem存的信息进行分析, 进而执行相关方法.
- 在主页面的
参考
- UITableviewCell 复用机制
- 应用测试与分发渠道简析
- You don”t always need weak self
- [iOS Deferred Deep Link 延遲深度連結實作 (Swift)](https://medium.com/zrealm-ios-dev/ios-deferred-deep-link- 延遲深度連結實作 -swift-b08ef940c196)
本博客文章采用 CC 4.0 协议,转载需注明出处和作者。
