---
# System prepended metadata

title: "\U0001F4D6일기장 - BMO, MINT"
tags: [project]

---

# Ground Rules

## 시간표

### 월 ,목

| 시간 | 내용 | 상세 | 
| :---------: | :---------: | :-: | 
| 09:00-11:30 | 스크럼, 활동학습 스터디 | |
| 11:30-14:00 | 점심시간, 공부 | |
| 14:00-17:00 | 활동학습 | | 
| 17:00-18:30 | 저녁시간 및 개인시간 | |
| 18:30~ | 프로젝트, 공부 | |

### 화, 수, 금 

| 시간 | 내용 | 상세 | 
| :---------: | :-----------: | :-: | 
| 09:00-11:30 | 스크럼 및 프로젝트 |  |
| 11:30-13:00 | 점심시간 | |
| 13:00-17:00 | 프로젝트 | |
| 17:00-18:30 | 저녁시간 및 개인 시간 | |
| 18:30~ | 프로젝트, 공부 | |

## 🤖 프로젝트 규칙
- 코드베이스도 같이 공부해보기
- 읽고 참고해야하는 문서 전부 공부한 후에 프로젝트 진행
- 다른 사람이 올린 PR에 휩쓸리지 않기
- 많이 질문하기
- 그때그때 궁금한 점 핵엠디에 정리

## 🚦 팀 규칙
- TIL, 일일 회고 작성 시간(매일 22시부터 1시간 작성 진행)
- 컨디션이 좋지 않을 때는 꼭 말해주기!!
- 방해금지시간 : 0시 - 9시
- 주말은 일정 공유를 통해 유동적인 진행
- 에러 캡쳐 많이 하기
- PR 메모 하기



# 일일 스크럼
### 🙌 08/28 월
- 오늘의 컨디션
    - MINT: 방학 갖고 싶어요!
    - BMO: 3학기를 시작하기 좋은 컨디션
- 특이사항
    - MINT: 배고파요.
    - BMO: 3학기 시작이라니!
- 오늘 할 일 
    - [x] 활동학습 예습
    - [x] 리스트 구현
    - [ ] JSON Data 파싱
    - [ ] 날짜 데이터 Date Formatter 지역 및 길이별 표현의 활용

### 🙌 08/29 화
- 오늘의 컨디션
    - MINT: 서울 도착! 민트 두두등장
    - BMO: 시원하네요
- 특이사항
    - MINT: 문서.. 졸려요 ㅋㅅㅋ
    - BMO: 문서 읽다가 눈알이 빠질거같습니다
- 오늘 할 일 
    - [x] SwiftLint 적용
    - [X] 날짜 데이터 Date Formatter 지역 및 길이별 표현의 활용

### 🙌 08/30 수
- 오늘의 컨디션
    - MINT: 졸리지만 해피해요
    - BMO: 맛있는걸 먹어서 행복해요
- 특이사항
    - MINT: 킥킥 비밀~
    - BMO: 저도 비밀입니다~
- 오늘 할 일 
    - [X] JSON Data 파싱
    - [x] Date Formatter 공부, 적용
    - [x] 활동학습 예습

### 🙌 08/31 목
- 오늘의 컨디션
    - MINT: 신나는 하루~
    - BMO: 즐거운 하루~
- 특이사항
    - MINT: 킥킥 오늘도 비밀민트
    - BMO: 저도 비밀인데요?
- 오늘 할 일 
    - [x] 화면 전환 구현
    - [x] 2번째 View Controller 만들기
    - [x] 활동학습 예습

### 🙌 09/01 금
- 오늘의 컨디션
    - MINT: 조금밖에 못 잤지만 해피 민트!
    - BMO: 맑은 정신 🤗
- 특이사항
    - MINT: 뭔가 상쾌한 하루~ 저녁에 못해요오
    - BMO: 집중이 잘되는 날이네요
- 오늘 할 일 
    - [ ] textView 공부
    - [x] PR 보내기
    - [x] 리드미 작성

### 🙌 09/04 월
- 오늘의 컨디션
    - MINT: 기절 민트
    - BMO: 너무 졸려요
- 특이사항
    - MINT: 스유...꺄아
    - BMO: 스유는 재밌지만, 재미가 잠을 쫓아주진 않았습니다
- 오늘 할 일 
    - [x] 코어데이터 모델 생성
    - [ ] 일기 CRUD 구현 (1/2)

### 🙌 09/05 화
- 오늘의 컨디션
    - MINT: 행복
    - BMO: 굿~
- 특이사항
    - MINT: 킥킥
    - BMO: 히히
- 오늘 할 일 
    - [ ] 일기 CRUD 구현 (2/2)

### 🙌 09/06 수
- 오늘의 컨디션
    - MINT: 모각코..! 으악 살려주세요
    - BMO: 사람이 많으면 조용해집니다
- 특이사항
    - MINT: 합정합정입니댜만?
    - BMO: 프로젝트 팍팍!
- 오늘 할 일 
    - [X] 일기 삭제 구현
    - [X] 일기 저장 조건 추가 구현

### 🙌 09/07 목
- 오늘의 컨디션
    - MINT: 엄...
    - BMO: 코랑 눈이 간질간질
- 특이사항
    - MINT: 리뷰 어렵
    - BMO: 프로젝트 순항 중
- 오늘 할 일 
    - [x] 일기 공유 구현(1/2)
    - [x] 삭제 확인 alert 구현

### 🙌 09/08 금
- 오늘의 컨디션
    - MINT: 뵤 메롱
    - BMO: 말끔
- 특이사항
    - MINT: 기분쪼음
    - BMO: 기분좋음
