提升Swift开发效率的Tips?请看这里

462 阅读3分钟

相比于直接使用compactMap,你可以扩展集合类型将其封装起来

www.swiftwithvincent.com/tips/better…

import Foundation
extension Collection {
    func nilValuesRemoved<Wrapped>() -> [Wrapped] where Element == Wrapped? {
        self.compactMap { $0 }
    }
}

let optionals = [1, 2, nil, 3, nil]
// for nonNilValue in optionals.compactMap { $0 }()
for nonNilValue in optionals.nilValuesRemoved() {
    print(nonNilValue)
}

使用 private(set) 修饰懒加载属性,达到无法懒加载let的效果

www.swiftwithvincent.com/tips/lazy-p…

import UIKit
class ViewController: UIViewController {
    // lazy let
    private(set) lazy var subview: UIView = {
        let view = UIView()
        // configure `view`
        return view
    }()
}

多使用guard来守卫必须达成的条件

www.swiftwithvincent.com/tips/guard-…

guard let user = getUser() else {
    throw UserError.noUser
}

guard let address = getAddress(of: user) else {
    throw UserError.noAddress
}

guard let paymentMethod = getPaymentMethod(of: user) else {
    throw UserError.noPaymentMethod
}

// do something

闭包通过捕获列表补货weak self后,在函数体里使用的self属性可以不写self.

www.swiftwithvincent.com/tips/concis…

publisher.sink { [weak self] newValue in
    guard let self else { return }
    doSomething(with: newValue)
}.store(in: &cancellables)

函数、闭包、计算属性等,单行表达式 省略return

www.swiftwithvincent.com/tips/omit-r…

// Functions
func helloWorld() -> String {
    "Hello, world!"
}

// Closures
let helloWorld = {
    "Hello, world!"
}

// Computed Properties
var helloWorld: String {
    "Hello, world!"
}

使用 @available来标记一些过期API

www.swiftwithvincent.com/tips/ios-ve…

import UIKit

@available(iOS, deprecated: 14)
func warningOnceiOS13IsDropped(_ message: String) { }

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        warningOnceiOS13IsDropped("remove once we drop iOS 13")

        /* some workaround for an iOS 13 bug */
    }
}

将 body计算属性中使用的常量 扔到 ViewBuilder中

import SwiftUI

struct MyView: View {
    var body: some View {
        // let localConstant = 42
        VStack {
            // 从body计算属性中,移动到VStack的ViewBuilder中
            let localConstant = 42

            /* ... */
        }
    }
}

多考虑使用 LazySequence来减少计算量

www.swiftwithvincent.com/tips/lazyse…

import Foundation

(1...10_000)
    .lazy
    // 如果不使用lazy,将会执行完整10_000次
    .map { $0 * $0 } // executed 15 times
    .filter { $0.isMultiple(of: 5) } // executed 15 times
    .first (where: { $0 > 100 })

多使用扩展 将重复逻辑封装

www.swiftwithvincent.com/tips/generi… www.swiftwithvincent.com/tips/or-emp… www.swiftwithvincent.com/tips/is-nil…

import Foundation
extension String? {
    var orEmpty: String {
        self ?? ""
    }
}
// 和上面的写法一样,都是针对 可选String类型,即Optional<String>
extension Optional where Wrapped == String {
    var orEmpty: String {
        self ?? ""
    }
    var isNilOrEmpty: Bool {
        self == nil || self == ""
    }
}

let optionalString = Bool.random() ? "Hello, world!" : nil
print(optionalString.orEmpty)

func handles(optionalString: String?) {
    if optionalString.isNilOrEmpty == false {
        // use `optionalString`
    }
}

对于字符串包含转义子串的,多考虑使用原始字符串。前后一个#号。对于多行字符传考虑使用"""

www.swiftwithvincent.com/tips/raw-st… www.swiftwithvincent.com/tips/multil…

import Foundation

let json = #"{"name":"Vincent"}"#

let multilineString = """
1st line
2nd line
3rd line
"""

写单元测试时,对于会抛出错误的case,可以将测试函数声明成throws可抛出错误的。然后case内使用try

www.swiftwithvincent.com/tips/xctest…

import XCTest

class Test: XCTestCase {
    func test() throws {
        XCTAssertEqual(try throwingFunction(), "Expected Result")
    }
}

多条件判断考虑使用switch代替if

www.swiftwithvincent.com/tips/if-els…

switch (boolean1: boolean1, boolean2: boolean2, boolean3: boolean3) {

case (boolean1: true, _, boolean3: false):
    functionA()
case (_, boolean2: false, boolean3: true):
    functionB()
case (boolean1: true, boolean2: false, boolean3: false):
    functionC()
default:
    break
}

接受多个参数时,考虑使用可变参数而不是Array

www.swiftwithvincent.com/tips/variad…

import UIKit

extension UIView {
    //     func add(subviews: [UIView])
    func add(subviews: UIView...) {
        for subview in subviews {
            self.addSubview(subview)
        }
    }
}

