---
# System prepended metadata

title: "\U0001F5C4️ 박스오피스 PR"

---

# 🗄️ 박스오피스 PR

## 박스오피스 [STEP 1] - Erick, kyungmin

안녕하세요 July!!
리뷰이 Erick🐶, Kyungmin🐼 입니다!
박스오피스 첫번째 PR입니다!
2주간 잘 부탁드립니다 🙇‍♂️

## 🤔 고민했던 점

### Unit Test 📋
`Json` 데이터가 `BoxOffice`객체로 정상적으로 변환되는지 확인하기 위해 `Unit Test`를 진행하였습니다.

```swift
let result = try? JSONDecoder().decode(BoxOffice.self, from: dataAsset.data)

XCTAssertNotNil(result)
```
- `decode` 작업을 `try?`로 실행하여 디코딩이 성공했을때 `nil`이 아닌 데이터를 반환하지 검사하였습니다. 

```swift
let expectation = "일별 박스오피스"
let boxOffice = try? JSONDecoder().decode(BoxOffice.self, from: dataAsset.data)
let result = boxOffice?.boxOfficeResult.boxOfficeType

XCTAssertEqual(result, expectation)
```
- `Json` 파일 내의 `boxOfficeType`의 데이터 "일별 박스오피스"와 `decode`를 통해 가져온 데이터의 값이 같은지 검사하였습니다.

```swift
let expectation = "경관의 피" 
let boxOffice = try? JSONDecoder().decode(BoxOffice.self, from: dataAsset.data)
let result = boxOffice?.boxOfficeResult.dailyBoxOfficeList.first?.movieName

XCTAssertEqual(result, expectation)
```
- `Json` 파일 내의 첫번째 영화의 `movieName`의 데이터 "경관의 피"와 `decode`를 통해 가져온 데이터의 값이 같은지 검사하였습니다.

</br>

## 🙇‍♂️ 조언을 얻고싶은 점

### Multiple Variable Declaration ⁉️
`DailyBoxOfficeList`객체의 프로퍼티를 관련된 프로퍼티끼리 `Multiple Variable Declaration` 해도 되는지에 대한 고민을 했습니다. `DTO`객체에 대하여 `Multiple Variable Declaration`을 하는 것이 맞는지에 대한 조언 부탁드립니다.

🍏 **Before**
```swift
struct DailyBoxOfficeList: Codable {
    let number: String
    let rank: String
    let rankIntensity: String
    let rankOldAndNew: String
    let movieCode: String
    let movieName: String
    let openDate: String
    let salesAmount: String
    let salesShare: String
    let salseIntensity: String
    let salesChange: String
    let salesAccumulation: String
    let audienceCount: String
    let audienceIntensity: String
    let audienceChange: String
    let audienceAccumulation: String
    let screenCount: String
    let showCount: String
```
🍎 **After**
```swift
struct DailyBoxOfficeList: Codable {
    let number: String
    let rank, rankIntensity: String
    let rankOldAndNew: String
    let movieCode, movieName: String
    let openDate: String
    let salesAmount, salesShare, salseIntensity, salesChange, salesAccumulation: String
    let audienceCount, audienceIntensity, audienceChange, audienceAccumulation: String
    let screenCount: String
    let showCount: String
}
```

