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)
        }
    }
}
相关推荐
若水无华1 天前
fiddler 配置ios手机代理调试
ios·智能手机·fiddler
Aress"1 天前
【ios越狱包安装失败?uniapp导出ipa文件如何安装到苹果手机】苹果IOS直接安装IPA文件
ios·uni-app·ipa安装
Jouzzy2 天前
【iOS安全】Dopamine越狱 iPhone X iOS 16.6 (20G75) | 解决Jailbreak failed with error
安全·ios·iphone
瓜子三百克2 天前
采用sherpa-onnx 实现 ios语音唤起的调研
macos·ios·cocoa
左钦杨2 天前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
努力成为包租婆2 天前
SDK does not contain ‘libarclite‘ at the path
ios
安和昂2 天前
【iOS】Tagged Pointer
macos·ios·cocoa
I烟雨云渊T3 天前
iOS 阅后即焚功能的实现
macos·ios·cocoa
struggle20253 天前
适用于 iOS 的 开源Ultralytics YOLO:应用程序和 Swift 软件包,用于在您自己的 iOS 应用程序中运行 YOLO
yolo·ios·开源·app·swift
Unlimitedz3 天前
iOS视频编码详细步骤(视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265)
ios·音视频