본문 바로가기
개발 블로그/아이폰개발

[iOS Swift] Swift Style Guide , 스위프트 코딩 스타일 가이드 (coding standard]

by snapshot 2020. 6. 10.

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

 

raywenderlich/swift-style-guide

The official Swift style guide for raywenderlich.com. - raywenderlich/swift-style-guide

github.com

이것의 기반으로 번역 및 나의 생각을 적어 봤다. 

가장 기본적인 스타일 가이드이지만 다른 사람과 협업을 할 때 이렇게 맞춰놓으면 가독성이 훨씬 좋을 것이다. 

코드 컨벤션을 너무 빡빡하게 할 필요는 없을 것이다. 

우리가 더 고민해야 할 것이 많기 때문에... 이 정도에서 협의를 보자..

 

 

Naming


기술적이고 일관된 명칭은 소프트웨어를 더 쉽게 읽고 이해할 수 있게 한다.

API Design Guide에 설명된 Swift 명명 규칙을 사용하십시오.

몇 가지 핵심 요인은 다음과 같은 것들이 있다.

  • 사용하는 곳에 명확하게 이해할 수 있게 이름을 만들어라
  • 간결함 보다는 명확함을 우선시
  • CamelCase를 사용한다.
  • 타입이나 프로토콜은 UpperCamelCase를 따르고 그 외에는 lowerCamelCase를 따른다.
  • 기능에 기반한 이름을 사용한다.
  • 팩토리 메서드는 make라는 이름으로 시작한다.
  • 설정 메서드는 set으로 시작
  • 네트워크 통신은 request로 시작
  • 어떤 설정에 대한 구성은 config로 시작
  • mutating함수는 명사형 동사를 쓰고, nonMutating함수는 -ed, -ing를 붙여서 사용한다.

https://k.kakaocdn.net/dn/bCBDNP/btqwZZjzKu9/TE1YhcdhlsAXRhuZKZPKh0/img.png

  • 무엇을 해야하는지 설명하는 프로토콜은 명사로
  • 능력(Capability)을 설명하는 프로토콜은 -able, -ible로 끝나야 한다.
  • 혼동을 주지 않는 용어를 사용한다.
  • 약어는 제발 피하자
  • 이름은 기존 문화를 최대한 따르자. ( 초보자용 용어로 최적화할 필요는 없다.)
  • free function 대신 Method와 Properties를 선호해라.

ex) function과 Method의 차이점

function과 Method의 의미에는 차이점이 있다.

둘 다 재 사용할 수 있는 코드지만, Method는 Class, Struct, Enum에 속해있고, function은 속해있지 않다.

func thisIsFunction() {
}

struct SomeA {
    func thisIsMethod() {
    }
}
  • 같은 의미를 공유하는 메서드들에 대해 동일한 기본 이름을 준다 (오버로딩)
  • 리턴 타입이 다른 오버로딩은 피하자
func A() -> Int {
 return 1
}

func A() -> String {
 return “a”
}
  • 오버로딩이 너무 많으면 어떤 함수가 불려줄지 모르는데.. 과연 필요한가.. 고민..
  • 좋은 파라미터 이름을 선택해야 된다.

크게 메서드생성자로 나눌 수가 있다.

1. 메서드

Swift API는 명확하게 전치사(at, from, in, with, to, by, for)등을 많이 사용한다.

일반적으로 전치사가 필요한 메서드들은 파라미터를 써주고, 자연스러운 흐름이 되는 메서드들은 생략한다. 즉, 자연스럽게 문장이 이어지게 만들어야 된다는 뜻.

// 전치사구로 인해 정확한 표현 가능
view.frame.inset(by: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10))
CGPoint(x: 30, y: 30)
[1,2,3].remove(at: 1)

// 생략 가능
view.addSubview(view2)
view.convert(point: CGPoint, from: UICoordinateSpace)
view.convert(point: CGPoint, to: UICoordinateSpace)

2. 생성자

Objectvice-C처럼 생성자의 첫 번째 파라미터명은 전치사나 접속사를 써서 문장처럼 이어지게 만들면 안 된다.

선호하는 예제:

let foreground = Color(red: 32, green: 64, blue: 128)
let newPart = factory.makeWidget(gears: 42, spindles: 14)
let ref = Link(target: destination)

