팀에서 사용 중인 Swift Style Guide (코드 컨벤션)
코드 컨벤션이 있으면 협업시 일관성있는 코드를 작성할 수 있어서 코드의 일관성 유지에 좋습니다. 또 다른 사람이 작성한 코드를 읽을 때 코드의 문법보다 로직에 집중할 수 있는 장점이 있습니다.
다음은 저희팀에서 사용 중인 Swift Style Guide입니다. 코드 컨벤션 결정시 참조하시면 좋을 것 같습니다.
이 코드 컨벤션 작성시 다음과 같은 다른 컨벤션을 참조했습니다.
[참조한 Swift 코드 컨벤션 ]
Swift Style Guide
목차
[1. 코드 포매팅]
1.1 임포트
- 모듈 임포트는 알파벳 순으로 정렬합니다. 내장 프레임워크를 먼저 임포트하고, 빈 줄로 구분해 3rd-party프레임워크를 임포트 합니다.
import UIKit
import SwiftyColor
import SwiftyImage
import Then
import URLNavigator
- 파일이 필요로하는 최소의 모듈만 임포트 합니다. 예를들어,
Foundation
으로 충분하면UIKit
은 임포트 하지 않습니다.
✅ Preferred
import UIKit
var view: UIView
var deviceModels: [String]
✅ Preferred
import Foundation
var deviceModels: [String]
⛔️ Not Preferred
import UIKit
import Foundation
var view: UIView
var deviceModels: [String]
⛔️ Not Preferred
import UIKit
var deviceModels: [String]
- 모듈의 상세까지 지정할 수 있으면 지정합니다.
✅ Preferred
import struct SwiftyJSON.JSON
import struct CoreLocation.CLLocation.CLLocationCoordinate2D
⛔️ Not Preferred
import SwiftyJSON
import CoreLocation
1.2 빈줄
- 빈 줄에는 공백이 포함되지 않도록 합니다.
- 모든 파일은 빈 줄로 끝나도록 합니다.
1.3 들여쓰기
- 탭을 눌렀을시 4개의 space를 사용합니다.
- 들여쓰기는 Xcode에서 제공하는
^ + i
를 눌렀을 때, 적용되는 space를 사용합니다. - 최대 가로 길이는 100 characters를 사용합니다.
1.4 띄어쓰기
- 콜론(
:
)을 사용할때는 콜론의 오른쪽에만 공백을 둡니다.
[상수]
✅ Preferred
let names: [String: String]?
⛔️ Not Preferred
let names: [String:String]?
let names: [String : String]?
[클래스]
✅ Preferred
class MyClass: SuperClass {
// ...
}
⛔️ Not Preferred
class MyClass : SuperClass {
// ...
}
- 삼항연산자의 경우 콜론 앞뒤로 띄웁니다.
✅ Preferred
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return shouldRotate ? .allButUpsideDown : .portrait
}
⛔️ Not Preferred
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return shouldRotate ? .allButUpsideDown: .portrait
}
[기타]
✅ Preferred
// specifying type
let pirateViewController: PirateViewController
// dictionary syntax (note that we left-align as opposed to aligning colons)
let ninjaDictionary: [String: AnyObject] = [
"fightLikeDairyFarmer": false,
"disgusting": true
]
// declaring a function
func myFunction<T, U: SomeProtocol>(firstArgument: U, secondArgument: T) where T.RelatedType == U {
/* ... */
}
// calling a function
someFunction(someArgument: "Kitten")
// superclasses
class PirateViewController: UIViewController {
/* ... */
}
// protocols
extension PirateViewController: UITableViewDataSource {
/* ... */
}
if let
,guard let
구문이 긴 경우에는 줄바꿈하고 한 칸 들여씁니다.
✅ Preferred
if let user = self.veryLongFunctionNameWhichReturnOptionalUser(),
let name = user.veryLongFunctionNameWhichReturnsOptionlName(),
user.gender == .female {
// ...
}
guard let user = self.veryLongFunctionNameWhichReturnsOptionalUser(),
let name = user.veryLongFunctionNameWhichReturnsOptionalName(),
user.gender == .female else {
return
}
- 일반적으로 콤마(
,
) 뒤에는 공백을 추가합니다.
✅ Preferred
let myArray = [1, 2, 3, 4, 5]
⛔️ Not Preferred
let myArray = [1,2,3,4,5]
- 연산자 앞뒤로 공백을 추가합니다.
✅ Preferred
let myValue = 20 + (30 / 2) * 3
⛔️ Not Preferred
let myValue = 20+(30/2)*3
- 화살표 양쪽에 가독성을 위해 빈 공백을 추가합니다.
[함수 리턴 타입]
✅ Preferred
func doSomething() -> String {
// ...
}
⛔️ Not Preferred
func doSomething()->String {
// ...
}
[클로저 리턴 타입]
✅ Preferred
func doSomething(completion: () -> Void) {
// ...
}
⛔️ Not Preferred
func doSomething(completion: ()->Void) {
// ...
}
[Rx]
✅ Preferred
.map { $0.call.farePaymentType }
.map { method -> String? in
guard let name = method.name else { return nil }
return Local.Taxi.taxiDispatchingPaidItemApply.string.phrase(["item_name": name])
}
⛔️ Not Preferred
.map {$0.call.farePaymentType}
.map {method -> String? in
guard let name = method.name else { return nil }
return Local.Taxi.taxiDispatchingPaidItemApply.string.phrase(["item_name": name])
}
✅ Preferred
var bizGroupHidden: Driver<Bool> {
return self.callInfo.asObservable()
.map { $0.call.bizGroup }
.map { $0 == nil }
.asDriverJustComplete()
}
⛔️ Not Preferred
var bizGroupHidden:Driver<Bool> {
return self.callInfo.asObservable()
.map { $0.call.bizGroup }
.map { $0 == nil }
.asDriverJustComplete()
}
1.5 기타
- 불필요한 괄호는 생략합니다.
✅ Preferred
if userCount > 0 { ... }
switch someValue { ... }
let evens = userCounts.filter { number in number % 2 == 0 }
let squares = userCounts.map { $0 * $0 }
⛔️ Not Preferred
if (userCount > 0) { ... }
switch (someValue) { ... }
let evens = userCounts.filter { (number) in number % 2 == 0 }
let squares = userCounts.map() { $0 * $0 }
enum
의 연관값을 사용하지 않는 경우는 생략합니다.
✅ Preferred
if case .done = result { ... }
switch animal {
case .dog:
...
}
⛔️ Not Preferred
if case .done(_) = result { ... }
switch animal {
case .dog(_, _, _):
...
}
2. 네이밍
- 묘사를 잘하고 일관된 네이밍은 코드를 읽고 이해하기 쉽게 해줍니다. 네이밍은 Apple의 API Design Guidelines을 따릅니다.
- 클래스(타입, 프로토콜 이름 포함) 이름에는 UpperCamelCase(첫 문자를 대문자로 시작하는 camel표기법), 함수 이름에는 camelCase를 사용합니다.
2.1 일반
[일반]
✅ Preferred
protocol SpaceThing {
// ...
}
class SpaceFleet: SpaceThing {
enum Formation {
// ...
}
class Spaceship {
// ...
}
var ships: [Spaceship] = []
static let worldName: String = "Earth"
func addShip(_ ship: Spaceship) {
// ...
}
}
let myFleet = SpaceFleet()
[변수/상수]
- 일반변수 / 상수인 경우 따로 접두사를 붙이지 않습니다.
✅ Preferred
let maximumNumberOfLines = 3
⛔️ Not Preferred
let kMaximumNumberOfLines = 3
let MAX_LINES = 3
static
상수인 경우 앞에 k를 붙여줍니다.
✅ Preferred
static let kMaximumNumberOfLines = 3
⛔️ Not Preferred
static let maximumNumberOfLines = 3
[열거형]
✅ Preferred
enum Result {
case success
case failure
}
⛔️ Not Preferred
enum Result {
case Success
case Failure
}
[RxSwift]
- RxSwift의 Subject, Driver, ControlerProperty, ControlEvent 등은 따로 접미사를 붙이지 않습니다.
✅ Preferred
let recommendItem = BehaviorRelay<RecommendRideItem?>(value: nil)
let optionSwitch = PublishSubject<Void>()
let showRideSuggestion = PublishSubject<Void>()
⛔️ Not Preferred
let recommendItem = BehaviorRelay<RecommendRideItem?>(value: nil)
let optionSwitchSignal = PublishSubject<Void>()
let showRideSuggestionPS = PublishSubject<Void>()
- 일반적인 부분이 앞에두고 구체적인 부분을 뒤에 둡니다.
✅ Preferred
let titleMarginRight: CGFloat
let titleMarginLeft: CGFloat
let bodyMarginRight: CGFloat
let bodyMarginLeft: CGFloat
⛔️ Not Preferred
let rightTitleMargin: CGFloat
let leftTitleMargin: CGFloat
let bodyRightMargin: CGFloat
let bodyLeftMargin: CGFloat
- 생략시 사용이 모호해지는 타입은 이름에 타입에 대한 힌트를 포함시킵니다.
✅ Preferred
let titleText: String
let cancelButton: UIButton
⛔️ Not Preferred
let title: String
let cancel: UIButton
2.2 클래스
- 함수 이름에는 되도록
get
을 붙이지 않습니다.
✅ Preferred
func name(for user: User) -> String?
⛔️ Not Preferred
func getName(for user: User) -> String?
2.3 함수
- 액션 함수의 네이밍은 ‘주어 + 동사 + 목적어’ 형태를 사용합니다.
- will은 특정 행위가 일어나기 직전이고, did는 특정 행위가 일어난 직후입니다.
✅ Preferred
func backButtonDidTap() {
// ...
}
⛔️ Not Preferred
func back() {
// ...
}
func pressBack() {
// ...
}
2.4 약어
- 약어로 시작하는 경우 소문자로 표기하고, 그 외 경우에는 항상 대문자로 표기합니다.
[예1]
✅ 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?
[예2]
✅ Preferred
class URLValidator {
func isValidURL(_ url: URL) -> Bool {
// ...
}
func isProfileURL(_ url: URL, for userID: String) -> Bool {
// ...
}
}
let urlValidator = URLValidator()
let isProfile = urlValidator.isProfileUrl(urlToTest, userID: idOfUser)
⛔️ Not Preferred
class UrlValidator {
func isValidUrl(_ URL: URL) -> Bool {
// ...
}
func isProfileUrl(_ URL: URL, for userId: String) -> Bool {
// ...
}
}
let URLValidator = UrlValidator()
let isProfile = URLValidator.isProfileUrl(URLToTest, userId: IDOfUser)
3. 기타
3.1 클로저
- 파라미터와 리턴 타입이 없는 클로저 정의시에는
() -> Void
를 사용합니다.
✅ Preferred
let completionBlock: (() -> Void)?
⛔️ Not Preferred
let completionBlock: (() -> ())?
let completionBlock: ((Void) -> (Void))?
- 클로저 정의시 파라미터에는 괄호를 사용하지 않습니다.
✅ Preferred
{ operation, responseObject in
// doSomething()
}
⛔️ Not Preferred
{ (operation, responseObject) in
// doSomething()
}
- 클로저 정의시 가능한 경우 타입 정의를 생략합니다.
✅ Preferred
completion: { finished in
// doSomething()
}
⛔️ Not Preferred
completion: { (finished: Bool) -> Void in
// doSomething()
}
- 클로저 호출시 또 다른 유일한 클로저를 마지막 파라미터로 받는 경우, 파라미터 이름을 생략합니다.
✅ Preferred
UIView.animate(withDuration: 0.5) {
// doSomething()
}
⛔️ Not Preferred
UIView.animate(withDuration: 0.5, animations: { () -> Void in
// doSomething()
})
- 사용하지 않는 파라미터는
_
를 사용해 표시합니다.
✅ Preferred
someAsyncThing() { _, _, argument3 in
print(argument3)
}
⛔️ Not Preferred
// WRONG
someAsyncThing() { argument1, argument2, argument3 in
print(argument3)
}
- 한줄 클로저는 반드시 각 괄호 양쪽을 공백을 추가해야 합니다.
✅ Preferred
let evenSquares = numbers.filter { $0 % 2 == 0 }.map { $0 * $0 }
⛔️ Not Preferred
let evenSquares = numbers.filter {$0 % 2 == 0}.map { $0 * $0 }
3.2 클래스와 구조체
- 구조체를 생성할 때는 Swift 구조체 생성자를 사용합니다.
✅ Preferred
let frame = CGRect(x: 0, y: 0, width: 100, height: 100)
⛔️ Not Preferred
let frame = CGRectMake(0, 0, 100, 100)
3.3 타입
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>?
3.4 타입추론 사용
- 컴파일러가 문맥속에서 타입을 추론할 수 있으면 더 간결한 코드를 위해 타입을 생략합니다.
✅ 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)
3.5 self
- 문법의 모호함을 제거하기 위해 언어에서 필수로 요구하지 않는 이상
self
는 사용하지 않습니다.
final class Listing {
private let isFamilyFriendly: Bool
private var capacity: Int
init(capacity: Int, allowsPets: Bool) {
✅ Preferred
self.capacity = capacity
isFamilyFriendly = !allowsPets
⛔️ Not Preferred
self.capacity = capacity
self.isFamilyFriendly = !allowsPets // `self.` not required here
}
private func increaseCapacity(by amount: Int) {
✅ Preferred
capacity += amount
⛔️ Not Preferred
self.capacity += amount
✅ Preferred
save()
⛔️ Not Preferred
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)
3.6 튜플
- 튜플의 맴버에는 명확성을 위해 이름을 붙여줍니다. 만약 필드가 3개를 넘는 경우
struct
를 사용을 고려해보는 것을 권장합니다.
✅ Preferred
func whatever() -> (x: Int, y: Int) {
return (x: 4, y: 4)
}
⛔️ Not Preferred
func whatever() -> (Int, Int) {
return (4, 4)
}
let thing = whatever()
print(thing.0)
✅ Okay
func whatever2() -> (x: Int, y: Int) {
let x = 4
let y = 4
return (x, y)
}
let coord = whatever()
coord.x
coord.y
✅ Preferred
.map{($0.coord, nil, false)}
.withLatestFrom(viewModel.swapController) { (mapInfo: $0, swapController: $1) }
.filter { guard case .search = $0.swapController else { return false}; return true }
.map{$0.0},
⛔️ Not Preferred
.map{($0.coord, nil, false)}
.withLatestFrom(viewModel.swapController) {($0, $1)}
.filter{guard case .search = $0.1 else {return false}; return true}
.map{$0.0},
3.7 패턴
- 프로퍼티의 초기화는 가능하면
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?
}
3.8 제네릭
- 제네릭 타입 파라미터는 대문자를 사용하고 묘사적이어야 합니다. 타입 이름이 의미있는 관계나 역할을 갖지 않는 경우에만
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)
3.9 static
- 디폴트 타입 매소드는
static
을 사용합니다.
✅ Preferred
class Fruit {
static func eatFruits(_ fruits: [Fruit]) { ... }
}
⛔️ Not Preferred
class Fruit {
class func eatFruits(_ fruits: [Fruit]) { ... }
}
3.10 final
- 더 이상 상속이 발생하지 않는 클래스는 항상
final
키워드로 선언합니다.
✅ Preferred
final class SettingsRepository {
// ...
}
⛔️ Not Preferred
class SettingsRepository {
// ...
}
3.11 프로토콜 extension
- 프로토콜을 적용할 때는 extension을 만들어서 관련된 매소드를 모아둡니다.
✅ Preferred
final class MyViewController: UIViewController {
// ...
}
// MARK: - UITableViewDataSource
extension MyViewController: UITableViewDataSource {
// ...
}
// MARK: - UITableViewDelegate
extension MyViewController: UITableViewDelegate {
// ...
}
⛔️ Not Preferred
final class MyViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
// ...
}
3.12 switch-case
switch-case
에서 가능한 경우default
를 사용하지 않습니다.- 새로운
case
가 생성됐을때 인지하지 못한 상태에서default
로 처리되지 않고 의도적으로 처리를 지정해 주기 위함입니다.
✅ Preferred
switch anEnum {
case .a:
// Do something
case .b, .c:
// Do something else.
}
⛔️ Not Preferred
switch anEnum {
case .a:
// Do something
default:
// Do something else.
}
3.13 return
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)
}
3.14 사용하지 않는 코드
- 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
}