[UIKit] InstagramClone: ProfileView

Junyoung Park·2022년 11월 7일
0

UIKit

목록 보기
85/142
post-thumbnail

Build Instagram App: Part 8 (Swift 5) - 2020 - Xcode 11 - iOS Development

InstagramClone:

구현 목표

  • 프로필 뷰 컬렉션 뷰 구현

구현 태스크

  • 컬렉션 뷰 섹션 별 구현
  • 컬렉션 뷰 supplementaryView 구현
  • 커스텀 컬렉션 뷰 셀 - 이미지 바인딩

핵심 코드

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        guard kind == UICollectionView.elementKindSectionHeader else { return UICollectionReusableView() }
        
        if indexPath.section == 0 {
            guard let profileHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileInfoHeaderCollectionReusableView.identifier, for: indexPath) as? ProfileInfoHeaderCollectionReusableView else { return UICollectionReusableView() }
            return profileHeader
        } else {
            guard let tabControllHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileTabsCollectionReusableView.identifier, for: indexPath) as? ProfileTabsCollectionReusableView else { return UICollectionReusableView() }
            return tabControllHeader
        }
    }
  • 프로필 뷰 컬렉션 뷰 내 헤더 뷰 등록
  • 두 개의 섹션이므로 indexPath.section을 통해 판별
 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        if section == 0 {
            return CGSize(width: collectionView.width, height: collectionView.height / 3)
        } else {
            return CGSize(width: collectionView.width, height: 65)
        }
    }
  • 컬렉션 뷰의 섹션 헤더 별 사이즈 리턴
func configure(with model: UserPostModel) {
        var imageSubscription: AnyCancellable?
        imageSubscription = NetworkingManager
            .download(with: model.postURL)
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: NetworkingManager.handleCompletion,
                  receiveValue: { [weak self] data in
                self?.photoImageView.image = UIImage(data: data)
                imageSubscription?.cancel()
            })
    }
  • one-shot 퍼블리셔를 통해 지속적으로 퍼블리셔를 구독하지 않고 이미지 다운로드 이후 캔슬
import Foundation
import Combine

class NetworkingManager {
    enum NetworkingError: LocalizedError {
        case badURLResponse(url: URL)
        case unknown
        var errorDescription: String? {
            switch self {
            case .badURLResponse(url: let url): return "[🔥] Bad Response from URL: \(url.absoluteString)"
            case .unknown: return "[⚠️] Unknown error occured"
            }
        }
    }
    
    static func handleURLResponse(output: URLSession.DataTaskPublisher.Output, url: URL) throws -> Data {
        guard
            let response = output.response as? HTTPURLResponse,
            response.statusCode >= 200 && response.statusCode < 300 else
        { throw NetworkingError.badURLResponse(url: url) }
        return output.data
    }
    
    static func download(with url: URL) -> AnyPublisher<Data, Error> {
        return URLSession
            .shared
            .dataTaskPublisher(for: url)
            .tryMap({try handleURLResponse(output: $0, url: url)})
            .retry(3)
            .eraseToAnyPublisher()
    }
    static func handleCompletion(completion: Subscribers.Completion<Error>) {
        switch completion {
        case .failure(let error):
            print(error.localizedDescription)
        case .finished: break
        }
    }
}
  • URLSessions을 사용한 데이터 퍼블리셔 재사용을 위한 별도의 클래스 생성

소스 코드

import UIKit
import Combine

