Swift style guide.
Introduction
API Design Guidelines是蘋果專門針對API的一個規范,本規范涉及到一些相關的內容,大都是保持和蘋果的規范一致。
它絕大部分內容,集合了The Official raywenderlich.com Swift Style Guide.和GitHub's Swift Style Guide,并刪減了一些不適合編碼的規則。
同時,Swift語言在快速的發展中,這個規范也會隨著Swift的發展、以及對Swift更多的使用和了解,持續地進行修改和完善。
Table of Contents
- Correctness
- Naming
- Code Organization
- Spacing
- Comments
- Classes and Structures
- Function Declarations
- Closure Expressions
- Types
- Functions vs Methods
- Memory Management
- Access Control
- Control Flow
- Golden Path
- Semicolons
- Parentheses
- Use whitespace around operator definitions
- 和Xcode集成的格式檢查工具
- Document Revision History
<h2 id="correctness"> Correctness </h2>
在Swift中把warnings當做errors。該規則會解決掉很多其他的代碼格式化規則,比如不要使用 ++
和--
操作符、不要使用C類型的for循環、不要直接使用字符串作為selector等。
<h2 id="naming"> Naming </h2>
classes, structures, enumerations 和 protocols采用首字母大寫的駝峰命名法,method names and variables采用首字母小寫的駝峰命名法。
Preferred:
swift
private let maximumWidgetCount = 100
class WidgetContainer {
var widgetButton: UIButton
let widgetHeightPercentage = 0.85
}
Not Preferred:
swift
let MAX_WIDGET_COUNT = 100
class app_widgetContainer {
var wBut: UIButton
let wHeightPct = 0.85
}
縮寫應該避免,但如URL、ID這種常見的縮寫可以使用。
API Design Guidelines中提到,如果使用這些縮寫,字母應該全為大寫或者小寫。Examples:
Preferred
let urlString: URLString
let userID: UserID
Not Preferred
let uRLString: UrlString
let userId: UserId
使用argument label,替代注釋,使代碼self-documenting,可讀性更強。
swift
func convertPointAt(column: Int, row: Int) -> CGPoint
func timedAction(afterDelay delay: NSTimeInterval, perform action: SKAction) -> SKAction!
// would be called like this:
convertPointAt(column: 42, row: 13)
timedAction(afterDelay: 1.0, perform: someOtherAction)
<h3 id="protocols"> Protocols </h3>
按照蘋果的API Design Guidelines,Protocols名字可以使用名詞來描述這個Protocol的內容,比如Collection
, WidgetFactory
。
或以-ing、-able結尾來描述Protocol實現的一些功能,比如: Equatable
, Resizing
。
<h3 id="enumerations"> Enumerations </h3>
按照蘋果的API Design Guidelines,枚舉值使用小寫開頭的駝峰命名法,也就是lowerCamelCase。
swift
enum Shape {
case rectangle
case square
case rightTriangle
case equilateralTriangle
}
<h3 id="class-prefixes"> Class Prefixes </h3>
在Swift里面,每一個module都是一個namesapce。而在ObjC里沒有namespace的概念,只是在每個類名前面添加前綴,比如NSArray。
當不同的module有同名的類名時,需要指明module name。
swift
import SomeModule
let myClass = MyModule.UsefulClass()
<h3 id="selectors"> Selectors </h3>
Selectors是在ObjC中為許多Cocoa and Cocoa Touch APIs做處理的函數。在Swift2.2,我們可以使用類型不安全的字符串來指定一個Selector。但在Swift3,這種方式將使Xcode報一個警告,在警告的"Fix it"按鈕里面,會使用完全類型安全的方式去替換這個不安全的字符串。并且我們經常能夠使用代碼所在的上下文來簡化Swift3中Selector的表達式。
Preferred:
swift
let sel = #selector(viewDidLoad)
Not Preferred:
swift
let sel = #selector(ViewController.viewDidLoad)
<h3 id="generics"> Generics </h3>
范型的類型名,應該是描述性的名詞、以大寫開頭的駝峰命名。如果不能起一個有意義的關系或角色名稱,可以使用T
、U
或V
。
Preferred:
swift
struct Stack<Element> { ... }
func writeTo<Target: OutputStream>(inout target: Target)
func max<T: Comparable>(x: T, _ y: T) -> T
Not Preferred:
swift
struct Stack<T> { ... }
func writeTo<target: OutputStream>(inout t: target)
func max<Thing: Comparable>(x: Thing, _ y: Thing) -> Thing
<h3 id="language"> Language </h3>
為了和蘋果的API匹配,使用美式英語。
Preferred:
swift
let color = "red"
Not Preferred:
swift
let colour = "red"
<h2 id="code-organization"> Code Organization </h2>
多使用extension來組織代碼。每個 extensions使用// MARK: -
來分隔開。
<h3 id="protocol-conformance"> Protocol Conformance </h3>
當一個類遵守一個協議時,使用extension來組織代碼。
Preferred:
swift
class MyViewcontroller: UIViewController {
// class stuff here
}
// MARK: - UITableViewDataSource
extension MyViewcontroller: UITableViewDataSource {
// table view data source methods
}
// MARK: - UIScrollViewDelegate
extension MyViewcontroller: UIScrollViewDelegate {
// scroll view delegate methods
}
Not Preferred:
swift
class MyViewcontroller: UIViewController, UITableViewDataSource, UIScrollViewDelegate {
// all methods
}
<h3 id="unused-code"> Unused Code </h3>
能刪除的代碼,都刪掉。
Not Preferred:
swift
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return Database.contacts.count
}
Preferred:
swift
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Database.contacts.count
}
<h3 id="minimal-imports"> Minimal Imports </h3>
盡可能減少依賴和import。比如只需要引入Foundation的時候,就不要引入UIKit。
<h2 id="spacing"> Spacing </h2>
- 縮進使用4個空格(該處只要團隊統一就行):
- 方法體的花括號和其它的花括號(
if
/else
/switch
/while
etc.),需要加入一個空格后在行尾開啟,在新一行關閉(Xcode默認)。 - 提示:?A選中代碼后使用Control-I (或者菜單Editor\Structure\Re-Indent)來調整縮進.
Preferred:
swift
if user.isHappy {
// Do something
} else {
// Do something else
}
Not Preferred:
swift
if user.isHappy
{
// Do something
}
else {
// Do something else
}
- methods之間只留一個空行。methods內部,使用空行來分隔不同功能的代碼,為不同的section。一個method內部的section不宜太多,否則應該考慮分拆成不同函數。
- 冒號左邊沒有空格,右邊有一個空格。Exception:
? :
和[:]
。
Preferred:
swift
class TestDatabase: Database {
var data: [String: CGFloat] = ["A": 1.2, "B": 3.2]
}
Not Preferred:
swift
class TestDatabase : Database {
var data :[String:CGFloat] = ["A" : 1.2, "B":3.2]
}
<h2 id="comments"> Comments </h2>
盡可能避免大量使用注釋,好的代碼應該盡可能是self-documenting。
如果需要注釋,它只用來解釋為什么這段代碼要這么寫,而不是解釋代碼的邏輯。
代碼變化時也需要馬上更新注釋,不能有誤導。如果不能及時更新就刪掉該處注釋。
Exception: 上面兩條不適用于生成文檔用的注釋.
<h2 id="classes-and-structures"> Classes and Structures </h2>
應該使用哪一個?
記住,struct具有值語義。沒有id(唯一標識符)的事物就應該使用struct。比如一個含有[a, b, c]的數組和另一個含有[a, b, c]的數組是完全可交換的。你使用第一個數組還是第二個完全沒有關系,因為他們代表同一個事物,這也是為什么在Swift里面數組是用struct結構來表示的。
而類具有引用語義。具有id(唯一標識符)或者具有特定生命周期的事物就應該使用類來表示。你將使用類來表示人的數據結構,因為兩個人完全是不同的事物,只是因為兩個人具有相同名字和生日,并不意味著這是相同的一個人。但是一個人的生日可以使用struct來表示,因為一個變量中的1950年3月3號和另一個的1950年3月3號是完全相同的。日期是不具有id的。
但有些時候,一個事物應該是struct的,但需要遵循AnyObject
或者由于歷史原因被模型化為類了(NSDate
, NSSet
)。除了這些異常情況,盡量遵循該條原則。
<h3 id="example-definition"> Example definition </h3>
下面是一個比較規范的Class定義:
swift
class Circle: Shape {
var x: Int, y: Int
var radius: Double
var diameter: Double {
get {
return radius * 2
}
set {
radius = newValue / 2
}
}
init(x: Int, y: Int, radius: Double) {
self.x = x
self.y = y
self.radius = radius
}
convenience init(x: Int, y: Int, diameter: Double) {
self.init(x: x, y: y, radius: diameter / 2)
}
func describe() -> String {
return "I am a circle at \(centerString()) with an area of \(computeArea())"
}
override func computeArea() -> Double {
return M_PI * radius * radius
}
private func centerString() -> String {
return "(\(x),\(y))"
}
}
上面的例子,給我們演示了這些規范:
- 冒號在用于指明類型時,左邊沒有空格,右邊有一個空格。
- 當一組變量、常量有關聯時,定義在一行里。
- 函數修飾符
internal
是缺省值,可以省略。重載一個函數時,訪問修飾符也可以省略掉。
<h3 id="use-of-self"> Use of Self </h3>
避免使用self來訪問屬性。除非需要區分函數參數和屬性。
swift
class BoardLocation {
let row: Int, column: Int
init(row: Int, column: Int) {
self.row = row
self.column = column
let closure = {
print(self.row)
}
}
}
<h3 id="computed-properties"> Computed Properties </h3>
Computed property一般是只讀,同時省略get clause。get clause只是當set clause存在時才需要寫。
Preferred:
swift
var diameter: Double {
return radius * 2
}
Not Preferred:
swift
var diameter: Double {
get {
return radius * 2
}
}
<h3 id="final"> Final </h3>
當一個類不想被繼承時,使用final
關鍵字。Example:
swift
// Turn any generic type into a reference type using this Box class.
final class Box<T> {
let value: T
init(_ value: T) {
self.value = value
}
}
<h2 id="function-declarations"> Function Declarations </h2>
在一行中保持簡短的函數聲明,函數體的左方括號也需要放在這一行。
swift
func reticulateSplines(spline: [Double]) -> Bool {
// reticulate code goes here
}
如果一個函數簽名過長,則選擇合適的地方換行,并在新的一行加入足夠的縮進。
swift
func reticulateSplines(spline: [Double], adjustmentFactor: Double,
translateConstant: Int, comment: String) -> Bool {
// reticulate code goes here
}
<h2 id="closure-expressions"> Closure Expressions </h2>
方法的參數列表最后一參數類型為閉包時,可以使用尾閉包。但只在只存在一個閉包參數時才使用尾閉包。
Preferred:
swift
UIView.animateWithDuration(1.0) {
self.myView.alpha = 0
}
UIView.animateWithDuration(1.0,
animations: {
self.myView.alpha = 0
},
completion: { finished in
self.myView.removeFromSuperview()
}
)
Not Preferred:
swift
UIView.animateWithDuration(1.0, animations: {
self.myView.alpha = 0
})
UIView.animateWithDuration(1.0,
animations: {
self.myView.alpha = 0
}) { f in
self.myView.removeFromSuperview()
}
只有一個表達式的、用來返回值的閉包,可以省略return。
swift
attendeeList.sort { a, b in
a > b
}
<h2 id="types"> Types </h2>
盡可能使用Swift原生類型,而不是使用ObjC的NS類型。
Preferred:
swift
let width = 120.0 // Double
let widthString = (width as NSNumber).stringValue // String
Not Preferred:
swift
let width: NSNumber = 120.0 // NSNumber
let widthString: NSString = width.stringValue // NSString
但是有些情況例外,比如在寫Sprite Kit代碼時,用CGFloat
可以減少轉換。
<h3 id="constants"> constants </h3>
盡可能使用let,只有在需要使用變量的時候使用var。
Tip: 有一個辦法能達到上面的目的,就是自己只寫let,讓編譯器幫你確定哪些需要改成var。
使用static let
定義類常量,而不是實例常量,或者全局常量。
Preferred:
swift
enum Math {
static let e = 2.718281828459045235360287
static let pi = 3.141592653589793238462643
}
radius * Math.pi * 2 // circumference
Note: 使用枚舉定義常量的好處是讓常量定義在特定的命名空間。
Not Preferred:
swift
let e = 2.718281828459045235360287 // pollutes global namespace
let pi = 3.141592653589793238462643
radius * pi * 2 // is pi instance data or a global constant?
<h3 id="optionals"> Optionals </h3>
當訪問一個optional value前,需要訪問多個optional value時,使用optional chaining:
swift
self.textContainer?.textLabel?.setNeedsDisplay()
訪問一個optional value后,需要執行多個操作,可以使用optional binding:
swift
if let textContainer = self.textContainer {
// do many things with textContainer
}
給optional變量命名時,不要使用類似optionalString
或maybeView
這樣的命名,因為optional已經在類型聲明中體現。
相對應的,使用unwrapped value時,避免使用unwrappedView或
actualLabel這樣的命名,使用optional變量名就可以了。
Preferred:
swift
var subview: UIView?
var volume: Double?
// later on...
if let subview = subview, volume = volume {
// do something with unwrapped subview and volume
}
Not Preferred:
swift
var optionalSubview: UIView?
var volume: Double?
if let unwrappedSubview = optionalSubview {
if let realVolume = volume {
// do something with unwrappedSubview and realVolume
}
}
避免使用可選值的強制解析
如果你有一個變量 foo
是 FooType?
或者 FooType!
類型,如果可能,盡量不要使用強制解析來獲取 foo
真正的值。
Preferred:
swift
if let foo = foo {
// Use unwrapped `foo` value in here
} else {
// If appropriate, handle the case where the optional is nil
}
或者,在一些情況下你可以使用可選鏈,例如:
swift
// Call the function if `foo` is not nil. If `foo` is nil, ignore we ever tried to make the call
foo?.callSomethingIfFooIsNotNil()
顯示的 if let
可選綁定是安全的代碼范式。強制解析常常會引起運行時的崩潰。
避免使用隱士強制解析
如果 foo
可以是nil, 盡量使用 let foo: FooType?
這種范式,而不要使用let foo: FooType!
(一般來說,使用!
的地方都可以使用?
)
顯示的可選類型是安全的代碼范式。隱士強制解析有可能引起運行時的崩潰。
<h3 id="struct-initializers"> Struct Initializers </h3>
使用Swfit原生的struct initializers,而不是遺留的CGGeometry constructors。
Preferred:
swift
let bounds = CGRect(x: 40, y: 20, width: 120, height: 80)
let centerPoint = CGPoint(x: 96, y: 42)
Not Preferred:
swift
let bounds = CGRectMake(40, 20, 120, 80)
let centerPoint = CGPointMake(96, 42)
同樣的,使用struct-scope constants CGRect.infinite
, CGRect.null
, etc, 而不是global constants CGRectInfinite
, CGRectNull
, etc。
你也可以使用.zero
來給變量賦值,如var frame = CGRect.zero
。對于一個已經存在的變量也可以這樣bounds = .zero
<h3 id="lazy-initialization"> Lazy Initialization </h3>
合適的時候考慮使用lazy加載來更好的控制一個對象的生命周期。這對UIViewController
懶加載view特別有用。你可以使用一個形如 { }()
的立即調用的閉包,或者調用一個private的工廠函數。例如:
swift
lazy var locationManager: CLLocationManager = self.makeLocationManager()
private func makeLocationManager() -> CLLocationManager {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self
manager.requestAlwaysAuthorization()
return manager
}
Notes:
-
[unowned self]
在這里不需要,這里不會產生循環引用。 - 因為Location manager會彈出彈框詢問用戶定位的權限,所以這里控制它的生命周期很有意義。
<h3 id="type-inference"> Type Inference </h3>
盡量讓編譯器使用類型推斷來標示常量或者變量的類型,這樣可以簡短代碼。類型推斷也適用于小數據量(不為空)的數組和字典。只有當需要的時候,才指明類型,如CGFloat
or Int16
。
Preferred:
swift
let message = "Click the button"
let currentBounds = computeViewBounds()
var names = ["Mic", "Sam", "Christine"]
let maximumWidth: CGFloat = 106.5
Not Preferred:
swift
let message: String = "Click the button"
let currentBounds: CGRect = computeViewBounds()
let names = [String]()
空數組或者字典的類型標注
對于空的數組、字典需要顯示使用類型標注。對于初始化的時候使用大量、對方數組、字典常量初始化的情況也需要顯示使用類型標注。
Preferred:
swift
var names: [String] = []
var lookup: [String: Int] = [:]
Not Preferred:
swift
var names = [String]()
var lookup = [String: Int]()
NOTE: 準守這條規則意味著取一個描述性的名稱顯示更加重要。
<h3 id="syntactic-sugar"> Syntactic Sugar </h3>
盡可能使用語法糖。
Preferred:
swift
var deviceModels: [String]
var employees: [Int: String]
var faxNumber: Int?
Not Preferred:
swift
var deviceModels: Array<String>
var employees: Dictionary<Int, String>
var faxNumber: Optional<Int>
<h2 id="functions-vs-methods"> Functions vs Methods </h2>
不依附于任何class或type的函數被稱為free function,應該盡量避免使用,因為不太好找到這個方法。
Preferred
swift
let sorted = items.mergeSort() // easily discoverable
rocket.launch() // clearly acts on the model
Not Preferred
swift
let sorted = mergeSort(items) // hard to discover
launch(&rocket)
Free Function Exceptions(下面的函數明確應該是fress function,并且比較好理解)
swift
let tuples = zip(a, b) // feels natural as a free function (symmetry)
let value = max(x,y,z) // another free function that feels natural
<h2 id="memory-management"> Memory Management </h2>
代碼應該避免參數循環引用。分析你的對象引用視圖,并使用weak
和 unowned
避免不必要的強引用。同時可以選擇使用值類型(struct
, enum
)來避免循環引用。
擴展一個對象的生命周期
可以使用 [weak self]
和 guard let strongSelf = self else { return }
組合的方式來擴展一個對象的生命周期。當 self
明顯生命周期比使用它的閉包更長的時候,最好選擇使用 [unowned self]
。
Preferred
swift
resource.request().onComplete { [weak self] response in
guard let strongSelf = self else {
return
}
let model = strongSelf.updateModel(response)
strongSelf.updateUI(model)
}
Not Preferred
swift
// might crash if self is released before response returns
resource.request().onComplete { [unowned self] response in
let model = self.updateModel(response)
self.updateUI(model)
}
Not Preferred
swift
// deallocate could happen between updating the model and updating UI
resource.request().onComplete { [weak self] response in
let model = self?.updateModel(response)
self?.updateUI(model)
}
<h2 id="access-control"> Access Control </h2>
訪問修飾符應該放在靠前的位置,前面只能有static
、@IBAction
和 @IBOutlet
。
Preferred:
swift
class TimeMachine {
private dynamic lazy var fluxCapacitor = FluxCapacitor()
}
Not Preferred:
swift
class TimeMachine {
lazy dynamic private var fluxCapacitor = FluxCapacitor()
}
對于全局的定義,一點顯示的標示訪問修飾符
頂層的函數、類型、變量總是應該顯示的指明訪問修飾符:
swift
public var whoopsGlobalState: Int
internal struct TheFez {}
private func doTheThings(things: [Thing]) {}
但是對于不少頂層的聲明,如果可以盡量不要顯示指定訪問修飾符:
swift
internal struct TheFez {
var owner: Person = Joshaber()
}
對于頂層的(全局的)聲明很少有被指定為 internal
的情況,顯示的指明訪問修飾符可以保證這個頂層聲明的設計是被充分考慮過的。但在這些聲明里面,使用已有的訪問修飾符是更好的方式。
<h2 id="control-flow"> Control Flow </h2>
相比于 while-condition-increment
格式的循環,請盡量使用 for-in
這種方式。
Preferred:
swift
for _ in 0..<3 {
print("Hello three times")
}
for (index, person) in attendeeList.enumerate() {
print("\(person) is at position #\(index)")
}
for index in 0.stride(to: items.count, by: 2) {
print(index)
}
for index in (0...3).reverse() {
print(index)
}
Not Preferred:
swift
var i = 0
while i < 3 {
print("Hello three times")
i += 1
}
var i = 0
while i < attendeeList.count {
let person = attendeeList[i]
print("\(person) is at position #\(i)")
i += 1
}
<h2 id="golden-path"> Golden Path </h2>
嵌套的if
會讓代碼的縮進層次不齊(整齊的縮進被稱作Golden Path),會讓代碼可讀性變差,使用guard
來做函數輸入合法性檢查,可以減少if嵌套。
Preferred:
swift
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {
guard let context = context else {
throw FFTError.noContext
}
guard let inputData = inputData else {
throw FFTError.noInputData
}
// use context and input to compute the frequencies
return frequencies
}
Not Preferred:
swift
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {
if let context = context {
if let inputData = inputData {
// use context and input to compute the frequencies
return frequencies
}
else {
throw FFTError.noInputData
}
}
else {
throw FFTError.noContext
}
}
Preferred:
swift
guard let number1 = number1,
number2 = number2,
number3 = number3 else {
fatalError("impossible")
}
// do something with numbers
Not Preferred:
swift
if let number1 = number1 {
if let number2 = number2 {
if let number3 = number3 {
// do something with numbers
}
else {
fatalError("impossible")
}
}
else {
fatalError("impossible")
}
}
else {
fatalError("impossible")
}
<h3 id="failing-guards"> Failing Guards </h3>
Guard檢查失敗執行的語句應該退出當前方法,并且應該只有一條語句,如return
, throw
, break
, continue
, and fatalError()
。如果需要多行語句,考慮使用defer
。
<h2 id="semicolons"> Semicolons </h2>
Swift并不要求每行語句都必須以冒號結束,除了你需要在一行寫多個語句的情況下。
建議不要在一行寫多個語句。
這條規則的唯一例外是for-conditional-increment
語句。但是我們推薦使用for-in
來代替傳統的for語句。
Preferred:
swift
let swift = "not a scripting language"
Not Preferred:
swift
let swift = "not a scripting language";
NOTE: Swift不像JavaScript那樣認為省略掉句尾的冒號是不安全的
<h2 id="parentheses"> Parentheses </h2>
包住條件語句的圓括號應該省略。
Preferred:
swift
if name == "Hello" {
print("World")
}
Not Preferred:
swift
if (name == "Hello") {
print("World")
}
<h2 id="use-whitespace-around-operator-definitions"> Use whitespace around operator definitions </h2>
在自定義操作符的時候,使用空格作為分割符:
Preferred:
swift
func <| (lhs: Int, rhs: Int) -> Int
func <|< <A>(lhs: A, rhs: A) -> A
Not Preferred:
swift
func <|(lhs: Int, rhs: Int) -> Int
func <|<<A>(lhs: A, rhs: A) -> A
如果構成操作符的字符后面立即跟隨類型或者參數的字符,將使其可讀性變差。加入合適的空格將讓它看起來更加清晰。
<h2 id="和xcode集成的格式檢查工具"> 和Xcode集成的格式檢查工具 </h2>
SwiftLint是開源社區貢獻的一個Swift格式檢查工具,可以較好的和Xcode集成,并提供warning、errors和修復提示的工具。它使用的規則基本遵循GitHub's Swift Style Guide,是團隊很好的統一格式的一個輔助工具。
<h2 id="document-revision-history"> Document Revision History </h2>
- 2016-11-11 Created by xdyang
- 2016-11-30 修改搜索跳轉方式