Airbnb 团队开源其 Swift 代码规范

2,587 阅读10分钟
原文链接: github.com

Goals

Following this style guide should:

  • Make it easier to read and begin understanding unfamiliar code.
  • Make code easier to maintain.
  • Reduce simple programmer errors.
  • Reduce cognitive load while coding.
  • Keep discussions on diffs focused on the code's logic rather than its style.

Note that brevity is not a primary goal. Code should be made more concise only if other good code qualities (such as readability, simplicity, and clarity) remain equal or are improved.

Guiding Tenets

  • This guide is in addition to the official Swift API Design Guidelines. These rules should not contradict that document.
  • These rules should not fight Xcode's ^ + I indentation behavior.
  • We strive to make every rule lintable:
    • If a rule changes the format of the code, it needs to be able to be reformatted automatically (either using SwiftLint autocorrect or SwiftFormat).
    • For rules that don't directly change the format of the code, we should have a lint rule that throws a warning.
    • Exceptions to these rules should be rare and heavily justified.

Table of Contents

  1. Xcode Formatting
  2. Naming
  3. Style
    1. Functions
    2. Closures
    3. Operators
  4. Patterns
  5. File Organization
  6. Objective-C Interoperability
  7. Contributors
  8. Amendments

Xcode Formatting

You can enable the following settings in Xcode by running this script, e.g. as part of a "Run Script" build phase.

  • (link) Each line should have a maximum column width of 100 characters.

    Why?

    Due to larger screen sizes, we have opted to choose a page guide greater than 80

  • (link) Use 2 spaces to indent lines.

  • (link) Trim trailing whitespace in all lines. SwiftFormat: trailingSpace

⬆ back to top

Naming

  • (link) Use PascalCase for type and protocol names, and lowerCamelCase for everything else. SwiftLint: type_name

    protocol SpaceThing {
      // ...
    }
    
    class Spacefleet: SpaceThing {
    
      enum Formation {
        // ...
      }
    
      class Spaceship {
        // ...
      }
    
      var ships: [Spaceship] = []
      static let worldName: String = "Earth"
    
      func addShip(_ ship: Spaceship) {
        // ...
      }
    }
    
    let myFleet = Spacefleet()

    Exception: You may prefix a private property with an underscore if it is backing an identically-named property or method with a higher access level

    Why?

    There are specific scenarios where a backing a property or method could be easier to read than using a more descriptive name.

    • Type erasure
    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
    
    }
    • Backing a less specific type with a more specific type
    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
      }
    }
  • (link) Name booleans like isSpaceship, hasSpacesuit, etc. This makes it clear that they are booleans and not other types.

  • (link) Acronyms in names (e.g. URL) should be all-caps except when it’s the start of a name that would otherwise be lowerCamelCase, in which case it should be uniformly lower-cased.

    // 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 */)
  • (link) Names should be written with their most general part first and their most specific part last. The meaning of "most general" depends on context, but should roughly mean "that which most helps you narrow down your search for the item you're looking for." Most importantly, be consistent with how you order the parts of your name.

    // 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
  • (link) Include a hint about type in a name if it would otherwise be ambiguous.

    // WRONG
    let title: String
    let cancel: UIButton
    
    // RIGHT
    let titleText: String
    let cancelButton: UIButton
  • (link) Event-handling functions should be named like past-tense sentences. The subject can be omitted if it's not needed for clarity.

    // WRONG
    class ExperiencesViewController {
    
      private func handleBookButtonTap() {
        // ...
      }
    
      private func modelChanged() {
        // ...
      }
    }
    
    // RIGHT
    class ExperiencesViewController {
    
      private func didTapBookButton() {
        // ...
      }
    
      private func modelDidChange() {
        // ...
      }
    }
  • (link) Avoid Objective-C-style acronym prefixes. This is no longer needed to avoid naming conflicts in Swift.

    // WRONG
    class AIRAccount {
      // ...
    }
    
    // RIGHT
    class Account {
      // ...
    }
  • (link) Avoid *Controller in names of classes that aren't view controllers.

    Why?

    Controller is an overloaded suffix that doesn't provide information about the responsibilities of the class.

⬆ back to top