view.add(subviews: firstView, secondView, thirdView)

对于枚举,最好实现CaseIterable,编译器会帮助生成allCases

www.swiftwithvincent.com/tips/caseit…

import Foundation

enum Direction: CaseIterable {
    case north
    case south
    case east
    case west
}

Direction.allCases // [.north, .south, .east, .west]

多使用协议编程思想。有泛型参数时多考虑使用some关键字

www.swiftwithvincent.com/tips/opaque… www.swiftwithvincent.com/tips/array-…

import Foundation
// func handle<T: Identifiable>:(value: T)
// func handle<T>:(value: T) where T: Identifiable
func handle(value: some Identifiable) {

}

struct User {
    let firstName: String
}
// func display(users: Array<User>)
func display(users: some Collection<User>) {
    for user in users {
        print(user)
    }
}

多使用is判断类型,而不是每次尝试使用as?做类型转换

www.swiftwithvincent.com/tips/as-vs-…

let mystery: Any = Bool.random() ? 1 : "One"
// if let _ = mystery as? Int
if mystery is Int {
    print("It's an Int!")
}

多使用函数式简洁的filter,少使用命令式复杂的 for loop

www.swiftwithvincent.com/tips/for-lo…

import Foundation

let integers = [1, 2, 3, 4, 5]
// for integer in integers where integer.isMultiple(of: 2)
let evensOnly = integers.filter { $0.isMultiple(of: 2) }

使用闭包时,多考虑使用捕获列表捕获直接使用的变量,不要捕获weak self后再通过self去引用

www.swiftwithvincent.com/tips/closur…

class ViewController: UIViewController {

    let service = Service()
    let formatter = Formatter()
    let label = UILabel()

    var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()

        label.text = "Retrieved data: "
        // service.call().sink { [weak self] data in guard let self else {return} }
        service.call().sink { [formatter, label] data in
            let formatted = formatter.format(data: data)
            label.text?.append(formatted)
        }.store(in: &cancellables)
    }
}

对于有两个返回值的,优先考虑使用Result而不是Optional

www.swiftwithvincent.com/tips/option…

import Foundation
// func fetchData(_ completion: @escaping (Data?, Error?) -> Void)
func fetchData(_ completion: @escaping (Result<Data, Error>) -> Void) {
    /* ... */
}

fetchData { result in
    switch result {
    case .success(let data):
        // use the data
    case .failure(let error):
        // handle the error
    }
}

多使用有可变参数的函数,而不是自己拼凑完整参数

www.swiftwithvincent.com/tips/variad…

import Foundation

var person = "Vincent"
var nationality = "French 🇫🇷"
// print("\(person) is \(nationality)")
print(person, " is ", nationality)

对于大数,书写的时候多使用千分符提高可读性

www.swiftwithvincent.com/tips/large-…

let bigNumber = 123_456_789

KeyPath也可以用于高阶函数的函数参数

www.swiftwithvincent.com/tips/keypat…

import Foundation

extension Int {
    var isEven: Bool { self.isMultiple(of: 2) }
}

let numbers = [1, 2, 3, 4, 5]
// let evens = numbers.filter{ $0.isHeven}
let evens = numbers.filter(\.isEven)

多使用Swift5.7的简短if let绑定

www.swiftwithvincent.com/tips/shorth…

var userName: String?
// if let userName = userName
if let userName {
    // `userName` is non-optional here
}

在for loop循环中多使用where语句,少在执行体中用if判断

www.swiftwithvincent.com/tips/where-…

import Foundation

let numbers = [1, 2, 3, 4, 5]

for number in numbers where number.isMultiple(of: 2) {
    print(number)
}

对于打印对象,多考虑使用dump而不是print

www.swiftwithvincent.com/tips/dump-v…

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: 31)

dump(me)

多个if嵌套可以考虑改成if多条件判断

www.swiftwithvincent.com/tips/combin…

func displayIfConditions(value: Int) {
    if value.isMultiple(of: 2),
       value.isMultiple(of: 3),
       value.isMultiple(of: 5),
       value.isMultiple(of: 7) {
        print(value)
    }
}

多使用纯净的Swift API,少使用OC风格的API

www.swiftwithvincent.com/tips/modern…

class ViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var button: UIButton!

    var counter = 0 {
        didSet {
            label.text = "Counter: \(counter)"
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // 不要使用addAction、#selector、@objc等OC风格的API
        button.addAction(UIAction { [weak self] _ in
            self?.counter += 1
        }, for: .touchUpInside)
    }
}

在单元测试中建议使用XCTUnwrap解包可选值,而不是自己进行强制解包

www.swiftwithvincent.com/tips/xctunw…

import XCTest

class MyTests: XCTestCase {
    func test() throws {
        let myData = [1, 2, 3]
        let first = try XCTUnwrap(myData.first)
        XCTAssert(first<3)
    }
}