[UIKit] InstagramClone: ProfileView 4 & NotificationView

Junyoung Park·2022년 11월 7일
0

UIKit

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

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

InstagramClone: ProfileView 4 & NotificationView

구현 목표

  • 노티피케이션 뷰 UI 세팅
  • 프로필 뷰의 팔로잉 / 팔로워 뷰 UI 및 뷰 바인딩

구현 태스크

  • 팔로잉 / 팔로워 테이블 뷰 셀 - 리스트 뷰 컨트롤러 연결

핵심 코드

extension ListViewController {
    private func bind(in output: AnyPublisher<UserFollowTableViewCell.Output, Never>) {
        output
            .sink { [weak self] result in
                switch result {
                case .followButtonDidTap(model: let model):
                    print("\(model.name) is currently \(model.type == .following ? "following" : "unfollowing")")
                }
            }
            .store(in: &cancellables)
    }
}
  • 리스트 뷰 컨트롤러에서 각 팔로잉 / 팔로워 셀의 상태 감지
override func prepareForReuse() {
        super.prepareForReuse()
        nameLabel.text = nil
        profileImageView.image = nil
        userNameLabel.text = nil
        followButton.setTitle(nil, for: .normal)
        followButton.backgroundColor = nil
        followButton.layer.borderWidth = 0
        output.send(completion: .finished)
    }
  • finished를 통해 해당 퍼블리셔 해제
 func configure(with model: UserRelationshipModel) {
        output = .init()
        userModel = model
        nameLabel.text = model.name
        userNameLabel.text = model.userName
        followingStatePublisher.send(model.type)
    }
  • 새롭게 셀이 뷰에 드러날 때 새로운 아웃풋 인스턴스를 넣어줌으로써 새로운 퍼블리셔 흐름이 가능해짐
private func bind() {
        followingStatePublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] type in
                self?.toggleButton(type: type)
            }
            .store(in: &cancellables)
        followButton
            .tapPublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                guard let currentUserModel = self?.userModel else { return }
                
                if let currentState = self?.followingStatePublisher.value {
                    if currentState == .following {
                        self?.followingStatePublisher.send(.not_following)
                        let newUserModel = UserRelationshipModel(userName: currentUserModel.userName, name: currentUserModel.name, type: .not_following)
                        self?.output.send(.followButtonDidTap(model: newUserModel))
                    } else {
                        self?.followingStatePublisher.send(.following)
                        let newUserModel = UserRelationshipModel(userName: currentUserModel.userName, name: currentUserModel.name, type: .following)
                        self?.output.send(.followButtonDidTap(model: newUserModel))
                    }
                }
            }
            .store(in: &cancellables)
    }
    
  • 버튼 토글 버튼을 통해 뷰 컨트롤러에 현재 바뀐 유저 정보 이넘으로 전달 가능

소스 코드

import UIKit
import Combine

class ListViewController: UIViewController {
    private let viewModel: ListViewModel
    private let tableView: UITableView = {
        let tableView = UITableView()
        tableView.register(UserFollowTableViewCell.self, forCellReuseIdentifier: UserFollowTableViewCell.identifier)
        return tableView
    }()
    private var cancellables = Set<AnyCancellable>()
    
    init(data: [UserRelationshipModel]) {
        self.viewModel = ListViewModel(listData: data)
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.frame = view.bounds
    }
    
    private func setUI() {
        view.backgroundColor = .systemBackground
        view.addSubview(tableView)
        tableView.delegate = self
        tableView.dataSource = self
    }
}

extension ListViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.listData.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: UserFollowTableViewCell.identifier, for: indexPath) as? UserFollowTableViewCell else { return UITableViewCell() }
        let model = viewModel.listData[indexPath.row]
        cell.configure(with: model)
        let output = cell.transform()
        bind(in: output)
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 75
    }
}

extension ListViewController {
    private func bind(in output: AnyPublisher<UserFollowTableViewCell.Output, Never>) {
        output
            .sink { [weak self] result in
                switch result {
                case .followButtonDidTap(model: let model):
                    print("\(model.name) is currently \(model.type == .following ? "following" : "unfollowing")")
                }
            }
            .store(in: &cancellables)
    }
}

extension ListViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    }
}
  • 팔로워 / 팔로잉 상태를 보는 리스트 뷰
import UIKit
import Combine

class UserFollowTableViewCell: UITableViewCell {
    enum Output {
        case followButtonDidTap(model: UserRelationshipModel)
    }
    
