Universal Link & Custom URL Scheme

커스텀 스킴과 유니버셜 링크에 대해 알아보도록 하자.

이 둘에 대해 얼핏 많이 들어보았을 것이다. 단순히 말해 모두 딥링크를 지원하는 방법이다. 그렇다면 이 둘의 차이점은 무엇일까? 그리고 딥링크의 역할은 무엇이며 딥링크를 통해 우리는 어떤 이점을 얻을 수 있을까. 이것도 함께 알아보도록 하자.

딥 링크란?

위키피디아의 정의에 따르면 딥링크는 특정 페이지에 도달할 수 있는 링크를 말한다. 링크란 단어는 실생활 대화에서도 많이 사용하고 실제로 링크도 많이 사용한다.

*”야~ 내가 링크 보냈어 확인해봐”*

우리는 실생활에서 이런 말을 자주 하곤 한다. 그렇다면 위의 말에 담긴 뜻은 무엇일까? 그것은 바로 정보 전달이다. 정보 전달로는 부족하다. 편리한 정보 전달이다. 그럼 여기서 편리하다는 것은 무엇을 의미할까?

상대방이 내가 알려준 정보, 컨텐츠를 스스로 키워드를 이용하여 검색을 통해 접하는 것이 아닌 링크로 하여금 상대방에게 내가 전달하고자 하는 정보, 컨텐츠를 바로 노출시키는 것이 나는 편리한 정보 전달의 수단으로써의 링크라고 생각한다.

위의 *”야~ 내가 링크 보냈어 확인해봐”*의 링크가 딥링크다. 이 딥링크는 단순히 웹 브라우저 상에서만 존재하는 수단이 아니다. 우린 이런 딥링크를 이용해 특정 앱의 특정 컨텐츠를 상대방에게 편리하게 전달할 수 있다.

이렇게 우리는 딥링크를 통해 사용자에게 우리 앱의 컨텐츠를 Seamless하게 노출시킬 수 있다.


커스텀 URL 스킴이란?

커스텀 스킴은 iOS에서 딥링크를 지원하는 방법 중 하나이다.

현재는 애플에서 유니버셜 링크를 사용하도록 강력하게 추천하고 있다.

Universal links are strongly recommended as a best practice.

이 이유에 대해선 밑에서 천천히 살펴보도록 하자.

공식 문서에서 커스텀 URL 스킴을 앱 내의 자원에 접근할 수 있는 방법을 제공할 수 있는 방법이라고 설명하고 있다. 애플은 시스템 앱들에 대해 미리 정의된 앱 스킴을 제공한다. mailto, tel 그리고 facetime이 그것들이다.

커스텀 URL 스킴을 지원하는 방법은 어렵지 않다.

  1. 앱의 URL 포맷을 정의한다.

  2. 스킴을 등록해 시스템이 정의된 URL에 따라 알맞는 앱으로 사용자를 보낼 수 있도록 한다.

  3. 앱이 열린 URL을 적절히 처리해야 한다.

URL은 반드시 정의된 커스텀 스킴 이름으로 시작해야 하며 파라미터를 함께 넘겨 이에 따른 별도의 동작을 처리하게 할 수도 있다.

1
2
3
4
5
6
7
let url = URL(string: "myphotoapp:Vacation?index=1")

UIApplication.shared.open(url!) { (result) in
if result {
// The URL was delivered successfully!
}
}

위와 같이 정의된 커스텀 스킴은 문서에서도 중복되지 않게 작성하라고 권고하고 있다. 그럼에도 불구하고 커스텀 스킴은 중복이 발생할 수 있다. 그리고 이렇게 중복된 스킴을 갖고 있는 두 앱이 존재한다면 시스템은 사용자가 의도하지 않은 다른 앱을 실행시키게 될 수 있다.

또한 앱이 설치되어 있지 않다면 스킴은 동작하지 않는다. canOpenURL(_:)를 통해 스킴에 해당하는 URL을 열 수 있는지를 검사할 수 있지만 앱이 설치되어 있지 않았을 때 우리가 원하는 궁극적인 행동은 커스텀 URL 스킴으로는 한계가 있다.

그렇다면 여기서 “우리가 궁극적인 행동”이란 무엇일까?

