Skip to content

Latest commit

 

History

History
1008 lines (699 loc) · 24.1 KB

code_style_guideline.md

File metadata and controls

1008 lines (699 loc) · 24.1 KB

Trendyol Swift Style Guide (work-in-progress 🔧️⛏)

Goals

Our overarching goals are clarity, consistency, readability, simplicity, and brevity, in that order.

Following this style guide should:

  • Improves the readability.
  • Make code easier to maintain.

Clarity is more important than brevity. Although Swift code can be compact, it is a non-goal to enable the smallest possible code with the fewest characters. Brevity in Swift code, where it occurs, is a side-effect of the strong type system and features that naturally reduce boilerplate. 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.

If you are having trouble describing your API’s functionality in simple terms, you may have designed the wrong API. -Apple

  1. Naming
  2. Style
    1. Functions
    2. Closures
    3. Operators
  3. Patterns
  4. File Organization
  5. Documentation Comments
  6. References
  • Static and Class Properties

    Static and class properties that return instances of the declaring type are not suffixed with the name of the type.

    // RIGHT
    public class UIColor {
      public class var red: UIColor {               
        // ...
      }
    }
    
    // WRONG
    public class UIColor {
      public class var redColor: UIColor {               
        // ...
      }
    }
  • Use PascalCase for type and protocol names, and lowerCamelCase for everything else. SwiftLint: type_name

    This one not using on our project right now. But we should. There is a lot of struct and enum which are starts lowerCamelCase. If we add this thing, it will generate 150 error. FYI, folks.

    protocol SpaceThing {
      // ...
    }
    
    class SpaceFleet: SpaceThing {
    
      enum Formation {
        // ...
      }
    
      class Spaceship {
        // ...
      }
    
      var ships: [Spaceship] = []
      static let worldName: String = "Earth"
    
      func addShip(_ ship: Spaceship) {
        // ...
      }
    }
    
    let myFleet = SpaceFleet()
  • Name booleans like isAddAddressVisible, hasChangePermission, etc. This makes it clear that they are booleans and not other types.

  • 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 radiusBorder: CGFloat
    let widthBorder: CGFloat
    let bodyRightMargin: CGFloat
    let bodyLeftMargin: CGFloat
    
    // RIGHT
    let borderRadius: CGFloat
    let borderRight: CGFloat
    let bodyMarginRight: CGFloat
    let bodyMarginLeft: CGFloat
  • Use enums for organizing constants into namespaces.. Feel free to nest namespaces where it adds clarity.

    Why Enums?

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

    // WRONG
    @IBOutlet private weak var icon: UIImageView!
    @IBOutlet private weak var rightArrow: UIImageView!
    @IBOutlet private weak var bottomLine: UIView!
    
    let cancel: UIButton
    
    // RIGHT
    @IBOutlet private weak var iconImageView: UIImageView!
    @IBOutlet private weak var rightArrowImageView: UIImageView!
    @IBOutlet private weak var bottomLineView: UIView!
  • 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 tappedCampaign() {
        // ...
      }
    
      private func modelChanged() {
        // ...
      }
    }
    
    // RIGHT
    class ExperiencesViewController {
      private func campaignViewTapped() {
        // ...
      }
    }
  • Avoid Objective-C-style acronym prefixes. This is no longer needed to avoid naming conflicts in Swift.

    // WRONG
    class AIRAccount {
      // ...
    }
    
    // RIGHT
    class Account {
      // ...
    }
  • Add localization parameters to domain based. When adding string key use below logic. And fileName must be end of the extension.

    <ScreenName>.<ScreenComponent>.<ComponentPart>.<Type>
    
  • Add accessibilityIdentifiers as Localizable.

  • Delete all TODO comments.

  • Use fetch prefix instead of get for interactor functions.

  • Don't use get prefix on function or variable.

  • Remove line spaces for class's first and last lines.

  • Convert cells to viper module. at least move logic into presenter (Who touches it changes it)

  • On UnitTest every it method should restore changed data if test case makes changes on any data.

  • Mark each extension. Also use MARK: - over MARK: it will add a seperator on each extension.

    // MARK: - UrlOpenable
    public extension UrlOpenable {
      func canOpenUrl(_ url: URL) -> Bool { ... }
      func openUrl(_ url: URL) { ... }
    }
  • Line length rule. Some view or requests are too long. We can move some parameters to new line.

    line_length:
      warning: 140
      error: 160
    
  • 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
    }
  • 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()
      }
    }
  • 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
  • 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]()
  • 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) {
      // ...
    }
  • 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 }
  • 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:
      ...
    }
  • Use constructors instead of Make() functions for NSRange and others. SwiftLint: legacy_constructor

    // WRONG
    let range = NSMakeRange(10, 5)
    
    // RIGHT
    let range = NSRange(location: 10, length: 5)
  • Omit unnecessary parameters.

    // WRONG
    @IBAction func deleteAddressClicked(_ sender: UIButton) {
        presenter?.notifyDeleteAddressClicked()
    }
    
    // RIGHT
    @IBAction func deleteAddressClicked() {
      presenter?.notifyDeleteAddressClicked()
    }
  • Omit Void return types from function definitions. SwiftLint: redundant_void_return

    // WRONG
    func doSomething() -> Void {
      ...
    }
    
    // RIGHT
    func doSomething() {
      ...
    }
  • Mostly don't use ommited parameters on function. If parameter name contains on method name at it is clear to recognize. You can use.

    // WRONG
    func addImage(_ imageUrl: String) {
      ...
    }
    
    func addImage(image: UIImage) {
      ...
    }
    
    // RIGHT
    func addImage(_ image: UIImage) {
      ...
    }
    
    // RIGHT
    func addImage(with image: UIImage) {
      ...
    }
  • Long function invocations should also break on each argument. Put the closing parenthesis on the last parameter of the invocation..

    // WRONG
    universe.generateStars(at: location, count: 5, color: starColor, withAverageDistance: 4)
    
    // WRONG
    universe.generateStars(
      at: location,
      count: 5,
      color: starColor,
      withAverageDistance: 4
    )
    
    // WRONG
    universe.generateStars(
      at: location,
      count: 5,
      color: starColor,
      withAverageDistance: 4)
    
    // WRONG
    universe.generate(
      5,
      .stars,
      at: location)
    
    // RIGHT
    universe.generateStars(at: location,
                           count: 5,
                           color: starColor,
                           withAverageDistance: 4)
    
    // RIGHT
    universe.generate(5,
                      .stars,
                      at: location)
  • 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) {
      ...
    }
  • 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)
    }
  • Single-line closures should have a space inside each brace. Also if you can do it on one line, do it. When you doing that remember readability is our most important thing. 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 }
  • 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)
  • Don't use chain delegate if you are not make any changes on every scope.

    extension ReviewCell: ReviewContainerViewDelegate {
      func photosTapped(imageUrls: [String], currentImageIndex: Int) {
          delegate?.photosTapped(imageUrls: imageUrls, currentImageIndex:   currentImageIndex)
      }
    }
    
    extension ReviewContainerViewPresenter: ReviewContainerViewPresenterInterface {
      func reviewPhotoTapped(imageUrls: [String], currentImageIndex: Int) {
          delegate?.photosTapped(imageUrls: imageUrls, currentImageIndex: currentImageIndex)
      }
    }
  • Don't use same delegate name when you use chain delegate. A unique method name helps engineers more quickly determine which modules depends on.

  • Prefer initializing properties at init time whenever possible, rather than using implicitly unwrapped optionals. 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
    }
  • 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.

  • Access control should be at the strictest level possible.

  • 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) }
  • 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 {
      // ...
    }
  • Dont 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.
    }
    

    Also if you can use ternary if use it on here.

    let value = anEnum == .a ? XXX : xxx
  • 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()
    }
  • Seperation of Concerns. Do not add everything to BaseProtocolInterface.

    Why?

    Add protocols with specific needs. With that, we can easily identify that interface what it can do? On Trendyol we are adding all common functions to Base...Interface. For example on our project most of ViewInterface can can openUrl but not all of them needs that ability.

    public protocol UrlOpenable {
      func canOpenUrl(_ url: URL) -> Bool
      func openUrl(_ url: URL)
    }
    
    public extension UrlOpenable {
      func canOpenUrl(_ url: URL) -> Bool { ... }
      func openUrl(_ url: URL) { ... }
    }
    public protocol NavigationBarConfigurable {
      func prepareBackButton(tintColor: Color)
      func prepareCrossButton(tintColor: Color, target: Any?, selector: Selector)
      func setNavigationBarHidden(_ shouldHide: Bool)
    }
    
    public extension NavigationBarConfigurable {
      func prepareBackButton(tintColor: Color) { ... }
      func prepareCrossButton(tintColor: Color, target: Any?, selector: Selector) { ... }
      func setNavigationBarHidden(_ shouldHide: Bool) { ... }
    }
    protocol ProductDetailViewInterface: ViewInterface, UrlOpenable, NavigationBarConfigurable { .... }
  • Use ternary if only setting value or calling function.

    Why?

    Swift build times are slow mostly because of expensive type checking. By default Xcode doesn't show code that's slow to compile. You can instruct it to show slowly compiling functions and expressions, though by adding:

    -Xfrontend -warn-long-function-bodies=100 (100 means 100ms here, you should experiment with this value depending on your computer speed and project) -Xfrontend -warn-long-expression-type-checking=100

    // Build time: 239.0ms
    let labelNames = type == 0 ? (1...5).map{type0ToString($0)} : (0...2).map{type1ToString($0)}
    
    // Build time: 16.9ms
    var labelNames: [String]
    if type == 0 {
        labelNames = (1...5).map{type0ToString($0)}
    } else {
        labelNames = (0...2).map{type1ToString($0)}
    }
    // Build time: 1034.0ms
    let moveValue = willShowKeyboard ? -(endKeyboardFrame.height/Constant.dividerNumberForKeyboardHeight + (view?.doneButtonHeight ?? 0)) : 0
    
    // Build time: 103.0ms
    var moveValue: CGFloat = 0.0
      if willShowKeyboard {
          moveValue = -(endKeyboardFrame.height/Constant.dividerNumberForKeyboardHeight)
          if let doneButtonHeight = view?.doneButtonHeight {
              moveValue += doneButtonHeight
          }
      }
  • 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. Also if some module has another module do not import it. Ex: BottomPopup has a UIKit

      // WRONG
      import SDWebImage
      import UIKit
      import BottomPopup
    
      //RIGHT
    
      import BottomPopup
      import SDWebImage
  • On group name don't add full name to subgroups. Don't use Module suffix if it already inside on Module.

    Why?

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

    // WRONG
    Modules > Account > BrandFollowingModule > Views > BrandFollowingTableViewHeader
    Modules > Account > AccountModule > AccountListFooterBanner
    Modules > Account > AccountSettingsModule
    
    //RIGHT
    Modules > Account > BrandFollowing > Views > TableViewHeader
    Modules > Account > Listing > FooterBanner
    Modules > Account > Settings
  • *Documentation comments are written using the format where each line is preceded by a triple slash (///). Beacuse if you use that it will see on autocomplete dropdown view. Javadoc-style block comments (/ * ... /) are not permitted.