---
## 📚 참고 링크
- [🍎Apple: CodingKey](https://developer.apple.com/documentation/swift/codingkey)




## 박스오피스 [STEP 2] - Erick, kyungmin

안녕하세요 July!!
리뷰이 Erick🐶, Kyungmin🐼 입니다!
박스오피스 두번째 PR입니다!
이번에도 많은 조언 해주시면 감사하겠습니다🙇‍♂️

## 📚 학습내용
### 1️⃣ URL 의 구조는 어떻게 되는가? 
**URL 구조**
- **Scheme**: 프로토콜
- **Host**: 서버의 호스트 이름
- **Path**: 서버 경로
- **Query**: 추가적인 파라미터

```swift
var url = URL(string: "http://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?&key=mykey&targetDt=20230725")
url?.scheme // http
url?.host   // kobis.or.kr
url?.path   // /kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json
url?.query  // &key=mykey&targetDt=20230725
```

### 2️⃣ HTTP method의 종류와 쓰임 
- **GET**: 서버로 부터 리소스 받음
- **POST**: 서버에 새로운 리소스를 게시
- **PUT**: 서버 내 리소스의 모든 데이터를 업데이트
- **PATCH**: 서버 내 리소스의 일부를 업데이트
- **DELETE**: 서버 내 리소스 삭제

```swift
var request = URLRequest(url: url)
request.httpMethod = // HTTP method
```
- `url`을 이용해 `URLRequest`를 만들어 `request`에 `httpMethod`를 추가할 수 있습니다.

### 3️⃣ Test Double의 종류인  mock spy stub dummy는 무엇인가 ?
- **mock**: 실제 객체와 비슷하게 구현되어 있으며 `행위 기반 테스트(Behavior Base Test)`를 위해 사용되는 객체, 예상되는 행위에 대한 시나리오를 만들어 그에 따라 테스트를 진행합니다.
- **spy**: `Stub`의 역할을 가지면서 호출된 내용을 기록하는 객체, 호출 여부나 횟수 등을 기록할 수 있습니다.
- **stub**: `Dummy`가 실제로 동작하는 것처럼 만들어 실제 코드를 대신해서 동작해 주는 `상태 기반 테스트(State Base Test)`를 위해 사용되는 객체, 테스트가 어려운 부분의 객체를 대체하여 역할을 최소한으로 구현한 객체입니다.
- **dummy**: 가장 기본적인 테스트 더블로서 실제로 동작하는 기능이 구현되어 있지 않은 객체, 객체를 전달하는 용도로 사용됩니다.

> `상태 기반 테스트`는 메서드를 호출하고 그 결괏값과 예상 값을 비교하는 식으로 동작하는 테스트이고, `행위 기반 테스트`는 예상되는 행위들에 대한 시나리오를 만들어 놓고, 시나리오대로 동작했는지에 대한 여부를 확인하는 테스트입니다.

## 🤔 고민했던 점
### 1️⃣ NetworkManager 타입 설계
요구사항을 확인했을때, 
>- 이번 스텝에서 진행할 네트워킹 요소
>   - 오늘의 일일 박스오피스 조회
>   - 영화 개별 상세 조회

위와 같이 두가지의 다른 데이터를 받아와야 했습니다. 하지만 네트워킹을 하는 객체에서 `loadBoxOffice()`, `loadMovie()`와 같이 데이터를 나눠 가져오는 것은 좋지 않다고 생각하였습니다. 
`url`이 다를때도 네트워킹을 할 수 있는 하나의 객체를 만들었고, 그 객체에 `url`을 나눠 넣어주거나 `decoding`할 타입을 나눠 넣어주는 것은 다른 객체에서 해야하는 작업이라고 생각했습니다.
따라서 `NetworkManager`는 아래와 같이 세가지 기능을 가지고 있게 설계했고, 추후에 NetworkManager가 어떻게 쓰일지를 고민하여 `URLQueryItems`를 만들어 주는 기능을 추가 할 예정입니다. 

✨`func configuredURL(scheme: String, host: String, path: String, queryItems: [URLQueryItem]) -> URL?` 
: ❗️`scheme`, `host`, `path`, `queryItems`를 통해 `URL`을 만들어 주는 기능

✨`func startLoad(_ url: URL, completion: @escaping (Data?) -> Void` :
: ❗️`URL`받아 데이터를 받아와 데이터를 반환해주는 기능

✨`func decodeJSON<T: Decodable>(data: Data) -> T?` :
: ❗️`Data`를 받아 `JSON`으로 디코딩 해주는 기능

### 2️⃣ TestDouble
네트워크가 연결되지 않은 상황에서 `NetworkManager`의 테스트를 하기 위해 `TestDouble` 객체 `StubUserSession`을 만들어주어 데이터를 받아오는 `startLoad`가 정상적으로 작동하는지 테스트 했습니다.

**의존성 주입**
```swift
// 실제 NetworkManager 생성
NetworkManager(urlSession: URLSession.shared)
// 테스트 NetworkManager 생성
NetworkManager(urlSession: StubURLSession())
```
- `URLSessionProtocol`을 만들어 `NetworkManager`가 프로퍼티로 해당 타입을 가지고 있도록 하여 `URLSession`뿐만 아니라 `StubURLSession`을 주입할 수 있도록 하여 테스트했습니다.

**테스트**
```swift
// given
// 받아올 데이터를 임의로 생성
let dummy = DummyData(data: data, response: response, error: nil)
let stubUrlSession = StubURLSession(dummy: dummy)
sut?.urlSession = stubUrlSession

// when
sut?.startLoad(url!, completion: { data in
    // then
    // startLoad로 받아온 데이터는 DummyData의 데이터
    XCTAssertEqual(data, dataAsset.data)
    expectation.fulfill()
})
```
- `StubURLSession`는 `DummyData`를 가지고 `dataTask` 메서드를 실행시 `StubURLSessionDataTask`을 이용해서 `DummyData`만 던져주도록 구현했습니다.
    


## 🙇‍♂️ 조언을 얻고싶은 점

### 🔑 API KEY 관리
`APIKey`는 공용`repository`에 가지고 있으면 안되고, `private`하게 관리 돼야 한다고 생각했습니다. 그래서 `.gitignore`에 `.xcconfig`를 추가 시켜줬습니다. 
하지만 로컬에서 API config 파일을 추가했을때 `project.pbxproj`에 생기는 파일경로를 `commit`해줄 때마다 `unstage`해주어야 했습니다.
이런식으로 `APIKey`를 관리하는 것이 괜찮은지, 주로 어떻게 `APIKey`를 관리하는지 조언을 얻고 싶습니다🙇

---
### 📚 참고 링크
- [🍎Apple: URLSession](https://developer.apple.com/documentation/foundation/urlsession)
- [🍎Apple: URL](https://developer.apple.com/documentation/foundation/url)
- [🍎Apple: Fetching Website Data into Memory](https://developer.apple.com/documentation/foundation/url_loading_system/fetching_website_data_into_memory)
- [🐻야곰닷넷: Unit Test](https://yagom.net/courses/unit-test-작성하기/)


## 박스오피스 [STEP 2] - Erick, kyungmin

안녕하세요 July!!
리뷰이 Erick🐶, Kyungmin🐼 입니다!
박스오피스 두번째 PR입니다!
이번에도 많은 조언 해주시면 감사하겠습니다🙇‍♂️

<br>

## 📚 학습내용

### Closure Capture Strong Reference Cycle
`Closure`는 `Reference Capture`를 하기 때문에 참조 타입을 `Capture`한다면 `RC`가 증가하며 이는 `Strong Reference Cycle`을 발생 시킬 수 있습니다.
따라서 순환 참조가 발생할 수 있는 참조타입을 캡쳐할 경우 `[weak self]`를 사용하여 이를 방지할 수 있습니다.

```swift
boxOfficeManager.fetchBoxOffice { [weak self] error in
                                 
    // ...
    DispatchQueue.main.async {
        self?.collectionView.reloadData()
        self?.collectionView.refreshControl?.endRefreshing()
        self?.activityIndicator.stopAnimating()
    }
}
```
<br>

## 🤔 고민했던 점
### 1️⃣ `UICollectionViewDiffableDataSource`, `UICollectionViewCompositionalLayout`
`UICollectionViewDiffableDataSource`와 `UICollectionViewCompositionalLayout`를 학습하고 적용해 보았지만 필요성을 느끼지 못해 기존의 `UICollectionViewDataSource`, `UICollectionViewDelegateFlowLayout`만을 사용하여 구현하였습니다.

🔗 [**적용코드**](https://github.com/h-suo/ios-box-office/blob/step3_2nd/BoxOffice/Controller/BoxOfficeViewController.swift)

**이유**
- 현재 프로젝트에서는 테이블뷰와 같이 한 줄의 아이템 열만 가지고 있어 `layout`을 그룹별이나 섹션별로 복잡하게 나눠줄 필요가 없어 `UICollectionViewCompositionalLayout`의 필요성을 느끼지 못했습니다. 
- UI업데이트를 하는 상황도 처음 화면을 열거나 `refreshControl`로 데이터를 리로드 할 경우에만 한번에 `CollectionView`를 업데이트 시켜주기 때문에 `UICollectionViewDiffableDataSource`를 사용했을 때의 아이템을 추가 및 삭제 했을 때 사용자에게 애니메이션을 보여주는 반응형 API로서의 이점을 가져가지 못하기 때문에 필요성을 느끼지 못했습니다.

### 2️⃣ 객체 분리
- `Entity` : 서버로부터 받은 `Data`를 `Decode`할때 쓰는 타입
- `DTO` : 실제 `App`에서 사용할 `Data` 타입 
- `NetworkManager` : 서버로부터 `Data`를 받아오는 역할
- `BoxOfficeManager` : `VC`와 연결된 타입, 
- `DataManager` : `Entity`를 `DTO`로 변환해주는 역활
- `Extension`
    - `URL` : `URL`을 생성하는 `makeKobisURL`메서드 추가
    - `UILabel` : `attributedText`의 부분 색을 변경해주는 `asColor`메서드 추가
    - `UIAlertController` : 하나의 액션을 가진 기본적인 `alert`을 반환하는 `makedBasicAlert` 메서드 추가
    - `DateFormatter` : 오늘로부터 원하는 만큼 떨어진 날짜를 원하는 포멧으로 반환하는 `bringDateString` 메서드 추가
    - `NumberFormatter` : 숫자로 이루어진 `String`을 받아 `Decimal`스타일로 포멧해주는 `bringDecimalString` 메서드 추가

<br>

## 🙇‍♂️ 조언을 얻고싶은 점
### ⭐️ private extension
`extension`을 해준 곳안의 메서드들에 `private`을 모두 붙여 줄 경우 `extension`에 `private`을 해야하는지 메서드별로 모두 `private`을 붙여주는것이 나을지에 대한 고민을 했습니다. 보통 어떤식의 방법을 많이 쓰는지 조언 부탁드립니다🙏🏻

**Before**
```swift
private extension BoxOfficeViewController {
    func configureUI() {
        ...
    }
    
    func setupConstraint() {
        ...
    }
}
```

**After**
```swift
extension BoxOfficeViewController {
    private func configureUI() {
        ...
    }
    
    private func setupConstraint() {
        ...
    }
}
```

## 😅 해결하지 못한 점
### ⛔️ stopAnimating
초기화면에서 데이터 로드에 실패하고 `refreshControl`를 이용해서 다시 `loadBoxOfficeData`를 호출했을때 `refreshControl?.endRefreshing()`이 정상적으로 작동하지 않는 것 같습니다.
하지만 `present(alert, animated: true)`를 하지 않으면 `refreshControl?.endRefreshing()` 정상적으로 작동합니다.

---
### 📚 참고 링크
- [🍎Apple: Capturing Values](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Capturing-Values)
- [🍎Apple: UICollectionViewDiffableDataSource](https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource)
- [🍎Apple: UICollectionViewCompositionalLayout](https://developer.apple.com/documentation/uikit/uicollectionviewcompositionallayout)
- [🍎Apple: attributedText](https://developer.apple.com/documentation/uikit/uilabel/1620542-attributedtext)

답변 감사합니다 July!!

### 1.
`json파싱 에러`가 발생하는 건 아마 `Resource`/`API_KEY` 파일에 `KOBIS_API_KEY` 키를 추가해주셔야 할 것 같습니다. 
`API` 키는 깃허브에 올리지 않고 각자 로컬에서 관리하는 방식으로 사용 중입니다! (임시 key: `7a526456eb8e084eb294715e006df16f`)

### 2.
제가 이해한 `Snapshot`의 장점은 UI 변경이 있을 때 `apply`를 호출하면 `Hashable`한 데이터를 이용해 이전 `Snapshot`과의 상태를 비교하여 UI를 업데이트하기 때문에 필요한 부분만 업데이트하기 때문에 성능상의 이점도 있다고 생각했습니다.

### 3.
`nil`인 `object`에 접근시 아무 일도 일어 나지 않습니다. 데이터를 받아와서 넣어주는 클로저가 끝나기 전에 `BoxOfficeManager`가 `deinit`될때 바로 `deinit`되게 하기 위해 `[weak self]`를 통해 `RC`를 올려주지 않았는데, 클로저가 끝나기 전까지 `deinit`을 기다려줘도 상관 없을 것 같습니다.

### 4.
저희는 `Entity`는 JSON 데이터를 1:1 매핑되는 구조를 가지는 객체이고, `DTO`는 실제 사용할때 필요한 데이터들만 모아둔 객체라고 생각했습니다.
하지만 통상적으로 `DTO`를 서버로부터 1:1 매핑되는 객체로 사용하고, `Entitiy`를 실제 로직에서 사용하는 객체인 것 같습니다.
두 부분 수정하겠습니다!


## 박스오피스II [step1] Erick, kyungmin

안녕하세요 July!!
리뷰이 Erick🐶, Kyungmin🐼 입니다!
박스오피스II의 첫 스탭입니다!
박스오피스 1-4 같이 포함된 코드입니다
이번에도 많은 조언 해주시면 새겨듣겠습니다🙇

## 🤔 고민했던 점
### 1️⃣ VC의 기능 분리
#### 🔺extension 하지 않은 부분
- stored property
- initializer
- viewDidLoad
#### 🔺extension-setupComponents
- component관련 세팅
#### 🔺extension-configureUI
- 각각의 UI객체의 addSubview
#### 🔺extension-setupConstraint
- 각각의 UI객체의 constraint
#### 🔺extension-buttonAction
- 버튼 메서드
#### 🔺그 외 기능
- 델리게이트, 데이터소스 등..

### 2️⃣ Hashable
July의 조언으로 `CompositionalLayout`와 `DiffableDataSource`를 적용해보았습니다.
`DiffableDataSource`는 `DiffableDataSourceSnapshot`을 이용해 UI를 업데이트 하는데 그때 저희가 `Item`을 구성하는데 사용하는 `DailyBoxOffice` 객체가 `Hashable`을 채택해야 했습니다.

`Hashalbe`을 채택할 때 `struct`는 모든 프로퍼티는 `Hashable`을 준수해야 한다고 나와있습니다.
> For a struct, all its stored properties must conform to Hashable.

그러나 `rankStateColor`는 `rankState property`를 통해서 만들어지는 `property`이기 때문에
`hash value`를 비교할때 필요하지 않는 값이라고 생각해서 `hash`함수에서 빼줬습니다.

```swift
typealias RankStateColor = (targetString: String, color: UIColor)

struct DailyBoxOffice: Hashable {
    let movieCode: String
    let rank: String
    let rankState: String
    let movieTitle: String
    let dailyAndTotalAudience: String
    let rankStateColor: RankStateColor
    
    static func == (lhs: DailyBoxOffice, rhs: DailyBoxOffice) -> Bool {
        return lhs.movieCode == rhs.movieCode
            && lhs.rank == rhs.rank
            && lhs.rankState == rhs.rankState
            && lhs.movieTitle == rhs.movieTitle
            && lhs.dailyAndTotalAudience == rhs.dailyAndTotalAudience
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(movieCode)
        hasher.combine(rank)
        hasher.combine(rankState)
        hasher.combine(movieTitle)
        hasher.combine(dailyAndTotalAudience)
    }
}
```

### 3️⃣ URLRequest
영화 포스터 이미지를 가져올 때 [다음 이미지 검색 API](https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide#search-image) 이용해야 했습니다.
기존 `API`는 `URLRequest`를 설정할 필요 없이 `URL`의 `QureyItem`만을 설정하여 데이터를 요청할 수 있었지만 `다음 이미지 검색 API`는 `Request Header`에 `Key`를 담아서 보내야 했습니다.
따라서 `NetworkManager`에 `URLRequest`를 받을 수 있는 `requestData`를 추가하였습니다.

```swift
func requestData(from urlRequest: URLRequest, completion: @escaping (Result<Data, NetworkError>) -> Void) {
    let task = urlSession.dataTask(with: urlRequest) { data, response, error in
        // ...
    }
    // ...
}
```

### 4️⃣ NSCache
데이터가 큰 이미지는 한번 받아온 후 재사용할 수 있도록 `CacheManager`를 만들어 관리해주었습니다.

```swift
final class CacheManager {
    static let shared = NSCache<NSString, UIImage>()
    
    private init() {}
}
```
- `NSString`을 키값으로 `UIImage`를 저장하는 `NSCache`를 싱글톤 패턴을 이용하여 구현했습니다.

```swift
func fetchPosterImage(_ movieName: String, completion: @escaping (Bool) -> Void) {
    if let cacheImage = CacheManager.shared.object(forKey: movieName as NSString) {
        posterImage = cacheImage
        completion(true)
        return
    }
    
    // ...
    networkManager.requestData(from: urlRequest) { [weak self] result in
        // ...
        switch result {
        case .success(let data):
            do {
                // ...
                CacheManager.shared.setObject(image, forKey: movieName as NSString)
                self.posterImage = image
                completion(true)
            } catch {
                // ...
            }
        case .failure(let error):
            // ...
        }
    }
}
```
- 실제 캐싱을 하는 위치는 `BoxOfficeManager`의 `fetchPosterImage`에서 이미지를 받아오기 전에 `movieName`을 `cacheKey`로 캐싱된 이미지가 있다면 `requestData`를 호출하지 않고 캐싱된 이미지를 사용합니다.
- 캐싱된 이미지가 없다면 `requestData`로 이미지를 받아와 캐싱하도록 구현했습니다.

### 5️⃣ 날짜선택
`UICalendarView`를 사용하여 날짜선택 뷰를 구현하였습니다.

```swift
private func setupCalendarView() {
    let fromDateComponents = DateComponents(calendar: Calendar(identifier: .gregorian), year: 2004, month: 1, day: 1)

    guard let fromDate = fromDateComponents.date else {
        return
    }

    let calendarViewDateRange = DateInterval(start: fromDate, end: Date.yesterday)
    calendarView.availableDateRange = calendarViewDateRange

    let dateSelection = UICalendarSelectionSingleDate(delegate: self)
    dateSelection.selectedDate = selectedDate
    calendarView.selectionBehavior = dateSelection
}
```
- `박스오피스 API`의 데이터가 `2003년 1월 1일`자는 가져오지 못하고 `2004년 1월 1일`자부터 가져오는 것을 확인하였습니다. 따라서 달력의 범위를 `2004년 1월 1일`부터 어제로 설정했습니다.
- `BoxOfficeViewController`에서 받아온 `selectedDate`를 선택된 날짜로 선택하도록 했습니다.
- `UICalendarSelectionSingleDate`로 단일 선택만 가능하도록 설정했습니다.

```swift
extension CalendarViewController: UICalendarSelectionSingleDateDelegate {
    func dateSelection(_ selection: UICalendarSelectionSingleDate, didSelectDate dateComponents: DateComponents?) {
        delegate?.selectedDate(date: dateComponents)
        dismiss(animated: true)
    }
}
```
- `UICalendarSelectionSingleDateDelegate`를 채택하여 날짜가 선택되었을 때 `Delegate` 패턴을 이용하여 `BoxOfficeViewController` 선택된 `DateComponents`를 넘겨주었습니다.

## 🙇‍♂️ 조언을 얻고싶은 점
### 1️⃣ AutoLayout
AutoLayout에 대한 Constant를 줄때 상수로 주는 것이 맞을 지 비율로 주는 것이 맞는 지, 현업에서는 어떤식으로 layout을 잡아 나가는지 궁금합니다🙏🏻
🔻 첫번째 경우
```swift
NSLayoutConstraint.activate([
    rankStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 32),
    rankStackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
    rankStackView.widthAnchor.constraint(equalToConstant: 40)
])
```
🔻 두번째 경우
```swift
NSLayoutConstraint.activate([
    rankStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, self.frame.width * 0.03),
    rankStackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
    rankStackView.widthAnchor.constraint(equalToConstant: self.frame.width * 0.1)
])
```
---
### 📚 참고 링크
- [🍎Apple: UICollectionViewDiffableDataSource](https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource)
- [🍎Apple: NSDiffableDataSourceSnapshot](https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesnapshot)
- [🍎Apple: Hashable](https://developer.apple.com/documentation/swift/hashable)
- [🍎Apple: UICalendarView](https://developer.apple.com/documentation/uikit/uicalendarview#3992448)



---

## 박스오피스II [STEP 2] Erick, kyungmin

안녕하세요 July!!
리뷰이 Erick🐶, Kyungmin🐼 입니다!
박스오피스II의 두번째 스탭입니다!
이번에도 많은 조언 부탁드립니다🙇

## 🤔 고민했던 점
### 1️⃣ 화면 모드 변경
`alert`의 버튼을 눌렀을 때 `CollectionView`의 `layout`과 `cell`을 어떻게 바꿀지 고민했습니다.
`CollectionViewMode`라는 열거형을 만들어 `Controller`가 프로퍼티로 열거형 인스턴스를 들고 있고 `collectionViewMode`를 분기처리하여 `layout`과 `cell`을 결정하도록 설정했습니다.

**CollectionViewMode 코드**
```swift
private enum CollectionViewMode {
    case list
    case grid
}
```

**dataSource 코드**
```swift
UICollectionViewDiffableDataSource<Section, DailyBoxOffice>(collectionView: collectionView) { collectionView, indexPath, dailyBoxOffice in
    switch self.collectionViewMode {
    case .list:
        // ...

        return listCell
    case .grid:
        // ...

        return gridCell
    }
}
```

### 2️⃣ 모드 변경 애니메이션
처음에는 모드 변경을 할 때 `collectionView.reloadData()`로 모든 셀을 다시 로드하였습니다. 하지만 이럴 경우 애니메이션 효과 없이 화면이 한번에 바뀌어 사용자 경험을 저하시킬 수 있기에 `setCollectionViewLayout`과 `reloadSections`을 사용하여 `layout`이 바뀌는 순간과 `section`이 리로드 되는 순간에 애니메이션 효과를 주었습니다.

**layout이 업데이트 될 때 애니메이션**
```swift
switch collectionViewMode {
case .list:
    collectionView.setCollectionViewLayout(listLayout(), animated: true)
case .grid:
    collectionView.setCollectionViewLayout(gridLayout(), animated: true)
}
```

**reloadSections와 apply를 사용하여 section이 업데이트 될 때 애니메이션**
```swift
var snapshot = NSDiffableDataSourceSnapshot<Section, DailyBoxOffice>()
snapshot.appendSections([.main])
snapshot.appendItems(boxOfficeManager.dailyBoxOffices, toSection: .main)
snapshot.reloadSections([.main])

dailyBoxOfficeDataSource.apply(snapshot, animatingDifferences: true)
```

### 3️⃣ dynamic하게 cell의 높이를 변경하기
처음 `label`들의 `text`에만 `adjustsFontForContentSizeCategory`속성을 `true`로 변경해줬을때는 `collectionView`의 `cell`높이가 `dynamic`하게 변경되지 않았습니다.
`centerY`로 설정돼있던 `label`들의 제약을 `topAnchor`와 `bottomAnchor`를 `contentView`의 `topAnchor`와 `bottomAnchor`를 잇고, `.estimated`를 활용해 `dynamic`하게 `cell`의 높이가 변경 되게 해줬습니다.
```swift
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                      heightDimension: .estimated(80))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 
                                       heightDimension: .estimated(80))
```

## 🙇‍♂️ 조언을 얻고싶은 점
### 1️⃣ 영화 상세화면 dynamic type 적용
요구사항에서는 `dynamic type`을 적용했을때 감독, 제작년도, 개봉일 등의 `titleLabel`이 크기가 커져서 모두 같은 크기를 갖는 것처럼 보였습니다. `titleLabel`의 `Compression priority`를 `.required`로 설정하는 방법이 제일 바람직하다고 판단하여 적용했지만, 세로 줄이 요구사항과는 다르게 맞지 않았습니다.

이를 해결하기 위해 `titleLabe`l과 오른쪽 정보를 닮는 `label`을 각각 다른 세로 스택뷰에 따로 담아서 해결하려 했으나 오른쪽 정보를 닮는 `label`크기가 유동적이기때문에 이 방법으로도 해결하지 못했습니다.

미리 `titleLabel`의 크기를 고정시켜놓는 방법 이외에 이 문제를 어떻게 해결할 수 있을까요?

#### 📃 요구사항 화면
<Img src = "https://hackmd.io/_uploads/r1rWYounh.png" width="350"/>

#### 🔨 현재 구현된 화면
<Img src = "https://hackmd.io/_uploads/SJppvj_2h.png" width="350"/>


---
### 📚 참고 링크
- [🍎Apple: estimated(_:)](https://developer.apple.com/documentation/uikit/nscollectionlayoutdimension/3199057-estimated)
- [🍎Apple: setCollectionViewLayout(_:animated:)](https://developer.apple.com/documentation/uikit/uicollectionview/1618086-setcollectionviewlayout)
- [🍎Apple: reloadSections(_:)](https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesnapshot/3375784-reloadsections)