반응형

원문 : https://www.raywenderlich.com/149753/bond-tutorial-bindings-swift


당신이 생각할수 있는 대부분의 앱은 어디에선가(디스크, 네트워크, 사용자) 데이터를 가져올수 있어야 하고 어딘가에 표시할수 있거나, 변형하거나 변경할 수도 있습니다. 종종 여러가지 방법으로 동일한 데이터를 표시 할수 있습니다.

모든 UI 요소들을 동기화(종종 UI를 ‘연결(wiring up)`이라고 합니다) 상태로 유지하는 것은 보기보다 어렵습니다. 빨리 확인하지 않고 놔두면, 유지가 불가능하며, 스파게티 괴물을 수정하기 어렵습니다. 우리는 모두 겪어봤습니다. :]

이 튜토리얼은 사용자가 수행하는 것을 화면에 보이도록 연결하는 과정을 명확하고 단순화 하기 위해 Bond 사용할 것입니다.

사소해 보이는 슬라이더(slider)와 텍스트 필드(text field)의 예제를 생각해 봅시다.


예제에서, 항상 슬라이더와 같은 값을 보여주도록 텍스트 필드를 업데이트 할것입니다. 텍스트 필드를 업데이트 하면 슬라이더도 업데이트 할것입니다. 추가적인 측정을 위해서, 슬라이더를 최대값과 최소값으로 설정하는 버튼을 연걸할 것입니다.

UIKit을 사용하면, 일반적으로 명령형(imperative) 코드를 작성할 수 있습니다 - 그것은, 프로그램이 무엇을(what) 하는지 선언하는 것보다, 어떻게(how) 해야하는지 알려줍니다. 이 예제에서, 누군가 슬라이더를 바꿀때, 텍스트 필드에 같은값을 보여주기 위해 프로그램에게 이봐(Hey)!라고 말합니다. 그리고 텍스트 필드를 변경하면, 슬라이더에 같은 값을 보여줍니다. 그렇게 하는 동안에, 누군가 최소 설정 버튼을 클릭하면 슬라이더를 0으로 설정합니다. 오!, 그리고 텍스트 필드를 '0'으로 설정하는 것을 잊으면 안됩니다.

대신에, Bond를 사용하면 선언적인(declarative) 코드를 사용할수 있으며, 어떻게(how)에 대해 걱정하지 않고 무엇을(what)에 대해 집중하게 합니다. 이봐(Hey)! 나는 슬라이더와 텍스트 필드가 같은 값이 보이길 원해. 그리고 누군가 '최소 설정’ 버튼을 클랙했을때, 슬라이더를 0으로 설정해Bond의 언어에서는, 텍스트 필드의 값을 슬라이더의 값에 바인드(bind) 한다고 합니다.

Bond는 ReactiveKit 프레임워크의 상위에 있는 binding framework입니다. ReactiveKit은 Swift에서 함수 반응형 프로그래밍(FRP: functional reactive programming)에 필요한 기본 클래스들을 제공합다. ReactiveKit에 익숙하지 않으면, 프로젝트를 중단하고 github의 문서를 검토 할수 있습니다.

Bond는 AppKit과 UIKit 바인딩(bindings)을 제공하며, ReactiveKit에 대한 반응형 델리게이터(reactive delegates)와 데이터소스(datasources)를 제공합니다. Bond는 애플의 API과 FRP세계의 연결을 쉽게한다. Bond를 바인딩 할 시간입니다… Swift Bond.

바인딩이 빠른 Bond(A Quick Bond Binding)

시작하려면, 기본(skeleton)앱이 포함되어 있는 시작 프로젝트를 다운로드 합니다. 인터페이스 빌더(Interface Builder) 대신에 Bond에 집중할 수 있습니다.

이 프로젝트는 CocoaPods를 사용하기에, BindingWithBond.xcworkspace 파일을 사용하여 프로젝트를 엽니다. (CocoaPods에 익숙하지 않다면, CocoaPods을 시작하는데 도움이 되는 튜토리얼을 확인합니다.)

이제 빌드하고 실행합니다. 다음과 같은 화면이 보일것입니다.


문제 없죠? 이제 첫번째 바인딩(binding)을 만들어 볼 시간입니다.

PhotoSearchViewController.swift를 열고 viewDidLoad()에 다음을 추가 하세요.

_ = searchTextField.reactive.text.observeNext {
  text in
  if let text = text {
    print(text)
  }
}

Bond 프레임워크는 reactive프록시에서, UIKit 컨트롤에 다양한 프로퍼티들을 추가하였습니다. 이 경우에, reactive.text는 UITextField의 text 프로퍼티를 기반으로 한 신호(signal) 입니다. Signal은 옵져버(observe) 가능한 이벤트의 순서를 나타냅니다.

observeNext는 이벤트가 발생했을때 동작하고 클로져를 실행합니다. 걱정하지 마세요, 이제 곧 자세히 배우게 될것입니다. 지금은, 빌드하고 실행한 다음에, hello를 입력하면 어떻게 되는지 보세요.

XCode콘솔에서 다음을 볼수 있을 것입니다.

h
he
hel
hell
hello

키를 누를때마다, 방금 추가한 코드의 클로져가 실행됩니다. 앱에서 텍스트 필드의 현재 상태를 기록한(logging)한 결과 입니다. 프로퍼티 변경과 같은 이벤트를 어떻게 옵져버 하는지 보여줍니다. 이제 무엇을 할 수 있는지 볼것입니다.

데이터 조작(Manipulating Data)

PhotoSearchViewController.swift로 돌아가서, super.viewDidLoad() 뒤를 다음 코드로 업데이트합니다.

let uppercase = searchTextField.reactive.text
  .map { $0?.uppercased() }
 
_ = uppercase.observeNext {
  text in
  if let text = text {
    print(text)
  }
}

map 연산은 reactive.text의 출력을 대문자 문자열로 변환하는 신호(signal)를 반환합니다. 그 결과는 이벤트가 발생(arise)한것으로 불리는, observeNext클로져에서 다시 출력됩니다.

주의map에 익숙하지 않다면, Colin Eberhardt's의 함수형 Swift에 대한 훌륭한 튜토리얼을 확인하세요.

빌드하고 실행하고 hello를 다시 입력하세요.

H
HE
HEL
HELL
HELLO

map을 적용한 결과가 바로 신호(signal)라는 것을 알아차렸을지 모릅니다. 따라서, 중간에 할당하는것이 필요하지 않습니다. 다음에 코드로 업데이트합니다.

_ = searchTextField.reactive.text
  .map { $0?.uppercased() }
  .observeNext {
    text in
    if let text = text {
      print(text)
    }
}

이것은 우아하고 부드러운(fluent) 문법 입니다. 앱을 빌드하고 실행하면 동일한 동작을 확인하세요.

아마도 잠깐동안은 인상 깊었을 것이며, 무조건 엄청난 무언가를 시도할 준비를 하세요.

현재 코드를 다음으로 교체 합니다.

_ = searchTextField.reactive.text
  .map { $0!.characters.count > 0 }
  .bind(to: activityIndicator.reactive.isAnimating)

map은 searchTextField에 있는 문자들이 존재하는지에 기반으로 해서, 각 텍스트 문자열을 boolean으로 변경합니다. bind은 놀랍지도 않게, activityIndicator의 isAnimationg프로퍼티에 boolean을 바인딩(binds)합니다.

앱을 빌드하고 실행합니다. 텍스트 필드에 값을 가질때에만 인디게이터(indicator) 에니메이션이 동작하는 것을 볼 수 있습니다. 단지 3줄의 코드만으로도 꽤 인상적인 기술입니다. - 아마도 제임스 본드(James Bond)의 자동차 추격전보다 훨씬 인상적일 것입니다.

보시다시피, Bond는 앱과 UI간의 데이터 전달을 설명하는 부드럽고(fluent) 표현적인(expressive) 방법을 제공합니다. 이렇게 하면 앱이 간단해지며, 이해하기 쉽고, 궁극적으로 일을 빨리 끝내게 합니다.

다음 섹션에서 바인딩이 실제 어떻게 동작하는지 자세히 배울것입니다. 하지만 지금은, Bond의 힘을 감상할 필요가 있습니다.


안쪽 엿보기(A Peek Under the Hood)

더 의미있는 앱을 만들기 전에, Bond가 어떤 기능을 하는지 간단히 살펴보세요. 아래 설명을 읽으면서 Bond타입을 (cmd+click) 통해서 탐색하는 것을 추천합니다.

현재 코드에서 시작하여, map함수는 검색 텍스트 필드의 텍스가 변경될때마다 발생하는 이벤트(Event) 신호(Signal)를 반환합니다. 신호(signal)를 Bond타입인 activityIndicator.reactive.isAnimating프로퍼티에 바인드(bind) 할수 있습니다.

Bond는 대상(activity indicator)에 대한 약한 참조를 가지고 신호(signal)의 옵져버 역할을 합니다.

신호(signal)가 값을 내보낼때마다, Bond는 setter을 호출하고 주어진 값으로 대상을 업데이트합니다.

주의SignalProtocol은 프로토콜 확장을 통해 정의된map연산자에 있습니다. 아마도 프로토콜 지향 프로그래밍(protocol oriented programming)을 들어보셨을것이고 이것은 훌륭한 예제입니다.

신호(signal)에 바인딩(binding) 하는 것뿐만아니라 그것을 옵져버(observe) 할수 있습니다. 예를 들어, 처음에 추가한 코드를 생각해 보세요.

_ = searchTextField.reactive.text.observeNext {
    text in
    print(text)
  }

observeNext를 호출하면 searchTextField의 text 프로퍼티가 변경될때마다 지정된 클로져가 실행되도록 설정합니다.

주의: 이것은 옵져버 패턴(Observer Pattern) 으로 알려진 고전(class) 디자인 패턴을 기반으로 합니다.

마지막으로, searchTextField의 reactive.text프로퍼티를 생각해 보세요. 분명히 이것은 옵져버 할수 있는 신호(Signal)입니다. reactive.text를 옵져버(Observer)라고 부릅니다. - 그리고 옵져버된(observed) 객체로 부터 값을 가져올수 있습니다.

텍스트를 설정하고 싶을지도 모릅니다. 그것은, 텍스트 필드의 text프로퍼티에서 다른 객체를 옵져버하고 싶어합니다. 여기에는 제목(Subject)이 필요합니다. - 신호(Signal)과 옵져버(Observer)인 객체.

Bond는 UIKit을 확장하며, Bond와 Subject과 유사한 프로퍼티를 추가 하세요. 이미 UITextField가 reactive.text 옵져버 프로퍼티를 가지고 있는것을 보았습니다. 이 튜토리얼의 후반부에서 옵져버(observables)에 대해서 살펴볼것입니다.

MVVM

Bond가 UI 프로퍼티를 서로 직접 바인딩하는것을 쉽게 할수 있으며, 실제로 어떻게 사용하고 싶어하는지 모릅니다. Bond는 MVVM(Model-View-ViewModel) 패턴을 지원하는 프레임워크 입니다. 이전에 이 패턴을 보지 못했다면, 튜토리얼 처음 섹션을 읽는 것을 추천합니다.

이제 충분합니다 - 아마도 더 많은 코드를 작성을 하고 싶어 할것입니다. :]

View Model 추가하기(Adding a View Model)

이제 더 현실적인 앱 구조로 만들 시간입니다. View Model그룹에 PhotoSearchViewModel.swift라는 새로운 파일을 만듭니다. 내용을 다음으로 교체 합니다.

import Foundation
import Bond
import ReactiveKit
 
class PhotoSearchViewModel {
  let searchString = Observable<String?>("")
 
  init() {
    searchString.value = "Bond"
 
    _ = searchString.observeNext {
      text in
      if let text = text {
        print(text)
      }
    }
  }
}

단일 searchString프로퍼티로 간단한 뷰 모델(view model)을 만듭니다. observeNext 클로져는 변경된 신호를 받을때마다 searchString의 내용을 출력합니다.

PhotoSearchViewController.swift를 열고 클래스의 맨위 근처에 다음 프로퍼티를 추가하세요.

PhotoSearchViewController에 다음 메소드를 추가 하세요.

func bindViewModel() {
  viewModel.searchString.bind(to:searchTextField.reactive.text)
}

bindViewModel()는 바인딩을 추가 할수 있지만, 지금은 searchTextField를 기본 뷰 모델에 연결해야 합니다.

viewDidLoad()의 내용을 다음으로 교체합니다.

super.viewDidLoad()
bindViewModel()

앱을 빌드하고 실행하면, Bond 텍스트를 볼수 있을 것입니다.


뷰 모델이 searchString에 옵져버를 변경하였지만, 텍스트 필드에 텍스트를 입력할때, 뷰 모델이 업데이트되지 않습니다. 뭐라구요?

현재 단방향(one-way) 바인딩(binding)입니다. 소스(viewModel프로퍼티)에서 대상(텍스트 필드의 reactive.text프로퍼티)으로 변경된 것을 전달(propagate)하는 것을 의미합니다.

다른 방법으로 전달하려면 어떻게 해야 하나요? 간단합니다 - PhotoSearchViewController에서 다음과 같이 bindViewModel()의 바인딩을 업데이트합니다.

viewModel.searchString.bidirectionalBind(to:searchTextField.reactive.text)

쉽다! bidirectionalBind(to:context:)는 소스에 
목적지의 변경된 신호(signal)를 다시 보내주는 bond를 설정 합니다.

이제, 이전의 테스트를 정리하기 위해, PhotoSearchViewModel.swift파일을 열고, init()에서 다음 줄을 제거합니다.

searchString.value = "Bond"

빌드하고 실행하고 나서 Bond Love를 입력하세요.

B
Bo
Bon
Bond
Bond 
Bond L
Bond Lo
Bond Lov
Bond Love

훌륭합니다. - 텍스트 필드 업데이트가 뷰 모델에게 다시 전달되는 것을 확인하였습니다.

옵져버에서 옵져버 만들기(Creating Observable from Observables)

옵져버 매핑으로 좀 더 유용한 것을 할 시간입니다. 검색 내용이 3자 이상이 될때까지 텍스트를 빨간색으로 하는 요구사항을 강요할 것입니다.

PhotoSearchViewModel.swift로 돌아가서, 뷰 모델에 다음 프로퍼티를 추가하세요.

let validSearchText = Observable<Bool>(false)

boolean 프로퍼티는 searchString갑이 유효한지 아닌지를 나타낼것입니다.

이제, 초기화 코드를 다음으로 교체하세요.

searchString
  .map { $0!.characters.count > 3 }
  .bind(to:validSearchText)

searchString옵져버를 문자열의 길이가 3 문자보다 클때 true인 boolean에 매핑하고나서, vailidSearchText 프로퍼티에 바인드(binds)합니다.

PhotoSearchViewController.swift에서, bindViewModel()을 찾아 다음을 추가하세요.

viewModel.validSearchText
  .map { $0 ? .black : .red }
  .bind(to: searchTextField.reactive.textColor)

validSearchText프로퍼티를 색상에 매핑하며, boolean값을 기반으로 합니다. 그리고 나서 searchTextField의 textColor프로퍼티에 결과 색상을 바인딩합니다.

이제 빌드하고 실행해서 텍스트 몇개를 입력하세요.


텍스트가 너무 짧아서 유효한 검색을 할수 없을때, 이제 빨간색이 됩니다. 유효한 것으로 간주되면 바로 검정색으로 변경됩니다.

Bond는 프로퍼티의 연결과 변환을 매우 쉽게 합니다.


검색 준비(Preparing To Search)

만들 앱은 사용자가 입력한 사진을 500px에서 조회하며, 사용자가 익숙한 구글의 피드백과 같은 종류를 제공합니다.

searchString 뷰 모델 프로퍼티가 변경될때마다, 검색을 수행할 수 있지만, 상당히 많은(TONS) 요청을 할수 있습니다. 이것은 초당 하나나 두개만 전송하도록 쿼리를 제한(throttle) 하는 것이 더 좋습니다.

Bond를 사용하면 이것은 정말 쉽습니다!

검색을 위해 PhotoSearchViewModel.swift에 다음 메소드를 추가하세요.

func executeSearch(_ text: String) {
  print(text)
}

지금은 전달된 문자열을 기록하기 때문에, 제한(throttle)하는 동작을 볼수 있습니다.

이제 init()의 아래에 다음을 추가하세요.

_ = searchString
  .filter { $0!.characters.count > 3 }
  .throttle(seconds: 0.5)
  .observeNext {
    [unowned self] text in
    if let text = text {
      self.executeSearch(text)
    }
}

유효하지 않는(lenth <= 3) 값을 제외(exclude)하기 위해 searchString을 필터링 하고나서, 변경사항을 제한하여 0.5초마다 한번씩 알림을 정확히 수행하기 위해 throttle연산자를 Bond합니다. 이벤트가 발생할때, 검색을 완료하기 위해 executeSearch(_:)이 호출됩니다.

빌드하고 실행하고 나서 약간의 텍스트를 입력해보세요.

이벤트 처리를 제한(throttle)하는 곳에서, 4개의 문자라는 임계점이 되면 0.5초에 한번씩 기록되므로 다음과 비슷한 것을 보게 될것입니다.

Bond
Bond Th
Bond Throttles
Bond Throttles Good!

Bond없이 이런 종류의 기능을 추가하는데 오래 걸릴것입니다.

500px API 사용하기(Using the 500px API)

앱은 500px API를 사용하여 만들고 있습니다. 이것은 비교적 간단한 인터페이스와 인증 메카니즘을 가지고 있어서 선택하였습니다.

주의: 시작 앱에는 500px을 조회할때 필요한 코드를 이미 가지고 있습니다. Model 그룹에서 볼수 있습니다.

인터페이스를 사용하려면, 개발자 등록을 해야 합니다. 다행인것은 무료이고, 빠르며 쉽습니다.

다음 URL https://500px.com/signup을 통해서 가입하세요.

가입한 후에 앱을 보세요. : https://500px.com/settings/applications. 여기에서 앱을 등록할수 있는 인터페이스를 볼수 있습니다.


앱 등록(Register your application)을 선택하고 등록양식을 채워주세요(몇개 필드만 필수입니다). 앱은 바로 생성될것입니다.


앱 링크를 클릭하고 고객 키(consumer key)를 복사합니다. 이 키는 모든 요청과 함께 500px에 전달됩니다.

Info.plist를 열고 apiKey를 편집해서 500px에서 제공되는 고객 키를 추가하세요.


PhotoSearchViewModel.swift로 돌아가서 PhotoSearchViewModel에 다음에 오는 lazy 프로퍼티를 추가하세요.

private let searchService: PhotoSearch = {
  let apiKey = Bundle.main.object(forInfoDictionaryKey: "apiKey") as! String
  return PhotoSearch(key: apiKey)
}()

500px를 조회하는 Swift API를 제공하는 PhotoSearch클래스를 초기화합니다.

PhotoSearchViewModel.swift에서 executeSearch(_:) 메소드를 내용을 다음으로 업데이트합니다.

var query = PhotoQuery()
query.text = searchString.value ?? ""
 
searchService.findPhotos(query) {
  result in
  switch result {
  case .success(let photos):
    print("500px API returned \(photos.count) photos")
  case .error:
    print("Sad face :-(")
  }
}

조회 매개변수를 나타내는 PhotoQuery를 구성하고 나서, searchService으로 검색을 수행합니다. 결과는 비동기적으로 성공이나 실패를 나타내는 result 열거형을 반환됩니다.

앱을 빌드하고 실행하고 문자열을 입력하세요. 뷰 모델은 조회 할것이고 다음과 같이 기록된것을 볼수 있을것입니다.

500px API returned 20 photos

잘못된 경우에 다음을 보게될것입니다.

Sad face :-(

오류가 발생하면, API 키와 인터넷 연결을 다시 확인하며, 손가락을 바꿔서 다시 시도해 보세요. 여전히 동작되지 않으면, 500px API가 다운 된것 같습니다.

결과 표현하기(Rendering the Results)

검색으로 반환된 사진을 실제 볼수 있다면 더 흥미롭지 않을까요?

PhotoSearchViewModel.swift를 열고, PhotoSearchViewModel에 다음 프로퍼티를 추가하세요.

let searchResults = MutableObservableArray<Photo>([])

이름에서 알수있듯이, MutableObservableArray는 하나의 배열을 지원하는 옵져버의 특별한 타입입니다.

그것을 시도하기 전에, MutableObservableArray(과 ObservableArray)에 대해서 더 자세히 살펴 봅시다. 다양한 Bond API를 탐색하려면, cmd+click을 사용하세요.

ObservableArray는 Observable과 비슷합니다. 마찬가지로 신호(Signal)입니다. 이것은 배열이 변경될때 신호(Signal)로 이벤트를 구독할 수 있다는 의미입니다. 이 경우에, ObservableArrayEvent인스턴스 이벤트를 발행합니다.

ObservableArrayEvent는 배열에 발생한 변화을 ObservableArrayChange열거형을 이용하여 인코딩 합니다. ObservableArray에서 발행된 이벤트는 매우 상세합니다. 옵져버에 새로운 배열 값을 알리는 대신에, 발생한 변화를 대신 설명합니다.

이 수준의 세부사항에 대해 아주 좋은 이유가 있습니다. , Bond Observable를 사용해서 아마도 테이블 뷰를 통해서 UI에 배열을 바인딩 할 수 있습니다. 하지만, 배열에 단일 항목을 추가하면, 옵져버(Observable)은 무언가(*Something*) 변경된 것을 나타낼 뿐이고, 결과적으로 전체 UI를 다시 만들어야 합니다. MutableObservableArray에서 제공하는 세부정보를 사용하면 훨씬 더 효율적으로 UI 업데이트이 가능합니다

이제 옵져버(observable) 배열이 무엇인지 알았으니 사용할 시간입니다.

PhotoSearchViewModel.swift에 있는executeSearch(_:)로 이동하고 다음과 같은 성공(success) 케이스를 업데이트 하세요.

case .success(let photos):
  self.searchResults.removeAll()
  self.searchResults.insert(contentsOf: photos, at: 0)

배열이 지워지고, 새 결과가 추가됩니다.

PhotoSearchViewController.swift를 열고, bindViewModel()의 끝에 다음을 추가 하세요.

viewModel.searchResults.bind(to: resultsTable) { dataSource, indexPath, tableView in
  let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! PhotoTableViewCell
  let photo = dataSource[indexPath.row]
  cell.title.text = photo.title
 
  let backgroundQueue = DispatchQueue(label: "backgroundQueue",
                                      qos: .background,
                                      attributes: .concurrent,
                                      autoreleaseFrequency: .inherit,
                                      target: nil)
  cell.photo.image = nil
  backgroundQueue.async {
    if let imageData = try? Data(contentsOf: photo.url) {
      DispatchQueue.main.async() {
        cell.photo.image = UIImage(data: imageData)
      }
    }
  }
  return cell
}

Bond는 옵져버 배열을 테이블 뷰에 바인딩하기 위해 SignalProtocol에 대한 프로토콜 확장을 가집니다. searchResult를 resultsTable에 바인딩하기 위해 이것을 사용합니다. 클로져는 표준 테이블 뷰 데이터소스와 비슷한 역할을 합니다.

그 클로져는 표준 테이블뷰 셀과 연결합니다. 셀은 큐에서 빼오고 다양한 프로퍼티를 설정합니다. UI 반응은 유지하기 위해 백그라운드 큐를 통해서 이미지가 다운로드 된것을 알려줍니다.

앱을 빌드하고 실행해서 실제 작동하는지 보세요.


텍스트 필드가 업데이트 될때마다 자동으로 업데이트 됩니다. 매우 멋집니다.

UI 성향의 일부(A Bit of UI Flair)

더 많은 UI에 연결할 시간입니다.

PhotoSearchViewModel.swif에서 다음 프로퍼티를 추가하세요.

let searchInProgress = Observable<Bool>(false)

이 옵져버(Observable)는 검색이 진행중인것을 나타낼때 사용합니다. 그것을 사용하려면 executeSearch(_:)의 내용을 업데이트 하세요.

var query = PhotoQuery()
query.text = searchString.value ?? ""
 
searchInProgress.value = true
 
searchService.findPhotos(query) {
  [unowned self] result in
  self.searchInProgress.value = false
  switch result {
  case .success(let photos):
    self.searchResults.removeAll()
    self.searchResults.insert(contentsOf: photos, at: 0)
  case .error:
    print("Sad face :-(")
  }
}

이것은 500px에서 조회하기 전에 searchResultProgress.value를 true로 설정하고 나서, 결과가 반환될때 false를 반환합니다.

PhotoSearchViewController.swift에서 bindViewModel()의 아래에 다음을 추가하세요.

viewModel.searchInProgress
  .map { !$0 }.bind(to: activityIndicator.reactive.isHidden)
 
viewModel.searchInProgress
  .map { $0 ? CGFloat(0.5) : CGFloat(1.0) }
  .bind(to: resultsTable.reactive.alpha)

조회가 진행중일때 액티비티 인디게이터(activity indicator)를 보여주고 resultTable의 투명도를 감소합니다.

이 동작을 보기위해 빌드하고 실행합니다.


이제 Bond의 장점을 느끼기 시작해야 합니다. 마티니의 효과 입니다. :]

오류 처리하기(Handling Errors)

500px 조회가 실패하면, 앱은 콘솔에 기록합니다. 사용자에게 도움이 되고 건설적인 방식으로 모든 실패를 보고해야 합니다.

문제는, 어떻게 모델링 해야하는가? 오류가 뷰 모델 프로퍼티인 것처럼 느껴지지 않으며, 그것은 상태의 변화보다는 일시적인 사건입니다.

대답은 간단합니다. Observable 보다는, `PublishSubject만 필요할 뿐입니다. PhotoSearchViewModel.swift에서, 다음 프로퍼티를 추가하세요

let errorMessages = PublishSubject<String, NoError>()

다음으로, executeSearch(_:)의 Error케이스를 다음으로 업데이트하세요.

 case .error:
    self.errorMessages.next("There was an API request issue of some sort. Go ahead, hit me with that 1-star review!")

PhotoSearchViewController.swift에 bindViewModel의 아래에 다음을 추가하세요

_ = viewModel.errorMessages.observeNext {
   [unowned self] error in
 
  let alertController = UIAlertController(title: "Something went wrong :-(", message: error, preferredStyle: .alert)
  self.present(alertController, animated: true, completion: nil)
  let actionOk = UIAlertAction(title: "OK", style: .default,
    handler: { action in alertController.dismiss(animated: true, completion: nil) })
 
  alertController.addAction(actionOk)
}

errorMessages 프로퍼티에서 발행된 이벤트를 구독하며, UIAlertController를 통해 에러 메시지 출력을 제공합니다.

앱을 빌드하고 실행하고 나서, 오류 메시지를 보기 위해서 인터넷을 끊거나 고객 키를 제거합니다.


완벽합니다. :]

