guard let s = InputStream(fileAtPath: path) else {
throw PbrtScannerError.noFile
}
stream = s
stream.open()
if stream.streamStatus == .error {
throw PbrtScannerError.noFile
}
var bytes = Array<UInt8>(repeating: 0, count: bufferLength)
buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferLength)
buffer.initialize(from: &bytes, count: bufferLength)
bufferIndex = 0
bytesRead = stream.read(buffer, maxLength: bufferLength)
FileHandle 的使用:
Swift复制代码
let fileURL = URL(fileURLWithPath: "path/to/file")
if let fileHandle = try? FileHandle(forReadingFrom: fileURL) {
let data = fileHandle.readData(ofLength: 12)
// 处理读取到的数据
fileHandle.closeFile()
}
let fileURL = URL(fileURLWithPath:"/")
do {
let values = try fileURL.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey])
if let capacity = values.volumeAvailableCapacityForImportantUsage {
print("Available capacity for important usage: \(capacity)")
} else {
print("Capacity is unavailable")
}
} catch {
print("Error retrieving capacity: \(error.localizedDescription)")
}
[SWIFT\] Get available disk space w... \| Apple Developer Forums](https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.apple.com%2Fforums%2Fthread%2F113250 "https://developer.apple.com/forums/thread/113250")
[Checking Volume Storage Capacity \| Apple Developer Documentation](https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Ffoundation%2Fnsurlresourcekey%2Fchecking_volume_storage_capacity "https://developer.apple.com/documentation/foundation/nsurlresourcekey/checking_volume_storage_capacity")
#### return
背景:当想通过插入个 return 来提前中断代码,结果发现 return 后面的代码被执行了。
return 下一行接个表达式,下一行的表达式也会被执行。因此要避免这种情况应写成:
```Swift
func returnInTheMiddle() {
print("This is called as expected")
return;
print("This is called as well")
}
returnInTheMiddle()
```
[Return keyword and following expression - Mateusz Karwat](https://link.juejin.cn?target=https%3A%2F%2Fmateuszkarwat.com%2Fpost%2Freturn-keyword%2F "https://mateuszkarwat.com/post/return-keyword/")
因此 return 充当个截断的语句时,警告应该是这样的:
```YAML
Code after 'return' will never be executed
```
而不是:
```YAML
Expression following 'return' is treated as an argument of the 'return'
```
当然,有返回值的就不会出现上面的歧义。
#### didSet loop
背景:发现在 disSet 中调用 set 逻辑不会循环调用,但在 didSet 中调用一个方法,在其中调用 set 就会造成循环调用。
> `didSet` 观察器会将旧的属性值作为参数传入,可以为该参数指定一个名称或者使用默认参数名 `oldValue`。如果在 `didSet` 方法中再次对该属性赋值,那么新值会覆盖旧的值。
按照上面的意思,隐含表达了在 didSet 中再次对属性赋值不会再触发 didSet,更不会陷入循环调用。但这也是仅限于 didSet 内,如下的 case,还是会陷入循环调用中:
```Swift
class Manager {
var isEnable: Bool = true {
didSet {
updateEnableState()
}
}
func updateEnableState() {
print("isEnable: \(isEnable)")
isEnable = true
}
}
let manager = Manager()
manager.isEnable = true
```
所以要进行属性值处理,需在 didSet 中完成,而不能新建一个方法。
另外,在构造方法中对属性赋值,也不会触发观察器的执行。
#### URL 语义化
不要直接使用 String 表达 URL 的组成部分以及解析 URL,而是使用这些类:URL、URLComponents、URLQueryItem。
你会发现 NSString 的"Working with Paths"章节的 API 在 String 上都移除了,这是因为这些 API 使用 URL 可以更准确地表达语义:
```Swift
/// NSString Working with Paths
class func path(withComponents: [String]) -> String
var pathComponents: [String]
var lastPathComponent: String
var pathExtension: String
func appendingPathComponent(String) -> String
func appendingPathExtension(String) -> String?
var deletingLastPathComponent: String
var deletingPathExtension: String
```
#### 扩展管理:使用"命名空间"
背景:扩展方法太多,希望对扩展方法归类拆分。
Swift 没有 C++ 的命名空间,但可以用类型仿照一个,实现访问权限的收拢。
下面代码对原本在 MediaContext 扩展的 `maxWidth` 方法转移到了 MediaContext.VideoWrapper。
```Swift
// 建立个命令空间
private extension MediaContext {
struct VideoWrapper {
let base: MediaContext
}
var video: VideoWrapper {
VideoWrapper(base: self)
}
}
// 在命名空间内写扩展方法
private extension MediaContext.VideoWrapper {
func maxWidth() -> CGFloat {
max(base.contentWidth(of: .video, flag: .normal), base.globalContentWidth())
}
}
```
使用:
```Swift
class ClipController {
let context: MediaContext
func readWidth() {
// 调用
let width = context.video.maxWidth()
}
}
```
#### 结构体默认构造函数不能跨模块使用
结构体定义了属性,就会自动有个默认的按属性顺序的构造函数,但这个默认构造函数只能在结构体定义的 Module 中能访问,在别的 Module 无法访问,需显示声明。
[Default initializer is inaccessible](https://link.juejin.cn?target=https%3A%2F%2Fuseyourloaf.com%2Fblog%2Fdefault-initializer-is-inaccessible%2F "https://useyourloaf.com/blog/default-initializer-is-inaccessible/")
#### 获取类型信息
模块类名:
```Swift
String(reflecting: type(of: receiver))
```
获取地址:
```Swift
Unmanaged.passUnretained(receiver).toOpaque()
```
#### Error.localizedDescription
自己实现一个 Error 并实现 localizedDescription 属性,并不能正常调用。
```Swift
struct StringError: Error {
let content: String
var localizedDescription: String { content }
}
print("错误".makeError().localizedDescription) // 会输出:"The operation couldn't be completed. (InfraKit.StringError error 1.)"
```
#### defer
A `defer` statement is used for executing code just before transferring program control outside of **the scope that the defer statement appears in**.
即 deder 定义的代码在作用域结束的时候会调用。
从语言设计上来说,`defer` 的目的就是进行资源清理和避免重复的返回前需要执行的代码,而不是用来以取巧地实现某些功能。这样做只会让代码可读性降低。
defer 放在函数末尾相当于没写,应尽可能放在靠前的地方。
> 以前很单纯地认为 `defer` 是在函数退出的时候调用,并没有注意其实是**当前 scope 退出的时候** 调用这个事实,造成了这个错误。在 `if`,`guard`,`for`,`try` 这些语句中使用 `defer` 时,应该要特别注意这一点。
>
> [关于 Swift defer 的正确使用 \| OneV's Den](https://link.juejin.cn?target=https%3A%2F%2Fonevcat.com%2F2018%2F11%2Fdefer%2F "https://onevcat.com/2018/11/defer/")
另一方面,利用这个特性,把锁的加锁和解锁放在同一行是个比较不错的实践,这样作用域内(从该代码开始到作用域结束)的代码都加锁了,而且即使后面 guard 语句提前返回了,也不担心出现加锁了忘记解锁的问题。
```Swift
locker.lock(); defer { locker.unlock() }
```
🔜
#### 💬高频复用又经常忘记的代码
##### Hashable 实现
Hashable 继承于 Equatable,所以两者都要实现。
```Swift
import Foundation
struct Person: Hashable {
var name: String
var age: Int
// 实现 == 操作符
static func == (lhs: Person, rhs: Person) -> Bool {
return lhs.name == rhs.name && lhs.age == rhs.age
}
// 实现 hash(into:) 方法
func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(age)
}
}
let person1 = Person(name: "Alice", age: 30)
let person2 = Person(name: "Bob", age: 25)
let person3 = Person(name: "Alice", age: 30)
let peopleSet: Set = [person1, person2, person3]
print(peopleSet) // 输出: [Person(name: "Alice", age: 30), Person(name: "Bob", age: 25)]
```
#### 💬调试
##### Swift 符号断点似乎要重新编译?
否则不生效?

#### 💬注释
##### 文档注释标记
一般规则:`Tag: Content`
```Swift
/**
两个整数相加
# 加法(标题一)
这个方法执行整数的加法运算。
## 加法运算(标题二)
想加个试试看
中间隔着一个横线
***
代码块的*使用*方法:
``(不用添加括号)`
let num = func add(a: 1, b: 2)
// print 3
``(不用添加括号)`
- c: 参数一
- d: 参数二
- f: 参数三
- Parameters:
- a: 加号左边的整数
- b: 加号右边的整数
- Throws: 抛出错误,此方法不抛出错误,只为另外演示注释用法。
- Returns: 和
- Important: 注意这个方法的参数。
- Version: 1.0.0
- Authors: Wei You, Fang Wang
- Copyright: 版权所有
- Date: 2020-12-28
- Since: 1949-10-01
- Attention: 加法的运算
- Note: 提示一下,用的时候请注意类型。
- Remark: 从新标记一下这个方法。
- Warning: 警告,这是一个没有内容的警告。
- Bug: 标记下bug问题。
- TODO: 要点改进的代码
- Experiment: 试验点新玩法。
- Precondition: 使用方法的前置条件
- Postcondition:使用方法的后置条件
- Requires: 要求一些东西,才能用这个方法。
- Invariant: 不变的
*/
func add(a: Int, b: Int) throws -> Int {
return a + b
}
```
更多:
* [Xcode 中的 Swift 代码注释 - 掘金](https://juejin.cn/post/6917520941933625358 "https://juejin.cn/post/6917520941933625358")
* [Swift 注释和文档_swift 文档-CSDN 博客](https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2Fqq_14920635%2Farticle%2Fdetails%2F130271138 "https://blog.csdn.net/qq_14920635/article/details/130271138")
##### 代码冲突
使用段落注释可以避免一些代码合并的冲突,但同时也会让你容易忽略掉注释内容的变更。
#### 💬泛型
##### 范型类型不支持存储属性
```YAML
Static stored properties not supported in generic types
```
所以想要在扩展中定义存储属性,要么放到具体的类中,要么定一个 fileprivate 的全局变量,再用一个计算属性中转一下(不推荐)。
##### 泛型扩展声明
以下两种形式指定范型类型的扩展都支持且等价:
```Swift
// 定义 UtilExtension 的 UIViewController 及其子类的泛型类型
extension UtilExtension {}
extension UtilExtension where Base: UIViewController {}
```
##### 容器元素类型不能为范型
背景:希望一个包含泛型实例的数组能声明为泛型类型的数组。
```Swift
struct Car {
let p: T
}
let arr = [
Car(p: 45),
Car(p: "String"),
Car(p: [1]),
] as [Any]
// 实际的类型
[
Car(...),
Car(...),
Car>(...),
]
```
容器是范型的,其类型必须确定,Swift 不能识别不同的范型类型,这样只会被认为是 Any 类型,因为泛型的具体实例之间没有继承关系,也没有公共遵循的协议。
##### 使用范型可以还原类型
相比使用协议,使用范型可以还原类型。示例:
```Swift
func addTargetAction(for controlEvents: UIControl.Event, _ action: @escaping (Base) -> Void) -> RemovableControlTarget
```
换到 C++ 的概念,就把泛型理解为模板吧,具体使用泛型时,即确定泛型类型时,其实就是泛型定义的占位符(如:T)替换成具体的类型。
#### 💬闭包
##### 嵌套函数循环引用陷阱
函数在 Swift 中几乎等同于闭包,从调用的视角,函数除了可以使用参数名称、参数标签外,与闭包无异。如下代码的 ②③ 的定义就是等价的。嵌套函数定义和使用都很方便,但嵌套函数的自动捕获的机制容易造成循环引用。
```Swift
var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .u.systemCyan
// ② 嵌套函数,也会自动捕获 self
func printButtonNested() {
print("🚧 button: \(self.button!)")
}
// ③ printButtonNested 等同于定义个捕获实例变量的闭包常量
let printButtonNested0 = { [self] in
print("🚧 button: \(self.button!)")
}
// ④ 比较保险的是定义成弱引用捕获变量的闭包,使用 weak self 打破循环引用
let printButtonClosure = { [weak self] in guard let self else { return }
print("🚧 button: \(self.button!)")
}
let button = makeButton(title: "Tap", action: printButtonClosure)
self.button = button
}
// ① 实例方法,自动捕获 self
func printButton() {
print("🚧 button: \(self.button!)")
}
```
上面 `makeButton` 方法会将 `action` 传入的闭包让 `button` 持有,`button` 被 self 持有,若 `action` 传入闭包强捕获了 self,就会造成循环引用。
所以如果将上面的 ①②③ 传入 `makeButton` 方法都会造成循环引用,Xcode 不会给任何警告或报错。
最佳实践:对于要传递的函数/闭包,应如 ④ 这样定义成闭包,并使用捕获列表,弱引用声明需要捕获的值。类似的若需要捕获一些可能触发循环引用的的引用类型值,也需要在捕获列表中弱引用声明。
##### 闭包中的 self 判断可能不会中断点
```Swift
let updateSelectedSegmentIfNeeded = { [weak self] (new: LVMediaSegment) in
guard let self = self else { return }
guard panel.isShowing else { return }
panel.disableAdjust()
self.viewModel.updateSelectedSegment(new)
panel.reset(dataSource: self.viewModel) // reset后会自动enable adjust
}
```
闭包中的第一行 `guard let self = self else { return }` 可能不会中断点,需要对下一行下断点。这个情况在自定义 tool chain 中可能会比较常见。
##### @escaping 等价于 optional?
背景:以下代码都能通过编译,看起来用 Optional 包一下闭包就不用写 `@escaping` 了?
```Swift
var actionHandler: (() -> Void)?
func a(action: @escaping () -> Void) {
actionHandler = action
}
func b(action: (() -> Void)?) {
actionHandler = action
}
```
[function - Swift optional escaping closure parameter - Stack Overflow](https://link.juejin.cn?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F39618803%2Fswift-optional-escaping-closure-parameter "https://stackoverflow.com/questions/39618803/swift-optional-escaping-closure-parameter")
[Swift 如何给回调添加 @escaping 和 optional \| Gpake's](https://link.juejin.cn?target=https%3A%2F%2Fgpake.github.io%2F2018%2F12%2F12%2FswiftEscapingOptional%2F "https://gpake.github.io/2018/12/12/swiftEscapingOptional/")
可以理解为 Optional 把闭包包装成一个 enum,闭包已经不再是参数列表中了。所被包装的闭包成了 Optional enum 的关联值,其实是个枚举实例的成员了,跟属性类似,默认就是 eacaping。所以 Optional 的闭包已经是 escaping 语义了。
#### 💬分支处理技巧
##### if/guard/case let/var
在所有分支语句中,包含 if/guard/switch,都可以用 let 创建一个符合条件的常量。
> 从 Swift 5.7 开始,`if let a = a` 的形式可以写成 `if let a` 了
注意 guard let/var 和 if let/var 在作用域上会有些细微的差别:
* guard 创建的常量/变量作用域是当前行代码到结尾,可以覆盖前面的参数列表,但不能覆盖前面定义的常量/变量。
* 但 else 里面不能访问 guard let 创建的常量。
* if 创建的常量/变量作用域是后续紧接着的花括号,所以即使前后出现同名常量/变量也不会编译冲突。
##### if ↔︎ guard
guard 的语义:确保后续语句都是基于 guard 条件为 true 的前提。
实际使用中经常需要对 if 和 guard 相互转换:
```Swift
// 对于提前退出的 case
guard condition else { return }
// 等同于
if !condition { return }
```
简单记忆:相同效果的语句,guard 和 if 后面的条件刚好相反
对于提前退出的 if 语句其实可以不改写成 guard,有些改写反而降低了可读性。例如表达"如果满足 A 条件就退出",这样直接写成 if 就好;如果表达"确保后续的代码都满足 B 条件(否则退出)",这样则考虑写成 guard 语句。
但嵌套的 if 语句改写成 guard 则有利于让代码更清晰。
##### 带关联值枚举判等
背景:枚举**只要有一个**带关联值的 case,该枚举就不能使用 == 判等(除非该枚举实现了 Equatable)。
需修改判断方式:
```Swift
if effectType == .prop
// ⬇️
if case .prop = effectType
```
具体实例:
```Swift
// 未遵循 Equatable 的枚举
enum Message {
case text(String)
case attachment(name: String, size: Int)
case timestamp(Date)
}
let message: Message = .attachment(name: "report.pdf", size: 10240)
// 1. 仅匹配枚举类型,忽略关联值
if case .attachment = message {
print("这是一个附件消息") // 会执行
}
// 2. 匹配枚举类型并绑定关联值(可用于后续判断)
if case .attachment(let name, let size) = message {
print("附件名:\(name),大小:\(size)") // 会执行
}
// 3. 匹配枚举类型并判断关联值条件
if case .attachment(_, let size) where size > 5000 {
print("大附件(超过5000字节)") // 会执行
}
// 条件等同于:
// if case .attachment(_, let size), size > 5000 {
// 4. 完全匹配关联值(需手动判断)
if case .attachment(let name, let size) = message,
name == "report.pdf",
size == 10240 {
print("匹配到指定附件") // 会执行
}
```
同时也应注意到,这样的表达式只能在 if/guard 后面使用,它不是个逻辑表达式,不能赋值到布尔量的。
[How to compare enum with associated values by ignoring its associated value in Swift? - Stack Overflow](https://link.juejin.cn?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F31548855%2Fhow-to-compare-enum-with-associated-values-by-ignoring-its-associated-value-in-s "https://stackoverflow.com/questions/31548855/how-to-compare-enum-with-associated-values-by-ignoring-its-associated-value-in-s")
##### switch-case
###### 作为右值
当然 if 语句也可以,多用于常量的定义。
```Swift
let menuIdentifier: MenuIdentifier = switch entrance {
case .global: .effectRoot
case .video: .videoEffectRoot
case .subVideo: .subVideoEffectRoot
}
```
###### case let
case let 是创建变量,这其中用法很丰富。
可以做类型转换:
```Swift
var imageData: Data? = nil
switch mediaAsset {
case let asset as ImageDataAsset:
imageData = asset.data
if let carttonImageFilePath = asset.cartoonFilePath, let cartoonImage = UIImage(contentsOfFile: carttonImageFilePath) {
imageData = cartoonImage.pngData()
}
case let asset as DraftImageAsset:
imageData = asset.photo.resize(limitMaxSize: size).pngData()
case let asset as DataAsset:
imageData = asset.data
default:
break
}
```
注意这里是直接使用 `as` 关键字,而不是 `as?`,与 `if/gruard let` 的变量定义有差别。
###### case range
做值域 case 划分,case 后可接 range,需要有个起点:
```Swift
func calculateUserScore() -> Int {
let diff = abs(randomNumber - Int(bullsEyeSlider.value))
switch diff {
case 0:
return PointsAward.bullseye.rawValue
case 1..<10:
return PointsAward.almostBullseye.rawValue
case 10..<30:
return PointsAward.close.rawValue
default:
return 0
}
}
```
区间判断对类型为整型的就比较好处理,如果是浮点数,就不一定能满足需求,因为它不能表达 `if value > 0.1` 的语义,即至少有一个起点,这就要求这些 case 排列是从小到大排列。但也不是不行,如:
```Swift
var progress: CGFloat!
switch CGFloat(progress) {
case 0 ... 0.25:
barColor = .red
case 0.25 ... 0.5:
barColor = .yellow
default:
break
}
```
因为 case 0 占用了 0.25,所以 case 1 是不会匹配 0.25 的。
**注意:分支判断需要覆盖所有值域。**
#### 💬Dictionary map
背景:批量修改字典 key、value;重建字典。
1. 使用 `mapValues(_:)` 方法:
1. 仅能修改值,过程中无法对 key 访问。
```Swift
let dictionary = ["foo": 1, "bar": 2, "baz": 5]
let newDictionary = dictionary.mapValues { value in
return value + 1
}
//let newDictionary = dictionary.mapValues { $0 + 1 } // also works
print(newDictionary) // prints: ["baz": 6, "foo": 2, "bar": 3]
```
1. 【不推荐】使用 `map` + `init(uniqueKeysWithValues:)`:
1. 会中间生成个 tuple array,需要多一步转换。
```Swift
let dictionary = ["foo": 1, "bar": 2, "baz": 5]
let tupleArray = dictionary.map { (key: String, value: Int) in
return (key, value + 1)
}
//let tupleArray = dictionary.map { ($0, $1 + 1) } // also works
let newDictionary = Dictionary(uniqueKeysWithValues: tupleArray)
print(newDictionary) // prints: ["baz": 6, "foo": 2, "bar": 3]
```
1. 【推荐】使用 `reduce` 方法:
1. 通过元组的方式遍历整个字典,注意两个 reduce 方法的异同,根据使用场景来选择:
* `reduce(_:_:)`:闭包中每次都需要返回每次修改的片段值。
* `reduce(into:_:)`:【更推荐】闭包中直接对结果重新赋值,无须返回。
```Swift
let dictionary = ["foo": 1, "bar": 2, "baz": 5]
let newDictionary = dictionary.reduce([:]) { (partialResult: [String: Int], tuple: (key: String, value: Int)) in
var result = partialResult
result[tuple.key] = tuple.value + 1
return result
}
print(newDictionary) // prints: ["baz": 6, "foo": 2, "bar": 3]
let dictionary = ["foo": 1, "bar": 2, "baz": 5]
let newDictionary = dictionary.reduce(into: [:]) { (result: inout [String: Int], tuple: (key: String, value: Int)) in
result[tuple.key] = tuple.value + 1
}
print(newDictionary) // prints: ["baz": 6, "foo": 2, "bar": 3]
```
1. 另外起一个字典变量在遍历中重新赋值:
```Swift
let dictionary = ["foo": 1, "bar": 2, "baz": 5]
var newDictionary = [String: Int]()
for (key, value) in dictionary {
newDictionary[key, default: value] += 1
//newDictionary[key] = value + 1
}
print(newDictionary) // prints: ["baz": 6, "foo": 2, "bar": 3]
```
#### 💬区间
##### 关系
背景:如果准确表达区间
```YAML
RangeExpression
ClosedRange
PartialRangeFrom
PartialRangeThrough
PartialRangeUpTo
Range
# 闭合区间。表达:min <= value <= max。支持遍历。
struct ClosedRange where Bound : Comparable
3...5 # 字面量,定义了运算符 ...
// from Range
init(Range)
# 单侧区间。表达:min <= value。
struct PartialRangeFrom where Bound : Comparable
5...
# 单侧区间。表达:value <= max。
struct PartialRangeThrough where Bound : Comparable
...5.0
# 单侧区间。表达:value < max。
struct PartialRangeUpTo where Bound : Comparable
# 半开区间。表达:min <= value < max。支持遍历。
struct Range where Bound : Comparable
0.0..<5.0 # 字面量,定义了运算符 ..<
# from NSRange
init?(NSRange, in: String)
init?(NSRange)
// from CloseRange
init(ClosedRange)
```
##### 使用场景
###### 作为 Collection
ClosedRange、Range 都遵循 Collection 协议,可以作为集合使用。常见的用于遍历:
```Swift
let range: ClosedRange = 0...10
print(range.first!) // 0
print(range.last!) // 10
let names = ["Antoine", "Maaike", "Jaap"]
for index in 0...2 {
print("Name \(index) is \(names[index])")
}
// Name 0 is Antoine
// Name 1 is Maaike
// Name 2 is Jaap
```
当然,也可以转换成数组:
```Swift
let intArray: [Int] = Array(min...max)
```
###### 取集合子集
```Swift
let names = ["Antoine", "Maaike", "Jaap"]
print(names[0.. = emojiText.startIndex.. NSRange
NSRange(range, in: title)
// NSRange -> Range
Range(nsRange, in: title)
```
具体应用:
```Swift
public extension String {
var nsRange: NSRange {
NSRangeFromString(self)
}
/// Range -> NSRange
func nsRange(from range: Range) -> NSRange {
return NSRange(range, in: self)
}
/// NSRange -> Range
func range(from nsRange: NSRange) -> Range? {
return Range(nsRange, in: self)
}
}
// 使用示例
let str = "测试转换 Range 和 NSRange"
if let subRange = str.range(of: "转换") {
let nsR = str.nsRange(from: subRange)
print("NSRange: location=\(nsR.location), length=\(nsR.length)")
if let convertedRange = str.range(from: nsR) {
print(str[convertedRange]) // 输出 "转换"
}
}
```
**注意:String 中的** **`NSRange`** **基本是 NSString 使用的,都是基于 UTF-16 编码单元。**
```Swift
// 下面两行代码等价
NSRangeFromString(self)
NSRange(location: 0, length: self.utf16.count)
```
🔜
#### 🚩PromiseKit
##### 设计思想借鉴
* 异步/同步逻辑原子化。对一段逻辑封装,统一返回 Promise 泛型,可以让这部分逻辑更容易被外部集成、调用和线程切换。
* 是 async await 的平替。
* 逻辑封装方法中,甚至不用指定队列执行,可以在 `then` 等 API 调用时再切换执行的队列。
* 同步转异步思路:把终点信号放到闭包返回出去。
* 短路求值/最小化求值:遇到错误直接忽略后续代码,更安全、高效、易读。
* 使用返回错误直接终止后续代码逻辑。
* 链式调用中途的 promise 发生错误也直接终止后续 promise 任务的执行。
##### API 备忘
* 提供的 API 大多在其 `body` 闭包参数中写逻辑,所以最简单使用 PromiseKit API 的方式就只关注 `body` 闭包的出参和入参即可。
* API 都提供 `on: DispatchQueue? = conf.Q.return` 和 `flags: DispatchWorkItemFlags? = nil` 的入参,用于配置逻辑 `body` 闭包执行的队列。
API `body` 闭包签名:
```YAML
# Promise
resolver: (Resolver) throws -> Void
pipe: (Result) -> Void
# Thenable
pipe: (Result) -> Void
then: (T) throws -> U: Thenable
map: (T) throws -> U
compactMap: (T) throws -> U?
done: (T) throws -> Void
get: (T) throws -> Void
tap: (Result) -> Void
# CatchMixin
catch: (Error) -> Void
recover: (Error) throws -> U: Thenable
recover: (Error) -> Guarantee
recover: (Error) -> Void
ensure: () -> Void
ensureThen: () -> Guarantee
finally: () -> Void
# Guarantee
resolver: ((T) -> Void) -> Void
pipe: (Result) -> Void
done: (T) -> Void
get: (T) -> Void
map: (T) -> U
then: (T) -> Guarantee
```
不常用 API `body` 闭包签名:
```YAML
# Thenable where T: Sequence
mapValues/flatMapValues: (T.Iterator.Element) throws -> U
compactMapValues: (T.Iterator.Element) throws -> U?
thenMap/thenFlatMap: (T.Iterator.Element) throws -> U
filterValues: (T.Iterator.Element) -> Bool
# Guarantee where T: Sequence
mapValues/flatMapValues: (T.Iterator.Element) -> U
compactMapValues: (T.Iterator.Element) throws -> U?
thenMap/thenFlatMap: (T.Iterator.Element) -> Guarantee
filterValues: (T.Iterator.Element) -> Bool
sortedValues: (T.Iterator.Element, T.Iterator.Element) -> Bool
```
不用处理/可忽略返回值的接口:
```YAML
catch -> PMKFinalizer
finally -> Void
cauterize -> PMKFinalizer # 用于消费/忽略掉 catch 中的错误处理
```
工具性接口:
```YAML
firstly # 语法糖
DispatchQueue.global().async(.promise) # 直接切队列构造 Promise/Guarantee
race # 完成其中一个 Promise/Guarantee 就能获得结果
when # 完全全部 Promise/Guarantee 才能获得结果
```
所以总的来说,仅有这么几个关键词:
* `resolver`:构建 Promise/Guarantee 时传递结果。
* `pipe`:连接结果。
* `then`:做下一步的异步任务,连接另一类型的 Thenable,即 Promise/Guarantee。
* `map`/`compactMap`:成功结果值转换,与 then 的区别是返回值类型,而不是 Thenable。
* `done`:无返回值的成功结果处理。与 catch 互斥。
* `catch`:失败结果处理。与 done 互斥。
* `recover`:修复/忽略/消费 部分/全部 错误。
* `ensure`/`finally`:有结果就执行,无论是成功还是失败结果。
* `get`/`tap`:旁路处理成功值,不影响流程。
* `race`、`when`:组合多个 Promise/Guarantee。
##### 使用构造函数快速创建
快速创建:
```Swift
func verify(completion: @escaping (()) -> Void) {}
func fetch(completin: @escaping (String) -> Void) {}
_ = Promise { verify(completion: $0.fulfill) }
_ = Guarantee { verify(completion: $0) }
_ = Guarantee { seal in
verify {
seal(())
}
}
_ = Guarantee { fetch(completin: $0) }
```
##### 抛错
在 `then` 闭包中返回 promise,若需中断/抛错,可以:
* `return Promise.init(error:)`:包装错误直接返回。
* **`throw Error`**:个人更推荐。Swift 中更自然、通用的抛错语句。
上述的抛错相对于整个方法体/函数体来说也是短路求值,即不会执行语句后续的代码。相对比自己加个 `failure: @escaping (Error) -> Void` 闭包回调更加安全和易用。闭包调用不紧接 return 就造成范围之外的代码逻辑的执行。
扩展:在自己的封装的方法中,也可以加上(`->` 前)`throws` 关键词使其成为 throwing 函数。日常在设计 API、逻辑时也多多使用 `throw Error` 的方式来抛错。外部使用时不需要处理错误则直接 `try? func` 忽略。
throwing 函数的优势:
* 可以使用抛错来代替 `return nil`,这样定义函数返回值也更容易使用非 Optional 的类型。
* 短路求值。
* 外部调用可选地、规范地处理错误。
##### 错误定义
一个 Service 可以定义一组错误(enum)。
也可以直接使用 PromiseKit 自身定义的错误:PMKError。
* `returnedSelf`
* `badInput`
* `cancelled`
值得借鉴:定义错误时可遵循 LocalizedError 协议,提供 `errorDescription` 错误描述。可以借鉴 PMKError 同时实现 CustomDebugStringConvertible 和 LocalizedError 协议,更便于 lldb 输出。
##### 忽略错误
Thenable 处理后返回的都是自身,即 Promise/Guarantee。Promise 链式调用一般都需要处理错误,若错误已在 `recover` 中或别处已处理,需要忽略错误处理环节,可使用 `CatchMixin.cauterize()` 代替 catch 语句。
##### 切换执行的线程队列
PromiseKit API 都提供 `on: DispatchQueue? = conf.Q.return`,默认是主队列。要切换其他队列可直接传入 `on` 参数,如 `.then(on: .global()) {}`。
##### 插入旁路逻辑
对于一些不影响主流程链路的操作,如计时、埋点、log,我们不应直接在主流程链路中插入代码,可以使用 `get`/`tap` 旁路地插入代码,也方便移除和屏蔽。
##### 常见编译报错
> cannot conform to 'Thenable' when I try to use Promise functions
出现这样的错误大概率是用 then 拼接了不返回 Promise 的函数。解决方法也很简单:
They replaced that usage of `then { }` with `done { }`.
```Swift
firstly {
promiseGetJWTToken()
}.done { tokenJWT in
// use your token
}
```
#### 🚩ObjectMapper
ObjectMapper 最巧妙之处是用自定义运算符 `<-` 连接了属性和对应的解析方式,将赋值引用与属性类型通过运算符传递到解析方式中,避开了 Codable 还需要定义 CodingKey 的额外操作。
##### 自定义解析
自定义解析的最佳时机是 `BaseMappable.mapping(map:)`。
官方给出的自定义参数是在对 `map` 取下标时传入 TransformOf 实例,如:
```Swift
let transform = TransformOf(fromJSON: { (value: String?) -> Int? in
// transform value from String? to Int?
return Int(value!)
}, toJSON: { (value: Int?) -> String? in
// transform value from Int? to String?
if let value = value {
return String(value)
}
return nil
})
id <- (map["id"], transform)
```
查看源码,其实还有更进阶的方式。
🔜 后续有空再展开
##### 扩展支持 plist 序列化反序列化
源码中通过 Mapper 作为解析管理类,通过这个类,甚至可以添加一个扩展,支持 plist 的序列化和反序列化。
```Swift
// Mapper+PropertyList.swift
import Foundation
import ObjectMapper
public extension Mapper {
// MARK: 反序列化
static func parsePropertyList(data: Data) -> [String: Any]? {
let parsed: Any?
do {
parsed = try PropertyListSerialization.propertyList(from: data, format: nil)
} catch {
print(error)
parsed = nil
}
return parsed as? [String: Any]
}
func map(propertyList data: Data) -> N? {
guard let parsed = Mapper.parsePropertyList(data: data) else { return nil }
return map(JSON: parsed)
}
// MARK: 序列化
static func toPropertyList(_ propertyListObject: Any, format: PropertyListSerialization.PropertyListFormat = .xml) -> Data? {
guard PropertyListSerialization.propertyList(propertyListObject, isValidFor: format) else { return nil }
let data: Data?
do {
data = try PropertyListSerialization.data(fromPropertyList: propertyListObject, format: format, options: 0)
} catch {
print(error)
data = nil
}
return data
}
func toPropertyList(_ object: N, format: PropertyListSerialization.PropertyListFormat = .xml) -> Data? {
let JSONDict = toJSON(object)
return Mapper.toPropertyList(JSONDict as Any, format: format)
}
}
public extension Mappable {
init?(propertyList data: Data, context: MapContext? = nil) {
guard let obj: Self = Mapper(context: context).map(propertyList: data) else { return nil }
self = obj
}
func toPropertyListData(format: PropertyListSerialization.PropertyListFormat = .xml) -> Data? {
Mapper().toPropertyList(self, format: format)
}
}
```
使用:
```Swift
let newSong: Song = makeModel(json: jsonText)!
// 序列化到 plist
guard let data = decodedSong.toPropertyListData() else { return }
print("🚧 song plist: \(String(data: data, encoding: .utf8))")
// 从 plist 反序列化
let songFromPlist = Song(propertyList: data)
dump(songFromPlist, name: "songFromPlist")
```
### LLDB
##### 类型转换
```Bash
p import Lib
po unsafeBitCast(address, to: Type.self)
```
##### 刷新 UI
```Bash
e CATransaction.flush()
```
##### 符号断点
系统 API 或闭源 API 断点需要下符号断点。
遇到 OC 接口,需要 OC 的符号。如:
```YAML
PHImageManager.h:188
- (PHImageRequestID)requestPlayerItemForVideo:(PHAsset *)asset options:(nullable PHVideoRequestOptions *)options resultHandler:(void (^)(AVPlayerItem *__nullable playerItem, NSDictionary *__nullable info))resultHandler API_AVAILABLE(macos(10.15));
"Copy Symbol Name"或"Copy Qualified Symbol Name"
requestPlayerItemForVideo:options:resultHandler:
```
### CocoaPods
#### 新建文件
Xcode 文件区(project navigator)展示的目录有两种类型,在引入之初就决定了:

| | group | folder reference |
|------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|
| 使用场景 | 最常用。代码、资源引入无脑选它。 | 蓝色图标。仅用于资源,如 bundle 资源。 |
| 细分 | 对应目录的 Group:创建即创建本地同名目录。无对应目录的 Group:虚拟的目录,无对应的本地目录。图标左下角有小三角或箭头。可与其他 group 同名。 | 无 |
| 更新逻辑 | 外部更新不会同步。引入时目录文件的结构就确定了,后续文件在 Xcode 外部增删不会同步到 Xcode 中,需手动 add files。Pod install 之所以会更新 group 中内容是因为根据本地目录重建了 group。Xcode 内更新:对应目录的 Group:重命名会直接修改本地目录名。添加文件添加到对应目录中。无对应目录的 Group:可以随意重命名。添加文件会添加到项目根目录。 | 相互更新。 |
而 Pod 可能存在两种 Group。所以为了确保新建文件位置正确。新建文件直接在源文件对应目录创建文件,再引入。避免因为目录不在源码目录中而导致 pod install 后索引不到。
#### 访问权限
Pod 作为 Swift Module,所以当设计的类是其他 Module 使用的,则一定要声明为 public!
### UI
#### 布局区域、响应区域、展示区域
一般来说,布局区域 = 响应区域 = 展示区域。即一般场景只要布局好视图,基本不用修改响应区域和展示区域,一旦要求响应区域、展示区域和布局区域不一致时,是时候将这三者解耦,单独考虑。
* 布局区域:1:1 对应还原到设计稿。
* 相关 API:auto layout、`UIView.intrinsicContentSize`、`UIView.frame`、`UIView.bounds`。
* 响应区域:根据 UX 要求扩大或缩小。
* 相关 API:`UIView.point(inside:with:)`、`UIView.hitTest(_:with:)`。
* 展示区域:按照设计稿扩大或缩小。
* 相关 API:`UIView.clipsToBounds`、`UIView.mask`、`CALayer.masksToBounds`、`CALayer.mask`。
通过修改对应 API 来修改对应的区域,三者相互独立解耦。
#### 弹簧动画
`usingSpringWithDamping` 是 `UIView` 的一个动画方法,用于创建一个弹簧动画。`usingSpringWithDamping` 方法接受两个参数:`dampingRatio` 和 `initialSpringVelocity`,分别用于指定弹簧动画的阻尼比和初始速度。
* `dampingRatio`:阻尼比,用于指定弹簧动画的震荡程度,取值范围为 0.0 到 1.0。当阻尼比为 0.0 时,动画会无限振荡;当阻尼比为 1.0 时,动画会立即停止。建议值为 0.7 到 0.8,较小的值会使动画更加弹性,较大的值会使动画更加刚性。
* `initialSpringVelocity`:初始速度,用于指定弹簧动画的初始速度,取值范围为任意值。初始速度为正数时,视图会向上移动;初始速度为负数时,视图会向下移动。建议值为 0,因为较大的值可能会导致动画过快或过慢。
以下是一个示例代码,演示如何使用 `usingSpringWithDamping` 方法来创建一个弹簧动画:
```Swift
UIView.animate(withDuration: 1.0, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0, options: [], animations: {
// 在此处设置视图的动画效果
view.transform = CGAffineTransform(translationX: 0, y: 100)
}, completion: nil)
```
在上面的示例中,我们使用 `usingSpringWithDamping` 方法来创建一个弹簧动画,并将阻尼比设置为 0.7,初始速度设置为 0。在动画块中,我们将视图的 `transform` 属性设置为一个平移变换,使其向下移动 100 个像素。
需要注意的是,当我们使用 `usingSpringWithDamping` 方法时,我们需要根据实际情况来选择合适的阻尼比和初始速度。建议在实际开发中进行多次测试和调整,以达到最佳的动画效果。
#### TextView 根据内容自动增高
背景:希望根据用户输入内容的来实时更新 text view 高度布局。
在 `didChange` 回调中重新计算高度,然后更新 textView 高度布局。计算高度如:
```Swift
let minHeight: CGFloat = Layout.TextView.minHeight
let maxHeight: CGFloat = Layout.TextView.maxHeight
let containerFrame = promptInputView.frame
if editText.isEmpty {
return minHeight
} else {
let constraintSize = CGSize(width: containerFrame.width, height: 1000)
let size = promptInputView.textView.sizeThatFits(constraintSize)
return min(max(size.height, minHeight), maxHeight)
}
```
`maxHeight` 用于实现把 text view 自动拉高到一个最大高度后,开始滚动内容。
#### ScrollView 居中
背景:让 scroll view 中的内容保持居中。
需要重新计算 cntent size 来设置 inset 实现居中。
```YAML
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
guard collectionView.numberOfSections == 1 else { return .zero }
var viewPortSize = collectionView.bounds.size
let contentInset = collectionView.contentInset
viewPortSize.width -= contentInset.horizontal
viewPortSize.height -= contentInset.vertical
let count = collectionView.numberOfItems(inSection: 0)
let contentWidth = CGFloat(count) * UI.itemSize.width + CGFloat(count - 1) * UI.itemSpacing
let contentHeight = UI.itemSize.height
var insets = UIEdgeInsets(inset: UIView.defaultOutlineWidth)
if viewPortSize.width > contentWidth {
insets.left = (viewPortSize.width - contentWidth) / 2
insets.right = insets.left
}
if viewPortSize.height > contentHeight {
insets.top = (viewPortSize.height - contentHeight) / 2
insets.bottom = insets.top
}
return insets
}
```
#### 监听页面页面过渡动画完成
背景:在页面 pod 动画完成后执行逻辑。
```Swift
func dismissToPresent(completion: @escaping () -> Void) {
guard let topVC = UIViewController.ibaseTopViewController else { return }
if let vc = topVC.presentingViewController {
CATransaction.begin()
vc.dismiss(animated: false)
let nav = vc as? UINavigationController ?? vc.navigationController
nav?.popToRootViewController(animated: false)
CATransaction.setCompletionBlock(completion)
CATransaction.commit()
} else {
DispatchQueue.main.async(execute: completion)
}
}
```
#### 设置行高
背景:自定义行高。
通过配置 NSMutableParagraphStyle 到富文本的 `paragraphStyle` 中:
```Swift
func makeText(_ text: String, font: UIFont, lineHeight: CGFloat, color: UIColor) -> NSAttributedString {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 0
paragraphStyle.maximumLineHeight = lineHeight
paragraphStyle.minimumLineHeight = lineHeight
return NSAttributedString(string: text, attributes: [
.paragraphStyle: paragraphStyle,
.foregroundColor: color,
.font: font,
])
}
```
#### 叠加与遮罩
overlay:
* 叠加效果。
* 只能加,不能减。
mask:
* 切除某部分,或让某部分变得透明。
* 只能减,不能加。
##### 颜色叠加
同色叠加,底部纯色,叠层透明度不同看不出效果。
```Swift
#FFFFFF33 = #FFFFFFFF - #000000CC # 顺序是从底往上
```
叠加是减法?越叠越暗。
#### CGMutablePath
CGMutablePath add arc 会接上之前线段的末尾,若是想画一段一段的圆弧,可能不符合预期,需要再添加 move 逻辑。
#### CALayer 似乎不能重写构造函数
#### 视图不展示问题排查
可按照以下思路排查:
1. 对象不在视图层级中(可能没 addSubview):lookin 找到对应的 view 对象。
2. 视图隐藏:`alpha == 0`,`isHidden == true`。
3. frame 是否正常:
1. w/h 为 0 都会表现为视图不展示。
2. 超出父视图可能会被裁切。
4. 确定是否有 mask:mask alpha 为 0 也会导致不展示。
#### 获取 icon 名称
在 lookin 中定位到 UIImageView,输出其 image 属性,即可在描述中看到 icon 名称。
```YAML
image
```
#### storyboard 不支持 Swift 嵌套类型

storyboard 设置 Class 时不支持 Swift 的嵌套类型,且必须勾选"Inhert Module From Target",否则将出现以下错误:
```YAML
[Storyboard] Unknown class _TtC5UILab22PageDataViewController in Interface Builder file.
```
storyboard/xib 这套 GUI 布局应该也是差不多要退出历史舞台了。
#### 找到焦点视图
找到当前处于焦点的视图,可对当前 UIWidnow 对象调用 `firstResponder` 扩展方法:
```Swift
public extension UIView {
/// SwifterSwift: Recursively find the first responder.
func firstResponder() -> UIView? {
var views = [UIView](arrayLiteral: self)
var index = 0
repeat {
let view = views[index]
if view.isFirstResponder {
return view
}
views.append(contentsOf: view.subviews)
index += 1
} while index < views.count
return nil
}
}
// 判断当前是否是焦点视图
xx == window?.firstResponder
```
#### SVG 路径绘制
一些走过的弯路:
* PaintCode 导入的 SVG 会做一些处理,导致与原来的 SVG 参数有些偏差。应该是我使用的姿势不对!
* [swiftvg -- Convert SVG path data to a Swift 3 UIBezierPath](https://link.juejin.cn?target=https%3A%2F%2Fswiftvg.mike-engel.com%2F "https://swiftvg.mike-engel.com/"):精度比较高,但画出来的图形会残缺。
最佳实践:
[SVG Converter](https://link.juejin.cn?target=http%3A%2F%2Fsvg-converter.kyome.io%2F "http://svg-converter.kyome.io/"),直接从文本编辑器打开 SVG,把其中的 viewBox 和路径参数拷贝出来,到这个网站进行转换。
备选方案:
参考 SVGKit 源码,使用代码直接从 SVG 读取并生成路径。
其他第三方组件:
* [GenerallyHelpfulSoftware/SVGgh: A framework for using SVG artwork in iOS Apps. Includes a UIView and a button class, printing and PDF export.](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2FGenerallyHelpfulSoftware%2FSVGgh "https://github.com/GenerallyHelpfulSoftware/SVGgh")
* [ap4y/UIBezierPath-SVG: NS/UIBezierPath from SVG string](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fap4y%2FUIBezierPath-SVG "https://github.com/ap4y/UIBezierPath-SVG")
* [IconJar/IJSVG: MacOS SVG rendering and exporting library](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2FIconJar%2FIJSVG "https://github.com/IconJar/IJSVG")
#### UIView drawRect 透明
需要额外设置 UIView 的 `backgroundColor` 属性为 `.clear`,单单在 `drawRect` 方法中做操作是做不到的。
#### 💬UITableView
##### 通过 auto layout 自适应高度
UITableViewCell 直接与 `contentView` 添加布局约束即可实现自适应高度。难搞的是 UITableView 的其他子部件。
###### header view 的特殊处理
header view 本身是不支持自动布局的,所以要特殊处理一番。
```Swift
func setAndLayoutTableHeaderView(header: UIView) {
self.tableHeaderView = header
self.tableHeaderView?.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
header.widthAnchor.constraint(equalTo: self.widthAnchor)
])
header.setNeedsLayout()
header.layoutIfNeeded()
header.frame.size = header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
self.tableHeaderView = header
}
```
或者在 layoutSubviews 中更新:
```Swift
func updateTableHeaderSize() {
if let topView = tableHeaderView {
let targetSize = bounds.size
topView.frame.size = topView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
}
}
```
###### reuse view
主要是解决 UIView-Encapsulated-Layout-Width 和 UIView-Encapsulated-Layout-Height 问题。
* [One solution for 90% of Auto Layout exceptions · aplus.rs](https://link.juejin.cn?target=https%3A%2F%2Faplus.rs%2F2017%2Fone-solution-for-90pct-auto-layout%2F "https://aplus.rs/2017/one-solution-for-90pct-auto-layout/")
* [ios - What is NSLayoutConstraint "UIView-Encapsulated-Layout-Height" and how should I go about forcing it to recalculate cleanly? - Stack Overflow](https://link.juejin.cn?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F25059443%2Fwhat-is-nslayoutconstraint-uiview-encapsulated-layout-height-and-how-should-i "https://stackoverflow.com/questions/25059443/what-is-nslayoutconstraint-uiview-encapsulated-layout-height-and-how-should-i")
* [UIView-Encapsulated-Layout-Width and Height Constraints error \| by Goal 栈 \| Medium](https://link.juejin.cn?target=https%3A%2F%2Fmedium.com%2F%40GoalStack%2Fuiview-encapsulated-layout-width-and-height-constraints-error-10cd25b5fa5e "https://medium.com/@GoalStack/uiview-encapsulated-layout-width-and-height-constraints-error-10cd25b5fa5e")
所以,基本的解法是降低发生冲突方向布局的优先级。但这样会有不确定性,不确定 break 掉的约束会是什么效果。
另一种方案是配置约束时考虑 width 会变成 0 的 case,确保各种约束(如缩进)不会导致某个 view 的 width 为负数。然后在 layoutSubviews 方法中更新约束到目标效果,或干脆直接重建约束。
#### 💬UITableViewCell
###### 设置背景色
`backgroundColor` 无效时,设置 `backgroundView`。目前发现 UITableViewHeaderFooterView 子类设置 `backgroundColor` 无效。
###### 取消高亮
```Swift
selectionStyle = .none
```
不行的话,在 `prepareForReuse` 中也设置下。
#### 💬UIView 生命周期
##### 获得上屏 view
1.
###### init + main.async
要获得显示在屏幕上的 View,最简单粗暴的方式是在初始化的位置,加个 `DispatchQueue.main.async` 闭包。
优点:
* 确保只执行一次
缺点:
* 不确定是否真的布局完成;
* 只适合那种初始化就配置好视图的情况。
1.
###### `didMoveToWindow`
另外,还可以在 `didMoveToWindow()` 方法中写相关的逻辑,这时的 next responder 是能拿到的。
优点:
* 确保已经添加到视图。
* 视图可以在任意时机布局。
缺点:
* 可能会执行多次。
#### 💬布局
##### UIView 如何防止被挤压
UIView 可以通过设置抗压缩和抗拉伸属性来防止被挤压。抗压缩属性表示视图不想缩小到比其内容更小的程度,而抗拉伸属性表示视图不想被拉伸到比其内容更大的程度。可以使用`setContentCompressionResistancePriority(_:for:)`方法设置抗压缩属性,使用`setContentHuggingPriority(_:for:)`方法设置抗拉伸属性。这些方法都需要传入一个优先级参数,优先级越高,视图越不容易被压缩或拉伸。默认的优先级为 750 和 250,可以通过设置更高的优先级来防止视图被挤压。
例如,如果您想防止一个 UILabel 的内容被压缩,可以使用以下代码:
```Swift
label.setContentCompressionResistancePriority(.required, for: .horizontal)
```
如果您想防止一个 UIView 被拉伸,可以使用以下代码:
```Swift
view.setContentHuggingPriority(.required, for: .horizontal)
```
请注意,这些方法只适用于使用 Auto Layout 进行布局的视图。如果您使用的是 Autoresizing Mask,则可以使用`autoresizingMask`属性来设置视图的自动调整大小行为。
`setContentHuggingPriority(_:for:)` 和 `setContentCompressionResistancePriority(_:for:)` 是 Auto Layout 中非常重要的两个方法,它们可以用来控制视图的自适应大小。以下是更详细的介绍和效果:
###### `setContentHuggingPriority(_:for:)`
`setContentHuggingPriority(_:for:)` 方法用于设置视图的抱紧优先级。抱紧优先级决定了视图在自适应大小时的最小大小限制。具体来说,它控制了视图在拉伸时的行为。
* `UILayoutPriority.required`:视图的大小必须等于或大于其内容的最小大小。这是默认的优先级。
* `UILayoutPriority.defaultHigh`:视图的大小可以小于其内容的最小大小,但不能小于其他具有较低抱紧优先级的视图。
* `UILayoutPriority.defaultLow`:视图的大小可以小于其内容的最小大小,并且可以小于其他具有较高抱紧优先级的视图。
例如,在一个水平方向的 UIStackView 中,如果一个视图的抱紧优先级设置为 `.required`,则它的宽度不会小于其内容的最小宽度。如果一个视图的宽度抱紧优先级设置为 `.defaultLow`,则它的宽度可以更小,以适应其父视图的大小。
###### `setContentCompressionResistancePriority(_:for:)`
`setContentCompressionResistancePriority(_:for:)` 方法用于设置视图的压缩阻力优先级。压缩阻力优先级决定了视图在自适应大小时的最大大小限制。具体来说,它控制了视图在压缩时的行为。
* `UILayoutPriority.required`:视图的大小必须等于或大于其内容的最小大小。这是默认的优先级。
* `UILayoutPriority.defaultHigh`:视图的大小可以小于其内容的最小大小,但不能小于其他具有较低压缩阻力优先级的视图。
* `UILayoutPriority.defaultLow`:视图的大小可以小于其内容的最小大小,并且可以小于其他具有较高压缩阻力优先级的视图。
例如,在一个水平方向的 UIStackView 中,如果一个视图的压缩阻力优先级设置为 `.required`,则它的宽度不会小于其内容的最小宽度。如果一个视图的宽度压缩阻力优先级设置为 `.defaultHigh`,则它的宽度可以更小,以适应其父视图的大小。
需要注意的是,抱紧优先级和压缩阻力优先级通常是成对使用的,以确保视图在自适应大小时的行为符合预期。例如,在一个水平方向的 UIStackView 中,一个视图的抱紧优先级设置为 `.required`,压缩阻力优先级设置为 `.defaultHigh`,则它的宽度在拉伸时会尽可能地保持其内容的最小宽度,而在压缩时会尽可能地保持其内容的最大宽度。
参考资料:
1. [AutoLayout - 内容压缩阻力(Content Compression Resistance)和内容吸附(Content Hugging)](https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2Flongshihua%2Farticle%2Fdetails%2F79819108 "https://blog.csdn.net/longshihua/article/details/79819108")
2. [UIView.AutoresizingMask](https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fuikit%2Fuiview%2Fautoresizingmask "https://developer.apple.com/documentation/uikit/uiview/autoresizingmask")
3. [setContentCompressionResistancePriority(_:for:)](https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fuikit%2Fuiview%2F1622526-setcontentcompressionresistancepriority "https://developer.apple.com/documentation/uikit/uiview/1622526-setcontentcompressionresistancepriority")
4. [setContentHuggingPriority(_:for:)](https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fuikit%2Fuiview%2F1622559-autoresizingmask "https://developer.apple.com/documentation/uikit/uiview/1622559-autoresizingmask")
若出现没有自动跟随尺寸变化,检查确保全部使用了 equalTo!!!
##### 布局更新时机
1.
###### `layoutSubviews`
放心在这里更新 auto layout 的约束常量,这不会出发循环调用。
1.
###### `didMoveToWindow`
这是 UI 更新布局的最晚时机,这时 superview、responder 都已经有值,但这时 auto layout 可能还没完成布局,要获得 auto layout 后到布局可以在下一次 runloop 中获取。
这个方法调用时机很巧妙,当 view appear/disappear 的时候也会被调用,因为这时的 window 对象会置为 nil,这时就可以把 controller 生命周期的事情归还到 UIView 中来做。
##### 获取自动布局后的 frame
1.
###### 强制布局
调用 `setNeedsLayout()` + `layoutIfNeeded()`,触发同步布局。然后获取 view 的 frame。
1.
###### 获得布局后的尺寸
调用 `systemLayoutSizeFitting(_:)` 方法,获取基于当前约束的视图的最佳大小。该方法只是做计算而已,并没有进行布局。
`targetSize`:偏好的视图尺寸。要获得尽可能小的视图,设置为 `UIView.layoutFittingCompressedSize`。要获得尽可能大的视图,则设置为 `UIView.layoutFittingExpandedSize`。
```Swift
label.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
```
另外,使用 `UIView.sizeThatFits` 也可达到同样的效果。
```Swift
label.sizeThatFits(.zero)
```
注意这里返回的是 CGSize。
##### 自动布局更新
要控制局部 UI,尽量使用 UIStackView 和约束常量(`NSLayoutConstraint.constant`)来实现布局更新,而不是使用 `snp.remakeConstraints`。而 `snp.updateConstraints` 更不建议使用,因为需要了解之前是怎么布局的,也是只能更新约束常量,且跟之前的布局强强耦合,容易出错,不好维护。
##### 不要尝试给系统的 layout guide 添加约束
UILayoutGuide 的作用如其名,是布局参照,如画图时的辅助线。当使用 layout guide 编写布局约束时,应永远把 layout guide 作为宾语,而不是主语。
```Swift
let contentGuide = scrollView.contentLayoutGuide
// 不能这样做!!
contentGuide.snp.makeConstraints { make in
make.edges.equalTo(label)
}
// 而是改成这样
label.snp.makeConstraints { make in
make.edges.equalTo(contentGuide)
}
```
如果是自建的一个 layout guide,则可以且优先作为主语进行布局,即先画好辅助线,再使用辅助线布局其他视图。
## macOS
#### 命令行工具执行异步代码
相关链接:
* [macos - Using NSURLSession from a Swift command line program - Stack Overflow](https://link.juejin.cn?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F30702387%2Fusing-nsurlsession-from-a-swift-command-line-program "https://stackoverflow.com/questions/30702387/using-nsurlsession-from-a-swift-command-line-program")
* [xcode - How to prevent a Command Line Tool from exiting before asynchronous operation completes - Stack Overflow](https://link.juejin.cn?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F31944011%2Fhow-to-prevent-a-command-line-tool-from-exiting-before-asynchronous-operation-co "https://stackoverflow.com/questions/31944011/how-to-prevent-a-command-line-tool-from-exiting-before-asynchronous-operation-co")
* [How to make async command-line tools and scripts - a free Swift Concurrency by Example tutorial](https://link.juejin.cn?target=https%3A%2F%2Fwww.hackingwithswift.com%2Fquick-start%2Fconcurrency%2Fhow-to-make-async-command-line-tools-and-scripts "https://www.hackingwithswift.com/quick-start/concurrency/how-to-make-async-command-line-tools-and-scripts")
大概有几种方式:
* 阻塞进程,让其不退出。
* run in main runloop.
使用信号量阻塞:
```Swift
var semaphore = DispatchSemaphore(value: 0)
runAsyncTask { // 完成回调
// 释放,退出
semaphore.signal()
}
// 阻塞不退出
semaphore.wait()
```
使用 runloop,run in main runloop:
```Swift
//...your magic here
// add a little 🤓iness to make it fun at least...
RunLoop.main.run(until: Date() + 0x10) //oh boi, default init && hex craze 🤗
// yeah, 16 seconds timeout
// or even worse (!)
RunLoop.main.run(until: .distantFuture)
```
dispatchMain:
```Swift
runAsyncTask { // 完成回调
// 退出
exit(EXIT_SUCCESS)
}
// Run GCD main dispatcher, this function never returns, call exit() elsewhere to quit the program or it will hang
dispatchMain()
```