Swift 文档编写规范

本篇为译文,原文可见:链接

如果超过 6 个月没看过自己写的代码的话,你可能会认为这些代码是其他人写的。

-Eagleson’s Law

当我们使用 Appleclass 时,如果不知道如何使用它们的话,我们有很多方式可以去查找资料。比如你可以通过 Apple Developer Documentation 在线文档或者通过 Xcode 来查找。

Quick help popover

你可以通过 ⌥ – Option + click 方式查看任何 class

Symbol inspector Quick Help

Quick help 也显示在 Quick Help 检查面板(inspector panel)上。

Code completion hint

当你开始打字时,Xcode 将会有相应的提示信息(包含 class 中的 function/property/enum

接下来将介绍如何给自己的代码加上这些提示。

Syntax

给代码写文档就像写注释一样,但是有一点点语法上的差别。你需要 /// 来标注单行的文档。

/// This is your User documentation.
struct User {
    let firstName: String
    let lastName: String
}

对于多行文档,你需要 /** ... */ 符号。

/**
    This is your User documentation.
    A very long one.
*/
struct User {
    let firstName: String
    let lastName: String
}

继续阅读“Swift 文档编写规范”

Swift Weekly 中文 – Issue #176

本篇为译文,原文可见:链接

本周的 issue ,我们主要讨论 updating the view stateanimations in SwiftUIdebugging Combine

文章

如果你正在查找关于 body 是计算型时,去尝试修改 view 状态的资料的话,这篇文章将会告诉你可以做哪些操作以及不应该做哪些操作。

Fernando Moya de Rivas 使用 SwiftUI 创建了几个有趣的动画。

学习不同方式来调试由 Swift Combine framework 编写的 functional reactive code 。通过 print()handleEvents() 来查看 console;通过 breakpointOnError()breakpoint() 来生成 Xcode 断点;以及通过绘制图表的方式。

Josh Adams 通过 SwiftUI 修改了他的一个 app,并分享了一些学习心得。

本篇文章中,Jim Dovey 解释了如何使用 SwiftUICoreData 进行绑定操作。

The folks at Just Eat have experimentation and feature flagging at their heart and they’ve developed a component, named JustTweak, to make things easier on iOS.

John Sundell 找到了一些不同的方式来添加插件支持,这样可以使得系统变得更加灵活。

介绍了如何编写 Swift 代码的文档注释。

介绍了 Alexander Grebenyuk 如何从手动测试他的框架到通过单元测试来自动化测试每一次的变更。

继续阅读“Swift Weekly 中文 – Issue #176”

Swift 之 Property Wrappers 特性

Swift 5.1 新增了 Property Wrappers 特性。该特性可以通过使用 @ 符号以注解的形式来实现某些功能,并达到简化代码的效果。

示例

iOS 中的 UserDefaults 为例,一般用法如下。

  • 存储数据
UserDefaults.standard.set(value, forKey: key)
  • 获取数据
UserDefaults.standard.object(forKey: key)

改造

Property Wrappers 需满足两个基本要求:

  • 必须使用 @propertyWrapper 关键字来修饰。
  • 必须有一个命名为 wrappedValue 的属性。

接下来以 Property Wrappers 方式改造 UserDefaults ,代码如下:

@propertyWrapper
struct MyUserDefault<T> {

    let key: String
    let defaultValue: T

    var wrappedValue: T {
        get {
            return (UserDefaults.standard.object(forKey: key) as? T) ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }

}

使用

@MyUserDefault(key: "username", defaultValue: "")
static var username: String

加了 MyUserDefault 自定义注解后,对 username 的赋值操作相当于是执行了 UserDefaults.standard.set 方法,对 username 的读取操作相当于执行了 UserDefaults.standard.object 方法。

限制

Property Wrappers 也有一些使用上的限制。比如:

  • 无法在 protocol 中进行声明。
  • 通过 wrapper 包装的实例属性(An instance property with a wrapper)无法在 extension 中进行声明。
  • 无法在 enum 中进行声明。
  • class 中通过 wrapper 包装的属性无法被另外一个属性通过 override 覆盖掉。
  • 通过 wrapper 包装的实例属性(An instance property with a wrapper)不能用 lazy@NSCopying@NSManagedweakunowned 来修饰。

继续阅读“Swift 之 Property Wrappers 特性”

Swift 服务端发展现状

最近 Swift Workgroup 发布了一则公告:

@IanPartridge and @Chris_Bailey let the group know that following a review by IBM of its open source priorities, it has been decided that they will not be continuing to work on Swift in 2020. As a result, they are both standing down from the workgroup.

@IanPartridge will work to hand over responsibilities for the Swift Docker images and suggested a potential new owner from the community.

The workgroup thanked @IanPartridge, @Chris_Bailey and the rest of the IBM team for their valuable work over the years in getting Swift on server off the ground, and providing the community with reliable solutions during those early days.

大致意思是说,根据 IBM 公司的开源优先级评估后,决定 IanPartridgeChris_Bailey 将于 2020 年不再继续从事 Swift 相关的工作了,同时他们也将退出 workgroup

其中,IanPartridge 将不再负责 Swift Docker images 相关的职责,社区将会有一名新的继承者来接管。

可以看出 IBM 在未来将会一定程度上减少 Swift 服务端的投入,算是 Swift 社区的一个损失。

目前主要由 Apple 官方维护的 Swift 库如下:

Event-driven network application framework for high performance protocol servers & clients, non-blocking.

A Logging API for Swift

A Metrics API for Swift

Metrics backend for swift-metrics that uses the statsd protocol.

目前 Swift 社区主要有如下 Web 框架:

另外,Swift 社区还有很多优秀的开源库,这里就不一一列出来了。

从目前来看,Vapor 框架的受关注度最高,且社区相对比较活跃。而且 Vapor 框架的作者 Logan Wright (@loganwright, Vapor) 和 Tanner Nelson (@tanner0101, Vapor) 均为 SSWG(Swift Server Work Group) 成员,与 Swift 官方组织联系也最为紧密,Vapor 几乎也都是第一时间采用 Swift 官方组件(比如:SwiftNIOSwiftLogSwiftMetrics 等)。

继续阅读“Swift 服务端发展现状”

Swift Weekly 中文 – Issue #175

本篇为译文,原文可见:链接

本周的 issue ,我们主要讨论 Thread SanitizerKeyValuePairsSPMUtility

文章

学习在 Swift 中如何使用 Thread Sanitizer to catch Data Races。修复怪异的 crash,并且可以看到 Data Reace 示例。

介绍了一些集合类型之间的区别,比如:ArraySetDictionary

本篇文章中,Derik Ramirez 将介绍如何使用 Swift Package Manager 中的 SPMUtility 模块,通过 ArgumentParser 来解析你的 swift command-line tool 的参数。

Xcode 11 介绍了一种新方式来测试可选类型。Sarun 向你演示了新的 XCTUnwrap 方法。

本周,John Sundell 看了看一些核心语言特性,这些特性可以使我们设计真正轻量级的 Swift API,并且可以帮助我们更好的开发一个新功能或系统。

如果你正在查找关于 body 是计算型时,去尝试修改 view 状态的资料的话,这篇文章将会告诉你可以做哪些操作以及不应该做哪些操作。

在本周文章中,Majid 将向你展示 UIKitSwiftUI 开发之间的主要不同点,并指出使用 SwiftUI 的时候必须要改变的习惯。

Alexey Naumov 解释了如何处理 SwiftUI 项目中的导航来实现 deep links

工作机会

Ctrl Group builds digital products for patients, healthcare practitioners and researchers to gather evidence and provide better care. We’re looking for an iOS engineer to join our team in London, or work remotely as part of our distributed team.

讨论

介绍了今年十月在 Bologna, Italy 举办的 #Pragma Conference 中的所有演讲 。

两个免费视频探索了 Apple 新的 Combine 框架,包括它的核心组件,以及如何集成进你的代码。

库 & 代码

继续阅读“Swift Weekly 中文 – Issue #175”

Vapor 4.0.0 Beta 3 发布

更新日志

Example output:

# TYPE http_requests_total counter
http_requests_total 0
http_requests_total{status="200", path="GET /hello/:name", method="GET"} 7
http_requests_total{status="200", path="GET /metrics", method="GET"} 3
# TYPE http_request_duration_seconds summary
http_request_duration_seconds{quantile="0.01"} 0.000115894
http_request_duration_seconds{quantile="0.05"} 0.000115894
...
  • Updates to RoutingKit beta 3 (#2126)

  • ClientResponse, HTTPStatus, and HTTPHeaders are now Codable (#2124)

  • Environment variables loaded from .env files can now be accessed immediately after Application has initialized (#2125)

.env (in same folder as Package.swift)

FOO=BAR

main.swift

let app = Application(...)
defer { app.shutdown() } 

let foo = Environment.get("FOO") 
print(foo) // BAR

iOS集成Flutter模块

本文主要介绍原生 iOS 应用如何集成 Flutter 模块。

安装 Flutter

Flutter 支持 macOSWindowsLinux

安装步骤可见官网:https://flutter.dev/docs/get-started/install

(注:本教程使用的是 macOS 系统)

iOS 集成 Flutter 模块

通过 Xcode 创建一个 iOS 原生项目,命名为 Example-Flutter

  1. 通过终端进入 Example-Flutter 当前目录下。
  2. 通过如下命令来创建 Flutter 模块。
flutter create --template module my_flutter

将会生成一个 my_flutter 新目录,结构如下:

my_flutter/
├─.ios/
│ ├─Runner.xcworkspace
│ └─Flutter/podhelper.rb
├─lib/
│ └─main.dart
├─test/
└─pubspec.yaml
  1. 通过 CocoaPods 集成 Flutter 模块。

Example-Flutter 项目目录下创建 Podfile 文件,内容编辑如下:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!

flutter_application_path = './my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'Example-Flutter' do
    install_all_flutter_pods(flutter_application_path)
end

然后,通过 pod install 命令集成 Flutter 模块到 iOS 工程项目中。

输出类似如下日志:

$ pod install
Analyzing dependencies
Downloading dependencies
Installing Flutter (1.0.0)
Installing FlutterPluginRegistrant (0.0.1)
Installing my_flutter (0.0.1)
Generating Pods project
Integrating client project

[!] Please close any current Xcode sessions and use `Example-Flutter.xcworkspace` for this project from now on.
Pod installation complete! There are 3 dependencies from the Podfile and 3 total pods installed.

此时,整个项目的目录结构类似是这样的。

some/path/Example-Flutter
├── Example-Flutter/
├── Example-Flutter.xcodeproj
├── Example-Flutter.xcworkspace
├── my_flutter/
│   └── .ios/
│       └── Flutter/
│         └── podhelper.rb
├── Podfile
├── Podfile.lock
└── Pods/
  1. 添加 Flutter 相关代码

修改 AppDelegate.swift 文件。

import FlutterPluginRegistrant
lazy var flutterEngine = FlutterEngine(name: "my flutter engine")

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    flutterEngine.run()
    GeneratedPluginRegistrant.register(with: self.flutterEngine);

    return true
}

然后在 iOS 原生页面上添加相应代码。

导入 Flutter package。

import Flutter

增加一个触发按钮,代码如下:

let button = UIButton(type:UIButton.ButtonType.custom)
button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside)
button.setTitle("Show Flutter!", for: UIControl.State.normal)
button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
button.backgroundColor = UIColor.blue
self.view.addSubview(button)

响应 Button 点击事件,代码如下:

@objc func showFlutter() {
  let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
  let flutterViewController =
      FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
  present(flutterViewController, animated: true, completion: nil)
}

运行效果

  • iOS 原生页面

  • 点击按钮后,显示 Flutter 页面效果。

示例代码

GitHub: iOS-Examples/Example-Flutter

iOS – UI事件传递与响应者链

UI事件传递

UIView 内部包含一个 hitTest 方法,用于 UI 事件的传递。

当一个触摸事件发生时,事件传递顺序如下:

UIApplication -> UIWindow -> 寻找合适的View

假设颜色对应的 View 关系如下:

黄色:View 1
绿色:View 1_1
灰色:View 1_1_1
红色:View 1_2

View 1 定义如下:

class View1: UIView {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        print("View 1 hitTest")

        return super.hitTest(point, with: event)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("View 1 touchesBegan")
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("View 1 touchesMoved")
    }

    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("View 1 touchesCancelled")
    }
}

同理,View11View111View12 定义类似于 View1

View 的层级关系如下:

View1 -> View11 -> View111

View1 -> View12

具体布局代码如下:

// view 1
let view1 = View1(frame: CGRect(x: 40, y: 100, width: 300, height: 400))
view1.backgroundColor = UIColor.yellow
self.view.addSubview(view1)

// view 1_1
let view11 = View11(frame: CGRect(x: 20, y: 30, width: 100, height: 120))
view11.backgroundColor = UIColor.green
view1.addSubview(view11)

// view 1_1_1
let view111 = View111(frame: CGRect(x: 10, y: 10, width: 60, height: 40))
view111.backgroundColor = UIColor.gray
view11.addSubview(view111)

// view 1_2
let view12 = View12(frame: CGRect(x: 20, y: 200, width: 100, height: 120))
view12.backgroundColor = UIColor.red
view1.addSubview(view12)

当一个触摸事件发生在灰色(View 1_1_1)区域时,hitTest 被调用的顺序如下。

View 1 hitTest
View 1_2 hitTest
View 1_1 hitTest
View 1_1_1 hitTest

表明触摸事件发生时,先从底层 View 开始逐级向上层进行遍历,直到找到灰色视图 View111

响应者链

寻找到合适的 View 后,将会响应对应 View 的触摸事件。

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    print("View 1_1_1 touchesBegan")
}

