[UIKit] InstagramClone: HomeView 2

Junyoung Park·2022년 11월 9일
0

UIKit

목록 보기
92/142
post-thumbnail

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

InstagramClone: HomeView 2

구현 목표

  • 홈 뷰의 커스텀 셀 UI 구현

구현 태스크

  • 헤더 셀 UI 및 인터렉션 바인딩
  • 액션 셀 UI 및 인터렉션 바인딩

핵심 코드

private func bind(in output: AnyPublisher<IGFeedPostHeaderTableViewCell.Output, Never>) {
        output
            .receive(on: DispatchQueue.main)
            .sink { [weak self] result in
                switch result {
                case .didTapMoreButton: self?.showActionSheet()
                }
            }
            .store(in: &cancellables)
    }
  • 헤더 테이즐 뷰 셀의 아웃풋을 퍼블리셔로 받아 현재 홈 뷰 컨트롤러에서 해당 버튼 이벤트를 감지하는 함수
func configure(with model: UserModel) {
        output = .init()
        userNameLabel.text = model.firstName + model.lastName
        var imageSubscription: AnyCancellable?
        imageSubscription = NetworkingManager
            .download(with: model.profilePhoto)
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] data in
                self?.profilePhotoImageView.image = UIImage(data: data)
                imageSubscription?.cancel()
            })
    }
  • 해당 셀의 아웃풋 퍼블리셔를 리턴하는 함수
