Build Instagram App: Part 3 (Swift 5) - 2022 - Xcode 11 - iOS Development
func transform(input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never> {
input
.receive(on: DispatchQueue.global(qos: .default))
.sink { [weak self] result in
switch result {
case .emailInput(text: let text):
self?.emailText.send(text)
print("Email: \(text)")
case .passwordInput(text: let text):
self?.passwordText.send(text)
print("Password: \(text)")
case .loginButtonDidTap:
self?.handleLogin()
case .createAccountButtonDidTap:
break
case .termsButtonDidTap:
self?.output.send(.showTermsView)
case .privacyButtonDidTap:
self?.output.send(.showPrivacyView)
}
}
.store(in: &cancellables)
return output.eraseToAnyPublisher()
}
private func bind() {
let output = viewModel.transform(input: input.eraseToAnyPublisher())
output
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
switch result {
case .showTermsView:
self?.showTermsView()
case .showPrivacyView:
self?.showPrivacyView()
case .isLoginDidSucceed(result: _): break
}
}
.store(in: &cancellables)
emailTextField
.textPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { [weak self] text in
self?.input.send(.emailInput(text: text))
}
.store(in: &cancellables)
emailTextField
.controlPublisher(for: .editingDidEndOnExit)
.sink { [weak self] _ in
self?.passwordTextField.becomeFirstResponder()
}
.store(in: &cancellables)
passwordTextField
.textPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { [weak self] text in
self?.input.send(.passwordInput(text: text))
}
.store(in: &cancellables)
passwordTextField
.controlPublisher(for: .editingDidEndOnExit)
.sink { [weak self] _ in
self?.passwordTextField.resignFirstResponder()
self?.loginButton.sendActions(for: .touchUpInside)
}
.store(in: &cancellables)
loginButton
.tapPublisher
.sink { [weak self] _ in
self?.input.send(.loginButtonDidTap)
}
.store(in: &cancellables)
termsButton
.tapPublisher
.sink { [weak self] _ in
self?.input.send(.termsButtonDidTap)
}
.store(in: &cancellables)
privacyButton
.tapPublisher
.sink { [weak self] _ in
self?.input.send(.privacyButtonDidTap)
}
.store(in: &cancellables)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(backgroundViewDidTap))
view.addGestureRecognizer(tapGesture)
}
termsButtonDidTap
등 버튼 이벤트에 대한 결과가 곧바로 sink
단에서 처리해도 상관없는 부분이 있지만 통일성을 위해 모두 뷰 모델에게 인풋을 보내는 방향으로 코드 작성import Foundation
import Combine
class LoginViewModel {
enum Input {
case emailInput(text: String)
case passwordInput(text: String)
case loginButtonDidTap
case createAccountButtonDidTap
case termsButtonDidTap
case privacyButtonDidTap
}
enum Output {
case isLoginDidSucceed(result: AnyPublisher<Bool, Never>)
case showTermsView
case showPrivacyView
}
private let emailText: PassthroughSubject<String, Never> = .init()
private let passwordText: PassthroughSubject<String, Never> = .init()
private let output: PassthroughSubject<Output, Never> = .init()
private var cancellables = Set<AnyCancellable>()
func transform(input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never> {
input
.receive(on: DispatchQueue.global(qos: .default))
.sink { [weak self] result in
switch result {
case .emailInput(text: let text):
self?.emailText.send(text)
print("Email: \(text)")
case .passwordInput(text: let text):
self?.passwordText.send(text)
print("Password: \(text)")
case .loginButtonDidTap:
self?.handleLogin()
case .createAccountButtonDidTap:
break
case .termsButtonDidTap:
self?.output.send(.showTermsView)
case .privacyButtonDidTap:
self?.output.send(.showPrivacyView)
}
}
.store(in: &cancellables)
return output.eraseToAnyPublisher()
}
private func handleLogin() {
// authentifiaction manager with current text input
output.send(.isLoginDidSucceed(result: Just(true).eraseToAnyPublisher()))
}
}
import UIKit
import Combine
import SafariServices
class LoginViewController: UIViewController {
struct Constants {
static let cornerRadius: CGFloat = 8.0
}
private let emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "UserName or Email"
textField.returnKeyType = .next
textField.leftViewMode = .always
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.layer.cornerRadius = Constants.cornerRadius
textField.layer.masksToBounds = true
textField.backgroundColor = .secondarySystemBackground
textField.layer.borderWidth = 1.0
textField.layer.borderColor = UIColor.secondaryLabel.cgColor
textField.tag = 1
return textField
}()
private let passwordTextField: UITextField = {
let textField = UITextField()
textField.textContentType = .newPassword
textField.placeholder = "Password"
textField.returnKeyType = .continue
textField.leftViewMode = .always
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.layer.cornerRadius = Constants.cornerRadius
textField.layer.masksToBounds = true
textField.backgroundColor = .secondarySystemBackground
textField.layer.borderWidth = 1.0
textField.layer.borderColor = UIColor.secondaryLabel.cgColor
textField.tag = 2
return textField
}()
private let loginButton: UIButton = {
let button = UIButton()
button.setTitle("Log In", for: .normal)
button.layer.masksToBounds = true
button.layer.cornerRadius = Constants.cornerRadius
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
return button
}()
private let headerView: UIView = {
let view = UIView()
let backgroundImageView = UIImageView(image: UIImage(named: "gradient"))
view.addSubview(backgroundImageView)
view.clipsToBounds = true
view.backgroundColor = .systemPurple
return view
}()
private let termsButton: UIButton = {
let button = UIButton()
button.setTitle("Terms of Service", for: .normal)
button.setTitleColor(.secondaryLabel, for: .normal)
return button
}()
private let privacyButton: UIButton = {
let button = UIButton()
button.setTitle("Privacy Policy", for: .normal)
button.setTitleColor(.secondaryLabel, for: .normal)
return button
}()
private let createAccountButton: UIButton = {
let button = UIButton()
button.setTitleColor(.label, for: .normal)
button.setTitle("Create Account", for: .normal)
return button
}()
private let input: PassthroughSubject<LoginViewModel.Input, Never> = .init()
private let viewModel = LoginViewModel()
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
setUI()
bind()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
headerView.frame = CGRect(x: 0, y: 0, width: view.width, height: view.height / 3.0)
emailTextField.frame = CGRect(x: 25, y: headerView.bottom + 40, width: view.width - 50, height: 52.0)
passwordTextField.frame = CGRect(x: 25, y: emailTextField.bottom + 10, width: view.width - 50, height: 52.0)
loginButton.frame = CGRect(x: 25, y: passwordTextField.bottom + 10, width: view.width - 50, height: 52.0)
createAccountButton.frame = CGRect(x: 25, y: loginButton.bottom + 10, width: view.width - 50, height: 52.0)
termsButton.frame = CGRect(x: 10, y: view.height - view.safeAreaInsets.bottom-100, width: view.width-20, height: 50)
privacyButton.frame = CGRect(x: 10, y: view.height - view.safeAreaInsets.bottom-50, width: view.width-20, height: 50)
configureHeaderView()
}
private func configureHeaderView() {
guard
headerView.subviews.count == 1,
let backgroundImageView = headerView.subviews.first else { return }
backgroundImageView.frame = headerView.bounds
let logoView = UIImageView(image: UIImage(named: "text"))
logoView.contentMode = .scaleAspectFit
headerView.addSubview(logoView)
logoView.frame = CGRect(x: headerView.width / 4.0, y: view.safeAreaInsets.top, width: headerView.width / 2, height: headerView.height - view.safeAreaInsets.top)
}
private func setUI() {
view.backgroundColor = .systemBackground
view.addSubview(emailTextField)
view.addSubview(passwordTextField)
view.addSubview(loginButton)
view.addSubview(headerView)
view.addSubview(termsButton)
view.addSubview(privacyButton)
view.addSubview(createAccountButton)
emailTextField.becomeFirstResponder()
}
private func showTermsView() {
guard let url = URL(string: "https://help.instagram.com/581066165581870") else { return }
let vc = SFSafariViewController(url: url)
present(vc, animated: true)
}
private func showPrivacyView() {
guard let url = URL(string: "https://help.instagram.com/155833707900388") else { return }
let vc = SFSafariViewController(url: url)
present(vc, animated: true)
}
private func bind() {
let output = viewModel.transform(input: input.eraseToAnyPublisher())
output
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
switch result {
case .showTermsView:
self?.showTermsView()
case .showPrivacyView:
self?.showPrivacyView()
case .isLoginDidSucceed(result: _): break
}
}
.store(in: &cancellables)
emailTextField
.textPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { [weak self] text in
self?.input.send(.emailInput(text: text))
}
.store(in: &cancellables)
emailTextField
.controlPublisher(for: .editingDidEndOnExit)
.sink { [weak self] _ in
self?.passwordTextField.becomeFirstResponder()
}
.store(in: &cancellables)
passwordTextField
.textPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { [weak self] text in
self?.input.send(.passwordInput(text: text))
}
.store(in: &cancellables)
passwordTextField
.controlPublisher(for: .editingDidEndOnExit)
.sink { [weak self] _ in
self?.passwordTextField.resignFirstResponder()
self?.loginButton.sendActions(for: .touchUpInside)
}
.store(in: &cancellables)
loginButton
.tapPublisher
.sink { [weak self] _ in
self?.input.send(.loginButtonDidTap)
}
.store(in: &cancellables)
termsButton
.tapPublisher
.sink { [weak self] _ in
self?.input.send(.termsButtonDidTap)
}
.store(in: &cancellables)
privacyButton
.tapPublisher
.sink { [weak self] _ in
self?.input.send(.privacyButtonDidTap)
}
.store(in: &cancellables)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(backgroundViewDidTap))
view.addGestureRecognizer(tapGesture)
}
@objc private func backgroundViewDidTap() {
emailTextField.resignFirstResponder()
passwordTextField.resignFirstResponder()
}
}
거의 델리게이트를 사용하지 않아도 될 것 같다.