# 🏛️만국박람회
## 🍀 소개
`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

### 🗂 폴더 구조
> 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)