Swift 🏎

Hero image for Swift 🏎

When it comes to Objective-C, most conventions are shared with Swift. But when there are differences, specific examples for Objective-C are added. For more generic conventions, the team follow Apple’s Guidelines and Ray Wenderlich’s Guidelines

Code formatting

General

  • On average, row width should not exceed 120 characters. However, the main criterion is readability.
  • Do not align code as ASCII-art.
  • When using Objective-C, class names must be unique across an entire application. The convention is to use prefixes on all classes. For Swift, class prefixes are optional. It will depend on the project’s type. If we are working with an Objective-C codebase, we will use prefixes. Otherwise, we will not use prefixes classes except for Error classes.

    // Good for Objective-C
    RPViewController, RPError
    
    // Good for Swift
    ViewController, Manager
    
  • Use proper data types. For non-integer types, use decimal point explicitly.

    let width = 320
    
    let width: CGFloat = 320.0
    
  • Do not keep outdated code in comments as it could confuse other team members and could cause less productivity. Source control is a way to go in this case 😌.

Braces and Indents

  • Curly braces open on the same line with the method signature and close on a new line.

    func viewDidLoad()
    {
        super.viewDidLoad()
    }
    
    func viewDidLoad() {
        super.viewDidLoad()
    }
    
  • Separate logical code blocks with 1 line wrap. A random number of spaces and line breaks are not allowed.
  • Add a blank line after the opening brace ({), within class, protocol, struct, extension or enum.

    class User() {
        let name: String
        let age: Int
    }
    
    class User() {
    
        let name: String
        let age: Int
    }
    
    enum Direction {
    
        case north
        case south
    }
    
  • Do not add a blank line after the opening brace, within func.

    func doSomething() {
    
        print("done")
    }
    
    func doSomething() {
        print("done")
    }
    
  • Use the same indent for switch and case sentence, within switch.

    switch value {
      case .caseA:
          print("a")
          print("aa")
      case .caseB:
          print("b")
          print("bb")
    }
    
    switch value {
    case .caseA:
        print("a")
        print("aa")
    case .caseB:
        print("b")
        print("bb")
    }
    

    Tip: Prevent Xcode add extra indents when formatting by unchecking Indent switch/case labels in for Swift in Settings > Text Editing.

  • Do not add a blank line after each case, within switch.

    switch value {
    case .caseA:
        print("A")
        doA()
    
    case .caseB:
        print("B")
        doB()
    
    case .caseC:
        print("C")
        doC()
    
    }
    
    switch value {
    case .caseA:
        print("A")
        doA()
    case .caseB:
        print("B")
        doB()
    case .caseC:
        print("C")
        doC()
    }
    
  • Do not add a line break after each single line case, within switch.

    switch value {
    case .caseA:
        print("a")
    case .caseB:
        print("b")
    }
    
    switch value {
    case .caseA: print("a")
    case .caseB: print("b")
    }
    
  • Use same line syntax for a short computed property.

    var a: Bool {
    
        true
    }
    // or
    var a: Bool {
        true
    }
    
    var a: Bool { true }
    

Declaration

Methods

  • Keep method signatures short and clear.
  • Do not keep empty methods, autogenerated methods, methods that only call super implementation.
  • Method must do exactly what is declared in the signature, to avoid implicit side effects.
  • Mark methods that perform heavy operations with special words: load, fetch, create.
  • On average, a method’s body should not exceed the limit of 80 lines of code.

Properties

  • Prefer outlets as weak, but be aware of a case where strong reference is required. Suppose we have an outlet of NSLayoutConstraint in UITableView where we need to activate and deactivate it. It’s possible that, once deactivated, the outlet will be released from memory since there’s no strong reference pointing to it.
  • Variables of Boolean type should have is, has prefixes.

Closures

  • Do not use the words callback, block, and closure as parts of the names. Instead, provide names that describe the task the closure performs or specify the moment it is called.

    callback, comparingBlock, sortingClosure, selectionAction, beginBlock
    
    completion, comparator, sortCriteria, didSelectItem, willBeginParsing
    
  • Use typealias for reused closure types.
  • For better readability, use Void instead of empty braces.

    var action: () -> ()
    
    var action: () -> Void
    

Lightweight Generics

  • Objective-C type declarations using lightweight generic parameterization are imported by Swift with information about the type of their contents preserved.
  • Do not have space between TYPE and GENERIC.

    NSArray <NSString *> *a
    
    NSArray<NSString *> *a
    

Reused resources

Formatter

  • Keep formatters in a separate file.
  • Do not create formatters in a cycle, they are heavy objects.
  • Use namespaces to group formatters.

Constants

  • Do not keep constants in a separate file.
  • Keep as few constants as possible.
  • Sets of settings keep in .plist files.

Colors

  • Give colors reasonable names.
  • Do not use the word color in color names.
  • Try to keep the color palette as small as possible. A large set of used colors is usually caused by inconsistency in design specifications.
  • Expressive names help to manage and remember reused colors.
  • Avoid giving too specific names, make names general.

    var textFieldBorder: UIColor
    var veryDarkGrey: UIColor
    
    var ashGrey: UIColor
    var mandyRed: UIColor
    
  • Do not use #colorLiteral. Sometimes, the color is not shown properly on Xcode.

    let dingleyGreen = #colorLiteral(red: 0.398, green: 0.523, blue: 0.285, alpha: 1.0)
    
    let dingleyGreen = UIColor(red: 0.398, green: 0.523, blue: 0.285, alpha: 1.0)
    

Images

  • Do not use #imageLiteral. Sometimes, the image is not shown properly on Xcode.

    let image = #imageLiteral(resourceName: "image-name")
    
    let image = UIImage(named: "image-name")
    

Localization

  • Localization is required regardless of the number of supported languages. Therefore adding an extra language won’t cause any difficulty in the future.
  • Keep all strings in Localizable.strings file. Do not use storyboard localization
  • For localizable string keys, use 3-level domain naming scheme with camel case: feature.useCase.description. Do not use snake case or any other cases to avoid ambiguity.

    "Expiry date" = "Expiry date";
    "OK" = "OK";
    
    "booking.confirmation.expiry_date" = "Expiry date";
    "general.button.ok" = "OK";
    

Project Structure

  • Organize .xcodeproj: Folder structure must reflect the logical structure of the project. Do not put all view controllers into a separate folder and all models into another one. Keep close related objects together.
  • Keep file structure in sync with .xcodeproj.
  • One .m / .swift == One class. No extra classes allowed.

Check the Project Structure.

Classes and Structures

  • Prefer Swift native types over NSObject.
  • Prefer value types to reference types for lightweight data types or until you explicitly need reference semantics.

    final class Coordinate {
    
        var x: CGFloat
        var y: CGFloat
    }
    
    struct Coordinate {
    
        var x: CGFloat
        var y: CGFloat
    }
    
  • Prefer let over var as it indicates directly that the property cannot be changed, and that makes it easier for other team members to understand the intention. Always use var only when it’s required to be changed. Otherwise, go for let.
  • Mark classes as final by default unless you’ve explicitly designed them to be inheritable.
  • Expose only methods and variables that are meant to be exposed. Hide internal implementation by using the private access specifier.
  • Keep life cycle methods such as deinit, viewDidLoad, viewWillAppear, etc., at the top of the class body.
  • Prefer declaring init with deinit right below it if required.
  • Avoid using self. when you don’t need it since Swift does not require it to access an object’s properties or invoke its methods. In closure expressions, the presence of self. is a clear signal that self is being captured by the closure.

Design Patterns

Many design patterns have distinctive declaration styles. Being consistent in method naming helps other developers to understand your intentions, relations between objects even without reading the code itself.

Delegate, Data Source

  • Always add the sender to method signatures:

    func parserDidEndParsingNode(_ node: Node)
    
    func parser(_ parser: Parser, didEndParsingNode node: Node)
    
  • Do not use verbs in past form. Use did + Verb in present form. The purpose is to avoid exotic forms of irregular verbs.

Target-Action

  • Always add the sender to the method signatures.
  • Use did as a prefix to describe how the action is triggered.

    @objc func showCart(_ sender: Any?)
    
    @objc func didSelectCart(_ sender: UIButton)
    

Listener

  • For methods which are called by NotificationCenter, always add the notification parameter to the signature.

    func applicationDidEnterForeground()
    
    func applicationDidEnterForeground(_ notification: Notification)
    

Code clarity & Best practices

Method and variable naming

Good code has a high signal/noise ratio. To declare methods and variables, use as little information as required to understand their responsibility.

A good rule of thumb is that, when declaring methods and variables that require a comment to explain what they are, it usually indicates that its name might not be clear enough. However, it could depend on the case and couldn’t apply to every case. Use it as an indicator. But, in the end, it leaves to the developer’s judgment upon the case.

Avoid large initialization

Initialize objects only with required parameters. Avoid large init methods. Do not init objects with delegate.

Avoid creating large protocols

Avoid creating large protocols. Each protocol must describe a single responsibility. Many small protocols are better than one large.

Naming classes should be straightforward

Avoid words with unclear meaning, such as Common, Utils, Helper, Custom. The name of a class must leave no questions about its task.

DataManager, StringUtils, PriceHelper, CustomStepper
DataStorage, Formatter, PriceCalculator, StepperWithCount

Be wary of creating large structs

Generally, value types are better than reference types in terms of performance as they get allocated in the stack. So there’s no need for the reference counting overhead.

Keep in mind that, once struct contains at least one reference type, its advantage over using class will be nullified, or might even get worse. Since struct is a pass-by-value, so its properties will need to be copied over and over when passing. This results in reference counting doing the job more than just using class.

But, the trade-off of value semantic could be worth enough to ignore the performance gain sometimes depending on how big the struct is. In that case, COW (Copy-on-write) should be implemented.

Guards for early exits

Use guard to exit functions early.

func doSomethingWithNumbers(number1: Int?, number2: Int?) {
  if let number1 = number1 {
    if let number2 = number2 {
      // do something with numbers
    } else {
      print("Impossible")
    }
  } else {
    print("Impossible")
  }
}
func doSomethingWithNumbers(number1: Int?, number2: Int?) {
  guard let number1 = number1, let number2 = number2 else {
    return print("Impossible")
  }
  // do something with numbers
}

Avoid force unwrapping, force casts, and implicitly unwrapped optional

Force unwrapping, force casting, and implicitly unwrapped optional are inherently unsafe and should be avoided.

Force unwraps are allowed in unit tests and test-only code without additional documentation.

Prefer higher-order functions

Favor higher-order functions (e.g. map, filter, flatMap, compact, etc.) but don’t force it. Developers can still choose to use a for loop when it makes sense.

Align multiline arguments and collection

  • Break function with more than three (3) arguments into multiple lines to keep the length minimum.
  • Line break should use the minimum indent and align with other function arguments at the same level.

The rule of thumb is to add a line break before the first item and after each item.

aLongFunction(number1: Int, number2: Int,
  text1: String, number3: Int
)

func anotherLongFunction(
  number1: Int, number2: Int,
  text1: String, number3: Int
) {}
aLongFunction(
  number1: Int,
  number2: Int,
  text1: String,
  number3: Int
)

func anotherLongFunction(
  number1: Int,
  number2: Int,
  text1: String,
  number3: Int
) {}
  • Break collections with more than three (3) items into multiple lines to keep the length minimum.
  • Line break should use the minimum indent and align with other collections at the same level.
var names: [String] = [
  "Alice",
      "Bob",
  "Cait", "David"
]

names.append(
  contentsOf: [ "Edward",
    "Fiona", "Greg"
  ]
)
var names: [String] = [
  "Alice",
  "Bob",
  "Cait",
  "David"
]

names.append(
  contentsOf: [
    "Edward",
    "Fiona",
    "Greg"
  ]
)

Naming Cells for UICollectionView and UITableView

We can omit CollectionView and TableView in the name of the cell.

// UICollectionViewCell
final class SomeCollectionViewCell: UICollectionViewCell {}
final class SomeCollectionCell: UICollectionViewCell {}

// UITableViewCell
final class SomeTableViewCell: UITableViewCell {}
final class SomeTableCell: UITableViewCell {}
// UICollectionViewCell
final class SomeCell: UICollectionViewCell {}

// UITableViewCell
final class SomeCell: UITableViewCell {}

Reference Resources