선호하지 않은 예제:

let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128)
let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)
let ref = Link(to: destination)

****

산문(Prose)

산문(prose)에서 메서드를 언급할 때, 모호하지 않는 것이 중요하다. 메서드 이름을 참조하려면, 가능하면 가장 간단한 형식을 사용한다

  1. 매개변수 없이 메소드 이름을 작성한다.
    1. 예제: 다음으로 addTarget메서드를 호출해야 한다
  2. 인자 레이블을 가지는 메소드 이름을 작성한다.
    1. 예제: 다음으로 addTarget(_:action:) 메서드를 호출해야 한다
  3. 인자 레이블과 타입을 가지는 메소드 전체 이름을 작성한다.
    1. 예제: 다음으로, addTarget(_: Any?, action: Selector?) 메소드를 호출해야 한다.

UIGestureRecognizer을 사용하는 위의 예제에서, 1은 모호하지 않고 선호된다


클래스 접두사(Class Prefixes)

Swift 타입은 모듈에 의해 자동적으로 namespaced가 포함되고, RW처럼 클래스 접두사를 추가하지 않아야 한다. 다른 모듈의 이름이 충돌하는 경우, 타입 이름 앞에 모듈 이름을 붙여 명확하게 할 수 있다.

하지만 혼동스러울 수 있는 경우에만 모듈 이름을 지정해서 구분해준다!

접두사는 프레임워크로 배포할 때는 필요할 거 같다. 다른 프레임워크와 겹칠 수 있으니...

하지만 내부 프로젝트에서는 굳이 필요가 없다.

import SomeModule

let myClass = MyModule.UsefulClass()

****

델리게이트(Delegates)

델리게이트 메서드를 만들 때 이름이 없는 첫 번째 매개변수는 델리게이트 이름이어야 한다.

(UIKit에 이에 대한 많은 예제가 있다)

선호하는 예제:

func namePickerView(_ namePickerView: NamePickerView, didSelected name: String)
func namePickerViewShouldReload(_ namePickerView: NamePickerView) -> Bool

선호하지 않은 예제:

func didSelectName(namePicker: NamePickerViewController, name: String)
func namePickerShouldReload() -> Bool

****

문맥에 맞는 타입 추론 사용하기(Use Type Inferred Context)

컴파일러에 의해 추론된 짧고, 명확한 코드를 사용해라

이건 고민 좀 해봐야지.. 나는 선호하지 않은 예제가 좋은데...

선호하는 예제:

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

선호하지 않은 예제:

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

****

제네릭(Generics)

제네릭 타입 매개변수는 대문자 카멜 표기법 이름으로 서술해야 한다.

타입의 이름에 의미나 규칙이 없을 때 전통적으로 T, U, V 같은 하나의 대문자를 사용한다.

선호하는 예제:

stuct Stack<Element> { ... }
func wirte<Target: OutputStream>(to target: inout Target)
func swp<T>(_ a: inout T, _ b: inout T)

선호하지 않은 예제:

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

****

언어(Language)

애플의 API와 일치하는 US English spelling을 사용해라

선호하는 예제:

let color = "red"

선호하지 않은 예제:

let colour = "red"

****

코드 구성(Code Organization)


Extension을 사용하여 코드를 논리적 기능 블록으로 구성해라.

//MARK: - 를 표현해주는 것이 좋다

Protocol Conformance

선호하는 예제:

class MyViewController: UIViewController {
  // class stuff here
}

// MARK: - UITableViewDataSource
extension MyViewController: UITableViewDataSource {
  // table view data source methods
}

// MARK: - UIScrollViewDelegate
extension MyViewController: UIScrollViewDelegate {
  // scroll view delegate methods
}

선호하지 않은 예제:

class MyViewController: UIViewController, UITableViewDataSource, UIScrollViewDelegate {
  // all methods
}

파생된(derived) 클래스에서 프로토콜 준수하는 것에 대해 재 선언(re-declare)하는 것은 컴파일러가 허용하지 않기 때문에, 항상 기본 클래스의 그룹을 extension으로 복사하는 것을 요구하지 않는다.

특히 파생된 클래스가 터미널 클래스이고 소수의 메서드가 재정의되고 있는 경우에 그러하다.

