Build the unofficial Udemy Home Screen (9)
APIResponse를 UIModel에 파싱하기
이전에는 Json으로 데이터를 가져오지 않았기에
1
2
let uiModel = HomeUIModel(sectionModels: [
.init(section:.mainBanner(id: "123"), body: [
이렇게 ViewDidLoad에 값을 넣어 주었는데, 이젠 JSON에 값이 있기에 JSON을 사용하여 바로 로드를 해보기로 한다.
ViewDidLoad에 있던 uiModel에 관한 내용은 모두 날리자.
Helper 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct HomeUIModelHelper {
typealias SectionModel = HomeUIModel.SectionModel
static func makeUIModel(response: APIResponse) -> HomeUIModel {
var sectionModels = [SectionModel]()
for layout in response.layouts {
switch layout {
case let .mainBanner(id, mainBanner):
let sectionModel = SectionModel(
section: .mainBanner(id: id),
body: [.mainBanner(
id: mainBanner.id,
imageLink: mainBanner.imageLink,
title: mainBanner.title,
caption: mainBanner.caption)
])
sectionModels.append(sectionModel)
case let .textHeader(id, textHeader):
let sectionModel = SectionModel(
section: .textHeader(id: id),
body: [.textHeader(
id: textHeader.id,
text: textHeader.text,
highlightedText: textHeader.highlightedText)
])
sectionModels.append(sectionModel)
case let .courseSwimlane(id, courses):
let items: [HomeUIModel.Item] = courses.map { course in
return .course(id: course.id, imageLink: course.imageLink, title: course.title, author: course.author, rating: course.rating, reviewCount: course.reviewCount, price: course.price, tag: course.tag)
}
일부만 적는다.
문제해결
갑자기
guard let sectionModel = self?.uiModel?.sectionModels[index] else { return nil }
여기부분에서 에러가 발생했다.
에러내용은 다음과 같다.
1
'nil' is not compatible with closure result type 'NSCollectionLayoutSection'
HomeUIModelHelper를 적으면서 생긴문제이니 거기서부터 확인을 해봐야겠다.
그부분은 문제가 없었다.
init에서 차이가있음을 발견
실행했을때는 차이가 없어서 이부분은 GPT를 통해 내용을 좀 더 적는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//wrong
init() {
super.init(frame: .zero, collectionViewLayout: UICollectionViewLayout())
collectionViewLayout = makeCompositionalLayout()
registerCells()
setupDataSource()
}
//correct
init() {
super.init(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) // different
collectionViewLayout = makeCompositionalLayout()
registerCells()
setupDataSource()
}
- UICollectionViewLayout
- 추상 클래스로, UICollectionView의 모든 레이아웃의 기본 클래스.
- 직접 사용하지 않고, 레이아웃을 커스터마이징하려면 이 클래스를 서브클래싱하여 구현함.
- 개발자가 컬렉션 뷰의 레이아웃을 완전히 제어하고 싶을 때 사용하는 클래스.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CustomLayout: UICollectionViewLayout {
override func prepare() {
super.prepare()
// 커스텀 레이아웃 준비 작업
}
override var collectionViewContentSize: CGSize {
// 컬렉션 뷰의 전체 콘텐츠 크기 정의
return CGSize(width: 1000, height: 1000)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes = [UICollectionViewLayoutAttributes]()
// 주어진 rect에 표시할 아이템의 레이아웃 속성 계산
return attributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
// 특정 아이템의 레이아웃 속성 정의
attributes.frame = CGRect(x: indexPath.row * 50, y: 0, width: 50, height: 50)
return attributes
}
}
- UICollectionViewFlowLayout
- UICollectionViewLayout의 서브클래스.
- 미리 정의된 표준 레이아웃을 제공함.
- 아이템을 그리드 형식으로 행(row)이나 열(column)에 자동으로 배치하는 레이아웃을 제공.
- 일반적인 수평 및 수직 스크롤을 처리하는 레이아웃으로 많이 사용됨.
- 간단한 설정으로 섹션 간 간격, 아이템 크기, 스크롤 방향 등을 제어 가능.
1
2
3
4
5
6
7
let flowLayout = UICollectionViewFlowLayout()
flowLayout.itemSize = CGSize(width: 100, height: 100)
flowLayout.minimumLineSpacing = 10
flowLayout.minimumInteritemSpacing = 10
flowLayout.scrollDirection = .vertical
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
차이점
- UICollectionViewLayout은 추상적인 기본 클래스로, 완전한 커스텀 레이아웃을 만들 때 사용.
- UICollectionViewFlowLayout은 그리드 레이아웃을 제공하는 구현된 클래스로, 커스텀이 필요 없는 경우 쉽게 사용할 수 있음.
실행해도 문제가 없는 이유.
- 초기 기본 레이아웃 설정:
- UICollectionViewFlowLayout은 컬렉션 뷰의 기본적인 그리드형 레이아웃(아이템 크기, 간격, 스크롤 방향 등)을 자동으로 처리해 줍니다. 만약 초기화 시 UICollectionViewFlowLayout을 사용하면 기본적인 레이아웃이 설정됨.
- 반면에, UICollectionViewLayout은 추상적인 기본 클래스이므로, 레이아웃 관련 동작을 모두 직접 구현해야 힌디. 만약 이후에 makeCompositionalLayout()으로 레이아웃을 변경할 예정이라면, 초기화 시 UICollectionViewLayout을 사용해도 문제가 발생하지 않을 수 있다.
- 의미적인 차이:
- UICollectionViewFlowLayout은 명시적으로 기본 그리드 레이아웃을 사용할 의도를 나타낸다. 즉, 기본적으로 표준적인 흐름(flow) 레이아웃을 사용하고 있음을 명확히 알 수 있다.
- UICollectionViewLayout은 그 자체로 아무런 레이아웃을 설정하지 않으므로, 이후에 별도로 레이아웃을 설정할 의도임을 나타낸다.
왜 문제가 없을까?
현재 코드에서 collectionViewLayout = makeCompositionalLayout()으로 나중에 레이아웃을 덮어씌우기 때문에 초기화 시 사용한 UICollectionViewFlowLayout이나 UICollectionViewLayout이 최종적으로 반영되지 않는다. makeCompositionalLayout()에서 설정한 레이아웃이 최종적으로 사용되기 때문에 문제가 발생하지 않는 것. 하지만 명확하게 의도를 표현하고 싶다면 UICollectionViewFlowLayout을 사용하는 것이 더 좋다.
그리고 makeCompositionalLayout
힘수에서도 발견
1
2
3
4
5
6
7
8
9
10
//wrong
case .textheader:
guard case let .textHeader(_, text, _) = sectionModel.body.first
else { return nil }
return self?.makeTextHeaderSection(text: text)
//correct
case .textHeader:
guard case let .textHeader(_, text, _) = sectionModel.body.first
else { return nil }
return self?.makeTextHeaderSection(text: text)
해당에러는 이런 대소문자나 기타 에러에 의해서 파생된 에러로 확인되었다.
이후 HomeVC로가서
1
2
3
4
5
6
7
8
9
private func loadJSON() {
let apiResponse: APIResponse? = FileManager.modelFromJSON(fileName: "payload")
if let response = apiResponse {
let uiModel = HomeUIModelHelper.makeUIModel(response: response)
collectionView.setDataSource(uiModel: uiModel)
}
}
uiModel을 적용시켜준다.
서버를 통한 JSON 로드하기
Mocky 를 사용하여, JSON의 내용을 모두 복사한뒤 업로드한다.
New Mock을 클릭하면 위와같이 나오는데 Response에 내용을 복사하자.
이후 Generate를 하면
이렇게 링크가 생성이 된다.
링크를 타고들어가면 우리가 복사한 내용이 그대로 보인다.
1
2
3
4
5
6
7
8
9
10
11
12
struct APIClient {
private let urlString =
"https://run.mocky.io/v3/7efcd0a5-21fd-4f12-9e73-7aaa6701d722"
func fetchLayout() -> AnyPublisher<APIResponse, Error> {
return URLSession.shared.dataTaskPublisher(for: URL(string: urlString)!)
.map({$0.data})
.decode(type: APIResponse.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
API호출 관련 Struct를 만들어 주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private let apiClient = APIClient()
private func fetchLayout() {
apiClient.fetchLayout()
.receive(on: DispatchQueue.main)
.sink { completion in
if case let .failure(error) = completion {
print("Error: \(error)")
}
} receiveValue: { [weak self] apiResponse in
let uiModel = HomeUIModelHelper.makeUIModel(response: apiResponse)
self?.collectionView.setupDataSource(uiModel: uiModel)
}.store(in: &cancellables)
// let apiResponse: APIResponse? = FileManager.modelFromJSON(fileName: "payload")
//
// if let response = apiResponse {
// let uiModel = HomeUIModelHelper.makeUIModel(response: response)
// collectionView.setupDataSource(uiModel: uiModel)
// }
}
loadJSON에서 fetchLayout으로 함수명을 바꿔주었다.
이젠 JSON에 아무런 내용이 없어도 서버에서 가져오기에 로드가 된다.
작동화면은 달라진게 없어서 패스