Wrappres' Studio.

Wrappers' Swift Style Guide

字数统计: 2.5k阅读时长: 12 min
2019/02/11 Share

本文参考Airbnb’s Swift Style Guide
总结出我个人认为最合适的代码风格
感谢Airbnb

Goals(目标)

  • 让阅读和理解代码更加简单
  • 使代码更易维护
  • 减少小的编程错误
  • 减少代码的认知负担
  • 更注重代码的逻辑,而不是代码风格

PS:简洁不是我们最主要的追求。只有代码质量得到保证(例如:可读性等),我们才会适当追求简洁。

Xcode Formatting(Xcode 样式)

  • 每行中最多包含 100 个单词
  • 用 4 个空格来进行缩减
  • 在每一行末尾以及在文件的末尾不要留有无用的空格

Naming(命名)

类和协议使用大驼峰式大小写(也叫帕斯卡命名法 PascalCase)

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protocol SpaceThing {
// ...
}

class Spacefleet: SpaceThing {

enum Formation {
// ...
}

class Spaceship {
// ...
}

var ships: [Spaceship] = []
static let worldName: String = "Earth"

func addShip(_ ship: Spaceship) {
// ...
}
}

let myFleet = Spacefleet()

例外:如果私有属性支持具有更高访问级别的同名属性或方法,则可以在其前面加下划线
Why?
在某些场景里,对属性和方法用一个更具描述性的命名更加易读。

  • 类型擦除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class AnyRequester<ModelType>: Requester {

public init<T: Requester>(_ requester: T) where T.ModelType == ModelType {
_executeRequest = requester.executeRequest
}

@discardableResult
public func executeRequest(_ request: URLRequest,
onSuccess: @escaping (ModelType, Bool) -> Void,
onFailure: @escaping (Error) -> Void) -> URLSessionCancellable
{
return _executeRequest(request, session, parser, onSuccess, onFailure)
}

private let _executeRequest: (URLRequest, @escaping (ModelType, Bool) -> Void, @escaping (NSError) -> Void) -> URLSessionCancellable
}
  • 用一个更具描述的命名更好
1
2
3
4
5
6
7
8
final class ExperiencesViewController: UIViewController {
// We can't name this view since UIViewController has a view: UIView property.
private lazy var _view = CustomView()

loadView() {
self.view = _view
}
}

用类似isSpaceshiphasSpacesuit来命名布尔(booleans)类型

让人更加明确这是一个布尔(booleans)类型

首字母缩略词(如:URL)应该是所有字母保持大写,除非作为名称开头应该为小写

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// WRONG
class UrlValidator {

func isValidUrl(_ URL: URL) -> Bool {
// ...
}

func isUrlReachable(_ URL: URL) -> Bool {
// ...
}
}

let URLValidator = UrlValidator().isValidUrl(/* some URL */)

// RIGHT
class URLValidator {

func isValidURL(_ url: URL) -> Bool {
// ...
}

func isURLReachable(_ url: URL) -> Bool {
// ...
}
}

let urlValidator = URLValidator().isValidURL(/* some URL */)

命名时应先写其普遍的部分,再写其特殊的部分

其中普遍的部分,要根据上下文来理解,但可以粗略的理解为‘当你搜索时,最能帮助你缩小范围的部分’。更重要的是你需要有条理的去管理你的命名。
例如:

1
2
3
4
5
6
7
8
9
10
11
// WRONG
let rightTitleMargin: CGFloat
let leftTitleMargin: CGFloat
let bodyRightMargin: CGFloat
let bodyLeftMargin: CGFloat

// RIGHT
let titleMarginRight: CGFloat
let titleMarginLeft: CGFloat
let bodyMarginRight: CGFloat
let bodyMarginLeft: CGFloat

如果名称不明确,则在名称中包含有关类型的提示

例如:

1
2
3
4
5
6
7
// WRONG
let title: String
let cancel: UIButton