private func bind() {
        moreButton
            .tapPublisher
            .sink { [weak self] _ in
                self?.output.send(.didTapMoreButton)
            }
          
  • 해당 셀의 컴포넌트와 아웃풋 퍼블리셔를 연결
override func prepareForReuse() {
        super.prepareForReuse()
        profilePhotoImageView.image = nil
        userNameLabel.text = nil
        output.send(completion: .finished)
    }
  • 현재 셀이 사라질 때 UI를 nil out
  • 변수로 선언한 퍼블리셔를 종료함으로써 연결 해제
  • configure 부분에서 output을 새롭게 이니셜라이즈하기 때문에 동작 보장

소스 코드

mport UIKit
import Combine

class HomeViewController: UIViewController {
    private let input: PassthroughSubject<HomeViewModel.Input, Never> = .init()
    private let viewModel = HomeViewModel()
    private var cancellables = Set<AnyCancellable>()
    private let tableView: UITableView = {
        let tableView = UITableView()
        tableView.register(IGFeedPostTableViewCell.self, forCellReuseIdentifier: IGFeedPostTableViewCell.identifier)
        tableView.register(IGFeedPostHeaderTableViewCell.self, forCellReuseIdentifier: IGFeedPostHeaderTableViewCell.identifier)
        tableView.register(IGFeedPostGeneralTableViewCell.self, forCellReuseIdentifier: IGFeedPostGeneralTableViewCell.identifier)
        tableView.register(IGFeedPostActionsTableViewCell.self, forCellReuseIdentifier: IGFeedPostActionsTableViewCell.identifier)
        return tableView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
        bind()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        input.send(.isAlreadyLogin)
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.frame = view.bounds
    }
    
    private func setUI() {
        view.backgroundColor = .systemBackground
        view.addSubview(tableView)
        tableView.dataSource = self
        tableView.delegate = self
    }
    
    private func bind() {
        let output = viewModel.transform(input: input.eraseToAnyPublisher())
        output
            .receive(on: DispatchQueue.main)
            .sink { [weak self] result in
                switch result {
                case .loginOutput(result: let result):
                    if !result {
                        self?.handleNotLogined()
                    }
                }
            }
            .store(in: &cancellables)
    }
    
    private func handleNotLogined() {
        let loginVC = LoginViewController()
        loginVC.modalPresentationStyle = .fullScreen
        present(loginVC, animated: true)
    }
}

extension HomeViewController: UITableViewDelegate {
    
}

extension HomeViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let x = section
        let model: HomeFeedRenderViewModel
        if x == 0 {
            model = viewModel.feedRenderModels[0]
        } else {
            let position = x % 4 == 0 ? x / 4 : ((x - (x % 4)) / 4)
            model = viewModel.feedRenderModels[position]
        }
        let subSection = x % 4
        switch subSection {
        case 0:
            //header
            return 1
        case 1:
            // post
            return 1
        case 2:
            return 1
            // actions
        case 3:
            let commentsModel = model.comments
            switch commentsModel.renderType {
            case .comments(comments: let comments):
                return comments.count > 2 ? 2 : comments.count
            default: return 0
            }
            // comments
        default: return 0
        }
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let x = indexPath.section
        let model: HomeFeedRenderViewModel
        if x == 0 {
            model = viewModel.feedRenderModels[0]
        } else {
            let position = x % 4 == 0 ? x / 4 : ((x - (x % 4)) / 4)
            model = viewModel.feedRenderModels[position]
        }

        let subSection = x % 4
        switch subSection {
        case 0:
            //header
            let headerModel = model.header
            switch headerModel.renderType {
            case .header(provider: let user):
                guard let cell = tableView.dequeueReusableCell(withIdentifier: IGFeedPostHeaderTableViewCell.identifier, for: indexPath) as? IGFeedPostHeaderTableViewCell else { return UITableViewCell() }
                cell.configure(with: user)
                let output = cell.transform()
                bind(in: output)
                return cell
            default: return UITableViewCell()
            }
        case 1:
            // post
            let postModel = model.post
            switch postModel.renderType {
            case .primaryContent(providier: let post):
                guard let cell = tableView.dequeueReusableCell(withIdentifier: IGFeedPostTableViewCell.identifier, for: indexPath) as? IGFeedPostTableViewCell else { return UITableViewCell() }
                cell.configure(with: post)
                return cell
            default: return UITableViewCell()
            }
        case 2:
            let actionModel = model.actions
            switch actionModel.renderType {
            case .actions(provider: let action):
                guard let cell = tableView.dequeueReusableCell(withIdentifier: IGFeedPostActionsTableViewCell.identifier, for: indexPath) as? IGFeedPostActionsTableViewCell else { return UITableViewCell() }
                let output = cell.transform()
                bind(in: output)
                return cell
            default: return UITableViewCell()
            }
            // actions
        case 3:
            let commentModel = model.comments
            // comments
            switch commentModel.renderType {
            case .comments(comments: let comments):
                guard let cell = tableView.dequeueReusableCell(withIdentifier: IGFeedPostGeneralTableViewCell.identifier, for: indexPath) as?  IGFeedPostGeneralTableViewCell else { return UITableViewCell() }
                return cell
            default: return UITableViewCell()
            }
        default: return UITableViewCell()
        }
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return viewModel.feedRenderModels.count * 4
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let subSection = indexPath.section % 4
        switch subSection {
        case 0: return 70
        case 1: return tableView.width
        case 2: return 60
        case 3: return 50
        default: return .zero
        }
    }
    
    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        let subSection = section % 4
        return subSection == 3 ? 70 : 0
    }
    
    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        return UIView()
    }
}

extension HomeViewController {
    private func bind(in output: AnyPublisher<IGFeedPostHeaderTableViewCell.Output, Never>) {
        output
            .receive(on: DispatchQueue.main)
            .sink { [weak self] result in
                switch result {
                case .didTapMoreButton: self?.showActionSheet()
                }
            }
            .store(in: &cancellables)
    }
    private func bind(in output: AnyPublisher<IGFeedPostActionsTableViewCell.Output, Never>) {
        output
            .receive(on: DispatchQueue.main)
            .sink { [weak self] result in
                switch result {
                case .likeButtonDidTap:
                    break
                case .commentButtonDidTap:
                    break
                case .sendButtonDidTap:
                    break
                }
            }
            .store(in: &cancellables)
    }
    