extension 그룹을 유지하는 것은 개발자의 재량이라고 볼 수 있다.

UIKit 뷰 컨트롤러의 경우 별도의 클래스 확장에서 그룹화 수명주기, 사용자 정의 접근자들 및 IBAction을 고려하십시오.

extension에서 변수 생성은 가능은 하지만 추천하지 않는다.

Associated objects API를 따라야 하고 이건 컴파일에 변수를 지정하는 게 아닌 런타임 때 만들어지기 때문에 디버깅도 어려울 수 있다.

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

    }

}

//MARK: - Action
extension ViewController {

    @IBAction func openPickerView(_ sender: UIButton) {
        let pickerVC = UIImagePickerController()
        pickerVC.delegate = self
        present(pickerVC, animated: true, completion: nil)
    }

}

//MARK: - UIImagePickerDelegate
extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {

    }
}

****

사용되지 않는 코드(Unused Code)

XCode 템플릿 코드를 포함하여 사용되지 않는(죽은) 코드, placeholder 주석을 제거해야 한다.

직접 관련되지 않은 고급 메서드와 함께 단순히 상위 클래스를 호출하는 구현의 튜토리얼도 제거해야 한다. 여기에는 비어있거나/사용되지 않는 UIApplicationDelegate 메서드들도 포함한다.

AppDelegate.swift 파일 보면 Xcode에서 기본적으로 제공하는 템플릿 함수들과 주석들이 있는데 안 쓰는 건 제거하라는 뜻이다

선호하는 예제:

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

선호하지 않은 예제:

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

override func numberOfSections(in tableView: UITableView) -> Int {
  // #warning Incomplete implementation, return the number of sectionsreturn 1
}

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

func applicationDidEnterBackground(_ application: UIApplication) {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}

func applicationWillEnterForeground(_ application: UIApplication) {
    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}

func applicationDidBecomeActive(_ application: UIApplication) {
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}