// RIGHT
let titleText: String
let cancelButton: UIButton

处理 Event 的函数应该用过去时态(可忽略)

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

// WRONG
class ExperiencesViewController {

private func handleBookButtonTap() {
// ...
}

private func modelChanged() {
// ...
}
}

// RIGHT
class ExperiencesViewController {

private func didTapBookButton() {
// ...
}

private func modelDidChange() {
// ...
}
}

避免 Objective-C 风格的缩写前缀

例如:

1
2
3
4
5
6
7
8
9
// WRONG
class AIRAccount {
// ...
}

// RIGHT
class Account {
// ...
}

避免在非视图控制器的类中是使用 Controller 单词

Style(风格)

当可以轻易推断出类型时,不显示的书写类型,保持代码简洁

例如:

1
2
3
4
5
// WRONG
let host: Host = Host()

// RIGHT
let host = Host()

1
2
3
4
5
6
7
8
9
10
11
12
enum Direction {
case left
case right
}

func someDirection() -> Direction {
// WRONG
return Direction.left

// RIGHT
return .left
}

只有在必须时(即:在消除歧义或语言要求时),才使用 self 关键词

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
final class Listing {

init(capacity: Int, allowsPets: Bool) {
// WRONG
self.capacity = capacity
self.isFamilyFriendly = !allowsPets // `self.` not required here

// RIGHT
self.capacity = capacity
isFamilyFriendly = !allowsPets
}

private let isFamilyFriendly: Bool
private var capacity: Int

private func increaseCapacity(by amount: Int) {
// WRONG
self.capacity += amount

// RIGHT
capacity += amount

// WRONG
self.save()

// RIGHT
save()
}
}

在多行数组的最后一个元素后加上逗号

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
// WRONG
let rowContent = [
listingUrgencyDatesRowContent(),
listingUrgencyBookedRowContent(),
listingUrgencyBookedShortRowContent()
]

// RIGHT
let rowContent = [
listingUrgencyDatesRowContent(),
listingUrgencyBookedRowContent(),
listingUrgencyBookedShortRowContent(),
]

对元组的每个成员进行命名,增加可读性

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// WRONG
func whatever() -> (Int, Int) {
return (4, 4)
}
let thing = whatever()
print(thing.0)

// RIGHT
func whatever() -> (x: Int, y: Int) {
return (x: 4, y: 4)
}

// THIS IS ALSO OKAY
func whatever2() -> (x: Int, y: Int) {
let x = 4
let y = 4
return (x, y)
}

let coord = whatever()
coord.x
coord.y

对于CGRectCGPointNSRange等使用构造函数,还是不make函数

例如:

1
2
3
4
5
// WRONG
let rect = CGRectMake(10, 10, 10, 10)

// RIGHT
let rect = CGRect(x: 0, y: 0, width: 10, height: 10)

使用现代的 Swift 扩展方法,而不是 Objective-C 的全局扩展

例如:

1
2
3
4
5
6
7
// WRONG
var rect = CGRectZero
var width = CGRectGetWidth(rect)

// RIGHT
var rect = CGRect.zero
var width = rect.width

冒号紧跟着前一个词,后面加一个空格

例如:

1
2
3
4
5
// WRONG
var something : Double = 0

// RIGHT
var something: Double = 0

1
2
3
4
5
6
7
8
9
// WRONG
class MyClass : SuperClass {
// ...
}

// RIGHT
class MyClass: SuperClass {
// ...
}
1
2
3
4
5
6
// WRONG
var dict = [KeyType:ValueType]()
var dict = [KeyType : ValueType]()

// RIGHT
var dict = [KeyType: ValueType]()

在返回箭头的两边都加上空格,增加可读性

例如:

1
2
3
4
5
6
7
8
9
// WRONG
func doSomething()->String {
// ...
}

// RIGHT
func doSomething() -> String {
// ...
}

1
2
3
4
5
6
7
8
9
// WRONG
func doSomething(completion: ()->Void) {
// ...
}

