Swift 编码规范

一、说明#

  • 本代码规范主要包含可以快速执行的内容,还会不断完善和修正,但已存在的部分不会有大的修改,主要是增变化主要在于增加新的内容。

  • 本代码规范默认为必须执行,只是建议而非强制执行的会用 “(不强制)” 文字标出。

  • 在新版本迭代开发过程,新加代码应严格按照本规范执行,在对之前文件进行修改时,也要积极主动的根据目前代码规范对文件进行局部重构,提高代码质量和可读性。

二、代码格式化#

1. 行数和列数的限制#

1.1 一个函数的长度不应该超过 60 行 (不强制)#

当函数长度超过 80 行后,应该将内部一些复杂的逻辑提炼出来,形成新的函数,然后调用之,微型重构工作也应该无处不在,而不是等项目完成后再来重构。

1.2 每行代码长度建议不超过 80 (不强制)#

建议每一行代码的长度超过 80 字符时做折行处理,处理时请以结构清晰为原则。通过 “Xcode => Preferences => TextEditing => 勾选 Show PageGuide / 输入 80 => OK” 来设置提醒。

1.3 每个类原则上不超过 600 行 (不强制)#

一个类不应该将很多复杂的逻辑揉合到一起来实现,我们约定当文件超过 600 行时,要考虑将这个文件进行拆分,可以使用协议方法来分离功能代码。如果逻辑过于复杂,则应该考虑从设计上将一些内部可以独立的逻辑提炼出来,形成新的类,以减轻单一类的复杂度。

2. 代码分段的使用#

推荐使用 // MARK: 将代码不同的处理段分隔开,分段名称的首母大写,如写中文也是可行,目的是可读性强,让别人一眼知道你的分段是干嘛的。方便在编辑器中快速定位到需要查看的代码。以下是一些建议:

2.1 Lifecycle 分段#

在 UIViewController 的子类实现文件中,建议添加一个 // MARK: - LifeCycle 分段,该分段包含,init、loadView、viewDidLoad、viewWillAppear、viewDidAppear、viewWillDisappear、viewDidDisappear、viewWillLayoutSubviews、deinit 等方法,并建议将 deinit 方法放到实现文件最前面。

建议将 Lifecycle 分段放到最前端,因为对一个类的阅读,往往是从 init、viewDidLoad 等方法开始的。示例:

    // MARK: - LifeCycle
    deinit {

    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    ...

2.2 代理分段#

建议为每一个代理添加一个分段,并且分段的名称应该是代理名称,代理名称务必正确拼写,这样可以通过 command + 单击来查看代理的定义,示例:

    // MARK: - UITableViewDataSource
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 0
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    }

2.3 Events 分段#

为 button 的点击事件,geture 的响应方法,KVO 的回调,Notification 的回调方法添加一个 events 分段,在该分段的方法中,调用对应业务的方法,对应业务的方法放在对应业务的分段。示例:

     // MARK: - Events
    func submit(_ sender:UIButton) {

    }

2.4 业务分段#

在一个类中,为相对比较独立的业务,或者是实现相对独立功能的方法进行分段,同一个类中业务不宜过多,业务过多时就应该考虑分拆该类了。示例:

     // MARK: - LoginLogic
     ...
     // MARK: - RegisterLogic
     ...

在 UIViewController 的实现文件中建议的分段及顺序如下图(根据情况选择需要哪些分段,不需要总是包含以下所有分段):

    // MARK: - LifeCycle

    // MARK: - Events

    // MARK: - Delegate

    // MARK: - xxx 按照业务划分

2.4 属性分段#

在一个类中,属性一般分为跟 view 相关和跟数据相关属性,跟 view 相关一般是 subviews 这种,比如 UILable,与数据相关一般是辅助性,比如 var isCheck: Bool = false 。示例:

    // MARK: - Data
    private var url: String = ""

    // MARK: - Subviews
    private lazy var webView: MQWKWebView = {
        let webView = MQWKWebView.init()
        webView.backgroundColor = UIColor.white
        return webView
    }()

三、代码规范#

一、命名#

1. 类型#

  • 类型名称(如 struct, enum, class, typedef, associatedtype, protocol 等)使用大驼峰命名法命名。

  • 变量和常量则以小驼峰命名法命名。

    enum Planet {
      case mercury, venus, earth, mars, jupiter, saturn, uranus,neptune
    }
    
    struct Person {
      var mobileNo: String
    }

2. 协议#

