Swift 奇巧淫技

转载至 我的博客

Swift 中的正常操作!!!

传入 KeyPath 作为函数

Swift 5.2新特性,使用 map 等函数来进行数据转换时是更加简洁了,一个小小的改动。

struct Movie {
    var name: String
    var isFavorite: Bool
    ...
}

let movies: [Movie] = loadMovies()

// Equivalent to movies.map { $0.name }
let movieNames = movies.map(\.name)

// Equivalent to movies.filter { $0.isFavorite }
let favoriteMovies = movies.filter(\.isFavorite)

使用 keypath 来匹配 switch

Swift 的 switch 已经如此强大了,配合 ~= 符号能让我们实现更强大的 switch

func ~=<T>(lhs: KeyPath<T, Bool>, rhs: T) -> Bool {
    rhs[keyPath: lhs]
}

func handle(_ character: Character) {
    switch character {
    case "<":
        parseElement()
    case "#":
        parseHashtag()
    case \.isNumber:
        parseNumber()
    case \.isNewline:
        startNewLine()
    default:
        parseAnyCharacter()
    }
}

计算属性和有返回值方法中如果自有一个表达式可以省略 return

好用

extension MarkdownReader {
    var isAtStart: Bool { index == string.startIndex }
    var didReachEnd: Bool { index == string.endIndex }
    var currentCharacter: Character { string[index] }
    
    func encodeCurrentCharacter() -> String {
        currentCharacter.encoded()
    }
}

Swift5.1 中枚举的关联值也可以使用默认参数

撒花

// Associated enum value defaults are specified the same way as
// default function arguments:
enum Content {
    case text(String)
    case image(Image, description: String? = nil)
    case video(Video, autoplay: Bool = false)
}

// At the call site, any associated value that has a default
// can be omitted, and the default will be used:
let video = Content.video(Video(url: url))

使用元组成组的捕获异常

如果遇见调用多个会抛出一样的函数,可以使用元组将其括起来,那样的话你就只需写一个 try。

// Here we have three highly related expressions that are
// all throwing, requiring separate assignments and separate
// 'try' keywords:
let contentFolder = try Folder.current.subfolder(named: "content")
let templatesFolder = try Folder.current.subfolder(named: "templates")
let output = try Folder.current.createSubfolderIfNeeded(withName: "output")

// By combining them all into a tuple, we only need one
// 'try', and can easily group our data into a single,
// lightweight container:
let folders = try (
    content: Folder.current.subfolder(named: "content"),
    templates: Folder.current.subfolder(named: "templates"),
    output: Folder.current.createSubfolderIfNeeded(withName: "output")
)

// The call sites also become really nice and clean, with
// increased "namespacing" for our local variables:
readFiles(in: folders.content)
loadTemplates(from: folders.templates)

用函数来联合变量

最后我们将生成一个无参闭包,针对一些闭包 API 可以直接传递,并且不需要在闭包当中捕获 self。

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

// BEFORE:

class ProductViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        buyButton.handler = { [weak self] in
            guard let self = self else {
                return
            }
            
            self.productManager.startCheckout(for: self.product)
        }
    }
}

// AFTER:

class ProductViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        buyButton.handler = combine(product, with: productManager.startCheckout)
    }
}

善用方法作依赖注入

如果需要注入的依赖并不复杂,就不允许使用协议或是导入整个 Model 了。

final class ArticleLoader {
    typealias Networking = (Endpoint) -> Future<Data>
    
    private let networking: Networking
    
    init(networking: @escaping Networking = URLSession.shared.load) {
        self.networking = networking
    }
    
    func loadLatest() -> Future<[Article]> {
        return networking(.latestArticles).decode()
    }
}

解包可选值失败时可以抛出一个异常

不用修改方法本身,就能将其改造成一个可以抛出异常的 API

extension Optional {
    func orThrow(_ errorExpression: @autoclosure () -> Error) throws -> Wrapped {
        switch self {
        case .some(let value):
            return value
        case .none:
            throw errorExpression()
        }
    }
}

let file = try loadFile(at: path).orThrow(MissingFileError())

自定义 UIView 的 layer

这样的话就可以自定义许多有趣的 View 了,不用添加多余的 layer,不用管理 layer 的布局,就像一个系统的 view 一样。

final class GradientView: UIView {
    override class var layerClass: AnyClass { return CAGradientLayer.self }

    var colors: (start: UIColor, end: UIColor)? {
        didSet { updateLayer() }
    }

    private func updateLayer() {
        let layer = self.layer as! CAGradientLayer
        layer.colors = colors.map { [$0.start.cgColor, $0.end.cgColor] }
    }
}

如果枚举的关联类型遵守 Equatable 则枚举自动遵守 Equatable

看起来很合理

struct Article: Equatable {
    let title: String
    let text: String
}

struct User: Equatable {
    let name: String
    let age: Int
}

extension Navigator {
    enum Destination: Equatable {
        case profile(User)
        case article(Article)
    }
}