검색 설정 추가하기(Adding Search Settings)

현재 앱은 간단한 검색어를 기반으로 500px API를 조회합니다. API 문서를 끝까지(documentation for the API endpoint) 보면, 다른 여러 매개변수를 지원한다는 것을 알수 있습니다.

엡에서 Settings버튼을 탭하면, 검색을 구체화하기 위한 간단한 UI를 이미 가지고 있는것을 알수 있습니다. 이제 그것을 연결 시킬것입니다.

ViewModel 그룹에 PhotoSearchMetadataViewModel.swift이름으로 새 파일을 추가하고 다음 내용을 넣습니다.

import Foundation
import Bond
 
class PhotoSearchMetadataViewModel {
  let creativeCommons = Observable<Bool>(false)
  let dateFilter = Observable<Bool>(false)
  let minUploadDate = Observable<Date>(Date())
  let maxUploadDate = Observable<Date>(Date())
}

이 클래스는 설정화면을 되돌릴 것입니다. 화면의 각 설정과 관련된Observable프로퍼티가 있습니다.

PhotoSearchViewModel.swift에 다음 프로퍼티를 추가하세요

let searchMetadataViewModel = PhotoSearchMetadataViewModel()

executeSearch(_:)를 업데이트하고, query.text프로퍼티를 설정하는 줄 다음에 다음을 추가하세요.