// RIGHT
func doSomething(completion: () -> Void) {
// ...
}

省略不必要的括号

例如:

1
2
3
4
5
6
7
8
9
10
11
// WRONG
if (userCount > 0) { ... }
switch (someValue) { ... }
let evens = userCounts.filter { (number) in number % 2 == 0 }
let squares = userCounts.map() { $0 * $0 }

// RIGHT
if userCount > 0 { ... }
switch someValue { ... }
let evens = userCounts.filter { number in number % 2 == 0 }
let squares = userCounts.map { $0 * $0 }

当所有参数都没有标记时,忽略 case 语句中的枚举关联值

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// WRONG
if case .done(_) = result { ... }

switch animal {
case .dog(_, _, _):
...
}

// RIGHT
if case .done = result { ... }

switch animal {
case .dog:
...
}

Functions(函数)

在函数定义时,忽略Void的返回类型

例如:

1
2
3
4
5
6
7
8
9
// WRONG
func doSomething() -> Void {
...
}

// RIGHT
func doSomething() {
...
}

Closures(闭包)

在闭包申明中优先使用Void返回类型,而不是()

例如:

1
2
3
4
5
6
7
8
9
// WRONG
func method(completion: () -> ()) {
...
}

// RIGHT
func method(completion: () -> Void) {
...
}

不被使用到参数应该写作_

例如:

1
2
3
4
5
6
7
8
9
// WRONG
someAsyncThing() { argument1, argument2, argument3 in
print(argument3)
}

// RIGHT
someAsyncThing() { _, _, argument3 in
print(argument3)
}

闭包的结束括号应该和开始括号具有相同的缩进

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// WRONG

match(pattern: pattern)
.compactMap { range in
return Command(string: contents, range: range)
}
.compactMap { command in
return command.expand()
}

values.forEach { value in
print(value)
}

// RIGHT

match(pattern: pattern)
.compactMap { range in
return Command(string: contents, range: range)
}
.compactMap { command in
return command.expand()
}

values.forEach { value in
print(value)
}

单行闭包要在大括号左右留出适当空间

例如:

1
2
3
4
5
// WRONG
let evenSquares = numbers.filter {$0 % 2 == 0}.map { $0 * $0 }

// RIGHT
let evenSquares = numbers.filter { $0 % 2 == 0 }.map { $0 * $0 }

Operators(操作符)

中置操作符左右都需要留有一个空格

宁愿使用圆括号(),也不要用多个操作符对语句进行可视分组。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
// WRONG
let capacity = 1+2
let capacity = currentCapacity ?? 0
let mask = (UIAccessibilityTraitButton|UIAccessibilityTraitSelected)
let capacity=newCapacity
let latitude = region.center.latitude - region.span.latitudeDelta/2.0

// RIGHT
let capacity = 1 + 2
let capacity = currentCapacity ?? 0
let mask = (UIAccessibilityTraitButton | UIAccessibilityTraitSelected)
let capacity = newCapacity
let latitude = region.center.latitude - (region.span.latitudeDelta / 2.0)

Patterns(模式)

尽可能在init时,初始化属性,而不要使用隐式解包

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// WRONG
class MyClass: NSObject {
var someValue: Int!

init() {
super.init()
someValue = 5
}
}

// RIGHT
class MyClass: NSObject {
var someValue: Int

init() {
someValue = 0
super.init()
}
}

避免在init方法中执行任何有意义或耗时的工作

避免执行打开数据库连接,发送网络请求,从硬盘中读取一个大的数据。如果你需要在对象生成之前完成一些事,你可以用类似start()的方法。

将属性观察器内复杂的代码,抽取到单独方法中

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// WRONG
class TextField {
var text: String? {
didSet {
guard oldValue != text else {
return
}

// Do a bunch of text-related side-effects.
}
}
}

