'Enum'에 해당하는 글 2건

제목: Swift: UserDefaults Protocol


스위프트3은 언어뿐만아니라 우리 코드베이스까지도 쓰나미같은 변경이 생겼는데, 이 글을 읽는 몇몇은 아직도 마이그레이션과 투쟁하고 있을지 모르겠다. 그러나 이런 모든 변경에도 stringly typed의 Foundation으로된 몇몇 API들이 남을 것인데, 이는 꽤 괜찮아 보이지만... 그렇지 않을 수도 있다.

이것은 일종의 애증관계인데, API에서 문자열의 유연성은 '애'이나, 그들이 가져오는 상속적인 이유때문에 이것을 사용해야 하는 것을 '증'한다. 신경쓰지 않으면 위험하게 작업하고 있는 것과 동일하다.

Foundation 프레임워크를 만드는 사람들은, 우리가 의도한대로 정확하게 미리 정의할 수 없게 해놓아서 우리는 stringly typed API를 쓸 수 있게 되었다. 그래서 그들의 모든 지혜로움, 능력, 지식으로 개발자로서 무한한 가능성으로 만들 수 있게 하려고 몇몇 API에서는 문자열을 사용하도록 해놓았다. 이것은 어둠의 비밀의 마법이다. (So in all their wisdom, power and knowledge, they decided to use strings in some of the APIs because of the unlimited possibilities it creates for us as developers. It’s either that or some type of dark arcane magic.)
(역자: stringly 타입의 API로 유연하게 사용할 수 있게 해놓은 장점을 말하는 중입니다.)

UserDefaults
오늘의 주제는 내가 iOS 개발을 하면서 배울때 처음으로 친숙해진 API 중 하나이다. 이것이 익숙하지 않는 사람들을 위해 설명하자면 UserDefaults는 한 이미지나 어플리케이션 세팅같은 작은 정보를 저장하지위한 간단한 영속 데이터 저장소이다. 어떤 사람들은 이것을 "다이어트한 코어데이터"라고 생각하기도하지만, 사람들이 그것의 대체물로 만드려 아무리 노력해도 이것은 견고하지 않다.

Stringly Typed API
UserDefaults.standard.set(true, forKey: “isUserLoggedIn”)
UserDefaults.standard.bool(forKey: "isUserLoggedIn")
일반적으로 앱에서 UserDefaults는 앱의 어디에서라도 값을 간단하게 영속적으로 저장(set)하고, 검색(retrieve)하며, 덮어쓰거나(override), 제거하는(remove) 할 수 있다. 그러나 조심하지 않으면 균일성이나 문맥이 없는채로 해볼 순 있겠지만 오타를 칠 가능성이 높아질 것이다. 이 포스팅에서는 UserDefaults의 일반적인 특징을 변형하여 커스터마이징 할 것이다.

상수를 이용하기
let key = "isUserLoggedIn"

UserDefaults.standard.set(true, forKey: key)

UserDefaults.standard.bool(forKey: key)
이런 이상한 트릭을 따라하면 일시적으로는 더 나은 코드를 작성할 수 있을 것이다. 문자열을 한번 이상 쓴다면 상수로 바꿔서 쓰는 규칙을 적용시켜보자. 아마 나에게 고마워 할지도 모르겠다.

그룹 상수
struct Constants {
    let isUserLoggedIn = "isUserLoggedIn"
}
...
UserDefaults.standard
   .set(true, forKey: Constants().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants().isUserLoggedIn)
균일성을 유지하기에 더 도움이 되는 방법은, 한곳에 중요한 디폴트 상수를 모아놓는 것이다. 그래서 여기에 디폴트를 저장하고 참조할 수 있는 Constants 구조체를 만들었다.

또다른 좋은 팁에는, 디폴트로 작업할 때 특히 프로퍼티 이름에 그 값을 반영해놓는 것이다. 이렇게하면 코드를 단순화 시켜주고 전반적인 속성을 더 균일화시켜줄 것이다. 프로퍼티 이름을 복사하고 문자열 안에 붙여넣으면 타이핑을 줄일 수 있을 것이다.
let isUserLoggedIn = "isUserLoggedIn"

문맥 추가하기
struct Constants {
    struct Account

