[Swift] 메모 앱 만들기 심화 (1) : UserDefaults, URLSession 적용하기

Oni·2023년 9월 4일
0

TIL

목록 보기
32/47
post-thumbnail

원문 포스팅 🔗

어제 배운 UserDefaluts와 URLSession을 기존에 작업한 메모앱에 적용을 해보려고 한다.

UserDefaluts

  • 기존에 작업한 모델을 최대한 변경하지 않고 유지하는 방법을 고민

  • 고민의 흔적
    • 1) Memo 값 전체를 UserDefaults로 설정하는 방법
    • 2) 기존 함수 내 실행코드에 UserDefaults를 적용하는 방법
    • 첫번째 방법은 반복되는 코드가 발생하고 쓸데없이 길어져 가독성이 떨어지는 문제가 있음
    • 두번째 방법은 기존에 있는 코드를 그대로 활용하면 되고, 추가되는 코드가 적기 때문에 가독성이 높음
  • 메모 생성/수정/삭제/로드(CRUD) 함수 내부 실행코드 마지막에 UserDefaults로 해당 데이터를 저장하는 코드를 추가하는 방법으로 구현
  • 그리고 클래스로 구현한 Memo를 구조체로 변경(딱히 클래스여야 할 이유가 없음.. 상속을 받는 것도 아니고.. 그렇다고 Memo를 바로 쓰는 것도 아니라서 변경함)

Memo 구조체

import Foundation

struct Memo: Codable {
    var content: String     // 메모 타이틀
    var isCompleted: Bool   // 완료여부
    var insertDate: Date   // 작성일
    var targetDate: Date?   // 목표일자
    var priority: String?   // 중요도(우선순위)
    var category: String?   // 카테고리
    var progress: Int?      // 진행율
}

MemoManager 클래스

class MemoManager {
    static let shared = MemoManager() // 싱글톤 패턴
    private var userDefaults = UserDefaults.standard // 접근 제어
    
    var memoList: [Memo] = []
    
    // (접근 제어) 초기화 : UserDefaults를 통해 메모 불러오기 - (1)
    private init() {
        if let data = userDefaults.data(forKey: "MemoList"),
           let saveMemoList = try? JSONDecoder().decode([Memo].self, from: data) {
            memoList = saveMemoList
        }
    }
    
    // 메모 추가
    func addMemo(content: String, isCompleted: Bool, priority: String?, category: String?, progress: Int?) {
        ...
        // 기존 코드
        saveMemoListToUserDefaults()
    }

    // 메모 수정
    func updateMemo(at index: Int, newContent: String, isCompleted: Bool, insertDate: Date, targetDate: Date?, priority: String?, category: String?, progress: Int?) {
        ...
        // 기존 코드
        saveMemoListToUserDefaults()
    }

    // 메모 삭제
    func deleteMemo(at index: Int) {
        ...
        // 기존 코드
        saveMemoListToUserDefaults()
    }
    
    // 모든 메모 삭제 - (3)
    func deleteAllCompletedMemos() {
        var indexesToRemove: [Int] = []
        for (index, memo) in memoList.enumerated() {
            if !memo.isCompleted {
                indexesToRemove.append(index)
            }
        }
        // 인덱스를 역순으로 정렬한 후 삭제(정확한 삭제를 위해)
        let reversedIndexes = indexesToRemove.sorted(by: >)
        for index in reversedIndexes {
            deleteMemo(at: index)
        }
    }
    
