# 🚩만국박람회 - MINT, Yetti ![](https://hackmd.io/_uploads/By4pbvLu3.png) ## Ground Rules ### 규칙 - TIL, 일일 회고 작성 시간(매일 23시부터 1시간 작성 진행) - 월, 목 10 - 2시는 활동학습 예습 시간 ### 스크럼 - 오전 10시 디스코드에서 진행 - 금일 진행 사항 공유하기(오늘의 할일) ### 프로젝트 규칙 - 네이밍 준수하기(가이드 라인) - 커밋 메시지 규칙 진행 - 코드에 대한 기록 그때 그때 하기 - 중간중간 리드미 작성 ### 팀 규칙 - 컨디션이 좋지 않을 때는 꼭 말해주기!! ## 일일 스크럼 ### 🙌 06/26 월 - 오늘의 컨디션 - MINT😈: 감기 때문에 목이 조금 아파요😭 - Yetti🦊: 좋은데 어깨가 좀 아프네요🫠 - 특이사항 - MINT😈: 오늘 저녁 소고기🤗 - Yetti🦊: 와 부럽다... - 오늘 할 일 - [x] JSON decoding 공부 - [x] 모델 타입 class와 struct 중 선택 ### 🙌 06/27 화 - 오늘의 컨디션 - MINT😈: 거의 8시간을 채워 자서 행복해요🤗 - Yetti🦊: 예티는 늦게자서 목이 잠겼어🥲 - 특이사항 - MINT😈: 날이 끕끕해요😵 - Yetti🦊: 안 더우면 좋겠어요 - 오늘 할 일 - [x] STEP 2 내용 구상 - [x] Scroll View 공부 - [x] Stack View 공부 - [x] JSON 사용 방법 다시 한 번 ### 🙌 06/28 수 - 오늘의 컨디션 - MINT😈: 저도 오늘 하루가 상쾌해요~ ☀️ - Yetti🦊: 너무 좋습니다 안 더우면 계속 좋을듯😂 - 특이사항 - MINT😈: 저녁 외식! 오늘부터 24살👻 - Yetti🦊: 6시 반 이후에 약속 있어요~! 오늘부터 27살🫠 - 오늘 할 일 - [x] navigation controller 공부 - [x] 화면 전환 구현 - [x] table view 공부 - [x] 2번째 화면 구현 ### 🙌 06/29 목 - 오늘의 컨디션 - MINT😈: 쏘 배드😭 계속되는 감기🥶 - Yetti🦊: 아침에 피곤했지만 지금 괜찮아요😊 - 특이사항 - MINT😈: 병원을 갔다올래요🚑 - Yetti🦊: 비가 와서 집에 박혀있을래요🏠 - 오늘 할 일 - [x] 화면 전환 구현 - [x] 3번째 화면 구현 - [ ] 활동학습 예습 ### 🙌 06/30 금 - 오늘의 컨디션 - MINT😈: 이상하게 괜찮지만 잠이 조금 필요한 것 같기도 해요🫠 - Yetti🦊: 푹 잔거 같아서 너무 상쾌하요!🎉 - 특이사항 - MINT😈: 뾰루지가 하나 올라왔어요 흑흑😥 - Yetti🦊: 리드미작성 마치고 프로젝트에 대해 더 공부해보겠습니다🙂 - 오늘 할 일 - [x] 리펙토링 - [x] 리드미 작성 - [x] table view 공부 - [x] navigation Bar 공부 - [x] 뷰컨끼리의 데이터 전달방식 여러가지 공부해보기 - [x] step2 PR보내기 ### 🙌 07/03 월 - 오늘의 컨디션 - MINT😈: 아주 약간의 근육통💪 - Yetti🦊: 더워서 미치겠어여☀️ - 특이사항 - MINT😈: 학습활동 내용 하나도 모르겠읍니다..🙀 - Yetti🦊: 저두요!...! - 오늘 할 일 - [x] 코멘트 확인 후 수정사항 반영 - [x] 활동학습 예습 및 복습 - [ ] 오토 레이아웃 공부 ### 🙌 07/04 화 - 오늘의 컨디션 - MINT😈: 사망 직전 - Yetti🦊: 많이 자서 개운해요😎 - 특이사항 - MINT😈: 중간에 낮잠을..🌚2시간까지 가능 - Yetti🦊: 더우면 중간에 카페가서 공부할 예정이에요🥵 - 오늘 할 일 - [x] Accessibility(Dynamic Type) 공부하기 - [x] 가능하면 STEP3 해보기 - [x] 참고링크 숙지하기 ### 🙌 07/05 수 - 오늘의 컨디션 - MINT😈: 잠이 부족한데도 상쾌! 왤까요... - Yetti🦊: 잠 잘자서 아주 좋아요!! - 특이사항 - MINT😈: 오뉴가 귀여워🐶 - Yetti🦊: 저도 키우고 싶어요🥲 - 오늘 할 일 - [x] PR 보내기 - [x] 개인 공부 ### 🙌 07/06 목 - 오늘의 컨디션 - MINT😈: 언제나 졸린 하루 - Yetti🦊: 에어컨 틀면 춥고 끄면 더워서 짜증나요.. - 특이사항 - MINT😈: 맛있는 저녁을 냠냠 - Yetti🦊: 햄버거 냠냠 - 오늘 할 일 - [x] PR 코멘트 반영 - [x] 활동학습 예습 - [x] autolayout 수정 ### 🙌 07/07 금 - 오늘의 컨디션 - MINT😈: 꽤나 해피해피😈 - Yetti🦊: 오늘따라 집중력이 꽝🥹 - 특이사항 - MINT😈: 코...드베이스...🫠 - Yetti🦊: 스...터디준비...망..🫠 - 오늘 할 일 - [ ] 코드베이스로 바꾸기 - [ ] 내일 스터디 준비 # 💌서로를 향한 기록💌 ## To. MINT😈 안녕하세요 민트~ 2주간의 프로젝트동안 정말 고생많으셨어요! 프로젝트 기간동안 제가 많이 부족한 부분들을 민트에게 배울 수 있는 기간이 된 것 같아 정말 좋았습니다! 제가 평소엔 먼저 말을 잘 못거는 스타일이라 저와 비슷한 분을 만나면 많이 어색했었는데 민트가 너무 성격도 좋으시고 먼저 잘 다가와주셔서 2주간 정말 즐거운 프로젝트 기간이었습니다.😊 캠프 기간동안 또 제 질문을 잘 받아주실 선생님이 한명 생긴것 같아서 기분이 좋네요 ㅋㅋㅋㅋ 이번 프로젝트 중에 가장 큰 배움 중 하나는 민트의 문제 해결에 대한 열정과 민트만의 커뮤니케이션?인 것 같아요. 상대방이 편하게 대화할 수 있게 해주는 건 전 정말 큰 능력이라고 생각합니다. 만국박람회 프로젝트는 끝나지만 캠프 기간동안 계속 잘 부탁드려요 민트!!😈 ## To. Yetti🦊 안녕하십니까 예티! 민트입니다.😈 3주보다는 짧고 1주보다는 긴, 소중한 2주의 시간동안 예티와 함께 팀을 할 수 있어서 저는 참 좋았어요. 전까지 접점이 적었기에 예티에 대해 몰랐던 부분들도 더 많이 알게 된 것 같아요. 언제나 성실하고, 집중해야할 때 집중하며, 어렵다고 생각한 것들을 끊임없이 고뇌하더니 결국 익히는 모습들이 정말 멋있다고 생각합니다. 오토 레이아웃에 관해서도 예티에게 엄청 배웠어요! 고마워요🤗 다음주부터 다른 팀이라고 하니 괜히 아쉬우면서도 여전히 같은 스터디라 다행입니다.☺️ 다른 팀이 되더라도 여유가 생기면 자주자주 찾아갈게요! 2주간 너무 고마웠고, 앞으로 스터디원으로서도 잘 부탁해요, 예티❣️ # 🚩만국박람회 - MINT😈, Yetti🦊 > 프로젝트 기간 23/06/26 ~ 23/07/07 ## 📖 목차 🍀 1. [소개](#-소개) <br> 👨‍💻 2. [팀원](#-팀원) <br> 🕰️ 3. [타임 라인](#-타임-라인) <br> 👀 4. [시각화된 프로젝트 구조](#-시각화된-프로젝트-구조) <br> 💻 5. [실행 화면](#-실행-화면) <br> 🧨 6. [트러블 슈팅](#-트러블-슈팅) <br> 📚 7. [참고 링크](#-참고-링크) <br> 👥 8. [팀 회고](#-팀-회고) <br> </br> ## 🍀 소개 🌿민트초코 민트😈와 🌠슈팅스타 예티🦊가 만든 만국 박람회 팜플렛 앱! <br> 1900년 파리에서 열린 만국 박람회와 관련한 한국의 출품작들을 확인해볼 수 있습니다! 🇰🇷 ⚠️ 주의 ⚠️ - 오로지 한국의 출품작 목록만을 확인 할 수 있습니다. - 혹시라도 다른 나라의 출품작이 궁금하시다면... 잘못된 접근입니다. 404 NOT Found🫣 </br> ## 👨‍💻 팀원 | 🦊Yetti🦊 | 😈MINT😈 | | :--------: | :--------: | | <Img src = "https://hackmd.io/_uploads/BJ-7TbPu3.png" width="200" height="200"> | <Img src = "https://hackmd.io/_uploads/rJKgcQ2dh.png" width="200" height="200"> | |[Github Profile](https://github.com/iOS-Yetti) |[Github Profile](https://github.com/mint3382) | </br> ## ⏱️ 타임라인 |날짜|내용| |:--:|--| |2023.06.26.| - Model 구현 | |2023.06.27.| - mainView 구현 <br> - Scroll View 구현 <br> - Stack View 구현 <br> - JSON decoding extension으로 구현 | |2023.06.28.| - navigation controller로 화면전환 구현 <br> - EntryListView 구현 <br> - Table View 구현 <br> - Table View Cell Custom | |2023.06.29.| - EntryDetailViewController 구현 <br> - EntryDetalView 구현 <br> - EntryListViewController에서 각 cell 선택시 화면 전환 구현 <br> - Int extension 구현 <br> - UIImageView extention 구현 | |2023.06.30.| - Decodable+ 리펙토링 <br> - 네이밍 및 컨벤션 수정 | |2023.07.03.| - Data unwrap을 타입 메서드로 수정 | |2023.07.04.| - Dynamic Type 구현 <br> - AppDelegate 사용하지 않는 메서드 삭제 <br> - AppDelegate의 supportedInterfaceOrientationsFor메서드 통해 첫화면만 세로방향 고정 | |2023.07.05.| - 불필요한 var를 let으로 전환 <br> - EntryListViewController autolayout 수정 | |2023.07.06.| - Accessibility 공부 <br> - autolayout 수정 | |2023.07.07.| - 리드미 작성 <br> - tableView 공부 | </br> ## 👀 시각화된 프로젝트 구조 ### ℹ️ File Tree ```` Expo1900 ├── Error │ └── DecoderError ├── Extension │ ├── Decodable+ │ └── Int+ ├── Model │ ├── Entry │ ├── ExpoGuide │ └── Data ├── View │ ├── Main │ └── LaunchScreen ├── Controller │ ├── AppDelegate │ ├── SceneDelegate │ ├── MainViewController │ ├── EntryListViewController │ ├── EntryTableViewCell │ └── DetailEntryViewController ├── Assets └── Info ```` ### 📐 Diagram <p align="center"> <img width="800" src= "https://hackmd.io/_uploads/H14tm-VK2.png" > </br> ## 💻 실행 화면 | 메인 화면 | 출품작 목록 화면 | 출품작 상세 화면 | |:--------:|:--------:|:--------:| |<img src="https://velog.velcdn.com/images/mintsong/post/e08d859b-79aa-4e76-8e8b-0110982f1960/image.gif" width="250">|<img src="https://velog.velcdn.com/images/mintsong/post/8afba4ca-5f3e-453a-a8c3-8ca1014896a2/image.gif" width="250">|<img src="https://velog.velcdn.com/images/mintsong/post/bddda467-f716-4e1f-9829-a3f0eb186ac4/image.gif" width="250">| | 메인 화면 다이나믹 적용 | 출품작 목록 화면 다이나믹 적용 | 출품작 상세 화면 다이나믹 적용 | |:--------:|:--------:|:--------:| |<img src="https://velog.velcdn.com/images/mintsong/post/120a591f-f7c9-47bb-9ae4-7310b5d83ba3/image.gif" width="250">|<img src="https://velog.velcdn.com/images/mintsong/post/fb5c327f-3d03-47f8-b2a7-1833ff7b0529/image.gif" width="250">|<img src="https://velog.velcdn.com/images/mintsong/post/5ecb90bc-2b3e-49d9-8bb9-3ab8dd66429d/image.gif" width="250">| | 출품작 목록 화면으로 전환 | 출품작 상세 화면으로 전환 | |:--------:|:--------:| |<img src="https://hackmd.io/_uploads/H1pGUz2_2.gif" width="250">|<img src="https://velog.velcdn.com/images/mintsong/post/e423875b-dacd-45a4-84df-3f6d25b68037/image.gif" width="250">| | 메인 화면으로 돌아가기 | 출품작 목록 화면으로 돌아가기 | |:--------:|:--------:| |<img src="https://velog.velcdn.com/images/mintsong/post/675745de-45ef-4915-9757-6248b2cd74a2/image.gif" width="250">|<img src="https://hackmd.io/_uploads/H1xkwfnOn.gif" width="250">| </br> ## 🧨 트러블 슈팅 ###### 핵심 트러블 슈팅위주로 작성하였습니다. 1️⃣ **superView까지 스크롤한 내용이 보이게 구현하는 방법** <br> - 🔒 **문제점** <br> 네비게이션을 이용해 화면전환이 이루어지기 때문에 네비게이션 컨트롤러를 사용하였습니다. 하지만 첫 화면에선 스크롤 했을 때 글자가 슈퍼 뷰까지 보여야 하는데 네비게이션 컨트롤러로 인해 자동적으로 생기는 네비게이션 바가 있었기 때문에 구현 시에 원하는 화면이 나오지 않았습니다. 추가적으로 각 뷰컨트롤러마다의 네비게이션바의 여부가 달랐기 때문에 어떤 방식을 사용해 네비게이션 바를 필요할 때만 보여줄 수 있을까에 대해 고민하였습니다. 🔑 **해결방법** <br> `isNavigationBarHidden`를 활용해 `MainViewController`에서 네비게이션 바를 없애주었고 `viewWillAppear`와 `viewWillDisAppear` 메서드 내부에서 `isNavigationBarHidden`를 활용해 네비게이션 바를 없앴다가 뷰가 전환될 때 네비게이션 바가 나타날 수 있게 해주었습니다. ```swift override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.isNavigationBarHidden = true } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationController?.isNavigationBarHidden = false } ``` <br> 2️⃣ **메인 viewController에서 요구사항 구현** <br> - 🔒 **문제점** <br> 1. 머리말 구현 요구사항에 보면 mainViewController의 view에서는 ExpoGuide의 visitors, location, duration에 관한 정보들이 무엇인지 명시해주고 있습니다. ![](https://hackmd.io/_uploads/SkvPqTjd2.png) 때문에 이 부분들은 받아와서 decode된 data를 그대로 쓰는 것이 아니라 약간의 변형이 필요했습니다. 2. 방문객 수 표현 다만 방문객 수를 표현하는 부분에서 고민이 있었습니다. 방문객 수는 `1. decimal style로 표현`되어 있고, `2. "명"이라는 String`도 붙여주어야 했습니다. 🔑 **해결방법** <br> 1. Label 사용 새로운 label을 stackView에 넣어서 View에서부터 선언해주었습니다. ![](https://hackmd.io/_uploads/HyTPyRi_h.png) 2. Int extension의 Numberfomatter와 연산 프로퍼티 사용 일단 이 내용을 label의 .text로 넣는 것이기에 Int 타입인 `visitors`를 Int의 extension안에 numberFormatter를 사용하여 Decimal style로 변환해 주었습니다. 처음에는 `2. 뒤에 "명"이라는 String`을 붙여주는 것을 ViewController에서 visitorsLabel.text = visitors + "명"의 형식으로 바로 넣어주려고 하였습니다. 그러나 앞의 visitors를 가지고 있는 ExpoGuideData가 옵셔널 타입이라 바인딩이 필요했습니다. 때문에 ExpoGuide 구조체에 연산 프로퍼티를 만들어 뒤에 "명"을 넣어 반환해주는 방법을 사용하였습니다. ```swift var visitorsText: String { return visitors.changeToDecimalStyle() + " 명" } ``` <br> 3️⃣ **instantiateViewController를 이용한 데이터 전달** <br> - 🔒 **문제점** <br> `EntryListViewController`에서 사용되는 데이터가 `DetailEntryViewController`에서도 사용되고 있었기 때문에 `DetailEntryViewController` 내에서 다시 데이터를 불러오는 것이 효율적이지 못하다 생각했습니다. 🔑 **해결방법** <br> 저희가 선택한 방법은 init을 구현하여 data를 주입해주는 것입니다. 이때 `EntryListViewController`에서 `DetailEntryViewController`를 구현한 init을 통해 생성할 때 `instantiateViewController`메서드의 `creator:`를 할용해 data를 주입할 수 있도록 구현하였습니다. ```swift // EntryListViewController func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let entryDetailViewController = self.storyboard?.instantiateViewController(identifier: "detailEntryViewController", creator: { coder in DetailEntryViewController(coder: coder, data: self.entryData[indexPath.row]) }) else { return } navigationController?.pushViewController(entryDetailViewController, animated: true) } // DetailEntryViewController // data를 주입받을 이니셜라이저 init?(coder: NSCoder, data: Entry) { self.entryData = data super.init(coder: coder) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } ``` <br> 4️⃣ **JSON data Decoding하여 사용하기** <br> - 🔒 **문제점** <br> 얻은 JSON data 매번 JSONDecoder 인스턴스를 생성하거나 do-catch를 통해 data를 unwrap하여 사용해야했습니다. 또한 길게 적다보면 이 작업이 무엇을 위한 것인지 한눈에 바라보기가 어려웠습니다. 🔑 **해결방법** <br> extension과 type 메서드로 하나씩 빼서 처리하였습니다. Data라는 구조체 안에 있는 타입 메서드 unwrap을 사용하면 Decodable에 extension으로 구현하고 있는 decode 메서드를 통해 data를 받아와 do - catch의 작업을 처리하여 반환합니다. 에러가 생겨서 catch로 빠진 경우는 매개변수를 통해 받은 초기값을 반환합니다. ```swift struct Data { static func unwrap<T: Decodable>(initialValue: T, file: String) -> T { do { let data = try T.decode(file) return data } catch { print(error.localizedDescription) return initialValue } } } ``` Decodable의 extension으로 Decodable 프로토콜을 채택하고 있는 경우는 decode 메서드를 통해 매개변수와 이름이 같은 파일에 있는 dataAsset을 가져올 수 있고 이는 JSONDecoder를 통해 decode됩니다. ```swift extension Decodable { static func decode(_ file: String) throws -> Self { guard let dataAsset = NSDataAsset(name: file) else { throw DecodeError.noFile } return try JSONDecoder().decode(Self.self, from: dataAsset.data) } } ``` 5️⃣ **뷰 마다 가로모드 지원여부 구분하기** <br> - 🔒 **문제점** <br> 첫 번째 뷰에서는 가로모드 지원이 불가능하게 구현한 후 서브 뷰들에서는 가로모드도 지원하도록 구현하는 과정에서 `AppDelegate`파일 내부에서 `isOnlyPortaitOrientation` 라는 Bool값 변수를 활용하여 true일 때는 세로모드만 가능하고 false일 때는 가로, 세로 모두 지원하도록 로직을 구성하였습니다. ```swift! class AppDelegate: UIResponder, UIApplicationDelegate { var isOnlyPortaitOrientation: Bool = false func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { if isOnlyPortaitOrientation { return .portrait } else { return .all } } } ``` 하지만 첫화면에서 넘어갈 때는 문제가 없었지만 2번째 뷰에서 첫번째 뷰로 넘어갈 때 첫번째 뷰가 가로모드가 되어버리는 문제가 있었습니다. 🔑 **해결방법** <br> 첫번째 뷰에선 viewDidLoad()는 처음에만 호출되고 다시 돌아왔을 때 호출되지 않았기 때문에 `isOnlyPortaitOrientation`변수의 값 변경을 viewDidLoad()가 아닌 viewWillAppear()에서 해주었습니다. ```swift! override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.isNavigationBarHidden = true appDelegate.isOnlyPortaitOrientation = true } ``` <br> ## 📚 참고 링크 - [🍎Apple Docs: ContentInset](https://developer.apple.com/documentation/uikit/uiscrollview/1619406-contentinset) - [🍎Apple Docs: NavigationController](https://developer.apple.com/documentation/uikit/uinavigationcontroller) - [🍎Apple Docs: UIImageView](https://developer.apple.com/documentation/uikit/uiimageview) - [🍎Apple Docs: Localizederror](https://developer.apple.com/documentation/foundation/localizederror) - [🍎Apple Docs: init(coder)](https://developer.apple.com/documentation/foundation/nscoding/1416145-init) - [🍎Apple Docs: UIAppDelegate](https://developer.apple.com/documentation/uikit/uiapplicationdelegate) - [🍎Apple Docs: Accessibility](https://developer.apple.com/documentation/accessibility) - [🍏Apple Archaive: NSCoding](https://developer.apple.com/library/archive/documentation/LegacyTechnologies/WebObjects/WebObjects_3.5/Reference/Frameworks/ObjC/Foundation/Protocols/NSCoding/Description.html#//apple_ref/occ/intf/NSCoding) - <Img src = "https://hackmd.io/_uploads/SkQWz1wd2.png" width="20"/> [네이버 부스트코스: 기상정보 애플리케이션 ](https://www.boostcourse.org/mo326/joinLectures/12973?isDesc=false) - <Img src = "https://github.com/mint3382/ios-calculator-app/assets/124643545/56986ab4-dc23-4e29-bdda-f00ec1db809b" width="20"/> [야곰닷넷: 오토레이아웃 정복하기 ](https://yagom.net/courses/autolayout/lessons/dynamic-type/) - <Img src = "https://hackmd.io/_uploads/ByTEsGUv3.png" width="20"/> [blog: static metatype](https://sujinnaljin.medium.com/swift-self-type-protocol-self%EA%B0%80-%EB%AD%94%EB%94%94%EC%9A%94-7839f6aacd4) - <Img src = "https://hackmd.io/_uploads/ByTEsGUv3.png" width="20"/> [blog: ]() - <Img src = "https://hackmd.io/_uploads/ByTEsGUv3.png" width="20"/> [blog: ]() - <Img src = "https://hackmd.io/_uploads/ByTEsGUv3.png" width="20"/> [blog: ]() </br> ## 👥 팀 회고 - [팀 회고 링크](https://github.com/iOS-Yetti/ios-exposition-universelle/wiki) ---PR--- # PR -- # STEP 1 ## 고민했던 점 ### expo_assets 파일 구성 주어진 expo_assets 파일을 열어보며 고민하는 시간이 있었습니다. STEP 2 안내 내용을 보면 총 3개의 화면을 구현해야 함을 알 수 있었습니다. 1. 메인 화면 2. 출품작 목록 화면 3. 출품작 상세 화면 또한 이러한 화면들에서 각각 어떤 파일들을 사용하는지 정리해 보았습니다. 1. 메인 화면: `poster.imageset`, `exposition_universelle_1900.dataset`, `flag.imageset` 2. 출품작 목록 화면: `출품작들의 imageset`, `items.dataset(desc 제외)` 3. 출품작 상세 화면: `출품작들의 imageset`, `items.dataset(image와 desc만)` ### Codable protocol Codable은 encoding과 decoding에 사용되는 protocol입니다. > 스위프트 타입의 인스턴스를 encoding 할 수 있는 protocol : Encodable > 스위프트 타입의 인스턴스로 decoding 할 수 있는 protocol : Decodable 이렇게 두 protocol을 모두 통용하여 사용되어 지는 것이 protocol인데 저희는 encoding을 하는 경우는 없기에 Codable에서 Decodable로 변경하였습니다. ### Coding Key 스위프트와 다르게 자바 스크립트에서는 언더바를 사용해 이름을 짓습니다. 이때 스위프트의 네이밍 컨벤션을 따라 각각의 프로퍼티의 이름이 어떻게 일치하는지 명시해주기 위해 coding key를 사용하였습니다. ```swift enum CodingKeys: String, CodingKey { case imageName = "image_name" case shortDescription = "short_desc" case name case description } ``` ## 조언을 얻고 싶은 점 ### class와 struct Codable은 protocol이기에 struct, class, enum 에서 전부 채택 가능합니다. 그렇다면 어떠한 기준으로 타입을 선택해야할지에 대한 고민이 있었습니다. 간단한 내용이고 상속이 필요치 않기에 struct를 사용하였습니다. 또한 여러가지 타입이 혼합된 경우도 아니기에 enum도 배제하였습니다. 그런데 참조 타입과 값 타입의 차이로 선택하는 경우는 어떠한 것이 있는지 잘 감이 오지 않습니다. 이러한 JSON decoding의 경우에도 값 타입과 참조 타입이 선택에 영향을 미치는 일이 있는지 궁금합니다. # Step2 ## 고민했던 점 ### `1. isNavigationBarHidden` 각 뷰컨트롤러마다 네비게이션바의 여부가 달랐기 때문에 어떻게 네비게이션바를 넣고 빼야 할지에 대해 고민했습니다. 저희가 선택한 방법은 `MainViewController`의 `viewWillAppear`와 `viewWillDisAppear`를 활용하는 것입니다. `viewWillAppear`가 호출될 때 `isNavigationBarHidden`에 true를 할당해주고 `viewWillDisAppear`가 호출될 때 false를 할당해주어 네비게이션바가 `EntryListViewController`부터 보일 수 있도록 해주었습니다. ### `2. 연산 프로퍼티를 이용한 String 반환과 Label의 사용` 요구사항에 보면 mainViewController의 view에서는 ExpoGuide의 visitors, location, duration에 관한 정보들이 무엇인지 명시해주고 있습니다. ![](https://hackmd.io/_uploads/SkvPqTjd2.png) 때문에 이 부분들은 받아와서 decode된 data를 그대로 쓰는 것이 아니라 약간의 변형이 필요했습니다. 그 중에서도 앞에 명시해주는 부분은 새로운 label을 stackView에 넣어서 View에서부터 선언해주었습니다. ![](https://hackmd.io/_uploads/HyTPyRi_h.png) 다만 방문객 수를 표현하는 부분에서 고민이 있었습니다. 방문객 수는 `1. decimal style로 표현`되어 있고, `2. 뒤에 "명"이라는 String`도 붙여주어야 했습니다. 일단 이 내용을 label의 .text로 넣는 것이기에 Int 타입인 `visitors`를 Int의 extension안에 numberFormatter를 사용하여 Decimal style로 변환해 주었습니다. 처음에는 `2. 뒤에 "명"이라는 String`을 붙여주는 것을 ViewController에서 visitorsLabel.text = visitors + "명"의 형식으로 바로 넣어주려고 하였습니다. 그러나 앞의 visitors를 가지고 있는 ExpoGuideData가 옵셔널 타입이라 바인딩이 필요했습니다. 때문에 ExpoGuide 구조체에 연산 프로퍼티를 만들어 뒤에 "명"을 넣어 반환해주는 방법을 사용하였습니다. ```swift var visitorsText: String { return visitors.changeToDecimalStyle() + " 명" } ``` ### `3. instantiateViewController를 이용한 데이터 전달` `EntryListViewController`에서 `DetailEntryViewController`로 데이터를 전달하는 과정에 대해 고민했습니다. 많은 방법 중에 저희가 선택한 방법은 init을 구현하여 data를 주입해주는 것입니다. 이때 `EntryListViewController`에서 `DetailEntryViewController`를 구현한 init을 통해 생성할 때 `instantiateViewController`메서드의 `creator:`를 활용하였습니다. `creator:`매개변수는 뷰 컨트롤러를 생성하고 커스텀 초기화한 후 반환합니다. 그리고 `NSCoder`타입을 만들어 ViewController를 반환하는 클로저의 형태입니다. 뷰컨트롤러에서 생성자를 만들 때 스토리보드로 만들면 자동으로 `NSCoder`가 들어간 생성자가 호출되기 때문에 `creator:`에서 원하는 데이터를 가져올 수 있었습니다. `NSCoder`는 스토리보드를 사용할 때 인코딩과 디코딩을 지원해주는 클래스입니다. 스토리보드의 nib 파일을 decode해서 사용할 수 있게 만들어줍니다. 또한 `DetailEntryViewController`에서 `required init`을 구현해주어야 했습니다. 이는 `viewController`가 상속받고 있는 `UIViewController`는 `NSCoding protocol`을 채택하고 있는데 이 `NSCoding protocol`은 반드시 `init`을 구현해주어야 합니다. 때문에 `required init`을 선언해주었습니다. 그러나 저희가 생성한 `init`이 아닌 `required init`으로 구현되면 안되기 때문에 `fatalError("init(coder:) has not been implemented")`를 통해 <절대 이곳으로는 오면 안된다>는 것을 표현해주었습니다. ### `4. LocalizedError` JSON파일을 디코딩하는 과정에서 에러가 발생한다면 catch 문을 통해 해당 에러를 출력하게 해주고 싶었습니다. 단순히 `print(error)`를 해도 error의 이름이 콘솔에 출력되면서 정보를 잘 확인할 수 있지만 에러의 이름을 한글로 바꿔주는 과정에서 LocalizedError protocol을 채택해 errorDescription을 사용해 보았습니다. error가 지금처럼 하나만 있는 것이 아니라 많이 있을 때 사용하면 더 편리할 것 같다는 생각을 하였습니다. 원래 LocalizedError의 목적은 영어권에서는 영어로, 한국에서는 한글로 유저에게 해당 에러를 보여주는 것으로 이해했습니다. ## 조언을 얻고 싶은 점 ### extension으로 decode 구현 `JSONDecoder`의 decode를 사용할 때 매번 `NSDataAsset`을 옵셔널 바인딩을 해주어야 합니다. 이를 항상 구현할 필요 없이 extension으로 선언해놓고 해당 메서드를 가져다가 쓰는 방법을 고민해 보았습니다. 두가지의 extension을 생각할 수 있었는데, 하나는 `Decodable`이라는 타입에서 decode를 할 수 있는 경우이고 다른 하나는 `JSONDecoder()` 인스턴스에서 decode를 해주는 경우입니다. 1. Decodable+ ```swift extension Decodable { static func decode(file: String) throws -> Self { guard let dataAsset = NSDataAsset(name: file) else { throw DecodeError.noFile } return try JSONDecoder().decode(Self.self, from: dataAsset.data) } } ``` Decodable을 채택하고 있는 타입에서 사용할 수 있습니다. 이 타입의 data를 decode를 통해서 채워넣겠다는 느낌으로 생각했습니다. 가독성의 측면에서 어색한 부분이 있다고도 생각되어지지만, `ExpoGuide.decode(file: "exposition_universelle_1900")`으로 사용하였을 때 ExpoGuide의 decode를 통해서 파일 exposition_universelle_1900의 정보들을 가져오겠다,라고 읽을 수 있다고 생각해서 이 방법을 선택하였습니다. 2. JSONDecoder+ ```swift extension JSONDecoder { func decode<T: Decodable>(_ type: T.Type, from assetName: String) throws -> T { guard let dataAsset = NSDataAsset(name: assetName) else { throw DecodeError.noFile } return try JSONDecoder().decode(type, from: dataAsset.data) } } ``` JSONDecoder() 인스턴스에서 사용할 수 있습니다. 이 JSONDecoder() 인스턴스를 사용해서 type의 data를 decode 해준다고 생각했습니다. 이 방법의 경우 decode하는 메서드 내부에서도 JSONDecoder()가 한번 더 생성된다는 점이 어색하게 느껴지기에 이 방법 대신 1번의 방법을 사용하였습니다. 이 두 가지 방법 중에서 웨더라면 어떤 방법을 사용하실지에 대한 조언이 듣고 싶습니다. ### 질문거리 - JSON 타입 구현할 때 class? struct? - Self에 대해 Swift에서 Self와 self는 모두 현재 타입 또는 인스턴스를 가리키는 데 사용되는 특수한 식별자입니다. 그러나 대소문자의 차이로 인해 미묘한 의미 차이가 있습니다. Self는 대문자로 시작하는 식별자로서, 해당 식별자가 정의된 타입 자체를 가리킵니다. 이는 주로 프로토콜 내에서 사용되며, 현재 타입이 프로토콜을 준수하는 경우 해당 타입을 참조하는 데 사용됩니다. 이렇게 하면 프로토콜 내에서 제약 조건이 있는 연관 타입(associated type)을 참조할 수 있습니다. Self는 프로토콜이 아닌 타입 내에서 사용될 때는 그냥 타입 자체를 가리킵니다. 예를 들어, 다음은 Self를 사용하여 프로토콜 내에서 연관 타입을 참조하는 방법을 보여줍니다: ```swift protocol Printable { static var typeName: String { get } func printTypeName() } struct MyStruct: Printable { static var typeName: String { return "MyStruct" } func printTypeName() { print(Self.typeName) // "MyStruct" } } ``` 요약하자면, Self는 현재 타입 자체를 가리키는 식별자로 주로 프로토콜 내에서 사용되며, self는 현재 인스턴스를 가리키는 식별자로 인스턴스 메서드 내에서 사용됩니다. # step3 PR ## 고민했던 것 ### 1️⃣ 다른 파일에 같은 기능의 메서드 `MainViewController`와 `EntryListViewController`에 각각 디코딩하는 메서드를 구현했었는데 웨더의 리뷰를 보고 어떤 식으로 하나의 기능으로 묶고 빈 배열값없이 구현할지 고민했습니다. ```swift struct Data { static func unwrap<T: Decodable>(type: T, file: String) -> T { do { let data = try T.decode(file) return data } catch { print(error.localizedDescription) return type } } } ``` 결과적으로 Data 구조체를 만들어 내부에 타입 메서드로 디코딩해줄 메서드를 구현하였고 디코딩이 필요한 곳에서 타입으로 바로 접근해 사용할 수 있도록 해주었습니다. 이렇게 하니 빈 배열값을 만들 필요 없이 property를 let으로 만들어 바로 선언해줄 수 있었습니다. ```swift // MainViewController private let expoGuideData: ExpoGuide? = Data.unwrap(initialValue: nil, file: "exposition_universelle_1900") // EntryViewController private let entryData: [Entry] = Data.unwrap(initialValue: [], file: "items") ``` ### 2️⃣ Label에 Dynamic Type 적용 화면에 Dynamic Type에 대해 적용하면서 Label에 적용하는 방법에 대한 고민이 있었습니다. Story Board의 Attribute Inspector 창에서 Dynamic Type을 체크하였는데 막상 simulator를 실행했을 시 나오는 Environment Override에서 Font Size를 키워봐도 크기가 변하지 않았습니다. Font를 System이 아닌 Text Style에서 선택하여 주었더니 Dynamic Type이 작동하였습니다. ### 3️⃣ AppDelegate를 통한 화면 방향 전환 제한 첫 화면을 세로로만 보게 하기 위해서 App Delegate를 처음으로 사용해 보았습니다. App Delegate는 View Controller의 역할을 덜기 위해 나온 것으로 앱의 공유 동작을 관리합니다. 그 중에서도 가로세로 화면 전환을 담당하는 것도 있습니다. UIInterfaceOrientation를 통해 방향을 설정해줄 수 있습니다. main 화면만 .portrait로 세로 방향 고정이기에 MainViewController의 viewWillApper에서는 appDelegate.isOnlyPortaitOrientation = true로 바꾸었고 viewWillDisappear에서는 false로 바꾸었습니다. 이 AppDelegate는 싱글톤으로 구성되어 있기에 .shared를 사용하여 mainViewController에서 할당해, 해당 프로퍼티의 값을 변경할 수 있게 하였습니다. ```swift private let appDelegate = UIApplication.shared.delegate as! AppDelegate override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.isNavigationBarHidden = true appDelegate.isOnlyPortaitOrientation = true } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationController?.isNavigationBarHidden = false appDelegate.isOnlyPortaitOrientation = false } ``` ## 조언이 필요한 것 ### 1️⃣ `AppDelegate` 파일 내부에 기본 구현되어있는 메서드 `portrait`모드와 `landscape`모드를 선택적으로 주기 위해 `AppDelegate` 내부에서 메서드를 구현해주었습니다. 그 과정에서 `AppDelegate` 내부에 기본적으로 구현이 되어있는 메서드들을 모두 지우고 저희가 필요한 메서드만 구현해주었습니다. 처음엔 지우게 되면 앱 동작에 있어서 문제가 생길줄 알았는데 앱 동작에는 큰 문제가 생기지 않았습니다. 이러한 메서드들이 왜 초기에 기본적으로 구현되어 있는지, 꼭 필요한 것인지 궁금합니다.🫠