Skip to content

APP-iOS3rd/EYE-Mate-swift-style-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 

Repository files navigation

EYE-Mate-swift-style-guide

EYE-Mate 구성원들이 Swift 코드를 이해하기 쉽고 명확하게 작성하기 위한 스타일 가이드입니다. 구성원들의 의사결정에 따라 수시로 변경될 수 있습니다.

본 문서에 나와있지 않은 규칙은 아래 문서를 따릅니다.

목차

코드 포매팅

1. Import 순서

  • 내장 프레임워크를 먼저 import하고 빈 줄로 구분해 third-party 프레임워크를 import합니다.

  • 모듈 import는 알파벳 순으로 정렬합니다.

✅ Preferred

import Foundation
import UIKit

import AdSupport
import AppTrackingTransparency
import RxCocoa
import RxSwift
import SwiftyJSON

⛔ Not Preferred

import UIKit
import AppTrackingTransparency
import Foundation
import RxSwift
import RxCocoa
import SwiftyJSON
import AdSupport
  • 사용하지 않는 모듈은 import 하지 않습니다.

✅ Preferred

import UIKit

var view: UIView

⛔ Not Preferred

import Foundation  // not required
import UIKit

var view: UIView

2. 들여쓰기

  • 들여쓰기는 Xcode에서 제공하는 control(^) + i 를 사용합니다.

✅ Preferred

var vaildProductCount: Int {
    allProducts
        .filter { $0.isVaild }
        .count
}

⛔ Not Preferred

var vaildProductCount: Int {
    allProducts
    .filter { $0.isVaild }
    .count
}

3. 띄어쓰기

  • 클래스 상속, 클래스 프로토콜 채택, 타입 선언, 호출 파라미터, 선언 파라미터, 제네릭 프로토콜 채택, 딕셔너리에서 콜론(:) 사용 시, 오른쪽에만 공백을 하나 둡니다.

✅ Preferred

class MyViewController: UIViewController { ... }
extension MyViewController: UITableViewDataSource { ... }
let headerDictionary: [String: String] = [
    "Content-Type": "application/json"
]
someFunction(name: someString)
func myFunction<T, U: SomeProtocol>(firstParam: T, secondParam: U) { ... }

⛔ Not Preferred

class MyViewController:UIViewController { ... }
extension MyViewController : UITableViewDataSource { ... }
let headerDictionary:[String : String] = [
    "Content-Type":"application/json"
]
someFunction(name : someString)
func myFunction<T, U:SomeProtocol>(firstParam : T, secondParam:U) { ... }
  • 삼항연산자에서 콜론(:) 사용 시, 양 옆에 공백을 하나 둡니다.

✅ Preferred

var dayString = day < 10 ? "0\\(day)" : "\\(day)"

⛔ Not Preferred

var dayString = day < 10 ? "0\\(day)": "\\(day)"
  • 배열, 함수, switch-case 등 모든 곳에서 쉼표(,) 사용 시, 오른쪽에만 공백을 하나 둡니다.

✅ Preferred

let array = ["A", "B", "C"]

func testFunc<T, U>(firstParam: T, secondParam: U) { ... } 

someFunction(name: "a", age: 15)

switch number {
case 1, 2, 3:
    // Do something
default:
    break
}

let dict: [String: Int] = [
    "a": 1,
    "b": 2
]

⛔ Not Preferred

let array = ["A","B" , "C"]

func testFunc<T,U>(firstParam: T , secondParam: U) { ... } 

someFunction(name: "a",age: 15)

switch number {
case 1 , 2,3:
    // Do something
default:
    break
}

let dict: [String: Int] = [
    "a": 1 ,
    "b": 2
]
  • 연산자는 양 옆에 공백을 하나 둡니다.

✅ Preferred

let answer = 1 + 2 / 3 % (4 * 5)

⛔ Not Preferred

let answer = 1+2/3%(4*5)
  • 함수와 클로저 리턴 타입에 사용되는 화살표는 양 옆에 공백을 하나 둡니다.

✅ Preferred

func someFunc() -> String { ... }
let completion: () -> Void

⛔ Not Preferred

func someFunc()->String { ... }
let completion: ()->Void
  • 일반적으로 함수 선언시, 함수명과 소괄호를 붙혀쓰고, 연산자 오버로딩시 공백을 하나 둡니다.

✅ Preferred

func testFunc() {}
func == (lhs: SomeClass, rhs: SomeClass) {}

⛔ Not Preferred

func testFunc () {}
func ==(lhs: SomeClass, rhs: SomeClass) {}
  • 한 줄 클로저 등, 중괄호 안쪽은 띄워쓰기를 하나 둡니다.

✅ Preferred

numArray
    .filter { $0 % 2 == 0 }
    .map { "\\($0)" }
    .joined(separator: ",")

guard let self = self else { return }

⛔ Not Preferred

numArray
    .filter {$0 % 2 == 0}
    .map {"\\($0)" }
    .joined(separator: ",")

guard let self = self else { return}

4. 줄 바꿈

  • 빈 줄에는 공백이 포함되지 않도록 하며 모든 파일은 빈 줄로 끝나도록 합니다.

  • Xcode 의 Page guide 기준, 120 charactor 을 넘으면 반드시 줄 바꿈 합니다.