此处合适的 View 就是 View111,将输出日志:

View 1_1_1 touchesBegan

响应者链的传递顺序与 UI 事件传递顺序正好相反,如果当前 View (即:View 1_1_1)无法响应触摸事件(比如:isUserInteractionEnabled = false),将向父一级传递事件。此时,View 1_1 将收到触摸事件,如果 View 1_1 可交互的话,将响应对应的 touchesBegan 事件。

View 1_1 touchesBegan

否则继续往父一级遍历,直到找到合适的响应对象。

示例代码

GitHub: iOS-Examples/Example-View响应链

Swift语法入门

本文简要介绍下 Swift 的基本语法。

  • 导入 package
import Foundation
  • 变量和常量
var str1 = "Hello, playground"
let str2 = "test"
  • 数组
var myArray = [
    "t1",
    "t2",
    "t3"
]
  • 集合
let ttt: Set = [0, 1, 2, 3, 6, 9, 90, 2]
  • 字典
var myDict = [
    "k1": "v1",
    "k2": "v2",
    "k3": "v3"
]
  • 方法
func test(_ value: Int) -> Bool {
    if value > 10 {
        return true
    }

    // 编译错误,Swift强制if语句加{}
    //    if value > 10
    //        return true

    return false
}
  • 多重返回值函数
func test1(_ value: Int) -> (errorCode: Int, errorDes: String) {
    if value > 10 {
        return (200, "Success")
    }

    return (404, "Not Found")
}

let r = test(100)
let r1 = test1(90)
print("result1 = (\(r1.0), \(r1.1))")
print("result1 = (\(r1.errorCode), \(r1.errorDes))")
  • 区间运算符
// 闭区间运算符(a...b)定义一个包含从a到b(包括a和b)的所有值的区间,b必须大于a。
for index in 1...5 {
    print("index = \(index)")
}
// 半开区间运算符
for index in 1..<5 {
    print("index = \(index)")
}
  • 闭包
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

func backwards(s1: String, s2: String) -> Bool {
    return s1 > s2
}

func closureTest(_ param1: Int, param2: (_ s1: String, _ s2: String) -> Bool) {
    print("param1 = \(param1), param2 = \(param2("d", "b"))")
}

closureTest(1, param2: backwards)
closureTest(2, param2: {
    (s1: String, s2: String) -> Bool in

    return s1 < s2
})
closureTest(3, param2: {
    (s1, s2) in // 类型可自动推倒出来

    return s1 < s2
})
closureTest(4, param2: {(s1, s2) in return s1 < s2})
closureTest(5, param2: {$0 > $1})
// 运算符函数
closureTest(6, param2: >)
  • 尾随闭包
