TIL 16

오늘 한 일 / 배운점

  • 개인 과제 Lv.2

과제

  • 개인 과제 Lv.3

느낀점

어제에 이어서 var toDoTasks = [String]() 배열에 값을 저장하던것을 좀 고쳐보려고 했다

  1. String 대신 ToDoTask라는 구조체를 만들어서 그 구조체를 타입으로 가지는 배열로 변경

    import Foundation
    
    struct ToDoTask {
        var id: UUID // UUID로 고유 ID값 생성. 향후 이 UUID값을 id값으로 사용해서 용이하게 데이터 추가/변경 가능
        var inputText: String
        var date: Date
        var isCompleted: Bool
    }
    
  2. 만든 ToDoTask 구조체는 model 폴더로 따로 빼기
    1

    저번에 뷰 컨트롤러들을 모아 놓은 것처럼 Models 폴더를 만들어서 구조체나 클래스 같은 데이터 타입을 따로 분리했다

  3. 기존에 있었던 String 배열을 위에서 만든 ToDoTask라는 구조체를 타입으로 가지는 배열로 변경 및 저장하고 불러오기도 그에 맞게 수정

    // 1. toDoTasks 배열 선언
    var toDoTasks = [ToDoTask]() {
        didSet {
            if toDoTasks.isEmpty {
                self.emptyTasksUILabel.isHidden = false
            } else {
                self.emptyTasksUILabel.isHidden = true
            }
        }
    }
    
    ...(생략)
    
    // 2. 값 저장
    if let textField = alertController.textFields?.first, let inputText = textField.text {
      	// 2-1. 입력 값 toDoTasks에 저장
        let newTask = ToDoTask(id: UUID(), inputText: inputText, date: Date(), isCompleted: false)
        self.toDoTasks.append(newTask)
    
        self.tableView.reloadData()
    		// 2-2. toDoTasks를 UserDefaults에 저장
        UserDefaults.standard.set(self.toDoTasks, forKey: self.toDoTasksKey) 
    }
    
    ...(생략)
    
    // 3. 값 로드
    extension ToDoViewController: UITableViewDataSource {
    		...(생략)
        // 3-1. toDoTasks배열에서 inputText 반환
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell =  tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
      cell.textLabel?.text = toDoTasks[indexPath.row].inputText
      return cell
    }
    ...(생략)
    

    이렇게 수정하니 더 이상 코드에 에러도 없어서
    이제 문제없겠지 하고 시뮬레이터로 앱 실행시키고
    할 일을 추가하니 갑자기
    2

    위와 같이 Thread 1: "Attempt to insert non-property list object (\n \"TODO.ToDoTask(id: 46481A51-576C-409E-87B2-84E76AC893D9, inputText: \\\"111\\\", date: 2023-08-08 10:22:19 +0000, isCompleted: false)\"\n) for key ToDoTasks" 이 에러를 내며 앱이 뻗어 버린다🙂

    구글링 하다가 아래 글에서 답을 찾게 되었는데

    [iOS - swift] UserDefaults에 struct 형태 저장 방법 (“Attempt to insert non-property list object” 오류 .. (https://ios-development.tistory.com/702)

    즉 전에 내가 앱이 껐다 켜져도 데이터를 남겨놓고 싶어서 UserDefaults에 저장을 했는데
    Int, String, Double 같은 기본 타입은 UserDefaults에서 바로 사용 가능해서 문제없었지만
    이번에는 구조체를 만들어서 그거를 UserDefaults에 집어넣으려 하니까 문제였던 것이다

    UserDefaults 저장하고 로드할 때는 각각 아카이빙/ 언아카이빙 하게 되는데

    저장할 때는 구조체(struct)를 Data형으로 변경해서 저장하고 → 아카이빙
    로드할 때는 역순으로 메모리에 저장된 Data형을 꺼내서 다시 구조체로 변경하여 씀 → 언아카이빙

    이런 과정이 들어간다고 한다

    따라서 아카이빙/언아카이빙을 위해 사용할 구조체가 Codable 프로토콜을 준수해야 한다고…

    해결 방법

    1. 일단 블로그에 나와있던 것처럼 만든 구조체를 Codable 프로토콜 준수하도록 변경

      struct ToDoTask: Codable {
          var id: UUID
          var inputText: String
          var date: Date
          var isCompleted: Bool
      }
      
    2. UserDefault에서 값 저장할때 JSONDecoder()사용

      // before
      if let savedData = UserDefaults.standard.array(forKey: toDoTasksKey) as? [ToDoTask] {
      		toDoTasks = savedData
      }
      
      // after
      if let savedData = UserDefaults.standard.object(forKey: toDoTasksKey) as? Data {
          let decoder = JSONDecoder()
        	// Data형으로 변환된 것을 다시 풀기
          if let savedObject = try? decoder.decode([ToDoTask].self, from: savedData) 	{
            toDoTasks = savedObject
        }
      }
      
    3. UserDefault에서 값 꺼내쓸때 JSONEncoder() 사용

      // before 
      UserDefaults.standard.set(self.toDoTasks, forKey: self.toDoTasksKey)
      
      // after
      let encoder = JSONEncoder()
      // toDoTasks 배열을 Data형으로 변환
      if let encoded = try? encoder.encode(self.toDoTasks) { 
      		// toDoTasks를 UserDefaults에 저장
          UserDefaults.standard.setValue(encoded, forKey: self.toDoTasksKey)
      }
      

    이렇게 하니까 해결됐다~!!
    어제는 뭐에서 막혔던 거지.. 일단 오늘 배운 것 꼭 기억해두자