阿权的开发经验小集

小集是日常开发中遇到问题的小结,或许可以帮助你少走一些弯路~

Git

跟踪上游分支

背景:执行 pull 没拉下新代码,但远端确实是有更新的。

目标:恢复与上游的同步。

YAML 复制代码
git branch -u origin/branch_name

删除分支

目标:本地分支和远程分支一起删除。

Bash 复制代码
# 删除本地分支
git branch -d localBranchName

# 删除远程分支
git push origin --delete remoteBranchName

空提交

背景:有些操作需要通过 git 提交记录的更新来触发,这里通过一个不影响代码的空提交触发。

YAML 复制代码
git commit --allow-empty -m "Empty-Commit"

Tag vs. Branch

背景:Tag 和 Branch 在执行 git 命令时常常不需要显式说明,但当两者同名时就需要显式声明。

Tag 是记录一个 commit 点,Branch 是记录一个 commit 序列串。

  • branch 的特点是该分支的指针的位置随着提交不断更新,一般是存储在 refs/heads/
  • tag 的特点与分支恰恰相反,指向的 commit 不会随着新的提交去更新。一般是存储在 refs/tags/

git merge 可以合并 tag 或 branch。若出现 tag 和 branch 重名的 case,可以通过补全路径处理:

Bash 复制代码
# push
git push origin :refs/heads/branch_name
git push origin :refs/tags/tag_name

# merge
git merge refs/heads/branch_name
git merge refs/tags/tag_name

回退合并

背景:执行了 merge 操作希望回退到 merge 前的 commit。

Bash 复制代码
# 场景:合并操作还没完成,希望中断并回退到合并前的状态。
# 中断当前正在合并还没提交的分支的合并操作
git merge --abort

# 场景:合并操作已完成,甚至已经 push 到远程,希望回退到合并前的状态。
# 回退刚才已经提交的第一个合并
git reset --merge HEAD~1

# 若合并还没 push 到远程,经过上面操作后,分支可能会落后于远程分支,所以还要同步一遍,以确保跟远程分支同步。
git pull

# 场景:需要将本地的覆盖远程分支状态
# force push
git push --force
git push --force-with-lease # 更安全,会检查远程是否有新提交(有则拒绝 push)

Rebase vs. Merge

merge rebase
作用 创建一个新的 "合并提交"(merge commit),将两个分支的历史记录连接起来,保留双方完整的提交历史(包括分支的分叉和合并节点)。 将当前分支的所有提交 "移植" 到目标分支的最新提交之后,改写当前分支的提交历史,使历史呈现线性(无合并提交)。
优点 完整保留操作分支的所有提交历史,仅新增一个合并提交。遇到冲突只需解决一次。 历史记录整洁,合并后历史呈线性,没有多余的合并提交。rebase 过程中可以通过 --interactive(交互式)对提交进行压缩、修改、删除,让历史更清晰(例如将多个 "修复 bug" 的小提交合并为一个有意义的大提交)。
缺点 频繁合并会产生大量 "合并提交",主分支历史可能出现很多分叉节点,长期来看难以快速理解项目演进脉络。不能对当前分支的提交进行压缩、修改(如需优化提交历史,需额外操作)。 rebase 会修改当前分支的提交哈希,因为提交被 "移植" 到了新的基础上。 甚至会修改提交顺序。可能需要多次解冲突,如果多个提交与目标分支有冲突,需要逐个提交解决冲突。修改提交顺序后会引入更多冲突。
选择 新手优先 整洁优先

选择:

多人共用一分支 单人单分支
保留完整分支历史 merge rebase
分支历史不重要 rebase rebase

为了平衡提交历史的简洁性与准确性(少点冲突),合并代码时可以这样做:先使用 rebase,遇到冲突时 abort 回退,切换为 merge,然后解冲突。

注意:主分支被团队所有人依赖,应尽可能使用 merge。

从分支维度:可以简单约定合并策略,主分支用 merge,功能分支用 rebase,来平衡两者的优缺点。

解冲突最佳实践

基本常识:

  • HEAD/ours 是指自己的改动;origin/theirs 是指上游的改动。

操作原则:

  • 只有是自己写的才应用自己的改动,否则应用上游的改动。
  • 存疑的(自己写的混合了他人写的,应用上游后编译不过)应保留两者的修改,对于不确定要丢弃的代码用段落注释(合并代码少用行注释)。

