[UIKit] InstagramClone: HomeView

Junyoung Park·2022년 11월 9일
0

UIKit

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

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

InstagramClone: HomeView

구현 목표

  • 홈 뷰 UI 구현

구현 태스크

  • 홈 뷰 섹션 별 데이터 이넘화
  • 홈 뷰 UI 구현

핵심 코드

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() }
                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() }
                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() }
                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()
        }
    }
  • 뷰 모델의 데이터가 네 가지 종류의 이넘을 가지고 있음
  • 각 섹션 별 캐스팅

소스 코드

import 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() }
                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() }
                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() }
                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()
    }
}
  • 뷰 모델이 던져주는 데이터의 인덱스에 따라 서로 다른 셀을 캐스팅
import Foundation
import Combine

struct HomeFeedRenderViewModel {
    let header: PostRenderViewModel
    let post: PostRenderViewModel
    let actions: PostRenderViewModel
    let comments: PostRenderViewModel
}

class HomeViewModel {
    enum Input {
        case isAlreadyLogin
    }
    
    enum Output {
        case loginOutput(result: Bool)
    }
    var feedRenderModels: [HomeFeedRenderViewModel]
    private let authManager = AuthManager.shared
    private let output: PassthroughSubject<Output, Never> = .init()
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        let counts = UserCountModel(follwers: 100, following: 100, posts: 100)
        let photoURL = URL(string: "https://i.stack.imgur.com/GsDIl.jpg")!
        var mockModel = [HomeFeedRenderViewModel]()
        for x in 0..<5 {
            let user = UserModel(email: "email" + "\(x)", bio: "bio", firstName: "firstName", lastName: "lastName", birthDate: Date(), counts: counts, gender: .male, joinDate: Date(), profilePhoto: photoURL)
            let post = UserPostModel(identifier: "post \(x)", postType: .photo, postURL: photoURL, thumnailImage: photoURL, caption: "Caption", comments: [], likeCount: [], createdDate: Date(), taggedUers: [user], owner: user)
            let comment = PostCommentModel(userName: user.firstName + user.lastName, text: "Comment_\(x)", createdDate: Date(), identifier: user.email + Date().formatted())
            let viewModel = HomeFeedRenderViewModel(header: PostRenderViewModel(renderType: .header(provider: user)), post: PostRenderViewModel(renderType: .primaryContent(providier: post)), actions: PostRenderViewModel(renderType: .actions(provider: "action_\(x)")), comments: PostRenderViewModel(renderType: .comments(comments: [comment])))
            mockModel.append(viewModel)
        }
        self.feedRenderModels = mockModel
    }
    
    func transform(input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never> {
        input
            .receive(on: DispatchQueue.global(qos: .default))
            .sink { [weak self] result in
                switch result {
                case .isAlreadyLogin: self?.isAlreadyLogin()
                }
            }
            .store(in: &cancellables)
        return output.eraseToAnyPublisher()
    }
    
    private func isAlreadyLogin() {
        if authManager.isSignedIn() {
            output.send(.loginOutput(result: true))
        } else {
            output.send(.loginOutput(result: false))
        }
    }
}
  • 가데이터 추가!

구현 화면

profile
JUST DO IT
post-custom-banner

0개의 댓글