// RIGHT
class TextField {
var text: String? {
didSet { updateText(from: oldValue) }
}

private func updateText(from oldValue: String?) {
guard oldValue != text else {
return
}

// Do a bunch of text-related side-effects.
}
}

将闭包中复杂的代码,抽取到单独的方法中

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//WRONG
class MyClass {

func request(completion: () -> Void) {
API.request() { [weak self] response in
if let strongSelf = self {
// Processing and side effects
}
completion()
}
}
}

// RIGHT
class MyClass {

func request(completion: () -> Void) {
API.request() { [weak self] response in
guard let strongSelf = self else { return }
strongSelf.doSomething(strongSelf.property)
completion()
}
}

func doSomething(nonOptionalParameter: SomeClass) {
// Processing and side effects
}
}

在代码块的开头优先使用guard

当所有的保护语句在顶部组合起来,而不是和业务逻辑混合起来,这样更容易对代码进行推理。

访问控制应该尽可能的严格

除非你需要,否则 public 优先于 open,privae 优先于 fileprivate

尽可能避免全局函数

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// WRONG
func age(of person, bornAt timeInterval) -> Int {
// ...
}

func jump(person: Person) {
// ...
}

// RIGHT
class Person {
var bornAt: TimeInterval

var age: Int {
// ...
}

func jump() {
// ...
}
}

如果常量是私有的,尽量放在文件的顶部

例如:

1
2
3
4
5
6
7
8
9
10
11
private let privateValue = "secret"

public class MyClass {

public static let publicValue = "something"

func doSomething() {
print(privateValue)
print(MyClass.publicValue)
}
}

只有在有语义含义时,才使用optionals

尽量使用不可以变的值

在数组操作中用mapcompactMap代替appending,用filter代替removing
例如:

1
2
3
4
5
6
7
8
9
// WRONG
var results = [SomeType]()
for element in input {
let result = transform(element)
results.append(result)
}

// RIGHT
let results = input.map { transform($0) }

1
2
3
4
5
6
7
8
9
10
// WRONG
var results = [SomeType]()
for element in input {
if let result = transformThatReturnsAnOptional(element) {
results.append(result)
}
}

// RIGHT
let results = input.compactMap { transformThatReturnsAnOptional($0) }

默认的静态方法

如果一个方法需要被复写,应该使用class
例如:

1
2
3
4
5
6
7
8
9
// WRONG
class Fruit {
class func eatFruits(_ fruits: [Fruit]) { ... }
}

// RIGHT
class Fruit {
static func eatFruits(_ fruits: [Fruit]) { ... }
}

尽量使用final关键词

如果一个class需要被继承,则不写final
例如:

1
2
3
4
5
6
7
8
9
// WRONG
class SettingsRepository {
// ...
}

// RIGHT
final class SettingsRepository {
// ...
}

switch一个enum时,永远不要使用default

enum要求遍历所有的case,如果使用default,在新加入一个case时,会造成不可以预见的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// WRONG
switch anEnum {
case .a:
// Do something
default:
// Do something else.
}

// RIGHT
switch anEnum {
case .a:
// Do something
case .b, .c:
// Do something else.
}

在检查optional是否是 nil 时,如果不需要它的值,不要使用值的绑定

例如:

1
2
3
4
5
6
7
8
9
10
11
var thing: Thing?

// WRONG
if let _ = thing {
doThing()
}

// RIGHT
if thing != nil {
doThing()
}

File Organization(文件组织)

按字母排序书写import语句,在import语句前,不要留有空行

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// WRONG

// Copyright © 2018 Airbnb. All rights reserved.
//
import DLSPrimitives
import Constellation
import Epoxy

import Foundation

//RIGHT

// Copyright © 2018 Airbnb. All rights reserved.
//

import Constellation
import DLSPrimitives
import Epoxy
import Foundation

例外:@testable import应该空一行,另起一组
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// WRONG

// Copyright © 2018 Airbnb. All rights reserved.
//