func applicationWillTerminate(_ application: UIApplication) {
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

****

Imports를 최소화하기(Minimal Imports)

예를 들어 Foundation을 import 했으면 UIKit은 하지 않아도 된다. 유사하게 UIKit을 임포트 했으면 Foundation을 임포트 하지 않아도 된다.

****

공백(Spacing)


공간을 절약하고 줄 바꿈을 방지하기 위해 탭 대신에 공백 2칸을 사용하여 들여 쓰기를 한다. 이 설정은 아래 그림과 같이 XCode와 프로젝트 환경 설정에서 설정해야 한다.

난 4개가 좋은데..??

Xcode → Preferences → Text Editing → Indentation

https://k.kakaocdn.net/dn/d25j19/btqw3Ottl5N/X4CiAVgxEkKnkfpcyxgaIK/img.png

선호하는 예제:

if user.isHappy {
  // Do something
} else {
  // Do something else
}

선호하지 않은 예제:

if user.isHappy
{
  // Do something
}
else {
  // Do something else
}

위의 올바른 예제처럼 오프닝 브레이스 **{** 후 또는 클로징 브레이스 } 앞에 빈 줄이 없어야 한다.

Colon(:)은 항상 왼쪽에는 공백이 없고 오른쪽엔 하나의 공백이 있다.

-> 예외적으로? : , 빈 딕셔너리 [:], #selector문법의 addTarget(:action:)

선호하는 예제:

class TestDatabase: Database {
  var data: [String: CGFloat] = ["A": 1.2, "B": 3.2]
}

선호하지 않은 예제:

class TestDatabase : Database {
  var data :[String:CGFloat] = ["A" : 1.2, "B":3.2]
}
  • 긴 줄은 70 글자 내외로 감싸야한다. 강제적인 제한은 의도적으로 지정되지 않는다.
  • 줄 끝에 공백은 피한다.
  • 각 파일의 끝에 하나의 개행 문자를 추가한다.

주석(Comments)


필요한 경우, 특정 코드가 무엇인가를 왜(why) 하는지 설명하기 위해 주석을 사용한다. 주석은 반드시 최신 상태로 유지하거나 삭제되어야 한다.

코드는 가능하면 자체적으로 문서가 되어야 하므로, 코드와 함께 있는 인라인(inline) 주석은 피한다. 예외: 문서 생성에 사용되는 주석은 제외된다.

C 스타일의 /* ... */ 의 주석을 피하고 //, /// 주석을 사용해라

클래스와 구조체(Classes and Structures)

****

Self 사용하기(Use of Self)

간결함을 위해, Swift는 객체의 프로퍼티에 접근하거나 메서드 호출할 필요가 없는 경우에 self 사용을 피한다.

컴파일러에 의해 요구될 때에만 self를 사용한다(@escaping 클로저나 초기화에서 인자가 프로퍼티와 애매모호할 때).

다른 말로, self 없이 컴파일하는 경우에 생략한다.

****

계산 프로퍼티(Computed Properties)

간결함을 위해 읽기 전용인 경우 get을 생략해야 된다.

get은 set이 필요할 때만 사용된다.

선호하는 예제:

var diameter: Double {
  return radius * 2
}

var diameter: Double {
    get {
        return radius * 2
    }
    set {
        radius = newValue / 2
    }
}

선호하지 않은 예제:

var diameter: Double {
  get {
    return radius * 2
  }
}

****

Final

final의 사용은 가끔씩 의도를 명확하게 해 주고 비용의 가치가 있다. 아래 예제에서, Box는 특별한 목적을 가지고 파생된 클래스의 사용자 정의는 의도되지 않는다. final로 표시하면 분명해진다.

// Turn any generic type into a reference type using this Box class.
final class Box<T> { 
  let value: T 
  init(_ value: T) {
    self.value = value 
  } 
}

final은 상속되거나 재정의되는 것을 막는 클래스 수식어.

****

메서드 선언(Function Declarations)


opening brace를 포함하여 한 줄의 메서드 선언이면 형태 그래도 유지해라

func reticulateSplines(spline: [Double]) -> Bool {
  // reticulate code goes here
}

긴 함수에 대해 각각의 파마 리터들을 새로운 라인으로 만들어라

func reticulateSplines(
  spline: [Double], 
  adjustmentFactor: Double,
  translateConstant: Int, comment: String
) -> Bool {
  // reticulate code goes here
}

input에 (Void)를 사용하지 말고 ()를 사용해라! 클로저나 함수의 output에 ()를 사용하지 말고 Void를 사용해라!

이유가 뭘까..? 가독성 때문인 건가..? 그냥 이렇게 규칙을 만들자는 건가..?

선호하는 예제:

func updateConstraints() -> Void {
  // magic happens here
}

typealias CompletionHandler = (result) -> Void

선호하지 않은 예제:

func updateConstraints() -> () {
  // magic happens here
}

typealias CompletionHandler = (result) -> ()

****

메서드 호출(Function Call)


파라미터가 한 개일 때:

let success = reticulateSplines(splines)

여러 개일 때: 정말 이렇게 사용해야 가독성이 좋더라..

let success = reticulateSplines(
  spline: splines,
  adjustmentFactor: 1.3,
  translateConstant: 2,
  comment: "normalize the display")

****

클로저 표현(Closure Expressions)


인자 목록 중 마지막 인자가 단일 클로저 형태일 때만 후행 클로저를 사용한다.

선호하는 예제:

UIView.animate(withDuration: 1.0) {
  self.myView.alpha = 0
}

UIView.animate(withDuration: 1.0, animations: {
  self.myView.alpha = 0
}, completion: { finished in
  self.myView.removeFromSuperview()
})

선호하지 않은 예제:

UIView.animate(withDuration: 1.0, animations: {
  self.myView.alpha = 0
})

UIView.animate(withDuration: 1.0, animations: {
  self.myView.alpha = 0
}) { f in
  self.myView.removeFromSuperview()
}

후행클로저를 사용으로 Chained 된 메서드는 문맥에 따라 명확하고 읽기 쉬워야 된다.

익명 인자로 지어진 이름을 사용할 때 간격, 줄 바꿈을 결정하는 것은 개발자의 자유!

~~let value = numbers.map { $0 * 2 }.filter { $0 % 3 == 0 }.index(of: 90)~~
나는 이렇게 연결된건 별로다.

let value = numbers
  .map {$0 * 2}
  .filter {$0 > 50}
  .map {$0 + 10}

****

타입


가능하면 항상 Swift의 기본 타입을 사용해야 된다.

Swift는 Objective-C에 Bridging을 제공하며, 필요에 따라 전체 메서드 세트를 사용할 수 있다.

선호하는 예제:

let width = 120.0                                    // Doublelet widthString = "\(width)"                         // String

덜 선호하는 예제:

let width = 120.0                                    // Doublelet widthString = (width as NSNumber).stringValue    // String

선호하지 않은 예제:

let width: NSNumber = 120.0                          // NSNumberlet widthString: NSString = width.stringValue        // NSString

****

상수(Constants)

상수는 let키워드를 사용, 변수는 var를 사용

변수의 값이 바뀌지 않을 경우에 언제나 let을 사용해라!

팁 : 좋은 방법은 let을 항상 사용하고, 컴파일러가 불평하면 var를 사용해라!

타입 프로퍼티(Type Properties)는 static let을 사용하여 타입의 인스턴스가 아닌 타입의 상수를 정의할 수 있다. 타입 프로퍼티는 인스턴스 프로퍼티와 구별하기 쉽기 때문에, 일반적인 전역 변수보다 선호한다.

선호하는 예제:

enum Math {
  static let e = 2.718281828459045235360287
  static let root2 = 1.41421356237309504880168872
}

let hypotenuse = side * Math.root2

Note: case가 없는 열거형의 장점은 실수로 인스턴스 화가 되지 않고, 순수 네임스페이스로 사용할 수 있다.

선호하지 않은 예제:

let e = 2.718281828459045235360287  // pollutes global namespacelet root2 = 1.41421356237309504880168872

let hypotenuse = side * root2 // what is root2?

****

static 메서드와 변수 타입 프로퍼티 (static Methods and Variable Type Properties)

Static 메서드와 타입 프로퍼티는 전역 함수와 전역 변수와 유사하게 동작하며, sparingly하게 사용되야된다.

기능이 특정 타입으로 범위가 지정되거나 Objective-C와 상호 운용이 필요한 경우에 유용하다.

****

옵셔널(Optionals)

변수와 메서드 리턴 타입을 nil로 접근할 수 있도록 ?으로 옵셔널 선언을 한다.

옵셔널 값에 접근할 때 값이 한 번만 접근되거나 체인에 옵셔널이 많은 경우 옵셔널 체인(optional chaining)을 사용하십시오.

textContainer?.textLabel?.setNeedsDisplay()

한 번 언랩(unwrap)을하고 여러 작업을 수행하는 것이 더 편리할 때 옵셔널 바인딩을 사용하십시오.

if let textContainer = textContainer {
  // do many things with textContainer
}

옵셔널 변수나 프로퍼티를 네이밍 지을 때 옵셔널이라고 해서optionalString, maybeView이렇게 이름을 짓는 것을 피해라!

옵셔널 바인딩의 경우에도 언래핑이 됐다고 해서unwrappedView, actualLabel

이런 식으로 이름을 짓지 말고 가능한 본래의 이름을 유지해라

선호하는 예제:

var subview: UIView?
var volume: Double?

// later on...if let subview = subview, let volume = volume {
  // do something with unwrapped subview and volume
}

