๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐ŸŽ iOS/UIKit

[iOS] FLO ์•ฑ ๊ฐœ๋ฐœ ์ผ์ง€ #1. MVVM ํŒจํ„ด๊ณผ Data Binding

by Danna 2021. 6. 29.
728x90
728x90

ํ”„๋กœ๊ทธ๋ž˜๋จธ์Šค ๊ณผ์ œ๊ด€์— FLO ๋ฎค์ง ํ”Œ๋ ˆ์ด์–ด iOS ์•ฑ ์—ฐ์Šต ๊ณผ์ œ๊ฐ€ ์žˆ์–ด์„œ, ์ด๋ฅผ ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ์ ์šฉํ•œ ๊ฒƒ์ด๋‚˜ ๋ฐœ์ƒํ–ˆ๋˜ ์ด์Šˆ๋“ค์— ๋Œ€ํ•ด์„œ ์ •๋ฆฌํ•ด๋ณด๋„๋ก ํ• ๊ฒŒ์š”! ํ•ด๋‹น ์•ฑ์„ ๊ฐœ๋ฐœํ•˜๊ธฐ๋กœ ํ•œ ์ด์œ ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

1. ์ •ํ•ด์ง„ ์•ฑ์˜ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ, ํ™”๋ฉด ๊ตฌ์„ฑ ์š”์†Œ, ๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž์ถฐ ๊ฐœ๋ฐœํ•˜๊ธฐ ์œ„ํ•ด์„œ
2. ๋น„๊ต์  ์ž‘์€ ๊ทœ๋ชจ์˜ ์•ฑ์—์„œ MVVM ํŒจํ„ด์„ ์ ์šฉํ•ด๋ณด๊ธฐ ์œ„ํ•ด์„œ
3. ๋ถ€์ŠคํŠธ์ฝ”์Šค 1๊ฐ•์—์„œ ํ•™์Šตํ•œ ์Œ์•… ํ”Œ๋ ˆ์ด์–ด์˜ ์—ฐ์žฅ์„ ์œผ๋กœ ํ•™์Šตํ•˜๊ธฐ ์œ„ํ•ด์„œ

 

๐ŸŽต Demo ์˜์ƒ (iPhone 11, iOS14.5)

Demo images


 

โœ”๏ธŽ FLO ์•ฑ์˜ ์Œ์•… ์žฌ์ƒ ํ™”๋ฉด์—์„œ MVVM ํŒจํ„ด ์ ์šฉํ•˜๊ธฐ

๐Ÿ’ก MVVM ํŒจํ„ด์˜ ๊ธฐ๋ณธ ๋ฃฐ
1. View (View Controller)๋Š” View Model ์„ ๊ฐ€์ง€๊ณ , View Model ์€ Model ์„ ๊ฐ€์ง„๋‹ค.
2. View Model ์€ ์ž…์ถœ๋ ฅ์„ ์ฒ˜๋ฆฌํ•˜๊ณ  UI ๊ฐ€ ์š”๊ตฌํ•˜๋Š” ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์—ญํ• ๋งŒ ๊ฐ€์ง„๋‹ค.
3. View Model ์€ UI๋ฅผ ์ˆ˜์ •ํ•  ์ˆ˜ ์—†์œผ๋ฉฐ, UIKit ๋“ฑ์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ๋…๋ฆฝ์ ์ด๋‹ค.
* View ์™€ View Model ์‚ฌ์ด์— ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์„ ์•Œ๋ ค์ค„ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด Data binding ์ด๋‹ค.
* Data binding ์€ MVVM ํŒจํ„ด ๋ฟ ์•„๋‹ˆ๋ผ, ๋‹ค๋ฅธ ํŒจํ„ด์—์„œ๋„ ์ ์šฉ๋  ์ˆ˜ ์žˆ๋‹ค.

 

  • MVVM ํŒจํ„ด์€ ๊ธฐ์กด MVC ํŒจํ„ด์—์„œ ๊ฑฐ๋Œ€ํ•ด์ง„ ViewController ์˜ ์—ญํ• ์„ ๋œ์–ด์ค„ ์ˆ˜ ์žˆ๋Š” ํŒจํ„ด์ด๋‹ค.
  • ์Œ์•… ์žฌ์ƒ ํ™”๋ฉด์ธ Player View ๋Š” Player View Model ์„ ๊ฐ–๊ณ , Player View Model ์€ Music model ์„ ๊ฐ€์ง„๋‹ค.
  • Player View ์—์„œ ์ดˆ๊ธฐ UI ์™€ Player ๋ฅผ ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•ด Player View Model ์— ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ ์šฉํ•œ๋‹ค.
  • ์ด ๋•Œ, listener ์™€ bind ํ•จ์ˆ˜๋ฅผ ๊ฐ–๋Š” Observable ํด๋ž˜์Šค๋ฅผ ๊ตฌํ˜„ํ•ด ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ ์šฉํ–ˆ๋‹ค.
  • Player View ์—์„œ ์Œ์•… ์žฌ์ƒ ์‹œ๊ฐ„์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค UI ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•ด Music Player ์— ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ ์šฉํ•œ๋‹ค.
  • ์ด ๋•Œ, AVPlayer.addPeriodicTimeObserver(forInterval: queue: using:) ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•ด ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ ์šฉํ–ˆ๋‹ค.

 

