# 🏛️만국박람회 ## 🍀 소개 `yyss99` `kyungmin`이 만든 만국박람회 앱입니다. `JSON`데이터를 `Decode`해서 `TableView` 와 `ScrollView`를 이용해 박람회에 대한 정보를 보여주는 앱입니다. ## 📖 목차 1. [팀원](#-팀원) 2. [그라운드룰](#-그라운드룰) 3. [일일 스크럼](#-일일-스크럼) 4. [타임라인](#-타임라인) 5. [실행 화면](#-실행-화면) 6. [시각화된 프로젝트 구조](#-시각화된-프로젝트-구조) 7. [트러블 슈팅](#-트러블-슈팅) 8. [프로젝트 회고](#-프로젝트-회고) 9. [참고 링크](#-참고-링크) <br> ## 👨‍💻 팀원 | yyss99(와이) | kyungmin(경민) | | :------: | :------: | |<Img src= "https://hackmd.io/_uploads/Byuz8kh_3.png" width="200"> |<Img src="https://cdn.discordapp.com/attachments/1100965172086046891/1108927085713563708/admin.jpeg" width="200"> | |[Github Profile](https://github.com/yy-ss99) |[Github Profile](https://github.com/YaRkyungmin)| <br> ## 📝 그라운드룰 - [그라운드룰](https://github.com/yy-ss99/ios-exposition-universelle/wiki/🤙Ground-Rules) ## 🌻 일일 스크럼 - [일일 스크럼](https://github.com/yy-ss99/ios-exposition-universelle/wiki/🌻일일-스크럼) ## ⏰ 타임라인 |날짜|내용| |:--:|--| |2023.06.27.(화)| - MVC 단위로 그룹화 <br> - ExpoInformation 타입 생성 및 구현 <br> - DataTransferObject 그룹 생성 <br> - ExpoEntry 타입 구현| |2023.07.02.(일)| - expo_assets 추가 <br> - ExpoViewController 추가 및 기능 구현 <br> - EntryViewController 추가 및 기능 구현 <br> - Entry TableCell 기능 구현 <br> - EntryDetailViewController 파일 추가| |2023.07.03.(월)| - 스토리보드 autolayout 제약 수정<br> - EntryDetailViewController entry 사진 및 설명 추가 <br> - ExpoViewController 네비게이션 바 숨기기 기능 추가 <br> - DecodeError 구현 <br> - 프로퍼티 접근제한 및 리팩토링 <br> - NumberFormatter 기능 구현 <br> - 네임스페이스 추가 및 분리 |2023.07.05(화)| - CommaFormatter 수정 및 추가 <br> - EntryTableViewCell에 super.prepareForReuse()추가 <br> - decode 오류 Alert 구현 <br> -EntryDetailViewController 의존성 주입 <br> - ViewController 로직 분리 및 String, Int 확장 | |2023.07.06(화)| - 스크롤 뷰 제약 수정 <br> - ContainerViewController 화면 고정 기능 추가| ## 📱 실행 화면 |첫 화면|테이블 뷰 화면 | |:--:|:--:| |<Img src = "https://hackmd.io/_uploads/HyEa0mHYn.gif" width="400"/>|<Img src = "https://hackmd.io/_uploads/HkqEyVBKh.gif" width="400"/>| |상세 화면|다이나믹 타입 적용 ||:--:|:--:| |<Img src = "https://hackmd.io/_uploads/By251NHF3.gif" width="400"/>|<Img src = "https://hackmd.io/_uploads/SyOelNrt3.gif" width="400"/>| |SE 화면으로 실행| SE 화면 다이나믹 타입 적용||:--:|:--:| |<Img src = "https://hackmd.io/_uploads/BkrOx4St3.gif" width="400"/>|<Img src = "https://hackmd.io/_uploads/S1MqlNBt3.gif" width="400"/>| ## 👀 시각화된 프로젝트 구조 ### 🌟 Class Diagram ![reference link](https://hackmd.io/_uploads/B1R5rEBtn.png) ### 🗂 폴더 구조 > Model : 앱 구동 로직에 필요한 모델 > View : 화면을 구성하는 뷰 > Controller : 화면의 이벤트와 전환을 컨트롤하는 컨트롤러 > App : AppDelegate, SceneDelegate ``` Expo1900 project └── Expo1900 ├── App │ ├── AppDelegate │ └── SceneDelegate ├── Model │ ├── DataTransferObject │ │ │── ExpoInformation │ │ └── ExpoEntry │ ├── NameSpace │ │ │── StoryBoardNameSpace │ │ │── AssetsNameSpace │ │ └── ExpoInformationNameSpace │ ├── AlertController │ ├── CommaFormatter │ ├── DecodeError │ ├── Decoder │ └── ExpoInformationSetter │── Controller ContainerViewController │ ├── EntryTableViewCell │ ├── ExpoViewController │ ├── EntryViewController │ ├── EntryDetailViewController │ └── ContainerViewController ├── View │ ├── Main │ └── LaunchScreen └── Resource ├── Assets └── Info ``` ## 🧨 트러블 슈팅 ### 1️⃣ Data의 상수명 바꾸기 `short_desc`나 `image_name` 같은 스위프트 네이밍과 다른 상수명을 바꾸기 위해서 `CodingKey`를 사용하였습니다. ``` swift struct ExpoEntry: Decodable { let name: String let imageName: String let shortDescription: String let description: String private enum CodingKeys: String, CodingKey { case name case imageName = "image_name" case shortDescription = "short_desc" case description = "desc" } } ``` ### 2️⃣ Codable 과 Decodable 어떤 것을 채택 할 것인가 `Codable`은 `Decodable`과 `Encodable`프로토콜을 둘 다 가지고 있어서 데이터를 인코딩하고 디코딩하는데에 사용됩니다. 반면에 `Decodable`프로토콜은 데이터를 디코딩하는데만 사용됩니다. 이번 프로젝트에서는 `JSON`데이터를 디코딩 하는 것만 필요할 것이라고 생각해서 `Decodable`을 채택했습니다. ### 3️⃣ 특정 화면만 세로 화면 고정하기 첫 화면만 세로로 고정하기 위해서 `UIViewController`에 있는 `supportedInterfaceOrientations`라는 `property`를 사용하기로 했습니다. `supportedInterfaceOrientations`는 기기방향이 변경되면 루트 뷰 컨트롤러 또는 창을 채우는 최상위 모달 뷰 컨트롤러에서 이 메서드를 호출한다고 공식문서에 나와 있었습니다. `view controller`에 상위 `view controller`가 있다면 그 상위 `view controller`의 `porperty`를 따른다고 이해했습니다. `UINavigationController`가 `all`이라면 그 안에 있는 `viewController`들은 `UINavigationController`를 따라 `all`이 됩니다. ``` swift final class ContainerViewController: UINavigationController { override var supportedInterfaceOrientations: UIInterfaceOrientationMask { guard let supportedInterfaceOrientations = self.topViewController?.supportedInterfaceOrientations else { return self.supportedInterfaceOrientations } return supportedInterfaceOrientations } } ``` 화면마다 바꾸어주기 위해서 `UINavigationController` 상속받는 `class`를 만들었습니다. 그런 뒤에 가장 최상위 `viewController`의 `supportedInterfaceOrientations` 속성을 `return`하도록 했습니다. ``` swift // 새로로 고정 할 첫 화면 override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait } // 회전이 가능하게 할 그 다음 화면 override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .all } ``` ### 4️⃣ 네비게이션 바 색상 변경 스크롤을 내리지 않아도 `Navigation Bar`의 색상이 `default`색으로 보이게하는 방법에 대해 고민했습니다. 스토리보드 상 `inspector`에 있는 `Navigation Bar Appearances`속성에는 다음과 같이 네가지가 있었습니다. - `Standard`: 일반적인 상태의 `Navigation Bar`를 나타내고 `Navigation Bar`의 높이가 일반적인 크기로 표시되며, 네비게이션 아이템과 타이틀을 포함합니다. - `Compact`: 네비게이션 바의 크기를 축소하여 표시합니다. `Navigation Bar`의 높이가 줄어들고, 아이템과 타이틀이 더 작은 공간에 표시됩니다. - `Scroll Edge`: 네비게이션 바가 스크롤 가장자리에 붙도록 지정합니다. 스크롤할 때 `Navigation Bar`가 화면 위쪽으로 이동하여 사라지지 않고 고정됩니다. - `Compact Scroll Edge`: `Compact`모드에서 `Scroll Edge`모드로 변경될 때 적용되는 속성입니다. `Compact`모드에서 `Navigation Bar`가 축소된 크기로 표시되다가 스크롤시 `Scroll Edge`모드로 전환되면서 `Navigation Bar`의 크기와 레이아웃이 변경됩니다. 이 속성들 중 목적에 맞게 `Scroll Edge`에 대한 속성만 적용하여서 네비게이션 바의 색상이 고정시켰습니다. ### 5️⃣ ScrollView의 autoLayout 설정 처음 `ScrollView`에 대한 제약을 설정해줄 때는 `VerticalStackView`를 바로 `ScrollView`에 담아줬습니다. autoLayout은 잡혔지만 기기를 변경해서 테스트해줄때 가로방향으로도 스크롤이 되는 문제가 생겼습니다. `WorkingwithScrollViews`에서 `Add a view to the scroll view. Set the view’s Xcode specific label to Content View.`라는 문장을 보고나서 `ScrollView`에 컨텐츠들을 담기전에 `ContentView`를 먼저 담은 뒤에 그 `ContentView`에 컨텐츠들을 넣어줘야 되는것을 알게 됐습니다. 가로방향으로만 스크롤이 되게 할 때는 `ConteneView`의 `height`를 `ScrollView`의 `FrameLaout height`와 같게 해주면 됐고, 세로방향으로만 스크롤이 되게 할때는 `ConteneView`의 `width`를 `ScrollView`의 `FrameLaout width`와 같게 해주면 된다는 것도 알게 되었습니다. ### 6️⃣ Decoder 파일의 위치 `JSON`파일명을 입력받아 디코딩해주는 `Decoder`객체의 파일의 위치는 `Model`에 있어야 한다고 생각했습니다. 하지만 `NSDataAsset` 클래스를 사용하기 위해 `UIKit`모듈을 `import` 해줘야 해서 Model안의 파일이 `UIKit`을 `import`해줘도 되는지 의문점이 들었습니다. 여러 문서를 찾아보고 `Model`은 `View`를 몰라야 한다는 것을 깨달았습니다. 그런데 이건 어디까지를 `View`로 볼것이냐부터 정해야 한다는 기준에 의해서 정하는 것 같습니다. `MVC`를 나눌때의 나름의 기준을 세우는 것이 맞다고 판단했습니다. ### 7️⃣ 화면 간 값 전달 시 의존성 주입 ```swift //EntryDetailViewController final class EntryDetailViewController: UIViewController { @IBOutlet private weak var entryImageView: UIImageView! @IBOutlet private weak var entryDescription: UILabel! var expoEntry: ExpoEntry? ... } //EntryViewController final class EntryViewController: UIViewController { .... } extension EntryViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let expoEntry = self.expoEntries[indexPath.row] guard let entryDetailViewController = storyboard?.instantiateViewController(withIdentifier: StoryBoardNameSpace.entryDetailViewController) as? EntryDetailViewController else { return } entryDetailViewController.expoEntry = expoEntry navigationController?.pushViewController(entryDetailViewController, animated: true) } } ``` 해당코드에서 `expoEntry`는생성자를 통해 주입해주면 외부에서 몰라도 되는 정보라고 판단했습니다. 또한 옵셔널이 아니어도 될 것 같아 `instantiateViewController(identifier:creator:)`를 활용해 봤습니다. ```swift //EntryDetailViewController final class EntryDetailViewController: UIViewController { private var expoEntry: ExpoEntry @IBOutlet private weak var entryImageView: UIImageView! @IBOutlet private weak var entryDescription: UILabel! override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .all } init?(expoEntry: ExpoEntry, coder: NSCoder) { self.expoEntry = expoEntry super.init(coder: coder) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } .... } //EntryViewController final class EntryViewController: UIViewController { .... } extension EntryViewController: UITableViewDataSource, UITableViewDelegate { ... func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let expoEntry = self.expoEntries[indexPath.row] guard let entryDetailViewController = storyboard?.instantiateViewController(identifier: StoryBoardNameSpace.entryDetailViewController, creator: { coder in EntryDetailViewController(expoEntry: expoEntry, coder: coder)}) else { return } navigationController?.pushViewController(entryDetailViewController, animated: true) } } ``` ## 👥 프로젝트 회고 ### 잘한 부분 👍🏻 - 그라운드 룰을 잘 지켰습니다. - 서로에 대한 배려와 존중을 지켜주었습니다. - 주장과 논리에 확실한 근거가 있으면 서로의 의견을 통합하고 더 좋은 방향으로 이끌어 나갔습니다. - 이해가 가지 않는 부분은 서로가 서로에게 질문하고 답하기를 반복하며 공부했습니다. - 기억보단 기록에 포커스를 두어서 최대한 기록하려 노력했습니다. - 도큐먼트의 내용을 이해하고 적용하도록 노력했습니다. ### 부족한 부분 🫣 - 오토레이아웃에 대한 개념이 확실하게 잡혀있지 않은 상태로 프로젝트에 도입하려 했지만, 후에 다시 제대로 공부하고 도큐먼트 가이드라인에 따라 오토레이아웃을 적용하려고 노력했습니다. ## 📚 참고 링크 [🍎Apple Docs: JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder) [🍎Apple Docs: Using JSON with Custom Types](https://developer.apple.com/documentation/foundation/archives_and_serialization/using_json_with_custom_types) [🍎Apple Docs: Encoding and Decoding Custom Types](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types) [🍎Apple Docs: UINavigationController](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types) [🍎Apple Docs: UITableViewDelegate](https://developer.apple.com/documentation/uikit/uitableviewdelegate) [🍎Apple Docs: UITableViewDataSource](https://developer.apple.com/documentation/uikit/uitableviewdatasource) [🍎Apple Docs: supportedInterfaceOrientations](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621435-supportedinterfaceorientations) [🍎Apple Docs: UIInterfaceOrientationMask](https://developer.apple.com/documentation/uikit/uiinterfaceorientationmask) [🍎Apple Docs: WorkingwithScrollViews](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithScrollViews.html) [🍎Apple Docs: Customize the Navigation Bar Appearance](https://developer.apple.com/documentation/uikit/uinavigationcontroller/customizing_your_app_s_navigation_bar#3950605)