Archive/패캠(올인원)

ch13 Todo 리스트 코드리뷰

lgvv 2021. 6. 26. 18:17

✅ 이번시간에는 Todo 리스트에 대해서 리뷰하도록 해볼게.

이게 금방 끝날줄 알았는데, 생각보다 배울게 많은 시간이었다. (모르는게 왜 점점 더 많아지지?)

🤦‍♂️ 깃에 대해 사용이 미숙한데 실수를 하는 바람에,, 깃허브를 다시 하^_^_^_^_^_

 

-> 깃허브 문제를 해결한 포스팅

https://rldd.tistory.com/117

 

🤦‍♂️ git 원격 저장소에 올라간 commit 되돌리기

이번에는 깃 원격 저장소에 올라간 commit 되돌리는 법에 대해서 알아보자. (문제) 깃허브에 수동으로 파일을 추가하였는데, 나중에 알고보니까 코드가 잘못되었었음. 따라서 깃 허브에서 다시 수

rldd.tistory.com

 

그럼 이제 다시 코드 리뷰를 시작해 보도록 할까

https://github.com/lgvv/fastCampus/tree/main/TodoList

 

lgvv/fastCampus

Contribute to lgvv/fastCampus development by creating an account on GitHub.

github.com

 

 

(목차)

1. ViewDidAppear 에는 인자값이 들어간다!

2. guard문에 대해서 다시금 짚고가자.

3. collectionView 섹션에 따라 다르게 구성하는 법

4. collectionView 섹션에 따른 아이템은 어떻게 구성할까?

5. Todo - MVVM 관점에서의 분석

6. Storage 파일에 대한 분석

 

 

✅ ViewDidAppear 에는 인자값이 들어간다!

    // viewDidLoad - 메모리에 올라왔을 때
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    // viewDidAppear - 화면에 보여질 때
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    }

보이는가..? viewDidAppear에는 animated라는 인자값이 들어간다. 크게 중요한건 아니지만, 에러를 하도 내서... 공식문서를 읽어보면 쉽게 이해할 수 있다.

 

<viewDidLoad>

https://developer.apple.com/documentation/uikit/uiviewcontroller/1621495-viewdidload

<ViewDidAppear>

https://developer.apple.com/documentation/uikit/uiviewcontroller/1621423-viewdidappear

 

 

 

✅ guard문에 대해서 다시금 짚고 가자.

guard let detail = inputTextField.text, detail.isEmpty == false else { return }

이런 코드가 있었다. guard문에 대해서는 매번 헷갈린다...

guard문은 let 안의 조건이 true 이면 지나가고, 그렇지 않으면 return 구문을 실행함으로써 사용 돼

프로그램의 죽는 상황을 막을 수 있어서 비교적으로 안전하게 사용할 수 있으며, if let과 비교되곤 한다.

이에 대한 더 이해는 참고자료로 남겨두도록 할게

자 그래서 저 코드를 해석해보면, inputTextField.text가 존재해서 text에 무엇이라도 입력되어 있다면! 스무스하게 넘어가고 그렇지 않다면 return 하라는 의미이다.

 

✅ collectionView 섹션에 따라 다르게 구성하는 법

 

   func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        // TODO: 섹션별 아이템 몇개
        if section == 0 {
            return todoListViewModel.todayTodos.count
        } else {
            return todoListViewModel.upcompingTodos.count
        }
    }

위의 코드에서 처럼 섹션에 따라 다르게 줄 수 있어. 

 

 

✅ collectionView 섹션에 따른 아이템은 어떻게 구성할까?

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        // TODO: 커스텀 셀
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TodoListCell", for: indexPath) as? TodoListCell else {
            return UICollectionViewCell()
        }
        
        var todo: Todo
        if indexPath.section == 0 {
            todo = todoListViewModel.todayTodos[indexPath.item]
        } else {
            todo = todoListViewModel.upcompingTodos[indexPath.item]
        }
        cell.updateUI(todo: todo)
        
        // TODO: todo 를 이용해서 updateUI
        // TODO: doneButtonHandler 작성
        // TODO: deleteButtonHandler 작성
        
        cell.doneButtonTapHandler = { isDone in
            todo.isDone = isDone
            self.todoListViewModel.updateTodo(todo)
            self.collectionView.reloadData()
        }
        
        cell.deleteButtonTapHandler = {
            self.todoListViewModel.deleteTodo(todo)
            self.collectionView.reloadData()
        }
        
        return cell
    }

MVVM 패턴이라서 var todo : Todo 타입인 것을 확인할 수 있어.

또한 뷰 모델에 따라서 todo에 서로 다른 정보를 부여함으로써, 섹션에 맞게 아이템을 배치하는 것을 확인할 수 있지.

