3์ 14์ผ (์)
- MVVM ํ๋ํ์ต
- RxCocoa - changed
- escaping closure๋ฅผ RxSwift๋ก ๋ฆฌํฉํ ๋งํด๋ณด๊ธฐ
- Coordinator ํจํด์ด๋?
ย
[3์ 14์ผ ํด์ฆ ์ค๋ต๋ ธํธ]
1์ด๋น 120ํ๋ ์์ผ๋ก ์คํ๋๋ ํ๋ก์ธ์ค์ด๋ฉฐ Constranints, Layout, Display์ 3๊ฐ Phase๋ก ๊ตฌ์ฑ๋๋ ๊ฒ์?
- Render Loop
๊ฐ์ฒด์งํฅ ํ๋ก๊ทธ๋๋ฐ OOP์ 4๋ ํน์ง
- ์บก์ํ, ์์, ๋คํ์ฑ, ์ถ์ํ
Render Loop์ ๋ค์ update Cycle์์ Constraints์ ๋ํ ๊ฐฑ์ ์ด ์ผ๊ด์ ์ผ๋ก ์ผ์ด๋๋๋ก ๋ณ๊ฒฝ์ฌํญ์ ์์ฝํ๋ ๋ฉ์๋๋?
- setNeedsUpdateConstraints
- ๋ค์ ์ ๋ฐ์ดํธ ์ฌ์ดํด์์ ์ ์ฝ์กฐ๊ฑด์ ๋ํ ๊ฐฑ์ ์ด ์ผ๊ด์ ์ผ๋ก ์ผ์ด๋๋๋ก ๋ณ๊ฒฝ์ฌํญ์ ์์ฝํ๋ค
์ค์ ๋ก ์ ๋ฐ์ดํธ ์คํ(์ฌ์ ์ํ ๋ฟ ์ง์ ํธ์ถํ๋ฉด ์๋๋ค!)
- updateConstraints
๋ค์ ์ ๋ฐ์ดํธ ์ฃผ๊ธฐ์ ์ ๋ฐ์ดํธ๊ฐ ํ์ํ ๊ฒ์ผ๋ก ๋ช ์์ ์ผ๋ก ํ์
- setNeedsUpdateConstraints
์ ๋ฐ์ดํธ๊ฐ ํ์ํ๋ฉด ์ฆ์ ์ ๋ฐ์ดํธ
- updateConstraintsIfNeeded
ํ์ฌ ๋ง์ด ์ฌ์ฉ๋๊ณ ์๋ HTTP์ ๋ฒ์ ์?
- 1.1 ๋ฒ์
- 1997๋ 1์์ ์ต์ด ๊ฐ๋ฐ๋ ํ์ค ํ๋กํ ์ฝ์ด๋ฉฐ ํ์ฌ๋ ๋ง์ด ์ฌ์ฉํ๊ณ ์๋ค.
์์ ํด๋์ค์์ ํ๋กํผํฐ๋ฅผ override ํ๋ ค๊ณ ํ๋๋ฐ, ์๋ฌ๋์ง ์๋ ๊ตฌ๋ฌธ์?
์ฌ๊ทํจ์๋ฅผ ์ฃผ์ํด์ ์ฌ์ฉํ์ง ๋ชปํ๋ฉด ์คํ ์ค๋ฒ ํ๋ก์ฐ๊ฐ ๋ฐ์ํ๋ค.
- ํจ์๋ ์คํ์์ญ์์ ์คํ๋๋๋ฐ ์ฌ๊ทํจ์๋ ์๊ธฐ์์ ์ ๋ฐ๋ณต์ ์ผ๋ก ํธ์ถํ๊ฒ ๋๋ค. ๋ฐ๋ผ์ ๋ฐ๋ณต์ ์ธ ํจ์ ํธ์ถ๋ก ์ธํ ์คํ ์ค๋ฒ ํ๋ก์ฐ๊ฐ ๋ฐ์ํ๋ค.
async await ๊ตฌ๋ฌธ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ
- async
- throws
- try
- await
[์จ๋์ ํจ๊ปํ๋ MVVM ์ค์ต!]
- ViewModel์ ๊ฒฝ์ฐ UIKit์ import ํ์ง ์๋ ๊ฒ์ด ์ค์ํ๋ค.
import Foundation
class YagomViewModel {
enum Color {
case red
case blue
}
private(set) var currentBackgroundColor: Color? {
didSet {
listener?()
}
}
private(set) var data: [String] = [] {
didSet {
reloadData?()
}
}
private var listener: (() -> Void)?
private var reloadData: (() -> Void)?
func bind(_ closure: @escaping () -> Void) {
listener = closure
}
func reloadDataBind(_ closure: @escaping () -> Void) {
reloadData = closure
}
func didTapRedButton() {
changedColor(color: .red)
}
func didTapblueButton() {
changedColor(color: .blue)
}
func didTapApiButton() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.data.append(contentsOf: ["์ผ", "๊ณฐ", "์", "์นด", "๋ฐ", "๋ฏธ"])
}
}
private func changedColor(color: Color) {
currentBackgroundColor = color
}
}
- View๋ ViewModel์๊ฒ ์ด๋ฒคํธ๋ง์ ์ ๋ฌํ๋ค.
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var redButton: UIButton!
@IBOutlet weak var blueButton: UIButton!
@IBOutlet weak var apiButton: UIButton!
@IBOutlet private weak var tableView: UITableView!
var viewModel = YagomViewModel()
override func viewDidLoad() {
super.viewDidLoad()
setUpBindings()
tableView.dataSource = self
}
// ๋ทฐ๋ชจ๋ธ๊ณผ ๋ทฐ๋ฅผ ๋ฐ์ธ๋ฉ
private func setUpBindings() {
viewModel.bind { [weak self] in
self?.view.backgroundColor = self?.viewModel.currentBackgroundColor == .red ? .systemRed : .systemBlue
}
viewModel.reloadDataBind { [weak self] in
self?.tableView.reloadData()
}
}
// ๋ทฐ๋ชจ๋ธ์๊ฒ ์ด๋ฒคํธ๋ง ์ ๋ฌ~
@IBAction func didTapRedButton(_ sender: UIButton) {
viewModel.didTapRedButton()
}
@IBAction func didTapblueButton(_ sender: UIButton) {
viewModel.didTapblueButton()
}
@IBAction func didTapApiButton(_ sender: UIButton) {
viewModel.didTapApiButton()
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") else {
return UITableViewCell()
}
cell.textLabel?.text = viewModel.data[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.data.count
}
}
- ๋ด ์ ๋ง๋๋ก ์ค๊ณํด๋ณธ MVVM ์ฝ๋...
- ์จ๋๋ Delegate ํจํด์ผ๋ก ๊ตฌํํด์ฃผ์ จ์ง๋ง ๋๋ Observable์ด ํธํด์ Observable ํํ๋ก ๊ตฌํํด๋ณด์๋ค.
- ๋ฐ์ดํฐ๋ฅผ ๊ด์ฐฐํ๋ฉด์, ๋ณํํ ๋๋ง๋ค ํน์ ํด๋ก์ ๋ฅผ ์คํํด์ค ํ๋กํผํฐ ์ต์ ๋ฒ(
currentBackgroundColor
,data
)๋ฅผ ์์ฑํ๊ณ , ํด๋น ํด๋ก์ ์ ๊ฐ์ ํ ๋นํด์ค ๋ฉ์๋(bind
,reloadDataBind
)๋ฅผ ๋ง๋ค์ด์ฃผ์๋ค. - ViewController์์ ViewModel์ ์์ฑํ๊ณ , ๋ฐ์ธ๋ฉ ์์
์ ํด์ฃผ์๋ค.
- ์ปฌ๋ฌ๊ฐ ๋ฐ๋ ๊ฒฝ์ฐ ๋ฐฐ๊ฒฝ์์ ๋ฐ๊พธ๋๋ก ํ๊ณ , ๋ฐฐ์ด์ด ๋ณํํ ๋๋ง๋ค reloadData() ๋ฉ์๋๋ฅผ ์คํํ๋๋ก ๋ฐ์ธ๋ฉ ํด์ฃผ์๋ค.
- ์ดํ ๊ฐ ๋ฒํผ๋ง๋ค ViewModel์๊ฒ ์ด๋ฒคํธ๋ฅผ ์ ๋ฌํ๋๋ก ViewModel์ ๋ฉ์๋๋ฅผ ํธ์ถํด์ฃผ์๋ค.
- TableView๋ ViewModel์ data๋ผ๋ ๋ฐฐ์ด๋ก ๊ตฌ์ฑํด์ฃผ์๋ค.
- ์ดํ ์คํํ๋ฉด ๊ฐ ๋ฒํผ์ ๋๋ฅผ๋, ViewModel์๊ฒ ์ด๋ฒคํธ๊ฐ ์ ๋ฌ๋๊ณ , ViewModel์์๋ ๊ฐ์ ๋ณ๊ฒฝํ๋ค. ๋ณ๊ฒฝ๋๋ฉด didSet์ ๋ฑ๋ก๋์ด์๋ ํด๋ก์ ๊ฐ ์คํ๋๋ค. ํด๋น ํด๋ก์ ๋ ViewController์์ ๋ฐ์ธ๋ฉ ์ฒ๋ฆฌํด์ค ์์ ๋ค์ด๋ค.
์จ๋ QnA
- MVVM์ ์~์ฐ๋ฉด ์ข๋ค.
- ์ ์ ๋ค ๋๋ถ๋ถ MVVM์ ์ ๋๋ก ์ฐ์ง ๋ชปํ๋ค. MVVM์ ์์ฐ๋์ง ์์๋ณด๊ณ ์ด์ ๋ฅผ ๊ฐ๊ณ ํ์ฉํ์.
- ๊ทธ๋ฆฌ๊ณ ViewModel์ด ViewModel์ค๋ฝ๊ฒ ์ฌ๋ฐ๋ฅธ ์ญํ ์ ํ๊ณ ์๋์ง ์ค์ํ๋ค.
- ํ
์คํธ๊ฐ ๊ฐ๋ฅํ๋๋ก ์ญํ ์ ์ ๋ถ๋ฆฌํ๋ ๊ฒ๋ ์ค์
- ํด๋ฆฐ ์ํคํ ์ฒ...๋ด์ฉ
๋๋์
- ๋๋ฌด ์ด๋ ต์ง๋ง... ๊ณ์ ๋ฐ๋ณตํ๋ค๋ณด๋ฉด ์ธ์ ๊ฐ ๊นจ๋ฌ์์ด ์ค๊ฒ ์ง...?
- ํด๋ฆฐ ์ํคํ ์ฒ๋ ์์ง๋ ์ด๋ ค์...
[Coordinator ํจํด]
- ํ๋ ์ด์์ ๋ทฐ ์ปจํธ๋กค๋ฌ๋ค์๊ฒ ์ง์๋ฅผ ๋ด๋ฆฌ๋ ๊ฐ์ฒด์ด๋ฉฐ, ์ฌ๊ธฐ์ ๋งํ๋ ์ง์๋ View์ ํธ๋์ง์ ์ ์๋ฏธํ๋ค.
- ์ฆ, Coordinator๋ ์ฑ ์ ๋ฐ์ ์์ด ํ๋ฉด ์ ํ ๋ฐ ๊ณ์ธต์ ๋ํ ํ๋ฆ์ ์ ์ดํ๋ ์ญํ ์ ํ๋ค.
- ํ๋ฉด ์ ํ์ ํ์ํ ์ธ์คํด์ค ์์ฑ(ViewController, ViewModel ...)
- ์์ฑํ ์ธ์คํด์ค์ ์ข ์์ฑ ์ฃผ์ (DI)
- ์์ฑ๋ ViewController์ ํ๋ฉด ์ ํ
- ViewController๊ฐ ๋ด๋นํ๋ ํ๋ฉด ์ ํ ์ฑ ์์ Coordinator๊ฐ ๋ด๋นํ๊ฒ๋๋ฉด์, ํ๋ฉด์ ํ ์ ViewController์์ ์ฌ์ฉํ ViewModel์ ํจ๊ป ์ฃผ์ ํด์ค ์ ์๋ค.
- ๋ํ ํ๋ฉด ์ ํ์ ๋ํ ์ฝ๋๋ฅผ ๋ฐ๋ก ๊ด๋ฆฌํ๊ฒ ๋๋ฉด์ ์ฌ์ฌ์ฉ๊ณผ ์ ์ง๋ณด์๋ฅผ ํธํ๊ฒ ๋ง๋ค์ด์ฃผ๊ธฐ ๋๋ฌธ์ ์ฃผ๋ก ์ฌ์ฉํ๋ค.
- ์ ๋ฆฌํ์๋ฉด Coordinator๋ ํ๋ฉด ์ ํ ์ ์ด ๋ด๋น๊ณผ ์์กด์ฑ ์ฃผ์ ์ ๊ฐ๋ฅํ๊ฒ ํด์ฃผ๋ ํ๋ธ๋ผ๊ณ ์๊ฐํ๋ฉด ๋ ๊ฒ ๊ฐ๋ค.
๋๋์
- ์ ์ผ๋จ ๋ง๋ค์ด๋ณด๊ธด ํ๋๋ฐ... ์ ๋๋ก ๋ง๋ ๊ฑด์ง ๋ชจ๋ฅด๊ฒ ๋ค... ์ค์ผ ์ด๋ ต์ง
- ๊ทธ๋๋ ๋ง๋ค๊ณ ๋๋๊น ํ๋ฉด์ ํ์ viewModel ๋จ์์ ํด๊ฒฐํ ์ ์์ด ๊ฐ์ฌ์์ง.
[UIAlertController๋ฅผ Rx์ค๋ฝ๊ฒ ๋ฆฌํฉํ ๋ง ํด๋ณด๊ธฐ]
func showActionSheet(
sourceView: UIView,
titles: (String, String),
topHandler: @escaping (UIAlertAction) -> Void,
bottomHandler: @escaping (UIAlertAction) -> Void
) {
let topAction = UIAlertAction(title: "Move to \(titles.0)", style: .default, handler: topHandler)
let bottomAction = UIAlertAction(title: "Move to \(titles.1)", style: .default, handler: bottomHandler)
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
alert.addAction(topAction)
alert.addAction(bottomAction)
if let popoverController = alert.popoverPresentationController {
popoverController.sourceView = sourceView
let rect = CGRect(x: .zero, y: .zero, width: sourceView.bounds.width, height: sourceView.bounds.height / 2)
popoverController.sourceRect = rect
popoverController.permittedArrowDirections = [.up, .down]
}
navigationController.topViewController?.present(alert, animated: true)
}
- ๋ผ์ด์ธํํ ์ฝ๋๋ฆฌ๋ทฐ ๋ฐ๊ณ ๋ ํ escaping ํด๋ก์ ๋ง ๋ณด๋ฉด... '์ ์ต์ ๋ฒ๋ธ ์ธ ์ ์์ ๊ฑฐ ๊ฐ์๋ฐ?' ๋ผ๋ ์๊ฐ์ ๋น ์ง๋ค.
- ์ค๋๋ ์ด๊น์์ด ์ต์ ๋ฒ๋ธ์ ์ธ ์ ์์ ๊ฒ ๊ฐ์์ ์ฐพ์๋ณด๋๊น... ์์ ์ฝ๋๋ค์ด ๋ง๊ธธ๋ ๋์ ํด๋ณด์๋ค.
- ๋ฐ๋ผ์ ์ ์ฝ๋๋ฅผ ์๋์ ๊ฐ์ด ์์ ํด๋ณด์๋ค.
enum ActionType: CaseIterable {
case top
case bottom
}
func showActionSheet(sourceView: UIView, titles: [String]) -> Observable<ProjectState> {
return Observable.create { observer in
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
ActionType.allCases.enumerated().forEach { index, _ in
let action = UIAlertAction(title: "Move to \(titles[index])", style: .default) { _ in
observer.onNext(ProjectState(rawValue: titles[index]) ?? ProjectState.todo)
observer.onCompleted()
}
alert.addAction(action)
}
if let popoverController = alert.popoverPresentationController {
popoverController.sourceView = sourceView
let rect = CGRect(
x: .zero,
y: .zero,
width: sourceView.bounds.width,
height: sourceView.bounds.height / 2
)
popoverController.sourceRect = rect
popoverController.permittedArrowDirections = [.up, .down]
}
self.navigationController.topViewController?.present(alert, animated: true)
return Disposables.create {
alert.dismiss(animated: true, completion: nil)
}
}
}
- ๋ญ๊ฐ ๋ง์ด ๋ฐ๋ ๊ฒ ๊ฐ์ง๋ง... ๋ณ๊ฑฐ์๋ค.
- ActionType์ด๋ผ๋ enum์ ๋ง๋ค๊ณ ํด๋น ์ผ์ด์ค๋ฅผ ๋ฐ๋ณตํ๋ฉด์ ํธ๋ค๋ฌ ๋ด๋ถ์ onNext๋ก ProjectState๋ผ๋ ๋ฐ์ดํฐ์ ํจ๊ป ์ด๋ฒคํธ๋ฅผ ์ ๋ฌํด์ค๋ค.
- ๋๋จธ์ง๋ iPad๋ฅผ ์ํ popover ์ค์ ...
showActionSheet(sourceView: cell, titles: project.status.excluded)
.subscribe(onNext: { state in
self.useCase.changedState(project, state: state)
}).disposed(by: disposeBag)
- ์ฌ์ฉํ ๋(๊ตฌ๋ )๋ onNext๋ก ์ ๋ฌ๋ฐ์ state๊ฐ์ผ๋ก project์ ์ํ๊ฐ์ ๋ฐ๊ฟ์ฃผ๋ ์์ ์ ํด์ฃผ์๋ค.
- ์ด๋ ํ๋ผ๋ฏธํฐ๋ก sourceView๋ฅผ ๋๊ฒจ์ฃผ๋ ์ด์ ๋ popover๋ฅผ ๋์ธ ์์น๋ฅผ ์ก๊ธฐ ์ํจ์ธ๋ฐ... ViewModel์ UIKit์ importํด์ผํด์ ๋ชน์ ๋ถํธํ๋ค..
- ์ด๋ถ๋ถ์ ๊ณ ๋ฏผํด๋ณด์์ง๋ง ์ข์ ๋ฐฉ๋ฒ์ด ๋ ์ค๋ฅด์ง๊ฐ ์์์ ๊ฐ์ ํ์ง ๋ชปํ๋ค.
[UI์ value๊ฐ ๋ณ๊ฒฝ๋์์ ๋๋ง ์ด๋ฒคํธ ๋ฐ๊ธฐ]
let input = DetailViewModel.Input(
didTapRightBarButton: rightBarButton.rx.tap.asObservable(),
didTapLeftBarButton: leftBarButton.rx.tap.asObservable(),
didChangeTitleText: titleTextField.rx.text.asObservable(),
didChangeDatePicker: datePicker.rx.date.asObservable(),
didChangeDescription: descriptionTextView.rx.text.asObservable())
didChangeTitleText: titleTextField.rx.text.changed.asObservable(),
didChangeDatePicker: datePicker.rx.date.changed.asObservable(),
didChangeDescription: descriptionTextView.rx.text.changed.asObservable()
)
- ์ฒ์์ ์์ ๊ฐ์ด ๋จ์ํ๊ฒ input์ ๋ง๋ค์ด์ฃผ์๋๋ฐ...
- ์ด๋ ๊ฒ ๋ง๋ค๋ค๋ณด๋ TextField์ ๊ฒฝ์ฐ ๊ฐ์ ์์ ํ์ง ์๊ณ tapํด์ ํ์ฑํ๋ง ํด๋ ์ด๋ฒคํธ๋ฅผ ์ ๋ฌ๋ฐ๋ ๊ฒ์ ํ์ธํ๋ค.
- ์ด๋ฌ๋ฉด ๊ฐ์ ๋ณ๊ฒฝํ์ง ์๊ณ modal์ ๋ซ์๋, ์ด๋ฒคํธ๋ฅผ ๋ฐ๊ณ ๊ฐ์ด ์์ ๋ ๊ฒ ๋ง๋ฅ ๋น๋ฌธ์์ด์ด ๋ค์ด์์ ๊ธฐ์กด ๋ฐ์ดํฐ๊ฐ ์ฌ๋ผ์ง๋... ๋ฒ๊ทธ๊ฐ ๋ฐ์ํ๋ค.
- ์๋ฌด๊ฒ๋ ์ํด๋.. Modal๋ง ๋์ฐ๊ณ ๋ซ์๋.. ๋น๋ฌธ์์ด ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ ๋ฐ์ดํฐ๊ฐ ์ง์์ง๋....๐ฅฒ
- ๊ตฌ๊ธ๋ง์ ํด๋ณด๋ changed๋ผ๋ ControlProperty๋ฅผ ์ฐพ๊ฒ ๋์๊ณ , ์๋์ ๊ฐ์ด ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋ ๋ง๋ค ์ด๋ฒคํธ๋ฅผ ์ ๋ฌํ๋ ์ต์ ๋ฒ๋ธ๋ก ๋ณ๊ฒฝํด์ฃผ์๋ค
let input = DetailViewModel.Input(
didTapRightBarButton: rightBarButton.rx.tap.asObservable(),
didTapLeftBarButton: leftBarButton.rx.tap.asObservable(),
didChangeTitleText: titleTextField.rx.text.changed.asObservable(), // changed
didChangeDatePicker: datePicker.rx.date.changed.asObservable(), // changed
didChangeDescription: descriptionTextView.rx.text.changed.asObservable() // changed
)
- ๊ทธ๋ฆฌ๊ณ output์ ์ค์ ํด์ค๋ ๋ ผ์ต์ ๋ ํ์ ์ผ๋ก ์ค์ ํด์ฃผ์๋๋ฐ, ์ต์ ๋ ํ์ ์ผ๋ก ๋ฐ๊ฟ์ฃผ๊ณ , nil์ผ ๊ฒฝ์ฐ ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํด์, ๊ฐ์ด ์์๋ก ๋ณ๊ฒฝ๋์ง ์๋๋ก ์ฒ๋ฆฌํด์ฃผ์๋ค.
- ์ด๋ ๊ฒ ํ๋๊น ๊ฐ์ ์์ ํ์ง ์์ผ๋ฉด ์ ์์ ์ผ๋ก ์์ ๋์ง ์์๊ณ , ํด๋น ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์๋ค.
- ์ฐธ๊ณ ๋งํฌ