Style

  • (link) Don't include types where they can be easily inferred.

    // WRONG
    let host: Host = Host()
    
    // RIGHT
    let host = Host()
    enum Direction {
      case left
      case right
    }
    
    func someDirection() -> Direction {
      // WRONG
      return Direction.left
    
      // RIGHT
      return .left
    }
  • (link) Don't use self unless it's necessary for disambiguation or required by the language. SwiftFormat: redundantSelf

    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()
      }
    }
  • (link) Add a trailing comma on the last element of a multi-line array. SwiftFormat: trailingCommas

    // WRONG
    let rowContent = [
      listingUrgencyDatesRowContent(),
      listingUrgencyBookedRowContent(),
      listingUrgencyBookedShortRowContent()
    ]
    
    // RIGHT
    let rowContent = [
      listingUrgencyDatesRowContent(),
      listingUrgencyBookedRowContent(),
      listingUrgencyBookedShortRowContent(),
    ]
  • (link) Name members of tuples for extra clarity. Rule of thumb: if you've got more than 3 fields, you should probably be using a struct.

    // 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
  • (link) Use constructors instead of Make() functions for CGRect, CGPoint, NSRange and others. SwiftLint: legacy_constructor

    // WRONG
    let rect = CGRectMake(10, 10, 10, 10)
    
    // RIGHT
    let rect = CGRect(x: 0, y: 0, width: 10, height: 10)
  • (link) Favor modern Swift extension methods over older Objective-C global methods. SwiftLint: legacy_cggeometry_functions SwiftLint: legacy_constant SwiftLint: legacy_nsgeometry_functions

    // WRONG
    var rect = CGRectZero
    var width = CGRectGetWidth(rect)
    
    // RIGHT
    var rect = CGRect.zero
    var width = rect.width
  • (link) Place the colon immediately after an identifier, followed by a space. SwiftLint: colon

    // WRONG
    var something : Double = 0
    
    // RIGHT
    var something: Double = 0
    // WRONG
    class MyClass : SuperClass {
      // ...
    }
    
    // RIGHT
    class MyClass: SuperClass {
      // ...
    }
    // WRONG
    var dict = [KeyType:ValueType]()
    var dict = [KeyType : ValueType]()
    
    // RIGHT
    var dict = [KeyType: ValueType]()
  • (link) Place a space on either side of a return arrow for readability. SwiftLint: return_arrow_whitespace

    // WRONG
    func doSomething()->String {
      // ...
    }
    
    // RIGHT
    func doSomething() -> String {
      // ...
    }
    // WRONG
    func doSomething(completion: ()->Void) {
      // ...
    }
    
    // RIGHT
    func doSomething(completion: () -> Void) {
      // ...
    }
  • (link) Omit unnecessary parentheses. SwiftFormat: redundantParens

    // 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 }
  • (link) Omit enum associated values from case statements when all arguments are unlabeled. SwiftLint: empty_enum_arguments

    // WRONG
    if case .done(_) = result { ... }
    
    switch animal {
    case .dog(_, _, _):
      ...
    }
    
    // RIGHT
    if case .done = result { ... }
    
    switch animal {
    case .dog:
      ...
    }

Functions

  • (link) Omit Void return types from function definitions. SwiftLint: redundant_void_return

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

Closures

  • (link) Favor Void return types over () in closure declarations. If you must specify a Void return type in a function declaration, use Void rather than () to improve readability. SwiftLint: void_return

    // WRONG
    func method(completion: () -> ()) {
      ...
    }
    
    // RIGHT
    func method(completion: () -> Void) {
      ...
    }
  • (link) Name unused closure parameters as underscores (_). SwiftLint: unused_closure_parameter

    Why?

    Naming unused closure parameters as underscores reduces the cognitive overhead required to read closures by making it obvious which parameters are used and which are unused.

    // WRONG
    someAsyncThing() { argument1, argument2, argument3 in
      print(argument3)
    }
    
    // RIGHT
    someAsyncThing() { _, _, argument3 in
      print(argument3)
    }
  • (link) Closure end braces should have the same indentation as the line with their opening brace. This makes it easier to follow control flow through closures. SwiftLint: closure_end_indentation

    // 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)
    }
  • (link) Single-line closures should have a space inside each brace. SwiftLint: closure_spacing

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

Operators

  • (link) Infix operators should have a single space on either side. Prefer parenthesis to visually group statements with many operators rather than varying widths of whitespace. This rule does not apply to range operators (e.g. 1...3) and postfix or prefix operators (e.g. guest? or -1). SwiftLint: operator_usage_whitespace

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

⬆ back to top

