Skip to content
forked from maxhyunm/ios-diary

야곰 아카데미 iOS 일기장 프로젝트 저장소입니다

Notifications You must be signed in to change notification settings

hemg2/ios-diary

 
 

Repository files navigation

📔 일기장

🍀 소개

프로젝트 기간: 2023.8.28 ~ 2023.9.15

일기를 생성, 수정, 삭제할 수 있는 앱입니다. 현재 위치에 따라 오늘의 날씨 이모티콘이 함께 들어갑니다.


📖 목차

  1. 👨‍💻 팀원
  2. 📅 타임라인
  3. 👀 시각화된 프로젝트 구조
  4. 💻 실행 화면
  5. 🪄 핵심 경험
  6. 🧨 트러블 슈팅
  7. 📚 참고 링크


👨‍💻 팀원

Max hamg
Github Profile Github Profile


📅 타임라인

날짜 내용
2023.08.28 SwiftLint 라이브러리 추가
2023.08.29 SwiftLint 조건 변경
2023.08.30 DiaryEntity CreateDiaryViewController 생성
keyboard NotificationCenter 생성 및 구현
2023.09.01 CoreData: Create 구현
2023.09.05 CoreData: UpDate, Delete 구현
Swipe share, delete 구현
AlertController 생성
2023.09.06 CoreData: fetchDiary 구현
2023.09.07 개인 학습 및 README 작성
2023.09.10 CoreDataError 생성, 예외처리 추가
AlertVC로직수정, Namespace생성
2023.09.13 WeatherAPI통신 진행
WeatherIcon Cache 구현
CoreLocation 생성
Migration-DiaryV2 구현


👀 시각화된 프로젝트 구조

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


💻 실행 화면

새 일기 작성
일기 수정
일기 공유(리스트) 일기 공유(더보기)
일기 삭제(리스트) 일기 삭제(더보기)


🪄 핵심 경험

🌟 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 데이터 통신과 아이콘 이미지 관련 통신에 모두 활용하였습니다.



🧨 트러블 슈팅

1️⃣ 반복적인 날짜 포매팅 처리

🔒 문제점
일기 리스트 화면과 새로운 일기를 생성하는 화면에서 모두 아래와 같이 날짜 포매팅을 사용해야 하는 것을 알 수 있었습니다.

 private let formatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.locale = Locale(identifier: "ko_kr")
    formatter.dateFormat = "yyyy년MM월dd일"
    return formatter
}()

동일한 코드가 두 개의 ViewController에서 반복되어 사용하고 있었으며 반복되는 것을 막기 위해 해당 코드를 분리하고자 했습니다.

🔑 해결방법
저장 프로퍼티가 아닌 메서드로 사용하여 재사용성을 높히게 되었습니다.

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일")

2️⃣ 화면이 꺼질 때 자동 저장 처리

🔒 문제점
요구사항에 따르면 사용자가 화면을 벗어날 때마다 자동 저장을 진행해야 했습니다. 이를 구현하기 위해 처음에는 CreateViewControllerviewWillDisappear 메서드에서 저장처리를 진행할 수 있도록 작업했습니다.

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    saveDiary()
}

하지만 이렇게 하니 일기 삭제 처리를 한 뒤 뷰컨트롤러를 pop할 때에도 저장처리를 거치게 되어 오류가 발생하였습니다.

🔑 해결방법
TextView가 수정될 때마다 뷰컨트롤러가 가지고 있는 일기 객체의 내용을 바꿔주고, 저장이 필요한 순간에 saveContext 처리만 진행할 수 있도록 아래와 같이 구현하였습니다.

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
}

3️⃣ 빈 일기가 저장되는 현상

🔒 문제점(1)
처음에는 키보드가 비활성화되면 무조건 내용을 저장하도록 구현을 하였습니다. 하지만 이렇게 하니, 신규 생성 버튼(+)을 누른 뒤 아무런 내용도 입력하지 않고 뒤로 가기 처리를 하면 제목과 내용이 모두 비어있는 일기가 생성이 되었습니다.

func textViewDidEndEditing(_ textView: UITextView) {
    CoreDataManager.shared.saveContext()
}
빈일기 생성 화면