最佳实践:

  1. 处理资源/二进制文件,简单选择用自己的还是用上游的。
  2. 处理文本:以行甚至段落为单位,选用自己的还是上游的版本。
  3. 处理存疑文本修改:以行为单位,不选用自己和上游的版本,直接编写预期的文本。
  4. 对解完冲突的文件暂存修改(git add)。
  5. 解完 git 仓库本次所有冲突后,提交修改(git commit)。

推荐工具:vscode。使用 vscode 打开 git 仓库,并用其解冲突。

修改作者信息

背景:提交完代码了才发现用错了邮箱提交,例如:外部仓库使用了公司邮箱提交了代码,需要改用私人邮箱。

Git 会为每一次提交记录提交者的姓名和邮箱,这是本地 Git 配置的 "身份标识",用于区分不同开发者的提交。

如何修改:

  1. git log 查看 commit id
  2. git rebase -i <最早commit> 重新设置基准线
  3. git commit --amend --author="Author Name autolinkemail@address.comautolink"来修改 commit
  4. ``git rebase --continue` 移动到下个 commit 作为基准线

例子:如当前历史为 A-B-C(HEAD),我想修改 B 和 C,这两个 commit 的作者。

  1. git rebase -i A,即要修改的最早提交的前一个节点。
    1. 如果想改 A 则使用git rebase -i --root
  2. pick 改为 edit。按 ESC,输入:wq。保存修改。
  3. 现在你已经开始可以修改,此时当前 commit 为 B。
  4. git commit --amend --author="Author Name autolinkemail@address.comautolink" 修改 B 的提交。
  5. git rebase --continue 定位到 C
  6. git commit --amend --author="Author Name autolinkemail@address.comautolink" 修改 C 的提交。
  7. git rebase --continue 修改已完成。
  8. git push -f 提交代码,大功告成。

文件修改检测不到

背景:本地文件有修改,但 Git 检测不到了。重启似乎就可以检测得到,但只能一次有效。

排查:去检查文件所在的路径,与 Git 识别的路径是否有大小写的差异。Git 区分大小写差异,但系统不区分,所以会有 gap,目录名只改了大小写,会导致一些奇怪的问题。

解决:把有大小写的路径段重命名。改大小写名称时,先重命名为临时名,再改为正确的大小写,分两次提交以避免文件系统的不识别。

iOS

留心延迟执行的代码

代码里看到延时执行要谨慎,非常可能是枯叶掩埋的陷阱。

延时执行可能是能解决作者提交时遇到的问题。但随着业务发展,可能后续那次修改后,延时执行就兜不住了。

  1. 首先自己不要写延时执行代码,不要期望延时能根治某个问题,延时能绕过的问题一般是执行时机、时序问题,应找到合适的时机执行逻辑。
  2. 其次看到别人写的延时代码要十分谨慎,可以先不去改别人写的延时代码,但尽可能不要依赖延时执行的时机做后续的逻辑,应自己找到合适的时机编写自己的代码。

主队列执行时序问题

Swift 复制代码
public extension DispatchQueue {
    private static var token: DispatchSpecificKey<Void> = {
        let key = DispatchSpecificKey<Void>()
        DispatchQueue.main.setSpecific(key: key, value: ())
        return key
    }()
    
    static var isMain: Bool {
        DispatchQueue.getSpecific(key: Base.token) != nil
    }
    
    static func onMainQueue(_ work: @escaping @convention(block) () -> Void) {
        if isMain {
            work()
        } else {
            DispatchQueue.main.async(execute: work)
        }
    }
}

通过标记 DispatchQueue.main 队列可以准确判断当前执行的队列是否是主队列。使用 onMainQueue 方法可以确保让任务在主线程和主队列中执行。这个做法在不要求时序的场景下,确实是最保险的。要保证时序性,就要重新思考了。

主队列只能在主线程中执行。主线程是 runloop 机制,DispatchQueue.main.async 就是把任务(一段代码)放入到下一个 runloop 中执行。主线程还会执行其他队列,如在主线程中 sync 执行一个普通 serial queue,这个 queue 也是在主线程中执行,但就不是主队列了,上面的 isMain 方法会判断为 false。如果这种 case 在需要任务按照严格时序执行的场景下,就会出现时序错乱的问题。因为这里会把一些在主线程但不在主队列的任务错误地放置到下一个 runloop 中执行。

相反要考虑时序性,只需要使用 Thread.isMainThread 就能准确识别当前是否是主线程了,绝大数 UI 场景都适用。

Swift 复制代码
public extension UtilExtension where Base: DispatchQueue {
    static func onMainThread(_ work: @escaping @convention(block) () -> Void) {
        if Thread.isMainThread {
            work()
        } else {
            DispatchQueue.main.async(execute: work)
        }
    }
}

使用锁,最终目的是为了解决竞态条件。

相关链接:

锁从基本原理上可分为互斥锁和自旋锁,其他类型的锁如:条件锁、递归锁、信号量,甚至是 GCD 的队列都是基于这两个基本锁的封装或扩展。

互斥锁 Mutex Lock 自旋锁 Spin Lock
原理 当线程尝试获取锁时,若锁已被占用,该线程会进入休眠状态(阻塞),直到锁被释放后被唤醒。 线程在获取锁失败时不会休眠,而是通过循环(忙等待)不断检查锁状态。
特性 互斥锁会休眠线程,避免了 CPU 空转,但涉及线程上下文切换,可能带来性能开销。适合高竞争或长时间持有。 自旋锁保持线程活跃,避免了上下文切换,但长时间等待会消耗 CPU 资源,适用于锁持有时间短的场景。适合短时间锁竞争。
具体实现 不可重入锁(非递归锁):线程必须释放锁后才能再次获取,否则会死锁。NSLockpthread_mutex(默认模式)可重入锁(递归锁) :允许同一线程多次获取同一锁而不死锁。NSRecursiveLock@synchronized条件锁:基于条件变量实现,线程需等待特定条件满足后才能继续执行。NSConditionNSConditionLock,需与互斥锁配合使用。 iOS 中早期使用OSSpinLock,但因优先级反转问题被废弃;现推荐使用os_unfair_lock作为轻量级替代。读写锁:允许并发读取(多个读线程),但写入时需独占资源。属于自旋锁的特殊形式,例如pthread_rwlock
信号量

通过计数器控制并发访问数量,底层可能依赖互斥锁实现,所以如果重入会死锁

Swift 复制代码
class Lock {
    private let semaphore = DispatchSemaphore(value: 1)
    func lock() {
        semaphore.wait()
    }
    func unlock() {
        semaphore.signal()
    }
    @inline(__always)
    final func performLocked<T>(_ action: () -> T) -> T {
        self.lock(); defer { self.unlock() }
        return action()
    }
}
同步队列

同步队列通过在一个串行队列中执行操作,也可以实现资源安全访问。

同步执行在一段时间内不会切换线程,异步执行会切线程,但在队列执行的任务还是串行的。这个这个特性,可以实现异步锁。但就会发生上下文切换,即线程切换。

Xcode

手动安装模拟器

背景:新安装 Xcode 时总要额外下载一个与该 Xcode 版本匹配的模拟器,这个过程总是很久。可以试试手动下载。

  1. 官网下载 Xcode 对应的模拟器版本:developer.apple.com/download/al...
  2. 执行命令:
Bash 复制代码
# 需要先选定操作的 Xcode
sudo xcode-select -s /Applications/Xcode_16.1.app
xcodebuild -runFirstLaunch
xcodebuild -importPlatform "$HOME/Downloads/iOS_18.1_Simulator_Runtime.dmg"

启动 Xcode 即可。

安装系统不支持的 Xcode 版本

背景:新系统不能打开旧 Xcode。

Bash 复制代码
plutil -replace CFBundleVersion -string 30000 /Applications/Xcode.app/Contents/Info.plist

查找 setter

背景:希望找到某属性所有修改的地方。

可以将属性改写成计算属性,这样就可以单独查找 setter 的调用栈。

转码控制台中的 JSON

背景:Xcode 控制台输出 json 常常是转义过的,配合 vscode 可以还原出原始的 json。

拷贝到 vscode,结合 Text Power Tools 插件,使用 json 解析。

  1. 去除头尾到双引号。
  2. 右键:Text Power Tools > Encode/decode > Unescape JSON escaped text

Swift 语言

KVO 备忘

背景:Swift 的 KVO 语法常常检索不到。

Swift 复制代码
// 定义可 KVO 监听的属性变量
@objc dynamic var myDate

// 监听,options 若不设置,change 的 oldValue、newValue 为 nil
observation = observe(\.objectToObserve.myDate, options: [.old, .new]) { object, change in
    print("myDate changed from: \(change.oldValue!), updated to: \(change.newValue!)")
}

Using Key-Value Observing in Swift | Apple Developer Documentation

枚举语义

  • enum 表达互斥的
  • 表达常量或常量表达式,其关联值都是常量,都需要构造的时候确定。
  • indirect 修饰 case 或 enum 可以在关联值使用自身类型,即表达递归语义。常用于常量表达式的表达。

特性:

  • 便捷的构造,直接点语法直接构建。类比到 struct/class 的静态方法/属性。
  • 关联值可忽略标签,直接用类型表达。

数组在遍历中删除元素

背景:遍历数组并删除元素一不小心就会数组越界。

可以通过以下方式规避:

  1. 使用高阶函数直接创建/修改一个符合条件的数组。如 filterremoveAll(where:)
  2. 反向遍历,可以安全地按索引删除元素,如 reversed()

枚举 raw value 不能是表达式

枚举 raw value 在定义的时候等号右侧不可以是表达式,而是一个字面常量,不可加条件。

读取大文件

可以使用 FileHandleInputStream 来读取大文件。

它们之间存在一些主要的不同:

  1. 使用方式InputStream 是基于流的,可以连续读取数据,这对于处理大文件或网络数据非常有用,因为你不需要一次性将所有数据加载到内存中。另一方面,FileHandle 允许你更精细地控制文件访问,例如,你可以选择从文件的任何位置开始读取或写入数据。
  2. 数据处理 :使用 InputStream 时,你需要自己处理数据缓冲区的分配和释放。使用 FileHandle 时,你可以直接获取 Data 对象,而无需关心底层的内存管理。
  3. 可用性InputStream 可以处理来自各种来源的数据,如文件、网络数据或内存中的数据。而 FileHandle 主要用于文件操作。
  4. 错误处理InputStream 有一个 streamError 属性,可以用来检查在读取或写入过程中是否发生错误。FileHandle 的方法则会抛出异常,需要使用 trycatch 来处理。

InputStream 使用实例:github.com/gonsolo/gon...

Swift 复制代码
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()
}

省略 inout 参数

背景:inout 参数是不能设置默认值的,但有时候想让其成为可选参数。

把 inout 参数改成 UnsafeMutablePointer 类型可以做成像默认参数的省略用法,如:

Swift 复制代码
func checkIfSupport(draft: Data, isSingle: inout Bool) -> Bool
func checkIfSupport(draft: Data, isSingle: UnsafeMutablePointer<Bool>? = nil) -> Bool

参考:option type - Swift optional inout parameters and nil - Stack Overflow

不建议在 extension 中重写

swift2 - Overriding methods in Swift extensions - Stack Overflow

使用 @objc 修饰的方法即使定义在 extension 中,也能被重写。@objc 可以直接修饰 extension。类似的,NSObject 子类定义的 objc 方法也可以在 extension 中重写。

Swift 复制代码
// MARK: Override
extension ExportViewControllerNew {
    override var preferredStatusBarStyle: UIStatusBarStyle {
        .lightContent
    }
}

这样写会把方法暴露给 Runtime。

但不太建议这么做,似乎不太正统的方式。需要重写的方法还是应放到类的定义中。

Decodable 详细使用

定義 Decodable 的 init(from:) 解析 JSON | by 彼得潘的 iOS App Neverland | 彼得潘的 Swift iOS App 開發問題解答集 | Medium

我在想,为什么不用 ObjectMapper 呢?

weak 对象所在的作用域结束后还不销毁

对于 Swift 中的对象,其销毁时机与作用域有关,但不是唯一决定因素。对象的生命周期是由引用计数(reference counting)管理的。当一个对象的强引用计数降至零时,该对象会被销毁。以下是一些可能导致对象未在作用域结束时被销毁的情况:

  1. 强引用计数:当对象的作用域结束时,如果对象的强引用计数不为零,对象不会被立即销毁。这可能是因为在作用域外还有其他地方保持着对该对象的强引用。
  2. 强引用循环:当对象之间存在强引用循环时,即使它们的作用域已经结束,对象也不会被销毁。强引用循环会导致内存泄漏,因为对象互相保持强引用,使得它们的引用计数永远不会降至零。这时,需要使用 weakunowned 关键字来解决强引用循环问题。
  3. 延迟释放:Swift 使用自动引用计数(ARC)来管理内存。ARC 通常在对象不再需要时立即释放内存,但在某些情况下,ARC 可能会延迟释放对象。这种延迟释放可能会导致对象在作用域结束后仍然存在。

虽然作用域对于对象的销毁有一定影响,但对象的生命周期主要还是由引用计数管理。因此,在编写 Swift 代码时,需要特别注意避免强引用循环和内存泄漏。

获取代码位置

Swift 复制代码
"\(#function) @\(#fileID):\(#line):\(#column)"

类判等

对于类实例,判断是否相同,可以简单以地址区分,使用 === 运算符比较。

Swift 复制代码
if b === Test.self {
    print("yes")
} else {
    print("no")
}

ios - Comparing types with Swift - Stack Overflow

打印地址

有时候我们想直接打印对象的地址,可以这么做:

Swift 复制代码
// 方式一
let s = Struct() // Struct
withUnsafePointer(to: s) {
    print(String(format: "%p", $0)
}

// 方式二
func printPointer<T>(ptr: UnsafePointer<T>) {
    print(ptr)
}
printPointer(ptr: &x)

// 方式三
///
/// http://stackoverflow.com/a/36539213/226791
///
func addressOf(_ o: UnsafeRawPointer) -> String {
    let addr = unsafeBitCast(o, to: Int.self)
    return String(format: "%p", addr)
}

func addressOf<T: AnyObject>(_ o: T) -> String {
    let addr = unsafeBitCast(o, to: Int.self)
    return String(format: "%p", addr)
}
  
// 方式三
Unmanaged.passUnretained(self).toOpaque()

参考:

获取磁盘空间

背景:快速获取与系统设置计算方式一致的剩余磁盘空间。

Swift 复制代码
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 符号断点似乎要重新编译? 否则不生效? ![img](https://oss.xyyzone.com/jishuzhan/article/1967890176325763073/dd4b639aec10596ff97d3d6c0a904338.webp) #### 💬注释 ##### 文档注释标记 一般规则:`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)展示的目录有两种类型,在引入之初就决定了: ![img](https://oss.xyyzone.com/jishuzhan/article/1967890176325763073/e19fb04d576b8f33cab4e9a525a08eb4.webp) | | group | folder reference | |------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------| | 使用场景 | 最常用。代码、资源引入无脑选它。 | 蓝色图标。仅用于资源,如 bundle 资源。 | | 细分 | ![img](https://oss.xyyzone.com/jishuzhan/article/1967890176325763073/ae83c6b0e4f397be5b4ca064bf0c5ff8.webp)对应目录的 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 嵌套类型 ![img](https://oss.xyyzone.com/jishuzhan/article/1967890176325763073/ded945740be1c98ceb2cc12154b5d266.webp) 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() ```

相关推荐
库奇噜啦呼26 分钟前
【iOS】简单的四则运算
macos·ios·cocoa
19岁开始学习6 小时前
Go语言中的Zap日志库
开发语言·golang·xcode
white-persist10 小时前
【burp手机真机抓包】Burp Suite 在真机(Android and IOS)抓包手机APP + 微信小程序详细教程
android·前端·ios·智能手机·微信小程序·小程序·原型模式
lingxiao1688816 小时前
Git常规应用
git
唐叔在学习18 小时前
【Git神技】三步搞定指定分支克隆,团队协作效率翻倍!
git·后端
稚辉君.MCA_P8_Java19 小时前
Git 基础 - 查看提交历史
spring boot·git·微服务·云原生·kubernetes
恋猫de小郭1 天前
Fluttercon EU 2025 :Let‘s go far with Flutter
android·开发语言·flutter·ios·golang
海上生明月丿1 天前
在IDEA中使用Git
java·git·intellij-idea
三口吃掉你1 天前
Git分布式版本控制工具
分布式·git
2501_915909061 天前
iOS 抓包工具有哪些?实战对比、场景分工与开发者排查流程
android·开发语言·ios·小程序·uni-app·php·iphone