// another exampleUIView.animate(withDuration: 2.0) { [weak self] in
  guard let self = self else { return }
  self.alpha = 1.0
}

선호하지 않은 예제:

var optionalSubview: UIView?
var volume: Double?

if let unwrappedSubview = optionalSubview {
  if let realVolume = volume {
    // do something with unwrappedSubview and realVolume
  }
}

// another example
UIView.animate(withDuration: 2.0) { [weak self] in
  guard let strongSelf = self else { return }
  strongSelf.alpha = 1.0
}

****

느린 초기화(Lazy Initialization)

lazy 변수는 사용되기 전까지는 연산이 되지 않는다.

개체 수명(lifetime)에 대한 보다 세밀한 제어를 위해 lazy initialization 사용을 고려해볼 수 있다.

특히 View를 lazy 하게 로드하는 UIViewController의 경우 더욱 그러하다.

아래의 예제와 같이 lazy에 클로저를 이용하거나 private factory 메서드를 이용할 수가 있다.

// 방법 1
lazy var location: CLLocationManager = {
    let manager = CLLocationManager()
    manager.desiredAccuracy = kCLLocationAccuracyBest
  manager.delegate = self
  manager.requestAlwaysAuthorization()
  return manager
}()

// 방법 2
lazy var locationManager = makeLocationManager()