query.creativeCommonsLicence = searchMetadataViewModel.creativeCommons.value
query.dateFilter = searchMetadataViewModel.dateFilter.value
query.minDate = searchMetadataViewModel.minUploadDate.value
query.maxDate = searchMetadataViewModel.maxUploadDate.value

이것은 단순히 PhotoQuery객체에 뷰 모델 상태를 복사합니다.

이 뷰 모델은 설정 뷰 컨트롤러에 바인딩됩니다. 이제 연결할 시간이기에, SettingViewController.swift를 열고 아울렛(outlets) 뒤에 다음 프로퍼티를 추가하세요.

var viewModel: PhotoSearchMetadataViewModel?

프로퍼티는 어떻게 가져오고 설정하나요? Settings버튼을 탭할때, 스토리보드의 세그웨이(segue)를 실행합니다. 뷰 컨트롤러에 데이터를 전달하기 위해 프로세스를 intercept 할 수 있습니다.

PhotoSearchViewController.swift에 다음 메소드를 추가하세요

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if segue.identifier == "ShowSettings" {
    let navVC = segue.destination as! UINavigationController
    let settingsVC = navVC.topViewController as! SettingsViewController
    settingsVC.viewModel = viewModel.searchMetadataViewModel
  }
}

ShowSettings 세그웨이가 실행할때 뷰 모델이 목적지(destination) 뷰 컨트롤러에 올바르게 설정되는 것을 보장합니다.