MVVM ํŒจํ„ด์œผ๋กœ ๊ตฌ๋ถ„ํ•œ ์•ฑ์˜ ๊ตฌ์„ฑ

 

๐Ÿ’ก Observable ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•œ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ(Data Binding)

  • ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์€ ์•ฑ์˜ UI์™€ UI๊ฐ€ ํ‘œ์‹œํ•˜๋Š” ๋ฐ์ดํ„ฐ ์‚ฌ์ด๋ฅผ ์—ฐ๊ฒฐํ•ด ๋ฐ์ดํ„ฐ์˜ ๋ณ€๊ฒฝ์„ ์•Œ๋ ค์ค„ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.
  • ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด listener ์™€ bind ํ•จ์ˆ˜๋ฅผ ๊ฐ–๋Š” Observable ์ด๋ผ๋Š” ํด๋ž˜์Šค๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค.
  • ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ ์šฉํ•˜๊ณ  ์‹ถ์€ ํ”„๋กœํผํ‹ฐ๋ฅผ Observable ์ธ์Šคํ„ด์Šค๋กœ ๋งŒ๋“ค๊ณ , bind ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด listener ๋ฅผ ์ „๋‹ฌํ•˜๋ฉด ๋œ๋‹ค.
  • ์ดํ›„ Observable ์ธ์Šคํ„ด์Šค์˜ value ํ”„๋กœํผํ‹ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ๋งˆ๋‹ค listener ๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค.
  • ์—ฌ๊ธฐ์„œ listener ๋Š” value ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋˜๋Š” ํด๋กœ์ ธ์ด๋‹ค.
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
    }
}

 

โœ”๏ธŽ ์Œ์•… ์žฌ์ƒ ํ™”๋ฉด์˜ View - View Model ๊ฐ„ Data binding ์ ์šฉํ•˜๊ธฐ

  • ์Œ์•… ์žฌ์ƒ ํ™”๋ฉด์˜ ๋ทฐ์ธ Player View Controller ์™€ Player View Model ์˜ music ํ”„๋กœํผํ‹ฐ์— ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ ์šฉํ–ˆ๋‹ค.
  • viewModel ์˜ music ํ”„๋กœํผํ‹ฐ๋ฅผ Observable ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๋กœ ์ƒ์„ฑํ•œ๋‹ค.
  • viewController ์—์„œ bind ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด listener ๋ฅผ ์ „๋‹ฌํ•œ๋‹ค. -- ์ดˆ๊ธฐ UI ์„ค์ •, AVPlayer ์„ค์ • ์ž‘์—…์„ ํ•˜๋Š” ํด๋กœ์ ธ
  • viewModel.fetchMusic() ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋„คํŠธ์›Œํ‚น ์ž‘์—… ์™„๋ฃŒ ํ›„ model.value ๋ฅผ ๋ณ€๊ฒฝํ•œ๋‹ค. -- APIManager ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•จ.
  • model.value ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๊ฒŒ ๋˜๋ฉด ์œ„์˜ listener ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๊ณ , ์ง€์ •ํ•œ ์ž‘์—…๋“ค์ด ์ˆ˜ํ–‰๋˜๊ฒŒ ๋œ๋‹ค.


