지난번엔 컬렉션 뷰를 View에서 잡아주고 Spinner를 정의해 줬다. 이제 컬렉션 뷰에 들어갈 셀을 만들고 형태를 잡아야 한다.
Cell을 만들기 전에 네이밍 컨벤션을 맞추자
기존에는 모든 VM, VC, V들에 RM prefix를 안 붙인 것들이 있을 것이다. 지금 다 고쳐놓는다.
Cell 생성
기본적으로 Cell을 만들때는 6가지 요소를 먼저 만들고 시작하자.
Cell의 이름인 cellIdentifier, 초기화를 담당하는 init, required init.
layout을 위한 addConstraints, 재사용을 위한 prepareForReuse, 마지막으로 ViewModel을 이용하여 화면을 그릴 configure. 이때 들어올 뷰 모델은 유일한 하나이기 때문에 타입명을 생성될 뷰모델의 이름 그대로 적는다.
/// Single cell for a character
final class RMCharacterCollectionViewCell: UICollectionViewCell {
static let cellIdentifier = "RMCharacterCollectionViewCell"
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .secondarySystemBackground
}
required init?(coder: NSCoder) {
fatalError("Unsopported")
}
private func addConstraints() {
}
override func prepareForReuse() {
super.prepareForReuse()
}
public func configure(with viewModel: RMCharacterCollectionViewCellViewModel) {
}
}
Cell UI 설정
imageView로 화면에 이미지를 출력하고 이름과 status를 출력할 것이다. status는 생존여부가 들어가 있다.
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.textColor = .label
label.font = .systemFont(ofSize: 18, weight: .medium)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let statusLabel: UILabel = {
let label = UILabel()
label.textColor = .secondaryLabel
label.font = .systemFont(ofSize: 16, weight: .medium)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
AddSubviews and prepareFouReuse
안에 들어가는 요소의 제약관계를 설정하는 건 Cell 자체가 아닌 ContentView에 붙여서 사용한다.
이는 Cell 클래스와 CollectionView가 ContentView를 제공하고 자동으로 SafeArea를 계산해서 화면에 보여주기 때문이다. 물론, 그냥 바로 설정하는 것도 가능하다. SafeArea를 무시하고 디자인을 적용하고자 하는 경우에는 그렇게 할 수도 있다.
- 애플 디벨로퍼 아카데미에서 수행한 프로젝트에서 Header Cell에서 Header Card라는 View를 만들고 여기 안에 요소를 넣은 적이 있다. 이거 때문에 레이아웃으로 문제가 발생했던 적이 있는데 그때 이걸 알았다면 더 쉽게 처리할 수 있지 않았나 싶다.
contentView.addSubviews(imageView, nameLabel, statusLabel)
override func prepareForReuse() {
super.prepareForReuse()
// 아래 세 요소는 셀이 재사용될 때 초기화되어야 한다.
imageView.image = nil
nameLabel.text = nil
statusLabel.text = nil
}
RMCharacterCollectionViewCellViewModel
이제 셀 설정이 끝났다. 셀에서 사용할 VM을 제작해야 한다.Cell에서는 세가지 요소가 필요하다. 캐릭터 이름, 캐릭터 상태, 캐릭터 이미지.이를 초기화 하는데 text만 노출시키고 싶기 때문에 Computed property로 text를 리턴하도록 한다.
final class RMCharacterCollectionViewCellViewModel {
public let characterName: String
private let characterStatus: RMCharacterStatus
private let characterImageUrl: URL?
// MARK: - INIT
init(
characterName: String,
characterStatus: RMCharacterStatus,
characterImageUrl: URL?
) {
self.characterName = characterName
self.characterStatus = characterStatus
self.characterImageUrl = characterImageUrl
}
public var characterStatusText: String {
return characterStatus.rawValue
}
}
patchImage 메서드 제작
URLError에는. badURL 등의 코드가 존재한다. 이를 적극적으로 쓰는 건 별다른 String 없이도 정확한 상태를 알려줄 수 있다.
간단한 구현 코드다. 근데 여기엔 cash 로직이 없다.
final class RMCharacterCollectionViewCellViewModel: Hashable, Equatable {
...
public func fetchImage(completion: @escaping (Result <Data, Error>) -> Void) {
guard let url = characterImageUrl else {
completion(.failure(URLError(.badURL)))
return
}
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data, error == nil else {
completion(.failure(error ?? URLError(.badServerResponse)))
return
}
completion(.success(data))
}
task.resume()
}
}
Test
테스트를 위해서 VM에서 직접 VM을 생성하고 이를 셀에 넣어서 확인이 가능하다. (실전에서는 VM을 생성하고 넣는 건 Cell View에서 해야 한다)
rick과. alive라는 객체를 받아서 작동하는 케이스를 상정해서 넣는다.
- RMCharacterListViewViewModel
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RMCharacterCollectionViewCell.cellIdentifier, for: indexPath) as? RMCharacterCollectionViewCell else {
fatalError("unsupported cell")
}
let viewModel = RMCharacterCollectionViewCellViewModel(
characterName: "rick",
characterStatus: .alive,
characterImageUrl: nil)
cell.configure(with: viewModel)
return cell
}
UI는 실제로 쓰일 거니까 어느 정도 제대로 만들어서 테스트하자. scaleFit 같은 거 두 번 맞출 필요가 없으니까
- RMCharacterCollectionViewCell
private func addConstraints() {
NSLayoutConstraint.activate([
statusLabel.heightAnchor.constraint(equalToConstant: 40),
nameLabel.heightAnchor.constraint(equalToConstant: 40),
statusLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 5),
statusLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -5),
nameLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 5),
nameLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -5),
statusLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -3),
nameLabel.bottomAnchor.constraint(equalTo: statusLabel.topAnchor, constant: -3),
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.rightAnchor.constraint(equalTo: contentView.rightAnchor),
imageView.leftAnchor.constraint(equalTo: contentView.leftAnchor),
imageView.bottomAnchor.constraint(equalTo: nameLabel.topAnchor)
])
imageView.backgroundColor = .systemMint
nameLabel.backgroundColor = .red
statusLabel.backgroundColor = .green
}
UI가 그려지긴 하는데 데이터를 나오게 안 했다.
요소를 출력하도록 해보자.
Closure 내부의 ARC 관리
상당히 중요한 요소가 나왔는데 모르는 분들은 이걸 보면 당황할 거 같다. 스위프트는 ARC라는 방법을 사용하여 메모리를 관리한다. 만약 Closure에서 self 등의 방법으로 참조를 하게 되면 강한 순환 참조 (Strong Reference Cycle)이라는 현상이 발생하고 이는 메모리 릭(Memory Leak)을 야기한다. ARC는 따로 공부해야 할 만큼 중요하고 양이 적이 않아 여기까지만 적겠지만, 이를 방지하기 위해서 [weak self]로 RC를 증가시키지 않게 하기 위함이다.
우리가 MVVM에서 데이터의 방향을 정할 때 양방향 연결을 의도하지 않는다. 그렇기 때문에 디스패치큐에서는 self에 ?를 붙여서 옵셔널로 사용하자.
아래와 같이 업데이트를 하게 만들면 정상적으로 데이터가 출력되는 걸 볼 수 있다. 물론 이상해 보이는 건 아직 틀만 잡았기 때문이다. 나머진 다음에 한다.
public func configure(with viewModel: RMCharacterCollectionViewCellViewModel) {
nameLabel.text = viewModel.characterName
statusLabel.text = viewModel.characterStatusText
viewModel.fetchImage { [weak self] result in
switch result {
case .success(let data):
DispatchQueue.main.async {
let image = UIImage(data: data)
self?.imageView.image = image
}
case .failure(let error):
print(String(describing: error))
break
}
}
}
끝
'Swift > Rick & Morty' 카테고리의 다른 글
[iOS] Rick&Morty - #9 Character Detail View (0) | 2023.03.06 |
---|---|
[iOS] Rick&Morty - #8 Showing Characters (0) | 2023.03.05 |
[iOS] Rick&Morty - #6 Character List View (0) | 2023.03.03 |
[iOS] Rick&Morty - #5 API Call (0) | 2023.03.02 |
[iOS] Rick&Morty - #4 API Request (0) | 2023.03.01 |