Page guide 켜는 방법

  • guard let self = self else { return } 은 한 줄로 씁니다.

  • if let, guard let 구문이 긴 경우 줄 바꿈하고 한 칸 들여씁니다.

✅ Preferred

guard let self = self else { return }

⛔ Not Preferred

guard let self = self else { 
    return 
}
  • 여는 중괄호({) 를 새 줄에 배치하지 않습니다. 1TBS 스타일을 사용합니다.

✅ Preferred

func someMethod() {
    // Do something
}

⛔ Not Preferred

func someMethod() 
{
    // Do something
}

5. 소괄호

  • if, switch-case, 파라미터, 후행 클로저 등 불필요한 소괄호는 생략합니다.

✅ Preferred

if count > 0 { ... }
switch type { ... }
_ = arr.filter { num in num % 2 == 0 }

⛔ Not Preferred

if (count > 0) { ... }
switch (type) { ... }
_ = arr.filter() { (num) in num % 2 == 0 }

네이밍

0. 명확한 이름

  • 가능한 명확하고 모든 의미를 포함하는 이름을 사용합니다.

  • 이름이 길어져도 의미가 있는 이름이 타인의 코드를 볼 때 훨씬 빠르고 편합니다.

✅ Preferred

class RoundAnimationButton { ... }
func startAnimating() { ... }
let animationDuration: CGFloat
let personImageView: UIImageView
let titleLabel: UILabel

⛔ Not Preferred

class CustomButton { ... }
class RoundAniBtn { ... }
func srtAnimating() { ... }
let aniDur: CGFloat
let personImage: UIImageView  // image? imageView?
let title: UILabel            // String? label?

1. 클래스와 구조체

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

  • 클래스 이름에는 접두사를 붙이지 않습니다.

✅ Preferred

class SomeClass {
  // class definition goes here
}

struct SomeStructure {
  // structure definition goes here
}

⛔ Not Preferred

class someClass {
// class definition goes here
}

struct someStructure {
// structure definition goes here
}

2. 함수

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

  • 함수 이름 앞에는 되도록이면 get을 붙이지 않습니다.

✅ Preferred

func date(from string: String) -> Date?
func anchor(for node: SCNNode) -> ARAnchor?                          
func distance(from location: CLLocation) -> CLLocationDistance        
func track(withTrackID trackID: CMPersistentTrackID) -> AVAssetTrack? 

⛔ Not Preferred

func getdate(from string: String) -> Date?
func getAnchor(for node: SCNNode) -> ARAnchor?                          
func Getdistance(from location: CLLocation) -> CLLocationDistance        
func Track(withTrackID trackID: CMPersistentTrackID) -> AVAssetTrack? 
  • Action 함수(유저 인터렉션을 처음 처리하는 함수) 와 ReactorKit.Action의 네이밍은 '주어 + 동사 + 목적어' 형태를 사용합니다.

    • Tap(눌렀다 뗌- ) 은 UIControlEvents의 .touchUpInside에 대응하고, Press(누름).touchDown에 대응합니다.

    • will~은 특정 행위가 일어나기 직전이고, did~는 특정 행위가 일어난 직후입니다.

    • should~, is~, can~ 은 일반적으로 Bool을 반환하는 함수에 사용합니다.

✅ Preferred

func backButtonDidTap() {
  // ...
}

⛔ Not Preferred

func back() {
  // ...
}

func pressBack() {
  // ...
}
  • API 를 호출하는 함수는 앞에 HTTP 함수(eg. get, post, put, delete, patch 등)를 붙힙니다.

✅ Preferred

static func getAccessToken() -> Observable<Data> { ... }

static func patchShippingAddress() -> Observable<Data> { ... }

⛔ Not Preferred

static func requestAccessToken() -> Observable<Data> { ... }
static func takeAccessToken() -> Observable<Data> { ... }
static func fetchAccessToken() -> Observable<Data> { ... }

static func updateShippingAddress() -> Observable<Data> { ... }
static func modifyShippingAddress() -> Observable<Data> { ... }

3. 변수와 상수

  • 변수 이름과 상수 이름에는 lowerCamelCase를 사용합니다.

✅ Preferred

let maximumNumberOfLines = 3

⛔ Not Preferred

let MaximumNumberOfLines = 3
let MAX_LINES = 3

4. 열거형

  • enum의 이름에는 UpperCamelCase를 사용합니다.

  • enum의 각 case에는 lowerCamelCase를 사용합니다.

✅ Preferred

enum Result {
  case .success
  case .failure
}

⛔ Not Preferred

enum Result {
  case .Success
  case .Failure
}

enum result {
  case .Success
  case .Failure
}

5. 프로토콜

  • 프로토콜의 이름은 애플의 API 디자인 가이드라인을 참고합니다.

    • 무엇인가 설명하는 프로토콜은 명사로 읽어야 합니다. (e.g. Collection).

    • 기능을 설명하는 프로토콜은 able, ible, ing 접미사를 사용합니다.(e.g. Equatable, ProgressReporting)

    • 어떤 것도 적합하지 않은 경우, Protocol 을 접미사로 사용할 수 있습니다.

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

✅ Preferred

protocol ImagePresentable { ... }
protocol NotificationUIProtocol { ... }

⛔ Not Preferred

protocol ImagePresent { ... }
protocol NotificationUIType { ... }
  • 구조체나 클래스에서 프로토콜을 채택할 때는 콜론(:)과 공백을 넣어 구분하여 명시합니다.

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

✅ Preferred

protocol SomeProtocol {
  // protocol definition goes here
}

struct SomeStructure: SomeProtocol, AnotherProtocol {
  // structure definition goes here
}

class SomeClass: SomeSuperclass, SomeProtocol, AnotherProtocol {
    // class definition goes here
}

extension UIViewController: SomeProtocol, AnotherProtocol {
  // doSomething()
}

⛔ Not Preferred

protocol someProtocol {
  // protocol definition goes here
}

struct SomeStructure:SomeProtocol, AnotherProtocol {
  // structure definition goes here
}

class SomeClass:SomeSuperclass, someProtocol, AnotherProtocol {
    // class definition goes here
}

extension UIViewController:SomeProtocol, AnotherProtocol {
  // doSomething()
}

6. 약어

  • 약어는 모두 대문자로 표기하고, 약어로 시작하는 경우에만 소문자로 표기합니다.

✅ Preferred

  let userID: Int?
  let html: String?
  let websiteURL: URL?
  let urlString: String?

⛔ Not Preferred

  let userId: Int?
  let HTML: String?
  let websiteUrl: NSURL?
  let URLString: String?

7. Delegate

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

✅ Preferred

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

⛔ Not Preferred

protocol UserCellDelegate {
  func didSetProfileImage()
  func followPressed(user: User)

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

코드 스타일

1. General

  • 가능한 var보다 let 을 선호합니다.

  • 한 컬렉션에서 다른 컬렉션으로 변환할 때는 반복문보다 map, filter, reduce 등의 구성을 선호합니다. 이러한 방법을 사용할 때는 side effect가 있는 클로저를 사용하지 않도록 주의하세요.

✅ Preferred

let stringOfInts = [1, 2, 3].flatMap { String($0) }
// ["1", "2", "3"]

let evenNumbers = [4, 8, 15, 16, 23, 42].filter { $0 % 2 == 0 }
// [4, 8, 16, 42]

⛔ Not Preferred

var stringOfInts: [String] = []
for integer in [1, 2, 3] {
    stringOfInts.append(String(integer))
}
// ["1", "2", "3"]

var evenNumbers: [Int] = []
for integer in [4, 8, 15, 16, 23, 42] {
    if integer % 2 == 0 {
        evenNumbers.append(integer)
    }
}
// [4, 8, 16, 42]
  • 상수나 변수에서 타입을 추론할 수 있는 경우 타입을 선언하지 않는 것이 좋습니다.

  • 함수가 여러 값을 반환하는 경우, inout 인수를 사용하는 것보다 튜플을 반환하는 것이 좋습니다(반환하는 값이 명확하지 않은 경우 레이블이 지정된 튜플을 사용하는 것이 가장 좋습니다). 특정 튜플을 두 번 이상 사용하는 경우 typealias 사용을 고려하세요. 하나의 튜플에 3개 이상의 항목을 반환하는 경우에는 structclass를 사용하는 것이 좋습니다.

✅ Preferred

func pirateName() -> (firstName: String, lastName: String) {
    return ("Guybrush", "Threepwood")
}

let name = pirateName()
let firstName = name.firstName
let lastName = name.lastName

⛔ Not Preferred

func pirateName(firstName: inout String, lastName: inout String) {
    firstName = "Guybrush"
    lastName = "Threepwood"
}

var firstName = ""
var lastName = ""
pirateName(&firstName, &lastName)
  • 클래스에서 Delegate/Protocol을 생성할 때 순환 참조(Retain Cycle)에 주의하세요. 일반적으로 이러한 프로퍼티는 weak로 선언해야 합니다.

  • Escaping Closure에서 직접 self를 호출할 때는 순환 참조(Retain Cycle)가 발생할 수 있으므로 주의하세요. 이 경우, capture list를 사용하세요.

myFunctionWithEscapingClosure() { [weak self] (error) -> Void in
    // you can do this

    self?.doSomething()

    // or you can do this

    guard let strongSelf = self else {
        return
    }

    strongSelf.doSomething()
}
  • Labeled break를 사용하지 마세요.

⛔ Not Preferred

outerloop: for i in 1...3{
  innerloop: for j in 1...3 {
    if j == 3 {
      break outerloop
    }
      print("i = \(i), j = \(j)")
  }
}
  • 제어 흐름(Control Flow) 조건식에 괄호를 넣지 마세요.

✅ Preferred

if x == y {
    /* ... */
}

⛔ Not Preferred

if (x == y) {
    /* ... */
}
  • 가능하면 enum 타입을 작성하지 말고 단축형을 사용하세요.

✅ Preferred

imageView.setImageWithURL(url, type: .person)

⛔ Not Preferred

imageView.setImageWithURL(url, type: AsyncImageView.Type.person)
  • 일반적으로 enum과 달리 클래스 메서드에서는 컨텍스트를 추론하기가 더 어렵기 때문에 클래스 메서드에는 단축형을 사용하지 마세요.

✅ Preferred

imageView.backgroundColor = UIColor.white

⛔ Not Preferred

imageView.backgroundColor = .white
  • 꼭 필요한 경우가 아니라면 self.를 쓰지 않는 것이 좋습니다.

  • 메서드를 작성할 때는 메서드를 재정의할 것인지 아닌지를 염두에 두세요. 그렇지 않은 경우 final로 표시하되, 테스트 목적으로 메서드를 덮어쓰지 않도록 주의하세요. 일반적으로 final 메서드를 사용하면 컴파일 시간이 단축되므로 해당되는 경우 이를 사용하는 것이 좋습니다. 그러나 라이브러리에서 final 키워드를 적용할 때는 로컬 프로젝트에서 non-finalfinal로 변경하는 것과는 달리 라이브러리에서 non-finalfinal로 변경하는 것은 간단하지 않으므로 특히 주의하세요.

  • 블록 뒤에 else, catch 등과 같은 문을 사용할 때는 이 키워드를 블록과 같은 줄에 넣습니다. 여기에서도 1TBS 스타일을 따르고 있습니다. if/elsedo/catch 코드의 예는 아래와 같습니다.

if someBoolean {
    // do something
} else {
    // do something else
}

do {
    let fileContents = try readFile("filename.txt")
} catch {
    print(error)
}
  • 클래스의 인스턴스가 아닌 클래스와 연관된 함수나 프로퍼티를 선언할 때는 class보다 static을 사용하세요. 서브클래스에서 해당 함수나 프로퍼티를 재정의하는 기능이 특별히 필요한 경우에만 class를 사용하되, 대신 protocol 사용을 고려하세요.

  • 인수를 받지 않고, side effect가 없으며, 일부 객체나 값을 반환하는 함수가 있다면 함수 대신 계산된 프로퍼티를 사용하는 것이 좋습니다.

2. Access Control

  • Access Control 키워드가 필요한 경우 먼저 작성합니다.

✅ Preferred

private static let myPrivateNumber: Int

⛔ Not Preferred

static private let myPrivateNumber: Int
  • Access Control 키워드는 한 줄에 홀로 존재하면 안 되고, 설명하는 내용과 인라인으로 유지해야 합니다.

✅ Preferred

open class Pirate {
    /* ... */
}

⛔ Not Preferred

open
class Pirate {
    /* ... */
}
  • 일반적으로 internal Access Control 키워드는 기본값이므로 작성하지 마세요.

  • 단위 테스트에서 프로퍼티에 액세스해야 하는 경우 @testable import ModuleName을 사용하여 프로퍼티를 internal로 만들어야 합니다. 프로퍼티가 private로 설정되어야 하지만 단위 테스트 목적으로 internal로 선언하는 경우 이를 설명하는 적절한 주석을 추가해야 합니다. 아래와 같이 명확하게 표시하기 위해 - warning: 마크업 구문을 사용할 수 있습니다.

/**
 This property defines the pirate's name.
 - warning: Not `private` for `@testable`.
 */
let pirateName = "LeChuck"
  • 가능하면 fileprivate보다 private로 설정하는 것이 좋습니다.

  • publicopen 중에서 선택할 때는 특정 모듈 외부에서 서브클래싱이 가능한 것을 만들려는 경우 open을, 그렇지 않은 경우 public을 사용합니다. internal 이상의 모든 항목은 @testable import을 사용하여 테스트에서 서브클래싱할 수 있으므로 이것이 open을 사용할 이유가 되어서는 안 된다는 점에 유의하세요. 일반적으로 라이브러리의 경우 open을 조금 더 자유롭게 사용하되, 여러 모듈에서 동시에 변경하기 쉬운 앱과 같은 코드베이스의 모듈에 대해서는 조금 더 보수적으로 사용하는 것이 좋습니다.

3. 사용자 지정 연산자

사용자 지정 연산자보다 named function을 만드는 것이 좋습니다.

사용자 정의 연산자를 도입하려는 경우 다른 구문을 사용하는 대신 새 연산자를 전역 범위에 도입하려는 매우 타당한 이유가 있는지 확인하세요.

새로운 타입(특히 ==)을 지원하기 위해 기존 연산자를 재정의할 수 있습니다. 하지만 새 정의는 연산자의 의미를 유지해야 합니다. 예를 들어 ==는 항상 동일성을 테스트하고 boolean을 반환해야 합니다.

4. Switch 문과 Enum

  • 유한한 가능성의 집합(enum)이 있는 switch 문을 사용할 때는 default case를 포함하지 마세요. 대신 사용하지 않는 case를 맨 아래에 배치하고 break 키워드를 사용하여 실행을 방지하세요. 새로운 case가 생성됐을때 인지하지 못한 상태에서 default로 처리되지 않고 의도적으로 처리를 지정해 주기 위함입니다.

✅ Preferred

switch someEnum {
case someCase1:
    print("someCase1")
case someCase2, soemCase3:
    break
}

⛔ Not Preferred

switch someEnum {
case someCase1:
    print("someCase1")
default:
    break
}
  • Swift의 swtich case는 기본적으로 break되므로 필요하지 않은 경우 break 키워드를 포함하지 마세요.

  • case 문은 기본 Swift 표준에 따른 switch과 일치해야 합니다.

  • 연관된 값이 있는 case를 정의할 때는 단순한 타입이 아니라 적절한 레이블을 지정합니다(e.g. case hunger(Int) 대신에 case hunger(hungerLevel: Int)).

enum Problem {
    case attitude
    case hair
    case hunger(hungerLevel: Int)
}

func handleProblem(problem: Problem) {
    switch problem {
    case .attitude:
        print("At least I don't have a hair problem.")
    case .hair:
        print("Your barber didn't know when to stop.")
    case .hunger(let hungerLevel):
        print("The hunger level is \(hungerLevel).")
    }
}
  • 가능하면 fallthrough 키워드보다 가능한 리스트(e.g. case 1, 2, 3:)를 사용합니다.

  • 도달해서는 안 되는 default case가 있는 경우에는 오류를 던지거나 assert 같은 다른 유사한 방식으로 처리하는 것이 좋습니다.

func handleDigit(_ digit: Int) throws {
    switch digit {
    case 0, 1, 2, 3, 4, 5, 6, 7, 8, 9:
        print("Yes, \(digit) is a digit!")
    default:
        throw Error(message: "The given number was not a digit.")
    }
}

5. Optional

  • 암시적으로 언래핑된 optional을 사용해야 하는 유일한 경우는 @IBOutlet입니다. 다른 모든 경우에는 non-optional 또는 일반 optional 프로퍼티를 사용하는 것이 좋습니다. 프로퍼티가 절대 nil이 되지 않는다고 '보장'할 수도 있지만, 안전하고 일관성을 유지하는 것이 좋습니다. 마찬가지로 강제 언래핑을 사용하지 마세요.

  • as!try!를 사용하지 마세요.

  • optional에 저장된 값을 실제로 사용할 계획은 없지만 이 값이 nil인지 여부를 확인해야 하는 경우, if let 구문을 사용하는 대신 이 값을 명시적으로 nil과 비교하여 확인합니다.

✅ Preferred

if someOptional != nil {
    // do something
}

⛔ Not Preferred

if let _ = someOptional {
    // do something
}
  • unowned를 사용하지 마세요. unowned는 암시적으로 언래핑되는 weak 프로퍼티와 비슷하다고 생각할 수 있습니다(unowned 프로퍼티는 참조 카운팅을 완전히 무시하기 때문에 약간의 성능 향상이 있지만). 암시적 언래핑을 원하지 않으므로 마찬가지로 unowned 프로퍼티도 원하지 않습니다.

✅ Preferred

weak var parentViewController: UIViewController?

⛔ Not Preferred

weak var parentViewController: UIViewController!
unowned var parentViewController: UIViewController
  • optinal을 언래핑 할 때는 언래핑된 상수 또는 변수에 동일한 이름을 사용하세요.
guard let myValue = myValue else {
    return
}
  • 테스트에서 강제 언래핑 대신 XCTUnwrap을 사용합니다. ✅ Preferred
func isEvenNumber(_ number: Int) -> Bool {
    return number % 2 == 0
}

func testWithXCTUnwrap() throws {
    let number: Int? = functionThatReturnsOptionalNumber()
    XCTAssertTrue(isEvenNumber(try XCTUnwrap(number)))
}

⛔ Not Preferred

func isEvenNumber(_ number: Int) -> Bool {
    return number % 2 == 0
}

func testWithForcedUnwrap() {
    let number: Int? = functionThatReturnsOptionalNumber()
    XCTAssertTrue(isEvenNumber(number!)) // may crash the simulator
}

6. Protocol

프로토콜을 구현할 때, 코드를 구성하는 방법에는 두 가지가 있습니다:

  1. 프로토콜 구현을 나머지 코드와 분리하기 위한 // MARK: 주석 사용
  2. class/struct 구현 코드 외부에 있지만 동일한 소스 파일에 있는 extension 사용

그러나 extension을 사용할 때는 extension의 메서드를 서브클래스로 재정의할 수 없으므로 테스트가 어려울 수 있습니다. 이것이 일반적인 use case라면 일관성을 위해 방법 1을 사용하는 것이 좋습니다. 그렇지 않은 경우에는 방법 2를 사용하면 우려 사항을 더 깔끔하게 분리할 수 있습니다.

방법 2를 사용하는 경우에도 Xcode의 메서드/프로퍼티/클래스 등 리스트 UI에서 가독성을 높이기 위해 // MARK: 문을 추가하세요.

  • 프로토콜을 채택할 때는 extension을 만들어서 관련된 메서드를 모아둡니다. ✅ Preferred
final class MyViewController: UIViewController {
  // ...
}

// MARK: - UITableViewDataSource

extension MyViewController: UITableViewDataSource {
  // ...
}

// MARK: - UITableViewDelegate

extension MyViewController: UITableViewDelegate {
  // ...
}

⛔ Not Preferred

final class MyViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
  // ...
}

7. Property

  • 읽기 전용의 계산된 프로퍼티를 만드는 경우, 그 주위에 get {}를 붙이지 않고 getter를 제공합니다.
var computedProperty: String {
    if someBool {
        return "I'm a mighty pirate!"
    }
    return "I'm selling these fine leather jackets."
}
  • get {}, set {}, willSet, didSet 사용 시, 이 블럭을 들여쓰기 합니다.

  • willSet/didSetset의 새 값이나 이전 값에 대한 사용자 정의 이름을 생성할 수 있어도 기본적으로 제공되는 표준 newValue/oldValue 식별자를 사용합니다.

var storedProperty: String = "I'm selling these fine leather jackets." {
    willSet {
        print("will set to \(newValue)")
    }
    didSet {
        print("did set from \(oldValue) to \(storedProperty)")
    }
}

var computedProperty: String  {
    get {
        if someBool {
            return "I'm a mighty pirate!"
        }
        return storedProperty
    }
    set {
        storedProperty = newValue
    }
}
  • 싱글톤 프로퍼티는 다음과 같이 선언할 수 있습니다:
class PirateManager {
    static let shared = PirateManager()

    /* ... */
}

8. Closure

매개변수의 타입이 명확하다면 타입 이름을 생략해도 괜찮지만, 명시하는 것도 괜찮습니다. 때로는 명확한 세부 사항을 추가하여 가독성을 높이고 때로는 반복되는 부분을 제거하여 가독성을 높일 수 있으므로 최선의 판단을 내리고 일관성을 유지하세요.

// omitting the type
doSomethingWithClosure() { response in
    print(response)
}

// explicit type
doSomethingWithClosure() { response: NSURLResponse in
    print(response)
}

// using shorthand in a map statement
[1, 2, 3].flatMap { String($0) }
  • 클로저를 타입으로 지정하는 경우, 타입이 optional이거나 클로저가 다른 클로저 안에 있는 경우와 같이 필수적인 경우가 아니라면 괄호로 묶을 필요가 없습니다. 클로저의 인수는 항상 괄호로 묶어야 합니다. 인수가 없음을 나타내려면 ()를 사용하고, 아무것도 반환되지 않음을 나타내려면 Void를 사용합니다.
let completionBlock: (Bool) -> Void = { (success) in
    print("Success? \(success)")
}

let completionBlock: () -> Void = {
    print("Completed!")
}

let completionBlock: (() -> Void)? = nil
  • 매개변수 이름은 가능하면 여는 중괄호와 같은 줄에 유지합니다.

  • 매개변수 이름 없이는 클로저의 의미가 명확하지 않은 경우(e.g. 메서드에 success 및 failure 클로저에 대한 매개변수가 있는 경우)를 제외하고는 후행 클로저 구문을 사용합니다.

// trailing closure
doSomething(1.0) { (parameter1) in
    print("Parameter 1 is \(parameter1)")
}

// no trailing closure
doSomething(1.0, success: { (parameter1) in
    print("Success with \(parameter1)")
}, failure: { (parameter1) in
    print("Failure with \(parameter1)")
})

9. Array

  • 일반적으로 subscript를 사용하여 배열에 직접 액세스하지 마세요. 가능하면 optional이며 충돌을 일으키지 않는 .first 또는 .last와 같은 접근자를 사용하세요. 가능하면 for i in 0 ..< items.count와 같은 구문보다는 for item in items와 같은 구문을 사용합니다. 배열 subscript로 직접 액세스해야 하는 경우 적절한 바운드 검사를 수행해야 합니다. for (index, value) in items.enumerated()를 사용하여 인덱스와 값을 모두 가져올 수 있습니다.

  • 배열을 추가/연결할 때, += 또는 + 연산자를 절대 사용하지 마세요. 대신 .append() 또는 .append(contentsOf:)를 사용하는 것이 Swift의 현재 상태에서는 (적어도 컴파일과 관련해서는) 훨씬 더 성능이 좋습니다. 다른 배열을 기반으로 하는 배열을 선언하고 이를 immutable로 유지하려는 경우, let myNewArray = arr1 + arr2 대신 let myNewArray = [arr1, arr2].joined()를 사용하세요.

10. Error Handling

myFunction 함수가 String을 반환해야 하지만 어느 시점에서 오류가 발생할 수 있다고 가정해 보겠습니다. 일반적인 접근 방식은 이 함수가 optional String?을 반환하도록 하는 것입니다. 여기서 문제가 발생하면 nil을 반환합니다.

func readFile(named filename: String) -> String? {
    guard let file = openFile(named: filename) else {
        return nil
    }

    let fileContents = file.read()
    file.close()
    return fileContents
}

func printSomeFile() {
    let filename = "somefile.txt"
    guard let fileContents = readFile(named: filename) else {
        print("Unable to open file \(filename).")
        return
    }
    print(fileContents)
}

대신 실패의 원인을 파악해야할 때, Swift의 try/catch 동작을 사용해야 합니다.

다음과 같은 struct를 사용할 수 있습니다:

struct Error: Swift.Error {
    public let file: StaticString
    public let function: StaticString
    public let line: UInt
    public let message: String

    public init(message: String, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) {
        self.file = file
        self.function = function
        self.line = line
        self.message = message
    }
}

사용 예시:

func readFile(named filename: String) throws -> String {
    guard let file = openFile(named: filename) else {
        throw Error(message: "Unable to open file named \(filename).")
    }

    let fileContents = file.read()
    file.close()
    return fileContents
}

func printSomeFile() {
    do {
        let fileContents = try readFile(named: filename)
        print(fileContents)
    } catch {
        print(error)
    }
}

오류 처리 대신 optional을 사용하는 것이 합당한 몇 가지 예외가 있습니다. 결과를 검색하는 동안 무언가 잘못되어 결과가 의미론적으로 nil이 될 가능성이 있는 경우에는 오류 처리 대신 optional을 반환하는 것이 좋습니다.

일반적으로 메서드가 '실패'할 수 있고 optional 반환 타입을 사용한다면, 실패의 이유가 즉시 명확하지 않은 경우, 메서드에서 오류를 발생시키는 것이 합리적일 수 있습니다.

11. guard 문 사용

  • 일반적으로 if 문에 코드를 중첩하는 대신, 적용 가능한 경우 "early return" 전략을 사용하는 것을 선호합니다. 이 사용 사례에 guard 문을 사용하면 코드의 가독성을 향상시키는 데 도움이 되는 경우가 많습니다.

✅ Preferred

func eatDoughnut(at index: Int) {
    guard index >= 0 && index < doughnuts.count else {
        // return early because the index is out of bounds
        return
    }

    let doughnut = doughnuts[index]
    eat(doughnut)
}

⛔ Not Preferred

func eatDoughnut(at index: Int) {
    if index >= 0 && index < doughnuts.count {
        let doughnut = doughnuts[index]
        eat(doughnut)
    }
}
  • optional을 언래핑 할 때, if 문 대신 guard 문을 사용하여 코드에서 중첩된 들여쓰기의 양을 줄입니다.

✅ Preferred

guard let monkeyIsland = monkeyIsland else {
    return
}
bookVacation(on: monkeyIsland)
bragAboutVacation(at: monkeyIsland)

// EVEN LESS PREFERRED
if monkeyIsland == nil {
    return
}
bookVacation(on: monkeyIsland!)
bragAboutVacation(at: monkeyIsland!)

⛔ Not Preferred

if let monkeyIsland = monkeyIsland {
    bookVacation(on: monkeyIsland)
    bragAboutVacation(at: monkeyIsland)
}
  • optional 언래핑이 필요하지 않은 경우, if 문과 guard 문 중 어떤 것을 사용할지 결정할 때 가장 중요한 것은 코드의 가독성입니다. 여기에는 서로 다른 두 개의 boolean에 의존하는 경우, 여러 비교를 포함하는 복잡한 논리문 등 여러 가지 경우가 있을 수 있으므로 일반적으로 가독성과 일관성이 있는 코드를 작성하기 위해 최선의 판단을 하세요. guardif 중 어느 쪽이 더 가독성이 높은지 확실하지 않거나 가독성이 똑같은 것 같으면 guard를 사용하는 것이 좋습니다.
// an `if` statement is readable here
if operationFailed {
    return
}

// a `guard` statement is readable here
guard isSuccessful else {
    return
}

// double negative logic like this can get hard to read - i.e. don't do this
guard !operationFailed else {
    return
}
  • 서로 다른 두 상태 중에서 선택하는 경우, guard 문이 아닌 if 문을 사용합니다.

✅ Preferred

if isFriendly {
    print("Hello, nice to meet you!")
} else {
    print("You have the manners of a beggar.")
}

⛔ Not Preferred

guard isFriendly else {
    print("You have the manners of a beggar.")
    return
}

print("Hello, nice to meet you!")
  • 또한, 실패로 인해 현재 컨텍스트를 종료해야 하는 경우에만 gurad를 사용해야 합니다. 아래는 gurad 두 개를 사용하는 대신 if 문 두 개를 사용하는 것이 더 적합한 예시입니다. 여기에는 서로 block해서는 안 되는 두 개의 관련 없는 조건이 있습니다.
if let monkeyIsland = monkeyIsland {
    bookVacation(onIsland: monkeyIsland)
}

if let woodchuck = woodchuck, canChuckWood(woodchuck) {
    woodchuck.chuckWood()
}
  • 종종 guard 문을 사용하여 여러 개의 optional을 언래핑해야 하는 상황에 직면할 수 있습니다. 일반적으로 각 언래핑의 실패를 처리하는 방법이 동일(e.g. return, break, continue, throw 또는 @noescape )하다면 언래핑을 하나의 guard 문으로 결합합니다.
// combined because we just return
guard let thingOne = thingOne,
    let thingTwo = thingTwo,
    let thingThree = thingThree else {
    return
}

// separate statements because we handle a specific error in each case
guard let thingOne = thingOne else {
    throw Error(message: "Unwrapping thingOne failed.")
}

guard let thingTwo = thingTwo else {
    throw Error(message: "Unwrapping thingTwo failed.")
}

guard let thingThree = thingThree else {
    throw Error(message: "Unwrapping thingThree failed.")
}

주석

  • 항상 // 뒤에 공백을 둡니다.

  • // MARK: - 주석은 위로 2개, 아래로 1개의 빈 줄을 둡니다.

class Pirate {


    // MARK: - instance properties

    private let pirateName: String


    // MARK: - initialization

    init() {
        /* ... */
    }

}

프로그래밍 권장사항

  • 가능하다면 변수를 정의할 때 함께 초기화하도록 합니다. Then을 사용하면 초기화와 함께 속성을 지정할 수 있습니다.
let label = UILabel().then {
  $0.textAlignment = .center
  $0.textColor = .black
  $0.text = "Hello, World!"
}
  • 상수를 정의할 때에는 enum를 만들어 비슷한 상수끼리 모아둡니다. 재사용성과 유지보수 측면에서 큰 향상을 가져옵니다. struct 대신 enum을 사용하는 이유는, 생성자가 제공되지 않는 자료형을 사용하기 위해서입니다. CGFloatLiteralSwiftyColor를 사용해서 코드를 단순화시킵니다.
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
  }

  private enum Font {
    static let nameLabel = UIFont.boldSystemFont(ofSize: 14)
    static let bioLabel = UIFont.boldSystemFont(ofSize: 12)
  }

  private enum Color {
    static let nameLabelText = 0x000000.color
    static let bioLabelText = 0x333333.color ~ 70%
  }

}