Patterns

  • (link) Prefer initializing properties at init time whenever possible, rather than using implicitly unwrapped optionals. A notable exception is UIViewController's view property. SwiftLint: implicitly_unwrapped_optional

    // WRONG
    class MyClass: NSObject {
    
      init() {
        super.init()
        someValue = 5
      }
    
      var someValue: Int!
    }
    
    // RIGHT
    class MyClass: NSObject {
    
      init() {
        someValue = 0
        super.init()
      }
    
      var someValue: Int
    }
  • (link) Avoid performing any meaningful or time-intensive work in init(). Avoid doing things like opening database connections, making network requests, reading large amounts of data from disk, etc. Create something like a start() method if these things need to be done before an object is ready for use.

  • (link) Extract complex property observers into methods. This reduces nestedness, separates side-effects from property declarations, and makes the usage of implicitly-passed parameters like oldValue explicit.

    // 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.
      }
    }
  • (link) Extract complex callback blocks into methods. This limits the complexity introduced by weak-self in blocks and reduces nestedness. If you need to reference self in the method call, make use of guard to unwrap self for the duration of the callback.

    //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
      }
    }
  • (link) Prefer using guard at the beginning of a scope.

    Why?

    It's easier to reason about a block of code when all guard statements are grouped together at the top rather than intermixed with business logic.

  • (link) Access control should be at the strictest level possible. Prefer public to open and private to fileprivate unless you need that behavior.

  • (link) Avoid global functions whenever possible. Prefer methods within type definitions.

    // WRONG
    func age(of person, bornAt timeInterval) -> Int {
      // ...
    }
    
    func jump(person: Person) {
      // ...
    }
    
    // RIGHT
    class Person {
      var bornAt: TimeInterval
    
      var age: Int {
        // ...
      }
    
      func jump() {
        // ...
      }
    }
  • (link) Prefer putting constants in the top level of a file if they are private. If they are public or internal, define them as static properties, for namespacing purposes.

    private let privateValue = "secret"
    
    public class MyClass {
    
      public static let publicValue = "something"
    
      func doSomething() {
        print(privateValue)
        print(MyClass.publicValue)
      }
    }
  • (link) Use caseless enums for organizing public or internal constants and functions into namespaces. Avoid creating non-namespaced global constants and functions. Feel free to nest namespaces where it adds clarity.

    Why?

    Caseless enums work well as namespaces because they cannot be instantiated, which matches their intent.

    enum Environment {
    
      enum Earth {
        static let gravity = 9.8
      }
    
      enum Moon {
        static let gravity = 1.6
      }
    }
  • (link) Use Swift's automatic enum values unless they map to an external source. Add a comment explaining why explicit values are defined. SwiftLint: redundant_string_enum_value

    Why?

    To minimize user error, improve readability, and write code faster, rely on Swift's automatic enum values. If the value maps to an external source (e.g. it's coming from a network request) or is persisted across binaries, however, define the values explicity, and document what these values are mapping to.

    This ensures that if someone adds a new value in the middle, they won't accidentally break things.

    // WRONG
    enum ErrorType: String {
      case error = "error"
      case warning = "warning"
    }
    
    enum UserType: String {
      case owner
      case manager
      case member
    }
    
    enum Planet: Int {
      case mercury = 0
      case venus = 1
      case earth = 2
      case mars = 3
      case jupiter = 4
      case saturn = 5
      case uranus = 6
      case neptune = 7
    }
    
    enum ErrorCode: Int {
      case notEnoughMemory
      case invalidResource
      case timeOut
    }
    
    // RIGHT
    enum ErrorType: String {
      case error
      case warning
    }
    
    /// These are written to a logging service. Explicit values ensure they're consistent across binaries.
    // swiftlint:disable redundant_string_enum_value
    enum UserType: String {
      case owner = "owner"
      case manager = "manager"
      case member = "member"
    }
    // swiftlint:enable redundant_string_enum_value
    
    enum Planet: Int {
      case mercury
      case venus
      case earth
      case mars
      case jupiter
      case saturn
      case uranus
      case neptune
    }
    
    /// These values come from the server, so we set them here explicitly to match those values.
    enum ErrorCode: Int {
      case notEnoughMemory = 0
      case invalidResource = 1
      case timeOut = 2
    }
  • (link) Use optionals only when they have semantic meaning.

  • (link) Prefer immutable values whenever possible. Use map and compactMap instead of appending to a new collection. Use filter instead of removing elements from a mutable collection.

    Why?

    Mutable variables increase complexity, so try to keep them in as narrow a scope as possible.

    // WRONG
    var results = [SomeType]()
    for element in input {
      let result = transform(element)
      results.append(result)
    }
    
    // RIGHT
    let results = input.map { transform($0) }
    // WRONG
    var results = [SomeType]()
    for element in input {
      if let result = transformThatReturnsAnOptional(element) {
        results.append(result)
      }
    }
    
    // RIGHT
    let results = input.compactMap { transformThatReturnsAnOptional($0) }
  • (link) Handle an unexpected but recoverable condition with an assert method combined with the appropriate logging in production. If the unexpected condition is not recoverable, prefer a precondition method or fatalError(). This strikes a balance between crashing and providing insight into unexpected conditions in the wild. Only prefer fatalError over a precondition method when the failure message is dynamic, since a precondition method won't report the message in the crash report. SwiftLint: fatal_error_message SwiftLint: force_cast SwiftLint: force_try SwiftLint: force_unwrapping

    func didSubmitText(_ text: String) {
      // It's unclear how this was called with an empty string; our custom text field shouldn't allow this.
      // This assert is useful for debugging but it's OK if we simply ignore this scenario in production.
      guard text.isEmpty else {
        assertionFailure("Unexpected empty string")
        return
      }
      // ...
    }
    
    func transformedItem(atIndex index: Int, from items: [Item]) -> Item {
      precondition(index >= 0 && index < items.count)
      // It's impossible to continue executing if the precondition has failed.
      // ...
    }
    
    func makeImage(name: String) -> UIImage {
      guard let image = UIImage(named: name, in: nil, compatibleWith: nil) else {
        fatalError("Image named \(name) couldn't be loaded.")
        // We want the error message so we know the name of the missing image.
      }
      return image
    }
  • (link) Default type methods to static.

    Why?

    If a method needs to be overridden, the author should opt into that functionality by using the class keyword instead.

    // WRONG
    class Fruit {
      class func eatFruits(_ fruits: [Fruit]) { ... }
    }
    
    // RIGHT
    class Fruit {
      static func eatFruits(_ fruits: [Fruit]) { ... }
    }
  • (link) Default classes to final.

    Why?

    If a class needs to be overridden, the author should opt into that functionality by omitting the final keyword.

    // WRONG
    class SettingsRepository {
      // ...
    }
    
    // RIGHT
    final class SettingsRepository {
      // ...
    }
  • (link) Never use the default case when switching over an enum.

    Why?

    Enumerating every case requires developers and reviewers have to consider the correctness of every switch statement when new cases are added.

    // WRONG
    switch anEnum {
    case .a:
      // Do something
    default:
      // Do something else.
    }
    
    // RIGHT
    switch anEnum {
    case .a:
      // Do something
    case .b, .c:
      // Do something else.
    }
  • (link) Check for nil rather than using optional binding if you don't need to use the value. SwiftLint: unused_optional_binding

    Why?

    Checking for nil makes it immediately clear what the intent of the statement is. Optional binding is less explicit.

    var thing: Thing?
    
    // WRONG
    if let _ = thing {
      doThing()
    }
    
    // RIGHT
    if thing != nil {
      doThing()
    }

