## 일기장
> 프로젝트 기간: 23/08/28 ~ 23/09/15
## 📂 목차
1. [팀원](#1.)
2. [타임 라인](#2.)
3. [시각화구조](#3.)
4. [실행 화면](#4.)
5. [트러블 슈팅](#5.)
6. [팀 회고](#6.)
7. [참고 문서](#7.)
<a id="1."></a>
## 1. 팀원
| Jusbug | yyss99 |
| :--------: | :--------: |
| <Img src = "https://github.com/JusBug/ios-box-office/assets/125210310/549c2726-aa5a-48cc-a39a-7c10d10bdda5" width="200" height="200"> | <Img src = "https://hackmd.io/_uploads/ryHsN0cTn.png" width="200" height="200"> |
|[Github Profile](https://github.com/JusBug) |[Github Profile](https://github.com/yy-ss99) |
- - -
<a id="2."></a>
## 2. 타임라인
<details>
<summary>타임 라인</summary>
<div markdown="1">
### 2023.08.28.(월)
- README 수정
### 2023.08.29.(화)
- DiaryTableViewCell 등록 및 구현
- Sample 데이터 타입 구현 및 JSON 파일 추가
- JSON파일을 Sample 타입으로 디코딩하는 decodeJSON() 구현
- DiaryTableViewCell 레이아웃 수정
- DateFormatter로 createdDate 날짜형식변환
### 2023.08.30.(수)
- 일기장 생성 버튼 구현
- TextView 생성 및 placeholder 구현
- DetailViewController 네비게이션 타이틀 오늘 날짜 추가
- DetailVC 생성 및 테이블 뷰 커스텀 이니셜라이져로 데이터를 전달하여 didSelectRowAt() 구현
- DetailViewController 편집 시 키보드가 글자 가리는 이슈
### 2023.09.01 (금)
- final 키워드 명시, 은닉화 처리, 불필요한 프로퍼티 삭제
- NewDiaryViewController 삭제 및 DetailViewController 수정
- 불필요한 주석 제거
- README 업데이트
### 2023.09.14 (목)
- CoreDataManager 생성 및 저장, 수정 메서드 구현
- textView를 하나로 통합
- 새로운 Diary 저장기능 구현
- 추상화 및 조건문 수정, 공유와 삭제가 가능한 didTapMenu() 구현
- 화면 다크모드 적용 및 textColor 변경
- AlertAtion에서 delete 작업 구현
- CoreData에 날짜 저장 기능 추가
</div>
</details>
<a id="3."></a>
## 3. 시각화 구조
### 📐 Diagram

### 🌲 File Tree
<details>
<summary>File Tree</summary>
<div markdown="1">
```
.
├── Diary
│ ├── Entity+CoreDataClass.swift
│ ├── Entity+CoreDataProperties.swift
│ ├── Application
│ │ ├── AppDelegate.swift
│ │ └── SceneDelegate.swift
│ ├── ViewController
│ │ ├── MainViewController.swift
│ │ ├── DetailViewController.swift
│ │ └── DiaryTableViewCell.swift
│ ├── Model
│ │ ├── Sample.swift
│ │ ├── CustomDateFormatter.swift
│ │ └── CoreDataManager.swift
│ ├── Resources
│ │ ├── Assets
│ │ ├── Diary
│ │ └── Info.plist
│ └── View
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ └── DiaryTableViewCell.xib
│
├── README.md
└── sample.json
```
</div>
</details>
</br>
<a id="4."></a>
## 4. 실행 화면
| Create | SwipeDelete | AlertDelete |
| :--------: | :--------: | :--------: |
|<Img src = "https://hackmd.io/_uploads/BkmegKW16.gif" width="200" height="400">|<Img src = "https://hackmd.io/_uploads/HJs43ub1a.gif" width="200" height="400">|<Img src = "https://hackmd.io/_uploads/BJVskY-Jp.gif" width="200" height="400">|
| Update | Share | Date |
| :--------: | :--------: | :--------: |
|<Img src = "https://hackmd.io/_uploads/Hy-I-KZya.gif" width="200" height="400">|<Img src = "https://hackmd.io/_uploads/rJRKkFZJp.gif" width="200" height="400">|<Img src = "https://hackmd.io/_uploads/S1MZVF-1p.gif" width="200" height="400">|
- - -
</br>
<a id="5."></a>
## 5. 트러블 슈팅
### 1. <키보드 가림 이슈>
🤯 **문제상황**
`textField`에 쓰는 글이 길어지다 보면 키보드에 의해 가려지는 현상이 발생했습니다.
🔥 **해결방법**
키보드의 높이 만큼 `textView`의 `bottomInset`을 올라가도록 만들었습니다. 그래서 글이 쓰여지는 동안에 키보드에 의해 가려지 않고 화면에 보여지게 만들었습니다.
```Swift
@objc func keyboardWillShow(_ sender: Notification) {
if originalFrame == nil {
originalFrame = view.frame
}
if let keyboardFrame = (sender.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
let keyboardHeight = keyboardFrame.height
let safeAreaBottom = view.safeAreaInsets.bottom
let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight - safeAreaBottom, right: 0)
bodyTextView.contentInset = contentInsets
}
}
@objc func keyboardWillHide(_ sender: Notification) {
if originalFrame != nil {
bodyTextView.contentInset = UIEdgeInsets.zero
}
}
```
- - -
### 2. <TextView Placeholder 구현>
🤯 **문제상황**
기본적으로 `UITextView`에서는 `placeholder` 기능을 제공하지 않아서 직집 구현해야 하는 문제가 있었습니다. 참고로 `placeholder`는 사용자에게 입력하라는 힌트를 주는 메시지 역할로 사용자 입장에서 보다 편리한 UI 경험을 제공하기 위해서 구현하게 되었습니다.
🔥 **해결방법**
`titleTextView`, `bodyTextView`에서 둘다 사용할 수 있도록 `placeHolderText` 문자열을 전역으로 두고 `UITextViewDelegate`의 `textViewDidBeginEditing` `textViewDidEndEditing` 메서드 즉, 텍스트 뷰의 편집의 시작과 종료 시점에서 호출되는 메서드 안에 텍스트와 컬러를 정의함으로서 직접 placeholder기능을 구현하였습니다.
```Swift
let placeHolderText = "Input Text"
...
extension NewDiaryViewController: UITextViewDelegate {
func textViewDidBeginEditing(_ textView: UITextView) {
if titleTextView.text == placeHolderText {
titleTextView.text = nil
titleTextView.textColor = .black
}
if bodyTextView.text == placeHolderText {
bodyTextView.text = nil
bodyTextView.textColor = .black
}
}
func textViewDidEndEditing(_ textView: UITextView) {
if titleTextView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
titleTextView.text = placeHolderText
titleTextView.textColor = .lightGray
}
if bodyTextView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
bodyTextView.text = placeHolderText
bodyTextView.textColor = .lightGray
}
}
}
```
<Img src = "https://hackmd.io/_uploads/rkFAu_2ph.gif" width="300" height="600">
- - -
### 3. <touchesBegan 메서드>
🤯 **문제상황**
`TextViewDelegate`의 `textViewDidEndEditing()`가 호출되기 위해서는 텍스트 뷰의 편집이 종료되어야 하는데 다른 터치이벤트를 이용하여 처리를 해주어야만 편집에서 벗어날 수 있었습니다.
🔥 **해결방법**
`touchesBegan()`는 터치 이벤트가 발생했을 때 호출되는 메서드로 뷰 안에서 편집중인 키보드를 찾아 닫을 수 있도록 해결하였습니다.
```Swift
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
```
<Img src = "https://hackmd.io/_uploads/H1T32O2T2.gif" width="300" height="600">
- - -
### 4. <DateFormatterManager 구현>
🤯 **문제상황**
`DateFormatter`를 여러번 생성해서 사용해야 하는 상황이 발생 했습니다. 같은 역할을 하는 인스턴스를 여러번 만들어 사용하는 것이 비효율적이었습니다.
🔥 **해결방법**
`static`을 사용하여 `DateFormatter`를 한번만 생성하여 여러 곳에서 쓸 수 있도록 `CustomDateFormatter` 구조체를 만들었습니다.
```Swift
struct CustomDateFormatter {
static let customDateFormatter: DateFormatter = {
let todayDateFormatter = DateFormatter()
todayDateFormatter.locale = Locale(identifier: "koKR")
todayDateFormatter.dateFormat = "yyyy년 MM월 dd일"
return todayDateFormatter
}()
static func formatTodayDate() -> String {
let today = Date()
let formattedTodayDate = customDateFormatter.string(from: today)
return formattedTodayDate
}
static func formatSampleDate( sampleDate: Int) -> String {
let timeInterval = TimeInterval(sampleDate)
let inputDate = Date(timeIntervalSince1970: timeInterval)
let formattedDate = customDateFormatter.string(from: inputDate)
return formattedDate
}
}
```
- - -
### 5. <CoreDataManager 데이터 공유>
🤯 **문제상황**
`CoreDataManager`파일로 CRUD를 구현하였는데 각 ViewController에서 어떤 패턴으로 데이터를 가져와 사용할지 고민하게 되었습니다.
🔥 **해결방법**
단일 인스턴스로 여러 곳에서 해당 인스턴스를 공유할 수 있는 싱글톤 패턴을 사용하였습니다.
```Swift
final class CoreDataManager {
static var shared: CoreDataManager = CoreDataManager()
...
```
- - -
### 6. <tableView 업데이트>
🤯 **문제상황**
텍스트 뷰의 생성하여 저장하거나 수정하여도 변경된 `Entity`데이터가 테이블 뷰에 바로 업데이트 되지 않았고 다시 빌드를 해야만 적용되는 문제가 있었습니다.
🔥 **해결방법**
`MainVC`가 화면에 뜨기 직전에 호출되는 `viewWillAppear()`가 호출될 때, `getAllEntity()`를 통하여 변경된 Entity의 데이터를 가져오고, 메인 스레드에서 비동기 작업을 통해 테이블 뷰에 다시 데이터를 로드하고 업데이트하는 방식으로 해결했습니다.
```Swift
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.callGetAllEntity()
}
private func callGetAllEntity() {
coreDataManager.getAllEntity()
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
```
- - -
### 7. <새로운 일기 생성와 기존 일기 수정 처리>
🤯 **문제상황**
이전 화면(리스트 화면)으로 이동하는 경우 저장 되는 기능을 구현하기 위해서 `viewWillDisappear` 메서드 안에 저장하는 기능을 구현했습니다. 하지만 새로 생성하는 일기와 전에 있던 일기를 수정하는 경우를 각각 다르게 처리해야 한다는 문제상황이 발생했습니다.
🔥 **해결방법**
초기에 `tableView`에서 전달받는 `entity`가 있는지 없는지 저장하는 `initEntity` 변수를 만들어주었습니다. 전달 받은 `entity`가 없다면 새로운 일기이므로 `CoreData`에 `createEntity`를 하고 아니라면 기존에 있는 일기이므로 `updateEntity`를 합니다
```Swift=
private var initEntity: Entity?
...
override func viewDidLoad() {
...
initEntity = self.entity
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
guard let text = textView.text, !text.isEmpty, text != placeHolderText else {
return
}
let (title, body) = self.splitText(text: text)
if initEntity == nil {
coreDataManager.createEntity(title: title, body: body)
} else {
guard let entity = self.entity else {
return
}
coreDataManager.updateEntity(entity: entity, newTitle: title, newBody: body)
}
}
```
- - -
### 8. <다크모드>
🤯 **문제상황**
ViewController에서 직접 다크모드로 값을 주어 선언했을 때, 변경된 배경으로 인해 상단 바와 Title이 보이지 않는 문제가 발생하였습니다.
<Img src = "https://hackmd.io/_uploads/HJX0x9ey6.png" width="300" height="600">
🔥 **해결방법**
SceneDelegate의 scene() 메서드에서 직접 window.overrideUserInterfaceStyle을 dark로 선언해주어 해결하였습니다. 참고로 다크모드는 iOS 13에 도입된 UI 옵션입니다. 또한 SceneDelegate는 멀티 윈도우의 관리를 지원하므로써 Scene 설정을 통해서 다크모드로 지정할 수 있습니다.
```Swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let _ = (scene as? UIWindowScene) else { return }
if #available(iOS 13.0, *) {
window?.overrideUserInterfaceStyle = .dark
}
}
...
```
<Img src = "https://hackmd.io/_uploads/rkA1ZqekT.png" width="300" height="600">
- - -
### 9. <title과 body 분리>
🤯 **문제상황**
텍스트 뷰에 입력한 문자열을 구분하여 title과 body로 어떻게 저장할지 고민하게 되었습니다.
🔥 **해결방법**
텍스트 뷰의 전체 문자열을 들여쓰기를 기준으로 하여 배열로 담아 분류하여 첫 번째 요소를 title로 주고 이후 나머지 값을 모두 body로 처리하여 구분하였습니다.
```Swift
private func splitText(text: String) -> (title: String, body: String) {
let lines = text.components(separatedBy: "\n")
var title = ""
var body = ""
if let firstLine = lines.first {
title = firstLine
}
if lines.count > 1 {
body = lines[2...]
.joined(separator: "\n")
}
return (title, body)
}
```
- - -
<a id="6."></a>
## 6. 팀 회고
### 우리팀이 잘한점👍
- 이해를 하는 데에 중심을 두고 프로젝트를 진행해서 공부를 많이 하게 되었습니다.
### 서로에게 피드백😀
- <To. yyss99>
- 적극적으로 질문을 해주셔서 저 또한 다시 이해해보고 공부하게 되는 시간이 될 수 있었습니다.
- 제 의견과 설명을 해드리면 바로 포인트를 캐치해서 빠르게 이해해주셔서 좋았습니다.
- <To. Jusbug🕷️>
- 의견을 제시했을 때 잘 반영해주셔서 좋았습니다.👍
- 프로젝트 진행보다 이해와 학습 우선시 하는 분위기를 만들어 주셔서 좋았습니다.📖
</br>
- - -
<a id="7."></a>
## 7. 참고 문서
- [🍎 Apple - Adaptivity and Layout](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/)
- [🍎 Apple - UIKit: Apps for Every Size and Shape
](https://www.wwdcnotes.com/notes/wwdc18/235/)
- [🎦 Video - Making apps adaptive 1](https://www.youtube.com/watch?v=hLkqt2g-450)
- [🎦 Video - Making apps adaptive 2](https://www.youtube.com/watch?v=s3utpBiRbB0)
- [🍎 Apple - dateformatter](https://developer.apple.com/documentation/foundation/dateformatter)
- [🍎 Apple - UITextView](https://developer.apple.com/documentation/uikit/uitextview)
- [🍎 Apple - coredata](https://developer.apple.com/documentation/coredata)
- [🍎 Apple - UItextviewdelegate](https://developer.apple.com/documentation/uikit/uitextviewdelegate)
- [🍎 Apple - UIswipeactionsconfiguration](https://developer.apple.com/documentation/uikit/uiswipeactionsconfiguration)
- [🍎 Apple - UIsearchcontroller](https://developer.apple.com/documentation/uikit/uisearchcontroller)
- [🍎 Apple - dark-mode](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/dark-mode)
- [🎦 Video - Typography and Fonts (WWDC 2016)](https://www.youtube.com/watch?v=7AeEkoKb52Y)