    // 접근 제어 - (2)
    private func saveMemoListToUserDefaults() {
        if let data = try? JSONEncoder().encode(memoList) {
            userDefaults.set(data, forKey: "MemoList")
        }
    }

}
  • 기존에 있는 코드를 그대로 살리고 MemoList를 생성/수정/삭제할 때 * UserDefaults로 저장하는 함수를 추가하는 형태로 구현
  • (1) UserDefaults를 통해 메모 불러오기
    • UserDefaults에서 MemoList라는 키로 저장된 데이터를 가져와서 JSONDecoder를 사용하여 Memo 타입의 배열로 디코딩함
    • 디코딩 성공 시 디코딩된 메모 리스트를 memoList에 할당
    • 이전에 저장된 메모 데이터를 복원하여 클래스 내부에서 활용할 수 있음
  • (2) UserDefaults에 저장하기
    • JSONEncoder를 사용하여 memoList 배열을 데이터로 변환(인코딩)
    • 변환된 데이터를 MemoList라는 키로 UserDefaults에 저장
    • 앱을 종료해도 저장된 데이터를 불러올 수 있음
  • (3) UserDefaults를 통해 삭제 및 전체삭제 구현
    • 기존에 삭제 로직을 아래와 같이 두개로 나누어 코드를 작성 : 할 일 확인하기(전체) 목록에서의 삭제, 완료한 일 목록에서의 개별 및 전체 삭제
    • 전체 목록에서의 삭제는 UserDefaults를 적용해야 앱을 종료했다 켜도 삭제된게 유지됨
    • 그러나 완료한 일 목록에서는 어차피 삭제하면 전체 목록에서 삭제되니까 * deleteMemo 함수 안에 있는 UserDefaults가 자동으로 적용됨. 그래서 전체 삭제 로직에는 추가하지 않아도 됨

🤳🏻 적용화면


URLSession

  • 기존 메모 앱 홈 화면에는 Asset을 이용하여 이미지뷰를 구성
  • URLSession을 이용해서 본 포스팅 상단에 있는 이미지로 바꿔보려고 함
  • 주의할 점 : UI는 Main Thread에서 업데이트 해야 함

URLManager

class URLManager {
    static let shared = URLManager()
    private init() {
        
    }
    
    let mainImageUrl = URL(string: "https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgdOoN%2Fbtsr4LBjZmp%2FPgZjUykhxAOcuTxiMIlDKk%2Fimg.png")!
    func getJsonData(completion: @escaping (Result<Data, Error>) -> Void) {
        let task = URLSession.shared.dataTask(with: mainImageUrl) { data, response, error in
            if let error {
                print("Error: Network Error")
                completion(.failure(NetworkError.emptyResponse))
                return
            }
            guard let data = data else {
                completion(.failure(NetworkError.emptyResponse))
                return
            }
            completion(.success(data))
        }
        task.resume()
    }
}

enum NetworkError: Error {
    case emptyResponse
}
  • 🚨 DispatchQueue를 통해 main에서 업데이트!
  • 코드설명 : 이전 포스팅에서 코드에 대한 설명을 자세하게 기재해두었으니 참조
    🔗 2023.08.22 - [TIL] - [Swift] URLSession 데이터 불러오기, UserDefaults CRUD

🤳🏻 적용화면


그 외 문제발견 및 해결

  • 문제발견 → 디테일페이지에서 수정 후 저장 누르고 이전 화면으로 돌아가지 않음
  • 지난 프로젝트 작업 당시에는 수정 후 저장할 때 확인이나 취소같은 액션을 추가하지 않음, 대신 저장하기 누르면 바로 저장하고 수정되었다는 Alert만 띄웠음
  • 저장을 누르고 Alert를 띄워주고 이전 뷰컨트롤러로 이동하게 하고 싶은데, popViewController 코드를 넣을 부분이 굉장히 애매
    • Alert는 title만 보이고 dismiss 되니까 이전에 넣기에는 알람이 안사라지고, 이후에 넣자니 구현이 안됨
    • 문제가 뭔지 확인이 필요
    • Unwind Segue는 IBAction이 동시에 적용이 안되서 불가
    • 수정/취소 버튼을 만들면 구현하는데 어렵지 않으나, 나는 버튼선택하는 과정을 없애고 싶고 바로 저장도 됐으면 좋겠음…
    • 기존 코드 사이에 프린트문도 넣어봤는데 이전화면으로 돌아가지는 않으면서 프린트문은 나오는 기이한 현상...
      -> dismiss - popViewController 코드 - 프린트문
  • (원인 확인) dismiss 되버려서 다음 코드를 실행할 수 있는 시간이 없어서 안됐던거임..
  • (해결방법) dismiss 이후 popViewController를 DispatchQueue를 통해 구현
let checkAlert = UIAlertController(title: "수정되었습니다.", message: "", preferredStyle: .alert)
self.present(checkAlert, animated: false)
dismiss(animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
	self.navigationController?.popViewController(animated: true)
}

🤳🏻 적용화면

profile
하지만 나는 끝까지 살아남을 거야!

0개의 댓글