이렇게 선언된 상수들은 다음과 같이 사용될 수 있습니다.

self.profileImageView.frame.origin.x = Metric.profileImageViewLeft
self.nameLabel.font = Font.nameLabel
self.nameLabel.textColor = Color.nameLabelText
  • Array<T>와, Dictionary<T: U> 보다는 [T], [T: U]를 사용합니다.

✅ Preferred

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

⛔ Not Preferred

var messages: Array<String>?
var names: Dictionary<Int, String>?
  • 컴파일러가 문맥 속에서 타입을 추론할 수 있으면, 더 간결한 코드를 위해 타입을 생략합니다.

✅ Preferred

let selector = #selector(viewDidLoad)
view.backgroundColor = .red
let toView = context.view(forKey: .to)
let view = UIView(frame: .zero)

⛔ Not Preferred

let selector = #selector(ViewController.viewDidLoad)
view.backgroundColor = UIColor.red
let toView = context.view(forKey: UITransitionContextViewKey.to)
let view = UIView(frame: CGRect.zero)
  • 문법의 모호함을 제거하기 위해 언어에서 필수로 요구하지 않는 이상 self는 사용하지 않습니다.

✅ Preferred

final class Listing {
  private let isFamilyFriendly: Bool
  private var capacity: Int
  
  init(capacity: Int, allowsPets: Bool) {
    self.capacity = capacity
    isFamilyFriendly = !allowsPets
  }

  private func increaseCapacity(by amount: Int) {
    capacity += amount
    save()
  }
}

⛔ Not Preferred

final class Listing {
  private let isFamilyFriendly: Bool
  private var capacity: Int
  
  init(capacity: Int, allowsPets: Bool) {
    self.capacity = capacity
    self.isFamilyFriendly = !allowsPets // `self.` not required here
  }

  private func increaseCapacity(by amount: Int) {
    self.capacity += amount
    self.save()
  }
}

✅ Preferred

TaxiPush.progressing.asPushActionObservable(callInfo.value.call.id)
            .map { $0.progressing }.unwrap()
            .map { $0/60 }
            .bind(to: timeRadius)
            .disposed(by: disposeBag)

⛔ Not Preferred

TaxiPush.progressing.asPushActionObservable(callInfo.value.call.id)
            .map { $0.progressing }.unwrap()
            .map { $0/60 }
            .bind(to: self.timeRadius)
            .disposed(by: self.disposeBag)
  • 프로퍼티의 초기화는 가능하면 init에서하고 가능하면 unwrapped Optionl의 사용을 지양합니다.

✅ Preferred

class MyClass: NSObject {

  init() {
    someValue = 0
    super.init()
  }

  var someValue: Int
}

⛔ Not Preferred

class MyClass: NSObject {