        let isUserLoggedIn = "isUserLoggedIn"
    }
}
...

UserDefaults.standard
  .set(true, forKey: Constants.Account().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account().isUserLoggedIn)
단지 Constants 구조체를 가지는 것만으로도 괜찮겠지만, 코드를 작성할 때 문맥을 제공해야함을 잊어서는 안된다. 여러분 자신을 포함한)함께 작업할 누군가에게 더 읽기 좋은 코드를 만들어야함을 목표로 하는것이 좋다.
Constants().token // Huh?
token의 의미가 무엇일까? 문맥에서 네임스페이스가 없기때문에, 이 코드베이스에 익숙하지 않은 누군가(혹은 미래의 이 코드를 관리하는 사람)가 token이 무엇인지 알아내려할때 고생할 것이다.
Constants.Authentication().token // better

초기화 피하기
struct Constants {
    struct Account
        let isUserLoggedIn = "isUserLoggedIn"
    }

    private init() { }
}
절때로 우리가 의도하지도 않았고 우리 Constants 구조체를 초기화시키고 싶지 않기 때문에, 생성자는 private로 선언되어야한다. 이거은 좀 더 예방적인 단계이지만 계속 추진하고 있는 방법이다. 최소한 적어도 static만 원할때도 실수로 인스턴스 프로퍼티를 선언하는 것을 막아줄 것이다. 그러나 static에 관해 말하자면... 다음을 보자.

static 변수들
struct Constants {
    struct Account
        static let isUserLoggedIn = "isUserLoggedIn"
    }
    ...
}
...

UserDefaults.standard
  .set(true, forKey: Constants.Account.isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account.isUserLoggedIn)
키에 접근할때마다 주의해야하는 것이 있는데, 접근할때마다 이것이 속한 구조체를 초기화해야할 수 있다. 그러지말고 static 선언을 사용하면 한번만 초기화한다.

구조체를 저장 타입으로 정했기 때문에 class대신 static을 사용한다. 스위프트 컴파일러 법에 따르면 구조체는 class 프로퍼티 정의를 사용할 수 없다고 한다. 또한 class 프로퍼티에 static 선언을 사용하면 그 프로퍼티는 final class로 선언한 것과 같다.
final class name: String

static name: String
// final class == static

열거형 케이스로 더 적게 타이핑하기
enum Constants {
    enum Account : String {
        case isUserLoggedIn
    }
    ...
}
...

UserDefaults.standard
    .set(true, forKey: Constants.Account.isUserLoggedIn.rawValue)
UserDefaults.standard
    .bool(forKey: Constants.Account.isUserLoggedIn.rawValue)
이 포스트의 초반부에서 말했듯, 균일성을 위해 프로퍼티는 그 값을 반영해야한다고 했었다. 여기에 static let 대신 enum case를 써서 그 과정을 자동화하여 한걸을 더 나가볼 것이다.

여러분도 인지했듯, 우리는 String을 따르는 Account열거형을 만들었는데, 이것은 RawRepresentable 프로토콜을 따른다. 이렇게 한 이유는, case를 위한 rawValue를 제공하지 않으려면 디폴트로 케이스가 반영될 것이기 때문에 이 작업을 한다. 우리가 해야할 타이핑이나 복사/붙여넣기를 줄이면 더 편해질 것이다.
// Constants.Account.isUserLoggedIn.rawValue == "isUserLoggedIn"

위에는 지금까지 UserDefaults로 꽤 괜찮은 것들을 달성했지만, 멋진것을 했다고 하기엔 좀 부족해 보인다. 가장 큰 문제는 문자열을 입고 있을지라도 여전히 stringly typed API로 작업하고 있어서 여전히 우리 프로젝트에 문제가 생길 수 있다는 점이다.

우리는 주어진 것으로만 작업할 수 있다는 마음을 가진다. 스위프트는 아주 많이 멋진 언어이고, 우리가 배워왔던, 그리고 Objective-C를 작성해가면서 알고 있는 많은 것들을 도전해볼 수 있다. 이 API에 문법 슈거를 만들어보자.