SettingsViewController.swift에 다음 메소드를 추가 하세요.

func bindViewModel() {
  guard let viewModel = viewModel else {
    return
  }
  viewModel.creativeCommons.bidirectionalBind(to: creativeCommonsSwitch.reactive.isOn)
}

이 코드는 viewModel을 creativeCommonsSwitch에 바인드 합니다

이제 viewDidLoad()의 끝에 다음을 추가하세요.

bindViewModel()

앱을 빌드하고 실행해서, 설정을 열고 Creative Commons 스위치를 켜세요. 상태가 계속 유지되는 것을 알수 있습니다. 만약 그것을 on하면, 여전히 on이고, 사진들은 다르게 반환됩니다.(새 요청을 강제적으로 하려면 검색 바를 다시 클랙해야 합니다)


날짜 바인딩하기(Binding the Dates)

설정 뷰에 연결이 필요한 몇가지 다른 컨트롤이 있고 그것은 다음 작업입니다.

SettingsViewController.swift에서 bindViewModel()에 다음을 추가하세요

viewModel.dateFilter.bidirectionalBind(to: filterDatesSwitch.reactive.isOn)
 
let opacity = viewModel.dateFilter.map { $0 ? CGFloat(1.0) : CGFloat(0.5) }
opacity.bind(to: minPickerCell.leftLabel.reactive.alpha)
opacity.bind(to: maxPickerCell.leftLabel.reactive.alpha)
opacity.bind(to: minPickerCell.rightLabel.reactive.alpha)
opacity.bind(to: maxPickerCell.rightLabel.reactive.alpha)

날짜 필터 스위치를 뷰 모델과 바인드하고, 사용하지 않을때 날짜 피커 셀(date picker cells)의 투명도를 줄입니다

날짜 피커 셀(date picker cells)은 날짜 피커(date picker)와 몇개의 라벨(labels)을 포함하며, CocoaPod의 DatePickerCell에서 구현이 제공됩니다. 하지만, 약간의 문제가 있습니다. Bond는 셀 타입에서 노출된 프로퍼티에 대해 바인딩을 제공하지 않습니다.

DatePickerCell에 대한 API를 살펴보면, 라벨(label)과 포함된 피커(picker) 모두를 설정하는 날짜 프로퍼티를 가지고 있는 것을 볼수 있을 것입니다. 피커(picker)에 양방향으로 바인드 할수 있지만, 이것은 라벨(label) setter 로직이 무시된다는 의미입니다.

다행히도 모델과 날짜 피커 모두 옵져버 하는 manual 양방향 바인딩은 아주 간단합니다.

SettingsViewController.swift에 다음 메소드를 추가하세요.

fileprivate func bind(_ modelDate: Observable<Date>, picker: DatePickerCell) {
  _ = modelDate.observeNext {
    event in
    picker.date = event
  }
 
  _ = picker.datePicker.reactive.date.observeNext {
    event in
    modelDate.value = event
  }
}

도우미 메소드는 Date와 DatePickerCell을 허용하고 둘 간에 바인딩을 생성합니다. modelDate는 picker.date에 바인딩되고 반대도 마찬가지 입니다.

이제 다음 두줄을 bindViewModel()에 추가하세요

bind(viewModel.minUploadDate, picker: minPickerCell)
bind(viewModel.maxUploadDate, picker: maxPickerCell)

새 도우미 메소드를 사용하여 피커(picker) 셀에 업로드 날짜를 바인딩 합니다.

빌드와 실행하고 기뻐하세요! 날짜가 정확히 바인딩되었습니다.


주의: 500px API는 날짜 필터를 지원하지 않습니다. 이것은 Model그룹에 있는 코드에 의해 반환된 사진에 적용됩니다. 결과적으로 500px에 의해 반환된 사진들 모두를 쉽게 필터링 할 수 있습니다. 더 나은 결과를 원하면, 서버쪽 날짜 필터링을 지원하는 Flickr와 통합을 시도해 보세요.

날짜 제약조건(Date Constraints)

현재, 사용자는 의미없는 날짜 필터를 만들수 있습니다. 예를들어, 최소 날짜가 최대 날짜 이후입니다.

이러한 제약조건을 적용하는 것은 정말로 쉽다. PhotoSearchMetadataViewModel.swift에서 초기화에 다음을 추가하세요.

init() {
  _ = maxUploadDate.observeNext {
    [unowned self]
    maxDate in
    if maxDate.timeIntervalSince(self.minUploadDate.value) < 0 {
      self.minUploadDate.value = maxDate
    }
  }
  _ = minUploadDate.observeNext {
    [unowned self]
    minDate in
    if minDate.timeIntervalSince(self.maxUploadDate.value) > 0 {
      self.maxUploadDate.value = minDate
    }
  }
}

위에서 단순히 각 데이터를 옵져버하고, 최소가 최대보다 큰(min > max) 상황인 경우에, 값을 적절하게 변경합니다.

빌드하고 실행해서 코드가 실제로 작동하는지 확인합니다. 최대 날짜를 최소 날짜보다 작게 설정해야 합니다. 최소날짜가 적절하게 변경될 것입니다.

작은 모순을 발견할지도 모릅니다. 검색 설정을 변경할때 앱이 조회를 반복하지 않습니다. 이것은 사용자에게 혼란을 줄 수 있습니다. 앱이 완벽하려면 설정을 하면 검색이 실행되야합니다.

각 설정은 옵져버 프로퍼티이며, 그것들 중 하나를 옵져버 할수 있습니다. 하지만, 많은 코드가 반복되어야 합니다. 다행이도 저 좋은 방법이 있습니다.

PhotoSearchviewModel.swif에서 init() 끝에 다음을 추가하세요.

_ = combineLatest(searchMetadataViewModel.dateFilter, searchMetadataViewModel.maxUploadDate,
                  searchMetadataViewModel.minUploadDate, searchMetadataViewModel.creativeCommons)
  .throttle(seconds: 0.5)
  .observeNext {
    [unowned self] _ in
    self.executeSearch(self.searchString.value!)
}

combineLatest함수는 여러개의 옵져버를 결합해서 하나처럼 취급할 수 있습니다. 위으 코드는 코드를 결합하여 조작한 후에 조회를 합니다.

빌드하고 실행해서 동작하는지 확인해 보세요. 날짜 또는 다른것을 변경해보세요. 변경될때마다 검색 결과가 업데이트 될것입니다.

얼마나 쉬웠나요? :]


반응형
Posted by 까칠코더
,