尽量避免将方法的引用传递给一个接受@escaping闭包的函数
www.swiftwithvincent.com/blog/bad-pr...
错误代码
publisher.sink(receiveValue: handle(value:)).store(in: &cancellables)
等价于
publisher.sink(receiveValue: {self.handle(value:$0)}).store(in: &cancellables)
编译器自动捕获了self,造成了循环引用self->publisher->closuer->self
正确代码
swift
class ViewModel {
var cancellables = Set<AnyCancellable>()
init() {
publisher
.sink(receiveValue: {[weak self] in
guard let self else { return }
self.handle(value:$0)}
)
.store(in: &cancellables)
}
func handle(value: String) {
// `self` can be used here
}
}
不要一有机会就让异步代码执行
www.swiftwithvincent.com/blog/discov...
错误代码
以下代码的写法导致getUserName
和getUserPicture
同步执行。
只有getUserName
执行完毕才会执行getUserPicture
swift
import Foundation
Task {
let userName = await getUserName()
let userPicture = await getUserPicture()
updateUI(userName: userName, userPicture: userPicture)
}
正确代码
采用结构化并发语法 async let,在需要使用结果的地方再await
这样能保证 getUserName
和 getUserPicture
并发执行,同时去拿结果
swift
import Foundation
Task {
async let userName = getUserName()
async let userPicture = getUserPicture()
await updateUI(userName: userName, userPicture: userPicture)
}
字符串判空多使用isEmpty
而不是count > 0
www.swiftwithvincent.com/blog/bad-pr...
错误代码
使用count
方法判空,在底层会遍历整个字符串获取其长度。
当遇到超长字符串或者有很多字符的字符串时它的耗时会很长,影响性能
swift
if myString.count > 0 {
// `myString` isn't empty
}
正确代码
使用isEmpty
只需要判断是否至少包含一个字符,不受字符串长度的影响
swift
if myString.isEmpty == false {
// `myString` isn't empty
}
由单个元素构成的Array,多使用CollectionOfOne
而不是手动构建
www.swiftwithvincent.com/blog/discov...
错误代码
swift
import Foundation
let someNumbers = [1, 2, 3]
let moreNumbers = someNumbers + [4]
正确代码
在使用for循环的操作中CollectionOfOne
有更好的性能
swfit
import Foundation
let someNumbers = [1, 2, 3]
let moreNumbers = someNumbers + CollectionOfOne(4)
善于使用#error
控制一些必须修改的代码
www.swiftwithvincent.com/blog/discov...
正确代码
#error
可以让编译器在编译期就暴露问题代码
swift
import Foundation
#error("You can get your apiKey at https://dev.myapi.com/")
let apiKey = "create_your_own_api_key"
对于大数的表示,善于使用分割符
www.swiftwithvincent.com/blog/discov...
let bigNumber = 123_456_789
写好Swift代码的三条提示
www.swiftwithvincent.com/blog/3-tips...
考虑使用多行字符串语法输出字符串
swift
// #01 -- Multiline String
let multilineString = """
1st line
2nd line
3rd line
"""
单个泛型参数的函数考虑使用some
swift
// #02 -- Opaque Arguments
func handle(value: some Identifiable) {
/* ... */
}
对于会抛出错误的单元测试,考虑将case函数修改为throws,在case函数内通过try调用需要覆盖的函数
swift
// #03 -- Throwing Tests
class Test: XCTestCase {
func test() throws {
XCTAssertEqual(try throwingFunction(), "Expected Result")
}
}
考虑使用带有关联类型的enum重构互斥的逻辑
www.swiftwithvincent.com/blog/how-to...
错误代码
swift
// 错误1 各种属性杂糅在一起
struct BlogPost {
// Common properties
var title: String
// Article properties
var wordCount: Int?
// Video properties
var videoURL: URL?
var duration: String?
}
// 错误2 分离了Video和Article 但并未给出互斥逻辑
struct ArticleMetadata {
var workdCount: Int
}
struct VideoMetadata {
var videoURL: URL
var duration: String
}
struct BlogPost {
var title: String
var articleMetadata: ArticleMetadata?
var videoMetadata: VideoMetadata?
}
正确代码
swift
// 带有关联类型的枚举
enum Metadata {
case article(wordCount: Int)
case video(videoURL: URL, duration: String)
}
// 视频和文章是互斥的
struct BlogPost {
var title: String
var metadata: Metadata
}
let correctBlogPost = BlogPost(
title: "My Awesome Video",
metadata: .video(
videoURL: URL(string: "https://mywebsite.com/myVideo.mp4")!,
duration: "4:35"
)
)
考虑使用dump打印引用类型,使用print打印值类型
www.swiftwithvincent.com/blog/discov...
错误代码
swift
class Person {
init(name: String, age: Int) {
self.name = name
self.age = age
}
var name: String
var age: Int
}
let me = Person(name: "Vincent", age: 32)
print(me)
正确代码
swift
class Person {
init(name: String, age: Int) {
self.name = name
self.age = age
}
var name: String
var age: Int
}
let me = Person(name: "Vincent", age: 32)
dump(me)
对于需要在主线程执行的结构,考虑使用 @MainActor
www.swiftwithvincent.com/blog/discov...
正确代码
@MainActor
可以使用在类型、方法、闭包中等地方。- 使用了
@MainActor
之后,编译器会保证所需要的操作是运行在主线程 @MainActor
只会在使用了Swift并发的异步代码中有效,对于我们自己写的completionHandler或者使用了Combine的代码,它是无效的。需要我们手动切换
swift
import Foundation
@MainActor
class ViewModel: ObservableObject {
@Published var text = ""
func updateText() {
Task {
let newText = await fetchFromNetwork()
// guaranteed to run on the Main Thread
text = newText
}
}
}
@MainActor
class ViewModel: ObservableObject {
@Published var text = ""
func updateText() {
fetchFromNetwork { [weak self] newText in
// ⚠️ @MainActor has no effect here
// 需要我们切换到主线程执行以下代码
self?.text = newText
}
}
}
使用async/await
时的3个提醒
www.swiftwithvincent.com/blog/three-...
不要一有可能就运行异步代码
错误代码
下一个await只有等上一个await完成之后才开始执行
swift
import Foundation
// the next call starts only after the previous one has finished.
let user = await getUser()
let address = await getAddress(of: user)
// 在 getAddress 执行完毕之后,getPaymentMethod才会执行
let paymentMethod = await getPaymentMethod(of: user)
print("\(address) \(paymentMethod)")
正确代码
使用结构化语法async let,能保证多个异步操作同时执行
swift
import Foundation
// #01 -- Not running code concurrently when possible
// This call will run first...
let user = await getUser()
// ...and after it has completed, the
// two others will then run concurrently
async let address = getAddress(of: user)
async let paymentMethod = getPaymentMethod(of: user)
// 只在使用结果的地方用await关键字
await print("\(address) \(paymentMethod)")
要时刻记住:Task会自动捕获self。要注意循环引用
错误代码
- Task自动捕获了self的引用
- await notification这个for循环一直在监听AsyncSequence notifications,而AsyncSequence并不会停止
- 只要AsyncSequence不停止,Task就不会退出,Task不退出它所捕获的self就不会被释放,从而造成内存泄漏
swift
@MainActor
class ViewModel {
func handle(_ notification : Notification) {
// do something with the `notification`
}
func listenToNotifications() {
Task {
let notifications = NotificationCenter.default.notifications(
named: UIDevice.orientationDidChangeNotification
)
for await notification in notifications {
// 这里使用handle必然需要有self
// 这里实际上捕获了self
handle(notification)
}
}
}
}
正确代码
- 使用捕获列表,在Task里捕获weak self
- 因为造成循环引用的根本在于AsyncSequence不停止,所以我们需要在for in循环中解包self,打破async for in的无限循环。
swift
// #02 -- Not understanding that `Task` automatically captures `self`
@MainActor
class ViewModel {
func handle(_ notification : Notification) {
// do something with the `notification`
}
func listenToNotifications() {
Task { [weak self] in
// 如果在这里解包self,依然无法使Task停止
// guard let self else { return }
// Here the `Task` still holds a local
// strong reference to `self` forever 😱
let notifications = NotificationCenter.default.notifications(
named: UIDevice.orientationDidChangeNotification
)
for await notification in notifications {
guard let self else { return }
self.handle(notification)
}
}
}
}
在需要捕获上下文的地方建议使用Task,否则使用Task.detached(大多数情况下)
错误代码
Task.detached
会忽略所有上下文- 使用
Task.detached
后,listenToNotifications将不会运行于异步上下文。需要在调用async方法的地方使用await关键字
swift
@MainActor
class ViewModel {
func handle(_ notification : Notification) {
// do something with the `notification`
}
func listenToNotifications() {
Task.detached { [weak self] in
let notifications = await NotificationCenter.default.notifications(
named: UIDevice.orientationDidChangeNotification
)
for await notification in notifications {
guard let self else { return }
await self.handle(notification)
}
}
}
}
正确代码
Task
继承了MainActor的上下文,所以Task内不需要再await self.handle
swift
// #03 -- Using `Task.detached` when not needed
@MainActor
class ViewModel {
func handle(_ notification : Notification) {
// do something with the `notification`
}
func listenToNotifications() {
Task { [weak self] in
let notifications = NotificationCenter.default.notifications(
named: UIDevice.orientationDidChangeNotification
)
for await notification in notifications {
guard let self else { return }
self.handle(notification)
}
}
}
}
多考虑使用LazySequence,尤其是在大量的CPU操作时
www.swiftwithvincent.com/blog/discov...
错误代码
- 不使用lazy,整个代码需要执行完10000次
- 我们其实只需要执行15次就可以,其余的9985次完全是无意义的。而且还消耗CPU
swift
import Foundation
(1...10_000)
.map { $0 * $0 } // executed 10000 times
.filter { $0.isMultiple(of: 5) } // executed 10000 times
.first (where: { $0 > 100 })
正确代码
这样只需要执行最初的15次就可以
swift
import Foundation
(1...10_000)
.lazy
.map { $0 * $0 } // executed 15 times
.filter { $0.isMultiple(of: 5) } // executed 15 times
.first (where: { $0 > 100 })
使用Optionals时的3个体型
www.swiftwithvincent.com/blog/three-...
要理解?和!的区别
- ?被叫做:可选链,有值时就是那个值,否则是nil
- !被叫做:强制解包,有值时就是那个值,否则崩溃
swift
// #01 - Not understanding the difference between `?` and `!`
let optionalString: String? = Bool.random() ? "Hello, world!" : nil
// Optional Chaining
optionalString?.reversed() // will return `nil` if `optionalString` is `nil`
// Force Unwrapping
optionalString!.reversed() // will crash if `optionalString` is `nil`
对于可选类型,多使用可选绑定而不是判断是不是nil
- 使用可选绑定可以避免后期if条件改变时的问题
swift
// #02 -- Not using Optional Binding
if let optionalString {
// `optionalString` is now of
// type `String` inside this scope
print(optionalString.reversed())
}
不要任何地方都使用可选。确认有值的地方就不要使用可选值
swift
import Foundation
// #03 - Using an Optional when it is not needed
struct Person {
let id: UUID
let name: String
// 初始化方法的name已经是个确定值,name就不用声明成String?
init(name: String) {
self.id = UUID()
self.name = name
}
}
对可选值进行单元测试时尽量使用XCTUnwrap,而不是自己解包判断
www.swiftwithvincent.com/blog/discov...
swift
import XCTest
class MyTests: XCTestCase {
func test() throws {
let myData = [1, 2, 3]
let first = try XCTUnwrap(myData.first)
XCTAssert(first<3)
}
}
使用Closure时的3个提醒
www.swiftwithvincent.com/blog/three-...
闭包捕获值时是捕获变量,无论该值时值类型还是引用类型
- 闭包默认会捕获值的变量,无论是值类型还是引用类型,只要外部通过变量修改了值,闭包内也会相应修改
swift
var someInteger = 2
let closure = { in
print(someInteger)
}
someInteger = 3
closure() // prints "3"
- 可以通过捕获列表捕获值,此时捕获的值是闭包创建时外部变量的值
swift
// #01 - Capturing a Variable
var someInteger = 2
let closure = { [someInteger] in
print(someInteger)
}
someInteger = 3
closure() // prints "2"
注意闭包引起的循环引用及内存泄漏问题
- ViewController持有Timer,Timer持有闭包,闭包持有self,造成了循环引用
- 通过捕获列表捕获weak self来打破循环引用
swift
// #02 -- Retain Cycles
import UIKit
class ViewController: UIViewController {
var timer: Timer?
let label = UILabel()
let formatter = DateFormatter()
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
guard let self else { return }
let now = Date()
self.label.text = self.formatter.string(from: now)
}
}
}
注意区分 escaping 和 nonescaping的闭包
- 闭包分为逃逸闭包(escaping)和非逃逸闭包(nonescaping)
- 逃逸闭包有可能造成循环引用,非逃逸闭包会在方法体内被消耗,不会造成循环引用
swift
import Foundation
// 03 - Escaping and non-escaping closures
extension ViewController {
func decorateTimeWithEmojis() -> [String] {
["⏲️", "⏰", "⏳"].map { emoji in
let now = Date()
return "\(emoji) \(self.formatter.string(from: now))"
}
}
}
对于枚举类型,最好实现CaseInterable
www.swiftwithvincent.com/blog/discov...
对于实现了CaseIterable协议的非关联枚举,编译器可以自动生成allCases静态属性
swift
import Foundation
enum Direction: CaseIterable {
case north
case south
case east
case west
}
Direction.allCases // [.north, .south, .east, .west]
多使用Swift中的泛型,对逻辑进行抽象
www.swiftwithvincent.com/blog/discov...
使用泛型可以对逻辑进行抽象,而不用关心具体的类型
swift
struct Stack<Element> {
private var values = [Element]()
mutating func push(_ value: Element) {
values.append(value)
}
mutating func pop() -> Element? {
return values.removeLast()
}
}
let stackOfInt = Stack<Int>()
let stackOfString = Stack<String>()
let stackOfPerson = Stack<Person>()
善于使用PropertyWrapper来封装简单的固有逻辑
www.swiftwithvincent.com/blog/discov...
swift
@propertyWrapper
struct Trimmed {
var string: String
init(wrappedValue: String) {
self.string = wrappedValue
}
var wrappedValue: String {
get {
string.trimmingCharacters(
in: .whitespacesAndNewlines
)
}
set {
string = newValue
}
}
}
import Foundation
struct API {
@Trimmed var url: String
}
var api = API(url: "https://myapi.com/ ")
URL(string: api.url) // valid URL ✅
关于KeyPath你不知道的
www.swiftwithvincent.com/blog/5-thin...
KeyPath可以作为闭包或函数传递给高阶函数的参数
编译器会自动将KeyPath转换成闭包
swift
struct Person {
let name: String
var age: Int
}
let people = [
Person(name: "John", age: 30),
Person(name: "Sean", age: 14),
Person(name: "William", age: 50),
]
people.map { $0.name }
people.map(\.name)
有多种类型的KeyPath
swift
// if struct Person
let readOnlyKeyPath = \Person.name // KeyPath<Person, String>
let readWriteKeyPath = \Person.age // WritableKeyPath<Person, Int>
// if class Person
let readWriteKeyPath = \Person.age // ReferenceWritableKeyPath<Person, Int>
KeyPath可以作为下标操作符
swift
struct Person {
let name: String
var age: Int
}
let people = [
Person(name: "John", age: 30),
Person(name: "Sean", age: 14),
Person(name: "William", age: 50),
]
let subscriptKeyPath = \[Person].[1].name
people[keyPath: subscriptKeyPath] // "Sean"
使用KeyPath写简单的DSL
swift
func > <Root, Value: Comparable>(
_ leftHandSide: KeyPath<Root, Value>,
_ rightHandSide: Value
) -> (Root) -> Bool {
return { $0[keyPath: leftHandSide] > rightHandSide }
}
people.filter(\.age > 18)
将KeyPath作为动态成员查找的参数
来看一下怎么直接访问Order中的address的属性,就好像它是Order的属性一样
swift
import Foundation
struct Address {
let city: String
let country: String
}
@dynamicMemberLookup
struct Order {
let customer: Person
let address: Address
/* ... */
subscript<T>(dynamicMember keyPath: KeyPath<Address, T>) -> T {
address[keyPath: keyPath]
}
}
let order = Order(
customer: Person(name: "Vincent", age: 32),
address: Address(city: "Lyon", country: "France")
)
order.city // equalivalent to `order.address.city`
order.country // equalivalent to `order.address.country`
对于异步操作,使用数据结构时考虑使用Actor
www.swiftwithvincent.com/blog/discov...
actor内部会帮助我们处理数据竞争,从而减少代码量
swift
actor ImageCache {
private var cache = [UUID: UIImage]()
func save(image: UIImage, withID id: UUID) {
cache[id] = image
}
func getImage(for id: UUID) -> UIImage? {
cache[id]
}
}
let imageCache = ImageCache()
Task.detached {
await imageCache.save(image: firstImage, withID: firstImageID)
}
Task.detached {
await imageCache.save(image: secondImage, withID: secondImageID)
}
let cachedImage = await imageCache.getImage(for: firstImageID)
异步代码,有条件就是用async、await吧
www.swiftwithvincent.com/blog/discov...
async、await是Swift异步编程的基础。
它基于协程,比现有的基于线程的handler有更好的性能,同时使用也更简单
swift
// Synchronous functions
func add(_ first: Int, _ second: Int) -> Int {
return first + second
}
func longToExecute() -> Int {
var result = 0
for i in 0...1_000_000 {
result += i
}
return result
}
// Asynchronous function
func loadFromNetwork() async -> Data {
let url = URL(string: "https://myapi.com/endpoint")!
let (data, _) = try! await URLSession.shared.data(from: url)
return data
}
// Calling `async` functions
func anAsyncFunction() async {
await anotherAsyncFunction()
}
func aSynchronousFunction() {
Task {
await anAsyncFunction()
}
}
多使用协议编程的思想,提高抽象能力
www.swiftwithvincent.com/blog/discov...
- 这里我们抽象了Servicing协议,并分别实现了Service和MockedService用于不同的目的
- 使用协议编程可以解耦和提高扩展性
swift
class Service: Servicing {
func getData(
_ completion: @escaping (Result<String, Error>) -> Void
) {
/* some networking code */
}
}
class ViewModel: ObservableObject {
@Published var data: String? = nil
@Published var error: Error? = nil
private let service: Servicing
init(service: Servicing) {
self.service = service
}
func fetchData() {
service.getData { [weak self] result in
switch result {
case .success(let data):
self?.data = data
case .failure(let error):
self?.error = error
}
}
}
}
class MockedService: Servicing {
var getDataCallCounter = 0
var result: Result<String, Error>!
func getData(
_ completion: @escaping (Result<String, Error>) -> Void
) {
getDataCallCounter += 1
completion(result)
}
}
final class ViewModelTests: XCTestCase {
func testSuccessCase() {
// Given
let mockedService = MockedService()
mockedService.result = .success("Hello, world!")
let viewModel = ViewModel(service: mockedService)
// When
viewModel.fetchData()
// Then
XCTAssertEqual(mockedService.getDataCallCounter, 1)
XCTAssertEqual(viewModel.data, "Hello, world!")
XCTAssertNil(viewModel.error)
}
}
最简单的MVVM模式
www.swiftwithvincent.com/blog/discov...
MVVM模式就是将逻辑处理封装到VM层,Model和View只做简单的业务解析和展示即可
错误代码
看以下代码,ViewController的责任比较多 获取数据、格式化数据、展示数据
swift
class ViewController: UIViewController {
let service = Service()
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
return formatter
}()
@IBOutlet weak var label: UILabel!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
service.fetchNumber{ [weak self] number in
let formatted = self?.formatter.string(for: number)
self?.label.text= formatted
}
}
}
正确代码
6步创建一个MVVM结构
- 创建处理逻辑的ViewModel
- 将Service和Formatter逻辑转移到ViewModel中
- 添加一个数据变化时更新UI的回调
- 把获取数据和格式化数据的逻辑也转移到ViewModel中
- 把UIViewController当成View,给它创建一个ViewModel的属性
- 调用viewModel的一些API以及设置ViewModel的一些回调处理
swift
class ViewController: UIViewController {
@IBOutlet weak var label: UILabel!
// 5.
let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.updateUI = { [weak self] newData in
self?.label.text = newData
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
viewModel.fetchData()
}
}
// 1.
class ViewModel {
// 2.
let service = Service()
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
return formatter
}()
// 3.
var updateUI: ((_ newDataToDisplay: String?) -> Void)?
// 4.
func fetchData() {
service.fetchNumber { [weak self] newNumber in
let formatted = self?.formatter.string(for: newNumber)
self?.updateUI?(formatted)
}
}
}