API 목표
UserDefaults.standard.set(true, forKey: .isUserLoggedIn)
// APIGoals
남은 이야기에서는 일반적인 populus 대신, UserDefaults와 소통할때 더 괜찮게 작업할 수 있는 API를 우리 필요에 맞게 만들어보려 할 것이다. and what better way than to do so than making extensions with protocols.

BoolUserDefaultable
protocol BoolUserDefaultable {
    associatedType BoolDefaultKey : RawRepresentable
}
불리언 UserDefaults를 위한 프로토콜을 만들면서 시작해보자. 변수나 함수가 없는 간단한 프로토콜이다. 그러나 RawRepresentable을 따르는 BoolDefaultKey라는 associatedType을 지원하는데 왜 이렇게 했는지는 바로 다음에 이해할 수 있을 것이다.

익스텐션
extension BoolUserDefaultable
    where BoolDefaultKey.RawValue == String { ... }
만약 Crusty's Laws 프로토콜을 따르려는 계획이라면 프로토콜 익스텐션을 선언할 수 있다. 그러나 associatedTyperawValueString 타입이라는 익스텐션에만 제약하는 where절을 적용시켰다.
모든 프로토콜로, 동등하고 해당되는 프로토콜 익스텐션이 있다 - Crusty's Third Law
With every protocol, there is an equal and corresponding protocol extension

UserDefaults 세터
// BoolUserDefaultable extension
static func set(_ value: Bool, forKey key: BoolDefaultKey) {
    let key = key.rawValue
    UserDefaults.standard.set(value, forKey: key)
}

static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = key.rawValue
    return UserDefaults.standard.bool(forKey: key)
}
그렇다. 이것은 표준 UserDefaults를 감싼 간단한 API이다. 이렇게 하는 이유는 Key-Path로된 문자열을 보내는것 보다 간략한 enum case를 보내는게 가독성면에서 더 좋기 때문이다.
UserDefaults.set(false,
    forKey: Aint.Nobody.Got.Time.For.this.rawValue)

프로토콜 따르기
extension UserDefaults : BoolUserDefaultable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn
    }
}
우리는 BoolDefaultable을 따르게 하기 위해 UserDefaults를 익스텐션하고 RawRepresentable (String)을 따르는 BoolDefaultKey라는 연관타입을 지원했다.
// Setter

UserDefaults.set(true, forKey: .isUserLoggedIn)

// Getter

UserDefaults.bool(forKey: .isUserLoggedIn)
다시 말하자면, 작업하는 표준에 우리것을 정의하는 것 대신 우리가 지원한 API로 도전하는 중이다. UserDefaults를 익스텐션하면서 우리 API와함께 문맥을 잃어버리기 때문이다. 만약 .isUserLoggedIn 말고 다른 키 였다면 무엇과 관련되었는지 이해할 수 있었을까?
UserDefaults.set(true, forKey: .isAccepted)
// Huh? isAccepted for what?
이 키는 매우 모호해서, 모든 범주의 어떤것이든 될 수 있다. 이것처럼 보이지 않더라도 문맥을 제공하는 것은 항상 유익할 것이다.
필요하지만 가지지 못한것 보다는, 필요없더라도 가지고 있는 편이 낫다.
어렵게 생각하지 말자. 문맥을 추가하는 것은 쉬운 일이다. 간단하게 키를 위한 네임스페이스를 만든다. 이 경우, isUserLoggedIn 키가 있는 곳인 Account 네임스페이스를 만들었다.
struct Account : BoolUserDefaultable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn

    }
    ...
}
...
Account.set(true, forKey: .isUserLoggedIn)

충돌
let account = Account.BoolDefaultKey.isUserLoggedIn.rawValue
let default = UserDefaults.BoolDefaultKey.isUserLoggedIn.rawValue
// account == default
// "isUserLoggedIn" == "isUserLoggedIn"
같은 프로토콜을 따르고 같은 키 케이스를 제공하는 서로다른 두 타입을 가지는 것이 가능하다. 이걸 출시하기 전까지 해결하지 못하면 분명 이것이 새벽에 우리를 깨우는 버그가 될것이다. 다른 값을 바꾸는 키를 가지는 위험을 안고 갈 수 없다. 그러니 우리 키를 네임스페이스한 것으로 만들자.

