Swift开发中的注意点

尽量避免将方法的引用传递给一个接受@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...

错误代码

以下代码的写法导致getUserNamegetUserPicture同步执行。

只有getUserName执行完毕才会执行getUserPicture

swift 复制代码
import Foundation

Task {
    let userName = await getUserName()
    let userPicture = await getUserPicture()

    updateUI(userName: userName, userPicture: userPicture)
}

正确代码

采用结构化并发语法 async let,在需要使用结果的地方再await

这样能保证 getUserNamegetUserPicture 并发执行,同时去拿结果

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...

正确代码

  1. @MainActor可以使用在类型、方法、闭包中等地方。
  2. 使用了@MainActor之后,编译器会保证所需要的操作是运行在主线程
  3. @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。要注意循环引用

错误代码

  1. Task自动捕获了self的引用
  2. await notification这个for循环一直在监听AsyncSequence notifications,而AsyncSequence并不会停止
  3. 只要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)
            }
        }
    }
}

正确代码

  1. 使用捕获列表,在Task里捕获weak self
  2. 因为造成循环引用的根本在于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(大多数情况下)

错误代码

  1. Task.detached 会忽略所有上下文
  2. 使用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...

错误代码

  1. 不使用lazy,整个代码需要执行完10000次
  2. 我们其实只需要执行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-...

要理解?和!的区别

  1. ?被叫做:可选链,有值时就是那个值,否则是nil
  2. !被叫做:强制解包,有值时就是那个值,否则崩溃
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

  1. 使用可选绑定可以避免后期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-...

闭包捕获值时是捕获变量,无论该值时值类型还是引用类型

  1. 闭包默认会捕获值的变量,无论是值类型还是引用类型,只要外部通过变量修改了值,闭包内也会相应修改
swift 复制代码
var someInteger = 2
let closure = { in
    print(someInteger)
}
someInteger = 3
closure() // prints "3"
  1. 可以通过捕获列表捕获值,此时捕获的值是闭包创建时外部变量的值
swift 复制代码
// #01 - Capturing a Variable
var someInteger = 2
let closure = { [someInteger] in
    print(someInteger)
}
someInteger = 3
closure() // prints "2"

注意闭包引起的循环引用及内存泄漏问题

  1. ViewController持有Timer,Timer持有闭包,闭包持有self,造成了循环引用
  2. 通过捕获列表捕获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的闭包

  1. 闭包分为逃逸闭包(escaping)和非逃逸闭包(nonescaping)
  2. 逃逸闭包有可能造成循环引用,非逃逸闭包会在方法体内被消耗,不会造成循环引用
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...

  1. 这里我们抽象了Servicing协议,并分别实现了Service和MockedService用于不同的目的
  2. 使用协议编程可以解耦和提高扩展性
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结构

  1. 创建处理逻辑的ViewModel
  2. 将Service和Formatter逻辑转移到ViewModel中
  3. 添加一个数据变化时更新UI的回调
  4. 把获取数据和格式化数据的逻辑也转移到ViewModel中
  5. 把UIViewController当成View,给它创建一个ViewModel的属性
  6. 调用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)
        }
    }
}
相关推荐
Magnetic_h15 小时前
【iOS】单例模式
笔记·学习·ui·ios·单例模式·objective-c
归辞...17 小时前
「iOS」——单例模式
ios·单例模式·cocoa
yanling202318 小时前
黑神话悟空mac可以玩吗
macos·ios·crossove·crossove24
归辞...20 小时前
「iOS」viewController的生命周期
ios·cocoa·xcode
crasowas1 天前
Flutter问题记录 - 适配Xcode 16和iOS 18
flutter·ios·xcode
2401_852403551 天前
Mac导入iPhone的照片怎么删除?快速方法讲解
macos·ios·iphone
SchneeDuan1 天前
iOS六大设计原则&&设计模式
ios·设计模式·cocoa·设计原则
JohnsonXin2 天前
【兼容性记录】video标签在 IOS 和 安卓中的问题
android·前端·css·ios·h5·兼容性
蒙娜丽宁2 天前
Go语言错误处理详解
ios·golang·go·xcode·go1.19
名字不要太长 像我这样就好2 天前
【iOS】push和pop、present和dismiss
学习·macos·ios·objective-c·cocoa