[UIKit] InstagramClone: EditProfileView

Junyoung Park·2022년 11월 7일
0

UIKit

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

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

InstagramClone: EditProfileView

구현 목표

  • 프로필 편집 뷰 구현

구현 태스크

  • 커스텀 셀 구현
  • 셀 별 서로 다른 메소드 동작 연결

핵심 코드

private func configureModels() {
        let section1Labels = ["Name", "UserName", "Bio"]
        var section1 = [EditProfileFormModel]()
        for label in section1Labels {
            let model = EditProfileFormModel(label: label, placeholder: "Enter \(label)...", value: nil)
            section1.append(model)
        }
        
        let section2Labels = ["Email", "UserName", "Bio"]
        var section2 = [EditProfileFormModel]()
        for label in section2Labels {
            let model = EditProfileFormModel(label: label, placeholder: "Enter \(label)...", value: nil)
            section2.append(model)
        }
        
        editProfileModel.send([section1, section2])
    }
  • 프로필 편집 뷰 모델의 퍼블리셔 초기화
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: FormTableViewCell.identifier, for: indexPath) as? FormTableViewCell else { return UITableViewCell() }
        let model = viewModel.editProfileModel.value[indexPath.section][indexPath.row]
        cell.configure(with: model)
        return cell
    }
  • 해당 모델을 데이터 소스로 사용하는 테이블 뷰
func configure(with model: EditProfileFormModel) {
        formLabel.text = model.label
        textField.placeholder = model.placeholder
        textField.text = model.value
    }
  • 해당 셀에서 해당 모델을 통해 텍스트 등 UI를 그리는 코드

소스 코드

import Foundation

struct EditProfileFormModel {
    let label: String
    let placeholder: String
    var value: String?
}
  • 수정할 텍스트 필드의 종류 및 텍스트를 기록하는 모델
mport Foundation
import Combine

class EditProfileViewModel {
    let editProfileModel: CurrentValueSubject<[[EditProfileFormModel]], Never> = .init([])
    
    init() {
        configureModels()
    }
    
    private func configureModels() {
        let section1Labels = ["Name", "UserName", "Bio"]
        var section1 = [EditProfileFormModel]()
        for label in section1Labels {
            let model = EditProfileFormModel(label: label, placeholder: "Enter \(label)...", value: nil)
            section1.append(model)
        }
        
        let section2Labels = ["Email", "UserName", "Bio"]
        var section2 = [EditProfileFormModel]()
        for label in section2Labels {
            let model = EditProfileFormModel(label: label, placeholder: "Enter \(label)...", value: nil)
            section2.append(model)
        }
        
        editProfileModel.send([section1, section2])
    }
}
  • 섹션 및 해당 로우의 셀 내용을 퍼블리셔에게 보내는 뷰 모델
mport UIKit
import Combine

class EditProfileViewController: UIViewController {
    private let tableView: UITableView = {
        let tableView = UITableView(frame: .zero, style: .grouped)
        tableView.register(FormTableViewCell.self, forCellReuseIdentifier: FormTableViewCell.identifier)
        return tableView
    }()
    private var cancellables = Set<AnyCancellable>()
    private let viewModel = EditProfileViewModel()

    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.tableHeaderView = createTableHeaderView()
        tableView.delegate = self
        tableView.dataSource = self
        setNavigationBar()
    }
    
    private func createTableHeaderView() -> UIView {
        let headerView = UIView(frame: CGRect(x: 0, y: 0, width: view.width, height: view.height / 3).integral)
        let size = headerView.height / 1.5
        let profilePhotoButton = UIButton(frame: CGRect(x: (view.width-size) / 2, y: (headerView.height-size) / 2, width: size, height: size))
        headerView.addSubview(profilePhotoButton)
        profilePhotoButton.layer.masksToBounds = true
        profilePhotoButton.layer.cornerRadius = size / 2.0
        profilePhotoButton
            .tapPublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.profilePhotoButtonDidTap()
            }
            .store(in: &cancellables)
        profilePhotoButton.setBackgroundImage(UIImage(systemName: "person.circle")?.withTintColor(.label, renderingMode: .alwaysOriginal), for: .normal)
        profilePhotoButton.layer.borderWidth = 1.0
        profilePhotoButton.layer.borderColor = UIColor.secondarySystemBackground.cgColor
        return headerView
    }
    
    private func setNavigationBar() {
        title = "Edit Profile"
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .done, target: self, action: #selector(saveButtonDidTap))
        navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelButtonDidTap))
    }
    
    private func profilePhotoButtonDidTap() {
        
    }
    
    @objc private func saveButtonDidTap() {
        // save data into db
        dismiss(animated: true)
    }
    
    @objc private func cancelButtonDidTap() {
        dismiss(animated: true)
    }
    
    private func changeProfilePicture() {
        let actionSheet = UIAlertController(title: "Profile Picture", message: "Change profile picture", preferredStyle: .actionSheet)
        actionSheet.addAction(UIAlertAction(title: "Take Photo", style: .default, handler: { _ in
            
        }))
        actionSheet.addAction(UIAlertAction(title: "Choose from Library", style: .default, handler: { _ in
            
        }))
        actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        actionSheet.popoverPresentationController?.sourceView = view
        actionSheet.popoverPresentationController?.sourceRect = view.bounds
        present(actionSheet, animated: true)
    }
}

extension EditProfileViewController: UITableViewDelegate {
}

extension EditProfileViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        guard section == 1 else { return nil }
        return "Private Information"
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return viewModel.editProfileModel.value.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: FormTableViewCell.identifier, for: indexPath) as? FormTableViewCell else { return UITableViewCell() }
        let model = viewModel.editProfileModel.value[indexPath.section][indexPath.row]
        cell.configure(with: model)
        return cell
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.editProfileModel.value[section].count
    }
}
  • 뷰 모델의 데이터 퍼블리셔를 데이터 소스화한 뷰 컨트롤러의 테이블 뷰
  • 커스텀 뷰를 통해 해당 데이터를 파라미터로 받는 configure 커스텀 함수 사용
  • 추후 셀 내 UI 인터렉션을 뷰 모델로 연결 예정
import UIKit
import Combine

class FormTableViewCell: UITableViewCell {
    static let identifier = "FormTableViewCell"
    private let formLabel: UILabel = {
        let label = UILabel()
        label.textColor = .label
        label.numberOfLines = 1
        return label
    }()
    
    private let textField: UITextField = {
        let textField = UITextField()
        textField.returnKeyType = .done
        return textField
    }()
    private var cancellables = Set<AnyCancellable>()
    
    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()
        formLabel.frame = CGRect(x: 5, y: 0, width: contentView.width / 2, height: contentView.height)
        textField.frame = CGRect(x: formLabel.right + 5, y: 0, width: contentView.width - 10 - formLabel.width, height: contentView.height)
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        formLabel.text = nil
        textField.placeholder = nil
        textField.text = nil
    }
    
    private func setUI() {
        contentView.addSubview(formLabel)
        contentView.addSubview(textField)
        clipsToBounds = true
        selectionStyle = .none
        textField
            .controlPublisher(for: .editingDidEndOnExit)
            .sink { [weak self] _ in
                self?.textField.resignFirstResponder()
            }
            .store(in: &cancellables)
    }
    
    func configure(with model: EditProfileFormModel) {
        formLabel.text = model.label
        textField.placeholder = model.placeholder
        textField.text = model.value
    }
}
  • 텍스트 필드의 내용 및 현재 종류를 뷰 모델로 연결하기 위한 커스텀 함수를 만들 예정

구현 화면

profile
JUST DO IT
post-custom-banner

0개의 댓글