네임스페이스로 만들기
protocol KeyNamespaceable { }
물론 우리는 스위프트 개발자니까 프로토콜을 만든다. 프로토콜은 우리가 직면한 문제를 풀때 제일 먼저 하게되는 시도일 것이다. 만약 프로토콜이 초콜릿 소스라면, 심지어 스테이크에까지도 어디든지 올려놓을 수 있다(역자: 왜하필 초콜릿 소스에 비유를 했는지.. 다목적 소스라면 역시 굴소스 아닌가요?). 이것이 우리가 프로토콜을 만들어가며 개발하는게 얼마나 좋은지 보여준다.
extension KeyNamespaceable {
    static func namespace<T>(_ key: T) -> String

    where T: RawRepresentable {
          return "\(Self.self).\(key.rawValue)"
    }
}
이 간단한 함수는 두 오젝트를 합친 문자열 보간법을 쓰고, 그 사이에 마침표로 구분했다. 클래스의 이름과 그 키의 rawValue이다. 이 함수는 RawRepresentable을 따르면 key 인자로 받을 수 있게 제네릭을 인자로 받도록 해놓았다.
// BoolUserDefaultable extension
static func set(_ value: Bool, forKey key: BoolDefaultKey) {
    let key = namespace(key)

    UserDefaults.standard.set(value, forKey: key)
}

static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = namespace(key)

    return UserDefaults.standard.bool(forKey: key)
}
...

let account = namespace(Account.BoolDefaultKey.isUserLoggedIn)

let default = namespace(UserDefaults.BoolDefaultKey.isUserLoggedIn)


// account != default

// "Account.isUserLoggedIn" != "UserDefaults.isUserLoggedIn"

문맥
우리가 이 프로토콜을 만들었기 때문에, UserDefaults API 사용으로부터 해방된 느낌을 받고, 아마 프로토콜의 힘에 취했을 것이다. 이렇게하여 우리은 키를 우리가 원하는 곳으로 옮겨 문맥을 만듦으로서 코드를 읽을때 이해할 수 있게 되었다.
Account.set(true, forKey: .isUserLoggedIn)
그러나 API가 완전히 이해되지 않게 문맥을 잃어버리기도 했다. 처음 보면 이 코드가, 불리언을 영속적으로 저장시키는지 아니면 UserDefaults에 넣는지에대한 아무런 정보도 주지 않는다. 따라서 모든 사이클을 보여주기위해 UserDefaults를 익스텐션하여 우리의 디폴트 타입을 그 안에 넣을 것이다.
extension UserDefaults {
    struct Account : BoolUserDefaultable { ... }
}
...

UserDefaults.Account.set(true, forKey: .isUserLoggedIn)
UserDefaults.Account.bool(forKey: .isUserLoggedIn)


NatashaTheRobot에게 감사의 말을 전한다. 9월에 try! Swift NYC에서 발표할 기회를 얻었었다. 내 발표가 녹화되어 Realm에서 이것을 남겨두었고 Speaker Deck에 슬라이드 자료가 있으니 확인해보자. 발표를 한 이례로 몇가지 배운점을 이 글에 반영했으며, 샘플코드는 Gist나 Playground
에 있다.


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
제목: That One Optional Property

때론 새로운 것을 위해 여러분의 뷰컨트롤러를 수정해야할 필요가 있다. 한가지는 다른것을 야기하는데, 뷰컨트롤러에 옵셔널 변수를 추가하는 자신을 발견할 것이다. 이것은 몇몇 케이스에서 설정될 것이고 몇몇은 아닐 것이다.

내 생각엔 더 자주 일어나고 이것은 결점이있는 방법이다. 여기에는 몇가지 이유가 있다. 첫째로 어떨때만 쓰이는 옵셔널 프로퍼티를가진 클래스는 정체성이 약한 느낌을 가진다. 바꿔 말해보면, 옵셔널 프로퍼티를 추가하면 그 타입의 근본적인 의미가 흐려진다. 첫번째로 이 옵셔널 프로퍼티는 어떤 시멘틱 의미를 고려하지 않는다. 이 타입이 nil이라면 이 오브젝트의 상태가 무엇이라 할 수 있을까? 여러분의 코드를 가볍게 읽고있는 사람들은 어떤 경우에 프로퍼티가 nil이될지, 혹은 그 오브젝트에서 가지는 분기가 무엇인지 말할 수 없을 것이다. 세번째로 코드는 가변으로 계속 될것이다. 한 옵셔널 프로퍼티는 누가지 선택적 프로퍼티로 될것이고, 이것은 세개로 되고, 당신이 알기 전에 미끄러운 경사의 아래에 가있을 것이다. 이 값이 반드시 존재하는지 이 값이 nil인지 표현하고 싶으면 간단한 옵셔널로는 그렇게 할 수 없다.