根据苹果接口设计指导准则,协议名称用来描述一些东西是什么的时候是名词,例如:Collection, WidgetFactory。若协议名称用来描述能力应该以 -ing, -able, 或 -ible 结尾,例如:Equatable, Resizing。

3. 类前缀#

  • Swift 中类别 (类,结构体) 在编译时会把模块设置为默认的命名空间,但是为了不冲突,相关项目默认使用。
委托#

在定义委托方法时,第一个未命名参数应是委托数据源。

正确代码:

func namePickerView(_ namePickerView: NamePickerView, didSelectName name: String)

func namePickerViewShouldReload(_ namePickerView: NamePickerView) -> Bool

错误代码:

func didSelectName(namePicker: NamePickerViewController, name: String)

func namePickerShouldReload() -> Bool
泛型#

泛型类参数应具有描述性,遵守 “大骆驼命名法”。如果一个参数名没有具体的含义,可以使用传统单大写字符,如 T, U, 或 V 等。

正确代码:

struct Stack<Element>{ ... }
func writeTo <Target: OutputStream>(to Target: inout Target)
func swap(_ a: inout T, _ b: inout T) 

错误代码:

struct Stack<T>{ ... }
func write<target: OutputStream> (to target: inout target)
func swap<Thing>(_ a: inout Thing, _ b: inout Thing)

二、代码逻辑#

1.self 推断#

  • 让编译器在所有允许的地方推断 self 。
  • 在 init 中设置参数时显性地使用 self 。
  • 在 non-escaping closures 中显性地使用 self 。
  • 在避免命名冲突时使用 self 。
    例:
class BoardLocation  {
    let row: Int, column: Int
    init(row: Int, column: Int)  {
        self.row = row
        self.column = column
        let closure = {
            print(self.row)    
         }  
    }
}

2. 常量#

  • 类常量应该在类型里声明为 static。使用 static 修饰常量可以允许他们在被引用的时候不需要实例化类型。
  • 除了单例以外,应尽量避免生成全局常量。
  • 定义常量使用 let 关键字,定义变量使用 var 关键字, 如果变量的值未来不会发生变化要使用常量。
struct PhysicsModel {
    static var speedOfLightInAVacuum = 299_792_458
}

class Spaceship {
    static let topSpeed = PhysicsModel.speedOfLightInAVacuum
    var speed: Double

    func fullSpeedAhead() {
        speed = Spaceship.topSpeed
    }
}

3. 计算型类型属性#

  • 当只需要继承 getter 方法时,返回简单的 Computed 属性即可。

正确代码:

class Example {
    var age: UInt32 {
        return arc4random()
    }
}

错误代码:

class Example {
    var age: UInt32 {
        get {
            return arc4random()
        }
    }
}
  • 如果你在属性中添加了 set 或者 didSet ,那么你应该显示地提供 get 方法。
class Person {
    var age: Int {
        get {
            return Int(arc4random())
        }
        set {
            print("That's not your age.")
        }
    }
}

4. 单例#

Swift 中单例很简单,Swift 的 runtime 会保证单例的创建并且采用线程安全的方式访问:

class ControversyManager {
    static let sharedInstance = ControversyManager()
    private init() {}
}

单例通常只需要访问 "sharedInstance" 的静态属性,除非你有不得已的原因去重命名它。注意,不要用静态函数或者全局函数去访问你的单例。

5. 错误处理#

可以使用 do/try/catch 机制,避免使用 try!try?

6. 可选值#

  • 声明一个函数的某个参数可以为 nil 时,用
  • 当你确定某个变量在使用时已经确定不是 nil 时,在后面加 !

7. 扩展声明周期#

用 [weak self] 和 guard let strongSelf = self else { return } 模式扩展生命周期。用 [weak self] 比 [unowned self] 更好。
正确代码:

resource.request().onComplete { 
    [weak self] response in
    guard let `self` = self else { return }
    let model = self.updateModel(response)
    self.updateUI(model)
}

resource.request().onComplete { [weak self] response in
  guard let strongSelf = self else { return }
  let model = strongSelf.updateModel(response)
  strongSelf.updateUI(model)
}

错误代码:

// might crash if self is released before response returns
resource.request().onComplete { [unowned self] response in
  let model = self.updateModel(response)
  self.updateUI(model)
}

// 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)
}

三、代码结构#

1. 注释#

  • 使用 Xcode8 自带的注释功能,快捷键 Option+Command+/
  • 使用 // MARK: 分隔代码(类似于 OC 中的#pragma mark

2. 缩进#