func testNavigatingToArticle() {
    let article = Article(title: "Title", text: "Text")
    controller.select(article)
    XCTAssertEqual(navigator.destinations, [.article(article)])
}

元组类型的解构

在 Swift 中,元组类型在赋值时可以像其他语言那样使用解构

class ImageTransformer {
    private var queue = [(image: UIImage, transform: Transform)]()

    private func processNext() {
        // When unwrapping an optional tuple, you can assign the members
        // directly to local variables.
        guard let (image, transform) = queue.first else {
            return
        }

        let context = Context()
        context.draw(image)
        context.apply(transform)
        ...
    }
}

嵌套泛型类型

嵌套的泛型类型,能够继承上层类型的泛型定义,这样省去了重复定义相同类型泛型的麻烦。

struct Task<Input, Output> {
    typealias Closure = (Input) throws -> Output

    let closure: Closure
}

extension Task {
    enum Result {
        case success(Output)
        case failure(Error)
    }
}

alias 也能用泛型

看起来似乎有一些用处

typealias Pair<T> = (T, T)

extension Game {
    func calculateScore(for players: Pair<Player>) -> Int {
        ...
    }
}

为类型扩展静态工厂方法

在构建 UI 时非常的有用,特别是对统一设计风格的通用 UI 来说,高度封装,使得对 UI 的修改变的简单,代码也更加的简洁,如果配合上点语法就更完美了。

extension UILabel {
    static func makeForTitle() -> UILabel {
        let label = UILabel()
        label.font = .boldSystemFont(ofSize: 24)
        label.textColor = .darkGray
        label.adjustsFontSizeToFitWidth = true
        label.minimumScaleFactor = 0.75
        return label
    }

    static func makeForText() -> UILabel {
        let label = UILabel()
        label.font = .systemFont(ofSize: 16)
        label.textColor = .black
        label.numberOfLines = 0
        return label
    }
}

class ArticleViewController: UIViewController {
    lazy var titleLabel = UILabel.makeForTitle()
    lazy var textLabel = UILabel.makeForText()
}

将实例方法当做静态方法来调用

同样是对函数式编程的实践

// This produces a '() -> Void' closure which is a reference to the
// given view's 'removeFromSuperview' method.
let closure = UIView.removeFromSuperview(view)

// We can now call it just like we would any other closure, and it
// will run 'view.removeFromSuperview()'
closure()

// This is how running tests using the Swift Package Manager on Linux
// works, you return your test functions as closures:
extension UserManagerTests {
    static var allTests = [
        ("testLoggingIn", testLoggingIn),
        ("testLoggingOut", testLoggingOut),
        ("testUserPermissions", testUserPermissions)
    ]
}

在 for-loop 循环中使用 where

如果你需要在循环中使用 if 来筛选数据,那不妨使用 where 来让代码的结构更加清晰

func archiveMarkedPosts() {
    for post in posts where post.isMarked {
        archive(post)
    }
}

func healAllies() {
    for player in players where player.isAllied(to: currentPlayer) {
        player.heal()
    }
}

使用点语法来访问静态方法、静态变量和构造方法

省略类型,直接使用点语法访问,用在 API 和默认值上时,让代码开起来无比的简洁

public enum RepeatMode {
    case times(Int)
    case forever
}

public extension RepeatMode {
    static var never: RepeatMode {
        return .times(0)
    }

    static var once: RepeatMode {
        return .times(1)
    }
}

view.perform(animation, repeated: .once)

// To make default parameters more compact, you can even use init with dot syntax

class ImageLoader {
    init(cache: Cache = .init(), decoder: ImageDecoder = .init()) {
        ...
    }
}

在 enum 和 struct 的构造方法里面可以直接设置 self

class 中我们只能设置 self 的属性,而在 enum 和 struct 中我们能设置 self 的值,这样的话我们就能很方便的为它们扩展各种便利构造方法了。

extension Bool: AnswerConvertible {
    public init(input: String) throws {
        switch input.lowercased() {
        case "y", "yes", "👍":
            self = true
        default:
            self = false
        }
    }
}

ExpressibleBy... 系列函数的使用

一定要确保使用时语义清楚,特别是针对自定义类型,否则一段时间后,你都未必知道它到底做了什么

extension URL: ExpressibleByStringLiteral {
    // By using 'StaticString' we disable string interpolation, for safety
    public init(stringLiteral value: StaticString) {
        self = URL(string: "\(value)").require(hint: "Invalid URL string literal: \(value)")
    }
}

// We can now define URLs using static string literals 🎉
let url: URL = "https://www.swiftbysundell.com"

闭包类型用来做泛型约束

闭包类型也是类型,作为 Swift 中的第一公民,当然也能用来做泛型约束啦

extension Sequence where Element == () -> Void {
    func callAll() {
        forEach { $0() }
    }
}

extension Sequence where Element == () -> String {
    func joinedResults(separator: String) -> String {
        return map { $0() }.joined(separator: separator)
    }
}