내가본 모든 코드베이스에서는, 여러분의 뷰컨트롤러에 왜 옵셔널 프로퍼티가 필요한지에대한 두가지 주된 이유를 발견했다. 나는 이 두가지 다를 탐험할 것이고 각 문제를 해결하기위한 더 나은 패턴을 제안할 것이다.

문제의 옵셔널 프로퍼티를 가지는것에대한 첫번째 이유는 이 뷰컨트롤러가 쓰일때마다 오브젝트나 몇 데이터를 반드시 가질 필요가 없을때이다.

최근에 만난 이것에대한 예시는 뷰컨트롤러가 어떤 경우 푸시 노티피케이션으로부터 표시되는 상황이다. 이때 노티피케이션에서 나온 특정 메시지를 보여줘야한다. 이 문제를 해결하기위한 가장 간단한 방법은 옵셔널 문자열 프로퍼티를 추가하는 것이었다.
class LocationViewController: UIViewController {
     //...
     var notificationMessage: String?
     //...
}
뷰컨트롤러에 다른 코드는 어떤 뷰에 할당할지, 어떻게 배치할지등을 결정하기위해 메시지가 있다고 전환했다. 프로퍼티의 선택성은 그냥 문자열의 존재보다 더 표현한다. It had implications in the rest of the view controller and the rest of the view layer.


여기서 더 중요한 점은 문자열이 더이상 거기에 있을지 아니면 없을지 표현하는게 아니게 된다. 이제 뷰컨트롤러 안에있는 표현 스타일이나 모드를 표현한다. 이것이 푸시 노티피케이션의 문맥에서 만들어진 것인가 혹은 일반 브라우징을 통해 만들어진 것인가? 답은 가까스로 관련된 프로퍼티에 있다.

이 문제를 해결하기위해, 이 모드를 모두 명시적으로 만들어야한다. 이 뷰컨트롤러에서 이것은 일급시민이면, 뷰컨트롤러의 그 영향은 더 분명해질것이다.
class LocationViewController: UIViewController {
     //...
     enum Mode {
          case fromNotification(message: String)
          case normal
     }

