View
-> ViewModel
바인딩과 ViewModel
-> View
바인딩을 통해 쌍방향으로 소통이 가능하다.
ViewModel
-> View
바인딩을 해보자!
class Clock {
static var currentTime: (() -> String) = {
let today = Date()
let hours = Calendar.current.component(.hour, from: today)
let minutes = Calendar.current.component(.minute, from: today)
let minStr = String(format: "%02d", minutes)
let seconds = Calendar.current.component(.second, from: today)
let secStr = String(format: "%02d", seconds)
return "\(hours):\(minStr):\(secStr)"
}
}
class ViewController: UIViewController {
// MARK: - UI Component
let titleLabel: UILabel = {
let label = UILabel()
label.text = "Clock"
label.font = UIFont.systemFont(ofSize: 20, weight: .bold)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let closureLabel: UILabel = {
let label = UILabel()
label.text = "closure"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let observableLabel: UILabel = {
let label = UILabel()
label.text = "observable"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
// MARK: - Properties
// 1. viewModel 생성 viewDidLoad()전이라서 메모리에만 올라간 상태
private let closureVM = ClosureViewModel()
// MARK: - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
setUI()
// 2. 바인딩 및 실행
setBindings()
startTimer()
}
// MARK: - Method
func setUI(){
view.backgroundColor = .systemBackground
view.addSubview(titleLabel)
view.addSubview(closureLabel)
view.addSubview(observableLabel)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 50),
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
closureLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 50),
closureLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
observableLabel.topAnchor.constraint(equalTo: closureLabel.bottomAnchor, constant: 50),
observableLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
// 4. viewModel의 checkTime함수 호출됨
// 6. 1초마다 값이 바뀌고 해당 내용 똑같이 진행됨
func startTimer() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.closureVM.checkTime()
}
}
// 3. 초기값으로 closureLabel.text 세팅, viewModel의 didChangeTime() 정의
func setBindings() {
closureVM.didChangeClosureTime = { [weak self] viewModel in
self?.closureLabel.text = viewModel.closureTime
}
}
}
class ClosureViewModel {
var didChangeClosureTime: ((ClosureViewModel) -> Void)?
var closureTime: String {
didSet {
didChangeClosureTime?(self)
}
}
init() {
closureTime = Clock.currentTime()
}
// 5. closureTime 값이 달라지고 didSet의 didChangeTime() 호출됨
func checkTime() {
closureTime = Clock.currentTime()
}
}
참고: https://ios-daniel-yang.tistory.com/59#article-2-2--closure%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
// 의존성 주입
let closureViewModel = ClosureViewModel()
let viewController = ViewController(closureVM: closureViewModel)
window?.rootViewController = viewController
window?.makeKeyAndVisible()
}
}
struct TodoModel {
let description: String
}
class ViewController: UIViewController {
// MARK: - UI Component
private let tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
return tableView
}()
private let closureButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("투두추가(클로저)", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 18)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(closureButtonTapped), for: .touchUpInside)
return button
}()
// MARK: - Properties
var closureViewModel: ClosureViewModelProtocol
// 0. 의존성 주입을 통해 ClosureViewModel 전달 (의존성 제거)
init(closureVM: ClosureViewModelProtocol) {
self.closureViewModel = closureVM
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
setUI()
setTableView()
setBindings()
}
// MARK: - Method
func setUI(){
view.backgroundColor = .systemBackground
view.addSubview(tableView)
view.addSubview(closureButton)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -80),
closureButton.topAnchor.constraint(equalTo: tableView.bottomAnchor, constant: 8),
closureButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
func setTableView(){
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
// MARK: - @objc
// 클로저이용 1. addButton을 누르면 뷰모델의 addTodo() 호출
@objc func closureButtonTapped() {
let alert = UIAlertController(title: "Add Todo", message: "Enter a new todo item", preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = "Todo item"
}
let addAction = UIAlertAction(title: "Add", style: .default) { [weak self] _ in
if let newTodo = alert.textFields?.first?.text {
self?.closureViewModel.addTodo(description: newTodo)
}
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alert.addAction(addAction)
alert.addAction(cancelAction)
present(alert, animated: true, completion: nil)
}
// 4. 뷰모델의 didChangedClosureViewModel 정의
func setBindings(){
closureViewModel.didChangedClosureTodo = { [weak self] viewModel in
self?.tableView.reloadData()
}
}
}
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
// 삭제도 똑같이 진행됨
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
closureViewModel.removeTodo(at: indexPath.row)
}
}
}
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return closureViewModel.todoCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
// 5. viewModel에서 바인딩 된 description을 textLabel의 text로 설정
cell.textLabel?.text = closureViewModel.todoDescription(indexPath.row)
return cell
}
}
protocol ClosureViewModelProtocol {
var didChangedClosureTodo: ((ClosureViewModel) -> Void)? { get set }
var todo: [TodoModel] { get set }
var todoCount: Int { get }
var todoDescription: (Int) -> String? { get }
func addTodo(description: String)
func removeTodo(at index: Int)
func todoDescription(at index: Int) -> String?
}
class ClosureViewModel: ClosureViewModelProtocol {
var didChangedClosureTodo: ((ClosureViewModel) -> Void)?
// 3. todo가 변하면 didChangedClosureViewModel()호출
var todo: [TodoModel] = [] {
didSet {
didChangedClosureTodo?(self)
}
}
var todoCount: Int {
return todo.count
}
var todoDescription: (Int) -> String? {
return { [weak self] index in
return self?.todoDescription(at: index)
}
}
// 2. addTodo가 호출되면 뷰모델의 todo가 변함
func addTodo(description: String){
todo.append(TodoModel(description: description))
}
func removeTodo(at index: Int) {
todo.remove(at: index)
}
func todoDescription(at index: Int) -> String? {
guard index >= 0, index < todo.count else {
return nil
}
return todo[index].description
}
}