  init() {
    super.init()
  }

  var someValue: Int?
}
  • 제네릭 타입 파라미터는 대문자를 사용하고 묘사적이어야 합니다. 타입 이름이 의미있는 관계나 역할을 갖지 않는 경우에만 T, U 혹은 V 같은 전형적인 단일 대문자를 사용하고 그 외에는 의미있는 이름을 사용합니다.

✅ Preferred

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

⛔ Not Preferred

struct Stack<T> { ... }
func write<target: OutputStream>(to target: inout target)
func swap<Thing>(_ a: inout Thing, _ b: inout Thing)
  • 디폴트 타입 메서드는 static을 사용합니다.

✅ Preferred

class Fruit {
  static func eatFruits(_ fruits: [Fruit]) { ... }
}

⛔ Not Preferred

class Fruit {
  func eatFruits(_ fruits: [Fruit]) { ... }
}
  • 더 이상 상속이 발생하지 않는 클래스는 항상 final 키워드로 선언합니다.

✅ Preferred

final class SettingsRepository {
  // ...
}

⛔ Not Preferred

class SettingsRepository {
  // ...
}
  • return은 생략하지 않습니다.

✅ Preferred

["1", "2", "3"].compactMap { return Int($0) }

var size: CGSize {
  return CGSize(
    width: 100.0,
    height: 100.0)
}

func makeInfoAlert(message: String) -> UIAlertController {
  return UIAlertController(
    title: "Info",
    message: message,
    preferredStyle: .alert)
}

⛔ Not Preferred

["1", "2", "3"].compactMap { Int($0) }

var size: CGSize {
  CGSize(
    width: 100.0,
    height: 100.0)
}

func makeInfoAlert(message: String) -> UIAlertController {
  UIAlertController(
    title: "Info",
    message: message,
    preferredStyle: .alert)
}
  • Xcode가 자동으로 생성한 템플릿을 포함한 사용하지 않는 코드는 placeholder 코멘트를 포함해 모두 제거합니다.

✅ Preferred

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  return Database.contacts.count
}

⛔ Not Preferred

override func didReceiveMemoryWarning() {
  super.didReceiveMemoryWarning()
  // Dispose of any resources that can be recreated.
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  // #warning Incomplete implementation, return the number of rows
  return Database.contacts.count
}

참고 링크

https://github.com/StyleShare/swift-style-guide

https://jusung.github.io/Swift-Code-Convention

https://github.com/linkedin/swift-style-guide

https://google.github.io/swift

https://soojin.ro/blog/english-for-developers-swift

About

EYE-Mate 팀 code style guide 문서

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published