MVC 패턴에서 cell.titleLabel 이런식으로 접근하는 것과는 다르게 우선 각 섹션에서 todo에 넣는 모습을 볼 수 있어.

💡여기서 의문!! todo는 배열이 아닌데, 어떻게 여러개의 아이템을 보여줄 수가 있는걸까?

-> 해답은 cellForItem 메소드는 셀의 갯수만큼 반복되서 실행 돼

즉, 셀이 10개면 이 함수 자체가 10번 실행되는거라고 생각하면 돼.

 

cell이 UI를 구성하는 부분을 한번 봐볼까?

    func updateUI(todo: Todo) {
        // TODO: 셀 업데이트 하기
        checkButton.isSelected = todo.isDone
        descriptionLabel.text = todo.detail
        descriptionLabel.alpha = todo.isDone ? 0.2 : 1 // 완료되었으면 투명 그렇지 않으면 불투명
        deleteButton.isHidden = todo.isDone == false
        showStrikeThrough(todo.isDone) // 레이아웃의 길이 조절

    }

updateUI는 이렇게 구성되었는다.

checkButton은 todo의 isDone을 통해 true, false를 확인한다.

descriptionLabel에 alpha를 줌으로써 isDone 상태에 따라 투명 불투명을 설정하게 한다.

deleteButton의 경우에는 코드가 조금 특이한데, 변수를 하나 따로 두어서 관리하는 방법도 있지만 굳이 그렇게 하지 않아도, todo.isDone이 true인 경우 선택했다는 의미라 삭제 버튼을 활성화 해야하는데, 불리언 변수를 사용하여 작성하였다.

즉, todo.isDone이 false와 같은경우 true를 반환 그렇지 않으면 false를 반환한다.

논리회로 시간에 배운 논리식을 떠올리면 쉽다.

마지막으로 showStrikeThrough는 레이아웃의 길이 조절하는 건데, 이건 다음 코드와 함께 보자.

 

showStrikeThrough 메소드에 대해서 알아보자.

   private func showStrikeThrough(_ show: Bool) {
        if show {
            strikeThroughWidth.constant = descriptionLabel.bounds.width
        } else {
            strikeThroughWidth.constant = 0
        }
    }

뷰를 하나 추가했다. 우측 이미지의 시뮬레이터에서 다음과 같이 작동한다.
showStrikeThrough에 대한 정보 추가!

 

⭐️ showStrikeThrough 메소드는 그냥 뷰를 하나 회색으로 꽉 채워서 하나의 선과 같은 효과를 연출한다. (나름 skill)

 

그럼 마지막으로 핸들러를 볼까?

이건 문법과 관련한 부분인데 아래 참고 부분에 우선 넣어두기도 했다

 var doneButtonTapHandler: ((Bool) -> Void)?
 var deleteButtonTapHandler: (() -> Void)?

핸들러에 대한 부분이다. 

completion Handler에 대한 부분은 꼭 참고를 보았으면 좋겠다. 이해가 훨씬 쉽다...!

 

 

✅ Todo - MVVM 관점에서의 분석

// TODO: Codable과 Equatable 추가
struct Todo: Codable, Equatable {
    let id: Int
    var isDone: Bool
    var detail: String
    var isToday: Bool
    
    mutating func update(isDone: Bool, detail: String, isToday: Bool) {
        // TODO: update 로직 추가
        self.isDone = isDone
        self.detail = detail
        self.isToday = isToday
        
    }
    
    static func == (lhs: Self, rhs: Self) -> Bool {
        // TODO: 동등 조건 추가
        return lhs.id == rhs.id
    }
}

나의 다른 포스팅에서 Equatable에 대해서 포스팅 했지만, 쉽게 말해서 스위프트에서 기본 타입이 아닌 구조체나 클래스의 인스턴스간의 값 비교를 위해선 내가 따로 정의해야 하는데 그에 필요한 것이다.

다음은 ⭐️Codable!! 이 친구는 내가 다른 프로젝트나 공부에서 많이 보았었는데, 제대로 이해가 어려웠다... 

아무튼 이번 시간에는 개념에 대해서 간략히 짚고 넘어가고 다음에는 직접 코드로 파싱하는 연습까지 해보도록 하자.

Codable이란, JSON형태로 있는 파일을 key를 변수명으로 삼아서, 우리가 손쉽게 사용할 수 있게 만들어주는 것이다.

다음은 mutating 키워드에 대해서 보자.

쉽게 말해서 구조체 내에서 데이터의 값을 수정해 주기 위해서 있어야 한다고 한다..!

 

마지막 func == 은 나의 포스팅을 참고하기 바람.

