(iOS) Rick&Morty – #20 가져오기 에피소드


#20

우리는 에피소드의 날짜를 잡고 세 번째 섹션을 작성하기 위해 노력하고 있습니다.

세 번째 섹션

마지막으로 두 번째 섹션인 정보 셀을 채우는 작업을 했습니다.

세 번째 섹션이 기억나지 않는다면 다음과 같이 파란색으로 칠하십시오. 파란색 카드 행이 있을 것입니다. 많은 에피소드가 만들어졌기에 릭 산체스의 경우 51개다.

final class RMCharacterEpisodeCollectionViewCell: UICollectionViewCell {
    ...
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.backgroundColor = .systemBlue
        contentView.layer.cornerRadius = 8
    }
		...
}


에피소드 VM에는 현재 URL만 있습니다. 이 URL에는 각 에피소드에 대한 정보가 포함되어 있으며 이는 추상화를 위한 모델을 만들어야 함을 의미합니다.

VM에서 fetchepisode 메서드를 만들고 configure에서 내보내도록 설정해 보겠습니다.

final class RMCharacterEpisodeCollectionViewCell: UICollectionViewCell {
    ...
    public func configure(with viewModel: RMCharacterEpisodeCollectionViewCellViewModel) {
        viewModel.fetchEpisode()
    }
}
final class RMCharacterEpisodeCollectionViewCellViewModel {
    ...
    public func fetchEpisode() {
        guard let url = episodeDataUrl,
              let request = RMRequest(url: url) else {
            return
        }
        RMService.shared.execute(request,
                                 expecting: RMEpisode.self) { result in
            
        }
    }
}

여기서 테이블뷰나 콜렉션뷰의 셀에 대한 이해가 필요합니다. 셀이 대기열에서 제거될 때마다 구성 기능이 호출됩니다. 즉, Congifure로 따라잡을 때 셀을 통과할 때 호출이 계속되는 상황이 발생합니다. 물론 이것은 불필요한 작업입니다.

이를 해결하는 방법에는 두 가지가 있습니다.

  1. 플래그를 설정하면 이미 가져온 데이터는 처리되지 않습니다.
  2. 모든 회차 데이터를 받기 위한 요청을 선제적으로 전송(보내기)하도록 설계되었습니다. 그렇다면 일종의 백그라운드 작업입니다.

먼저 첫 번째 방법. 패치는 isFetching이라는 플래그를 사용하여 각 CellVM에 대해 한 번만 수행됩니다.

final class RMCharacterEpisodeCollectionViewCellViewModel {
    ...
    private var isFetching = false
    ...
    public func fetchEpisode() {
        guard !isFetching else {
            return
        }
        
        guard let url = episodeDataUrl,
              let request = RMRequest(url: url) else {
            return
        }
        isFetching = true
        
        RMService.shared.execute(request,
                                 expecting: RMEpisode.self) { result in
            switch result {
            case .success(let success):
                print(String(describing: success.id))
            case .failure(let failure):
                print(String(describing: failure))
            }
        }
    }
}

인쇄해보면 화면에서 카드가 dequeue될 때마다 ID가 인쇄되지 않고 일단 생성되면 인쇄되지 않는 것을 확인할 수 있습니다.

이제 VM은 모델과 함께 데이터를 인쇄하도록 보기에 알려야 합니다. 어떻게? 물론 지금까지 작성했던 델리게이트도 사용할 수 있습니다. 그러나 다른 방법이 있습니다. 새로운 방법을 시도해보자

final class RMCharacterEpisodeCollectionViewCellViewModel {
    ...
    private var episode: RMEpisode?
    ...
    public func fetchEpisode() {
        ...
        RMService.shared.execute(request,
                                 expecting: RMEpisode.self) { (weak self) result in
            switch result {
            case .success(let model):
                self?.episode = model
            ...
            }
        }
    }
}

게시자 및 구독자 패턴

  • Pub Sub 패턴이라고도 합니다.

registerForData 메서드를 사용하여 게시합니다. 이 메서드에는 완료 기능이 있으며 이제 이를 블록이라고 합니다. Alfraz는 그것이 더 나은 용어라고 생각합니다.

다음으로 프로토콜이 생성되고 프로토콜은 필요한 데이터를 정의합니다. 다음으로 데이터를 등록할 때 모델이 아닌 로그를 직접 가져오고자 합니다.

이렇게 하면 숨겨진 동안 모델의 모든 정보를 유지할 수 있습니다.

블록은 전역 범위에서 수신 및 사용되어야 합니다. 여기서 매우 흥미로운 부분은 블록 자체의 유형을 (Protocol → Void)로 설정할 수 있다는 것입니다. 생각지도 못한 방법이었다.

다음으로 에피소드의 didset은 모델을 dataBlock(model)에 전달합니다. 모델이 성공적으로 전달되면 성공하면 백그라운드에서 데이터를 업데이트합니다.

protocol RMEpisodeDataRnder {
    var name: String { get }
    var airDate: String { get }
    var episode: String { get }
}

final class RMCharacterEpisodeCollectionViewCellViewModel {
    ...
    private var dataBlock: ((RMEpisodeDataRnder) -> Void)?
    
    private var episode: RMEpisode? {
        didSet {
            guard let model = episode else {
                return
            }
            dataBlock?(model)
        }
    }
    ...
    public func registerForData(_ block: @escaping (RMEpisodeDataRnder) -> Void) {
        self.dataBlock = block
    }
    
    public func fetchEpisode() {
        ...
        RMService.shared.execute(request,
                                 expecting: RMEpisode.self) { (weak self) result in
            switch result {
            case .success(let model):
                DispatchQueue.main.async {
                    self?.episode = model
                }
            ...
            }
        }
    }
}

VM에서 작업이 완료되면 Cell에 표시합니다. 먼저 출력입니다.

final class RMCharacterEpisodeCollectionViewCell: UICollectionViewCell {
    ...
    public func configure(with viewModel: RMCharacterEpisodeCollectionViewCellViewModel) {
        viewModel.registerForData { data in
            print(String(describing: data))
        }
        viewModel.fetchEpisode()
    }
}

이제 이 구조를 가로지르면 경계선의 경우가 발생합니다.

극단적인 경우는 가져오기를 통해 이미 데이터를 가져온 상태에서 데이터를 셀로 가져오는 경우입니다.

이 문제를 해결하기 위해 모델을 보호자에게 반환하기 전에 dataBlock으로 전달합니다.

public func fetchEpisode() {
        guard !isFetching else {
            if let model = episode {
                self.dataBlock?(model)
            }
            return
        }
        ...
    }

Protocol, DidSet, DataBlock으로 델리게이트를 사용하는 것과 동일한 기능을 수행하는 테마를 구현해봤습니다.

Cell View에서 viewModel을 통해 접근 가능한 데이터를 보면 log에 name, airDate, episode만 정의되어 있는 것을 확인할 수 있다. 이를 통해 효과적으로 데이터를 숨길 수 있습니다.

여기서 우리가 한 모든 작업을 기억한다면 탭 표시줄에 “에피소드” 탭이 있음을 알 수 있습니다. 그런데 생각해보면 에피소드 탭에 있는 패치를 통해 에피소드 정보를 불러와야 할까요? 왜 두 번 소통해야 합니까?

존재하지 않습니다. 나중에 세션을 캐싱하거나 DB 또는 Core Data를 통해 데이터를 저장하면 해결됩니다.