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
和delegate
UIScrollView
与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
属性frame
bounds
center
transform
alpha
backgroundColor
contentStretch
组透明
UIView
有一个叫做alpha
的属性来确定视图的透明度.CALayer
有一个等同的属性叫做opacity
, 这两个属性都是影响子层级的. 也就是说, 如果给一个图层设置了 opacity 属性, 那它的子图层都会受此影响.iOS 常见的做法是把一个控件的
alpha
值设置为 0.5(50%) 以使其看上去呈现为不可用状态. 对于独立的视图来说还不错, 但是当一个控件有子视图的时候就有点奇怪了, 下图展示了一个内嵌了UILabel
的自定义UIButton
; 左边是一个不透明的按钮, 右边是 50% 透明度的相同按钮. 可以注意到, 里面的标签的轮廓跟按钮的背景很不搭调.这是由透明度的混合叠加造成的, 当显示一个 50% 透明度的图层时, 图层的每个像素都会一半显示自己的颜色, 另一半显示图层下面的颜色. 这是正常的透明度的表现. 但是如果图层包含一个同样显示 50% 透明的子图层时, 所看到的视图, 50% 来自子视图, 25% 来了图层本身的颜色, 另外的 25% 则来自背景色.
可以设置 CALayer 的一个叫做
shouldRasterize
属性来实现组透明的效果, 如果它被设置为 YES, 在应用透明度之前, 图层及其子图层都会被整合成一个整体的图片, 这样就没有透明度混合的问题了swiftbutton2.layer.shouldRasterize = true
UITableView
代理方法呼叫的顺序:- 首先执行
numberOfRowsInSection:
方法, 返回 cell 个数为 10. - 其次执行的就是
heightForRowAtIndexPath:
方法, 如上图, 此时执行该方法会将所有 cell 的高度全部返回. - 这时候就开始执行
cellForRowAtIndexPath:
方法, 因为当前页面只能布局 3 条 cell, 所以该方法会被执行三次. 并且, 执行一次cellForRowAtIndexPath:
方法紧接着就会执行一次heightForRowAtIndexPath:
方法返回 cell 高度.
因此, 当我们从网络或者本地缓存中获取到所需数据 (array) 后, 可以直接执行代码:
self.tableView reloadData
, 然后就会调用cellForRowAtIndexPath:
方法和heightForRowAtIndexPath:
方法.- 首先执行
UIView
animate 的动画选项layoutSubviews
allowUserInteraction
: 允许在动画执行过程中对用户交互进行反馈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
的防触摸边缘功能, 选中后自动缩进20
autolayout
即设置物件的长宽以及横纵坐标- 长宽: 通过物件长或宽与屏幕 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 -h
UUID: 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
的subview
isZoomBouncing
: 是否正在缩放的惯性动画中isExclusiveTouch
: 当设置了isExclusiveTouch = true
的控件 (View) 是事件的第一响应者, 那么到你的所有手指离开屏幕前, 其他的控件 (View) 是不会响应任何触摸事件的. 如果设置类别较多, 可直接设置全局UIView.appearance().isExclusiveTouch = true
isFirstResponder
: 是否是第一响应者
UITableView
的行高默认情况下如果我们实现了
cellForRowAtIndexPath
方法, 那么如果有 500 行, 在reloadData
的时候就会调用高度方法heightForRowAt
500 次.最好的优化办法是如果 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().timeIntervalSinceReferenceDate
mach_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 协议,转载需注明出处和作者。