https://rldd.tistory.com/114

 

😂 ch13 swift Equatable?!

✅ 이번 시간에는 Equtable에 대해서 알아보도록 할게. Equatable이 뭐냐? 두 값이 동일한지 확인할 수 있는 프로토콜이야. 우리가 코딩을 하면서 "abc" == "abc" 혹은 33 != 33 등 스위프트의 기본 타입을

rldd.tistory.com

 

 

다음은 ViewModel에 대해서 보자

class TodoViewModel {
    
    enum Section: Int, CaseIterable {
        case today
        case upcoming
        
        var title: String {
            switch self {
            case .today: return "Today"
            default: return "Upcoming"
            }
        }
    }
    
    private let manager = TodoManager.shared
    
    var todos: [Todo] {
        return manager.todos
    }
    
    var todayTodos: [Todo] {
        return todos.filter { $0.isToday == true }
    }
    
    var upcompingTodos: [Todo] {
        return todos.filter { $0.isToday == false }
    }
    
    var numOfSection: Int {
        return Section.allCases.count
    }
    
    func addTodo(_ todo: Todo) {
        manager.addTodo(todo)
    }
    
    func deleteTodo(_ todo: Todo) {
        manager.deleteTodo(todo)
    }
    
    func updateTodo(_ todo: Todo) {
        manager.updateTodo(todo)
    }
    
    func loadTasks() {
        manager.retrieveTodo()
    }
}

CaseIterable은 enum 타입을 참조할때 순서대로 참조할 수 있는 메소드이다.

다음은 todoManager의 shared인데 싱글톤 패턴이다. 하나의 변수를 여러곳에서 공유해서 사용하는 것!

변수 todo를 참조할때는 todoManager에게 물어보게 설계되어 있다.

스위프트의 filter는 뒤의 조건에 맞는 것만 찾아서 뽑아오게 된다.

나머지 함수들은 manager에게서 받아오게끔 설계되어 있다.

 

그럼 manager에 대해서도 알아볼까?

class TodoManager {
    
    static let shared = TodoManager()
    
    static var lastId: Int = 0
    
    var todos: [Todo] = []
    
    func createTodo(detail: String, isToday: Bool) -> Todo {
        //TODO: create로직 추가
        let nextId = TodoManager.lastId + 1 // 새로운 아이디가 되겠다
        TodoManager.lastId = nextId
        return Todo(id: nextId, isDone: false, detail: detail, isToday: isToday)
    }
    
    func addTodo(_ todo: Todo) {
        //TODO: add로직 추가
        todos.append(todo)
        saveTodo()
    }
    
    func deleteTodo(_ todo: Todo) {
        //TODO: delete 로직 추가
        
        todos = todos.filter{ existingTodo in
            return existingTodo.id != todo.id
        } // filter는 일치하는 결과물만 반환
        saveTodo()
        
    }
    
    func updateTodo(_ todo: Todo) {
        //TODO: updatee 로직 추가
        guard let index = todos.firstIndex(of: todo) else { return } // 문법 firstIndex는 of: 로 주어진 값이 가장 첫번째로 나오는 것을 조회한다
        todos[index].update(isDone: todo.isDone, detail: todo.detail, isToday: todo.isToday)
        saveTodo()
        
    }
    
    func saveTodo() {
        Storage.store(todos, to: .documents, as: "todos.json")
    }
    
    func retrieveTodo() {
        todos = Storage.retrive("todos.json", from: .documents, as: [Todo].self) ?? []
        
        let lastId = todos.last?.id ?? 0
        TodoManager.lastId = lastId
    }
}

shared는 싱글톤 패턴으로 매니저 자신을 가르키고 있다.

lastId는 우리가 json 파일에 저장할때 아이디로 구분지을 건데, 그렇게 하기 위한 변수이다.

createTodo시 구조체 형태로 리턴한다.

createTodo와 add와의 차이는 뒤에 스토리지를 보면서 설명하자.

deleteTodo는 filter를 이용하여 현재 일치하는 결과만 반환함으로써 그 데이터를 삭제할 수 있다.

saveTodo 및 retreieveTodo는 스토리지에 저장하는데, 이건 스토리지와 함께 보도록 하자.

 

✅ Storage 파일에 대한 분석

강의에 Storage를 수업에서 따로 작성하지는 않았지만, 나는 그전에 공부를 해서 알고 있었음으로 내 지식을 다시 점검해보는 겸 다시 해보면서 쭉 읽어보자. 필요하면 구글링해서 써도 될듯..?

public class Storage {
    
    private init() { }
    
    // TODO: directory 설명
    // TODO: FileManager 설명
    enum Directory {
        case documents
        case caches
        