private func makeLocationManager() -> CLLocationManager {
  let manager = CLLocationManager()
  manager.desiredAccuracy = kCLLocationAccuracyBest
  manager.delegate = self
  manager.requestAlwaysAuthorization()
  return manager
}

****

타입 추론(Type Inference)

간단한 코드나 컴파일러가 단일 인스턴스의 변수나 상수에 대해 추론하는 것을 선호한다.

타입 추론은 for small에 적합하고, 비어있지 않은(non-empty) 배열이나 딕셔너리에 적합하다.

CGFloat, Int16같이 필요한 경우에 타입을 지정한다.

선호하는 예제:

let message = "Click the button"
let currentBounds = computeViewBounds()
let names = ["Mic", "Sam", "Christine"]
let maximumWidth: CGFloat = 106.5

선호하지 않은 예제:

let message: String = "Click the button"
let currentBounds: CGRect = computeViewBounds()
var names = [String]()

****

빈 배열과 빈 딕셔너리에 대한 타입 주석(Type Annotation for Empty Arrays and Dictionaries)

빈 배열과 빈 딕셔너리의 경우 Type Annotation을 사용한다.

선호하는 예제:

var names: [String] = []
var lookup: [String: Int] = [:]

선호하지 않은 예제:

var names = [String]()
var lookup = [String: Int]()

문법적 설탕(Syntactic Sugar)

제네릭으로 선언한 구문보다 아래와 타입 선언의 짧은 버전을 선호한다.

제네릭 선언이 더 가독성이 좋지 않나...? 고민해보자..아..Optional 이거 빼고..

선호하는 예제:

var deviceModels: [String]
var emplyees: [Int: String]
var faxNumber: Int?

선호하지 않은 예제:

var deviceModels: Array<String>
var emplyees: Dictionary<Int, String>
var faxNumber: Optional<Int>

****

함수 vs 메서드(Functions vs Methods)


Class, Struct, Enum에 속해있지 않은 Free Funciton은 드물게 사용되야된다.

가능한 찾기도 쉽고 읽기도 쉬운 메서드를 사용해야 된다.

function은 특정 타입이나 인스턴스에 관련이 없을 때 사용하는 것이 적합하다.

선호하는 예제:

let sorted = items.mergeSorted()  // easily discoverable
rocket.launch()  // acts on the model

선호하지 않은 예제:

let sorted = mergeSort(items)  // hard to discover
launch(&rocket)

free function이 사용되야될 경우:

let tuples = zip(a, b)  // feels natural as a free function (symmetry)let value = max(x, y, z)  // another free function that feels natural

****

메모리 관리(Memory Management)


코드는 순환 참조(reference cycles)를 생성하면 안 된다.

weak와 unowned를 사용하여 강한 순환 참조를 방지해야 된다.

그렇지 않으면, 값 타입(struct, enum)을 이용해서 순환 참조를 방지해야 된다!

값 타입은 race condition문제 해결에 도움이 되기도 하지..

하지만 나는 struct 보다는 클래스가 좋다..

객체의 수명 연장(Extending object lifetime)

[weak self]나 guard let self = self else { return }구문을 사용하여 객체 수명 연장을 한다.

[weak self]는 self가 클로저에서 살아 있는지 불확실할 때 [unowned self]보다 선호된다.

명시적으로 수명 연장은 옵셔널 체이닝보다 선호한다.

아래의 예제를 보면 이해가 될 것이다.

선호하는 예제:

resource.request().onComplete { [weak self] response in
    guard let self = self else { 
        return
    }
    let model = self.updateModel(response)
    self.updateUI(model)
}

선호하지 않은 예제:

그렇다 unowned는 런타임시에 self가 해제되면 크래쉬가 난다..그러니 차라리 weak를 사용하는게 낫다.

// response를 받기 전에 self가 해제되면 Crash가 일어날 수 있다.// might crash if self is released before response returns
resource.request().onComplete { [unowned self] response in
  let model = self.updateModel(response)
  self.updateUI(model)
}

선호하지 않은 예제:

이건 뭐..self가 해제 되었을 때 아무런 반응을 안하기 때문에..

// 모델 업데이트와 UI 업데이트 사이에 할당 취소 가능
// deallocate could happen between updating the model and updating UI
resource.request().onComplete { [weak self] response in
  let model = self?.updateModel(response)
  self?.updateUI(model)
}

****

접근 제어(Access Control)


private와 fileprivate는 적절하게 사용하면, 명확성을 추가하고 캡슐화를 촉진한다.

가능하면 fileprivate보다 private를 더 선호한다.

컴파일러에서 fileprivate를 요구하는 경우에만 사용한다.

open, public, internal같이 완전한 접근 제어 규격이 필요한 경우에만 명시적으로 사용하십시오.

static지정자나 @IBAction, @IBOutlet, @discardableResult같은 속성들은 유일하게 접근제어자 전에 올 수 있고,

static private let message = "Hi"
@IBOutlet private weak var testView: UIView!

그 이외는 맨 앞에 사용되어야 한다.

private dynamic lazy var fluxCapacitor = FluxCapacitor()

선호하는 예제:

private let message = "Great Scott!"

class TimeMachine {  
  private dynamic lazy var fluxCapacitor = FluxCapacitor()
}

선호하지 않은 예제:

fileprivate let message = "Great Scott!"

class TimeMachine {  
  lazy dynamic private var fluxCapacitor = FluxCapacitor()
}

****

Control Flow


while-condition-increment스타일 보다 for반복문의 for-in스타일을 선호한다.

선호하는 예제:

for _ in 0..<3 {
  print("Hello three times")
}

for (index, person) in attendeeList.enumerated() {
  print("\(person) is at position #\(index)")
}

for index in stride(from: 0, to: items.count, by: 2) {
  print(index)
}

for index in (0...3).reversed() {
  print(index)
}

선호하지 않은 예제:

var i = 0
while i < 3 {
  print("Hello three times")
  i += 1
}

var i = 0
while i < attendeeList.count {
  let person = attendeeList[i]
  print("\(person) is at position #\(i)")
  i += 1
}

****

삼항 연산자(Ternary Operator)

삼항 연산자(? : )는 명확성 또는 코드의 깔끔성을 높일 때 사용해야 한다.

하나의 조건을 계산하는 것에 보통 사용되고,

복수의 조건을 계산하는 것은 일반적으로 if 문으로 이해하거나 인스턴스 변수로 리팩터링 한다.

일반적으로, 삼항 연산자의 최선의 사용은 변수를 할당하고 사용할 값을 결정하는 것이다.

선호하는 예제:

let value = 5
result = value != 0 ? x : y

let isHorizontal = true
result = isHorizontal ? x : y

선호하지 않은 예제:

이렇게 쓰는 사람 만나면...하...

result = a > b ? x = c > d ? c : d : y

****

Golden Path


조건문으로 코딩을 할 때, 코드의 왼쪽 여백은 "golden"이나 "happy" path여야 한다.

즉, if문을 중첩하지 않는다.

여러 개의 return문은 괜찮다. guard문은 이를 위해 만들어졌다.

예외 처리는 guard let으로 하는게 낫다.

코드 가독성으로 보자면 중첩을 하면 할 수록 가독성과 유지 보수는 쉽지 않다고 경험으로 느낄 수 있을 것이다.

선호하는 예제:

func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {

  guard let context = context else {
    throw FFTError.noContext
  }
  guard let inputData = inputData else {
    throw FFTError.noInputData
  }

  // use context and input to compute the frequenciesreturn frequencies
}

선호하지 않은 예제:

func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {

  if let context = context {
    if let inputData = inputData {
      // use context and input to compute the frequenciesreturn frequencies
    } else {
      throw FFTError.noInputData
    }
  } else {
    throw FFTError.noContext
  }
}

여러개의 옵셔널이 guard 나 if let에 의해 언래핑 됐을 때, 가능한 혼합 해서 중첩을 최소화해야 한다.

선호하는 예제:

guard 
  let number1 = number1,
  let number2 = number2,
  let number3 = number3
  else {
    fatalError("impossible")
}
// do something with numbers

선호하지 않은 예제:

if let number1 = number1 {
  if let number2 = number2 {
    if let number3 = number3 {
      // do something with numbers
    } else {
      fatalError("impossible")
    }
  } else {
    fatalError("impossible")
  }
} else {
  fatalError("impossible")
}