우리는 다른 앱에서 우리의 컨텐츠를 그대로 보여주는 것이 아닌 사용자가 우리 앱에서 우리의 컨텐츠를 소비할 수 있도록 이동시키고 싶을 것이다. (사파리를 통해 미디움의 컨텐츠를 보는 것보다, 미디움 앱을 통해 컨텐츠를 보는 것이 보다 쾌적한 사용자 경험을 제공할 수 있다.)

하지만 앱이 설치되어 있지 않았을 때 단순히 이동시키지 못하는 것에 그치지 않고 최소한 우리 앱을 설치할 수 있는 앱 스토어로 이동이라도 시켜준다면 우리 앱으로의 유입은 앞의 상황보다 더욱 나아질 수 있다.

위의 내용을 통해 우리는 커스텀 URL 스킴의 두 가지 단점을 알 수 있었다.

  1. 중복이 일어나 다른 앱을 실행시킬 수 있다.
  2. 앱이 설치되어 있지 않을 때 충분한 조치를 취할 수 없다.

그리고 1번의 이유는 치명적인 보안 이슈의 여부도 있기 때문에 애플은 이러한 커스텀 URL 스킴의 단점을 보완하고자 유니버셜 링크라는 것을 만들었다.

중요

iOS 9.0 이상의 앱에선 열고자 하는 앱의 커스텀 스킴을 Info.plistLSApplicationQueriesSchemes키의 값으로 반드시 등록을 해야 한다. 그렇지 않으면 설령 해당 스킴의 앱이 설치되어 있다 하더라도 canOpenURL(_:) 메소드는 항상 false를 반환할 것이다.


유니버셜 링크란?

유니버셜 링크도 커스텀 URL 스킴과 동일하게 모바일 환경에서 딥링크를 지원하기 위한 수단이다. 하지만 유니버셜 링크는 앱이 설치되어 있지 않으면 웹 페이지를 통해 컨텐츠를 보여준다는 것이다. (물론 앱이 설치되어 있다면 바로 해당 앱을 실행시켜 컨텐츠를 보여준다.) 그렇기 때문에 유니버셜 링크는 표준 HTTP, HTTPS 링크다.

앱이 설치되어 있지 않을 경우 유니버셜 링크를 통해 컨텐츠를 웹을 통해 노출시키는 방법과 앱 스토어로 사용자를 보내는 방법이 있다.

