You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
RxSwift는 매우 큰 주제거리이기 때문에 세션에서는 애플리케이션 아키텍처에 대해선 우선적으로 자세히 설명하지 않았다
RxSwift가 앱에 특정 아키텍처만을 따라서 적용하는 서드 파티도 아닌 이유도 있다.
단, RxSwift와 MVVM은 연동하여 사용하기에 매우 적합하므로 이번 챕터에서는 MVVM 아키텍처 패턴에 대해 살펴보도록 하자.
Introducing MVVM
MVVM은 Model-ViewModel의 약자로, Apple의 대표적인 MVC(Model-View-Controller) 패턴을 약간 다르게 구현한 버전이다.
열린 마음으로 MVVM에 접근하는 것이 중요하다.
MVVM은 소프트웨어 아키텍처의 해결책이 아니라, MVVM을 소프트웨어 설계 패턴 중의 하나다.
특히, MVC 사고방식에서 출발하여 개발을 시작한 경우, 나아가 더 나은 애플리케이션 아키텍처를 향한 시작 단계로 볼 수 있다.
Background on MVC
지금까지 코드를 작성하며 MVVM과 MVC 사이에 약간의 긴장감을 느껴왔다.
정확히 그들의 관계의 본질은 무엇일까?
그들은 비슷하고 먼 친척이라고 말할 수도 있다.
하지만 이러한 설명이 납득될 만큼 비슷하면서 충분히 다르기도하다.
프로그래밍에 관한 책의 대부분의 예에서는 코드 샘플에 MVC 패턴을 사용한다.
MVC는 많은 간단한 앱의 간단한 패턴으로 다음과 같다.
각 영역에는 역할이 할당되어 있다.
컨트롤러 영역은 모델과 뷰를 모두 갱신할 수 있기 때문에 중심적인 역할을 한다.
뷰는 화면에 데이터를 표시하고 제스처와 같은 이벤트를 컨트롤러로 전송한다.
마지막으로, 모델은 앱 상태를 유지하기 위해 데이터를 읽고 쓴다.
MVC는 잠시 동안 유용하게 사용할 수 있는 단순한 패턴이지만,
앱이 성장함에 따라 많은 영역의 지분을 뷰와 모델이 아닌 컨트롤러가 차지한다는 것을 알게 된다.
일반적인 함정은 단일 컨트롤러 클래스에 점점 더 많은 코드를 추가하기 시작한다는 점이다
iOS 앱이 포함된 View Controller에서 시작하므로 모든 코드를 해당 View Controller 클래스에 넣는 경우가 되기 쉽다
컨트롤러가 수백, 수천 회선으로 확장되어 MVC, 즉 "Massive View Controller"가 되기 쉽다
클래스를 오버로드하는 것은 단순히 잘못된 관행일 뿐 MVC 패턴의 단점은 아니다.
Apple의 많은 개발자들은 MVC의 팬이며, 놀라울 정도로 잘 만들어진 MacOS와 iOS 소프트웨어를 생산한다.
View Model은 MVC에서 View Controller와 동일한 기능을 하지 않나? - 이건 맞으면서도 틀린 이야기다
앞에서 설명한 바와 같이 View Controller를 View 자체를 제어하지 않는 코드로 채우는 것이 일반적인 문제점이다.
MVVM은 View Controller를 View와 함께 그룹화하여 이 문제를 해결하려고 하며 View 제어에 대한 단독 책임을 할당한다.
MVVM 아키텍처의 또 다른 장점은 코드의 테스트 가능성이 높아진다는 점이다.
View 라이프 사이클을 비즈니스 로직에서 분리하면 View Controller와 View Model을 모두 쉽게 테스트할 수 있다.
마지막으로 View Model은 프레젠테이션 레이어에서 완전히 분리되어
필요에 따라 플랫폼 간에 재사용할 수 있다.
View-View Controller 쌍을 교체하고 iOS에서 MacOS 또는 tvOS로 앱을 마이그레이션할 수 있다.
Deciding what goes where
모든 항목이 View Model 클래스에 포함된다고 생각하지마라.
MVC에서 때때로 겪게 되는 것과 매시브와 같은 광기가 될 수도 있다.
코드 베이스 전체에 걸쳐 합리적인 방법으로 책임을 분담하고 할당하는 것은 개발자에게 달려 있다.
따라서 View Model은 데이터와 화면 사이의 브레인 역할을 유지하되,
네트워킹, 탐색, 캐시 및 이와 유사한 책임은 다른 클래스로 분할해야 한다.
그렇다면 이러한 추가적인 클래스가 MVVM 범주에 속하지 않는 경우 어떻게 작업해야 할까?
MVVM은 이에 대한 규칙을 적용하지는 않지만, 이번 챕터에서는 몇 가지 가능한 솔루션을 소개하는 프로젝트를 진행한다.
챕터에서 다룰 한 가지 좋은 아이디어는 View Model에 필요한 모든 객체를 View Model의 init 또는 라이프 사이클의 후반부에 주입하는 거다.
즉, 상태 저장 API 클래스 또는 지속성 레이어 객체와 같은 수명이 긴 객체를 View Model에서 View Model로 전달할 수 있다.
이번 챕터의 프로젝트인 Tweetie의 경우,
앱 안에서 내비게이션을 관리하는 오브젝트(Navigator),
Twitter 계정에 로그인되어 있는 상태(TwitterAPI.AccountStatus) 등
위에서 제시한 방법으로 메시지를 전달한다.
하지만 작은 코드량만이 MVVM의 장점일까?
올바르게 사용하면 MVC에 비해 다음과 같이 개선된다.
View Controller의 책임은 View의 '제어'뿐이기 때문에 View Controller는 훨씬 단순해지고 그 이름에 걸맞게 된다. MVVM은 특히 RxSwift/RxCocoa에서 잘 작동한다. Observable한 객체를 UI 컴포넌트에 바인딩할 수 있기 때문이다. UI 컴포넌트는 이러한 패턴을 가능하게 하는 주축으로 역할한다.
View Model은 명확한 Input -> Output 패턴을 따른다. 또한 미리 정의된 Input과 예상되는 Output에 대한 테스트를 제공하므로 유닛 테스트하기 쉽다.
View Controller를 UI 테스트하는 것은 Mock View Model을 작성하고 예상되는 View Controller 상태를 테스트하는 것으로 훨씬 쉬워진다.
MVVM은 MVC에서 크게 벗어나기 때문에 추가적인 소프트웨어 아키텍처 패턴을 탐색하는 데 도움이 되고 영감을 준다.
이번 챕터를 통해 MVVM을 시험해 보고 많은 이점을 얻어보자.
Getting started with Tweetie
챕터에서는 Tweetie라는 멀티플랫폼 프로젝트에 대해 설명한다.
이것은 트위터에서 구동되는 매우 간단한 앱으로, 미리 정의된 사용자 목록을 사용하여 트윗을 표시합니다.
기본적으로 스타터 프로젝트는 이번 세션의 모든 작성자와 편집자가 포함된 트위터 목록을 사용한다.
원하는 경우 목록을 쉽게 변경하여 프로젝트를 스포츠, 글쓰기, 영화관 중심의 앱으로 만들 수 있다.
게다가 이 앱은 특별한 능력을 가지고 있다.
Twitter 개발자가 아닌 경우, API에 액세스 할 수 없는 경우, Xcode 프로젝트에서 번들된 캐시된 API 데이터로 완전히 실행된다.
이 프로젝트는 macOS와 iOS를 대상으로 하며
MVVM 패턴을 사용하여 많은 실제 프로그래밍 작업을 해결한다.
스타터 프로젝트에는 이미 많은 코드가 포함되어 있으며.
MVVM과 관련된 부분만 중점적으로 다루면 된다.
이번 챕터를 통해 MVVM이 어떻게 다음 항목을 명확하게 구분하는지 확인할 수 있다.
UI와 관련된 코드이며 iOS용 UIKit을 사용하는 View Controller 및 AppKit을 사용하는 별도의 MacOS 전용 View Controller와 같이 플랫폼마다 달라지는 코드.
모델 및 View Model과 같은 특정 플랫폼의 UI 프레임워크에 의존하지 않으므로 그대로 재사용되는 코드
Project structure
이번 챕터의 시작 프로젝트를 찾아 모든 코코아팟을 설치한 후 Xcode로 프로젝트를 열어보자.
코드를 작업하기 전에 프로젝트 구조를 간단히 살펴보면 탐색기에서 여러 폴더를 찾을 수가 있다
API 캐시: Twitter Developer 승인을 기다리는 동안 사용할 Twitter API 응답을 캐시.
공통 클래스: MacOS와 iOS 간의 공유 코드.Rx 포함Reachability 클래스 확장 및 확장 UITableView, NSTableView, 기타.
데이터 엔티티: 디스크에 데이터를 유지하기 위해 Realm Mobile Database와 함께 사용할 데이터 객체.
Twitter API: Twitter의 JSON API에 요청을 하기 위한 베어본 Twitter API 구현. TwitterAccountAPI에서 사용하는 액세스토큰을 취득하는 클래스.TwitterAPI는 Web JSON 엔드포인트에 auth request를 실시.
View Model: 앱의 3가지 View Model이 상주하는 위치.하나는 완전히 기능하고 나머지 2개는 구성할 예정.
iOS 트윗: 스토리보드 및 iOS View Controller 포함하여 iOS 버전의 Tweetie를 포함
Mac Twe: 스토리보드, asset 및 View Controller와 함께 macOS 대상을 포함
트윗 테스트: 앱의 테스트 및 mock 객체가 상주하는 위치.
테스트는 챕터의 과제를 완료할 때까지 통과되지 않는다
또, 제공된 테스트를 사용해 과제를 올바르게 완료했는지 확인할 수 있다
당장 작동되지 않더라도 일단은 과제를 수행해보자!
개발자는 목록에 있는 모든 사용자의 트윗을 볼 수 있도록 앱을 완성해야 한다.
네트워크 레이어를 완료하고 나서 View Model 클래스를 작성하고,
최종적으로 완성된 View Model을 사용하여 데이터를 화면에 표시하는 2개의 View Controller(iOS용과 MacOS용)를 작성한다
다양한 클래스에서 작업하고 MVVM을 직접 체험해보자.
Optionally getting access to Twitter’s API
Twitter의 API는 아쉽게도 폐쇄되어 있기 때문에 데이터에 접근하기 위해서는 개발자 신청 절차를 먼저 거쳐야한다.
해당 작업은 시간이 걸릴 수 있으므로(또는 애플리케이션이 거부될 수 있음)
Tweetie Xcode 프로젝트에 API 응답 데이터를 번들하여
인터넷에 연결하지 않고도 캐시된 데이터를 통해 완전히 작동하게 할 수 있다.
프로젝트 작업을 즉시 시작하려면 다음 장 섹션인 "네트워크 계층 완료"로 바로 이동하여 구성을 시작하면 된다.
Twitter 개발자 계정을 신청하고 실시간 Twitter 데이터를 사용하여 이 챕터에서 작업하려는 경우에만 다음 단계를 수행하면 된다.
2023년 2월 부로 현재 트위터 API는 유로로 전환되었다.
앱의 세부 정보 페이지에서 키 및 토큰 탭을 선택.여기서 API 키와 API secret을 찾을 수 있다.
이제 권한 탭을 선택하고 앱의 권한을 읽기 전용으로 설정.새 앱은 데이터 읽기만 하므로 추가 권한이 필요하지 않다.
Xcode에서 Twitterie 프로젝트를 열고 TwitterAPI/TwitterAccount.swift에서 다음 값을 설정.
API key 와 secret 프로퍼티 수정.
그러면 credential을 사용하여 iOS 및 MacOS 프로젝트가 설정되고 Twitter API로 작업할 수 있게 된다
Finishing up the network layer
프로젝트에는 이미 꽤 많은 코드가 포함되어 있다.
Observable이나 View Controller의 설정과 같은 작업은 이미 구성되어 있다.
먼저 프로젝트 네트워킹을 완료해보자
TimelineFetcher 클래스는 앱이 연결되어 있는 동안 최신 트윗을 자동으로 다시 가져온다.
클래스는 매우 단순하며 Rx 타이머를 사용하여 웹에서 JSON을 가져오는 서브스크립션을 반복적으로 호출한다.
TimelineFetcher 두 가지 convenience inits이 있다
하나는 특정 트위터 목록에서 트윗을 가져오는 것이고 다른 하나는 특정 사용자의 트윗을 가져온다.
해당 섹션에서는 웹 요청을 만들고 응답을 Tweet 객체로 매핑하는 코드를 추가한다.
이전 챕터에서 유사한 작업을 완료했으므로, 이러한 코드의 대부분은 Tweet.swift에 이미 작성되어 있다.
코드를 확인하면 Unbox라는 서드파티를 이용하여 JSON을 Tweet로 파싱하고 있다
MVVM 프로젝트에서 작업할 때 네트워킹을 추가할 위치를 묻는 경우가 많기 때문에
네트워킹을 직접 추가할 수 있도록 챕터를 구성해두었다.
네트워킹은 특별한 것이 아니라 View Model에 주입하는 TwitterAPIProtocol을 준수하는 일반 클래스다.
TimelineFetcher.swift 파일에서 init(account:jsonProvider:) 밑으로 내려보면 아래와 같은 코드를 발견할 수 있다.
스타터 프로젝트에서 코드를 실행하기 위한 Placeholder다
타이머 Observable인 reachableTimerWithAccount를 가져가서 feedCursor와 결합해보자
feedCursor는 현재 아무것도 하지 않지만 해당 릴레이를 사용하여
현재 위치를 트위터 타임라인에 저장하여 이미 가져온 트윗을 표시하게 된다
메서드 매개 변수 jsonProvider를 플랫 매핑하는 것으로 시작한다.
jsonProvider는 이 매개 변수에 주입되는 클로져다.
각각의 convenience 이니셜라이저는 서로 다른 API 엔드 포인드를 가져오게 되어 있으므로
jsonProvider를 주입하는 것이 if 문을 사용하거나 메인 이니셜라이저 init(account:jsonProvider:)에서 논리를 분기하는 것을 피하는 편리한 방법입니다.
jsonProvider는 Observable<[JSONObject]> 항목을 반환한다.
그래서 다음 단계는 그것을 Observable<[Tweet]>에 매핑하면 된다.
제공된 Tweet.unboxMany 메서드를 사용하여 JSON 객체를 트윗 배열로 변환할 수 있다.
몇 줄의 코드로 트윗을 가져올 준비가 되었다.
타임라인은 public Observable 이므로, 이렇게 하면 View Model이 최신 트윗 목록에 액세스할 수 있다.
앱의 View Model은 트윗을 디스크에 저장하거나 앱의 UI를 구동하기 위해 바로 사용할 수 있지만,
이 일은 전적으로 View Model 안에서만 이루어진다
TimelineFetcher는 단순히 트윗을 가져와서 결과를 공개하는 역할만 하게 된다
위의 구독은 반복적으로 호출되므로 동일한 트윗을 반복적으로 가져오지 않도록 현재 위치(또는 커서)도 저장해야 한다
마지막 코드에서 입력한 위치 바로 아래에 Store the latest position through timeline 주석 부분에 아래 코드를 추가해보자
feedCursor는 TimelineFetcher의 BehaviorRelay 타입의 프로퍼티다
TimelineCursor는 지금까지 가져온 가장 오래된 트윗 ID와 최신 트윗 ID를 저장하는 커스텀 구조체다.
이전 코드에서는 scan을 사용하여 ID를 추적한다.
새로운 트윗 덩어리를 가져올 때마다 feedCursor의 값이 업데이트된다.
타임라인 cursor를 업데이트하는 로직에 관심이 있다면 TimelineFetcher.currentCursor() 내부를 살펴보자
커서 로직은 Twitter API에만 한정되므로 자세히 다루지 않는다.
Cursoring에 대한 자세한 내용은 http://bit.ly/2zLF7mx 에서 확인할 수 있다.
다음으로 View Model을 작성해보자.
완성한 TimelineFetcher 사용하여 API에서 최신 트윗을 가져오면 된다
Adding a View Model
프로젝트에는 이미 navigation 클래스, 데이터 엔티티 및 Twitter account access 클래스가 포함되어 있다.
이제 네트워크 계층이 완료되었으므로 모든 것을 결합하여 사용자를 트위터에 로그인하고 트윗을 가져올 수 있다.
참고: 캐시된 API 데이터를 사용하는 경우 앱은 항상 사용자가 Twitter에 로그인한 것으로 가정하게 된다
TwitterAccount.swift 파일에서 어떻게 코딩되는지 볼 수 있지만, 그것을 자세히 다루지는 않는다
이번 섹션에서는 컨트롤러에 관심이 없다.
프로젝트 폴더 View Models를 찾아 ListTimelineViewModel.swift를 열어보자.
이름에서 알 수 있듯이 이 View Model은 지정된 사용자 목록의 트윗을 가져옵니다.
View Model 코드에서 3개의 섹션을 명확하게 정의하는 것이 좋다
(그러나, 확실히 유일한 방법은 아니다):
Init: 모든 종속성을 주입하는 하나 이상의 항목을 정의
Input: View Controller가 입력을 제공할 수 있도록 하는 일반 변수 또는 RxSwift subjects/relays와 같은 public 프로퍼티를 포함
Output: View Model의 출력을 제공하는 public 프로퍼티(일반적으로 Observable 또는 Driver)을 포함. 일반적으로 테이블이나 컬렉션 뷰를 구동하기 위한 객체 목록 또는 View Controller가 앱의 UI를 구동하기 위해 사용하는 다른 유형의 데이터 목록.
ListTimelineViewModel에는 이미 fetcher 프로퍼티를 초기화하는 코드가 있다.
fetcher는 TimelineFetcher의 인스턴스로 트윗을 가져오는 역할을 한다.
View Model에 먼저 다음 두 가지 프로퍼티를 추가해보자.
해당 프로퍼티는 입력도 출력도 아니지만 주입된 종속성을 유지하는 데 도움이 된다:
이들은 상수이므로 초기화할 수 있는 유일한 기회는 init(account:list:apiType)에서 가능하다
클래스 이니셜라이저의 맨 위에 다음을 추가해보자
self.account = account
self.list = list
이제 입력 프로퍼티를 추가해보자.
하지만, 이미 해당 클래스의 모든 의존성은 주입했으니, 그것들은 어떤 프로퍼티여야 할까?
init에 제공하는 삽입된 종속성 및 매개 변수를 사용하여 초기화 시 입력을 제공할 수 있다.
다른 public 프로퍼티를 사용하면 View Model의 수명 동안 언제든지 View Model에 입력을 제공할 수 있다.
예를 들어, 사용자가 데이터베이스를 검색할 수 있는 앱을 생각해보자.
검색 텍스트 필드를 View Model의 입력 프로퍼티에 바인딩할 수 있다.
검색어가 변경되면 View Model은 데이터베이스를 검색하고 그에 따라 출력을 변경하며, 결과를 표시하기 위해 테이블 뷰에 바인딩된다.
현재 View Mode의 경우 TimelineFetcher 클래스를 일시 중지하고 다시 시작할 수 있는 프로퍼티만 입력할 수 있다.
TimelineFetcher는 이를 위해 BehaviorRelay 기능을 이미 갖추고 있으므로 View Model에 프록시 프로퍼티가 필요하다.
ListTimelineViewModel의 입력 섹션에 MARK: - Input 주석 아래 코드를 추가하자:
이 프로퍼티는 TimelineFetcher 클래스에서 일시 중지된 값을 설정하는 프록시로 활용된다
이제 View Model의 출력으로 이동할 수 있다.
View Model은 가져온 트윗 목록과 로그인 상태를 표시한다.
전자는 Realm에서 로드된 트윗 객체의 observable 시퀀스가 될 것이고,
후자는 사용자가 현재 트위터에 로그인되어 있는지 여부를 나타내기 위한 Driver로 단순히 false나 true을 방출할 것이다.
Observable<[Tweet]> 타입의 fetcher.timeline를 구독하고
그리고 결과(트윗 배열)를 Realm.rx.add(update:)에 바인딩한다
Realm.rx.add는 들어오는 객체를 앱의 기본 Realm 데이터베이스에 유지하게 된다.
마지막 코드 조각은 View Model의 데이터 유입을 처리하므로
남은 것은 View Model의 출력을 빌드하면 된다.
bindOutput이라는 메서드를 찾고 Bind tweets 주석 아래에 코드를 추가해보자:
guard let realm =try?Realm()else{return}
tweets =Observable.changeset(from: realm.objects(Tweet.self))
Realm의 Results 클래스를 사용하여 Observable 시퀀스를 얼마나 쉽게 만들 수 있는지 확인할 수 있다.
위의 코드에서는 지속된 모든 트윗으로 Result 집합을 만들고 해당 컬렉션에 대한 변경 사항을 구독한다
이를 관찰하고자 하는 객체에게 Observable 프로퍼티인 tweets을 노출하는데
보통 우리가 작성한 View Controller가 이를 관찰한다.
다음으로, 출력 섹션에 있던 다른 하나인 loggedIn 프로퍼티를 처리해야 한다.
간단하게 처리할 수 있는데 account를 구독하고 계정의 요소를 true 또는 false으로 매핑하면 된다
bindOutput의 Bind if an account is available 주석 아래에 코드를 추가하자:
loggedIn = account
.map{ status in
switch status {case.unavailable:return false
case.authorized:return true
}}.asDriver(onErrorJustReturn: false)
View Model이 수행해야 하는 작업은 이것뿐이다!
모든 종속성을 init에 주입하도록 주의를 기울였고,
다른 클래스가 입력을 제공할 수 있도록 일부 프로퍼티를 추가했으며,
마지막으로 View Model의 출력을 다른 클래스가 관찰할 수 있는 public 프로퍼티에 바인딩했다.
보시다시피 View Model은 초기화 프로그램을 통해 삽입되지 않은 View Controller, 뷰 또는 기타 클래스에 대해 전혀 알지 못한다
View Model은 코드의 나머지 부분과 매우 잘 분리되어 있으므로
화면에 출력이 표시되기 전에 테스트를 작성하여 정상적으로 작동하는지 확인할 수 있다.
Adding a View Model test
Xcode의 프로젝트 탐색기에서 TweetieTests 폴더를 열어보자.
그 안에는 다음과 같은 몇 가지 파일이 있다:
Mocks/TestData.swift: 일부 테스트 JSON 및 테스트 객체를 포함.
Mocks/TwitterTestAPI.swift: 호출된 메서드를 추적하고 API 응답을 기록하는 Twitter API 모의 클래스.
Mocks/TestRealm.swift: Realm이 테스트에 임시 메모리 데이터베이스를 사용하도록 보장하는 테스트 Realm 구성.
ListTimelineViewModelTests.swift를 열어 새 테스트를 추가해보자.
클래스에는 ListTimelineViewModel의 새 인스턴스를 만드는 유틸리티 메서드와 두 가지 테스트가 이미 있다:
그런 다음 PublishSubject를 생성하여 테스트 AccountStatus 값을 내보낼 수 있다.
PublishSubject를 createViewModel()로 전달하여 ViewModel을 만들고
마지막으로 ViewModel 인스턴스를 가져오면서 테스트를 위한 준비가 완료된다
구독이 준비되었으니 테스트 값을 내보낼 수 있다.
이 코드는 세 개의 이벤트를 비동기식으로 대기한 다음
기록된 이벤트가 .next(true), .next(false) 및 .completed(.completed)의 정확한 시퀀스인지 확인한다
이것으로 테스트 케이스는 작성 완료되었다.
고도로 격리된 View Model 클래스를 사용하면 모의 객체를 쉽게 주입하고 입력을 시뮬레이션할 수 있다.
테스트 제품군의 나머지 부분을 읽어보고 테스트 중인 다른 항목을 확인해보자.
만약 유용한 몇 가지 새로운 시험들을 발견한다면, 자유롭게 추가할 수 있다
Tweetie 프로젝트의 View Model은 앱의 나머지 인프라와 매우 잘 격리되어 있기 때문에
테스트를 실행하기 위해 전체 앱을 실행할 필요가 없다
iOS Tweetie/AppDelegate.swift를 들여다보면 코드가 앱의 내비게이션을 만들지 않고 테스트 중 컨트롤러를 보는 방법을 알 수 있다
또한, 테스트할 때 호스트 앱을 모두 사용하지 않도록 설정할 수 있다.
이제 완전히 작동하는 View Model이 있으며, 테스트도 가능하다.
지금 당장 앱을 실행할 수 있지만, 아직까지 로직이 연결되지 않았기에 흥미로운 일은 일어나지 않을 거다.
이제 기능적인 View Model을 사용해서 구성해보자
Adding an iOS view controller
이번 섹션에서는 미리 설정된 목록에 사용자의 구성된 트윗을 표시하는 컨트롤러인
ListTimelineViewController의 View에 ViewModel의 출력을 연결하는 코드를 작성해보자
먼저, iOS 버전의 트위티를 작업을 시작하자.
프로젝트 탐색기에서 iOS Tweetie/View Controllers/List Timeline 폴더를 열면
내부에는 View Controller와 iOS 전용 테이블 셀 뷰 파일이 있다.
ListTimelineViewController.swift를 열고 간단히 살펴보자.
ListTimelineViewController 클래스에는 View Model 프로퍼티와 Navigator 프로퍼티가 있다.
두 클래스 모두 createWith(navigator:storyboard:viewModel) 정적 팩토리 메서드를 통해 주입되게 된다.
두 세트의 설정 코드를 View Controller에 추가하자.
하나는 viewDidLoad()의 정적 할당이고, 다른 하나는 bindUI()의 UI에 대한 View Model과의 바인딩이다.
bindUI() 호출 전에 viewDidLoad() 아래 코드를 추가하자:
title ="@\(viewModel.list.username)/\(viewModel.list.slug)"
navigationItem.rightBarButtonItem =UIBarButtonItem(barButtonSystemItem:.bookmarks, target:nil, action:nil)
그러면 title이 목록의 이름으로 설정되고 navigation item의 오른쪽에 새 단추가 만들어진다.
다음은 작업은 View Model 바인딩하면 된다.
bindUI() 메서드에 Bind button to the people view controller 주석 아래 다음 코드를 추가하자:
navigationItem.rightBarButtonItem!.rx.tap
.throttle(.milliseconds(500), scheduler:MainScheduler.instance).subscribe(onNext:{[weak self] _ in
guard let self =selfelse{return}self.navigator.show(segue:.listPeople(self.viewModel.account,self.viewModel.list), sender:self)}).disposed(by: bag)
오른쪽 navigation item 항목의 탭을 구독하고 이중 탭을 방지하기 위해 throttle을 통해 조절한다.
그런 다음 navigator 프로퍼티에서 show(segue:sender:) 메서드를 호출하여
화면에 세그를 표시하려는 의도를 표시하면된다.
segue는 선택한 트위터 목록의 구성원과 같은 사용자 목록을 표시한다.
navigator는 요청된 화면을 표시하거나 다른 매개 변수에 따라서으로 사용자의 의도와는 관계없이 결정된 화면을 보여 줄 수 있다
Navigator 클래스 구현에 대한 자세한 내용은 정의를 읽어보도록 하자.
여기에는 탐색 가능한 모든 화면 목록이 포함되어 있으며, 필요한 모든 입력 매개 변수를 제공해야만 이러한 segue를 호출할 수 있다.
또한 테이블 뷰에 최신 트윗을 표시하려면 다른 바인딩을 만들어야 한다.
RxRealm 결과를 테이블 및 컬렉션 뷰에 쉽게 바인딩하려면 파일의 맨 위로 스크롤하고 다음 라이브러리를 가져와보자:
import RxRealmDataSources
다시 bindUI() 메서드로 돌아가서 Show tweets in table view 주석 아래에 다음 코드를 추가해보자:
letdataSource=RxTableViewRealmDataSource<Tweet>(cellIdentifier:"TweetCellView", cellType:TweetCellView.self){ cell, _, tweet in
cell.update(with: tweet)}
dataSource는 테이블 뷰 데이터 소스이며,
특히 Realm 컬렉션 변경사항을 방출하는 Observable 시퀀스에서 테이블 뷰를 구동하는 데 적합하다
라이브러리를 통해서 한 줄로 데이터 소스를 완전히 구성하게 된다:
모델 타입을 트윗으로 설정하고
그런 다음 셀 재사용 식별자를 TweetCellView로 설정한다.
마지막으로 각 셀이 화면에 표시되기 전에 각 셀을 구성하기 위한 클로저를 제공하면 된다
이제 데이터 원본을 View Controller의 테이블 뷰에 바인딩할 수 있게 되었다.
마지막 블록 아래에 이 코드를 추가하자:
이 바인딩은 현재 loggedIn 값을 기준으로 messageView.isHidden을 전환하게 된다
캐시된 API 데이터로 작업하는 경우 사용자 계정은 항상 로그인이 되도록 되어있다
viewModel.loggedIn도 정상적으로 바인딩 하게 되고, 앱을 시작하자마자 messageView 배너를 숨기게 된다
이번 섹션에서는 바인딩이 MVVM 패턴의 주요 활성화 요소인 이유를 알 수 있었다.
View Controller가 "연결" 코드로만 작동하기 때문에 문제를 쉽게 구분할 수 있다.
View Model은 UIKit이나 Cocoad와 같은 UI 프레임워크를 import 하지 않기에
현재 플랫폼에서 실행되는 것에 대해 대부분 무지하다.
앱을 실행하고 빛나는 View Model 작동과 바인딩을 모두 확인해보자:
앱이 JSON 요청을 완료하는 즉시(트위터의 실제 API를 사용하는 경우) 상단의 메시지가 사라진다.
그러면 가져온 트윗들이 경쾌한 애니메이션과 함께 밀어들어오게 된다
마지막으로 오른쪽에 있는 navigation bar item 항목을 누르면 앱이 사용자 목록 보기 컨트롤러로 이동하게 된다:
다음 섹션에서는 플랫폼 간에 View Model을 얼마나 쉽게 재사용할 수 있는지 알아보자.
Adding a macOS view controller
View Model은 뷰 또는 뷰를 사용하는 View Controller에 대해 아무것도 알지 못한다.
그런 의미에서 View Model은 필요할 때 플랫폼에 구애받지 않을 수 있다.
동일한 View Model은 iOS와 MacOS View Controller 모두에 데이터를 쉽게 제공할 수 있습니다.
ListTimelineViewModel은 정확히 그러한 View Model 중 하나다.
이것의 유일한 종속성은 RxSwift, RxCocoa 및 Realm 데이터베이스다.
그 라이브러리들은 자체적으로 크로스 플랫폼이기 때문에 View Model 자체도 크로스 플랫폼이다.
Xcode 프로젝트를 macOS 대상으로 전환하고
위에 구축한 iOS를 미러링하는 View Controller를 구축해보자
Xcode의 scheme 셀렉터에서 MacTweetie/My Mac을 선택하고 프로젝트를 실행하여
MacOS 스타터 프로젝트가 어떻게 보이는지 확인해보자
앱은 미리 정의된 트위터 목록에 포함된 모든 계정의 목록을 표시하지만 창 오른쪽은 비어 있을 거다.
비어있는 View Controller는 트윗 타임라인을 표시해야 하는 컨트롤러다.
완료되면 iOS Tweetie 앱 용으로 만든 트윗 목록과 매우 유사해야 한다.
Mac Tweetie/ViewControllers/List Timeline을 열고 ListTimelineViewController.swift를 선택한다.
이 파일의 이름은 iOS View Controller 파일과 유사하지만 대신 Mac Tweetie 폴더에 있다.
iOS 앱에서 했던 것처럼 목록의 이름을 맨 위에 표시하는 것부터 시작해보자.
bindUI() 메서드를 호출 전에 viewDidLoad()에 다음 코드를 추가하자:
이제 바인딩으로 이동할 수 있다.
만약 macOS View Controller의 코드를 훑어본다면,
iOS와 동일한 View Model과 네비게이터 클래스를 사용한다는 것을 알 수 있다.
이미 타임라인 View Model 목록을 알고 있기 때문이다.
실제로 View Controller 코드는 iOS 버전과 거의 동일하다
이러한 코드 유사성은 RxSwift의 많은 이점 중 하나며, 많은 Rx 코드들은 언어들 사이에서도 꽤 비슷하게 보인다.
RxJava로 작성된 자바나 RxJS로 작성된 자바스크립트를 읽고 이해할 수 있는 쉬운 방법에 놀랄 수도 있다
iOS View Controller와 마찬가지로 현재 파일을 위로 스크롤하여 RxRealmDataSources를 가져오자:
import RxRealmDataSources
이제 bindUI() 메서드 아래로 스크롤하여 UI를 바인딩하자.
View Model의 트윗을 테이블 뷰에 바인딩하기 위해 Show tweets in table view 주석 아래 코드를 추가하자:
letdataSource=RxTableViewRealmDataSource<Tweet>(cellIdentifier:"TweetCellView", cellType:TweetCellView.self){ cell, row, tweet in
cell.update(with: tweet)}
여기서 테이블 뷰 바인딩을 위해서 "TweetCellView" 식별자인 셀이 있는 Tweet 객체가 포함된 데이터 소스를 만들고
각 셀을 다시 사용하기 전에 해당 update(with:)를 호출하여 구성한다
이미 초기화된 data source 객체를 사용하여 테이블 뷰 행과 realmChanges 간의 바인딩을 만든다
이제 단순히 View Model의 tweets 프로퍼티를 구성된 바인딩에 바인딩할 수 있다. 다음을 추가하자:
바인딩은 테이블 뷰에 생기를 불어넣을 거다.
앱을 실행하고 창 오른쪽에 나타나는 트윗을 관찰해보자
정말로 쉽게 동일한 로직을 프레임워크와 관계없이 구현할 수 있었다
네트워킹, 데이터 변환 또는 JSON 검증을 수행할 필요가 없기에 보다 편리하다
앱의 다른 부분이 아닌 View Controller에서만 작업하고
View Model에서 모든 것을 처리하기 때문에 해야 할 일은 데이터를 UI에 바인딩하는 것뿐이었다.
이제 코드를 모델, 뷰 모델 및 뷰 컨트롤러가 있는 뷰로 분할하는 방법을 기본적으로 이해했다.
MVVM은 단순한 앱 이상의 모든 것에 대해 MVC에 비해 이점이 있지만,
MVVM만이 유일한 옵션이 아니라는 것을 기억하는 것이 중요하다
MVVM은 RxSwift와 함께 사용하기에 특히 좋은 패턴이다.
따라서 더 쉽게 읽고 테스트할 수 있는 더 클린한 코드가 생성된다
다른 아키텍처 패턴은 다양한 이점을 가지고 있으며,
그러한 패턴에 더 적합한 다른 라이브러리가 있을 수 있다.
하지만 MVVM + RxSwift를 배우고 싶다면 아래의 과제를 꼭 시도해 보도록 하자
Challenges
과제 1: 구성원 목록에서 "로딩 중..." 전환
사용자 목록을 표시하는 화면에서 Loading... 라벨이 항상 표시된다.
로드 표시기가 있는 것은 유용하지만, 앱이 서버에서 JSON을 가져오는 동안에만 표시되기를 원한다.
해당 과제를 완료하기 위해, iOS와 macOS 앱 모두에 적용시켜야한다.
프로젝트의 iOS 부분에서 먼저 ListPeopleViewController.swift를 열자.
bindUI() 안에서 viewModel.people를 구독하고
드라이버로 변환과 더불어 요소를 true과 false으로 매핑해보자
viewModel.people가 nil일 때 false를 방출하게 된다
messageView.rx.isHidden은 Driver의 결과와 함께 동작하게 된다
결국 앱이 JSON을 가져올 때만 "로딩 중…"이 표시되고
작업이 완료되면 레이블이 자동으로 사라지게 된다
iOS 앱의 결과가 마음에 들면, macOS 대상으로 넘어가자.
macOS View Controller 아웃렛의 이름이 같기 때문에
iOS View Controller에서 직접 코드를 macOS 앱의 ListPeopleViewController.swift에 복사할 수 있다.
과제 2: Uber 챌린지 - 사용자의 타임라인에 대한 전체 View Model 및 View Controller
당신은 iOS와 macOS 앱 모두에 여전히 누락된 부분이 있다는 것을 알 수 있다.
사용자 목록에서 사용자를 선택하면 비어 있는 새 View Controller가 나타난다.
이 장의 über 과제로 프로젝트에서 두 개의 앱을 완료하고 선택한 사용자의 개인 트위터 타임라인을 표시해보자
이 과제를 스스로 완료하려면 아래 지침을 따라가자.
그렇지 않으면 이 장의 과제 폴더에 해결책이 포함되어 있기 때문이다.
PersonTimelineViewModel.swift에서 tweets라는 프로퍼티을 찾을 수 있다.
이 변수를 lazy로 변경하고 다음 코드를 사용하여 초기화해보자.
이 코드는 TimelineFetcher 클래스 인스턴스를 구독한다
그리고 목록에 있는 모든 내보낸 트윗을 수집한다.
PersonTimelineViewModelTests.swift에서 tweets 프로퍼티에 대한 테스트 사례를 찾을 수 있으며, 이제 주석을 해제할 수 있게 된다.
그런 다음 iOS PersonTimelineViewController.swift로 전환하고 스크롤하여
bindUI() 메서드로 가서 viewModel.tweets에 두 개의 구독을 추가하면 된다
첫 번째 구독을 사용하여 View Controller의 rx.title을 drvie한다.
트윗을 가져오기 전에 "찾을 수 없음"을 표시하고, 트윗이 표시될 때 사용자의 viewModel에서 사용자 이름과 함께 보여준다
두 번째 구독의 경우 제공된 createTweetsDataSource()를 사용하여 dataSource 객체를 가져온 다음
tweets을 단일 TweetSection에 매핑하고(도움이 필요한 경우 RxDataSources 챕터 참조) 테이블을 drive한다.
앱의 macOS 버전(해당 PersonTimelineViewController.swift)의 경우 제공된 tweets 배열 프로퍼티를 사용하면 된다.
viewModel.tweets를 구독하고 tweets 배열을 업데이트한 후 테이블을 다시 로드한다.
선택적으로 iOS 앱에서와 마찬가지로 창 제목을 업데이트할 수 있다.
이제 사용자 목록을 열고 사용자를 선택한 다음 사용자의 개인 트윗 타임라인이 앱에 다음과 같이 표시된다:
The text was updated successfully, but these errors were encountered:
Ch. 18 MVVM with RxSwift
RxSwift는 매우 큰 주제거리이기 때문에 세션에서는 애플리케이션 아키텍처에 대해선 우선적으로 자세히 설명하지 않았다
RxSwift가 앱에 특정 아키텍처만을 따라서 적용하는 서드 파티도 아닌 이유도 있다.
단, RxSwift와 MVVM은 연동하여 사용하기에 매우 적합하므로 이번 챕터에서는 MVVM 아키텍처 패턴에 대해 살펴보도록 하자.
Introducing MVVM
MVVM은 Model-ViewModel의 약자로, Apple의 대표적인 MVC(Model-View-Controller) 패턴을 약간 다르게 구현한 버전이다.
열린 마음으로 MVVM에 접근하는 것이 중요하다.
MVVM은 소프트웨어 아키텍처의 해결책이 아니라, MVVM을 소프트웨어 설계 패턴 중의 하나다.
특히, MVC 사고방식에서 출발하여 개발을 시작한 경우, 나아가 더 나은 애플리케이션 아키텍처를 향한 시작 단계로 볼 수 있다.
Background on MVC
지금까지 코드를 작성하며 MVVM과 MVC 사이에 약간의 긴장감을 느껴왔다.
정확히 그들의 관계의 본질은 무엇일까?
그들은 비슷하고 먼 친척이라고 말할 수도 있다.
하지만 이러한 설명이 납득될 만큼 비슷하면서 충분히 다르기도하다.
프로그래밍에 관한 책의 대부분의 예에서는 코드 샘플에 MVC 패턴을 사용한다.
MVC는 많은 간단한 앱의 간단한 패턴으로 다음과 같다.
각 영역에는 역할이 할당되어 있다.
컨트롤러 영역은 모델과 뷰를 모두 갱신할 수 있기 때문에 중심적인 역할을 한다.
뷰는 화면에 데이터를 표시하고 제스처와 같은 이벤트를 컨트롤러로 전송한다.
마지막으로, 모델은 앱 상태를 유지하기 위해 데이터를 읽고 쓴다.
MVC는 잠시 동안 유용하게 사용할 수 있는 단순한 패턴이지만,
앱이 성장함에 따라 많은 영역의 지분을 뷰와 모델이 아닌 컨트롤러가 차지한다는 것을 알게 된다.
일반적인 함정은 단일 컨트롤러 클래스에 점점 더 많은 코드를 추가하기 시작한다는 점이다
iOS 앱이 포함된 View Controller에서 시작하므로 모든 코드를 해당 View Controller 클래스에 넣는 경우가 되기 쉽다
컨트롤러가 수백, 수천 회선으로 확장되어 MVC, 즉 "Massive View Controller"가 되기 쉽다
클래스를 오버로드하는 것은 단순히 잘못된 관행일 뿐 MVC 패턴의 단점은 아니다.
Apple의 많은 개발자들은 MVC의 팬이며, 놀라울 정도로 잘 만들어진 MacOS와 iOS 소프트웨어를 생산한다.
MVVM to the rescue
MVVM은 MVC와 많이 비슷하지만 확실히 더 나은 느낌이다.
MVC를 좋아하는 사람들은 대개 MVVM을 좋아한다.
새로운 패턴으로 인해 MVC에 공통되는 많은 문제를 쉽게 해결할 수 있기 때문이다.
MVC로부터의 명백한 차이는 View Model이라는 이름의 새로운 영역에서 알 수 있다.
View Model은 아키텍처에서 중심적인 역할을 수행한다.
비즈니스 로직을 관리하고 모델과 뷰 모두에 대해 메시지를 전달한다.
MVVM은 다음과 같은 간단한 규칙을 따른다.
View Model은 MVC에서 View Controller와 동일한 기능을 하지 않나? - 이건 맞으면서도 틀린 이야기다
앞에서 설명한 바와 같이 View Controller를 View 자체를 제어하지 않는 코드로 채우는 것이 일반적인 문제점이다.
MVVM은 View Controller를 View와 함께 그룹화하여 이 문제를 해결하려고 하며 View 제어에 대한 단독 책임을 할당한다.
MVVM 아키텍처의 또 다른 장점은 코드의 테스트 가능성이 높아진다는 점이다.
View 라이프 사이클을 비즈니스 로직에서 분리하면 View Controller와 View Model을 모두 쉽게 테스트할 수 있다.
마지막으로 View Model은 프레젠테이션 레이어에서 완전히 분리되어
필요에 따라 플랫폼 간에 재사용할 수 있다.
View-View Controller 쌍을 교체하고 iOS에서 MacOS 또는 tvOS로 앱을 마이그레이션할 수 있다.
Deciding what goes where
모든 항목이 View Model 클래스에 포함된다고 생각하지마라.
MVC에서 때때로 겪게 되는 것과 매시브와 같은 광기가 될 수도 있다.
코드 베이스 전체에 걸쳐 합리적인 방법으로 책임을 분담하고 할당하는 것은 개발자에게 달려 있다.
따라서 View Model은 데이터와 화면 사이의 브레인 역할을 유지하되,
네트워킹, 탐색, 캐시 및 이와 유사한 책임은 다른 클래스로 분할해야 한다.
그렇다면 이러한 추가적인 클래스가 MVVM 범주에 속하지 않는 경우 어떻게 작업해야 할까?
MVVM은 이에 대한 규칙을 적용하지는 않지만, 이번 챕터에서는 몇 가지 가능한 솔루션을 소개하는 프로젝트를 진행한다.
챕터에서 다룰 한 가지 좋은 아이디어는 View Model에 필요한 모든 객체를 View Model의 init 또는 라이프 사이클의 후반부에 주입하는 거다.
즉, 상태 저장 API 클래스 또는 지속성 레이어 객체와 같은 수명이 긴 객체를 View Model에서 View Model로 전달할 수 있다.
이번 챕터의 프로젝트인 Tweetie의 경우,
앱 안에서 내비게이션을 관리하는 오브젝트(Navigator),
Twitter 계정에 로그인되어 있는 상태(TwitterAPI.AccountStatus) 등
위에서 제시한 방법으로 메시지를 전달한다.
하지만 작은 코드량만이 MVVM의 장점일까?
올바르게 사용하면 MVC에 비해 다음과 같이 개선된다.
View Controller의 책임은 View의 '제어'뿐이기 때문에 View Controller는 훨씬 단순해지고 그 이름에 걸맞게 된다. MVVM은 특히 RxSwift/RxCocoa에서 잘 작동한다. Observable한 객체를 UI 컴포넌트에 바인딩할 수 있기 때문이다. UI 컴포넌트는 이러한 패턴을 가능하게 하는 주축으로 역할한다.
View Model은 명확한 Input -> Output 패턴을 따른다. 또한 미리 정의된 Input과 예상되는 Output에 대한 테스트를 제공하므로 유닛 테스트하기 쉽다.
MVVM은 MVC에서 크게 벗어나기 때문에 추가적인 소프트웨어 아키텍처 패턴을 탐색하는 데 도움이 되고 영감을 준다.
이번 챕터를 통해 MVVM을 시험해 보고 많은 이점을 얻어보자.
Getting started with Tweetie
챕터에서는 Tweetie라는 멀티플랫폼 프로젝트에 대해 설명한다.
이것은 트위터에서 구동되는 매우 간단한 앱으로, 미리 정의된 사용자 목록을 사용하여 트윗을 표시합니다.
기본적으로 스타터 프로젝트는 이번 세션의 모든 작성자와 편집자가 포함된 트위터 목록을 사용한다.
원하는 경우 목록을 쉽게 변경하여 프로젝트를 스포츠, 글쓰기, 영화관 중심의 앱으로 만들 수 있다.
게다가 이 앱은 특별한 능력을 가지고 있다.
Twitter 개발자가 아닌 경우, API에 액세스 할 수 없는 경우, Xcode 프로젝트에서 번들된 캐시된 API 데이터로 완전히 실행된다.
이 프로젝트는 macOS와 iOS를 대상으로 하며
MVVM 패턴을 사용하여 많은 실제 프로그래밍 작업을 해결한다.
스타터 프로젝트에는 이미 많은 코드가 포함되어 있으며.
MVVM과 관련된 부분만 중점적으로 다루면 된다.
이번 챕터를 통해 MVVM이 어떻게 다음 항목을 명확하게 구분하는지 확인할 수 있다.
Project structure
이번 챕터의 시작 프로젝트를 찾아 모든 코코아팟을 설치한 후 Xcode로 프로젝트를 열어보자.
코드를 작업하기 전에 프로젝트 구조를 간단히 살펴보면 탐색기에서 여러 폴더를 찾을 수가 있다
개발자는 목록에 있는 모든 사용자의 트윗을 볼 수 있도록 앱을 완성해야 한다.
네트워크 레이어를 완료하고 나서 View Model 클래스를 작성하고,
최종적으로 완성된 View Model을 사용하여 데이터를 화면에 표시하는 2개의 View Controller(iOS용과 MacOS용)를 작성한다
다양한 클래스에서 작업하고 MVVM을 직접 체험해보자.
Optionally getting access to Twitter’s API
Twitter의 API는 아쉽게도 폐쇄되어 있기 때문에 데이터에 접근하기 위해서는 개발자 신청 절차를 먼저 거쳐야한다.
해당 작업은 시간이 걸릴 수 있으므로(또는 애플리케이션이 거부될 수 있음)
Tweetie Xcode 프로젝트에 API 응답 데이터를 번들하여
인터넷에 연결하지 않고도 캐시된 데이터를 통해 완전히 작동하게 할 수 있다.
프로젝트 작업을 즉시 시작하려면 다음 장 섹션인 "네트워크 계층 완료"로 바로 이동하여 구성을 시작하면 된다.
Twitter 개발자 계정을 신청하고 실시간 Twitter 데이터를 사용하여 이 챕터에서 작업하려는 경우에만 다음 단계를 수행하면 된다.
2023년 2월 부로 현재 트위터 API는 유로로 전환되었다.
그러면 credential을 사용하여 iOS 및 MacOS 프로젝트가 설정되고 Twitter API로 작업할 수 있게 된다
Finishing up the network layer
프로젝트에는 이미 꽤 많은 코드가 포함되어 있다.
Observable이나 View Controller의 설정과 같은 작업은 이미 구성되어 있다.
먼저 프로젝트 네트워킹을 완료해보자
TimelineFetcher 클래스는 앱이 연결되어 있는 동안 최신 트윗을 자동으로 다시 가져온다.
클래스는 매우 단순하며 Rx 타이머를 사용하여 웹에서 JSON을 가져오는 서브스크립션을 반복적으로 호출한다.
TimelineFetcher 두 가지 convenience inits이 있다
하나는 특정 트위터 목록에서 트윗을 가져오는 것이고 다른 하나는 특정 사용자의 트윗을 가져온다.
해당 섹션에서는 웹 요청을 만들고 응답을 Tweet 객체로 매핑하는 코드를 추가한다.
이전 챕터에서 유사한 작업을 완료했으므로, 이러한 코드의 대부분은 Tweet.swift에 이미 작성되어 있다.
코드를 확인하면 Unbox라는 서드파티를 이용하여 JSON을 Tweet로 파싱하고 있다
TimelineFetcher.swift 파일에서 init(account:jsonProvider:) 밑으로 내려보면 아래와 같은 코드를 발견할 수 있다.
스타터 프로젝트에서 코드를 실행하기 위한 Placeholder다
해당 코드를 다음과 같이 교체해보자
타이머 Observable인 reachableTimerWithAccount를 가져가서 feedCursor와 결합해보자
feedCursor는 현재 아무것도 하지 않지만 해당 릴레이를 사용하여
현재 위치를 트위터 타임라인에 저장하여 이미 가져온 트윗을 표시하게 된다
메서드 매개 변수 jsonProvider를 플랫 매핑하는 것으로 시작한다.
jsonProvider는 이 매개 변수에 주입되는 클로져다.
각각의 convenience 이니셜라이저는 서로 다른 API 엔드 포인드를 가져오게 되어 있으므로
jsonProvider를 주입하는 것이 if 문을 사용하거나 메인 이니셜라이저 init(account:jsonProvider:)에서 논리를 분기하는 것을 피하는 편리한 방법입니다.
jsonProvider는 Observable<[JSONObject]> 항목을 반환한다.
그래서 다음 단계는 그것을 Observable<[Tweet]>에 매핑하면 된다.
제공된 Tweet.unboxMany 메서드를 사용하여 JSON 객체를 트윗 배열로 변환할 수 있다.
몇 줄의 코드로 트윗을 가져올 준비가 되었다.
타임라인은 public Observable 이므로, 이렇게 하면 View Model이 최신 트윗 목록에 액세스할 수 있다.
앱의 View Model은 트윗을 디스크에 저장하거나 앱의 UI를 구동하기 위해 바로 사용할 수 있지만,
이 일은 전적으로 View Model 안에서만 이루어진다
TimelineFetcher는 단순히 트윗을 가져와서 결과를 공개하는 역할만 하게 된다
위의 구독은 반복적으로 호출되므로 동일한 트윗을 반복적으로 가져오지 않도록 현재 위치(또는 커서)도 저장해야 한다
마지막 코드에서 입력한 위치 바로 아래에 Store the latest position through timeline 주석 부분에 아래 코드를 추가해보자
feedCursor는 TimelineFetcher의 BehaviorRelay 타입의 프로퍼티다
TimelineCursor는 지금까지 가져온 가장 오래된 트윗 ID와 최신 트윗 ID를 저장하는 커스텀 구조체다.
이전 코드에서는 scan을 사용하여 ID를 추적한다.
새로운 트윗 덩어리를 가져올 때마다 feedCursor의 값이 업데이트된다.
타임라인 cursor를 업데이트하는 로직에 관심이 있다면 TimelineFetcher.currentCursor() 내부를 살펴보자
다음으로 View Model을 작성해보자.
완성한 TimelineFetcher 사용하여 API에서 최신 트윗을 가져오면 된다
Adding a View Model
프로젝트에는 이미 navigation 클래스, 데이터 엔티티 및 Twitter account access 클래스가 포함되어 있다.
이제 네트워크 계층이 완료되었으므로 모든 것을 결합하여 사용자를 트위터에 로그인하고 트윗을 가져올 수 있다.
이번 섹션에서는 컨트롤러에 관심이 없다.
프로젝트 폴더 View Models를 찾아 ListTimelineViewModel.swift를 열어보자.
이름에서 알 수 있듯이 이 View Model은 지정된 사용자 목록의 트윗을 가져옵니다.
View Model 코드에서 3개의 섹션을 명확하게 정의하는 것이 좋다
(그러나, 확실히 유일한 방법은 아니다):
ListTimelineViewModel에는 이미 fetcher 프로퍼티를 초기화하는 코드가 있다.
fetcher는 TimelineFetcher의 인스턴스로 트윗을 가져오는 역할을 한다.
View Model에 먼저 다음 두 가지 프로퍼티를 추가해보자.
해당 프로퍼티는 입력도 출력도 아니지만 주입된 종속성을 유지하는 데 도움이 된다:
이들은 상수이므로 초기화할 수 있는 유일한 기회는 init(account:list:apiType)에서 가능하다
클래스 이니셜라이저의 맨 위에 다음을 추가해보자
이제 입력 프로퍼티를 추가해보자.
하지만, 이미 해당 클래스의 모든 의존성은 주입했으니, 그것들은 어떤 프로퍼티여야 할까?
init에 제공하는 삽입된 종속성 및 매개 변수를 사용하여 초기화 시 입력을 제공할 수 있다.
다른 public 프로퍼티를 사용하면 View Model의 수명 동안 언제든지 View Model에 입력을 제공할 수 있다.
예를 들어, 사용자가 데이터베이스를 검색할 수 있는 앱을 생각해보자.
검색 텍스트 필드를 View Model의 입력 프로퍼티에 바인딩할 수 있다.
검색어가 변경되면 View Model은 데이터베이스를 검색하고 그에 따라 출력을 변경하며, 결과를 표시하기 위해 테이블 뷰에 바인딩된다.
현재 View Mode의 경우 TimelineFetcher 클래스를 일시 중지하고 다시 시작할 수 있는 프로퍼티만 입력할 수 있다.
TimelineFetcher는 이를 위해 BehaviorRelay 기능을 이미 갖추고 있으므로 View Model에 프록시 프로퍼티가 필요하다.
ListTimelineViewModel의 입력 섹션에 MARK: - Input 주석 아래 코드를 추가하자:
이 프로퍼티는 TimelineFetcher 클래스에서 일시 중지된 값을 설정하는 프록시로 활용된다
이제 View Model의 출력으로 이동할 수 있다.
View Model은 가져온 트윗 목록과 로그인 상태를 표시한다.
전자는 Realm에서 로드된 트윗 객체의 observable 시퀀스가 될 것이고,
후자는 사용자가 현재 트위터에 로그인되어 있는지 여부를 나타내기 위한 Driver로 단순히 false나 true을 방출할 것이다.
MARK: - Output 주석으로 표시된 섹션에 다음 두 가지 프로퍼티을 추가하자:
tweets에는 최신 트윗 객체의 리스트가 포함되어 있다.
사용자가 자신의 트위터 계정에 로그인하기 전의 포인트와 같이
트윗이 로드되기 전에 기본값은 0이 된다.
loggedIn은 Driver 타입으로 나중에 초기화할 프로퍼티다.
이제 TimelineFetcher를 구독할 수 있다
결과를 가져와 트윗을 Realm에 저장한다.
물론, 이것은 RxRealm을 하면 꽤나 쉽게 구현된다.
init(account:list:apiType:)에 다음 코드를 추가하자:
Observable<[Tweet]> 타입의 fetcher.timeline를 구독하고
그리고 결과(트윗 배열)를 Realm.rx.add(update:)에 바인딩한다
Realm.rx.add는 들어오는 객체를 앱의 기본 Realm 데이터베이스에 유지하게 된다.
마지막 코드 조각은 View Model의 데이터 유입을 처리하므로
남은 것은 View Model의 출력을 빌드하면 된다.
bindOutput이라는 메서드를 찾고 Bind tweets 주석 아래에 코드를 추가해보자:
Realm의 Results 클래스를 사용하여 Observable 시퀀스를 얼마나 쉽게 만들 수 있는지 확인할 수 있다.
위의 코드에서는 지속된 모든 트윗으로 Result 집합을 만들고 해당 컬렉션에 대한 변경 사항을 구독한다
이를 관찰하고자 하는 객체에게 Observable 프로퍼티인 tweets을 노출하는데
보통 우리가 작성한 View Controller가 이를 관찰한다.
다음으로, 출력 섹션에 있던 다른 하나인 loggedIn 프로퍼티를 처리해야 한다.
간단하게 처리할 수 있는데 account를 구독하고 계정의 요소를 true 또는 false으로 매핑하면 된다
bindOutput의 Bind if an account is available 주석 아래에 코드를 추가하자:
View Model이 수행해야 하는 작업은 이것뿐이다!
모든 종속성을 init에 주입하도록 주의를 기울였고,
다른 클래스가 입력을 제공할 수 있도록 일부 프로퍼티를 추가했으며,
마지막으로 View Model의 출력을 다른 클래스가 관찰할 수 있는 public 프로퍼티에 바인딩했다.
보시다시피 View Model은 초기화 프로그램을 통해 삽입되지 않은 View Controller, 뷰 또는 기타 클래스에 대해 전혀 알지 못한다
View Model은 코드의 나머지 부분과 매우 잘 분리되어 있으므로
화면에 출력이 표시되기 전에 테스트를 작성하여 정상적으로 작동하는지 확인할 수 있다.
Adding a View Model test
Xcode의 프로젝트 탐색기에서 TweetieTests 폴더를 열어보자.
그 안에는 다음과 같은 몇 가지 파일이 있다:
ListTimelineViewModelTests.swift를 열어 새 테스트를 추가해보자.
클래스에는 ListTimelineViewModel의 새 인스턴스를 만드는 유틸리티 메서드와 두 가지 테스트가 이미 있다:
테스트 사례를 완료하려면 하나의 최종 테스트,
즉, loggedIn 출력 프로퍼티가 계정 인증 상태를 반영하는지 확인하는 테스트를 추가해야한다
테스트 클래스 본문 내부에 다음을 추가하자:
이 테스트는 비동기식 테스트이므로 RxBlocking을 사용한다
챕터 16의 "Testing with RxTest"에서 편리한 라이브러리에 대해 알아보았다.
View Model에 의해 출력된 요소인 loggedIn 프로퍼티를 테스트해보자
이를 관찰하고자하는 객체에게 Bool 요소를 들으라고 하면 된다
다음 코드를 추가하자:
그런 다음 PublishSubject를 생성하여 테스트 AccountStatus 값을 내보낼 수 있다.
PublishSubject를 createViewModel()로 전달하여 ViewModel을 만들고
마지막으로 ViewModel 인스턴스를 가져오면서 테스트를 위한 준비가 완료된다
구독이 준비되었으니 테스트 값을 내보낼 수 있다.
다음 비동기 블록을 추가해보자:
마지막으로 loggedIn을 구독하고 3개의 이벤트를 수행한 후 해당 이벤트가 예상되는 이벤트인지 확인하면 된다:
이 코드는 세 개의 이벤트를 비동기식으로 대기한 다음
기록된 이벤트가 .next(true), .next(false) 및 .completed(.completed)의 정확한 시퀀스인지 확인한다
이것으로 테스트 케이스는 작성 완료되었다.
고도로 격리된 View Model 클래스를 사용하면 모의 객체를 쉽게 주입하고 입력을 시뮬레이션할 수 있다.
테스트 제품군의 나머지 부분을 읽어보고 테스트 중인 다른 항목을 확인해보자.
만약 유용한 몇 가지 새로운 시험들을 발견한다면, 자유롭게 추가할 수 있다
이제 완전히 작동하는 View Model이 있으며, 테스트도 가능하다.
지금 당장 앱을 실행할 수 있지만, 아직까지 로직이 연결되지 않았기에 흥미로운 일은 일어나지 않을 거다.
이제 기능적인 View Model을 사용해서 구성해보자
Adding an iOS view controller
이번 섹션에서는 미리 설정된 목록에 사용자의 구성된 트윗을 표시하는 컨트롤러인
ListTimelineViewController의 View에 ViewModel의 출력을 연결하는 코드를 작성해보자
먼저, iOS 버전의 트위티를 작업을 시작하자.
프로젝트 탐색기에서 iOS Tweetie/View Controllers/List Timeline 폴더를 열면
내부에는 View Controller와 iOS 전용 테이블 셀 뷰 파일이 있다.
ListTimelineViewController.swift를 열고 간단히 살펴보자.
ListTimelineViewController 클래스에는 View Model 프로퍼티와 Navigator 프로퍼티가 있다.
두 클래스 모두 createWith(navigator:storyboard:viewModel) 정적 팩토리 메서드를 통해 주입되게 된다.
두 세트의 설정 코드를 View Controller에 추가하자.
하나는 viewDidLoad()의 정적 할당이고, 다른 하나는 bindUI()의 UI에 대한 View Model과의 바인딩이다.
bindUI() 호출 전에 viewDidLoad() 아래 코드를 추가하자:
그러면 title이 목록의 이름으로 설정되고 navigation item의 오른쪽에 새 단추가 만들어진다.
다음은 작업은 View Model 바인딩하면 된다.
bindUI() 메서드에 Bind button to the people view controller 주석 아래 다음 코드를 추가하자:
오른쪽 navigation item 항목의 탭을 구독하고 이중 탭을 방지하기 위해 throttle을 통해 조절한다.
그런 다음 navigator 프로퍼티에서 show(segue:sender:) 메서드를 호출하여
화면에 세그를 표시하려는 의도를 표시하면된다.
segue는 선택한 트위터 목록의 구성원과 같은 사용자 목록을 표시한다.
navigator는 요청된 화면을 표시하거나 다른 매개 변수에 따라서으로 사용자의 의도와는 관계없이 결정된 화면을 보여 줄 수 있다
또한 테이블 뷰에 최신 트윗을 표시하려면 다른 바인딩을 만들어야 한다.
RxRealm 결과를 테이블 및 컬렉션 뷰에 쉽게 바인딩하려면 파일의 맨 위로 스크롤하고 다음 라이브러리를 가져와보자:
다시 bindUI() 메서드로 돌아가서 Show tweets in table view 주석 아래에 다음 코드를 추가해보자:
dataSource는 테이블 뷰 데이터 소스이며,
특히 Realm 컬렉션 변경사항을 방출하는 Observable 시퀀스에서 테이블 뷰를 구동하는 데 적합하다
라이브러리를 통해서 한 줄로 데이터 소스를 완전히 구성하게 된다:
이제 데이터 원본을 View Controller의 테이블 뷰에 바인딩할 수 있게 되었다.
마지막 블록 아래에 이 코드를 추가하자:
여기서 viewModel.tweets를 realmChanges에 바인딩하고 미리 구성된 데이터 소스를 제공한다.
이 값은 애니메이션 변경으로 테이블 보기를 구동하는 데 필요한 최소 값입니다.
이 View Controller의 최종 바인딩은 사용자가 Twitter에 로그인했는지 여부에 따라 맨 위에 메시지를 표시하거나 숨기면 된다.
Show message when no account available 주석 아래에 다음을 추가해보자:
이 바인딩은 현재 loggedIn 값을 기준으로 messageView.isHidden을 전환하게 된다
이번 섹션에서는 바인딩이 MVVM 패턴의 주요 활성화 요소인 이유를 알 수 있었다.
View Controller가 "연결" 코드로만 작동하기 때문에 문제를 쉽게 구분할 수 있다.
View Model은 UIKit이나 Cocoad와 같은 UI 프레임워크를 import 하지 않기에
현재 플랫폼에서 실행되는 것에 대해 대부분 무지하다.
앱을 실행하고 빛나는 View Model 작동과 바인딩을 모두 확인해보자:
앱이 JSON 요청을 완료하는 즉시(트위터의 실제 API를 사용하는 경우) 상단의 메시지가 사라진다.
그러면 가져온 트윗들이 경쾌한 애니메이션과 함께 밀어들어오게 된다
마지막으로 오른쪽에 있는 navigation bar item 항목을 누르면 앱이 사용자 목록 보기 컨트롤러로 이동하게 된다:
다음 섹션에서는 플랫폼 간에 View Model을 얼마나 쉽게 재사용할 수 있는지 알아보자.
Adding a macOS view controller
View Model은 뷰 또는 뷰를 사용하는 View Controller에 대해 아무것도 알지 못한다.
그런 의미에서 View Model은 필요할 때 플랫폼에 구애받지 않을 수 있다.
동일한 View Model은 iOS와 MacOS View Controller 모두에 데이터를 쉽게 제공할 수 있습니다.
ListTimelineViewModel은 정확히 그러한 View Model 중 하나다.
이것의 유일한 종속성은 RxSwift, RxCocoa 및 Realm 데이터베이스다.
그 라이브러리들은 자체적으로 크로스 플랫폼이기 때문에 View Model 자체도 크로스 플랫폼이다.
Xcode 프로젝트를 macOS 대상으로 전환하고
위에 구축한 iOS를 미러링하는 View Controller를 구축해보자
Xcode의 scheme 셀렉터에서 MacTweetie/My Mac을 선택하고 프로젝트를 실행하여
MacOS 스타터 프로젝트가 어떻게 보이는지 확인해보자
앱은 미리 정의된 트위터 목록에 포함된 모든 계정의 목록을 표시하지만 창 오른쪽은 비어 있을 거다.
비어있는 View Controller는 트윗 타임라인을 표시해야 하는 컨트롤러다.
완료되면 iOS Tweetie 앱 용으로 만든 트윗 목록과 매우 유사해야 한다.
Mac Tweetie/ViewControllers/List Timeline을 열고 ListTimelineViewController.swift를 선택한다.
이 파일의 이름은 iOS View Controller 파일과 유사하지만 대신 Mac Tweetie 폴더에 있다.
iOS 앱에서 했던 것처럼 목록의 이름을 맨 위에 표시하는 것부터 시작해보자.
bindUI() 메서드를 호출 전에 viewDidLoad()에 다음 코드를 추가하자:
이제 바인딩으로 이동할 수 있다.
만약 macOS View Controller의 코드를 훑어본다면,
iOS와 동일한 View Model과 네비게이터 클래스를 사용한다는 것을 알 수 있다.
이미 타임라인 View Model 목록을 알고 있기 때문이다.
실제로 View Controller 코드는 iOS 버전과 거의 동일하다
이러한 코드 유사성은 RxSwift의 많은 이점 중 하나며, 많은 Rx 코드들은 언어들 사이에서도 꽤 비슷하게 보인다.
RxJava로 작성된 자바나 RxJS로 작성된 자바스크립트를 읽고 이해할 수 있는 쉬운 방법에 놀랄 수도 있다
iOS View Controller와 마찬가지로 현재 파일을 위로 스크롤하여 RxRealmDataSources를 가져오자:
이제 bindUI() 메서드 아래로 스크롤하여 UI를 바인딩하자.
View Model의 트윗을 테이블 뷰에 바인딩하기 위해 Show tweets in table view 주석 아래 코드를 추가하자:
여기서 테이블 뷰 바인딩을 위해서 "TweetCellView" 식별자인 셀이 있는 Tweet 객체가 포함된 데이터 소스를 만들고
각 셀을 다시 사용하기 전에 해당 update(with:)를 호출하여 구성한다
이미 초기화된 data source 객체를 사용하여 테이블 뷰 행과 realmChanges 간의 바인딩을 만든다
이제 단순히 View Model의 tweets 프로퍼티를 구성된 바인딩에 바인딩할 수 있다. 다음을 추가하자:
바인딩은 테이블 뷰에 생기를 불어넣을 거다.
앱을 실행하고 창 오른쪽에 나타나는 트윗을 관찰해보자
정말로 쉽게 동일한 로직을 프레임워크와 관계없이 구현할 수 있었다
네트워킹, 데이터 변환 또는 JSON 검증을 수행할 필요가 없기에 보다 편리하다
앱의 다른 부분이 아닌 View Controller에서만 작업하고
View Model에서 모든 것을 처리하기 때문에 해야 할 일은 데이터를 UI에 바인딩하는 것뿐이었다.
이제 코드를 모델, 뷰 모델 및 뷰 컨트롤러가 있는 뷰로 분할하는 방법을 기본적으로 이해했다.
MVVM은 단순한 앱 이상의 모든 것에 대해 MVC에 비해 이점이 있지만,
MVVM만이 유일한 옵션이 아니라는 것을 기억하는 것이 중요하다
MVVM은 RxSwift와 함께 사용하기에 특히 좋은 패턴이다.
따라서 더 쉽게 읽고 테스트할 수 있는 더 클린한 코드가 생성된다
다른 아키텍처 패턴은 다양한 이점을 가지고 있으며,
그러한 패턴에 더 적합한 다른 라이브러리가 있을 수 있다.
하지만 MVVM + RxSwift를 배우고 싶다면 아래의 과제를 꼭 시도해 보도록 하자
Challenges
과제 1: 구성원 목록에서 "로딩 중..." 전환
사용자 목록을 표시하는 화면에서 Loading... 라벨이 항상 표시된다.
로드 표시기가 있는 것은 유용하지만, 앱이 서버에서 JSON을 가져오는 동안에만 표시되기를 원한다.
해당 과제를 완료하기 위해, iOS와 macOS 앱 모두에 적용시켜야한다.
프로젝트의 iOS 부분에서 먼저 ListPeopleViewController.swift를 열자.
bindUI() 안에서 viewModel.people를 구독하고
드라이버로 변환과 더불어 요소를 true과 false으로 매핑해보자
viewModel.people가 nil일 때 false를 방출하게 된다
messageView.rx.isHidden은 Driver의 결과와 함께 동작하게 된다
결국 앱이 JSON을 가져올 때만 "로딩 중…"이 표시되고
작업이 완료되면 레이블이 자동으로 사라지게 된다
iOS 앱의 결과가 마음에 들면, macOS 대상으로 넘어가자.
macOS View Controller 아웃렛의 이름이 같기 때문에
iOS View Controller에서 직접 코드를 macOS 앱의 ListPeopleViewController.swift에 복사할 수 있다.
과제 2: Uber 챌린지 - 사용자의 타임라인에 대한 전체 View Model 및 View Controller
당신은 iOS와 macOS 앱 모두에 여전히 누락된 부분이 있다는 것을 알 수 있다.
사용자 목록에서 사용자를 선택하면 비어 있는 새 View Controller가 나타난다.
이 장의 über 과제로 프로젝트에서 두 개의 앱을 완료하고 선택한 사용자의 개인 트위터 타임라인을 표시해보자
이 과제를 스스로 완료하려면 아래 지침을 따라가자.
그렇지 않으면 이 장의 과제 폴더에 해결책이 포함되어 있기 때문이다.
PersonTimelineViewModel.swift에서 tweets라는 프로퍼티을 찾을 수 있다.
이 변수를 lazy로 변경하고 다음 코드를 사용하여 초기화해보자.
이 코드는 TimelineFetcher 클래스 인스턴스를 구독한다
그리고 목록에 있는 모든 내보낸 트윗을 수집한다.
PersonTimelineViewModelTests.swift에서 tweets 프로퍼티에 대한 테스트 사례를 찾을 수 있으며, 이제 주석을 해제할 수 있게 된다.
그런 다음 iOS PersonTimelineViewController.swift로 전환하고 스크롤하여
bindUI() 메서드로 가서 viewModel.tweets에 두 개의 구독을 추가하면 된다
트윗을 가져오기 전에 "찾을 수 없음"을 표시하고, 트윗이 표시될 때 사용자의 viewModel에서 사용자 이름과 함께 보여준다
tweets을 단일 TweetSection에 매핑하고(도움이 필요한 경우 RxDataSources 챕터 참조) 테이블을 drive한다.
앱의 macOS 버전(해당 PersonTimelineViewController.swift)의 경우 제공된 tweets 배열 프로퍼티를 사용하면 된다.
viewModel.tweets를 구독하고 tweets 배열을 업데이트한 후 테이블을 다시 로드한다.
선택적으로 iOS 앱에서와 마찬가지로 창 제목을 업데이트할 수 있다.
이제 사용자 목록을 열고 사용자를 선택한 다음 사용자의 개인 트윗 타임라인이 앱에 다음과 같이 표시된다:
The text was updated successfully, but these errors were encountered: