Skip to content

Swift Code Convention

Joonyong Ji edited this page Dec 21, 2023 · 14 revisions

23.12.21.(목) 변경사항

  • MARK 주석 - (하이픈) 명시 내용 구체화
  • 파라미터 마지막에 클로저가 등장할 시, 후행 클로저 사용에 관한 내용 추가
  • 클로저 사용 시, Shorthand Argument(단축인자. $0, $1, ...) 적극 활용 내용 추가
  • 네임 스페이스 카테고리 추가
  • 프로그래밍 권장사항에 addSubview(UIView), constraint 코드 스타일 관련 사항 추가

코드 레이아웃

들여쓰기 및 띄어쓰기

  • 들여쓰기는 4개의 space를 사용한다.
  • 콜론(:)을 사용할 때는, 콜론의 오른쪽에만 공백을 둔다.

줄바꿈

  • 코드 한 줄의 최대 길이를 99자로 제한하고, 최대 길이 초과 시 줄바꿈한다.

    • 단, 불가피한 경우 최대 길이 초과를 허용한다.
    • Xcode에서 Settings - Text Editing - DisplayPage guide at column 옵션을 체크하고 99자 설정.
  • 함수의 정의가 최대 길이를 초과할 시, 아래와 같이 줄바꿈한다.

    func collectionView(
      _ collectionView: UICollectionView,
      cellForItemAt indexPath: IndexPath
    ) -> UICollectionViewCell {
      // doSomething()
    }
    
    func animationController(
      forPresented presented: UIViewController,
      presenting: UIViewController,
      source: UIViewController
    ) -> UIViewControllerAnimatedTransitioning? {
      // doSomething()
    }
  • 함수를 호출하는 코드가 최대 길이를 초과할 시, 파라미터 이름을 기준으로 줄바꿈한다.

    let actionSheet = UIActionSheet(
      title: "정말 계정을 삭제하실 건가요?",
      delegate: self,
      cancelButtonTitle: "취소",
      destructiveButtonTitle: "삭제해주세요"
    )
    • 단, 파라미터에 클로저가 2개 이상 존재하는 경우, 무조건 내려쓰기를 한다.
      UIView.animate(
        withDuration: 0.25,
        animations: {
          // doSomething()
        },
        completion: { finished in
          // doSomething()
        }
      )
  • if let구문이 길다고 느껴질 경우, 줄바꿈하고 한 칸 들여쓴다.

    if let user = self.veryLongFunctionNameWhichReturnsOptionalUser(),
       let name = user.veryLongFunctionNameWhichReturnsOptionalName(),
      user.gender == .female {
      // ...
    }
  • guard let구문이 길다고 느껴질 경우, 줄바꿈하고 한 칸 들여쓴다.

    guard let user = self.veryLongFunctionNameWhichReturnsOptionalUser(),
          let name = user.veryLongFunctionNameWhichReturnsOptionalName(),
          user.gender == .female
    else {
      return
    }

빈 줄

  • 모든 파일은 빈 줄로 끝나도록 한다.
  • MARK 구문 위와 아래에는 공백을 삽입한다.
    // MARK: - Layout
    
    override func layoutSubviews() {
      // doSomething()
    }
    
    // MARK: - Methods
    
    override func menuButtonDidTap() {
      // doSomething()
    }

임포트(Import)

  • 모듈 임포트는 내장 프레임워크를 먼저 임포트하고, 빈 줄로 구분하여 서드파티 프레임워크를 임포트한다.
  • 모듈 임포트는 알파벳 순으로 정렬한다.
    import UIKit
    
    import SwiftyColor
    import SwiftyImage
    import Then
    import URLNavigator

네임 스페이스(Name Space)

  • 네임 스페이스는 Font, Color, Text 등으로 구분되며, 각각의 파일로 관리한다.
      enum TextLiteral {
        static let nameLabelText = "홍길동"
        static let bioLabelText = "남성"
      }
      enum FontLiteral {
        static let nameLabelFont = UIFont.boldSystemFont(ofSize: 14)
        static let bioLabelFont = UIFont.boldSystemFont(ofSize: 12)
      }
      enum ColorLiteral {
        static let nameLabelText = 0x000000.color
        static let bioLabelText = 0x333333.color ~ 70%
      }

네이밍(Naming)

클래스와 구조체(Class & Structure)

  • 클래스와 구조체의 이름에는 UpperCamelCase를 사용한다.