- 오늘 할 일 
    - [x] 일기 공유 구현(2/2)
    - [x] README 작성
    - [x] PR 작성

### 🙌 09/11 월
- 오늘의 컨디션
    - MINT: 죽어가요
    - BMO: 피곤해요
- 특이사항
    - MINT: 사망😇
    - BMO: 비염, 탈진, 수면부족
- 오늘 할 일 
    - [x] 코멘트 수정
    - [ ] 학습활동 예습
    - [x] 병원 가기
    - [x] 약 먹기
    - [x] 푹 쉬기

### 🙌 09/12 화
- 오늘의 컨디션
    - MINT: 흑흑 주사 필요
    - BMO: 비염이 어제보다 괜찮아요~
- 특이사항
    - MINT: 주사 으악
    - BMO: 집중하기 좋은 상태
- 오늘 할 일 
    - [x] 코멘트 수정

### 🙌 09/13 수
- 오늘의 컨디션
    - MINT: 많이 잘 거에요
    - BMO: 보통
- 특이사항
    - MINT: 씻어야 해요
    - BMO: 너무 안나가서 좀이 쑤시는 상태
- 오늘 할 일 
    - [X] 코멘트 수정

### 🙌 09/14 목
- 오늘의 컨디션
    - MINT: 열이 조금
    - BMO: 보통~좋음
- 특이사항
    - MINT: 휴식~
    - BMO: 암장 오픈런~
- 오늘 할 일 
    - [ ] 스텝 3

### 🙌 09/15 금
- 오늘의 컨디션
    - MINT: 기절
    - BMO: 좋음!
- 특이사항
    - MINT: 흑흑 흑흑흑
    - BMO: 바쁜 날입니다
- 오늘 할 일 
    - [ ] 리드미
    - [ ] 

# 📖일기장