import DLSPrimitives
@testable import Epoxy
import Foundation
import Nimble
import Quick

//RIGHT

// Copyright © 2018 Airbnb. All rights reserved.
//

import DLSPrimitives
import Foundation
import Nimble
import Quick

@testable import Epoxy

垂直的空格分隔只能为一行

文件应该最后留有一个空行

感谢阅读,祝您生活愉快

CATALOG
  1. 1. Goals(目标)
  2. 2. Xcode Formatting(Xcode 样式)
  3. 3. Naming(命名)
    1. 3.1. 类和协议使用大驼峰式大小写(也叫帕斯卡命名法 PascalCase)
    2. 3.2. 用类似isSpaceship。hasSpacesuit来命名布尔(booleans)类型
    3. 3.3. 首字母缩略词(如:URL)应该是所有字母保持大写,除非作为名称开头应该为小写
    4. 3.4. 命名时应先写其普遍的部分,再写其特殊的部分
    5. 3.5. 如果名称不明确,则在名称中包含有关类型的提示
    6. 3.6. 处理 Event 的函数应该用过去时态(可忽略)
    7. 3.7. 避免 Objective-C 风格的缩写前缀
    8. 3.8. 避免在非视图控制器的类中是使用 Controller 单词
  4. 4. Style(风格)
    1. 4.1. 当可以轻易推断出类型时,不显示的书写类型,保持代码简洁
    2. 4.2. 只有在必须时(即:在消除歧义或语言要求时),才使用 self 关键词
    3. 4.3. 在多行数组的最后一个元素后加上逗号
    4. 4.4. 对元组的每个成员进行命名,增加可读性
    5. 4.5. 对于CGRect,CGPoint,NSRange等使用构造函数,还是不make函数
    6. 4.6. 使用现代的 Swift 扩展方法,而不是 Objective-C 的全局扩展
    7. 4.7. 冒号紧跟着前一个词,后面加一个空格
    8. 4.8. 在返回箭头的两边都加上空格,增加可读性
    9. 4.9. 省略不必要的括号
    10. 4.10. 当所有参数都没有标记时,忽略 case 语句中的枚举关联值
  5. 5. Functions(函数)
    1. 5.1. 在函数定义时,忽略Void的返回类型
  6. 6. Closures(闭包)
    1. 6.1. 在闭包申明中优先使用Void返回类型,而不是()
    2. 6.2. 不被使用到参数应该写作_
    3. 6.3. 闭包的结束括号应该和开始括号具有相同的缩进
    4. 6.4. 单行闭包要在大括号左右留出适当空间
  7. 7. Operators(操作符)
    1. 7.1. 中置操作符左右都需要留有一个空格
  8. 8. Patterns(模式)
    1. 8.1. 尽可能在init时,初始化属性,而不要使用隐式解包
    2. 8.2. 避免在init方法中执行任何有意义或耗时的工作
    3. 8.3. 将属性观察器内复杂的代码,抽取到单独方法中
    4. 8.4. 将闭包中复杂的代码,抽取到单独的方法中
    5. 8.5. 在代码块的开头优先使用guard
    6. 8.6. 访问控制应该尽可能的严格
    7. 8.7. 尽可能避免全局函数
    8. 8.8. 如果常量是私有的,尽量放在文件的顶部
    9. 8.9. 只有在有语义含义时,才使用optionals
    10. 8.10. 尽量使用不可以变的值
    11. 8.11. 默认的静态方法
    12. 8.12. 尽量使用final关键词
    13. 8.13. 在switch一个enum时,永远不要使用default
    14. 8.14. 在检查optional是否是 nil 时,如果不需要它的值,不要使用值的绑定
  9. 9. File Organization(文件组织)
    1. 9.1. 按字母排序书写import语句,在import语句前,不要留有空行
    2. 9.2. 垂直的空格分隔只能为一行
    3. 9.3. 文件应该最后留有一个空行
  10. 10. 感谢阅读,祝您生活愉快