열거형(Enum)

  • enum의 이름에는 UpperCamelCase를 사용한다.
  • enum의 각 case에는 lowerCamelCase를 사용한다.

프로토콜(Protocol)

  • 프로토콜의 이름에는 UpperCamelCase를 사용합니다.

  • ~을 할수 있음을 설명하는 프로토콜은 형용사로 작성해야 한다.

    protocol Car {
      var speed: Int { get set }
      var name: String { get }
    
      func speedUp(speed: Int) -> Bool
    }
    protocol Drivable {
      func accelerate(speed: Int) -> ()
      func slowDown(speed: Int) -> ()
    }
  • 구조체나 클래스에서 프로토콜을 채택할 때는 콜론과 빈칸을 넣어 구분하여 명시합니다.

  • extension을 통해 채택할 때도 동일하게 적용됩니다.

함수

  • 함수 이름에는 lowerCamelCase를 사용한다.

  • 함수 이름에는 되도록 getset을 붙이지 않는다.

    좋은 예:

    func name(for user: User) -> String?

    나쁜 예:

    func getName(for user: User) -> String?
  • Action 함수의 네이밍은 주어 + 동사 + 목적어의 형태를 사용하며, 주어는 유추 가능하다면 생략할 수 있다.

    • Tap(눌렀다 뗌)UIControlEvents.touchUpInside에 대응하고, Press(누름).touchDown에 대응한다.
    • will은 특정 행위가 일어나기 직전이고, did는 특정 행위가 일어난 직후이다.
    • should는 일반적으로 Bool을 반환하는 함수에 사용한다.
    • request는 에러가 발생허가너, 실패할 수 있는 비동기 작업에 사용한다
    • fetch는 요청이 실패하지 않고, 결과를 바로 반환할 때 사용한다.

    좋은 예:

    func backButtonDidTap() {
      // ...
    }

    나쁜 예:

    func back() {
      // ...
    }
    
    func pressBack() {
      // ...
    }

변수

  • 변수 이름에는 lowerCamelCase를 사용한다.

상수

  • 상수 이름에는 lowerCamelCase를 사용한다.

약어

  • 약어로 시작하는 경우 소문자로 표기하고, 그 외의 경우에는 항상 대문자로 표기한다.
  • 잘 알려지지 않은 약어 사용은 지양하되, 필요 시 협의 후 사용한다.
    let userID: Int? // ~ID
    let html: String? // html
    let websiteURL: URL? // ~URL
    let urlString: String? // url~

Delegate

  • Delegate메서드는 프로토콜명으로 네임스페이스를 구분한다.

    좋은 예

    protocol UserCellDelegate {
      func userCellDidSetProfileImage(_ cell: UserCell)
      func userCell(_ cell: UserCell, didTapFollowButtonWith user: User)
    }

    나쁜 예

    protocol UserCellDelegate {
      func didSetProfileImage()
      func followPressed(user: User)
    
      // `UserCell`이라는 클래스가 존재할 경우 컴파일 에러 발생
      func UserCell(_ cell: UserCell, didTapFollowButtonWith user: User)
    }

클로저(Closure)

  • 파라미터와 리턴 타입이 없는 Closure 정의 시, () -> Void형식을 사용한다. 좋은 예

    let completionBlock: (() -> Void)?

    나쁜 예

    let completionBlock: (() -> ())?
    let completionBlock: (Void -> Void)?
  • 파라미터 마지막에 클로저가 등장할 시, Trailing Closure(후행 클로저)로 작성한다.

    func complete(number: Int, completion: @escaping () -> Void) { 
      //... 
    }
    
    complete(1) { print("Hello") }
  • Closure 정의 시, 가능한 경우 타입 정의를 생략하고, Shorthand Argument(= 단축인자. $0, $1, ...)를 적극 활용한다.

    좋은 예

    ...,
    completion: { 
      print($0) // 단축인자 활용 권장
    }
    ...,
    completion: { finished in // Argument를 명시하는 것이 코드 가독성에 유리할 때는 Argument 사용
      doSomething()
    }

    나쁜 예

    ...,
    completion: { (finished: Bool) -> Void in
      // doSomething()
    }

타입(Type)

타입 추론

  • 컴팩트한 코드를 지향하고, 컴파일러가 단일 인스턴스의 상수나 변수의 타입을 추론하도록 한다.

  • CGFloat나 Int64와 같은 노멀 타입이 아닌 경우, 특정 타입을 지정한다. 좋은 예

    let apple = "Developer"
    let book1 = Book()
    let age = 25
    let frameWidth: CGFloat = 120

    나쁜 예

    let apple: String = "Developer"
    let book1: Book = Book()
    let age: Int = 25

