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 observableVM = ObservableViewModel()
// 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함수 호출됨
// 7. 1초마다 값이 바뀌고 해당 내용 똑같이 진행됨
func startTimer() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.observableVM.checkTime()
}
}
// 3-1. 초기값으로 observableLabel.text 세팅, bind함수를 통해 listner 클로저 정의
func setBindings() {
observableVM.observableTime.bind { [weak self] time in
self?.observableLabel.text = time
}
}
}
class ObservableViewModel {
// 만들어 놓은 Observable 객체를 원하는 초기 값으로 생성
var observableTime: Observable<String> = Observable(value: "Observable")
// 5. Observable타입의 observableTime.value가 현재시간으로 바뀜
func checkTime() {
observableTime.value = Clock.currentTime()
}
}
class Observable<T> {
// 6. value값이 바뀌면 listner 클로저가 실행됨
var value: T? {
didSet {
self.listner?(value)
}
}
init(value: T?) {
self.value = value
}
var listner: ((T?) -> Void)?
// 메서드(bind)대신 위의 클로저(listener)를 사용해도 되지만, 코드 정리를 위해 bind란 메서드를 만들어 줌, 보통 이 형태로 사용하는게 일반적
// 3-2. bind 실행 시, 클로저 안쪽의 동작들을 listner에 저장한다.
func bind(_ listener: @escaping (T?) -> Void) {
listener(value) // 생략 가능하지만 초기값을 갖기 위해 설정해줌
self.listner = listener
}
}
참고: 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
struct TodoModel {
let description: String
var isCompleted: Bool
}
class MainViewController: UIViewController {
// MARK: - Properties
private let mainView = MainView()
private let observableVM = ObservableViewModel()
// MARK: - Life Cycle
override func loadView() {
view = mainView
}
override func viewDidLoad() {
super.viewDidLoad()
setAddtarget()
}
// MARK: - Method
private func setAddtarget() {
mainView.goTodoButton.addTarget(self, action: #selector(goTodoButtonTapped), for: .touchUpInside)
mainView.goDoneButton.addTarget(self, action: #selector(goDoneButtonTapped), for: .touchUpInside)
}
// MARK: - @objc
// 1-1. 뷰컨 생성 시 프로토콜 타입 뷰모델 의존성 주입
@objc func goTodoButtonTapped() {
let todoVC = TodoViewController(observableVM: observableVM)
self.navigationController?.pushViewController(todoVC, animated: true)
}
@objc func goDoneButtonTapped() {
let doneVC = DoneViewController(viewModel: observableVM)
self.navigationController?.pushViewController(doneVC, animated: true)
}
}
class TodoViewController: UIViewController {
// MARK: - Properties
private let todoView = TodoView()
private let viewModel: ObservableVMProtocol
// 1-2. 의존성 주입을 통해 ClosureViewModel 전달 (의존성 제거)
init(observableVM: ObservableVMProtocol) {
self.viewModel = observableVM
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = todoView
}
// MARK: - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
setAddTarget()
setTableView()
setBindings()
}
// MARK: - Method
private func setAddTarget() {
todoView.addButton.addTarget(self, action: #selector(addButtonTapped), for: .touchUpInside)
}
private func setTableView() {
todoView.tableView.delegate = self
todoView.tableView.dataSource = self
todoView.tableView.register(TableViewCell.self, forCellReuseIdentifier: "customCell")
}
// 2. 뷰모델의 observableTodo.bind 정의
// 6. Observable.value값이 변하면 실행됨
private func setBindings(){
viewModel.observableTodo.bind { [weak self] todo in
self?.todoView.tableView.reloadData()
}
}
// MARK: - @objc
// 옵저버이용 3. addButton을 누르면 뷰모델의 addTodo() 호출
@objc func addButtonTapped() {
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?.viewModel.addTodo(description: newTodo, isCompleted: false)
}
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alert.addAction(addAction)
alert.addAction(cancelAction)
present(alert, animated: true, completion: nil)
}
}
// MARK: - UITableViewDelegate
extension TodoViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
viewModel.removeTodo(at: indexPath.row)
}
}
}
// MARK: - UITableViewDelegate
extension TodoViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.todoCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as? TableViewCell else {
return UITableViewCell()
}
cell.callBackMethod = { [weak self] in
self?.viewModel.toggleTodo(at: indexPath.row)
}
cell.todoLabel.text = viewModel.todoDescription(indexPath.row)
cell.checkButton.setImage(UIImage(systemName: viewModel.todoIsCompleted(indexPath.row)), for: .normal)
return cell
}
}
class DoneViewController: UIViewController {
// MARK: - Properties
private let doneView = DoneView()
private let viewModel: ObservableVMProtocol
init(viewModel: ObservableVMProtocol) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Life Cycle
override func loadView() {
view = doneView
}
override func viewDidLoad() {
super.viewDidLoad()
setTableView()
setBindings()
}
// MARK: - Method
private func setTableView() {
doneView.tableView.dataSource = self
doneView.tableView.register(TableViewCell.self, forCellReuseIdentifier: "customCell")
}
// doneList가 변하면 didSet호출, 실행 될 함수 정의
private func setBindings(){
viewModel.observableDone.bind { [weak self] done in
self?.doneView.tableView.reloadData()
}
}
}
// MARK: - UITableViewDataSource
extension DoneViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.doneCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as? TableViewCell else {
return UITableViewCell()
}
let description = viewModel.doneDescription(indexPath.row)
cell.callBackMethod = { [weak self] in
self?.viewModel.removeDone(description: description!)
}
cell.todoLabel.text = description
cell.checkButton.setImage(UIImage(systemName: viewModel.doneIsCompleted(indexPath.row)), for: .normal)
return cell
}
}
// MARK: - ObservableVMProtocol
protocol ObservableVMProtocol {
var observableTodo: Observable<[TodoModel]> { get }
var observableDone: Observable<[TodoModel]> { get }
var todoCount: Int { get }
var todoDescription: (Int) -> String? { get }
var todoIsCompleted: (Int) -> String { get }
var doneCount: Int { get }
var doneDescription: (Int) -> String? { get }
var doneIsCompleted: (Int) -> String { get }
func addTodo(description: String, isCompleted: Bool)
func removeTodo(at index: Int)
func toggleTodo(at index: Int)
func removeDone(description: String)
}
// MARK: - ObservableViewModel
class ObservableViewModel: ObservableVMProtocol {
var observableTodo: Observable<[TodoModel]> = Observable([])
var observableDone: Observable<[TodoModel]> {
return Observable(observableTodo.value.filter { $0.isCompleted })
}
// Todo 개수
var todoCount: Int {
return observableTodo.value.count
}
// Todo 내용
var todoDescription: (Int) -> String? {
return { [weak self] index in
return self?.todoDescription(at: index)
}
}
// Todo isCompleted 상태
var todoIsCompleted: (Int) -> String {
return { [weak self] index in
return self?.todoCompleted(at: index) ?? "defaultImageName"
}
}
var doneCount: Int {
return observableDone.value.count
}
var doneDescription: (Int) -> String? {
return { [weak self] index in
return self?.doneDescription(at:index)}
}
var doneIsCompleted: (Int) -> String {
return { [weak self] index in
return self?.doneCompleted(at: index) ?? "defaultImageName"
}
}
// 4. Observable의 value에 추가됨
// Todo 추가
func addTodo(description: String, isCompleted: Bool) {
let newTodo = TodoModel(description: description, isCompleted: isCompleted)
observableTodo.value.append(newTodo)
}
// Todo 삭제
func removeTodo(at index: Int) {
observableTodo.value.remove(at: index)
}
// Todo 토글
func toggleTodo(at index: Int) {
guard index >= 0, index < observableTodo.value.count else {
return
}
observableTodo.value[index].isCompleted.toggle()
}
// DoneTodo 삭제
func removeDone(description: String) {
if let index = observableTodo.value.firstIndex(where: { $0.description == description && $0.isCompleted }) {
observableTodo.value[index].isCompleted = false
}
}
// Todo 내용
func todoDescription(at index: Int) -> String? {
guard index >= 0, index < observableTodo.value.count else {
return nil
}
return observableTodo.value[index].description
}
// Todo isCompleted 상태
func todoCompleted(at index: Int) -> String {
guard index >= 0, index < observableTodo.value.count else {
return "defaultImageName"
}
let isCompleted = observableTodo.value[index].isCompleted
return isCompleted ? "chevron.down.circle.fill" : "chevron.down.circle"
}
// Done 내용
func doneDescription(at index: Int) -> String? {
guard index >= 0, index < observableDone.value.count else {
return nil
}
return observableDone.value[index].description
}
// Done isCompleted 상태
func doneCompleted(at index: Int) -> String {
guard index >= 0, index < observableDone.value.count else {
return "defaultImageName"
}
let isCompleted = observableDone.value[index].isCompleted
return isCompleted ? "chevron.down.circle.fill" : "chevron.down.circle"
}
}
class Observable<T> {
// 5. listner 호출됨
var value: T {
didSet {
self.listener?(value)
}
}
init(_ value: T) {
self.value = value
}
var listener: ((T) -> Void)?
func bind(_ listener: @escaping (T) -> Void) {
listener(value)
self.listener = listener
}
}
TodoVC todo 추가 시
viewModel의 addTodo() 실행 -> observer의 value가 변함 -> listen() 실행 -> TodoVC tableView reload
TodoVC에서 todo 삭제 시
viewModel의 removeTodo() 실행 -> observableTodo value 변경됨 (observableDone에도 있었다면 observableDone의 value도 변경됨) -> 각각 listen() 실행 -> 각각 tableView reload
TodoVC에서 버튼 토글시
viewModel의 toggleTodo() 실행 -> observableTodo value & observableDone의 value 변함 -> 각각 listen()실행 -> 각각 tableView reload
doneVC에서 버튼 토글시
viewModel의 removeDone() 실행 -> observableDone value 변경됨 -> listen() 실행 -> DoneVC tableView reload