func trailingClosuresTest(_ param: () -> ()) {
    print("trailingClosuresTest 1")

    param()

    print("trailingClosuresTest 2")
}

// 以下是不使用尾随闭包进行函数调用
trailingClosuresTest({
    () -> () in

    print("trailingClosuresTest 3")
})
trailingClosuresTest({
    print("trailingClosuresTest 4")
})

// 以下是使用尾随闭包进行函数调用
trailingClosuresTest() {
    print("trailingClosuresTest 5")
}

// 如果函数只需要闭包表达式一个参数,当使用尾随闭包时,甚至可以把()省略掉。
trailingClosuresTest {
    print("trailingClosuresTest 6")
}
  • 捕获值
func makeIncrementor(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 1
    func incrementor() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementor
}

let capture1 = makeIncrementor(forIncrement: 9)
print(capture1())
print(capture1())

print 输出如下:

10
19

  • 枚举
enum Barcode {
    case UPCA(Int, Int, Int)
    case QRCode(String)
}

func enumTest(value: Barcode) {
    switch(value) {
    case Barcode.UPCA(let numberSystem, let identifier, let check):
        print("UPCA numberSystem = \(numberSystem), identifier = \(identifier), check = \(check)")
    case Barcode.QRCode(let productCode):
        print("QRCode productCode = \(productCode)")
    }
}

enumTest(Barcode.QRCode("qs"))
enumTest(Barcode.UPCA(1, 2, 3))

enum Planet: Int {
    case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}