callbacks.callAll()
let names = nameProviders.joinedResults(separator: ", ")

将一组方法合并起来

在函数式编程当中,就是所谓的管道函数 |>

internal func +<A, B, C>(lhs: @escaping (A) throws -> B,
                         rhs: @escaping (B) throws -> C) -> (A) throws -> C {
    return { try rhs(lhs($0)) }
}

public func run() throws {
    try (determineTarget + build + analyze + output)()
}

使用 mapflatMap 来优化可选链

Swift 中的可选链可真是简洁代码杀手,我们总是会陷入 if let 和 gaurd let 的解包泥潭中去,不过在单一参数的情况下,我们可以利用 mapflatMap来简化我们的可选链代码

// BEFORE

guard let string = argument(at: 1), let url = URL(string: string) else {
    return
}

handle(url)

// AFTER

argument(at: 1).flatMap(URL.init).map(handle)

使用可变参数

如果你的 API 需要传入一组手动创建的数据,使用可变参数要比使用数组看起来更合理。

public extension UIView {
    func addSubviews(_ views: UIView...) {
        views.forEach(self.addSubview(_:))
    }
}

有关联值的 enum 类型,在不存入关联值的时候是一个闭包

看来枚举是函数式编程和柯里化的忠实粉丝啊!

enum UnboxPath {
    case key(String)
    case keyPath(String)
}

struct UserSchema {
    static let name = key("name")
    static let age = key("age")
    static let posts = key("posts")
    
    private static let key = UnboxPath.key
}

print(type(of: UnboxPath.key)) // (String) -> UnboxPath

初始化方法可被用来设置参数和闭包的默认值

如果用的到的话,代码看起来会更简洁明了

class Logger {
    private let storage: LogStorage
    private let dateProvider: () -> Date
    
    init(storage: LogStorage = .init(), dateProvider: @escaping () -> Date = Date.init) {
        self.storage = storage
        self.dateProvider = dateProvider
    }
    
    func log(event: Event) {
        storage.store(event: event, date: dateProvider())
    }
}

利用 #function 将属性名指定为 UserDefault 的 key

灵魂操作!!!

extension UserDefaults {
    var onboardingCompleted: Bool {
        get { return bool(forKey: #function) }
        set { set(newValue, forKey: #function) }
    }
}

类型名称与系统框架内类型名称重复

首先尽量不用使用与系统内部框架重名的标识符,当然如果你非要用,可以使用 Swift. 来为系统类型指定命名空间

extension Command {
    enum Error: Swift.Error {
        case missing
        case invalid(String)
    }
}

善用 typealias 来减少类型名的长度

首先,就像在 OC 中一样用 typealias 来声明长长的闭包类型。其次嵌套类型的名称往往很长,如果需要用到了一个其他类型嵌套下的子类型时,不妨在当前作用域给它起一个短别名。

public class PathFinder<Object: PathFinderObject> {
    public typealias Map = Object.Map
    public typealias Node = Map.Node
    public typealias Path = PathFinderPath<Object>
    
    public static func possiblePaths(
        for object: Object,
        at rootNode: Node,
        on map: Map
    ) -> Path.Sequence {
        return .init(object: object, rootNode: rootNode, map: map)
    }
}

自动闭包

如果你的方法中有一个参数需要耗时计算才能得到,而这个参数可能不会被使用或是不会被立即使用,这个时候我们应该使用 @autoclosure将其转换成一个闭包。这样即保证了调用形式和原来一样,也能够保证仅在需要的时候才去运算获取相应的值。

// Swift 中的 || 就是这样实现的,如果左侧为 ture 就不用再费力执行右侧的结果了。
public static func || (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool {
    if lhs {
        return true
    } else {
        return try rhs()
    }
}

使用嵌套类型实现命名空间

很常用,例如一个特殊 API 返回的数据类型不通用,但是又想使用 Model 的便利性,此时创建一个全局的 Model 就太过浪费了,在当前类型的作用域下创建一个 Model 是一个不错的方法。当然也包括了 UI 模块,一个 UI 模块内部的更小模块,无须占用全局的类型名称,可以创建在当前模块的作用域下。

public struct Map {
    public struct Model {
        public let size: Size
        public let theme: Theme
        public var terrain: [Position : Terrain.Model]
        public var units: [Position : Unit.Model]
        public var buildings: [Position : Building.Model]
    }
    
    public enum Direction {
        case up
        case right
        case down
        case left
    }
    
    public struct Position {
        public var x: Int
        public var y: Int
    }
    
    public enum Size: String {
        case small = "S"
        case medium = "M"
        case large = "L"
        case extraLarge = "XL"
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,227评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,755评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,899评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,257评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,617评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,757评论 1 221
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,982评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,715评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,454评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,666评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,148评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,512评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,156评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,112评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,896评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,809评论 2 279
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,691评论 2 272

推荐阅读更多精彩内容