์ฌ์ค ์๊ณ ์๊ณ ์ง์ ์ฌ์ฉํ๋ ์ํคํ ์ณ๋ MVC๊ฐ ์ ๋ถ์์ต๋๋ค. MVP, MVVM, VIPER ๋ฑ๋ฑ ์ฌ๋ฌ ์ํคํ ์ณ์ ๋ํด ๋ค์ด๋ง ๋ดค์ ๋ฟ ์ง์ ๊ณต๋ถํด๋ณด๊ณ ๋์ ํด๋ณด์ง๋ ์์์ต๋๋ค.
ํ์ง๋ง Naver Hackday๋ฅผ ํตํด ํ๋ก๊ทธ๋จ ๊ตฌ์กฐ์ ์ค์์ฑ์ ๊นจ๋ฌ์์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ํ๋ก๊ทธ๋จ์ ์๋ง๋ ๊ตฌ์กฐ๋ฅผ ์๊ฐํ๋ ํ์ ๊ธฐ๋ฅด๊ธฐ ์ํด ์ํคํ ์ณ์ ๋ํด ๊ณต๋ถํด๋ณด๊ณ ์ด๋ฅผ ์ ๋ฆฌํด๋ณด๋ ์๊ฐ์ ๊ฐ์ง๋ ค ํฉ๋๋ค.
iOS Architecture Patterns ๊ธ์ ๋ฐํ์ผ๋ก ์ฌ๋ฌ ๋ ํผ๋ฐ์ค๋ค์ ์ฐธ๊ณ ํ๋ฉฐ ์ง์์ ์ผ๋ก ์ ๋ฐ์ดํธํ ์์ ์ ๋๋ค.
์ฌ๋ฌ ์ํคํ ์ณ ํจํด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ๋ค์ด๊ฐ๊ธฐ ์ ์ด๋ฐ ์ํคํ ์ณ๊ฐ ํ๋ก๊ทธ๋จ์ ์์ฑํ๋๋ฐ ํ์ํ ์ด์ ์ ๊ทธ ํจ๊ณผ๋ค์ ๋จผ์ ๊ณต๋ถํด๋ณด๊ณ ์ ํฉ๋๋ค.
๊ตฌ์กฐ๋ฅผ ์๊ฐํ์ง ์๊ณ ํ๋ก๊ทธ๋จ์ ์์ฑํ๋ค๊ณ ํ๋ก๊ทธ๋จ์ด ๋์๊ฐ์ง ์๋ ๊ฒ์ ์๋๋๋ค. ํ์ง๋ง ์ด๋ฐ ํ๋ก๊ทธ๋จ์ ๊ฐ๋ ์ฑ์ ๋จ์ด์ง๋ฉฐ ์ ์ง ๋ณด์์ ๊ต์ฅํ ๋ง์ ๋น์ฉ์ด ๋ญ๋๋ค. ๋ํ ํ ์คํ ๋จ๊ณ์์๋ ํ ์คํ ์์ฒด๊ฐ ๊ฑฐ์ ๋ถ๊ฐ๋ฅํ๊ฑฐ๋ ํจ๊ณผ๋ฅผ ๋ณผ ์ ์๋ ๋๊ด์ ๋ด์ฐฉํ ์ ์์ต๋๋ค.
๋จ์ํ ๋ชจ๋(ํด๋์ค)์ ์ญํ ๋ณ๋ก ๋๋์ด ๊ด๋ฆฌํ๋ ๊ฒ์ ์ํคํ ์ณ๋ผ๊ณ ํ ์ ์์ต๋๋ค. ํน์ ๊ธฐ์ค์ผ๋ก ์ญํ ์ ์ ์ํ๋ฉฐ ์ด๋ ๊ฒ ์ญํ ๋ณ๋ก ๋๋์ด์ง ๋ชจ๋(ํด๋์ค)๊ฐ์ ๊ด๊ณ๋ฅผ ์ ๊ธฐ์ ์ผ๋ก ํ์ฑ์ํค๋ ๊ฒ์ด ์ํคํ ์ณ๋ผ ํ ์ ์์ต๋๋ค.
์ํคํ ์ณ์ ์ ๋ต์ ์์ต๋๋ค. ์ํคํ ์ณ๋ ๊ฐ ํ๋ก์ ํธ์ ์ฑ๊ฒฉ์ ๋ง๊ฒ ์ ํํด์ผ ํฉ๋๋ค. ํ์ง๋ง ๋ถ๋ช ์ข์ ์ํคํ ์ณ์ ๊ธฐ์ค๊ณผ ํน์ง์ ์กด์ฌํฉ๋๋ค.
- Balanced Distribution : ๊ฐ์ฒด๋ค์ ์ญํ ์ด ํ์คํ๋ฉฐ ์ด๋ฐ ์ญํ ๋ค์ด ๊ท ํ์กํ ๋ถ๋ฐฐ๋์ด ์๋์ง, ์ฆ ๊ฐ ๋ชจ๋(ํด๋์ค)์ด ๋
๋ฆฝ์ ์ธ์ง
- ์ด๋ฌํ ํ๊ณ ํ ์ญํ ์ ๋ถ๋ฐฐ๋ ํ๋ก๊ทธ๋จ์ ๋ณต์ก๋๋ฅผ ๋ฎ์ถ๋ค.
- ๊ฐ์ฒด์งํฅ์ 5์์น์ธ SOLID์ Single Responsibility์ ๊ธฐ๋ฐ
- ํ๋์ ๊ฐ์ฒด๋ ํ๋์ ์ญํ ๋ง์ ๊ฐ์ ธ์ผ ํ๋ค๋ ์์น
- ๋ชจ๋(ํด๋์ค)์ ๋ ๋ฆฝ์ฑ์ด ๋จ์ด์ง๋ฉด ํ ์คํ ์ ์งํํ๋๋ฐ ์ด๋ ค์์ด ์๋ค.
- Testability : ํ
์คํธ๋ฅผ ์งํํ ์ ์๋์ง
- ํ ์คํ ๊ณผ์ ์ ๋ฐํ์ ์ค ๋ฐ์ํ๋ ์ด์๋ฅผ ์ฌ์ ์ ์ฐพ์๋ด๊ธฐ ์ํด ํ์ํ ๋จ๊ณ
- ํ ์คํ ์ ์์ด์ ๊ทธ ์์ฒด๊ฐ ๋ฌธ์ ๋ผ๊ธฐ๋ณด๋ค๋ ํ ์คํ ์ ์งํํ๋ ค๋ ์ํคํ ์ณ๊ฐ ๋ฌธ์ ์ธ ๊ฒฝ์ฐ ๋ง๋ค.
- Easy of Use : ์ฌ์ฉํ๊ธฐ ์ฌ์ด์ง
- ์ฌ์ฉํ๊ธฐ ์ฌ์ด์ง๋ ๊ฐ๋ฐ ์๋์ ๊ด๊ณ๊ฐ ์์ ์ ์๋ค.
- Unidirectional Data Flow : ๋จ๋ฐฉํฅ์ฑ์ ๋ฐ์ดํฐ ํ๋ฆ
- ๋จ์ํ ๋ฐ์ดํฐ์ ํ๋ฆ์ ์ฝ๋๋ฅผ ์ฝ๊ฒ ์ดํดํ ์ ์๊ฒ ํด์ฃผ๋ฉฐ ์ฌ์ด ๋๋ฒ๊น ์ ์ ๊ณตํ๋ค. ์ฌ๋ฌ ๊ฐ์ฒด๋ค์ ์ค๊ฐ๋ ๋ฐ์ดํฐ์ ํ๋ฆ์ ์ณ์ง ์๋ค.
- Shared Resource์ ์ฌ์ฉ๋ ๊ธฐํผํด์ผํ๋ค. ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ์์ธ์ ์ฐพ๊ธฐ ํ๋ค์ด์ง๋ค.
๋ฌผ๋ก ์ด ์ธ ๊ฐ์ง์ ๊ธฐ์ค์ ์๋ฒฝํ๊ฒ ์ถฉ์กฑ์ํค๋ ์ํคํ ์ณ๋ ์์ต๋๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ์งํํ๊ณ ์๋ ํ๋ก์ ํธ์ ์ฑ๊ฒฉ์ ๋ง๊ฒ ์ ํ์ ์ผ๋ก ๋์ ํด์ผ ํฉ๋๋ค.
์์ Distribution์ ํฌ๊ฒ ์ธ ๊ฐ์ง์ ์นดํ ๊ณ ๋ฆฌ๋ก ๋๋์ด ์งํ๋ฉ๋๋ค.
- Model : ํ๋ก๊ทธ๋จ์์ ์ฌ์ฉ๋๋ ๋ฐ์ดํฐ์ ์กฐ์์ด ์ผ์ด๋๊ณ ์ด๋ฅผ ๋ด๋นํ๋ ๋ถ๋ถ
- View : ์๊ฐ์ ์ธ ๋ถ๋ถ์ผ๋ก UI์ ํด๋น. (iOS ํ๊ฒฝ์์๋ 'UI' ์ ๋์ด๊ฐ ๋ถ์ ๋ชจ๋ ๊ฒ๋ค์ด ์ด์ ํด๋น)
- Controller / Presenter / ViewModel : Model๊ณผ View ์ฌ์ด์ ์ค์ฌ์๋ก ์ผ๋ฐ์ ์ผ๋ก View๋ฅผ ํตํด ๋ฐ์ํ ์ฌ์ฉ์์ ์ก์ ์ ๋ค๋ฃจ๋ฉฐ ํ์์ ์ด์ ๋ฐ๋ฅธ Model์ ๊ฐ์ ์กฐ์ ์ ์์ฒญํ๋ฉฐ Model ๊ฐ์ ๋ณํ์ ๋ง๊ฒ View ๋ฅผ ๊ฐฑ์ ํ๋ ์ญํ
์ด๋ ๊ฒ ์ธ ๊ฐ์ง์ ๊ธฐ์ค์ผ๋ก ๋๋์ด Distribution์ ์งํํ๊ฒ ๋๋ค๋ฉด ์ฌ์ฌ์ฉ์ฑ์ด ์ฆ๊ฐํ๋ฉฐ ๊ทธ๋ค์ ๋ ๋ฆฝ์ ์ผ๋ก ํ ์คํ ํ ์ ์๊ฒ ๋ฉ๋๋ค.
๊ทธ๋ผ ์ด์ ๋ณธ์ น์ ์ผ๋ก ๋ง์ด ์ฌ์ฉ๋๊ณ ์ ๋ช ํ ์ํคํ ์ณ ํจํด๋ค์ ํ๋์ฉ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
- M : Model
- V : View
- C : Controller
๋จผ์ ์ดํด๋ณผ ์ํคํ ์ณ ํจํด์ ๋ฐ๋ก MVC์ ๋๋ค. ๊ฐ์ฅ ์ ๋ช ํ๋ฉฐ ์์ฐ์ค๋ฝ๊ฒ ์ฌ์ฉ๋๋ ์ํคํ ์ณ์ด๊ธฐ๋ ํฉ๋๋ค. ์ ๋ ๋ ๊ฐ์ง์ MVC ์ํคํ ์ณ๋ฅผ ์ดํด๋ณด๋ ค ํฉ๋๋ค. ์ ํต์ ์ธ MVC ์ํคํ ์ณ๋ถํฐ ์์ํ๊ฒ ์ต๋๋ค.
์์ ๋ค์ด์ด๊ทธ๋จ์ ํตํด ์ฐ๋ฆฌ๋ Model, View ๊ทธ๋ฆฌ๊ณ Controller, ์ด ์ธ ์์๊ฐ ์๋ก ๊ฐํ๊ฒ ์ฐ๊ฒฐ๋์ด ์์์ ์ ์ ์์ต๋๋ค. View๋ ์ฌ์ฉ์์ ์ก์ ์ Controller์๊ฒ ์ ๋ฌํ๊ณ Controller๋ ์ด์ ๋ฐ๋ฅธ ๋ฐ์ดํฐ์ ๊ฐฑ์ ์ Model์๊ฒ ์์ฒญํฉ๋๋ค. ์ด๋ ๊ฒ Model์์ ๋ฐ์ดํฐ์ ๊ฐฑ์ ์ด ์ผ์ด๋๊ณ Model์ ์ด๋ฐ ์ํ ๋ณํ๋ฅผ View์๊ฒ ์ ๋ฌํฉ๋๋ค. ๊ทธ๋ ๊ฒ ๋๋ฉด View ์ญ์ ๊ฐฑ์ ๋ ๋ฐ์ดํฐ์ ๋ง์ถ์ด ๊ฐฑ์ ๋ฉ๋๋ค.
์ด๋ ๊ฒ ๊ฐํ๊ฒ ์ฐ๊ฒฐ๋ ์ ์ ๋ ๋ฆฝ์ฑ์ด ๋ฎ๊ธฐ ๋๋ฌธ์ ์ด๋ค ๊ฐ๊ฐ์ ์ฌ์ฌ์ฉ์ฑ์ ๊ต์ฅํ ๋จ์ด์ง๋๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ํ์ฌ iOS ๊ฐ๋ฐ์๋ ์ ํต์ ์ธ MVC ์ํคํ ์ณ๋ ๋ง์ง ์๋ค๊ณ ๋ณผ ์ ์์ต๋๋ค.
๊ทธ๋์ ์ ํ์์๋ ์๋ก์ด MVC ์ํคํ ์ณ๋ฅผ ์ ์ํ์์ต๋๋ค. ์ด๋ฅผ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
์ ํ์ด ์ ์ํ Cocoa MVC์์ Controller๋ View์ Model์ ์ค์ฌ์๋ก View์ Model์ ์ง์ ์ ์ธ ์ฐ๊ฒฐ์ ๋ง์ต๋๋ค. ์ด๋ ์ ํต์ ์ธ MVC๋ณด๋ค ๋์ ๋ ๋ฆฝ์ฑ์ ๋ณด์ฅ์ ๊ธฐ๋ํฉ๋๋ค. ํ์ง๋ง ์ด๋ฌํ ๊ธฐ๋๊ฐ ์ค์ ๊ฐ๋ฐ์ ํฐ ํจ๊ณผ๋ฅผ ๊ฐ์ ธ์ฌ๊น์? ๋จผ์ Cocoa MVC ํจํด์ ๋ค์ด์ด๊ทธ๋จ์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
์์ ๋ค์ด์ด๊ทธ๋จ์ ์ผํ๋ณด๋ฉด View์ Model์ ๋ ๋ฆฝ์ฑ์ด ๋ณด์ฅ๋๋ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค. ์ค์ ๊ฐ๋ฐ์ ์ด๋ป๊ฒ ์ด๋ฃจ์ด์ง๊น์?
Cocoa MVC ์ํคํ
์ณ์์ Controller์ ์ญํ ์ UIViewController
๊ฐ ๋ด๋นํ๊ฒ ๋ฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ UIViewController
๋ View๋ฅผ ์์ ํ๊ฒ ๋๊ณ View๋ค์ Lify Cycle๊ณผ ๊ฐํ๊ฒ ์ฐ๊ฒฐ๋๊ฒ ๋ฉ๋๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ View์ Controller์ ๋ถ๋ฆฌ๊ฐ ์ฝ์ง ์์ผ๋ฉฐ Controller์ ์ฌ์ฌ์ฉ์ด ์ด๋ ค์์ง๊ณ ์ด๋ก์ธํด ์ฐ๊ด๋์ด ์๋ View์ ์ฌ์ฌ์ฉ ์ญ์ ์ด๋ ค์์ง๋๋ค.
์ด๋ ๊ฒ View์ Controller๊ฐ ๊ฐํ๊ฒ ์ฐ๊ฒฐ๋์ด ์๊ธฐ์ ํ
์คํ
์ ๊ณผ์ ์ญ์ ๊ต์ฅํ ํ๋ค์ด์ง๋๋ค. ๋
๋ฆฝ์ ์ด๋ผ๊ณ ๋งํ ์ ์๋ ๊ฒ์ Model์ด ์ ๋ถ์
๋๋ค. ๊ทธ๋ฆฌ๊ณ View ์์์์ ์ฌ์ฉ์์ ์ก์
๊ณผ ์ด์ ๋ฐ๋ฅธ ๋ฉ์๋๋ฟ๋ง ์๋๋ผ UIViewController
์์ ์ผ์ด๋๋ ๊ฐ์ข
ํ์๋ก (๋คํธ์ํฌ ํต์ , Delegation ๋ฑ) Controller๋ ๋ฐฉ๋ํด์ง๊ณ ์ด๋ฅผ ํํ Massive ViewController๋ผ๊ณ ๋ถ๋ฅด๊ธฐ๋ ํฉ๋๋ค.
๊ทธ๋์ ์ค์ ๋ค์ด์ด๊ทธ๋จ์ ๋ค์๊ณผ ๊ฐ์ ํ๋ฆ์ ๊ฐ๊ฒ ๋ฉ๋๋ค.
์ด๋ ๊ฒ ๋ฐฉ๋ํด์ง UIViewController
๋ฅผ ์ค์ด๋ ํ์, View Controller Offloading์ iOS ๊ฐ๋ฐ์๋ค์๊ฒ ์ค์ํ ๊ณผ์ ๊ฐ ๋์์ต๋๋ค. Cocoa MVC ์ํคํ
์ณ๋ฅผ ๊ตฌํํ ์ฝ๋๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
import UIKit
import PlaygroundSupport
struct Person { // Model
let firstName:String
let lastName:String
}
class GreetingViewController: UIViewController { // Controller
var person:Person!
// Views are belong to Controller => tightly COUPLED
lazy var showGreetingButton: UIButton = {
let button = UIButton()
button.setTitle("Click me", for: .normal)
button.setTitle("You badass", for: .highlighted)
button.setTitleColor(UIColor.white, for: .normal)
button.setTitleColor(UIColor.red, for: .highlighted)
button.addTarget(self, action: #selector(didTapButton(sender:)), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
var greetingLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor.white
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
self.setupLayout()
}
// Layout codes in Controller
func setupLayout() {
self.setupButton()
self.setupLabel()
}
private func setupButton() {
self.view.addSubview(showGreetingButton)
showGreetingButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
showGreetingButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
}
private func setupLabel() {
self.view.addSubview(greetingLabel)
greetingLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
greetingLabel.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -30).isActive = true
}
@objc func didTapButton(sender: UIButton) { // Update View
self.greetingLabel.text = "Hello " + self.person.firstName + " " + self.person.lastName
}
}
let model = Person(firstName: "Wasin", lastName: "Thonkaew")
let vc = GreetingViewController()
vc.person = model
PlaygroundPage.current.liveView = vc.view
์์ ์ฝ๋๋ฅผ ๋ณด๋ฉด View์ ์์ฑ๊ณผ ๋ฐฐ์น์ ๊ด๋ จ๋ ์ฝ๋๋ค๋ Controller์์ ์์นํ๊ฒ ๋๊ณ ์ด๋ค์ ๊ฐฑ์ ํ๋ ์ฝ๋ ์ญ์ Controller ์์ ์์นํ๊ฒ ๋ฉ๋๋ค. ์ฝ๋๋ก๋ง ๋ณด์๋ Controller์ View๊ฐ ๊ต์ฅํ ๊ฐํ๊ฒ ์ฐ๊ฒฐ๋์ด ์๋ค๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
๋ํ View์ ํ
์คํ
๊ณผ์ ์ญ์ Controller์ View Life Cycle ๊ด๋ จ ๋ฉ์๋(viewDidLoad
, viewWillAppear
๋ฑ)์ ํธ์ถ์ด ์๋ค๋ฉด ์งํํ ์ ์๊ธฐ ๋๋ฌธ์ ์ญ์ ์ด ๋์ด ๊ฐํ๊ฒ ์ฐ๊ฒฐ๋์ด ์๋ค๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๊ทธ๋ผ ์ฌ๊ธฐ์ MVC ์ํคํ ์ณ๋ ์์์ ์ธ๊ธํ๋ ์ข์ ์ํคํ ์ณ์ ๊ธฐ์ค๋ค์ ์ผ๋ง๋ ๋ถํฉํ๋์ง ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
- Distribution : View์ Model์ ํ์คํ ๋ถ๋ฆฌ๋์ด ์์ต๋๋ค. ํ์ง๋ง View์ Controller๋ ๊ฐํ๊ฒ ์ฐ๊ฒฐ๋์ด ์์ต๋๋ค.
- Testability : View์ Controller๊ฐ ๊ฐํ๊ฒ ์ฐ๊ฒฐ๋์ด ์๊ธฐ ๋๋ฌธ์ ์ค๋ก์ง Model๋ง ํ ์คํ ์ ์งํํ ์ ์์ต๋๋ค.
- Easy of Use : ์ฌ๋ฌ ์ํคํ ์ณ ์ค ๊ฐ์ฅ ์ ์ ์ฝ๋๋ฅผ ํ์๋ก ํ๋ฉฐ ๊ฐ์ฅ ์น์ํ ์ํคํ ์ณ ํจํด์ผ๋ก ๋ง์ ๊ฒฝํ์ด ์๋ ๊ฐ๋ฐ์๋ค๋ ์ฝ๊ฒ ์ ์ง ๋ณด์ํ ์ ์์ต๋๋ค.
๊ฐ๋ฐ ์งํ ์๋์ ์์ด์๋ ๊ฐ์ฅ ๋น ๋ฅธ ์ํคํ ์ณ ํจํด์ด๋ผํ ์ ์์ต๋๋ค.
iOS ๊ฐ๋ฐ์ ์์ด์ ์ํคํ ์ณ์ ํฌ๊ฒ ์ ๊ฒฝ์ ์ธ ์ ์๊ฑฐ๋ ์ง์์ด ์ ๋ฌดํ๋ค๋ฉด ๊ฐ์ฅ ์ฌ์ฉํ๊ธฐ ์ฌ์ด ํจํด์ด ๋ฐ๋ก MVC์ ๋๋ค. ํ์ง๋ง ์ด๋ ์์ฃผ ์์ ํ๋ก์ ํธ๋ผ ํ๋๋ผ๋ ๋ง์ ์ ์ง ๋ณด์ ๋น์ฉ์ด ๋ค์ด๊ฐ๊ฒ ๋ฉ๋๋ค.
- M : Model
- V : View (
UIView
๊ทธ๋ฆฌ๊ณ /ํน์UIViewController
) - P : Presenter
๋จผ์ ๋ค์ด์ด๊ทธ๋จ์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
์์์ ์ดํด๋ณธ Cocoa MVC์ ๊ต์ฅํ ๋น์ทํ ๋ชจ์ต์ ํ๊ณ ์๋ ๊ฑธ ํ์ธํ ์ ์์ต๋๋ค. ๊ทธ๋ฌ๋ฉด ์ค์ ๋ก๋ Cocoa MVC์ ์ ์ฌํ ๊น์? ์ ํ ๊ทธ๋ ์ง ์์ต๋๋ค.
๋จผ์ MVC์๋ ๋ค๋ฅด๊ฒ UIView
๋ UIViewController
๋ ๋ชจ๋ View์ ํด๋นํฉ๋๋ค. Cocoa MVC์์ UIViewController
๋ Controller์ ํด๋นํ์๊ณ ๊ทธ๋ก์ธํด View์ ๊ฐํ๊ฒ ์ฐ๊ฒฐ๋์ด ์์์ต๋๋ค. ์ด ๋์ View๋ก ๋ถ๋ฅํ๋ ๋์ MVP ํจํด์์๋ Presenter๋ผ๋ ๊ฒ์ด ๋ฑ์ฅํฉ๋๋ค.
Presenter๋ Cocoa MVC์๋ ๋ค๋ฅด๊ฒ View(UIView
, UIViewController
)์ Life Cycle์ ์ํฅ์ ๋ฐ์ง ์๊ณ ๋ ์ด์์ ์ฝ๋ ์ญ์ Presenter์ ์กด์ฌํ์ง ์์ต๋๋ค. ํ์ง๋ง ๋ณด๋ค Controller์ ์ญํ ๋ต๊ฒ View๋ฅผ ๋ฐ์ดํฐ์ ์ํ์ ๋ง์ถ์ด ๊ฐฑ์ ํ๋ ์ญํ ์ ๊ฐ๊ฒ ๋ฉ๋๋ค. ์ฆ Presenter๋ Model๋ก ๋ถํฐ ๊ฐฑ์ ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ ๋ทฐ๋ฅผ ๊ฐฑ์ ํ๋ ์ญํ ์ ํฉ๋๋ค.
์์์ ์ธ๊ธํ๋ฏ์ด Cocoa MVC์ ๋ค๋ฅด๊ฒ MVP ํจํด์์ UIViewController
์ ์ด๋ฅผ ์์๋ฐ๋ ํด๋์ค๋ค์ Presenter(Controller)๊ฐ ์๋๋ผ View์ ํด๋นํฉ๋๋ค. ์ด๋ ๋ณด๋ค ํ
์คํ
์ ํจ๊ณผ๋ฅผ ๋์ผ ์ ์์ต๋๋ค.
์ฝ๋๋ก ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
import UIKit
import PlaygroundSupport
struct Person { // Model
let firstName:String
let lastName:String
}
protocol GreetingView:class { // View Protocol
func setGreeting(greeting:String)
}
protocol GreetingViewPresenter { // Presenter Protocol
init(view: GreetingView, person: Person)
func showGreeting()
}
class GreetingPresenter : GreetingViewPresenter { // Presenter
weak var view: GreetingView?
let person: Person
required init(view: GreetingView, person: Person) {
self.view = view
self.person = person
}
// 3.
func showGreeting() { // Update View
let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
self.view?.setGreeting(greeting: greeting)
}
}
class GreetingViewController : UIViewController, GreetingView { // View
var presenter: GreetingViewPresenter!
...
// Properties
override func viewDidLoad() {
super.viewDidLoad()
self.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
setupLayout()
self.showGreetingButton.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
}
...
// Layout Code
// 2.
@objc func didTapButton(button: UIButton) {
self.presenter.showGreeting() // Send Action to Presenter
}
// 1.
func setGreeting(greeting: String) {
self.greetingLabel.text = greeting
}
// layout code goes here
}
// Present the view controller in the Live View window
// Assembling of MVP
let model = Person(firstName: "Wasin", lastName: "Thonkaew")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
PlaygroundPage.current.liveView = view
๋ค์ด์ด๊ทธ๋จ๊ณผ ์ฝ๋๋ฅผ ํตํด ์ดํด๋ณด๊ณ ๊ฐ์ผํ ๋ช ๊ฐ์ง๊ฐ ์กด์ฌํฉ๋๋ค.
๋จผ์ View๋ Presenter๋ฅผ ์์ ํ๊ณ ์์ด์ผ ํ๋ฉฐ Presenter๋ ์ ์ ์ก์ , ๋ฐ์ดํฐ ๊ฐฑ์ , ์ํ ๊ฐฑ์ ์ ๋ฐ๋ผ View๋ฅผ ๊ฐฑ์ ํด์ฃผ์ด์ผ ํฉ๋๋ค. ์ด๋ฅผ ์ฝ๋๋ก์จ ๊ตฌํํ ๋ View๋ Presenter๋ฅผ ๊ฐํ ์ฐธ์กฐ๋ก ์์ ํ๊ณ ์๊ณ Presenter๋ ์ฝํ ์ฐธ์กฐ๋ก View๋ฅผ ๋จ์ํ ๊ฐ๋ฆฌํค๊ณ ๋ง ์์ต๋๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ View์ Life Cycle์ ์ํฅ๊ณผ ๋ ์ด์์ ์ฝ๋์ ์ก์ ์ฝ๋๊ฐ ๊ณต์กดํ๋ ๋ฑ์ ์์กด์ฑ์์๋ ๋ฒ์ด๋ ์ ์์ง๋ง ์ฐธ์กฐ์ ์ํ 1:1 ์์กด์ฑ์์๋ ๋ฒ์ด๋ ์ ์๋ค๋ ํ๊ณ๊ฐ ์กด์ฌํฉ๋๋ค.
๋ค์์ผ๋ก๋ GreetingViewController
์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค. ์ด ๊ณณ์๋ ๋ ์ด์์๊ณผ ์ ์ ์ ์ก์
์ ์ ๋ฌํ๋ ์ฝ๋๋ง์ด ์์นํ๊ฒ ๋ฉ๋๋ค. ์ค์ ๋ก ํ๋ฆ์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
- ํ๋กํ ์ฝ ๋ฉ์๋๋ก ๋ทฐ๋ฅผ ๊ฐฑ์ ํ๋ ๋ฉ์๋๋ฅผ ์ ์ (ํธ์ถ์ด ์๋์ ๋ช ์ฌํ์.)
- View ์์ ์กด์ฌํ๋ ๋ฒํผ์
.touchUpInside
์ก์ ์ด ๋ค์ด์ค๋ฉด View๋didTapButton
๋ฉ์๋๋ฅผ ํตํด Presenter์ ์ด๋ฌํ ์ฌ์ค์ ์๋ฆฝ๋๋ค. - Presenter๋ ์ ์ ์ ์ก์ ์ ๋ํด Model๋ก๋ถํฐ ๊ฐ์ ๊ฐ์ ธ์ ๋ทฐ๋ฅผ ๊ฐฑ์ํ๋ ๋ฉ์๋๋ฅผ ํธ์ถ (ํธ์ถ์ด๋ผ๋ ํ์๋ Presenter์ ์ํด ํํด์ง๋ค.)
MVC์ ๋ง์ฐฌ๊ฐ์ง๋ก MVP๋ ์ข์ ์ํคํ ์ณ์ ๊ธฐ์ค์ ์ผ๋ง๋ ๋ถํฉํ๋์ง ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
- Distribution : ์ ํต์ ์ธ MVC์์ ๋ฐ์ํ Model๊ณผ View์ ์์กด์ฑ ๋ฌธ์ ๋ ํด๊ฒฐํ์๋ค. ํ์ง๋ง ์ฐธ์กฐ์ ์ํ View์ Controller์ ์์กด์ฑ์ ์กด์ฌํ์ง๋ง ๋น๊ต์ ์ ๋ชจ๋ ์ญํ ๋ณ๋ก ์ ์ ํ ๋๋์ด์ ธ ์๋ค๊ณ ๋งํ ์ ์์ต๋๋ค.
- Testability : ๊ฐ๊ฐ์ ์์๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ํ ์คํ ํ๊ธฐ ์ฉ์ดํฉ๋๋ค.
- Easy of Use : Presenter์ ์ถ๊ฐ์ ์ด๋ฅผ ๊ตฌํํ๊ธฐ ์ํ ํ๋กํ ์ฝ๋ฑ์ ์ถ๊ฐ๋ก ์ฝ๋๊ฐ MVC๋ณด๋ค ๊ธธ์ด์ง๋๋ค.
MVVM ํจํด์ RxSwift์ ๋ํ ๊ฒฝํ์ด ์๋ ๊ด๊ณ๋ก ๋ค๋ฅธ ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ์ง ์๊ณ MVVM์ ์๊ฐํ๊ณ ์๋ How not to get desperate with MVVM implementation์ ์ฐธ๊ณ ํ์ฌ ์์ฑํ์์ต๋๋ค.
- M : Model
- V : View
- VM : ViewModel
๋จผ์ ๋ค์ด์ด๊ทธ๋จ์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
MVVM์ ์ ์์ ์ํ๋ฉด View๋ ์ค์ง ์๊ฐ์ ์ธ ์์๋ก๋ง ์ด๋ฃจ์ด์ ธ์ผ ํฉ๋๋ค. View์์๋ ๋ ์ด์์, ์ ๋๋งค์ด์ ๊ทธ๋ฆฌ๊ณ UI ์์๋ค์ ๋ํ ์ด๊ธฐํ ์์ ์ฝ๋๋ค๋ง์ด ์์นํ๊ฒ ๋ฉ๋๋ค. MVVM์์ View์ Model ์ฌ์ด์ ViewModel์ด ์์นํ๊ฒ ๋ฉ๋๋ค. ViewModel์ View์ ๊ฐ UI ์์๋ค์ ๋ํ ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํฉ๋๋ค. View์ UI ์์๋ค๊ณผ ViewModel์ ์ธํฐํ์ด์ค๋ฅผ ์ฐ๊ฒฐ์ํค๋ ์์ ์ "๋ฐ์ธ๋ฉ(Binding)" ์ด๋ผ๊ณ ํฉ๋๋ค.
MVVM์์ View์ ๋น์ฆ๋์ค ๋ก์ง์ ViewModel์ ์ ์๋์ด ์์ผ๋ฉฐ ์ด์ ๋ง์ถฐ View๊ฐ ๊ฐฑ์ ๋ฉ๋๋ค. ์๋ฅผ๋ค์ด Date
๋ฅผ String
์ผ๋ก ๋ณํํ๋ ์์
์ ViewModel์์ ์งํ๋๊ณ View์์๋ ์ด์ ๋ง์ถฐ ๊ฐฑ์ ๋ง ์ผ์ด๋๊ฒ ๋ฉ๋๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ View๊ฐ ์ด๋ป๊ฒ ๊ตฌ์ฑ๋์ด ์๋์ง์ ์๊ด์์ด View์ ๋น์ฆ๋์ค ๋ก์ง์ ๋ํด์ ํ
์คํ
์ด ๊ฐ๋ฅํด์ง๋๋ค.
์ ์ฒด์ ์ธ ํ๋ฆ์ผ๋ก ๋ณด์์ ๋ ViewModel์ View๋ก๋ถํฐ ์ฌ์ฉ์์ ์ก์ ์ ๋ฐ์์ค๊ณ Model๋ก๋ถํด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ ์ด๋ ๊ฒ ๋ฐ์์จ ๋ฐ์ดํฐ๋ฅผ View์์ ๋ณด์ฌ์ค ๊ฐ(Ready-To-Display Property)์ผ๋ก ๊ฐ๊ณต์ ํฉ๋๋ค. ๊ทธ์ ๋์์ View๋ ViewModel์ ์ด๋ฌํ Ready-To-Display Property ๊ฐ์ observingํ๊ณ ์์ด ๊ฐ์ด ๊ฐฑ์ ๋๋ฉด ์ด์ ๋ง์ถฐ View๋ฅผ ๊ฐฑ์ ํ๊ฒ ๋ฉ๋๋ค.
MVP์ ๋ง์ฐฌ๊ฐ์ง๋ก UIView
์ UIViewController
๋ฅผ View๋ก ๋ฌถ์ด ๋ถ๋ฅํฉ๋๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ View์์๋ ๋ค์์ ์์
๋ค๋ง ํด์ฃผ๋ฉด ๋ฉ๋๋ค.
- Initiate/Layout/Present UI components.
- Bind UI components with the ViewModel.
๊ทธ๋ฆฌ๊ณ ViewModel์์๋ ๋ค์๊ณผ ๊ฐ์ ์์ ์ ํด์ฃผ๋ฉด ๋ฉ๋๋ค.
- Write controller logics such as pagination, error handling, etc.
- Write presentational logic, provide interfaces to the View.
๊ทธ๋ผ ์ด์ ์ด๋ฅผ ๊ตฌํํ ์ฝ๋๋ก ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค. ์ฝ๋๋ก ๋ฐ๋ก MVVM ์ํคํ ์ณ๋ฅผ ๊ตฌํํด๋ณด๋ ๊ฒ์ด ์๋ MVC ์ํคํ ์ณ๋ก ๋ง๋ค์ด์ง ํ๋ก์ ํธ๋ฅผ MVVM์ผ๋ก ๊ณ ์ณ๊ฐ๋ฉฐ ํ๋ํ๋ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค. ๋ง๋ค์ด ๋ณผ ์์ ๋ ๊ธฐ๋ณธ์ ์ธ ํ ์ด๋ธ ๋ทฐ์ ๊ทธ ์ ์ ์ธ๊ทธ๋ก ์ฐ๊ฒฐ๋๋ ๋ทฐ ์ปจํธ๋กค๋ฌ๋ก ๋์ด๊ฐ๋ ์ ๋์ ๊ฐ๋จํ ์์ค์ ๋๋ค.
์ฑ์ ์์ฑ๋ ๊ฒฐ๊ณผ๋ฅผ ๋งํฌ๋ฅผ ํตํด ๋จผ์ ํ์ธํด์ฃผ์ธ์.
MVC version
๋จผ์ MVC ์ํคํ ์ณ๋ก ๊ตฌํํ ๋ช๋ช ์ฝ๋๋ค์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค. ์ด ์ฝ๋๋ค์ ์๋นํ ๋ฏ์ ์ต์ ๊ฒ์ด๋ผ๊ณ ์์๋ฉ๋๋ค! (์ ๋ ๊ทธ๋ฌ๊ฑฐ๋ ์!)
์์ ์์ Model์ ๋ด๋นํ๋ Photo
๊ตฌ์กฐ์ฒด์
๋๋ค.
struct Photo {
let id: Int
let name: String
let description: String?
let created_at: Date
let image_url: String
let for_sale: Bool
let camera: String?
}
Model์ ์ฑ์์ค ๋ฐ์ดํฐ๋ ์์ ๋ด์ APIService
๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์์ ํ
์ด๋ธ ๋ทฐ์ ๋ฟ๋ ค์ฃผ๊ฒ ๋ฉ๋๋ค. ๊ทธ ์ฝ๋๋ ์๋์ ๊ฐ์ต๋๋ค. ํจ์น์ ํ์๊ฐ ์๋ฃ๋๋ฉด ํ
์ด๋ธ ๋ทฐ๋ฅผ reloadData()
ํด์ค์ผ๋ก์จ ์
์ ๋ฐ์ดํฐ์ ๋ง์ถ์ด ๊ฐฑ์ ํด์ฃผ๋ ์์
์
๋๋ค.
self?.activityIndicator.startAnimating()
self.tableView.alpha = 0.0
apiService.fetchPopularPhoto { [weak self] (success, photos, error) in DispatchQueue.main.async {
self?.photos = photos
self?.activityIndicator.stopAnimating()
self?.tableView.alpha = 1.0
self?.tableView.reloadData()
}
}
๊ทธ๋ฆฌ๊ณ UITableViewDataSource
ํ๋กํ ์ฝ ๋ฉ์๋ ์ญ์ ๋ค์๊ณผ ๊ฐ์ ๋ชจ์ต์ผ ๊ฒ์
๋๋ค.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// ....................
let photo = self.photos[indexPath.row]
//Wrap the date
let dateFormateer = DateFormatter()
dateFormateer.dateFormat = "yyyy-MM-dd"
cell.dateLabel.text = dateFormateer.string(from: photo.created_at)
//.....................
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.photos.count
}
์์ ๋ฉ์๋๋ ๋ทฐ ์ปจํธ๋กค๋ฌ์์ ์ ์ํด์ฃผ์๊ณ ํ๋ฉด์ ๋ฟ๋ ค์ฃผ๊ณ ์ด๋ฅผ ๊ฐ๊ณตํ๋ ์์ ๊น์ง ๋ชจ๋ ๋ทฐ ์ปจํธ๋กค๋ฌ์์ ์งํ๋๊ณ ์๋๊ฑธ ํ์ธํ์ค ์ ์์ต๋๋ค.
๋ง์ง๋ง์ผ๋ก ๋ค์์ UITableViewDelegate
ํ๋กํ ์ฝ ๋ฉ์๋๋ฅผ ๊ตฌํํ ๊ฒ์
๋๋ค.
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
let photo = self.photos[indexPath.row]
if photo.for_sale { // If item is for sale
self.selectedIndexPath = indexPath
return indexPath
}else { // If item is not for sale
let alert = UIAlertController(title: "Not for sale", message: "This item is not for sale", preferredStyle: .alert)
alert.addAction( UIAlertAction(title: "Ok", style: .cancel, handler: nil))
self.present(alert, animated: true, completion: nil)
return nil
}
}
์
์ ์ ํํ ์ฌ์ฉ์์ ์ก์
์ ๋ฐ๊ณ ์ ํํ ์
์ ๋ฐ๋ผ alert
๋ฅผ ๋์ด์ค์ง ๋ทฐ ์ปจํธ๋กค๋ฌ๋ก ๋์ด๊ฐ ๊ฒ์ธ์ง๋ฅผ ๊ฒฐ์ ํ๊ณ ์คํํ๋ ์ญํ ๊น์ง ๋ทฐ ์ปจํธ๋กค๋ฌ์ ํ๋์ ๋ฉ์๋์์์ ์งํ๋๊ณ ์์ต๋๋ค.
์ด ๋ฌธ์๋ฅผ ์์์๋ถํฐ ์ฝ์ด์ค์
จ๋ค๋ฉด ๋ฌด์ธ๊ฐ ๋๋ฌด ๊ฐํ๊ฒ ์ฐ๊ฒฐ๋์ด ์๋ค๋ ๊ฒ์ ๋๋ผ์ค ์ ์์ต๋๋ค. ์์ ์ฝ๋๋ค์ ๊ฐ๋ตํ๊ฒ ์๊ฐํ๋ ๋ถ๋ถ์์ ์ธ๊ธํ ๊ฒ๋ค๋ฟ๋ง ์๋๋ผ ๋ทฐ ์ปจํธ๋กค๋ฌ๋ APIService
์ ๋ํด ์์กด์ฑ ๋ฌธ์ ๋ฅผ ๊ฐ๊ณ ์์ต๋๋ค.
์ด๋ ๊ฒ ๋ง์ ๊ฒ๋ค์ด ๋ทฐ ์ปจํธ๋กค๋ฌ ๋ด์์ ๊ฐํ๊ฒ ์ฐ๊ฒฐ๋์ด ์๊ณ ์์กด์ฑ์ด ์กด์ฌํ๋ค๋ฉด ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ๊ฐ ๋งค์ฐ ๊น๋ค๋ก์์ง ๊ฒ์ด๊ณ ์ํ๋ ํ ์คํ ์ฑ๋ฅ์ ๋ฝ์๋ผ ์ ์์ ๊ฒ์ ๋๋ค. ๊ทธ๋ผ ์ด์ ์ด๋ค์ ๋ถ๋ฆฌํ์ฌ ๋ณด๋ค ํ ์คํ ์ ์ฉ์ดํ ์ ์๋ MVVM ์ํคํ ์ณ๋ก ์์ ํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
MVVM version
์์ ๋ฌธ์ ์ ๋ค์ ํด๊ฒฐํ๊ธฐ ์ํด์๋ ๊ฐ์ฅ ๋จผ์ ๋ทฐ ์ปจํธ๋กค๋ฌ์ ๋ถ๋ด์ ์ค์ฌ์ฃผ์ด์ผ ํฉ๋๋ค. ์ด๋ฅผ ์ํด ๋จผ์ ์์ ์์ ํ์ํ UI ์์๋ค์ ์ดํด๋ณด๊ณ ๊ทธ๋ค์ ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋ ์ด์์ ๋ก์ง์ ๋ถ๋ฆฌํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
์ด ์์ ์์๋ ๋ค์๊ณผ ๊ฐ์ด ์ธ ๊ฐ์ง์ UI ์์๊ฐ ์ฌ์ฉ๋ฉ๋๋ค.
- activityIndicator (Loading / Finish)
- tableView (Show / Hide)
- cells (title, description. created date)
์ด๋ค์ View์ ViewModel๋ก ๋๋ ๊ฒ์ ์ถ์ํํ๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋ค์ด์ด๊ทธ๋จ์ผ๋ก ํํ๋ ์ ์์ ๊ฒ์ ๋๋ค.
๊ฐ๊ฐ์ UI ์์๋ ViewModel์ ํ๋กํผํฐ์ ์ผ๋์ผ ๋์ํฉ๋๋ค. ๊ทธ๋ผ ์ด๋ฐ ๋ฐ์ธ๋ฉ์ ๊ตฌํํ๋ ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น์? ์ค์ํํธ์์๋ ์ด๋ฌํ ์์ ์ ๋ค์์ ๋ฐฉ๋ฒ๋ค๋ก ๊ตฌํํ ์ ์์ต๋๋ค.
- KVO (Key-Value Observing) ํจํด
- RxSwift๋ ReactiveCocoa๊ฐ์ FRP(Functional Reactive Programming) ๋ผ์ด๋ธ๋ฌ๋ฅผ ํ์ฉ.
- Delegation
- Property Observer
์ ๋ ์ฐธ๊ณ ํ๊ณ ์๋ ๋ธ๋ก๊ทธ์ ๊ธ์ ๋ฐ๋ผ Property Observer์ Closure๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌํํด๋ณด์์ต๋๋ค. ๋ชจ์์์ ์ฌ์ฉ ์ฉ๋๋ง์ ์ฝ๋๋ก ๊ฐ๋จํ ์ดํด๋ณด์๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
ViewModel
var prop: T {
didSet{ // Property Observer
self.propChanged?()
}
}
View
viewModel.propChanged = { [weak self] in
DispatchQueue.main.async {
// View์ ์
๋ฐ์ดํธ ์์
.
}
}
View์์ ViewModel์ ๋ฐ์ธ๋ฉ Closure๋ค์ ๊ตฌํํด์ค์ผ๋ก์จ View์ ๊ฐฑ์ ์ ๋ํ ์ฝ๋๋ฅผ ์ ์ํด์ฃผ๊ณ ๊ฐ์ ๋ณํ์ ๋ฐ๋ฅธ ๋ทฐ ๊ฐฑ์ ์ ํธ์ถํ๋ ํ์๋ ViewModel์ ์์นํ๊ฒ ๋ฉ๋๋ค. ์ฆ ๋ฐ์ดํฐ์ ๋ฐ๋ผ ๋ทฐ์ ๊ฐฑ์ ์ ๋ช ๋ นํ๋ ํ์๋ ViewModel์์ ์ด๋ฃจ์ด์ง๊ฒ ๋ฉ๋๋ค.
์ด๋ ๊ฒ ๋ฐ์ธ๋ฉ ๊ณผ์ ์ ํตํ๋ฉด ViewModel์ MVP์ Presenter์์ ํ๋กํ ์ฝ์ ํํ๋ก๋ผ๋ View์ ์กด์ฌ๋ฅผ ์๋ ๊ฒ๊ณผ๋ ๋ค๋ฅด๊ฒ ์ ํ View์ ๋ํ ์ด๋ ํ ์ฐธ์กฐ๋ ์กด์ฌํ์ง ์๊ฒ ๋ฉ๋๋ค.
์์ ์ ์ ์ฒด ์ฝ๋๋ ํ์ฌ ๋ฌธ์์ ๋์ผํ ๋ ํฌ์งํฐ๋ฆฌ์ ์์ผ๋ฏ๋ก ํด๋น ํด๋๋ฅผ ํ์ธํด์ฃผ์๊ธฐ ๋ฐ๋๋๋ค. ์ฌ๊ธฐ์ ์์ ๊ฐ์ ๋ฐฉ์์ ์ฝ๋๊ฐ ์ค์ ์ด๋ป๊ฒ ๊ตฌํ๋์๋์ง๋ฅผ ๊ฐ๋จํ๊ฒ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค. ํ ์ด๋ธ ๋ทฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ฟ๋ ค์ฃผ๊ธฐ ์ํ ๋ฐ์ธ๋ฉ Closure์ ํธ์ถ์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
ViewModel
let apiService: APIServiceProtocol
//MARK: Initializer
init( apiService: APIServiceProtocol = APIService()) {
self.apiService = apiService
}
...
// Activity Indicator
var isLoading: Bool = false {
didSet{
// notify
self.updateLoadingStatus?()
}
}
// Table View
private var cellViewModels:[PhotoListCellViewModel] = [PhotoListCellViewModel]() {
didSet{
// notify
self.reloadTableViewClosure?()
}
}
// Number of cells
var numberOfCells: Int {
return cellViewModels.count
}
//MARK: Binding Closures
var reloadTableViewClosure: (()->())?
var updateLoadingStatus: (()->())?
...
// Request Data
func requestFetchData(){
self.isLoading = true // trigger activity indicator startAnimating
apiService.fetchPopularPhoto { [weak self] (success, photos, error) in
// Compelete Fetching Data
self?.isLoading = false // trigger activity indicator stopAnimating
if let error = error {
self?.alertMessage = error.rawValue
}else {
self?.processFetchedPhoto(photos: photos)
}
}
}
// Generate cell's ViewModel
private func processFetchedPhoto( photos: [Photo] ) {
self.photos = photos // Cache
var viewModels = [PhotoListCellViewModel]() // TableViewCellViewModel
photos.forEach({viewModels.append(createCellViewModel(photo: $0))})
self.cellViewModels = viewModels // trigger photoListTableView reloadData
}
// Get Cell
func getCellViewModel( at indexPath: IndexPath ) -> PhotoListCellViewModel {
return cellViewModels[indexPath.row]
}
๊ฐ์ฅ ๋จผ์ APIService
๋ ๋ ์ด์ View(ViewController)์ ์์นํ์ง ์์ต๋๋ค. ๊ทธ๋ค์ ์ฝ๋์ ์ ์ฒด์ ์ธ ํ๋ฆ์ ์ดํด๋ณด์๋ฉด requestFetchData
๋ฉ์๋๊ฐ ํธ์ถ๋๋ฉด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ์ค๊ณผ ๋๋ ์ํฉ์์ isLoading
์ ์ ์ ํ ๊ฐ์ ํ ๋นํด์ฃผ์ด didSet
์ ํตํ View์ activityIndicator
ํ์๋ฅผ ์กฐ์ํด์ค ์ ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ํ์๊ฐ ์ ์์ ์ผ๋ก ์๋ฃ๋์๋ค๋ฉด Cell์ ViewModel์ ๋ง๋๋ ๊ณผ์ ์ ๊ฑฐ์ณ cellViewModels
์ ํ
์ด๋ธ ๋ทฐ ์์ ๋ฟ๋ ค์ค ๋ฐ์ดํฐ๊ฐ ํ ๋น๋ฉ๋๋ค. ์ด๋ ๊ฒ ๊ฐ์ด ํ ๋น๋๋ฉด ์ญ์ didSet
์ ํตํด tableView
์ reloadData
์์
์ด ์งํ๋๋ ๊ฒ์
๋๋ค. ๊ทธ๋ผ ์ด์ ๋ํ View์ ์ฝ๋๋ฅผ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
View
//MARK: Outlets
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
//MARK: ViewModel For TableView
lazy var viewModel: PhotoListViewModel = {
return PhotoListViewModel()
}()
//MARK: Life cycle
override func viewDidLoad() {
super.viewDidLoad()
...
initializeViewModel()
}
//MARK: Setup ViewModel
func initializeViewModel(){
...
viewModel.updateLoadingStatus = { [weak self] in
DispatchQueue.main.async {
let isLoading = self?.viewModel.isLoading ?? false
if isLoading {
self?.activityIndicator.startAnimating()
UIView.animate(withDuration: 0.2, animations: {
self?.tableView.alpha = 0
})
}else{
self?.activityIndicator.stopAnimating()
UIView.animate(withDuration: 0.2, animations: {
self?.tableView.alpha = 1
})
}
}
}
viewModel.reloadTableViewClosure = { [weak self] in
DispatchQueue.main.async {
self?.tableView.reloadData()
}
}
viewModel.requestFetchData()
}
//MARK: TableView DataSource
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "photoCellIdentifier", for: indexPath) as? PhotoListTableViewCell else {
fatalError("Cell not exists in storyboard")
}
// get data from cellViewModel
let cellVieWModel = viewModel.getCellViewModel(at: indexPath)
cell.setupViews(viewModel: cellVieWModel)
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.numberOfCells
}
initializeViewModel
๋ฉ์๋๋ฅผ ํตํด ViewModel์ ๋ฐ์ธ๋ฉ Closure๋ค์ ์ ์ํด์ฃผ๊ณ ๋ง์ง๋ง์ requestFetchData
๋ฉ์๋๋ฅผ ํธ์ถํจ์ผ๋ก์จ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ์์
์ ์์ํฉ๋๋ค.
UITableViewDataSource
ํ๋กํ ์ฝ ๋ฉ์๋๋ ์ญ์ ViewModel๋ก๋ถํฐ ๊ฐ์ ๋ฐ์์ ์ฌ์ฉํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
User Interaction์ ์ ์ฒด ์ฝ๋์์ ํ์ธํ์ค ์ ์์ต๋๋ค.
๊ทธ๋ฆฌํ์ฌ ์ ์ฒด์ ์ธ ๊ทธ๋ฆผ์ ๋ค์๊ณผ ๊ฐ์ ๊ฒ์ ๋๋ค.
MVP์์ ์ฐจ์ด์
์ ๊ฐ ๋๋ผ๊ธฐ์ ๊ฐ์ฅ ํฐ ์ฐจ์ด์ ์ Presenter๋ View์ ์ฐ๊ฒฐ์ฑ์ด ์ฝํ์ง๋ง ํ๋กํ ์ฝ๋ก์จ ๊ฐ์ ์ ์ผ๋ก ์ด๋ฅผ ์ฐธ์กฐํ๊ณ ์๊ณ ViewModel์ ๋ฐ์ธ๋ฉ ์์ ์ ํตํด ViewModel์์ View์ ๊ดํ ์ด๋ ํ ์์กด์ฑ์ด๋ ์ฐ๊ฒฐ์ฑ๋ ์กด์ฌํ์ง ์๋๋ค๋ ๊ฒ์ ๋๋ค.
MVVM์ด ๋ฌผ๋ก ์๋ฒฝํ๋ค๊ณ ํ ์ ์์ต๋๋ค. ๋ค์์ MVVM์ ๋จ์ ์ ์๊ฐํ๊ณ ์๋ ๊ธ๋ค์ ๋๋ค.
๋จ์ ์ค ํ๋๊ฐ ๋ฐ๋ก ์์์ ์ฝ๋๋ฅผ ์ ๊น ์ดํด๋ณด์๋ ๊ฒ๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก ViewModel์์ ๋๋ฌด ๋ง์ ์ผ๋ค์ ํ๋ค๋ ๊ฒ๋ ํ๋์ ๋ฌธ์ ์ ์ผ๋ก ์ง์ ๋๊ณค ํฉ๋๋ค. ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์ค์ ๋ก Builder๋ Router์ ๊ฐ๋ ์ด ๋์ ๋์์ต๋๋ค. ์ญ์ ๋ค์์ ๊ธ๋ค์ ์ฐธ๊ณ ํด์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.