# 📔 일기장 ## 🍀 소개 >프로젝트 기간: 2023.8.28 ~ 2023.9.15 일기를 생성, 수정, 삭제할 수 있는 앱입니다. 현재 위치에 따라 오늘의 날씨 이모티콘이 함께 들어갑니다. </br> ## 📖 목차 1. [👨‍💻 팀원](#1.) 2. [📅 타임라인](#2.) 3. [👀 시각화된 프로젝트 구조](#3.) 4. [💻 실행 화면](#4.) 5. [🪄 핵심 경험](#5.) 6. [🧨 트러블 슈팅](#6.) 7. [📚 참고 링크](#7.) </br> <a id="1."></a></br> ## 👨‍💻 팀원 | Max | hamg | | :--------: | :--------: | | <Img src = "https://hackmd.io/_uploads/B1FqbcBAn.png" width="200" height="200"> |<Img src="https://hackmd.io/_uploads/BknBM9rC2.jpg" width="200" height="200"> | |[Github Profile](https://github.com/maxhyunm)|[Github Profile](https://github.com/hemg2) | </br> <a id="2."></a></br> ## 📅 타임라인 |날짜|내용| |:--:|--| |2023.08.28| `SwiftLint` 라이브러리 추가| |2023.08.29| `SwiftLint` 조건 변경 | |2023.08.30| `DiaryEntity` `CreateDiaryViewController `생성<br> `keyboard` `NotificationCenter` 생성 및 구현 | |2023.09.01| `CoreData`: `Create` 구현| |2023.09.05| `CoreData`: `UpDate`, `Delete` 구현 <br> `Swipe` `share`, `delete` 구현 <br> `AlertController` 생성 | |2023.09.06| `CoreData`: `fetchDiary` 구현 | |2023.09.07| 개인 학습 및 `README` 작성 | |2023.09.10| `CoreDataError` 생성, 예외처리 추가<br>`AlertVC`로직수정, `Namespace`생성 | |2023.09.13| `WeatherAPI`통신 진행<br> `WeatherIcon Cache` 구현 <br> `CoreLocation` 생성 <br> `Migration-DiaryV2` 구현 | </br> <a id="3."></a></br> ## 👀 시각화된 프로젝트 구조 ### FileTree ├── Diary │   ├── Protocol │   │   ├── AlertDisplayble.swift │   │   └── ShareDisplayable.swift │   ├── Extension │   │   └── DateFormatter+.swift │   ├── Error │   │   ├── APIError.swift │   │   ├── CoreDataError.swift │   │   └── DecodingError.swift │   ├── Model │   │   ├── CoreData │   │   │   ├── CoreDataManager.swift │   │   │   ├── Diary+CoreDataClass.swift │   │   │   └── Diary+CoreDataProperties.swift │   │   ├── DTO │   │   │   ├── DecodingManager.swift │   │   │   └── WeatherResult.swift │   │   ├── ImageCache │   │   │   └── ImageCachingManager.swift │   │   └── Namespace │   │   ├── AlertNamespace.swift │   │   └── ButtonNamespace.swift │   ├── Network │   │   ├── NetworkConfiguration.swift │   │   └── NetworkManager.swift │   ├── Controller │   │   ├── DiaryDetailViewController.swift │   │   └── DiaryListViewController.swift │   ├── View │   │ └── DiaryListTableViewCell.swift │   ├── App │   │   ├── AppDelegate.swift │   │   └── SceneDelegate.swift │   ├── Assets.xcassets │   ├── Info.plist │   └── Diary.xcdatamodeld ├── Diary.xcodeproj └── README.md </br> <a id="4."></a></br> ## 💻 실행 화면 | 새 일기 작성 | |:--:| |<img src="https://hackmd.io/_uploads/rJ_15vbJ6.gif" width="200"/>| | 일기 수정 | |:--:| |<img src="https://hackmd.io/_uploads/Bk_kqvbkT.gif" width="200"/>| | 일기 공유(리스트) | 일기 공유(더보기) | |:--:|:--:| |<img src="https://hackmd.io/_uploads/S1Q83w-ya.gif" width="200"/>|<img src="https://hackmd.io/_uploads/SyQL2vZkp.gif" width="200"/> | 일기 삭제(리스트) | 일기 삭제(더보기) | |:--:|:--:| |<img src="https://hackmd.io/_uploads/rkXU3wWJa.gif" width="200"/>|<img src="https://hackmd.io/_uploads/rJXL2DZka.gif" width="200"/>| </br> <a id="5."></a></br> ## 🪄 핵심 경험 #### 🌟 CoreData를 활용한 데이터 저장 일기 데이터를 위한 저장소로 CoreData를 활용하였습니다. #### 🌟 MappingModel 파일을 활용한 CoreData Migration 진행 CoreData의 버전 정보를 추가하고 이를 MappingModel로 연결하여 DB 변경사항에 대한 Migration을 진행하였습니다. #### 🌟 Singleton 패턴을 활용한 CoreDataManager 구현 데이터 처리를 위한 로직 전반을 Singleton 패턴으로 구현하여 앱 전역에서 활용 가능하도록 하였습니다. #### 🌟 NotificationCenter를 활용한 키보드 인식 키보드 활성화 여부에 따라 뷰의 크기를 변경하여 커서 위치가 가려지지 않도록 NotificationCenter를 활용하였습니다. #### 🌟 여러 개의 생성자를 통한 상황별 데이터 전달 상황에 따라 ViewController에서 다른 데이터를 표시해야 하는 경우에 대비해 생성자를 활용하였습니다. #### 🌟 Protocol과 Extension을 활용한 코드 분리 Alert, Swipe 등 별개의 작업으로 분리할 수 있는 내용들은 Protocol과 Extension을 통해 분리하였습니다. #### 🌟 URLSessionDataTask를 활용한 NetworkManager 구현 하나의 NetworkManager 타입을 구현하여 날씨 API 데이터 통신과 아이콘 이미지 관련 통신에 모두 활용하였습니다. </br> <a id="6."></a></br> ## 🧨 트러블 슈팅 ### 1️⃣ **반복적인 날짜 포매팅 처리** 🔒 **문제점** </br> 일기 리스트 화면과 새로운 일기를 생성하는 화면에서 모두 아래와 같이 날짜 포매팅을 사용해야 하는 것을 알 수 있었습니다. ```swift private let formatter: DateFormatter = { let formatter = DateFormatter() formatter.locale = Locale(identifier: "ko_kr") formatter.dateFormat = "yyyy년MM월dd일" return formatter }() ``` 동일한 코드가 두 개의 `ViewController`에서 반복되어 사용하고 있었으며 반복되는 것을 막기 위해 해당 코드를 분리하고자 했습니다. </br></br> 🔑 **해결방법** </br> 저장 프로퍼티가 아닌 메서드로 사용하여 재사용성을 높히게 되었습니다. ```swift extension DateFormatter { func formatToString(from date: Date, with format: String) -> String { self.dateFormat = format return self.string(from: date) } } DateFormatter().formatToString(from: entity.createdAt, with: "YYYY년 MM월 dd일") ``` </br> ### 2️⃣ **화면이 꺼질 때 자동 저장 처리** 🔒 **문제점**</br> 요구사항에 따르면 사용자가 화면을 벗어날 때마다 자동 저장을 진행해야 했습니다. 이를 구현하기 위해 처음에는 `CreateViewController`의 `viewWillDisappear` 메서드에서 저장처리를 진행할 수 있도록 작업했습니다. ```swift override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) saveDiary() } ``` 하지만 이렇게 하니 일기 삭제 처리를 한 뒤 뷰컨트롤러를 pop할 때에도 저장처리를 거치게 되어 오류가 발생하였습니다. </br></br> 🔑 **해결방법**</br> `TextView`가 수정될 때마다 뷰컨트롤러가 가지고 있는 일기 객체의 내용을 바꿔주고, 저장이 필요한 순간에 `saveContext` 처리만 진행할 수 있도록 아래와 같이 구현하였습니다. ```swift func textViewDidChange(_ textView: UITextView) { let contents = textView.text.split(separator: "\n") guard !contents.isEmpty, let title = contents.first else { return } let body = contents.dropFirst().joined(separator: "\n") diary.title = "\(title)" diary.body = body } ``` </br> ### 3️⃣ **빈 일기가 저장되는 현상** 🔒 **문제점(1)**</br> 처음에는 키보드가 비활성화되면 무조건 내용을 저장하도록 구현을 하였습니다. 하지만 이렇게 하니, 신규 생성 버튼(+)을 누른 뒤 아무런 내용도 입력하지 않고 뒤로 가기 처리를 하면 제목과 내용이 모두 비어있는 일기가 생성이 되었습니다. ```swift func textViewDidEndEditing(_ textView: UITextView) { CoreDataManager.shared.saveContext() } ``` |빈일기 생성 화면| |:--:| |<img src="https://cdn.discordapp.com/attachments/1148871276677562388/1148871347871686706/3639304423dabd43.gif" width="200" height="400"/>| </br></br> 🔑 **해결방법(1)**</br> 빈 일기가 생성되는것을 막기 위해 title 이 없을 경우 저장 되지 않게 진행하였습니다. ```swift func textViewDidEndEditing(_ textView: UITextView) { let contents = textView.text.split(separator: "\n") guard !contents.isEmpty else { return } CoreDataManager.shared.saveContext() } ``` </br></br> 🔒 **문제점(2)**</br> 위의 처리를 통해 더 이상 데이터베이스에 빈 일기가 저장되지는 않았지만, saveContext 되지 않은 객체가 여전히 context 내부에 남아 일시적으로 빈 일기가 리스트에 보이는 현상이 생겼습니다. ```swift func readCoreData() { do { diaryList = try container.viewContext.fetch(Diary.fetchRequest()) tableView.reloadData() } catch { .... } } ``` </br></br> 🔑 **해결방법(2)**</br> fetch해 온 일기들 중에 title이 비어있는 건은 걸러낼 수 있도록 filter 처리를 추가하였습니다. ```swift private func readCoreData() { do { let fetchedDiaries = try CoreDataManager.shared.fetchDiary() diaryList = fetchedDiaries.filter { $0.title != nil } tableView.reloadData() } catch { ..... } } ``` </br></br> ### 4️⃣ **아이콘 이미지 통신** 🔒 **문제점**</br> 일기장 앱은 모든 셀이 서버통신을 통해 아이콘을 가지고 오도록 구현되어 있습니다. 하지만 날씨 아이콘은 몇 개의 정해진 아이콘을 반복하여 활용합니다. 따라서 동일한 이미지를 매번 통신을 통해 가져오는 것은 비효율적이라고 생각되었습니다. </br></br> 🔑 **해결방법**</br> 한 번 활용된 이미지는 `NSCache`를 통해 캐싱 처리하여 바로 보여줄 수 있도록 구현하였습니다. ```swift class ImageCachingManager { static let shared = NSCache<NSString, UIImage>() ... } ``` ```swift guard let image = UIImage(data: data) else { return } DispatchQueue.main.async { ImageCachingManager.shared.setObject(image, forKey: NSString(string: icon)) self?.weatherIconImageView.image = image } ``` </br></br> ### 5️⃣ **CoreLocation** 🔒 **문제점 (1) - CoreLocation을 통해 정보를 받아오는 위치**</br> 실질적으로 Location 정보가 필요한 것은 `DiaryDetailViewController`에서 날씨 API를 호출할 때입니다. 때문에 처음에는 `DiaryDetailViewController`에서 활용 동의를 받고 위치 정보를 업데이트하도록 구현하려 하였습니다. 하지만 이렇게 하면 앱을 실행한 뒤 일기장 생성 화면에 넘어가서야 위치정보 활용 동의 창이 활성화되어 흐름상 어색해지고, 또 위치 정보가 제때 업데이트되지 않아 API 호출이 이루어지지 않는 등 다양한 문제가 발생했습니다. </br></br> 🔑 **해결방법 (1)**</br> 위치 정보 업데이트 자체는 첫 화면인 `DiaryListViewController`에서 진행하고, `DiaryDetailViewController`에서는 API 통신에 필요한 위도, 경도 데이터만 넘겨받을 수 있도록 구현하였습니다. ```swift let createDiaryView = DiaryDetailViewController(latitude: self.latitude, longitude: self.longitude) self.navigationController?.pushViewController(createDiaryView, animated: true) ``` 또한 위치정보 활용에 동의하지 않은 경우에도 일기 자체는 작성 가능하도록 구현하기 위해(날씨 이모티콘만 제외) 위도, 경도 데이터는 nil로도 전달될 수 있도록 하였습니다. ```swift init(latitude: Double?, longitude: Double?) { self.diary = CoreDataManager.shared.createDiary() self.isNew = true self.latitude = latitude self.longitude = longitude super.init(nibName: nil, bundle: nil) fetchWeather() } ``` </br></br> 🔒 **문제점 (2) - 시뮬레이터의 위치 정보 설정**</br> 시뮬레이터로 `CoreLocation` 기능을 테스트하면 시뮬레이터 자체에 설정된 Location 정보에 따라 위치를 표시하게 됩니다. 따라서 이 설정이 None으로 되어있을 경우에는 위치가 정상적으로 불러와지지 않습니다. 이 사실을 간과하여 테스트 과정에서 많은 시행착오를 거쳤습니다. </br></br> 🔑 **해결방법 (2)**</br> Custom Location을 활용하여 정상적으로 테스트를 진행할 수 있었습니다.</br> <img src="https://hackmd.io/_uploads/BJ20Xkgyp.png" width="500"> </br> <a id="7."></a></br> ## 📚 참고 링크 - [Apple Docs: Adaptivity and Layout](https://developer.apple.com/design/human-interface-guidelines/layout) - [Apple Docs: DateFormatter](https://developer.apple.com/documentation/foundation/dateformatter) - [Apple Docs: UITextView](https://developer.apple.com/documentation/uikit/uitextview) - [Apple Docs: Core Data](https://developer.apple.com/documentation/coredata) - [Apple Docs: Making Apps with Core Data](https://developer.apple.com/videos/play/wwdc2019/230/) - [Apple Docs: NSFetchedResultsController](https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller) - [Apple Docs: UITextViewDelegate](https://developer.apple.com/documentation/uikit/uitextviewdelegate) - [Apple Docs: UISwipeActionsConfiguration](https://developer.apple.com/documentation/uikit/uiswipeactionsconfiguration) - [Apple Docs: CoreLocation](https://developer.apple.com/documentation/corelocation) - [Apple Docs: Migrating your data model automatically](https://developer.apple.com/documentation/coredata/migrating_your_data_model_automatically) - [Apple Docs: NSCache](https://openweathermap.org/current) - [Open Weather API](https://openweathermap.org/current) </br>