[UIKit] UICollectionView: Create

Junyoung Park·2022년 10월 28일
0

UIKit

목록 보기
56/142
post-thumbnail
post-custom-banner

Swift for Beginners: Create Collection View in Xcode (iOS - 2022)
>

UICollectionView: Create

구현 목표

  • 컬렉션 뷰의 기본적인 구현

구현 태스크

  • 코드를 통해 컬렉션 뷰 구현
  • Combine을 통해 이미지 다운로드
  • 이미지 다운로드에 따라 비동기적 컬렉션 뷰 표현

핵심 코드

private let collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 120, height: 120)
        layout.minimumLineSpacing = 10
        layout.scrollDirection = .vertical
        layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
        return collectionView
    }()
  • 커스텀 컬렉션 뷰
extension CollectionViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return viewModel.imageModels.value?.count ?? 0
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as? CustomCollectionViewCell else { return CustomCollectionViewCell() }
        if let models = viewModel.imageModels.value {
            let model = models[indexPath.row]
            cell.configure(with: model)
        }
        return cell
    }
}
  • 컬렉션 뷰에 해당하는 데이터소스를 사용하는 커스텀 셀 바인딩

소스 코드

import Foundation
import UIKit
import Combine

class CollectionViewModel {
    private let urlString: String
    private var cancellables = Set<AnyCancellable>()
    private let picsumModels: PassthroughSubject<[PicsumModel], Never> = .init()
    let imageModels: CurrentValueSubject<[ImageModel]?, Never> = .init(nil)
    
    
    init(urlString: String = "https://picsum.photos/v2/list") {
        self.urlString = urlString
        addSubcription()
    }
    
    private func addSubcription() {
        guard let url = URL(string: urlString) else { return }
        URLSession
            .shared
            .dataTaskPublisher(for: url)
            .receive(on: DispatchQueue.global(qos: .background))
            .tryMap(handleOutput)
            .decode(type: [PicsumModel].self, decoder: JSONDecoder())
            .sink { completion in
                switch completion {
                case .finished: break
                case .failure(let error):
                    print(error.localizedDescription)
                }
            } receiveValue: { [weak self] models in
                self?.picsumModels.send(models)
            }
            .store(in: &cancellables)
        picsumModels
            .receive(on: DispatchQueue.global(qos: .background))
            .sink { [weak self] models in
                models.forEach { model in
                    self?.handlePicsumModel(with: model)
                }
            }
            .store(in: &cancellables)
    }
    
    private func handlePicsumModel(with model: PicsumModel) {
        guard let url = URL(string: model.download_url) else { return }
        URLSession
            .shared
            .dataTaskPublisher(for: url)
            .receive(on: DispatchQueue.global(qos: .background))
            .tryMap(handleOutput)
            .compactMap({UIImage(data: $0)})
            .sink { completion in
                switch completion {
                case .failure(let error):
                    print(error.localizedDescription)
                case .finished: break
                }
            } receiveValue: { [weak self] image in
                var currentModels = self?.imageModels.value ?? []
                let currentModel = ImageModel(imageId: model.id, image: image)
                currentModels.append(currentModel)
                self?.imageModels.send(currentModels)
            }
            .store(in: &cancellables)


    }
    
    private func handleOutput(output: URLSession.DataTaskPublisher.Output) throws -> Data {
           guard
               let response = output.response as? HTTPURLResponse,
               response.statusCode >= 200 && response.statusCode < 300 else {
               throw URLError(.badServerResponse)
           }
           return output.data
       }
}
  • URL을 통해 이미지를 다운로드받는 뷰 모델
  • 컬렉션 뷰의 데이터를 핸들링
import UIKit
import Combine

class CollectionViewController: UIViewController {
    private let collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 120, height: 120)
        layout.minimumLineSpacing = 10
        layout.scrollDirection = .vertical
        layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
        return collectionView
    }()
    private let viewModel: CollectionViewModel
    private var cancellables = Set<AnyCancellable>()
    
    init(viewModel: CollectionViewModel = CollectionViewModel()) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
    }
    
    private func setUI() {
        view.backgroundColor = .systemRed
        collectionView.delegate = self
        collectionView.dataSource = self
        view.addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        viewModel
            .imageModels
            .receive(on: DispatchQueue.main)
            .sink { [weak self] models in
                self?.collectionView.reloadData()
            }
            .store(in: &cancellables)
    }
}

extension CollectionViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        collectionView.deselectItem(at: indexPath, animated: true)
    }
}

extension CollectionViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return viewModel.imageModels.value?.count ?? 0
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as? CustomCollectionViewCell else { return CustomCollectionViewCell() }
        if let models = viewModel.imageModels.value {
            let model = models[indexPath.row]
            cell.configure(with: model)
        }
        return cell
    }
}
  • 컬렉션 뷰를 로드 및 뷰 모델 데이터를 통해 리로드
import UIKit

class CustomCollectionViewCell: UICollectionViewCell {
    static let identifier = "customCollectionViewCell"
    private let idLabel: UILabel = {
        let label = UILabel()
        label.textColor = .black
        label.textAlignment = .center
        label.font = .preferredFont(forTextStyle: .headline)
        return label
    }()
    private let imageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .center
        return imageView
    }()
    
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.backgroundColor = .secondarySystemGroupedBackground
        contentView.addSubview(idLabel)
        contentView.addSubview(imageView)
        contentView.clipsToBounds = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        idLabel.frame = CGRect(x: 10, y: 0, width: contentView.frame.size.width - 15, height: contentView.frame.size.height / 2)
        imageView.frame = CGRect(x: 10, y: contentView.frame.size.height / 2, width: contentView.frame.size.width - 15, height: contentView.frame.size.height / 2)
    }
    
    private func setLayout() {
        idLabel.translatesAutoresizingMaskIntoConstraints = false
        imageView.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(idLabel)
        contentView.addSubview(imageView)
        idLabel.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
        idLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        idLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
        imageView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
        imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
        imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
        imageView.leadingAnchor.constraint(equalTo: idLabel.trailingAnchor).isActive = true
    }
    
    func configure(with model: ImageModel) {
        idLabel.text = model.imageId
        imageView.image = model.image
    }
}
  • 입력받은 이미지 모델을 통해 커스텀 셀 바인딩 함수 구현
struct ImageModel {
    let imageId: String
    let image: UIImage
}

struct PicsumModel: Codable {
    let id: String
    let author: String
    let width: Int
    let height: Int
    let url: String
    let download_url: String
}
  • 다운로드에 사용할 Codable 데이터 및 이를 통해 컬렉션 뷰가 사용할 모델

구현 화면

매우 간단한 복습 과정.

profile
JUST DO IT
post-custom-banner

0개의 댓글