서비스에 유니버셜 링크를 지원하기 위해선 웹 서버에서의 별도의 작업을 필요로 한다. 앱의 번들 ID와 앱이 열어야 할 경로(path)를 포함하는 AASA(Apple-App-Site-Association 파일이 웹 서버에 등록되어 있어야 한다.

AASA 파일을 서버에 등록해야 하기 때문에 해당 도메인 소유자가 아닌 이상 등록을 할 수가 없다. 그렇기 때문에 고유하고 안전하게 딥링크를 지원할 수 있다.

AASA 파일은 JSON 형태로 다음과 같이 작성되어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"applinks": {
"apps": [],
"details": [{
"appID": "D3KQX62K1A.com.example.photoapp",
"paths": ["/albums"]
},
{
"appID": "D3KQX62K1A.com.example.videoapp",
"paths": ["/videos"]
}]
}
}
  • appID: .의 포맷의 값으로 앱의 식별자를 의미하는 값.

  • paths: 앱에서 처리할 수 있는 링크 경로의 배열

    와일드카드 문자를 사용해 보다 다양한 경로를 간편하게 지원할 수 있다.

    /videos/samples/201?/* : videos/sample에서 2010년대(201?)의 하위 모든 경로를 의미한다.

이미 웹 브라우저를 통해 컨텐츠를 즐기고 있던 사용자가 웹 브라우저 컨텐츠 내부의 유니버셜 링크를 클릭하면 웹 브라우저를 통해 계속해서 컨텐츠를 즐기수 있도록 한다. 계속해서 웹 브라우저에서 컨텐츠를 즐기던 사용자를 앱으로 보내버리는 것은 오히려 사용자 경험을 해칠 수 있기 때문이다.

사용자가 유니버셜 링크를 클릭하여 링크가 활성화되면 iOS는 앱을 실행시키고 NSUserActivity 객체에 정보를 담아보낸다. 그리고 사용자는 AppDelegate 메소드인 application(_:continue:restorationHandler:)를 오버라이딩하여 처리한다.

유니버셜 링크 활성화를 통해 전달된 NSUserActivityactivityTypeNSUserActivityTypeBrowsingWeb이다. 또한 NSUserActivity 객체의 webpageURL 프로퍼티는 사용자가 클릭한 URL을 포함하고 있다. 그리고 NSURLComponents를 통해 URL의 경로나 파라미터를 추출해낼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([Any]?) -> Void) -> Bool
{
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL,
let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
let path = components.path,
let params = components.queryItems else {
return false
}

print("path = \(path)")

if let albumName = params.first(where: { $0.name == "albumname" } )?.value,
let photoIndex = params.first(where: { $0.name == "index" })?.value {

print("album = \(albumName)")
print("photoIndex = \(photoIndex)")
return true

} else {
print("Either album name or photo index missing")
return false
}
}

번외

일반적인 딥링크가 아닌 디퍼드 딥링크(Deferred)라는 개념이 존재한다. 그럼 디퍼드 딥링크는 기존의 딥링크와 다른 점은 무엇일까?

기존의 딥링크를 통해 우리가 할 수 있는 최선의 행동은 웹을 통해 컨텐츠를 보여주던지, 혹은 앱스토어로 사용자를 보내는 것에 불과하다. 디퍼드 딥링크를 굳이 번역해보자면 지연된 딥링크 정도로 해석해볼 수 있을 것이다. 그렇다면 여기서 무엇이 지연되었다는 것일까?

상황을 가정해보자. 앱이 설치되어 있지 않은 상태에서 기존의 딥링크를 통해 앱스토어까지 사용자를 보냈고 사용자가 앱을 설치했다. 딥링크의 궁극적인 목표는 앱의 설치가 아니라 특정 컨텐츠로 사용자를 보내는 것이다. 그렇기 때문에 현재 상황에서 딥링크 본연의 목적을 달성했다고 보기는 힘들다. 여기서 디퍼드 딥링크가 이런 문제를 해결해 줄 수 있다.

디퍼드 딥링크를 사용하면 링크를 통해 앱스토어로 보내진 사용자가 앱을 설치하고 최초로 앱을 실행시켰을 때 최초 링크를 통해 보여주었어야 할 컨텐츠로 사용자를 이동시킨다. 이렇게 앱스토어 > 앱 설치 > 앱 실행이라는 지연된 과정을 거쳐 컨텐츠를 사용자에게 보여줄 수 있는 기능의 딥링크를 디퍼드 딥링크라고 한다.

그렇다면 링크를 통해 앱을 설치하고 최초로 실행한 사용자와 일반적으로 앱 스토어에서 바로 앱을 설치하여 최초로 실행한 사용자를 어떻게 구분하여 컨텐츠를 보여줄까? 이 부분에 대해서 정확하게 설명된 내용을 찾기 힘들었지만 큰 그림에서 설명해준 글에선 이를 다음과 같이 설명하고 있다.

Historically this has been done through fingerprinting. Fingerprinting works by generating a “fingerprint” of a web user consisting of a device’s IP address and user-agent (operating system, operating system version, and other device specific parameters) and generating another fingerprint when a user opens the app. Recent techniques like Branch’s People Based Attribution model have moved beyond fingerprinting to using an unique Identity Graph that matches a device’s unique browser identifier to its unique app identifier, tying it back to the actual user across web and app to achieve true cross platform attribution, giving better matching between link licks and app opens.


참고자료

  1. Allowing Apps and Websites to Link to Your Content
  2. Universal links in iOS
  3. Deferred Deep Linking

Bitcode in iOS

오늘은 iOS의 비트코드(Bitcode)에 대해 알아보려 한다. 비트코드를 알아보게 된 계기는 현재 인턴을 진행하면서 기존 네이버 지도 API를 네이버 지도 V3 API로 교체를 해야 하는 작업을 과제로 받았다.

이를 위해 데모 프로젝트와 문서를 보면서 샘플 프로젝트를 만드려는데 시뮬레이터에서는 정산적으로 빌드가 되고 실행이 되는 반면 실제 디바이스에서 테스트를 진행하려니 비트코드 에러가 발생하면서 빌드조차 되지 않았다.

이 에러를 해결 방법은 간단했는데 Build Setting 탭으로 들어가 Enable Bitcode 항목을 No로 지정해주면 되었다. 하지만 늘 그렇듯 어떤 문제를 했는데 그 해결 방법이 어떠한 방법으로 문제를 해결했는지에 대해 알지 못하는 것만큼 불안한 것은 없다. 특히 이렇게 빌드 세팅의 속성을 변경해주는 경우에는 더 그렇다.

그래서 이에 관해 조금 찾아보았고 그 정보들을 정리해서 기록해보려 한다.

Bitcode

먼저 애플 공식 문서는 비트코드를 다음과 같이 설명하고 있다.

비트코드는 컴파일된 프로그램의 중간 표현(intermediate representation)이다. 비트코드를 포함한 앱을 앱 스토어에 업로드하면 앱은 컴파일되고 앱 스토어어와 링크될 것이다. 비트코드를 포함하면 새로운 버전의 앱을 앱 스토어에 다시 제출할 필요 없이 애플이 앱 바이너리(컴퓨터가 실행할 수 있는 코드를 포함하고 있는 파일)를 다시 최적화할 수 있다.

iOS 앱에서 비트코드는 기본으로 포함되지만 이를 선택할 수 있다. watchOS와 tvOS 앱에서 비트코드는 필수다. 만일 비트코드를 제공한다면 모든 앱과 앱 번들 안의 모든 프레임워크(프로젝트 안의 모든 타겟)는 비트코드를 포함해야 한다.

Xcode는 기본적으로 앱의 심볼을 숨기기 때문에 이는 애플이 읽을 수 없다. 앱을 앱 스토어에 올릴 때 심볼을 포함할 것인지에 대한 옵션이 주어진다. 심볼을 포함하면 테스트플라이트 혹은 앱 스토어를 통해 앱을 배포했을 때 애플은 앱에 대한 크래시 리포트를 제공한다. 만약 크래시리포트를 직접 수집하고 상징화(Symbolication : 상징화는 크래시 로그의 메모리 주소를 사람이 읽을 수 있는 함수명과 라인 넘버로 교체하는 작업을 말한다.)하기를 원한다면 심볼을 업로드할 필요 없다. 대신 앱 배포 후 비트코드 컴파일 dSYM 파일을 다운로드 받을 수 있다.

모든 뜻을 이해할 순 없었지만 내 프로젝트에서 에러가 발생하는 이유는 추측할 수 있었다. Xcode를 통해 프로젝트를 생성하면 기본적으로 비트코드는 포함되도록 설정되어 있다. 하지만 네이버 지도 V3 API 프레임워크는 비트코드를 포함하고 있지 않기 때문에 문서의 내용 중 *”만일 비트코드를 제공한다면 모든 앱과 앱 번들 안의 모든 프레임워크(프로젝트 안의 모든 타겟)는 비트코드를 포함해야 한다.”*에 위배된다. 그래서 프로젝트의 빌드 세팅에서 비트코드 포함 옵션을 No로 지정하니 프로젝트가 정상적으로 빌드 되고 실행될 수 있던 것이다.

하지만 여전히 비트코드 이 자체에 대한 이해는 부족하다. 좀 더 자료를 찾아보자. 역시 나와 비슷한 사람은 많았고 누군가 Quora에 질문을 올렸고 이에 대한 답변이 애플의 공식 문서보다 친절하고 보다 명확하게 와닿았다.

LLVM

비트코드를 이해하기 위해선 먼저 LLVM(Low Level Virtual Machine)에 대해 알아야 한다. LLVM은 라이브러리로 코드를 중간 매체 혹은 기계 코드로 컴파일하는데 사용된다. LLVM을 통해 많은 컴파일러와 언어들을 만들어낸다.

이렇게 만들어진 컴파일러의 컴파일 과정은 세 단계로 나뉜다.

  1. 컴파일러 프론트 엔드는 소스 코드를 중간 표현 단계로 변환한다.
  2. 이 중간 표현 단계는 불필요한 코드를 제거하고 하는 등의 최적하 과정을 겪는다. 이 과정은 소스 코드도 기계 코드도 아닌 중간 표현 단계에서 진행되는데 옵티마이저가 더욱 쉽게 해석할 수 있는 형태이기 때문이다.
  3. 컴파일러 백 엔드가 중간 표현 단계를 기반으로 기계 코드를 생성한다.

비트코드는 LLVM을 통해 앱의 코드를 받아 이를 비트코드로 전환하고 주어진 지침을 통해 이를 실행 가능한 앱으로 전환하는 방법을 안다. 즉 비트코드는 어떤 아키텍쳐에서도 실행되기를 준비하는 중간 단계인 것이다. 간단히 말해서, 이러한 구조는 애플이 앱 스토어에 새로운 CPU 지원을 백엔드에 쉽게 추가할 수 있다는 것을 의미하며 이렇게 되면 비트코드는 이를 통해 새로운 아키텍처로 컴파일 하는 방법을 알 수 있는 것이다.

비트코드를 포함하지 않는다면 컴파일러는 머신 코드만을 포함하는 실행 파일을 생성할 것이다.

하지만 비트코드를 포함한다면 비트코드는 기계 코드와 나란히 실행 파일에 포함될 것이다.

비트코드의 형태로 앱 스토어에 제출하면 앱 스토어는 해당 앱을 다운로드 받는 디바이스 환경에 맞춰 최적화를 진행하여 내려보낼 것이다. 이 과정은 앱 시닝(App Thinning)에 포함된다. 앱 시닝에 대한 내용은 추후에 살펴보고 포스팅할 예정이다.

일례로 살펴보면 애플은 2013년에 아이폰 5s부터 64비트 칩셋으로 교체할 것이라고 발표했고 앱 개발자들은 이를 위해 앱을 다시 컴파일해서 제출해야 했다. 그 이유는 비트코드가 아닌 실행 가능한 코드 자체를 올렸기 때문에 이는 새로운 아키텍쳐 칩셋 환경에서 동작할 수 없었기 때문이다. 이제는 비트코드가 포함된 실행 파일을 올려도 새로운 디바이스 환경에서 앱이 동작할 수 있게 되었다.


참고 자료

init?(coder:), init(nibName:, bundle:), awakeFromNib()

평소에 뷰 컨트롤러나 뷰를 만들기 위해 스토리보드뿐만 아니라 .xib나 코드의 형태로 만들어 사용하기도 한다. 하지만 이들 각각의 방법으로 뷰 컨트롤러를 생성할 때 생성되는 시점이나 불리는 메소드를 명확히 알지 못해 매번 작성한 코드를 이리저리 움직여가며 동작을 확인했다. 오늘은 이렇게 정확하기 알지 못했던 개념을 개념을 더욱 명확하게 하기 위해 공부하고 기록한 내용이다.


xib? nib?

  • nib - NeXT Interface Builder
  • xib - XML Interface Builder

.xib.nib의 차이점은 무엇일까? 차이점이라기보단 .xib 파일은 빌드 시점에 .nib 파일의 형태로 바뀐다. 인터페이스 빌더로 작업한 UI는 .xib 파일의 형태로 저장되고 빌드 시점에 앱 번들로 복사되고 런타임에 로드된다. 또한 .xib 파일은 텍스트 기반의 파일로 바이너리 파일인 .nib 파일보다 소스 컨트롤에 용이하며 읽기에 용이하다.

init?(coder:)

xib 파일로 만든 뷰는 실제로 저장될 때 아카이빙 되어 저장된다. 그러므로 해당 뷰를 불러올 때는 언아카이빙를 통해 불러와야 한다. 스토리보드도 내부적으론 일종의 xib 파일들로 이루어져 있기 때문에 로그를 찍어보면 init?(coder:) 생성자가 불리는 것을 확인할 수 있다. 하지만 아직 이 시점에선 @IBOutlet이나 @IBAction은 준비되어 있지 않다.

init(nibName:, bundle:)

nib 파일명은 결국 우리가 생성해준 xib 파일이 바뀐 결과물이기 때문에 이 둘의 이름은 확장자를 제외하곤 동일하다.

init(nibName:, bundle:) 생성자는 코드로 nib 파일로 만들어진 뷰를 불러올 때 사용하는 생성자이다. 이는 본인도 적지 않게 사용해봤다. 사용법은 nibName 아규먼트에는 nib파일의 이름에 해당하는 값을 넣어주는데 본인은 언제나 두 아규먼트 모두에 nil 값을 할당해주었다. 그 이유가 궁금해서 찾아봤는데 공식 문서는 이를 다음과 같이 설명하고 있었다.

nibName

이 프로퍼티는 init(nibName:, bundle:) 생성자에 의해 생성되는 시점에 지정된 값을 포함한다. 이 프로퍼티는 nil일 수 있다.

뷰 컨트롤러의 뷰를 저장하기 위해 nib 파일을 사용한다면 뷰 컨트롤러를 생성하는 시점에 생성자에 해당 이름을 명시해주는 것을 추천한다. 하지만 만일 nib 파일 명을 지정해주지 않고 loadView를 오버라이딩하지 않는다면 뷰 컨트롤러는 xib 파일을 다른 방법을 이용해 이를 찾아낼 것이다. 구체적으로 말하자면 적절한 파일 이름 (.nib 확장자를 제외한)의 nib를 찾고 뷰를 필요로 할 때 이를 로드하여 사용할 것이다. 적절한 이름을 찾는 과정은 다음의 순서를 따라 찾는다.

  1. 만일 뷰 컨트롤러 클래스의 이름이 MyViewController와 같이 Controller 끝난다면 Controller를 제외한 MyView.nib 파일을 찾을 것이다.
  2. 뷰 컨트롤러와 이름이 동일한 nib 파일을 찾는다. MyViewController라면 MyViewController.nib

위와 같은 이유로 생성 시점에 아규먼트를 넘겨줘야 한다면 다음과 같이 생성자를 정의해줄 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyViewController: UIViewController {
private var prop: Int

init(prop: Int) {
self.prop = prop
super.init(nibBundle: nil, bundle: nil)
}

// 사용자 정의 생성자를 작성해주었기 때문에 반드시 required 생성자를 구현해주어야 한다.
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}

awakFromNib()

사실 awakeFromNib()는 생성자가 아니다. 근데 생각보다 이를 생성자라고 생각하고 사용하는 경우가 적지 않은데 이를 잘못 이해한다면 원하는 결과물을 얻을 수 없을 것이다. 그럼 awakeFromNib은 무엇일까?

이 메소드는 init?(coder:)를 통해 뷰가 모두 언아카이빙된 후 호출된다. @IBOulet@IBAction이 모두 자리가 잡힌 후 호출되는 것이다. init?(coder:)가 언아카이빙의 시작점이라면 awakeFromNib()은 끝나는 시점이라고 할 수 있다.

이 세 메소드들을 공부하면서 각각 로그를 찍어보았는데 한곳에서 알 수 없는 현상이 발생하였다. 이는 다음과 같은 상황이었다. xib 파일을 하나 만들고 UIViewController를 상속받는 뷰 컨트롤러를 생성하였고 이를 xib 파일의 File's Owner로 지정하였다. 그리고 뷰 컨트롤러의 awakeFromNib() 메소드에 로그를 출력하도록 코드를 추가해보았지만 awakeFromNib()은 호출되지 않았다.

그 이유를 찾아본 결과 File's Owner는 이렇게 언아카이빙 되는 뷰 객체와 관련성이 없다. 위에서 언급했듯이 awakeFromNib() 언아카이빙이 모두 끝난 후 호출되는 메소드이다. File's Owner는 언아카이빙이 시작되기 전에 이미 존재하고 언아카이빙이 모두 끝난 후 해당 객체와 연결되기 때문에 File's Owner인 뷰 컨트롤러 안에서의 awakeFromNib()의 호출은 무의미하다.

하지만 스토리보드와 뷰 컨트롤러에선 얘기가 좀 달라진다. 스토리보드에 뷰 컨트롤러를 올리고 로그를 찍어보면 정상적으로 찍히는 것을 확인할 수 있다.

1
2
ViewController - init(coder:)
ViewController - awakeFromNib

그 이유는 해당 뷰 컨트롤러 객체 클래스를 File's Owner가 아닌 해당 뷰 컨트롤러 자체이기 때문이다. 그렇기 때문에 스토리보드 위에 올라가있는 뷰 컨트롤러는 언아키이빙 과정을 거치기 때문에 정상적으로 awakeFromNib()이 호출되는 것을 확인할 수 있다.


참고자료

  1. iOS: initWithCoder:, initWithNibName:bundle:, awakeFromNib, loadView
  2. What’s up with the .NIB -> .XIB?