****

Guard의 약점(Failing Guards)

guard문법은 어떤 식으로든 종료할 때 필요하다.

guard문 안에는 큰 코드 블럭은 피하고, 일반적으로 return, throw, break, continue, fatalError()처럼 간단히 한 줄의 문장이 되어야 한다.

여러 종료 지점에 대해 cleanup code가 필요한 경우 cleanup code 중복을 방지하기 위해 defer블록을 사용하는 것을 고려해야 된다.

func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {

  guard let context = context else {
        print("FFTError")
    throw FFTError.noContext
  }
  guard let inputData = inputData else {
        print("FFTError")
    throw FFTError.noInputData
  }

  return frequencies
}

위의 소스를 보면 예를 들어 print("FFTError") 프린트를 찍는로직이 있다고 했을 때,

guard문이 종료되는 시점에 전부 넣어줘야 된다.

func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {

    defer {
        print("FFTError")
    }
  guard let context = context else {
    throw FFTError.noContext
  }
  guard let inputData = inputData else {
    throw FFTError.noInputData
  }

  return frequencies
}

cleanup code의 중복을 defer를 통해서 한 번에 처리된 것을 볼 수 있습니다.

이런 상황에서 defer를 사용해서 중복을 제거할 수 있다는 뜻입니다.

세미콜론(Semicolons)


Swift는 코드에서 각 문장 뒤에 세미콜론이 필요하지 않다. 한 줄에 여러 개의 문장을 결합할 때에만 필요하다.

한 줄에 세미콜론으로 구분하여 여러 문장들을 사용하지 말라.

이건 스위프트만 사용한다면 괜찮은데..

스위프트와 옵젝을 같이 사용하는 프로젝트에서는...정말 너무 힘들다..ㅜㅜㅜ

선호하는 예제:

let swift = "not a scripting language"

선호하지 않은 예제:

let swift = "not a scripting language";

****

괄호(Parentheses)


조건문 주변의 괄호는 필요하지 않으므로 생략해야 한다.

선호하는 예제:

if name == "Hello" {
  print("World")
}

선호하지 않은 예제:

if (name == "Hello") {
  print("World")
}

더 큰 표현에서 선택적 괄호는 때때로 코드를 더 명확하게 읽을 수 있다.

선호하는 예제:

let playerMark = (player == current ? "X" : "O")

****

Multi-line String Literals


긴 문자열을 문자열로 작성할 때 multi-line string literal syntax(""") 구문을 사용하는 것이 좋다.

선호하는 예제:

let message = """
  You cannot charge the flux \
  capacitor with a 9V battery.
  You must use a super-charger \
  which costs 10 credits. You currently \
  have \(credits) credits available.
  """

선호하지 않은 예제:

let message = """You cannot charge the flux \
  capacitor with a 9V battery.
  You must use a super-charger \
  which costs 10 credits. You currently \
  have \(credits) credits available.
  """

선호하지 않은 예제:

+로 하지 말자..

let message = "You cannot charge the flux " +
  "capacitor with a 9V battery.\n" +
  "You must use a super-charger " +
  "which costs 10 credits. You currently " +
  "have \(credits) credits available."

아래의 경우와 같이 표현할 경우 우리는 검색에서 "네트워크 에러" 라고 검색을 해서 한 번에 찾아야 하는데 "network" 를 찾고 찾아 들어가야한다. 저러지 말자.

static let network = "네트워크"
static let error = "에러"
static let networkError= "네트워크 에러"

let resultMessage = "\(network) + \(error)" //네트워크 에러지만 쓰지 말자...

let resultMessage01 = "\(networkError)"  //이렇게 한번에 쓰자...

조직과 번들 식별자(Organization and Bundle Identifier)


Xcode 프로젝트와 관련된 경우,

Organization은 Ray Wenderlich로 해야 되고,

Bundle Identifier는 com.raywenderlich.TutorialName이 되어야 한다.

여기서 TutorialName은 프로젝트 이름을 나타낸다.

예) Domain.Organization.ProjectName

https://k.kakaocdn.net/dn/PjmVp/btqw7c96vWD/zw4rFSxFGT45kUcba7mDxk/img.png

댓글