    private func showActionSheet() {
        let actionSheet = UIAlertController(title: "Post Options", message: nil, preferredStyle: .actionSheet)
        actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        actionSheet.addAction(UIAlertAction(title: "Report Post", style: .destructive, handler: { [weak self] _ in
            self?.reportPost()
        }))
        present(actionSheet, animated: true)
    }
    
    private func reportPost() {
        
    }
}
  • 헤더 셀의 버튼 클릭 시 액션 시트 반응
import UIKit
import Combine
import AVFoundation

class IGFeedPostTableViewCell: UITableViewCell {
    static let identifier = "IGFeedPostTableViewCell"
    private let postImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        imageView.backgroundColor = nil
        return imageView
    }()
    private var player: AVPlayer?
    private var playerLayer = AVPlayerLayer()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        postImageView.frame = contentView.bounds
        playerLayer.frame = contentView.bounds
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        postImageView.image = nil
    }
    
    private func setUI() {
        contentView.layer.addSublayer(playerLayer)
        contentView.addSubview(postImageView)
    }
    
    func configure(with model: UserPostModel) {
        switch model.postType {
        case .photo:
            var photoSubscription: AnyCancellable?
            photoSubscription = NetworkingManager
                .download(with: model.postURL)
                .receive(on: DispatchQueue.main)
                .sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] data in
                    self?.postImageView.image = UIImage(data: data)
                    photoSubscription?.cancel()
                })
        case .video:
            player = AVPlayer(url: model.postURL)
            playerLayer.player = player
            playerLayer.player?.volume = 0
            playerLayer.player?.play()
            
            // load and play video
        }
    }
}
  • URL 데이터를 통해 사진 및 비디오 로드
import UIKit
import Combine

class IGFeedPostHeaderTableViewCell: UITableViewCell {
    enum Output {
        case didTapMoreButton
    }
    static let identifier = "IGFeedPostHeaderTableViewCell"
    private let profilePhotoImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.clipsToBounds = true
        imageView.layer.masksToBounds = true
        imageView.contentMode = .scaleAspectFill
        return imageView
    }()
    private let userNameLabel: UILabel = {
        let label = UILabel()
        label.textColor = .label
        label.numberOfLines = 1
        label.font = .systemFont(ofSize: 18, weight: .medium)
        return label
    }()
    private let moreButton: UIButton = {
        let button = UIButton()
        button.tintColor = .label
        button.setImage(UIImage(systemName: "ellipsis"), for: .normal)
        return button
    }()
    private var output: PassthroughSubject<Output, Never> = .init()
    private var cancellables = Set<AnyCancellable>()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setUI()
        bind()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        let size = contentView.height - 4
        profilePhotoImageView.frame = CGRect(x: 2, y: 2, width: size, height: size)
        profilePhotoImageView.layer.cornerRadius = size / 2
        moreButton.frame = CGRect(x: contentView.width - size - 2, y: 2, width: size, height: size)
        userNameLabel.frame = CGRect(x: profilePhotoImageView.right + 10, y: 2, width: contentView.width - (size * 2) - 15, height: contentView.height - 4)
    }
    
    private func setUI() {
        contentView.addSubview(profilePhotoImageView)
        contentView.addSubview(userNameLabel)
        contentView.addSubview(moreButton)
    }
    
    func configure(with model: UserModel) {
        output = .init()
        userNameLabel.text = model.firstName + model.lastName
        var imageSubscription: AnyCancellable?
        imageSubscription = NetworkingManager
            .download(with: model.profilePhoto)
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] data in
                self?.profilePhotoImageView.image = UIImage(data: data)
                imageSubscription?.cancel()
            })
    }
    
    func transform() -> AnyPublisher<Output, Never> {
        return output.eraseToAnyPublisher()
    }
    
    private func bind() {
        moreButton
            .tapPublisher
            .sink { [weak self] _ in
                self?.output.send(.didTapMoreButton)
            }
            .store(in: &cancellables)
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        profilePhotoImageView.image = nil
        userNameLabel.text = nil
        output.send(completion: .finished)
    }

}
  • 헤더 담당 셀

구현 화면

profile
JUST DO IT

0개의 댓글