     var mode: Mode
     //...
}
Sandi Metz가 말한것처럼, 한 특수화는 절때 없다(there's never one specialization). 옵셔널 프로퍼티를 쓰면 그 코드는 이 프로퍼티의 nil 상태에대한 의미를 가지거나 고유의 의미를 가진다고 할 수 없다. 열거형을 사용하면 코드에서 구체화되고 형식화된다.

새로운 열겨형이 Optional 타입 정의의 열거형 형태와 아주 비슷하다는 점을 인지하자.
enum Optional<Wrapped> {
     case some(Wrapped)
     case none
}
그러나 여기에는 몇가지 분명히 다른 유용한점이 존재한다.

첫째로 시멘틱이다. somenone은 추상적이다. normalfromNotification은 그것과 관련된 의미를 가진다. 여러분의 코드를 읽는 사람들은 여러분에게 감사할 것이다.

두전째로 확장성이다. 뷰 컨트롤러에 다른 모드가 추가되면 그것을 완전히 설명하는 더나은 의미를 가진다. 만약 새로운 모드가 두 모드와 절때 겹치지 않으면, 필요한 연관된 데이터가 어떻든 새로운 열거형 케이스를 추가할 수 있다. 새로운 모드가 현재 모드들과 겹친다면, 새로운 열거형이되어 새로운 프로퍼티를 함께할 수 있다. 오브젝트의 상태는 읽기에 더욱 설명가능한 것이 된다. 어떤 선택이든 새 모드를 위해 또다른 옵셔널을 추가하는 것보다는 낫다.

세번째도 확장성이다. LaunchViewController.Mode가 일급 타입이기 때문에 우리는 함수와 계산된 프로퍼티를 추가할 수 있다. 예를들어 노티피케이션 메시지를 잡아두는 레이블의 높이는 아마 그 메시지의 존재에 달렸을 것이다. 따라서 코드를 열거형으로 옮길 수 있다.
extension Mode {
     var notificationLabelHeight: CGFloat {
          switch self {
          case .normal: return 0
          case .fromNotification(let message): return message.size().height
          }
     }
}
더 풍요로운 타입으로 데이터를 옮기는 것은 적은 코드를 투자하여 이 모든 앞단에서의 이득을 제공한다.

옵셔널 프로퍼티를 사용할지도 모르는 두번째 이유는 첫번째 이유의 부분집합이다. 어떤 경우, 옵셔널 프로퍼티는 뷰컨트롤러의 다른 모드를 표현하지 않는데, 코드에서 임시적인 특징을 표현한다. 아직 값을 가지고 있지 않기 때문에 값으로 프로퍼티를 초기화할 수 없다. 이것의 일반적인 예시는 네트워크로부터 뭔가 패치를 하거나, 시스템으로부터 뭔가 긴 검색시간이 걸려 비동기가 필요해서 기다려야 할때이다.
class UserViewController: UIViewController {
     //...

     // will be loaded asynchronously
     var user: User?
     //...
}
이런 종류의 문제는 어떨때 데이터가 배열형식으로 들어오면 준비될 수 있다. 빈 배열로 존재하지 않는 상태를 표현할 수 있기 때문에, 옵셔널을 사용하지 않더라도 이 문제는 여전히 숨어있다. 테이블 뷰 컨트롤러에 빈 상태를 추가하는 힘든 상황의 횟수가 바로 이런 문제의 정도를 나타낸다.

이 문제는 첫번째 문제의 부분집합이기 때문에, 열거형으로 같은 방법으로 해결할 수 있다.
enum LoadingState<Wrapped> {
     case initialcase loading
     case loaded(Wrapped)
     case error(Error)
}
이 솔루션이 동작하는동안, 비동기 상태를 관리하기위한 더 나은 추상화가 있다. 아직 존재하진 않지만 미래의 어느 시점엔 있을 한 값은 Promise에의해 잘 표현된다. 프로미스는 여러분의 유스케이스에따라 여러 이점을 가진다.

먼저, 프로미스는 가변을 허락하지만, 오직 한번만 그리고 오직 결정되지 않을때까지만이다. 프로미스가 한번 변경되면, 다시는 변경할 수 없다. (여러번에걸처 변경해야한다면, Signal이나 Observable이 여러분이 찾던 것일 것이다.) 이 말은 let과같은 시멘틱을 가지지만 여전히 비동기 데이터를 관리한다.

다음으로, 프로미스는 값이 들어왔을때, 없어질 블럭을 추가하는 기능을 가진다. 만약 프로퍼티가 간단한 옵셔널 프로퍼티를 남긴다면, 추가된 블럭들은 didSet 프로퍼티 옵저버에서 코드와 동일하다. 그러나 프로미스 블럭은 하나 이상 추가할 수 있고, 클래스 어디에서든 추가될 수 있기 때문에 어욱 강력하다. 게다가 프로미스가 이미 값으로 채워져 있을때 추가한다면 그들은 즉시 실행될것이다.

마짐가으로 필요에따라 프로미스는 에러도 처리한다. 특정 종류의 비동기 데이터에대해, 이것이 중요하며, 이것으로부터 자유로울 것이다.

Promise 라이브러리를 사용하고 있다면 빈 생성자로 미결정된 Promise를 생성할 수 있고, fulfill 메소드로 언제든지 채울 수 있다.

옵셔널 프로퍼티를 사용할 때는, 가끔 예전에 보이지 않던 것들이 드러낸다. 뷰컨트롤러에 옵셔널 파라미터를 추가하는 자신을 발견할 때 스스로에게 물어보자. 이 옵셔널이 정말로 의미하는바는 무엇인가? 이 데이터를 표현할 더 좋은 방법은 없는가?



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret