개발을 시작하는 이야기

Data Binding (Observable) 본문

개발 이야기/Swift

Data Binding (Observable)

Teiresias 2022. 3. 26. 18:52

이전 MVVM 패턴을 정리한 글에서 MVVM은 데이터 바인딩을 필수적으로 요구한다고 적었는데 이번엔 데이터 바인딩에 대해 정리해보는 시간을 갖도록 하자.

 

데이터 바인딩의 개념은 쉽게 말해 Model과 UI 요소 간에 싱크를 맞춰주는 것이다. View와 로직이 분리되어 있어도 한 쪽이 바뀌면 다른 쪽도 업데이트가 이루어져 데이터의 일관성을 유지하는 것이다. iOS에서 데이터 바인딩을 하는 방법은 다음과 같다.

  • KVO
  • Delegation
  • Functional Reactive Programming
  • Property Observer

이중에 내가 사용했던 방식은 Functional Reactive Programming으로, 가장 쉽고 가장 널리 사용되는 방식이라고 한다. Bond 같은 라이브러리를 사용한다면 쉽게 바인딩 할 수 있지만, Observarble이라는 자체 Helper Class를 만드는 것이다. 이 클래스는 우리가 Observe하길 원하는 값으로 초기화되고, Binding 역할과 값을 얻어오는 역할을 하는 Bind라는 함수를 제공한다.

class Observable<T> {
    
    typealias Listener = (T) -> Void
    var listener: Listener?
    
    func bind(_ listener: Listener?) {
        self.listener = listener
        listener?(value)
    }
    
    var value: T {
        didSet {
            listener?(value)
        }
    }
    
    init(_ value: T) {
        self.value = value
    }
}

위 코드에서 listener는 값이 변할 때마다 호출되는 클로저이다.

 

그럼 이제 ViewModel을 살펴보자. 이제 View와 ViewModel간에 Data Binding을 적용할 차래이다. View인 PlayerViewController와 PlayerViewModel의 music 프로퍼티에 베이터 바인딩을 적용한다. ViewModel의 music 프로퍼티를 Observable 클래스의 인스턴스로 생성한다. ViewController에서 bind 함수를 통해 listener를 전달하고, ViewModel의 fetchMusic() 함수를 통해 네트워킹 작업 완료 후 model.value를 변경한다. model.value 값이 변경이 되면 위의 listener 함수가 호출되고, 지정한 작업들이 수행되게 된다.

class PlayerViewModel {
    static let shared = PlayerViewModel()

    var apiManager: APIManger
    var music: Observable<Music>
    var imageData: Data
    
    func fetchMusic() {
        apiManager.getMusic { (music) in
            print("fetch music ::", music.title)
            self.apiManager.loadImageData(url: music.image) { (image) in
                self.imageData = image
                self.music.value = music
                self.getLyrics()
            }
        }
    }
}

ViewModel에서 Observable 인스턴스를 생성하고, value가 변경된다.

class PlayerViewController: UIViewController {
    var player = MusicPlayer.shared
    var viewModel = PlayerViewModel.shared
    var timeObserver: Any?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        addGestureLyricsLabel()
        bindViewModel()
    }
    
    func bindViewModel() {
        viewModel.music.bind({ (music) in
            DispatchQueue.main.async {
                self.initializeUI()
                self.initializePlayer()
            }
        })
        viewModel.fetchMusic()
    }
}

ViewController에서 bind 를 통해 listener 전달하는 과정

 

이제는 ViewController와 Music Player간에 데이터 바인딩 적용해보자. 음악이 재생됨에 따라 현재 재생시간과 slider 값을 변경, 가사 변경 등의 UI 업데이트가 필요하다. PlayerView에 음악 재생 시간이 변경될 떄마다 UI를 업데이트 하기 위해서 Music Player에 데이터 바인딩을 적용한다. 이때  AVPlayer.addPeriodicTimeObserver(forInterval: queue: using:)를 활용하여 데이터 바인딩을 적용해주었다.

class MusicPlayer {
    static let shared = MusicPlayer()
    
    var player = AVPlayer()
    var isPlaying: Bool = false
    
    func addPeriodicTimeObserver(forInterval: CMTime, queue: DispatchQueue?, using: @escaping(CMTime) -> Void) -> Any {
        player.addPeriodicTimeObserver(forInterval: forInterval, queue: queue, using: using)
    }
    
    func removeTimeObserver(token: Any) {
        player.removeTimeObserver(token)
    }
}

Music Player 클래스에서 Observer를 등록하고 해제할수 있는 함수를 추가한다.

class PlayerViewController: UIViewController {
    
    var player = MusicPlayer.shared
    var viewModel = PlayerViewModel.shared
    var timeObserver: Any?
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        playPauseButton.isSelected = player.isPlaying
        addObserverToPlayer()
    }

    func addObserverToPlayer() {
        timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1), queue: DispatchQueue.main) { time in
            self.updateTime(time: time)
        }
    }
}

PlayerViewController에서 시간이 변할때마다 수행할 클로저를 전달한다. 매 초마다 재생시간, Slider값과 가사 변경 등 UI업데이트를 수행한다.

 

참고 : MVVM패턴과 Observable 클래스 구현에 대한 글

          프로그래머스 과제관 - 연습과제

        RealmAcademy

'개발 이야기 > Swift' 카테고리의 다른 글

[Error] Command Ld failed with a nonzero exit code  (0) 2022.03.28
UserDefaults  (0) 2022.03.27
MVVM(Model, View, ViewModel) 패턴  (0) 2022.03.25
MVC(Model, View, Controller) 패턴  (0) 2022.03.24
XIB를 활용하기  (0) 2022.03.23