class PhotoCollectionViewCell: UICollectionViewCell {
    static let identifier = "PhotoCollectionViewCell"
    private let photoImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.clipsToBounds = true
        imageView.contentMode = .scaleAspectFill
        return imageView
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        photoImageView.frame = contentView.bounds
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        photoImageView.image = nil
    }
    
    private func setUI() {
        contentView.addSubview(photoImageView)
        contentView.clipsToBounds = true
        contentView.backgroundColor = .secondarySystemBackground
        accessibilityLabel = "User post image"
        accessibilityHint = "Double-tap to open post"
    }
    
    func configure(with image: UIImage) {
        photoImageView.image = image
    }
    
    func configure(with model: UserPostModel) {
        var imageSubscription: AnyCancellable?
        imageSubscription = NetworkingManager
            .download(with: model.postURL)
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: NetworkingManager.handleCompletion,
                  receiveValue: { [weak self] data in
                self?.photoImageView.image = UIImage(data: data)
                imageSubscription?.cancel()
            })
    }
    
}
  • 모델과의 바인딩을 담당할 configure 함수 구현한 커스텀 컬렉션 뷰 셀
import UIKit

class ProfileViewController: UIViewController {
    private var collectionView: UICollectionView?

    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        collectionView?.frame = view.bounds
    }
    
    private func setUI() {
        view.backgroundColor = .systemBackground
        setNavigationBar()
        setCollectionView()
    }
    
    private func setCollectionView() {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        layout.sectionInset = UIEdgeInsets(top: 0, left: 1, bottom: 0, right: 1)
        layout.minimumLineSpacing = 1
        layout.minimumInteritemSpacing = 1
        let size = (view.width - 4) / 3
        layout.itemSize = CGSize(width: size, height: size)
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView?.delegate = self
        collectionView?.dataSource = self
        collectionView?.register(PhotoCollectionViewCell.self, forCellWithReuseIdentifier: PhotoCollectionViewCell.identifier)
        collectionView?.register(ProfileInfoHeaderCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: ProfileInfoHeaderCollectionReusableView.identifier)
        collectionView?.register(ProfileTabsCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: ProfileTabsCollectionReusableView.identifier)
        guard let collectionView = collectionView else { return }
        view.addSubview(collectionView)
    }
    
    private func setNavigationBar() {
        navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "gear")?.withTintColor(UIColor.black, renderingMode: .alwaysOriginal), style: .done, target: self, action: #selector(settingButtonDidTap))
    }
    @objc private func settingButtonDidTap() {
        let vc = SettingsViewController()
        vc.title = "Settings"
        navigationController?.pushViewController(vc, animated: true)
    }
}

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

extension ProfileViewController: UICollectionViewDelegateFlowLayout {
}

extension ProfileViewController: UICollectionViewDataSource {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 2
    }
    
    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        guard kind == UICollectionView.elementKindSectionHeader else { return UICollectionReusableView() }
        
        if indexPath.section == 0 {
            guard let profileHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileInfoHeaderCollectionReusableView.identifier, for: indexPath) as? ProfileInfoHeaderCollectionReusableView else { return UICollectionReusableView() }
            return profileHeader
        } else {
            guard let tabControllHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileTabsCollectionReusableView.identifier, for: indexPath) as? ProfileTabsCollectionReusableView else { return UICollectionReusableView() }
            return tabControllHeader
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        if section == 0 {
            return CGSize(width: collectionView.width, height: collectionView.height / 3)
        } else {
            return CGSize(width: collectionView.width, height: 65)
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        if section == 0 {
            return 0
        } else {
            return 30
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoCollectionViewCell.identifier, for: indexPath) as? PhotoCollectionViewCell else { return UICollectionViewCell() }
        if let image = UIImage(named: "background") {
            cell.configure(with: image)
        }
        return cell
    }
}
  • 헤더, 섹션 별 컬렉션 뷰 구현

import Foundation

enum UserPostType {
    case photo, video
}

struct UserPostModel {
    let identifier: String
    let postType: UserPostType
    let postURL: URL
    let thumnailImage: URL
    let caption: String?
    let comments: [PostCommentModel]
    let likeCount: [PostLikeModel]
    let createdDate: Date
    let taggedUers: [UserModel]
}
  • 포스팅을 담당할 구조체

구현 화면

profile
JUST DO IT

0개의 댓글