Preface

SwiftTipsJohn Sundell 在 GitHub 开源的 Swift 小技巧列表。随着 Swift 5 的发布以及 ABI 稳定,是时候再学习一遍 Swift 啦。本文将是该列表的实践版本,并保证文中代码的可运行性,且尽可能做到倒序日更。(But why in reverse? 🤫)

关于本文的代码,都可以在 swift-tips-in-practice 下载并实际运行。

Date Update Date Update Date Update
2019.08.22 #102 2019.08.23 #100 2019.08.27 #99

#102 让异步测试执行更快更稳定

异步代码似乎总是很难去编写单元测试,因为我们不清楚什么时候请求才能回来。现在在 Swift 中,我们可以使用 expectation(预料)简单设定超时时间,并在 Closure 回调时调用 fulfill() 即可轻松实现。

Talk is cheap, show me the code!

import XCTest
@testable import TestUnitTest

class TestUnitTestTests: XCTestCase {
    func fetchFromNetwork(_ completion: @escaping (String) -> Void) {
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.5) {
            let response = "Foo"
            completion(response)
        }
    }

    // 🙅 Not good
    func testAsyncOperationsWithSleep() {
        fetchFromNetwork { res in
            XCTAssert(res == "Foo", "Response should be 'Foo'.")
        }

        sleep(2)
    }

    // 🚩 Preferred
    func testAsyncOperationsWithoutSleep() {
        // 声明 exp
        let exp = expectation(description: "Async Test")

        var result = ""
        fetchFromNetwork { res in
            result = res

            // Closure 执行完毕
            exp.fulfill()
        }

        // 等待 exp,超时时间 2 秒
        wait(for: [exp], timeout: 2.0)
        XCTAssert(result == "Foo", "Response should be 'Foo'.")
    }
}

对于单元测试,在 iOS 项目中使用「Quick」&「Nimble」、在 Swift Package Management 项目中使用「Spectre」可以编写更加符合 BDD(Behavior Driven Design 即行为驱动设计)的测试用例。后者我曾在开源项目 WWDCHelper 中使用过,配合 CI 即可持续保障代码覆盖率。

Reference

#101 支持 Apple Pencil 双击

  • 迫于没有设备,跳过该节。

#100 函数绑定值

将值和函数使用额外的 combine 函数绑定起来,可无需在闭包(Closure)中捕获 self,相当于只将最终需要检查的执行者与参数告知即可。对于 combine 函数是支持泛型的,即其无需关心具体的业务内容,它只负责将参数与闭包组装并执行:

import UIKit

// 类型别名
typealias Handler = () -> Void

// 闭包包裹器,否则无法存储在关联对象内
fileprivate class ClosureWrapper {
    var handler: Handler?

    init(_ closure: Handler?) {
        handler = closure
    }
}

extension UIButton {
    // 关联键
    private struct AssociatedKeys {
        static var touchUpInside = "touchUpInside"
    }

    // 处理方法
    var handler: Handler? {
        get {
            if let wrapper = objc_getAssociatedObject(self,
                                                      &AssociatedKeys.touchUpInside) as? ClosureWrapper,
                let handler = wrapper.handler {
                return handler
            }

            return nil
        }

        set {
            if let newValue = newValue {
                objc_setAssociatedObject(self,
                                         &AssociatedKeys.touchUpInside,
                                         ClosureWrapper(newValue),
                                         .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

                addTarget(self, action: #selector(runHandler), for: .touchUpInside)
            } else {
                removeTarget(self, action: #selector(runHandler), for: .touchUpInside)
            }
        }
    }

    @objc func runHandler() {
        handler?()
    }
}


struct Product {
    var bar: Int
}

struct ProductManager {
    func startCheckout(for product: Product) {
        switch product.bar {
        case 0..<60:
            print("不合格")
        case 60...100:
            print("合格")
        default:
            fatalError()
        }
    }
}

// 泛型 A B
func combine<A, B>(_ value: A,
                   with closure: @escaping (A) -> B) -> () -> B {
    return { closure(value) }
}

class Demo100ViewController: UIViewController {
    lazy var fooButton: UIButton! = {
        let button = UIButton(type: .system)

        button.frame = CGRect(x: 0.0, y: 100.0, width: 100, height: 50)
        button.setTitle("Foo", for: .normal)

        view.addSubview(button)

        return button
    }()

    var product = Product(bar: 89)
    var productManager = ProductManager()

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "100"
        view.backgroundColor = .white

        // 🙅‍♂️ Not good
        fooButton.handler = { [weak self] in
            guard let self = self else { return }

            self.productManager.startCheckout(for: self.product)
        }

        // 🚩 Preferred
        fooButton.handler = combine(self.product, with: productManager.startCheckout)
    }
}

然而在真正实现这一段的 handler 时,却花费了一些周折。UIButton 在官方的设计模式是 Target Action,而我们需要改为闭包回调的形式。由于 Swift 的 extension 并不支持存储属性,类似 Obj-C 的 Category 不支持实例变量,这里仍然使用了关联对象来将闭包关联在对象上。又由于闭包无法直接被关联,仍然需要包裹在类中才得以实现。

#99 使用函数进行依赖注入

在开始前,我们先来认识一下什么是依赖注入(Dependency Injection)

依赖注入又称控制反转(Inversion of Control,IoC)。简单来说,依赖注入的重点在于注入,即将依赖以注入的方式引入,而非直接在依赖内创建,以避免耦合。举个简单的例子:

struct Foo {
    var bar: String
}

struct FooFactory {
    static func generate(_ bar: String) -> Foo {
        return Foo(bar: bar)
    }
}

struct Bar {
    var foo: Foo

    // 🙅 Not good
    init(_ bar: String) {
        foo = Foo(bar: bar)
        // foo = FooFactory.generate(bar)
    }

    // 🚩 Preferred
    init(_ foo: Foo) {
        self.foo = foo
    }
}

设计模式上更加推荐将 foo 直接注入到 Bar 中,显而易见的好处是这样可以便于单元测试,因为我们将 Foo 的构造暴露了在外部,避免增加了 Bar 的耦合度。这也正符合其另一个名称控制反转,即将控制权交由外界,Bar 无需关心 foo 是如何创建的(通过构造器,抑或工厂方法等),其只需要直接使用即可。

在 Swift 中,函数是一类公民,即可以作为一种类型。因此函数的注入也使得内部无需关心函数具体的定义,只需要定义好参数和返回值即可。比如在网络请求中,我们可以无需关心网络层到底是通过 URLSession 还是 Alamofire 这样封装好的网络库来构造的:


Reference

#98 使用自定义异常处理器


All rights reserved by JohnSundell/SwiftTips.