세 종류의 Cell에 어떤 내용이 들어가는지 생각하고 구현해 보자
Cell VMs
photo 셀은 imageURL이 필요하다.
final class RMCharacterPhotoCollectionViewCellViewModel {
private let imageUrl: URL?
init (imageURL: URL?) {
self.imageUrl = imageURL
}
}
Info 셀은 안에서는 Status: XXX와 같은 구조를 구현할거기 때문에 value와 title이 필요하다.
final class RMCharacterInfoCollectionViewCellViewModel {
public let value: String
public let title: String
init (
value: String,
title: String
) {
self.value = value
self.title = title
}
}
Episode는 따로 별도의 모델을 이용해서 구현한다. 그리고 해당 에피소드에 대한 Url로 데이터를 받을 수 있으므로 url만 받자.
final class RMCharacterEpisodeCollectionViewCellViewModel {
private let episodeDataUrl: URL?
init (episodeDataUrl: URL?) {
self.episodeDataUrl = episodeDataUrl
}
}
photo는 사진만 하니까 1개, Info는 8가지만 사용해서 8개, episode는 셀 수 없이 많아서 url 수에 따라 동적으로 생성하자.
final class RMCharacterDetailViewViewModel {
....
private func setUpSections() {
sections = [
.photo(viewModel: .init(imageURL: URL(string: character.image))),
.information(viewModels: [
.init(value: character.status.text, title: "Status"),
.init(value: character.gender.rawValue, title: "Gender"),
.init(value: character.type, title: "Type"),
.init(value: character.species, title: "Species"),
.init(value: character.origin.name, title: "Origin"),
.init(value: character.location.name, title: "Location"),
.init(value: character.created, title: "Created"),
.init(value: "\(character.episode.count)", title: "Total Episodes"),
]),
.episodes(viewModels: character.episode.compactMap ({
return RMCharacterEpisodeCollectionViewCellViewModel(episodeDataUrl: URL(string: $0))
}))
]
}
...
}
View 생성
각 VM에서 받은 프로퍼티 대로 셀을 만들자. 이번 글에서는 Photo Cell을 구성한다.
Photo Cell
final class RMCharacterPhotoCollectionViewCell: UICollectionViewCell {
...
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(imageView)
setUpConstraints()
}
...
private func setUpConstraints() {
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leftAnchor.constraint(equalTo: contentView.leftAnchor),
imageView.rightAnchor.constraint(equalTo: contentView.rightAnchor),
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
}
public func configure(with viewModel: RMCharacterPhotoCollectionViewCellViewModel) {
}
}
틀이 잡혔으니 실제로 로직을 정의하기 위해 photoVM으로 간다.
여기서 fetchImage를 사용해서 image를 불러올건데 재밌는 점은 우리는 이 시점에서 이미 이미지를 가지고 있어야 정상이라는 점이다. 캐시를 이용하여 url 주소에 상응하는 데이터가 저장되어 있기 때문인데 만약 없다면 문제가 발생한 것이다. guard로 처리해 주자
final class RMCharacterPhotoCollectionViewCellViewModel {
private let imageUrl: URL?
init (imageURL: URL?) {
self.imageUrl = imageURL
}
public func fetchImage(completion: @escaping (Result<Data, Error>) -> Void) {
guard let imageUrl = imageUrl else {
completion(.failure(URLError(.badURL)))
return
}
RMImageLoader.shared.downloadImage(imageUrl, completion: completion)
}
}
다음으로 photo Cell View에서 VM을 주입하자
imageView의 UI를 그리고 configure에 받아온 completion을 적용하여 imageView를 캐시에 저장된 그림으로 바꾸자.
final class RMCharacterPhotoCollectionViewCell: UICollectionViewCell {
...
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(imageView)
setUpConstraints()
}
...
private func setUpConstraints() {
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leftAnchor.constraint(equalTo: contentView.leftAnchor),
imageView.rightAnchor.constraint(equalTo: contentView.rightAnchor),
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
}
public func configure(with viewModel: RMCharacterPhotoCollectionViewCellViewModel) {
viewModel.fetchImage { [weak self] result in
switch result {
case .success(let data):
DispatchQueue.main.async {
self?.imageView.image = UIImage(data: data)
}
case .failure:
break
}
}
}
}
여기서 진짜 생뚱맞은 버그가 있어서 너무 화가 났는데 이건 다른 글로 정리하겠다.
끝
'Swift > Rick & Morty' 카테고리의 다른 글
[iOS] Rick&Morty - #19 Character Info ViewModel (0) | 2023.03.18 |
---|---|
[iOS] Rick&Morty - #18 Character Info Cell (0) | 2023.03.16 |
[iOS] Rick&Morty - #16 Character Detail ViewModels (0) | 2023.03.14 |
[iOS] Rick&Morty - #15 CollectionView Layouts (0) | 2023.03.13 |
[iOS] Rick&Morty - #14 Compositional Layout (1) | 2023.03.12 |