![](https://hackmd.io/_uploads/Bk88JsYTn.png)
> 프로젝트 기간: 23.08.28 ~ 23.09.15

## 📖 목차
1. [🍀 소개](#소개)
2. [💻 실행 화면](#실행-화면)
3. [🧨 트러블 슈팅](#트러블-슈팅)
4. [📚 참고 링크](#참고-링크)
5. [👥 팀](#팀)

</br>

<a id="소개"></a>

## 🍀 소개
일기를 작성, 수정, 삭제, 공유 할 수 있는 앱
> 지원 언어 : 한국어, English

</br>

<a id="실행-화면"></a>

## 💻 실행 화면

| 일기 목록 스크롤 | 일기 내용 보기 |
|:--------:|:--------:|
|<img src="https://github.com/bubblecocoa/storage/assets/67216784/df3191b2-fdda-46f0-9953-5ece1a232ba5" alt="diary_scroll" width="250">|<img src="https://github.com/bubblecocoa/storage/assets/67216784/771da24b-a121-48b7-ae43-5ed37a49be20" alt="diary_detail" width="250">|

| 키보드 영역 겹침 방지 | 일기 추가 |
|:--------:|:--------:|
|<img src="https://github.com/bubblecocoa/storage/assets/67216784/4b12fde6-a814-45a3-a3e1-7905a740fef9" alt="diary_keyboard" width="250">|<img src="https://github.com/bubblecocoa/storage/assets/67216784/9292db31-5391-46a6-85fe-4c72201937ae" alt="diary_push_add_diary_view" width="250">|

| 일기 삭제 | 일기 공유 |
|:--------:|:--------:|
|<img src="https://github.com/bubblecocoa/storage/assets/67216784/e4b5a412-357d-4da8-bb21-97aea0c67c5c" alt="diary_delete" width="250">|<img src="https://github.com/bubblecocoa/storage/assets/67216784/4395c0af-56af-43a1-8381-cff8a262beb4" alt="diary_share" width="250">|

| 일기 수정 |
|:--------:|
|<img src="https://github.com/bubblecocoa/storage/assets/67216784/f0102b0f-8618-447e-b87c-16c07bb4844c" alt="diary_edit" width="250">|

</br>

<a id="트러블-슈팅"></a>

## 🧨 트러블 슈팅
###### 핵심 트러블 슈팅위주로 작성하였습니다.
1️⃣ **Swift Lint 규칙변경** <br>
-
🔒 **문제점** <br>
`Pod`을 통해 `SwiftLint`를 설치하고 프로젝트에 적용했습니다. `Lint`는 프로젝트 빌드 시 코드 컨벤션에 대한 경고를 띄워주었고, 경고를 모두 없애면 전체적으로 읽기 좋은 코드가 되었습니다.
하지만 `Lint`를 모두 따르기에는 어색한 부분이 있었는데, 줄바꿈에 대한 부분이었습니다.
> `SwiftLint`가 경고를 띄우는 부분
```swift
struct 구조체 {
    let 프로퍼티1
    let 프로퍼티2
    // Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace) 
    func 메서드1() {}
    func 메서드2() {}
    // Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
    enum 열거형 {
        case 경우1
        case 경우2
    }
}
```
저희 팀은 줄바꿈은 `SwiftLint`의 제안을 받아들이기보다 저희의 컨벤션을 지키고 싶었으나, `XCode`의 `Issue Navigator`에 `Lint`로 인한 경고가 많이 누적되는것을 보고싶지 않았습니다.

🔑 **해결방법** <br>
`SwiftLint`의 기본 옵션을 변경할 수 있다는 것을 알게 되었습니다.
[SwiftLint Rule](https://realm.github.io/SwiftLint/rule-directory.html)에 따르면 경고에 계속 노출되었던 `trailing_whitespace`는  줄 뒤에 공백이 있어서는 안 됨을 의미합니다.
프로젝트 `root` 경로에 `.swiftlint.yml` 파일을 만들고 내부에 다음 내용을 작성했습니다.
```yml
# 기본 활성화된 룰 중에 비활성화할 룰을 지정
disabled_rules:
    - trailing_whitespace
```
`disabled_rules`에 `trailing_whitespace`를 추가함으로써 `Lint`가 줄바꿈 관련된 경고를 띄우지 않도록 변경했습니다.

<br>


2️⃣ **일기 작성 및 수정 시 textView 개수 선택** <br>
-
🔒 **문제점** <br>
제목과 본문의 구현을 어떻게 해야할지에 대한 고민이 있었습니다. 제목 `textView`와 본문 `textView`를 나누고 `stackView`에 넣어줄 경우 여러가지 문제점이 생겼습니다. 
1. 제목에 특정한 제약을 주지 않아 길어지게 되서 한 화면을 전부 차지하게 될 경우 본문 `textView`로 넘어갈 수가 없다.
2. 본문 `textView`를 스크롤 할 경우 제목은 올라가지 않고 계속 남아있게 된다.


🔑 **해결방법** <br>
`textView`를 제목과 본문으로 나누지 않고 `contentTextView`라는 하나의 `textView`에서 제목과 본문을 모두 입력받을 수 있게 변경하여 처리하였습니다.

<br>

3️⃣ **iOS 타겟 버전 변경 - UIKeyboardLayoutGuide** <br>
-
🔒 **문제점** <br>
키보드를 사용할 때 글자를 가리는 일이 생겨 `textView`도 같이 올려주는 방법에 대한 고민이 있었습니다. 그 중에서도 두가지 방법을 찾을 수 있었습니다.
1. `Notification`을 사용하여 키보드가 올라올 때마다 키보드의 `contentInset`을 빼주는 방법
2. `keyboardLayoutGuide`를 제약 조건에 적용하는 방법

간단하기는 2번이 간단했지만 `iOS 15.0` 부터 사용할 수 있어 고민이 있었습니다.


🔑 **해결방법** <br>
1번의 방법을 사용할 때 `keyboardFrameEndUserInfoKey`을 사용합니다. 그런데 [keyboardFrameEndUserInfoKey](https://developer.apple.com/documentation/uikit/uiresponder/1621578-keyboardframeenduserinfokey) 공식문서를 보면 다음 내용이 있었습니다.
> Important
>
> Instead of using this key to track the keyboard’s frame, consider using UIKeyboardLayoutGuide, which allows you to respond dynamically to keyboard movement in your app. For more information, see [Adjusting Your Layout with Keyboard Layout Guide](https://developer.apple.com/documentation/uikit/keyboards_and_input/adjusting_your_layout_with_keyboard_layout_guide).
> 
> 이 키를 사용하여 키보드 프레임을 추적하는 대신 앱의 키보드 움직임에 동적으로 반응할 수 있는 UIKeyboardLayoutGuide를 사용하는 것이 좋습니다. 자세한 내용은 [키보드 레이아웃 가이드로 레이아웃 조정](https://developer.apple.com/documentation/uikit/keyboards_and_input/adjusting_your_layout_with_keyboard_layout_guide)을 참조하세요.
 
[UIKeyboardLayoutGuide](https://developer.apple.com/documentation/uikit/uikeyboardlayoutguide)는 `iOS 15.0` 이후로 지원하기 때문에 2번의 방법을 선택하여 진행하였습니다.

<br>

4️⃣ **지역화 적용** <br>
-
🔒 **문제점** <br>
날짜 관련된 문자열을 출력하기 위해 `DateFormatter`의 확장에 다음 메서드를 추가했습니다.
```swift
func configureDiaryDateFormat() {
    dateStyle = .long
    timeStyle = .none
    locale = Locale(identifier: "ko_KR")
    dateFormat = "yyyy년 MM월 dd일"
}
```
하지만 이렇게 날짜 포맷을 지정할 경우 사용자가 어떠한 `Locale`을 선택하더라도 'XXXX년 XX월 XX일' 형태로 출력되게 됩니다.

🔑 **해결방법** <br>
```swift
func configureDiaryDateFormat() {
    dateStyle = .long
    timeStyle = .none
    locale = Locale(identifier: Locale.current.identifier)
}
```
`DateFormatter`의 `locale`을 현재의 `Locale.current.identifier`를 통해 인식하도록 했습니다. 이 값은 디바이스의 `설정` - `일반` - `언어 및 지역`의 정보를 가져오게 됩니다. 이것으로 사용자 각각의 `Locale`에 맞게 날짜가 포매팅 되어 출력됩니다.

<br>


5️⃣ **layoutMarginsGuide** <br>
-
🔒 **문제점** <br>
`tableView`의 `Custom Cell`을 설정할 때 제약조건을 `ContentView`에 맞췄더니 글자들이 `leading`에 딱 붙어서 표시되었습니다. 
``` swift
private func configureCellConstraint() {
    NSLayoutConstraint.activate([
        contentStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
        contentStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
        contentStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
        contentStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
    ])
}
```
이를 `seperate line`에 맞게 보기 좋은 간격을 주기 위한 고민이 있었습니다. 


🔑 **해결방법** <br>
`layoutMarginGuide`라는 여백 기준을 사용하여 간격을 맞춰주었습니다. 
```swift
private func configureCellConstraint() {
    NSLayoutConstraint.activate([
        contentStackView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
        contentStackView.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
        contentStackView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
        contentStackView.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor)
    ])
}
```
`readableContentGuide`를 이용하여 간격을 줄 수도 있지만 넓은 아이패드 같은 화면에서 사용하게 되는 경우 퍼지는 것을 잡아주는 데에 사용하는 가이드인데 현재의 프로젝트에서는 `layoutMarginGuide`로도 충분할 것 같아서 이것을 사용하였습니다.

<br>

6️⃣ **UUID** <br>
-
🔒 **문제점** <br>
일기를 수정﹒삭제 하기 위해 `CoreData`로부터 일기를 구분하기 위한 `Identifier`가 필요했습니다.
일기 `Entity`의 `Attribute`에 있는 값은 `title: String`, `body: Stirng?`, `date: Date` 세가지 였는데, 이 중 무엇하나 식별자로 사용 가능한 값이 없었습니다. 일기 제목, 내용, 작성날짜 모두 같은 일기가 존재 할 수 있었기 때문입니다.

🔑 **해결방법** <br>
`Entity`의 `Attribute`에 `id: UUID`를 추가했습니다. `UUID`는 범용 고유 식별자로 단순히 이것을 추가하는 것만으로 각각의 일기를 명확하게 구분할 수 있게 됩니다. `UUID`를 가지고 있는 경우 해당 `Entity`의 `title`, `body`의 내용이 바뀌어도 새로운 객체나 다른 객체에 내용이 작성되는 것이 아닌 현재의 객체에 정확하게 내용이 작성﹒업데이트 되는 것을 확인할 수 있었습니다.

<br>

7️⃣ **UIContextualAction 커스텀** <br>
-
🔒 **문제점** <br>
`UIContextualAction`의 `activityItems`에 일기 제목, 일기 내용을 넣을 경우 공유 될 내용이 아래 이미지처럼 미리보기로 출력됩니다.
![](https://hackmd.io/_uploads/BJidTmuA3.png)
이 문제를 해결하기 위해 일기 제목과 일기 내용을 하나의 문자열로 합치고, 중간에 줄바꿈 기호 `\n`을 추가하는 방식을 적용 해봤습니다.
문자열은 원하는 형태로 출력되었으나 폰트 스타일은 여전히 저희가 원하는 형태가 아니었습니다.
|현재 출력 방식|내용 합치고 줄바꿈 적용 시|원하는 표현 방식|
|-|-|-|
|**일반 텍스트**<br>일기 제목 및 일기 내용|일기 제목<br>일기 내용|**일기 제목**<br>일기 내용|

🔑 **해결방법** <br>
**UIActivityItemSource 및 LinkPresentation 프레임워크를 이용**

기존 방법에서 출력되는 Activity Items는 실제로 공유할 내용에 대한 것들만 미리보기로 출력할 수 있었습니다. 때문에 UIActivityItemSource로는 공유할 내용을 따로 지정하고 LinkPresentation 프레임워크는 원하는 표현 방식으로 미리보기를 구현하는 과정에서 필요를 느껴 import 해주었습니다.

![](https://hackmd.io/_uploads/H1Q3W4uC3.png)

먼저 UIActivityItemSource 프로토콜을 채택하면 공유할 개체가 데이터 공급자가 되어 항목에 대한 액세스 권한을 View Controller에 제공합니다.

- 공유할 데이터에 대해 placeHolder로 사용할 수 있는, 실제 데이터는 아니지만 그에 가깝게 표시하는 값을 return 합니다.

    ```swift
    func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
        return diary.title
    }
    ```
- 공유하고자 하는 데이터를 리턴합니다.

    ```swift
    func activityViewController(
        _ activityViewController: UIActivityViewController,
        itemForActivityType activityType: UIActivity.ActivityType?
    ) -> Any? {
        let dateFormatter = DateFormatter()
        
        dateFormatter.configureDiaryDateFormat()
        
        let formattedDate = dateFormatter.string(from: diary.date)
        let sharedData = "\(formattedDate)\n\n\(diary.title)\n\n\(diary.body ?? "")"
        
        return sharedData
    }
    ````
- LinkPresentation 프레임워크가 기본 컴포넌트로 존재하고 있어서 사용해보았습니다. 해당 프레임워크는 메타 데이터를 활용하여 원하는 데이터를 유저에게 표시하며 쉽게 공유할 수 있게 만들어 줍니다. 이를 UIActivityItemSource에 구현되어 있는 메서드와 함께 사용하면 공유할 때 커스텀한 미리보기를 사용할 수 있습니다.

    ```swift
    func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
        let metadata = LPLinkMetadata()
        
        metadata.title = diary.title
        metadata.originalURL = URL(fileURLWithPath: (diary.body ?? ""))
        
        return metadata
    }
    ```

<br>

8️⃣ **Background 전환시 일기 자동 저장** <br>
-
🔒 **문제점** <br>
일기 작성 중 홈으로 나가는 경우 메모리 부족으로 인해 앱이 종료되어 현재 작성중인 텍스트가 모두 사라질 수 있습니다. 이 경우를 대비하기 위해 앱이 백그라운드로 전환되기 전 일기를 저장하는 로직이 필요했습니다. 해당 `ViewController`에서 백그라운드로 전환되는 것을 인식할 수 있나 싶었으나, 일기가 저장되는 것은 `DiaryViewController`만의 문제가 아니기 때문에 다른 방식으로 백그라운드 전환을 인식할 수 있어야 했습니다.

🔑 **해결방법** <br>
`SceneDelegate`의 `sceneDidEnterBackground(_:)`메서드를 활용했습니다. 저희는 기존 `SceneDelegate`내에 `NSPersistentContainer`를 이미 구현해두었고, 모든 `ViewController`가 해당 `PersistentContainer`를 의존성 주입받아 사용하고 있기 때문에 현재 `PersistentContainer`의 `viewContext.save()`를 해주어 손쉽게 변경 내용을 저장할 수 있었습니다.

<br>

9️⃣ **weak self** <br>
-
🔒 **문제점** <br>
```swift
private func configureNavigationItem() {
    let action = UIAction { _ in
        self.showActionSheet()
    }
    let barButtonItem = UIBarButtonItem.init(
        image: UIImage.init(systemName: "ellipsis.circle"),
        primaryAction: action
    )
    
    navigationItem.rightBarButtonItem = barButtonItem
}
```
```swift
private func showActionSheet() {
    let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
    let shareAction = UIAlertAction(title: String(localized: "Share"), style: .default) { _ in
        self.shareDiary(data: self.diary)
    }
    let deleteAction = UIAlertAction(title: String(localized: "Delete"), style: .destructive) { _ in
        self.presentDeleteConfirmAlert(by: { self.deleteDiary()})
    }
    let cancelAction = UIAlertAction(title: String(localized: "Cancel"), style: .cancel)
    
    sheet.addAction(shareAction)
    sheet.addAction(deleteAction)
    sheet.addAction(cancelAction)
    
    present(sheet, animated: true)
}
```
메서드 내 `closure capture`에는 반복적으로 `self`가 호출되고 있습니다. 코드를 계속 타고 들어가면 언젠가는 순환참조인지 아닌지 확인할 수 있겠지만, 코드 파악이 어려워 어느 순간 순환참조임을 놓칠 수 있습니다.


🔑 **해결방법** <br>
해결 방법에 앞서 저희가 순환참조가 일어나는지 알아보기 위해 사용한 방법입니다.
1. `ViewController`의 `deinit` 호출 확인
    - `ViewController`가 화면에서 사라졌을 때 `deinit`이 호출되지 않으면 순환참조일 수 있습니다.
2. `Debug Memory Graph` 메뉴를 이용해 시각적으로 순환참조가 발생하고 있는지 확인
    - 도형과 화살표로 표기되는 관계 중 순환적으로 보이는 부분이 있다면 순환참조입니다.
3. `lldb`에서 `CFGetRetainCount`를 이용해 참조 카운트 확인
    - 메서드가 종료된 후에도 카운트에 변화가 없다면 순환참조일 수 있습니다.
4. `self`를 캡쳐하고 있는 함수를 반복적으로 호출하여 메모리 사용량 증가 확인
    - 메모리가 증가하기만 하고 일정수치까지 내려오는 과정이 없다면 순환참조일 수 있습니다.

위 방법들 중 가장 명확한 방법은 4번 이었습니다. 1~3번 방법에는 휴먼에러로 놓칠 수 있는 부분이 있지만, 4번의 경우 메모리 증가가 명확하기 때문입니다. 저희는 분명 순환참조가 발생하고 있었지만 2번으로 순환점을 찾는것에는 실패했습니다.

```swift
private func configureNavigationItem() {
    let action = UIAction { [weak self] _ in
        guard let self else {
            return
        }
        
        self.showActionSheet()
    }
    let barButtonItem = UIBarButtonItem.init(
        image: UIImage.init(systemName: "ellipsis.circle"),
        primaryAction: action
    )
    
    navigationItem.rightBarButtonItem = barButtonItem
}
```
```swift
private func showActionSheet() {
    let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
    let shareAction = UIAlertAction(title: String(localized: "Share"), style: .default) { [weak self] _ in
        guard let self else {
            return
        }
        
        self.shareDiary(data: self.diary)
    }
    let deleteAction = UIAlertAction(title: String(localized: "Delete"), style: .destructive) { [weak self] _ in
        guard let self else {
            return
        }
        
        self.presentDeleteConfirmAlert(by: { self.deleteDiary()})
    }
    let cancelAction = UIAlertAction(title: String(localized: "Cancel"), style: .cancel)
    
    sheet.addAction(shareAction)
    sheet.addAction(deleteAction)
    sheet.addAction(cancelAction)
    
    present(sheet, animated: true)
}
```
결론적으로 `self`를 캡쳐하고 있는 모든 부분에 `[weak self]` 를 추가해주는 것으로 반복적인 메소드 호출에도 메모리가 더이상 증가하지 않고 일정수치를 유지하는 것을 확인했습니다.
순환참조가 발생하지 않는다고 확신한다면 `[weak self]`를 붙이지 않아도 상관없지만, 사용되는 메서드 내부적으로 언제 `self`를 필요로 할지, 또 그것이 순환참조를 발생시킬 지 알 수 없기 때문에 `self`를 사용 할 일이 생긴다면 항상 `[weak self]`를 붙여주는 것으로 결론지었습니다.

<br>

1️⃣0️⃣ **CoreDataManageable** <br>
-
🔒 **문제점** <br>
처음에는 `CoreData`가 `SceneDelegate`에서 직접 `container`가 생성되는 구조였습니다. 그러나 `SceneDelegate`가 그러한 역할을 한다는 점이 어색하여 객체로 분리하기 위한 과정이 필요했습니다. 이때 두 가지 주의점이 있었습니다.
>1. `CoreData Manager`는 `CoreData`를 사용한다면 필요한 관리 객체는 전부 사용할 수 있게 해야 한다.
>2. `Diary`는 `CoreDataManager`를 사용해 `Diary`를 생성, 저장, 삭제하는 `Diary` 전용 `Manager`가 필요하다.

🔑 **해결방법** <br>
때문에 저희는 `CoreDataManageable Protocol`과 `DiaryService` 객체를 만들었습니다. `DiaryService`는 `CoreDataManageable`을 채택하고 있어 `CoreDataManage`와 같은 역할을 수행하면서도 `Diary` 전용 `Manager`의 역할을 수행할 수 있습니다. 또한 처음 `SceneDelegate`에서 이 `DiaryService`를 한번 만든 후 그대로 `ViewController`들은 주입 받아서 사용하기 때문에 의존성 방향을 일관적으로 주입할 수 있었습니다. 추가로 수정사항이 필요한 경우도 `DiaryService`만 수정하면 되기에 개방 폐쇄의 원칙을 지킬 수 있었습니다.


<br>

1️⃣1️⃣ **API Key 은닉화** <br>
-
🔒 **문제점** <br>
`API KEY`는 외부에 노출이 되어서는 안되는 키입니다. 때문에 이를 은닉화 하기 위해 `info.plist` 파일을 새로 생성하는 방법을 선택하였습니다. 그런데 이를 가져와서 사용할 때 필요한 `NSDictionary`의 기존 초기화 방법이 `deprecated`되어 더는 사용할 수 없게 된 문제가 있었습니다. 

[deprecated된 초기화](https://developer.apple.com/documentation/foundation/nsdictionary/1414949-init)


🔑 **해결방법** <br>
새롭게 제공되어 있는 초기화 방법을 이용해 해결하였습니다.

[새롭게 제공된 초기화](https://developer.apple.com/documentation/foundation/nsdictionary/2879140-init)

<br>

1️⃣2️⃣ **where Self: Type** <br>
-
🔒 **문제점** <br>
protocol을 사용할 때 특정한 Type에서만 채택할 수 있게 해주고 싶었습니다. 그래야 extension에서 함수를 구현할 때도 가능한 동작이 있었고 때문에 매개변수로 항상 타입을 받아와야 했습니다.

```swift
protocol DiaryAlertPresentable { }

extension DiaryAlertPresentable {
    func showDeleteConfirmAlert(in viewController: UIViewController, by action: @escaping () -> Void) {
    }
}
```

🔑 **해결방법** <br>
where Self: Type으로 채택할 수 있는 Type을 제한해 줌으로서 해결하였습니다.

```swift
protocol DiaryAlertPresentable where Self: UIViewController { }

extension DiaryAlertPresentable {
func presentDeleteConfirmAlert(by action: @escaping () -> Void) {
    }
}
```

<br>

<a id="참고-링크"></a>

## 📚 참고 링크
- [🍎Apple Docs: keyboardFrameEndUserInfoKey](https://developer.apple.com/documentation/uikit/uiresponder/1621578-keyboardframeenduserinfokey)
- [🍎Apple Docs: Adjusting Your Layout with Keyboard Layout Guide](https://developer.apple.com/documentation/uikit/keyboards_and_input/adjusting_your_layout_with_keyboard_layout_guide)
- [🍎Apple Docs: UIKeyboardLayoutGuide](https://developer.apple.com/documentation/uikit/uikeyboardlayoutguide)
- [🍎Apple Docs: Metatdata](https://developer.apple.com/documentation/avfoundation/avcapturephotosettings/2875951-metadata)
- [🍎Apple Docs: UUID](https://developer.apple.com/documentation/foundation/uuid)
- [🍎Apple Docs: Link Presentation](https://developer.apple.com/documentation/linkpresentation)
- [🍎Apple Docs: sceneDidEnterBackground(_:)](https://developer.apple.com/documentation/uikit/uiscenedelegate/3197917-scenedidenterbackground)
- [🍎Apple Docs: UIActivityItemSource](https://developer.apple.com/documentation/uikit/uiactivityitemsource)
- [🍎Apple Docs: init(contentsOf:error:)](https://developer.apple.com/documentation/foundation/nsdictionary/2879140-init)
- [🍎Apple Docs: Core Location](https://developer.apple.com/documentation/corelocation)
- [🍎Apple Docs: CFGetRetainCount(_:)](https://developer.apple.com/documentation/corefoundation/1521288-cfgetretaincount)
- <Img src = "https://github.com/mint3382/ios-calculator-app/assets/124643545/56986ab4-dc23-4e29-bdda-f00ec1db809b" width="20"/> [야곰닷넷: Swift Lint 써보기](https://yagom.net/forums/topic/swift-lint-%EC%8D%A8%EB%B3%B4%EA%B8%B0/)
- <Img src = "https://github.com/mint3382/ios-calculator-app/assets/124643545/56986ab4-dc23-4e29-bdda-f00ec1db809b" width="20"/> [야곰닷넷: LinkPresentation](https://yagom.net/forums/topic/linkpresentation/)
- <Img src = "https://hackmd.io/_uploads/ByTEsGUv3.png" width="20"/> [blog: [iOS] Swiftlint 룰 적용하기](https://velog.io/@whitehyun/iOS-Swiftlint-%EB%A3%B0-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0)


</br>

<a id="팀"></a>

## 👥 팀

### 👨‍💻 팀원
| 🤖BMO🤖 | 😈MINT😈 |
| :--------: | :--------: |
| <img src="https://hackmd.io/_uploads/BJdXmAAph.jpg" width="200" height="200"> | <img src="https://hackmd.io/_uploads/ByLbQ0RT2.jpg"  width="200" height="200"> |
|[Github Profile](https://github.com/bubblecocoa) |[Github Profile](https://github.com/mint3382) |

</br>

- [팀 회고 링크](https://github.com/mint3382/ios-diary/wiki)


---

![](https://hackmd.io/_uploads/Byn23Gop2.png)
[Macbook M1 Pro에서 부딪히는 문제 - pod install error](https://effectivecode.tistory.com/1583)
[SwiftLint github](https://github.com/realm/SwiftLint)
---

![](https://hackmd.io/_uploads/HJKRnGsph.png)
![](https://hackmd.io/_uploads/HJ8M6Gjp2.png)
[iOS 앱 Xcode 14 대응하기](https://velog.io/@jacob-ios/iOS-%EC%95%B1-Xcode-14-%EB%8C%80%EC%9D%91%ED%95%98%EA%B8%B0)

-----------------------

# 일기장 [STEP 1] MINT, BMO

안녕하세요 올라프!
이번 프로젝트에서 리뷰이가 된 MINT😈, BMO🤖입니다!
일기장 프로젝트 STEP 1 PR 보냅니다.
잘 부탁드립니다 🙇‍♂️

---

## 고민했던 점
### 1️⃣ Swift Lint 규칙변경
`Pod`을 통해 `SwiftLint`를 설치하고 프로젝트에 적용했습니다. `Lint`는 프로젝트 빌드 시 코드 컨벤션에 대한 경고를 띄워주었고, 경고를 모두 없애면 전체적으로 읽기 좋은 코드가 되었습니다.
하지만 `Lint`를 모두 따르기에는 어색한 부분이 있었는데, 줄바꿈에 대한 부분이었습니다.
> `SwiftLint`가 경고를 띄우는 부분
```swift
struct 구조체 {
    let 프로퍼티1
    let 프로퍼티2
    // Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace) 
    func 메서드1() {}
    func 메서드2() {}
    // Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
    enum 열거형 {
        case 경우1
        case 경우2
    }
}
```
저희 팀은 줄바꿈은 `SwiftLint`의 제안을 받아들이기보다 저희의 컨벤션을 지키고 싶었으나, `XCode`의 `Issue Navigator`에 `Lint`로 인한 경고가 많이 누적되는것을 보고싶지 않았습니다.

그러던 중 `SwiftLint`의 기본 옵션을 변경할 수 있다는 것을 알게 되었습니다.
[SwiftLint Rule](https://realm.github.io/SwiftLint/rule-directory.html)에 따르면 경고에 계속 노출되었던 `trailing_whitespace`는  줄 뒤에 공백이 있어서는 안 됨을 의미합니다.
프로젝트 `root` 경로에 `.swiftlint.yml` 파일을 만들고 내부에 다음 내용을 작성했습니다.
```yml
# 기본 활성화된 룰 중에 비활성화할 룰을 지정
disabled_rules:
    - trailing_whitespace
```
`disabled_rules`에 `trailing_whitespace`를 추가함으로써 `Lint`가 줄바꿈 관련된 경고를 띄우지 않도록 변경했습니다.



### 2️⃣ 일기 작성 및 수정 시 textView 개수 선택
제목과 본문의 구현을 어떻게 해야할지에 대한 고민이 있었습니다. 제목 `textView`와 본문 `textView`를 나누고 `stackView`에 넣어줄 경우 여러가지 문제점이 생겼습니다. 
1. 제목에 특정한 제약을 주지 않아 길어지게 되서 한 화면을 전부 차지하게 될 경우 본문 `textView`로 넘어갈 수가 없다.
2. 본문 `textView`를 스크롤 할 경우 제목은 올라가지 않고 계속 남아있게 된다.

위 문제들로 인해 `textView`를 제목과 본문으로 나누지 않고 `contentTextView`라는 하나의 `textView`에서 제목과 본문을 모두 입력받을 수 있게 변경하여 처리했습니다.

---

## 해결하지 못한 점
### 1️⃣ textView 조건 처리
요구사항에 있는 일기장 하나의 페이지가 스크롤 된 짤이 없어서 본문을 스크롤 하여 올릴 때 제목도 같이 올라가는지에 대한 여부가 불분명했습니다. 때문에 저희는 같이 올라간다고 가정하고, 하나의 textView로 처리하였습니다. 다만 이 경우에 제목과 본문을 구분하는 것에 있어서 추후에 수정하면 불분명해질 수 있다는 문제점이 있었는데, 두번째 줄을 항상 공백으로 두는 방법을 찾지 못했습니다.

---

## 조언을 얻고 싶은 점
### 1️⃣ iOS 타겟 버전 변경 - UIKeyboardLayoutGuide
키보드를 사용할 때 글자를 가리는 일이 생겨 `textView`도 같이 올려주는 방법에 대한 고민이 있었습니다. 그 중에서도 두가지 방법을 찾을 수 있었습니다.
1. `Notification`을 사용하여 키보드가 올라올 때마다 키보드의 `contentInset`을 빼주는 방법
2. `keyboardLayoutGuide`를 제약 조건에 적용하는 방법

간단하기는 2번이 간단했지만 `iOS 15.0` 부터 사용할 수 있어 고민이 있었습니다.

1번의 방법을 사용할 때 `keyboardFrameEndUserInfoKey`을 사용합니다. 그런데 [keyboardFrameEndUserInfoKey](https://developer.apple.com/documentation/uikit/uiresponder/1621578-keyboardframeenduserinfokey) 공식문서를 보면 다음 내용이 있었습니다.
> Important
>
> Instead of using this key to track the keyboard’s frame, consider using UIKeyboardLayoutGuide, which allows you to respond dynamically to keyboard movement in your app. For more information, see [Adjusting Your Layout with Keyboard Layout Guide](https://developer.apple.com/documentation/uikit/keyboards_and_input/adjusting_your_layout_with_keyboard_layout_guide).
> 
> 이 키를 사용하여 키보드 프레임을 추적하는 대신 앱의 키보드 움직임에 동적으로 반응할 수 있는 UIKeyboardLayoutGuide를 사용하는 것이 좋습니다. 자세한 내용은 [키보드 레이아웃 가이드로 레이아웃 조정](https://developer.apple.com/documentation/uikit/keyboards_and_input/adjusting_your_layout_with_keyboard_layout_guide)을 참조하세요.
 
[UIKeyboardLayoutGuide](https://developer.apple.com/documentation/uikit/uikeyboardlayoutguide)는 `iOS 15.0` 이후로 지원하기 때문에 2번의 방법을 선택하여 진행했습니다.

올라프라면 이 경우에 어떤 방법을 선택하실지 의견을 듣고 싶습니다.


# 일기장 [STEP 2] MINT, BMO

## 고민했던 점
### 1️⃣ **UUID** <br>

일기를 수정﹒삭제 하기 위해 `CoreData`로부터 일기를 구분하기 위한 `Identifier`가 필요했습니다.
일기 `Entity`의 `Attribute`에 있는 값은 `title: String`, `body: Stirng?`, `date: Date` 세가지 였는데, 이 중 무엇하나 식별자로 사용 가능한 값이 없었습니다. 일기 제목, 내용, 작성날짜 모두 같은 일기가 존재 할 수 있었기 때문입니다. `Entity`의 `Attribute`에 `id: UUID`를 추가했습니다. `UUID`는 범용 고유 식별자로 단순히 이것을 추가하는 것만으로 각각의 일기를 명확하게 구분할 수 있게 됩니다. `UUID`를 가지고 있는 경우 해당 `Entity`의 `title`, `body`의 내용이 바뀌어도 새로운 객체나 다른 객체에 내용이 작성되는 것이 아닌 현재의 객체에 정확하게 내용이 작성﹒업데이트 되는 것을 확인할 수 있었습니다.

<br>

### 2️⃣ **UIContextualAction 커스텀** <br>
`UIContextualAction`의 `activityItems`에 일기 제목, 일기 내용을 넣을 경우 공유 될 내용이 아래 이미지처럼 미리보기로 출력됩니다.
![](https://hackmd.io/_uploads/BJidTmuA3.png)
이 문제를 해결하기 위해 일기 제목과 일기 내용을 하나의 문자열로 합치고, 중간에 줄바꿈 기호 `\n`을 추가하는 방식을 적용 해봤습니다.
문자열은 원하는 형태로 출력되었으나 폰트 스타일은 여전히 저희가 원하는 형태가 아니었습니다.
|현재 출력 방식|내용 합치고 줄바꿈 적용 시|원하는 표현 방식|
|-|-|-|
|**일반 텍스트**<br>일기 제목 및 일기 내용|일기 제목<br>일기 내용|**일기 제목**<br>일기 내용|

고민하다, UIActivityItemSource 및 LinkPresentation 프레임워크를 이용하였습니다. 기존 방법에서 출력되는 Activity Items는 실제로 공유할 내용에 대한 것들만 미리보기로 출력할 수 있었습니다. 때문에 UIActivityItemSource로는 공유할 내용을 따로 지정하고 LinkPresentation 프레임워크는 원하는 표현 방식으로 미리보기를 구현하는 과정에서 필요를 느껴 import 해주었습니다.

![](https://hackmd.io/_uploads/H1Q3W4uC3.png)

먼저 UIActivityItemSource 프로토콜을 채택하면 공유할 개체가 데이터 공급자가 되어 항목에 대한 액세스 권한을 View Controller에 제공합니다.

- 공유할 데이터에 대해 placeHolder로 사용할 수 있는, 실제 데이터는 아니지만 그에 가깝게 표시하는 값을 return 합니다.

    ```swift
    func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
            return diary.title
        }
    ```
- 공유하고자 하는 데이터를 리턴합니다.

    ```swift
    func activityViewController(
        _ activityViewController: UIActivityViewController,
        itemForActivityType activityType: UIActivity.ActivityType?
    ) -> Any? {
        let dateFormatter = DateFormatter()
        
        dateFormatter.configureDiaryDateFormat()
        
        let formattedDate = dateFormatter.string(from: diary.date)
        let sharedData = "\(formattedDate)\n\n\(diary.title)\n\n\(diary.body ?? "")"
        
        return sharedData
    }
    ````
- LinkPresentation 프레임워크가 기본 컴포넌트로 존재하고 있어서 사용해보았습니다. 해당 프레임워크는 메타 데이터를 활용하여 원하는 데이터를 유저에게 표시하며 쉽게 공유할 수 있게 만들어 줍니다. 이를 UIActivityItemSource에 구현되어 있는 메서드와 함께 사용하면 공유할 때 커스텀한 미리보기를 사용할 수 있습니다.

    ```swift
    func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
        let metadata = LPLinkMetadata()
        
        metadata.title = diary.title
        metadata.originalURL = URL(fileURLWithPath: (diary.body ?? ""))
        
        return metadata
    }
    ```

## 조언을 얻고 싶은 점
### LinkPresentation 사용 시 URL
`metadata`를 설정해줄 때 제목이 아닌 본문을 밑에 나오도록 커스텀하는 경우 `URL`을 만들어서 넣어주어야 했습니다. 실제로 해당 타입이 필요해서 사용하는 것이 아니라 원하는 커스텀만을 위해서 `String`을 `URL`로 변환하여 넣어주는 건데 이러한 방식이 괜찮을까요? 타입이 존재함에도 해당 타입에 맞지 않게 사용했다는 생각이 듭니다. 

