# 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`로 변환하여 넣어주는 건데 이러한 방식이 괜찮을까요? 타입이 존재함에도 해당 타입에 맞지 않게 사용했다는 생각이 듭니다.