⬆ back to top

File Organization

  • (link) Alphabetize module imports at the top of the file a single line below the last line of the header comments. Do not add additional line breaks between import statements. SwiftFormat: sortedImports

    Why?

    A standard organization method helps engineers more quickly determine which modules a file depends on.

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

    Exception: @testable import should be grouped after the regular import and separated by an empty line.

    // 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
  • (link) Limit empty vertical whitespace to one line. Favor the following formatting guidelines over whitespace of varying heights to divide files into logical groupings. SwiftLint: vertical_whitespace

  • (link) Files should end in a newline. SwiftLint: trailing_newline

⬆ back to top

Objective-C Interoperability

  • (link) Prefer pure Swift classes over subclasses of NSObject. If your code needs to be used by some Objective-C code, wrap it to expose the desired functionality. Use @objc on individual methods and variables as necessary rather than exposing all API on a class to Objective-C via @objcMembers.

    class PriceBreakdownViewController {
    
      private let acceptButton = UIButton()
    
      private func setUpAcceptButton() {
        acceptButton.addTarget(
          self,
          action: #selector(didTapAcceptButton),
          forControlEvents: .TouchUpInside)
      }
    
      @objc
      private func didTapAcceptButton() {
        // ...
      }
    }

⬆ back to top

Contributors

⬆ back to top

Amendments

We encourage you to fork this guide and change the rules to fit your team’s style guide. Below, you may list some amendments to the style guide. This allows you to periodically update your style guide without having to deal with merge conflicts.

⬆ back to top