목차

목차

iOS 로그인 UI (Code UI 종합 예제)

목차

프로퍼티 생성 3가지 방식, UI 설정(기본, NSLayout), .addTarget 사용법(objc, 순수 Swift)이 모두 포함된 로그인 화면 예제.

//
//  ViewController.swift
//  LoginWithCodeUI
//
//  Created by SangHyuk Moon on 2/13/25.
//

import UIKit

final class ViewController: UIViewController {

    // MARK: - Properties
    //클로저 사용
    private lazy var emailTextFieldView: UIView = {
        let view = UIView()
        view.backgroundColor = #colorLiteral(red: 0.3333333433, green: 0.3333333433, blue: 0.3333333433, alpha: 1)
        view.layer.cornerRadius = 5
        view.clipsToBounds = true
        view.addSubview(emailTextField)
        view.addSubview(emailInfoLabel)
        return view
    }()
    private var emailInfoLabel: UILabel = {
        let label = UILabel()
        label.text = "이메일 주소 또는 전화번호"
        label.textColor = .white
        label.font = .systemFont(ofSize: 18)
        return label
    }()
    private var emailTextField: UITextField = {
        let textField = UITextField()
        textField.autocapitalizationType = .none
        textField.autocorrectionType = .no
        textField.keyboardType = .emailAddress
        textField.textColor = .white
        textField.tintColor = .white
        textField.backgroundColor = .clear
        textField.frame.size.height = 48
        textField.addTarget(self, action: #selector(textFieldEditingChanged(_:)), for: .editingChanged)
        return textField
    }()

    //메서드 사용
    private lazy var passwordTextFieldView = setupPasswordTextFieldView()
    private lazy var passwordInfoLabel = setupPasswordInfoLabel()
    private lazy var passwordTextField = setupPassswordTextField()
    private lazy var passwordToggleButton = setupPasswordToggleButton()


    //creationUI()에 설정하여 사용
    private var loginButton = UIButton()

    private  var passwordResetButton = UIButton()

    private lazy var stackView = UIStackView(arrangedSubviews: [
        emailTextFieldView,
        passwordTextFieldView,
        loginButton
    ])

    private let textViewHeight: CGFloat = 48

    //Label center Y constraint
    private lazy var emailInfoLabelCenterYConstraint: NSLayoutConstraint = {
        return emailInfoLabel
            .centerYAnchor
            .constraint(equalTo: emailTextFieldView.centerYAnchor)
    }()
    private lazy var passwordInfoLabelCenterYConstraint: NSLayoutConstraint = {
        return passwordInfoLabel
            .centerYAnchor
            .constraint(equalTo: passwordTextFieldView.centerYAnchor)
    }()

    // MARK: - Lifecycle Methods
    override func viewDidLoad() {
        super.viewDidLoad()
        createUI()
        setupUI()
        setupDelegate()
    }

    // MARK: - UI Setup
    private func setupUI() {
        view.backgroundColor = .black
        view.addSubview(stackView)
        view.addSubview(passwordResetButton)

        emailInfoLabel.translatesAutoresizingMaskIntoConstraints = false
        emailTextField.translatesAutoresizingMaskIntoConstraints = false
        passwordInfoLabel.translatesAutoresizingMaskIntoConstraints = false
        passwordTextField.translatesAutoresizingMaskIntoConstraints = false
        passwordToggleButton.translatesAutoresizingMaskIntoConstraints = false
        stackView.translatesAutoresizingMaskIntoConstraints = false
        passwordResetButton.translatesAutoresizingMaskIntoConstraints = false

        //Default
        emailInfoLabel
            .leadingAnchor
            .constraint(equalTo: emailTextFieldView.leadingAnchor, constant: 8)
            .isActive = true
        emailInfoLabel
            .trailingAnchor
            .constraint(equalTo: emailTextFieldView.trailingAnchor, constant: -8)
            .isActive = true
        emailInfoLabelCenterYConstraint.isActive = true

        //Use NSLayout
        NSLayoutConstraint.activate([
            emailTextField
                .leadingAnchor
                .constraint(equalTo: emailTextFieldView.leadingAnchor, constant: 8),
            emailTextField
                .trailingAnchor
                .constraint(equalTo: emailTextFieldView.trailingAnchor, constant: -8),
            emailTextField
                .topAnchor
                .constraint(equalTo: emailTextField.topAnchor, constant: 15),
            emailTextField
                .bottomAnchor
                .constraint(equalTo: emailTextFieldView.bottomAnchor, constant: -2),

            passwordInfoLabel
                .leadingAnchor
                .constraint(equalTo: passwordTextFieldView.leadingAnchor, constant: 8),
            passwordInfoLabel
                .trailingAnchor
                .constraint(equalTo: passwordTextFieldView.trailingAnchor, constant: -8),
            passwordInfoLabelCenterYConstraint,

            passwordTextField
                .leadingAnchor
                .constraint(equalTo: passwordTextFieldView.leadingAnchor, constant: 8),
            passwordTextField
                .trailingAnchor
                .constraint(equalTo: passwordTextFieldView.trailingAnchor, constant: -8),
            passwordTextField
                .topAnchor
                .constraint(equalTo: passwordTextField.topAnchor, constant: 15),
            passwordTextField
                .bottomAnchor
                .constraint(equalTo: passwordTextFieldView.bottomAnchor, constant: -2),

            passwordToggleButton
                .trailingAnchor
                .constraint(equalTo: passwordTextFieldView.trailingAnchor, constant: -8),
            passwordToggleButton
                .topAnchor
                .constraint(equalTo: passwordTextFieldView.topAnchor, constant: 15),
            passwordToggleButton
                .bottomAnchor
                .constraint(equalTo: passwordTextFieldView.bottomAnchor, constant: -15),

            stackView
                .centerXAnchor
                .constraint(equalTo: view.centerXAnchor),
            stackView
                .centerYAnchor
                .constraint(equalTo: view.centerYAnchor),
            stackView
                .leadingAnchor
                .constraint(equalTo: view.leadingAnchor, constant: 30),
            stackView
                .trailingAnchor
                .constraint(equalTo: view.trailingAnchor, constant: -30),
            stackView
                .heightAnchor
                .constraint(equalToConstant: textViewHeight * 3 + 36),

            passwordResetButton
                .leadingAnchor
                .constraint(equalTo: view.leadingAnchor, constant: 30),
            passwordResetButton
                .trailingAnchor
                .constraint(equalTo: view.trailingAnchor, constant: -30),
            passwordResetButton
                .topAnchor
                .constraint(equalTo: stackView.bottomAnchor, constant: 10),
            passwordResetButton
                .heightAnchor
                .constraint(equalToConstant: textViewHeight),
        ])

    }

    // MARK: - setupDelegate
    private func setupDelegate() {
        emailTextField.delegate = self
        passwordTextField.delegate = self
    }

    // MARK: - UI Creation Methods
    private func createUI() {
        setupLoginButton()
        setupPasswordResetButton()
        setupStackView()
    }


    private func setupPasswordTextFieldView() -> UIView {
        let view = UIView()
        view.backgroundColor = #colorLiteral(red: 0.3333333433, green: 0.3333333433, blue: 0.3333333433, alpha: 1)
        view.layer.cornerRadius = 5
        view.clipsToBounds = true
        view.addSubview(passwordTextField)
        view.addSubview(passwordInfoLabel)
        view.addSubview(passwordToggleButton)
        return view
    }
    private func setupPasswordInfoLabel() -> UILabel{
        let label = UILabel()
        label.text = "비말번호"
        label.textColor = .white
        label.font = .systemFont(ofSize: 18)
        return label
    }
    private func setupPassswordTextField() -> UITextField {
        let textField = UITextField()
        textField.autocapitalizationType = .none
        textField.autocorrectionType = .no
        textField.spellCheckingType = .no
        textField.keyboardType = .default
        textField.isSecureTextEntry = true
        textField.clearsOnBeginEditing = true
        textField.textColor = .white
        textField.tintColor = .white
        textField.backgroundColor = .clear
        textField.frame.size.height = textViewHeight
        textField.addTarget(self, action: #selector(textFieldEditingChanged(_:)), for: .editingChanged)
        return textField
    }
    private func setupPasswordToggleButton() -> UIButton {
        let button = UIButton(type: .system)
        button.setTitle("표시", for: .normal)
        button.setTitleColor(#colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1), for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 14, weight: .light)
        button.addTarget(self, action: #selector(passwordToggleModeSetting), for: .touchUpInside)
        return button
    }
    private func setupLoginButton() {
        loginButton.setTitle( "로그인", for: .normal)
        loginButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
        loginButton.layer.cornerRadius = 5
        loginButton.clipsToBounds = true
        loginButton.layer.borderWidth = 1
        loginButton.layer.borderColor = #colorLiteral(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
        loginButton.isEnabled = false
        loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)
    }
    private func setupPasswordResetButton() {
        passwordResetButton.setTitle( "비밀번호 재설정", for: .normal)
        passwordResetButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .light)
        passwordResetButton.backgroundColor = .clear
        //ojbc -> pure Swift
        passwordResetButton.addAction(UIAction { [weak self] _ in
            guard let self else { return }
            self.resetButtonTapped()
        }, for: .touchUpInside)
    }
    private func setupStackView() {
        stackView.axis = .vertical
        stackView.spacing = 18
        stackView.distribution = .fillEqually
        stackView.alignment = .fill
    }
    // MARK: - Action Methods

    @objc func passwordToggleModeSetting() {
        passwordTextField.isSecureTextEntry.toggle()
    }

    @objc func loginButtonTapped() {
        view.endEditing(true)
        print("success login")
    }

    //ojbc -> pure Swift
    private func resetButtonTapped() {
        let alert = UIAlertController(
            title: "비밀번호 바꾸기",
            message: "비밀번호를 바꾸시겠습니까",
            preferredStyle: .alert
        )

        let success = UIAlertAction(title: "확인", style: .default) { _ in
            print("확인")
        }
        let cancel = UIAlertAction(title: "취소", style: .cancel) { _ in
            print("취소")
        }

        alert.addAction(success)
        alert.addAction(cancel)

        present(alert, animated: true)
    }

    //Dismiss keyboard when tapping outside text fields
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        view.endEditing(true)
    }
}

// MARK: - Extension UITextFieldDelegate
extension ViewController: UITextFieldDelegate {