    static let identifier = "UserFollowTableViewCell"
    private var cancellables = Set<AnyCancellable>()
    private let profileImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.layer.masksToBounds = true
        imageView.backgroundColor = .secondarySystemBackground
        imageView.contentMode = .scaleAspectFill
        return imageView
    }()
    private let nameLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 1
        label.font = .systemFont(ofSize: 17, weight: .semibold)
        return label
    }()
    private let userNameLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 1
        label.font = .systemFont(ofSize: 16, weight: .regular)
        label.textColor = .secondaryLabel
        return label
    }()
    private let followButton: UIButton = {
        let button = UIButton()
        button.backgroundColor = .link
        return button
    }()
    private var output: PassthroughSubject<Output, Never> = .init()
    private var userModel: UserRelationshipModel?
    private let followingStatePublisher: CurrentValueSubject<FollowState, Never> = .init(.following)
    
    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 prepareForReuse() {
        super.prepareForReuse()
        nameLabel.text = nil
        profileImageView.image = nil
        userNameLabel.text = nil
        followButton.setTitle(nil, for: .normal)
        followButton.backgroundColor = nil
        followButton.layer.borderWidth = 0
        output.send(completion: .finished)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        profileImageView.frame = CGRect(x: 3, y: 3, width: contentView.height-6, height: contentView.height-6)
        profileImageView.layer.cornerRadius = profileImageView.height / 2.0
        let labelHeight = contentView.height / 2
        let buttonWidth = contentView.width > 500 ? 220 : contentView.width / 3
        nameLabel.frame = CGRect(x: profileImageView.right + 5, y: 0, width: contentView.width - 8 - profileImageView.width - buttonWidth, height: labelHeight)
        userNameLabel.frame = CGRect(x: profileImageView.right + 5, y: nameLabel.bottom, width: contentView.width - 8 - profileImageView.width - buttonWidth, height: labelHeight)
        followButton.frame = CGRect(x: contentView.width - 5 - buttonWidth, y: (contentView.height - 40) / 2, width: buttonWidth, height: 40)
    }
    
    private func setUI() {
        contentView.addSubview(nameLabel)
        contentView.addSubview(profileImageView)
        contentView.addSubview(followButton)
        contentView.addSubview(userNameLabel)
        contentView.clipsToBounds = true
    }
    
    func configure(with model: UserRelationshipModel) {
        output = .init()
        userModel = model
        nameLabel.text = model.name
        userNameLabel.text = model.userName
        followingStatePublisher.send(model.type)
    }
    
    private func toggleButton(type: FollowState) {
        if type == .following {
            followButton.setTitle("Unfollow", for: .normal)
            followButton.setTitleColor(.label, for: .normal)
            followButton.backgroundColor = .systemBackground
            followButton.layer.borderWidth = 1
            followButton.layer.borderColor = UIColor.label.cgColor
        } else {
            followButton.setTitle("Follow", for: .normal)
            followButton.setTitleColor(.label, for: .normal)
            followButton.backgroundColor = .systemBlue
            followButton.layer.borderWidth = 0
        }
    }
    
    private func bind() {
        followingStatePublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] type in
                self?.toggleButton(type: type)
            }
            .store(in: &cancellables)
        followButton
            .tapPublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                guard let currentUserModel = self?.userModel else { return }
                
                if let currentState = self?.followingStatePublisher.value {
                    if currentState == .following {
                        self?.followingStatePublisher.send(.not_following)
                        let newUserModel = UserRelationshipModel(userName: currentUserModel.userName, name: currentUserModel.name, type: .not_following)
                        self?.output.send(.followButtonDidTap(model: newUserModel))
                    } else {
                        self?.followingStatePublisher.send(.following)
                        let newUserModel = UserRelationshipModel(userName: currentUserModel.userName, name: currentUserModel.name, type: .following)
                        self?.output.send(.followButtonDidTap(model: newUserModel))
                    }
                }
            }
            .store(in: &cancellables)
    }
    
    func transform() -> AnyPublisher<Output, Never> {
        return output.eraseToAnyPublisher()
    }
}
  • 아웃풋 데이터 퍼블리셔의 finish 이후 새롭게 인스턴스를 줌으로써 생성 가능
import UIKit

class NotificationViewController: UIViewController {
    private let tableView: UITableView = {
        let tableView = UITableView()
        tableView.isHidden = true
        tableView.register(NotificationLikeTableViewCell.self, forCellReuseIdentifier: NotificationLikeTableViewCell.identifier)
        tableView.register(NotificationFollowEventTableViewCell.self, forCellReuseIdentifier: NotificationFollowEventTableViewCell.identifier)
        return tableView
    }()
    private lazy var noNotificationView = NoNotificationView()
    private let spinner: UIActivityIndicatorView = {
        let spinner = UIActivityIndicatorView(style: .large)
        spinner.hidesWhenStopped = true
        spinner.tintColor = .label
        return spinner
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.frame = view.bounds
        noNotificationView.frame = CGRect(x: 0, y: 0, width: view.width / 2, height: view.width / 4)
        noNotificationView.center = view.center
        spinner.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        spinner.center = view.center
    }
    
    private func setUI() {
        view.addSubview(spinner)
        spinner.startAnimating()
        view.backgroundColor = .systemBackground
        view.addSubview(tableView)
        view.addSubview(noNotificationView)
        noNotificationView.isHidden = true
        navigationItem.title = "Notifications"
        tableView.delegate = self
        tableView.dataSource = self
    }
}

extension NotificationViewController: UITableViewDelegate {
    
}

extension NotificationViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if let cell = tableView.dequeueReusableCell(withIdentifier: NotificationFollowEventTableViewCell.identifier, for: indexPath) as? NotificationFollowEventTableViewCell {
            return cell
        } else if let cell = tableView.dequeueReusableCell(withIdentifier: NotificationLikeTableViewCell.identifier, for: indexPath) as? NotificationLikeTableViewCell {
            return cell
        } else {
            return UITableViewCell()
        }
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 0
    }
    
}
  • 스피너(UIActivityIndicaterView)를 통해 뷰 로딩 중을 표시 가능

구현 화면

퍼블리셔 구독이 끝난 뒤 새로 생성하는 방법을 배웠다! Reference

profile
JUST DO IT
post-custom-banner

0개의 댓글