타입 어노테이션

  • Array<T>Dictionary<T: U>보다는 [T], [T: U]를 사용한다.

    좋은 예

    var messages: [String]?
    var names: [Int: String]?

    나쁜 예

    var messages: Array<String>?
    var names: Dictionary<Int, String>?
  • 빈 배열과 딕셔너리 선언 시, 타입 명시를 지향한다. 좋은 예

    var student: [String: String] = [:]
    var students: [String] = []

    나쁜 예

    var student = [String: String]()
    var students = [String]()

주석(Comment)

  • 주석 사용을 최소화한다.
  • 설명이 필요한 코드일 시, ///를 사용해 문서화에 사용되는 주석을 남긴다.
    /// 사용자 프로필을 그려주는 뷰
    class ProfileView: UIView {
    
      /// 사용자 닉네임을 그려주는 라벨
      var nameLabel: UILabel!
    }
  • MARK주석 사용 시, -(하이픈)을 사용해 연관된 코드를 구분짓는다.
    // MARK: - Init
    
    override init(frame: CGRect) {
      // doSomething()
    }
    
    deinit {
      // doSomething()
    }
    
    // MARK: Layout
    
    override func layoutSubviews() {
      // doSomething()
    }

프로그래밍 권장사항

초기화

  • 가능하다면 변수를 정의할 때, 함께 초기화한다.

    좋은 예

    var number = 0

    나쁜 예

      var number: Int
      number = 0

Enum

  • 상수를 정의할 때는 enum을 만들어 비슷한 상수끼리 모아둔다.
    • 재사용성 및 유지보수성 향상
    • struct를 사용하지 않아 생성자가 제공되지 않는 자료형을 사용할 수 있다.
      final class ProfileViewController: UIViewController {
      
        private enum Metric {
          static let profileImageViewLeft = 10.f
          static let profileImageViewRight = 10.f
          static let nameLabelTopBottom = 8.f
          static let bioLabelTop = 6.f
        }
      }

Protocol

  • 프로토콜(Protocol)을 채택할 때에는 extension을 만들어 해당 프로토콜과 관련된 메서드를 모아둔다.

    좋은 예

    final class MyViewController: UIViewController {
      // ...
    }
    
    // MARK: - UITableViewDataSource
    
    extension MyViewController: UITableViewDataSource {
      // ...
    }
    
    // MARK: - UITableViewDelegate
    
    extension MyViewController: UITableViewDelegate {
      // ...
    }

    나쁜 예

    final class MyViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
      // ...
    }
  • 더 이상 상속이 발생하지 않는 클래스는 항상 final키워드로 선언한다.

  • 변수 및 메서드에 접근제어자(open, public, internal, filePrivate, private)를 사용해 불필요한 접근을 최소화한다.

UIKit

  • Properties, Views, LifeCycle, Methods 순으로 구분하는 것을 원칙으로 한다.
    • 필요 시, 세분화하여 구분할 수 있다.
  • LifeCycle 내부에는 직접적인 코드 작성을 지양한다.
      override func viewDidLoad() {
          super.viewDidLoad()
    
          attribute()
          layout()
      }
  • UIView 등의 view의 속성을 클로저로 초기화할 때, 리턴 값은 Shorthand Argument($0, $1, ...)를 적극 활용한다.
  • ViewController에 view의 레이아웃을 배치하는 것을 layout(), view의 속성을 다루는 것은 attribute(), 그 외 설정에 대한 것은 configure~() 또는 setup~() 메서드에 작성한다.
  • layout()메서드는 addSubview(UIView)를 먼저 작성해 모든 view를 메모리에 올려둔 상태에서 각 view에 대한 Constraint를 설정한다.

예시

import UIKit

final class ViewController: UIViewController {

  // MARK: - Properties

  private var datum = [String]()

  // MARK: - Enum
    
  private enum Metric {
    case ~
    case ~
  }

  // MARK: - Views

  private let label: UILabel = {
    $0.text = ""
    return $0
  }(UILabel())

  // MARK: - LifeCycle

  override func viewDidLoad() {
    super.viewDidLoad()

    layout()
    attribute()
  }

  // MARK: - Methods
  
  private func layout() {
    view.addSubview(label)
    label.snp.makeConstraints {
      $0.edges.equalToSuperview()
    }
  }

  private func attribute() {
    view.background = .white
  }
}