🔑 해결방법(1)
빈 일기가 생성되는것을 막기 위해 title 이 없을 경우 저장 되지 않게 진행하였습니다.

func textViewDidEndEditing(_ textView: UITextView) {
        let contents = textView.text.split(separator: "\n")
        guard !contents.isEmpty else { return }
        
        CoreDataManager.shared.saveContext()
    }



🔒 문제점(2)
위의 처리를 통해 더 이상 데이터베이스에 빈 일기가 저장되지는 않았지만, saveContext 되지 않은 객체가 여전히 context 내부에 남아 일시적으로 빈 일기가 리스트에 보이는 현상이 생겼습니다.

func readCoreData() {
        do {
            diaryList = try container.viewContext.fetch(Diary.fetchRequest())
            tableView.reloadData()
        } catch {
           ....
        }
    }



🔑 해결방법(2)
fetch해 온 일기들 중에 title이 비어있는 건은 걸러낼 수 있도록 filter 처리를 추가하였습니다.

 private func readCoreData() {
        do {
            let fetchedDiaries = try CoreDataManager.shared.fetchDiary()
            diaryList = fetchedDiaries.filter { $0.title != nil }
            tableView.reloadData()
        } catch {
           .....
        }
    }



4️⃣ 아이콘 이미지 통신

🔒 문제점
일기장 앱은 모든 셀이 서버통신을 통해 아이콘을 가지고 오도록 구현되어 있습니다. 하지만 날씨 아이콘은 몇 개의 정해진 아이콘을 반복하여 활용합니다. 따라서 동일한 이미지를 매번 통신을 통해 가져오는 것은 비효율적이라고 생각되었습니다.

🔑 해결방법
한 번 활용된 이미지는 NSCache를 통해 캐싱 처리하여 바로 보여줄 수 있도록 구현하였습니다.

class ImageCachingManager {
    static let shared = NSCache<NSString, UIImage>()
   ...
}
guard let image = UIImage(data: data) else { return }
DispatchQueue.main.async {
    ImageCachingManager.shared.setObject(image, forKey: NSString(string: icon))
    self?.weatherIconImageView.image = image
}



5️⃣ CoreLocation

🔒 문제점 (1) - CoreLocation을 통해 정보를 받아오는 위치

실질적으로 Location 정보가 필요한 것은 DiaryDetailViewController에서 날씨 API를 호출할 때입니다. 때문에 처음에는 DiaryDetailViewController에서 활용 동의를 받고 위치 정보를 업데이트하도록 구현하려 하였습니다. 하지만 이렇게 하면 앱을 실행한 뒤 일기장 생성 화면에 넘어가서야 위치정보 활용 동의 창이 활성화되어 흐름상 어색해지고, 또 위치 정보가 제때 업데이트되지 않아 API 호출이 이루어지지 않는 등 다양한 문제가 발생했습니다.

🔑 해결방법 (1)
위치 정보 업데이트 자체는 첫 화면인 DiaryListViewController에서 진행하고, DiaryDetailViewController에서는 API 통신에 필요한 위도, 경도 데이터만 넘겨받을 수 있도록 구현하였습니다.

let createDiaryView = DiaryDetailViewController(latitude: self.latitude, longitude: self.longitude)
self.navigationController?.pushViewController(createDiaryView, animated: true)

또한 위치정보 활용에 동의하지 않은 경우에도 일기 자체는 작성 가능하도록 구현하기 위해(날씨 이모티콘만 제외) 위도, 경도 데이터는 nil로도 전달될 수 있도록 하였습니다.

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()
}



🔒 문제점 (2) - 시뮬레이터의 위치 정보 설정

시뮬레이터로 CoreLocation 기능을 테스트하면 시뮬레이터 자체에 설정된 Location 정보에 따라 위치를 표시하게 됩니다. 따라서 이 설정이 None으로 되어있을 경우에는 위치가 정상적으로 불러와지지 않습니다. 이 사실을 간과하여 테스트 과정에서 많은 시행착오를 거쳤습니다.

🔑 해결방법 (2)

Custom Location을 활용하여 정상적으로 테스트를 진행할 수 있었습니다.


📚 참고 링크


About

야곰 아카데미 iOS 일기장 프로젝트 저장소입니다

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Swift 100.0%