        var url: URL {
            let path: FileManager.SearchPathDirectory
            switch self {
            case .documents:
                path = .documentDirectory
            case .caches:
                path = .cachesDirectory
            }
            return FileManager.default.urls(for: path, in: .userDomainMask).first!
        }
    }
    
    // TODO: Codable 설명, JSON 타입 설명
    // TODO: Codable encode 설명
    // TODO: Data 타입은 파일 형태로 저장 가능
    
    static func store<T: Encodable>(_ obj: T, to directory: Directory, as fileName: String) {
        let url = directory.url.appendingPathComponent(fileName, isDirectory: false)
        print("---> save to here: \(url)")
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        
        do {
            let data = try encoder.encode(obj)
            if FileManager.default.fileExists(atPath: url.path) {
                try FileManager.default.removeItem(at: url)
            }
            FileManager.default.createFile(atPath: url.path, contents: data, attributes: nil)
        } catch let error {
            print("---> Failed to store msg: \(error.localizedDescription)")
        }
    }
    
    // TODO: 파일은 Data 타입형태로 읽을수 있음
    // TODO: Data 타입은 Codable decode 가능
    
    static func retrive<T: Decodable>(_ fileName: String, from directory: Directory, as type: T.Type) -> T? {
        let url = directory.url.appendingPathComponent(fileName, isDirectory: false)
        guard FileManager.default.fileExists(atPath: url.path) else { return nil }
        guard let data = FileManager.default.contents(atPath: url.path) else { return nil }
        
        let decoder = JSONDecoder()
        
        do {
            let model = try decoder.decode(type, from: data)
            return model
        } catch let error {
            print("---> Failed to decode msg: \(error.localizedDescription)")
            return nil
        }
    }
    
    static func remove(_ fileName: String, from directory: Directory) {
        let url = directory.url.appendingPathComponent(fileName, isDirectory: false)
        guard FileManager.default.fileExists(atPath: url.path) else { return }
        
        do {
            try FileManager.default.removeItem(at: url)
        } catch let error {
            print("---> Failed to remove msg: \(error.localizedDescription)")
        }
    }
    
    static func clear(_ directory: Directory) {
        let url = directory.url
        do {
            let contents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
            for content in contents {
                try FileManager.default.removeItem(at: content)
            }
        } catch {
            print("---> Failed to clear directory ms: \(error.localizedDescription)")
        }
    }
}

// MARK: TEST 용
extension Storage {
    static func saveTodo(_ obj: Todo, fileName: String) {
        let url = Directory.documents.url.appendingPathComponent(fileName, isDirectory: false)
        print("---> [TEST] save to here: \(url)")
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        
        do {
            let data = try encoder.encode(obj)
            if FileManager.default.fileExists(atPath: url.path) {
                try FileManager.default.removeItem(at: url)
            }
            FileManager.default.createFile(atPath: url.path, contents: data, attributes: nil)
        } catch let error {
            print("---> Failed to store msg: \(error.localizedDescription)")
        }
    }
    
    static func restoreTodo(_ fileName: String) -> Todo? {
        let url = Directory.documents.url.appendingPathComponent(fileName, isDirectory: false)
        guard FileManager.default.fileExists(atPath: url.path) else { return nil }
        guard let data = FileManager.default.contents(atPath: url.path) else { return nil }
        
        let decoder = JSONDecoder()
        
        do {
            let model = try decoder.decode(Todo.self, from: data)
            return model
        } catch let error {
            print("---> Failed to decode msg: \(error.localizedDescription)")
            return nil
        }
    }
}

 

 

 

 

- 참고

https://velog.io/@dev-lena/guard-let%EA%B3%BC-if-let%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90

 

guard let과 if let의 차이점

guard let과 if let의 차이점

velog.io

https://duwjdtn11.tistory.com/520

 

[iOS] Completion Handler

Completion Handler 본 문서에는 평소에 공부를 진행하며 한번 정리가 필요하다고 생각했던 Completion Handler 에 대한 내용을 기재한다. Prerequisite Completion Handler 개념은 알면 알수록 어려운 개념이다....

duwjdtn11.tistory.com

https://devmjun.github.io/archive/Mutating_Struct

 

Swift. 구조체 Mutating 정리

 

devmjun.github.io

https://www.swiftbysundell.com/articles/enum-iterations-in-swift-42/

 

Enum iterations in Swift | Swift by Sundell

With each new release, Swift keeps getting better and better at creating compiler-generated implementations of common boilerplate. One such new feature in Swift 4.2 is the new CaseIterable protocol - that enables us to tell the compiler to automatically sy

www.swiftbysundell.com