enum StrEnum: String {
    case T1 = "a"
    case T2 = "b"
}
  • 计算属性
struct Point {
    var x = 0.0
    var y = 0.0
}
struct Size {
    var width = 0.0
    var height = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2.0)
            let centerY = origin.y + (size.height / 2.0)

            return Point(x: centerX, y: centerY)
        }

        set {
            origin.x = newValue.x - (size.width / 2.0)
            origin.y = newValue.y - (size.height / 2.0)
        }
    }
}

var square = Rect(origin: Point(x: 0.0, y: 0.0), size: Size(width: 10.0, height: 10.0))
print("square.center = \(square.center)")
square.center = Point(x: 15.0, y: 15.0)
print("square origin = \(square.origin)")

print 输出如下:

square.center = Point(x: 5.0, y: 5.0)
square origin = Point(x: 10.0, y: 10.0)

  • 属性观察器
class StepCounter {
    var totalSteps: Int = 0 {

        willSet(newTotalSteps) {
            print("1 totalSteps = \(totalSteps)")
        }
        didSet {

            if(totalSteps > oldValue) {
                print("2 totalSteps = \(totalSteps)")
            }
        }
    }
}

let stepCounter = StepCounter()
stepCounter.totalSteps = 100

print 输出如下:

1 totalSteps = 0
2 totalSteps = 100

  • 结构体
struct PointTest {
    var x = 0.0
    var y = 0.0
    mutating func moveBy(deltaX: Double, deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}

使用 mutating 关键字是为了能在该方法中修改 struct 的变量。

  • 构造、析构、! 、?
class Player {
    init() {
        print("Player init")
    }

    deinit {
        print("Player deinit")
    }

    func test() {

    }
}

var player: Player? = Player()    // 调用构造方法

if(nil != player) {
    player!.test()
}
// 等价于上面这种情况
player?.test()

player = nil    // 调用析构方法
  • 继承 - 单继承,多实现
class A {

}

class B: A {

}

protocol IA {

}

protocol IB {

}

protocol IC {

}

class C: A, IA, IB, IC {

}
  • extension
extension Double {
    var km: Double { return self * 1_000.0 }
    var m : Double { return self }
    var cm: Double { return self / 100.0 }
    var mm: Double { return self / 1_000.0 }
    var ft: Double { return self / 3.28084 }
}

let t = 45.7.km
print(t)

print 输出如下:

45700.0

Vapor 4.0.0 Beta 2 发布

更新日志

  • Services has been refactored to be more type-safe. (#2098)

Services, Container, and Provider have been replaced by a pattern built on Swift extensions. This is best explained with examples.

Telling Vapor to use Leaf:

import Leaf
import Vapor

// beta.1
s.register(ViewRenderer.self) { c in
    return c.make(LeafRenderer.self)
}

// beta.2
app.views.use(.leaf)

Registering a factory service:

// beta.1
s.register(Foo.self) { c in
    return Foo(...)
}
app.make(Foo.self).bar()

// beta.2
extension Application {
    var foo: Foo { 
        return Foo(...)
    }
}
app.foo.bar()

Registering a singleton service:

// beta.1
s.register(Foo.self) { c in
    return Foo(...)
}
app.make(Foo.self).bar = 0
app.make(Foo.self).bar += 1
print(app.make(Foo.self).bar) // 1

// beta.2
extension Application {
    var foo: Foo { 
        if let existing = self.storage[FooKey.self] as? Foo {
            return existing
        } else {
            let new = Foo()
            self.storage[FooKey.self] = new
            return new
        }
    }

    private struct FooKey: StorageKey { 
        typealias Value = Foo
    }
}

app.foo.bar = 0
app.foo.bar += 1
print(app.foo.bar) // 1

This new pattern of extending Application also works with Request:

extension Application {
    var foo: Foo { ... }
}

extension Request {
    var bar: Bar { 
        return self.application.foo.bar(for: self)
    }
}
  • Validations has been refactored to yield better and more type-safe errors (#2071)

  • Authentication methods are now grouped under a new req.auth helper (#2111)

  • All authenticators now accept Request in their authenticate methods (#2111)

  • Added new ErrorSource struct to the AbortError protocol (#2093)

This new struct makes it easier to pass around information about where an error came from. It also makes it easier to indicate that a given error has no source information. This helps the logger avoid muddying logs with useless error source information.

  • RouteBuilder HTTP method helpers now support an array of [PathComponent] (#2097)

  • User-provided HTTP headers are no longer ignored when using XCTVapor test methods (#2108)

  • Enabled test discovery on Linux (#2118)