遵守 Xcode 内置的缩进格式

选中 control + i Xcode 自动缩进

3. 类型#

1. 类型声明时:紧跟属性,与类型之间加空格,等号两边须加一个空格#

正确代码:

let width = 120.0 
let height: Double = 120.0 
var title: String

错误代码:

let width=1.0
let height:Double  =  1.0
var title:String
2. 优先使用 Swift 原生类型,可以根据需要使用 Objective-C 提供的方法,因为 Swift 提供了到 Objective-C 的桥接。#

正确代码:

let width: Double = 120.0  // Double
let widthString = (width as NSNumber).stringValue // String

错误代码:

let width: NSNumber = 120.0 // NSNumber
let widthString: NSString=width.stringValue  // NSString

4. 协议一致性 (不强制)#

当一个对象要实现协议一致性时,推荐使用 extension 隔离协议中的方法集,这样让相关方法和协议集中显示在一起,也简化了类支持一个协议和实现相关方法的流程。
正确代码:

class MyViewcontroller: UIViewController {
      // 方法
} 

extension MyViewcontroller: UITableViewDataSource {
      // UITableViewDataSource 方法
}

extension MyViewcontroller: UIScrollViewDelegate {
      // UIScrollViewDelegate 方法
} 

错误代码:

class MyViewcontroller: UIViewController, UITableViewDataSource, UIScrollViewDelegate {
       // 所有的方法
}

5. 良好的类定义#

  • 属性,变量,常量和参数等在声明定义时,其中:符号后有空格,而:符号前没有空格,比如 x: Int, 和 Circle: Shape
  • 定义多个变量 / 数据结构时,出于相同的目的和上下文,可以定义在同一行。(不强制)
  • 缩进 getter,setter 的定义和属性观察器的定义。
  • 不需要添加 internal 这样的默认的修饰符。也不需要在重写一个方法时添加访问修饰符。
  • 给那些不打算被继承的类使用 final 修饰符
final class Circle: Shape {
  var x: Int, y: Int
  var radius: Double
  var diameter: Double{
      get {
        returnradius * 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))"
  }
}

6. 控制流程#

  • 循环使用 for-in 表达式,而不使用 while 表达式。
    正确代码:
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(from: 0, to: items.count, by: 2) {
  print(index)
}
for index in (0...3).reverse() {
    print(index)
}

错误代码:

var i=0 
while i<3 {
  print("Hello three times") 
  i+=1
}

var i=0
while i<3 {
  let person = attendeeList[i]
  print("\(person)is at position #\(i)")  
  i+=1
}
  • Switch 模块中不用显式使用 break。

7. 黄金路径 (不强制)#

当编码遇到条件判断时,左边的距离是黄金路径或幸福路径,因为路径越短,速度越快。不要嵌套 if 循环,多个返回语句是可以的。guard 就为此而生的。
正确代码:

func computeFFT(context: Context?, inputData: InputData?)  throws -> Frequencies {
    guard let context = context else {throwFFTError.NoContext }

    guard let inputData = inputData else { throwFFTError.NoInputData }

    //计算frequencies
    return frequencies
}

错误代码:

func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {
if let context = context {
    if let inputData = inputData {
           // 计算frequencies
          return frequencies    
    } else {
         throwFFTError.NoInputData    
     }  
   } else {
      throwFFTError.NoContext  
   }
}

当有多个条件需要用 guard 或 if let 解包,可用复合语句避免嵌套。

guard let number1=number1, number2=number2, number3=number3 else{
    fatalError("impossible") 
}
// 处理number

8. 语法糖#

//对空的数据和字典,使用类型注解
var names:  [String] = []
var lookup:  [String: Int] = [:]

var deviceModels: [String]
var employees: [Int:String]
var faxNumber: Int?

9. 函数#

自由函数不依附于任何类或类型,应该节制地使用。
正确代码:

let sorted = items.mergeSort() // 易发现性
rocket.launch()  // 可读性

错误代码:

let sorted = mergeSort(items)// 不易被发现
launch(&rocket)

天然的自由函数:

let value = max(x,y,z)  // another free function that feels natural

10. 用扩展只读属性 代替 单参数函数(不强制)#

正确代码:

extension Int {
    var doubleValue: Double {
        return Double(self)
    }
}

//调用
intValue.doubleValue

错误代码:

func doubleValue(_ value: Int) -> Double {
    return Double(value)
}

//调用
doubleValue(intValue)
本作品采用《CC 协议》,转载必须注明作者和本文链接