1) View Model ์—์„œ Observable ์ธ์Šคํ„ด์Šค ์ƒ์„ฑํ•˜๊ณ , value ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋Š” ๊ณผ์ •

class PlayerViewModel {
    
    // MARK: - Singleton
    static let shared = PlayerViewModel()

    // MARK: - Properties
    var apiManager: APIManger
    var music: Observable<Music>
    var imageData: Data
    
    // Other properties and methods ...
    
    // MARK: - Methods
    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()
            }
        }
    }
}


2) View Controller ์—์„œ bind ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ฉฐ listener ์ „๋‹ฌํ•˜๋Š” ๊ณผ์ •

class PlayerViewController: UIViewController {
    
    // MARK: - Properties
    var player = MusicPlayer.shared
    var viewModel = PlayerViewModel.shared
    var timeObserver: Any?
    
    // Other properties and methods ...
    
    // MARK: - View Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        addGestureLyricsLabel()
        bindViewModel()
    }
    
    // MARK: - Custom method
    func bindViewModel() {
        viewModel.music.bind({ (music) in
            DispatchQueue.main.async {
                self.initializeUI()
                self.initializePlayer()
            }
        })
        viewModel.fetchMusic()
    }
}

 

โœ”๏ธŽ ์Œ์•… ์žฌ์ƒ ํ™”๋ฉด์˜ View Controller ์™€ Music Player ๊ฐ„ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ์ ์šฉํ•˜๊ธฐ

  • ์Œ์•…์ด ์žฌ์ƒ๋จ์— ๋”ฐ๋ผ ํ˜„์žฌ ์žฌ์ƒ ์‹œ๊ฐ„, slider ์˜ ๊ฐ’ ๋ณ€๊ฒฝ, ๊ฐ€์‚ฌ ๋ณ€๊ฒฝ ๋“ฑ์˜ UI ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
  • Player View ์—์„œ ์Œ์•… ์žฌ์ƒ ์‹œ๊ฐ„์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค UI ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•ด Music Player ์— ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ ์šฉํ•œ๋‹ค.
  • ์ด ๋•Œ, AVPlayer.addPeriodicTimeObserver(forInterval: queue: using:) ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•ด ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ ์šฉํ–ˆ๋‹ค.


1) Music Player ํด๋ž˜์Šค์—์„œ Observer ๋ฅผ ๋“ฑ๋กํ•˜๊ณ  ํ•ด์ œํ• ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

class MusicPlayer {

    // MARK: - Singleton
    static let shared = MusicPlayer()
    
    // MARK: - Properties
    var player = AVPlayer()
    var isPlaying: Bool = false
    
    // Other properties and methods ...

    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)
    }
}



2) Player View Controller ์—์„œ ์œ„์˜ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด, ์‹œ๊ฐ„์ด ๋ณ€ํ• ๋•Œ๋งˆ๋‹ค ์ˆ˜ํ–‰ํ•  ํด๋กœ์ ธ๋ฅผ ์ „๋‹ฌํ•œ๋‹ค.
-- ์‹œ๊ฐ„์ด 1์ดˆ์”ฉ ๋ณ€ํ•  ๋•Œ๋งˆ๋‹ค ํ˜„์žฌ ์‹œ๊ฐ„, slider ๊ฐ’ ๋ณ€๊ฒฝ, ๊ฐ€์‚ฌ ๋ณ€๊ฒฝ ๋“ฑ UI ์—…๋ฐ์ดํŠธ๊ฐ€ ์ˆ˜ํ–‰๋œ๋‹ค.

class PlayerViewController: UIViewController {
    
    // MARK: - Properties
    var player = MusicPlayer.shared
    var viewModel = PlayerViewModel.shared
    var timeObserver: Any?

    // Other properties and methods ...
    
    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)
        }
    }
}

 

 

songda515/FLO

ํ”„๋กœ๊ทธ๋ž˜๋จธ์Šค ๊ณผ์ œ๊ด€ - FLO ์•ฑ ๊ฐœ๋ฐœ. Contribute to songda515/FLO development by creating an account on GitHub.

github.com



์ฐธ๊ณ  ๋งํฌ / ํ•™์Šต ์ž๋ฃŒ

๋‹ค์Œ ๊ฐœ๋ฐœ ์ผ์ง€

728x90
728x90