    func textFieldDidBeginEditing(_ textField: UITextField) {

        if textField == emailTextField {
            emailTextFieldView.backgroundColor = #colorLiteral(red: 0.2972877622, green: 0.2973434925, blue: 0.297280401, alpha: 1)
            emailInfoLabel.font = UIFont.systemFont(ofSize: 11)
            emailInfoLabelCenterYConstraint.constant = -13
        }

        if textField == passwordTextField {
            passwordTextFieldView.backgroundColor = #colorLiteral(red: 0.2972877622, green: 0.2973434925, blue: 0.297280401, alpha: 1)
            passwordInfoLabel.font = UIFont.systemFont(ofSize: 11)
            passwordInfoLabelCenterYConstraint.constant = -13
        }

        animateLabel()
    }

    func textFieldDidEndEditing(_ textField: UITextField) {

        if textField == emailTextField {
            emailTextFieldView.backgroundColor = #colorLiteral(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
            if emailTextField.text == "" {
                emailInfoLabel.font = UIFont.systemFont(ofSize: 18)
                emailInfoLabelCenterYConstraint.constant = 0
            }
        }
        if textField == passwordTextField {
            passwordTextFieldView.backgroundColor = #colorLiteral(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
            if passwordTextField.text == "" {
                passwordInfoLabel.font = UIFont.systemFont(ofSize: 18)
                passwordInfoLabelCenterYConstraint.constant = 0
            }
        }
        animateLabel()
    }

    @objc func textFieldEditingChanged(_ textField: UITextField) {
        if textField.text?.count == 1 {
            if textField.text?.first == " " {
                textField.text = ""
                return
            }
        }
        guard
            let email = emailTextField.text, !email.isEmpty,
            let password = passwordTextField.text, !password.isEmpty else{
            loginButton.isEnabled = false
            loginButton.backgroundColor = .clear
            return
        }
        loginButton.backgroundColor = .red
        loginButton.isEnabled = true
    }

    func animateLabel() {
         UIView.animate(withDuration: 0.3) {
            self.stackView.layoutIfNeeded()
        }
    }
}