본문 바로가기
iOS/UIKIt

[iOS/UIKit] UITextField와는 다른 UITextView 활용 방법과 차이점

by 개발하는 감자입니다 2024. 3. 6.
728x90

 

안녕하세요! 개발감자입니다 :)

이번 글에서는 iOS UIKit에서 제공하는 텍스트 필드(UITextField)와 텍스트 뷰(UITextView)의 활용 방법과 차이점에 대해 자세히 알아보겠습니다. AskViewController.swift 파일을 통해 각 컴포넌트의 구현 예제를 살펴봅니다.

 

문의하기의 문의 내용 입력 화면

 

1. UITextField와 UITextView의 차이점

UITextField사용자로부터 단일 라인의 텍스트 입력을 받는 데 사용됩니다.

주로 로그인 화면의 아이디와 비밀번호 입력, 검색어 입력 등 간단한 텍스트를 입력받을 때 활용됩니다. 텍스트 필드는 기본적으로 편집 가능하며, 키보드 입력을 통해 텍스트를 수정할 수 있습니다.

 

UITextView여러 줄의 텍스트를 입력하고 표시하기에 적합한 컴포넌트입니다.

이메일 본문, 긴 메시지 작성, 설명 텍스트 표시 등의 용도로 사용됩니다. UITextView는 UITextField와 달리 스크롤이 가능하여 긴 텍스트를 다룰 수 있으며, 텍스트 포맷팅(예: 굵은 글씨, 이탤릭)도 지원합니다.

2. 델리게이트의 활용

델리게이트 패턴을 활용함으로써 AskViewController는 사용자와의 상호작용에 따라 동적으로 인터페이스를 조정할 수 있습니다. 이메일 형식의 검증, 플레이스홀더의 동적 관리, 텍스트 입력 길이의 제한 등 사용자 경험을 향상시키는 다양한 기능을 구현하는 데 중요한 역할을 합니다. 델리게이트를 통해 구체적인 이벤트 처리 로직을 분리함으로써, 더욱 깔끔하고 관리하기 쉬운 코드를 작성할 수 있습니다.

2-1. UITextFieldDelegate

UITextFieldDelegate 프로토콜은 텍스트 필드와 관련된 사용자 액션을 처리하는 데 사용됩니다. 예를 들어, 사용자가 키보드에서 리턴 키를 누를 때, 텍스트 필드의 내용이 변경될 때, 텍스트 필드의 편집이 시작되거나 종료될 때 등입니다. AskViewController에서는 다음 델리게이트 메서드를 구현합니다:

  • textField(_:shouldChangeCharactersIn:replacementString:): 이 메서드는 텍스트 필드의 내용이 변경될 때 호출됩니다. 예제에서는 이 메서드를 사용하여 입력된 이메일 주소의 형식을 검증하고, 유효하지 않은 경우 사용자 인터페이스에 피드백을 제공합니다.

2-2. UITextViewDelegate

UITextViewDelegate 프로토콜은 텍스트 뷰와 관련된 사용자 액션을 처리하는 데 사용됩니다. 이는 사용자가 텍스트 뷰 내의 텍스트를 편집할 때 발생하는 다양한 이벤트를 감지하는 데 도움이 됩니다. AskViewController에서 구현한 델리게이트 메서드는 다음과 같습니다:

  • textViewDidBeginEditing(_:): 사용자가 텍스트 뷰의 편집을 시작할 때 호출됩니다. 이 예제에서는 이 메서드를 사용하여 플레이스홀더 텍스트를 제거하고, 사용자가 입력을 시작할 수 있도록 텍스트 뷰의 텍스트 색상을 변경합니다.
  • textViewDidEndEditing(_:): 텍스트 뷰의 편집이 종료될 때 호출됩니다. 사용자가 텍스트 뷰를 떠날 때, 텍스트 뷰가 비어 있으면 플레이스홀더 텍스트를 다시 표시합니다.
  • textView(_:shouldChangeTextIn:replacementText:): 텍스트 뷰의 내용이 변경될 때 호출됩니다. 이 메서드를 사용하여 입력된 텍스트의 길이를 제한하고, 사용자가 입력 가능한 최대 글자 수를 초과하지 않도록 관리합니다.

  

3. AskViewController에서의 활용 예

//
//  AskViewController.swift
//  Money-Planner
//
//  Created by p_kxn_g on 2/3/24.
//

import Foundation
import UIKit

//protocol ProfileViewDelegate : AnyObject{
//    func profileNameChanged(_ userName : String)
//    
//}
class AskViewController: UIViewController,UITextFieldDelegate,UITextViewDelegate,PopupViewDelegate {
    func popupChecked() {
        // 확인 누르면 문의하기 화면도 없어짐
        dismiss(animated: true)
    }
    
    private var UserName: String?
    //weak var delegate: ProfileViewDelegate?
    var completeCheck = compeleBtnCheck()
    var currTextSize : Int?

    private lazy var headerView = HeaderView(title: "문의하기")
    var currText : String = ""
    let picContainer : UIView = {
        let view = UIView()
        //view.backgroundColor = .red
        return view
    }()
    let picButton : UIButton = {
        let button = UIButton()
        button.layer.cornerRadius = 45
        button.layer.masksToBounds = true
        button.backgroundColor = .red
        let buttonImg = UIImage(systemName: "pencil")
        button.setImage(buttonImg, for: .normal)
        button.backgroundColor = .red
        return button
        
        
    }()

    
    let nameContainer : UIView = {
        let view = UIView()
        view.backgroundColor = UIColor.mpGypsumGray
        view.layer.cornerRadius = 8
        view.translatesAutoresizingMaskIntoConstraints = false // Add this line
        return view
    }()
    let errorContainer : UIView = {
        let view = UIView()
        //view.backgroundColor = .red
        return view
    }()
 
  
    private var completeButton = MainBottomBtn(title: "완료")
    private let emailTextField : UITextField = {
        let text = UITextField()
        text.placeholder = "example@email.com"
        let placeholderColor = UIColor.mpGray // Replace with your desired color

        let attributes: [NSAttributedString.Key: Any] = [
            .foregroundColor: placeholderColor,
        ]

        let attributedPlaceholder = NSAttributedString(string: "example@email.com", attributes: attributes)
        text.attributedPlaceholder = attributedPlaceholder        
        text.layer.cornerRadius = 8
        text.layer.masksToBounds = true
        text.borderStyle = .none
        text.font = UIFont.mpFont20M()
        text.tintColor = UIColor.mpMainColor
        text.backgroundColor = .mpGypsumGray
        text.keyboardType = .default
        // 여백 추가
        let leftView = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: text.frame.height)) // 조절하고자 하는 여백 크기
        text.leftView = leftView
        text.leftViewMode = .always
        // 활성화
        text.isEnabled = true

        return text
    }()
    private let contentsTextField : UITextView = {
        let text = UITextView()
        text.text = "어떤 내용이 궁금하신가요?"
        text.layer.cornerRadius = 8
        text.layer.masksToBounds = true
        text.font = UIFont.mpFont16M()
        text.tintColor = UIColor.mpMainColor
        text.textColor = .mpGray
        text.backgroundColor = .mpGypsumGray
        text.keyboardType = .default
        text.isMultipleTouchEnabled = true
        // 자간 설정
        // Set letter spacing directly on typingAttributes
        let letterSpacing: CGFloat = -0.02 // Adjust the value as needed
            text.typingAttributes[NSAttributedString.Key.kern] = letterSpacing


        // 여백 추가
        text.textContainerInset = UIEdgeInsets(top: 20, left: 18, bottom: 0, right: 20)

        // Set placeholder
        text.textColor = UIColor.lightGray


        
        return text
    }()
    private let emailLabel : MPLabel = {
        let label = MPLabel()
        label.font = .mpFont14B()
        label.text = "이메일"
        label.textColor = .mpGray
        return label
    }()
    private let emailLabel2 : MPLabel = {
        let label = MPLabel()
        label.font = .mpFont14M()
        label.text = "문의에 대한 답변을 이메일로 보내드려요"
        label.textColor = .mpDarkGray
        return label
    }()
    private let contentsLabel : MPLabel = {
        let label = MPLabel()
        label.font = .mpFont14B()
        label.text = "문의내용"
        label.textColor = .mpGray
        return label
    }()
    
    private let contentsSize : MPLabel = {
        let label = MPLabel()
        label.font = .mpFont14M()
        label.text = "0/250"
        label.textColor = .mpDarkGray
        return label
    }()
    private let contentsError : MPLabel = {
        let label = MPLabel()
        label.font = .mpFont14M()
        label.text = "최대 글자수는 250자입니다."
        label.textColor = .clear
        return label
    }()
    override func viewDidLoad() {
        setupUI()
    }
    private func setupUI() {
        // 배경색상 추가
        super.viewDidLoad()
        view.backgroundColor = UIColor(named: "mpWhite")
        view.backgroundColor = .systemBackground
        // 완료 버튼 활성화 확인
        print(completeCheck.contentsError)
        // 헤더
        setupHeader()
        
        // 완료 버튼 추가
        setupCompleteButton()
        
        setupEmailLabel()
        setupEmailTextField()
        setupContentsLabel()
        setupContentsTextField()
        
        //nameTextField.delegate = self // Make sure to set the delegate
        emailTextField.delegate = self
        contentsTextField.delegate = self
       
    }
    
    // 세팅 : 헤더
    private func setupHeader(){
        view.addSubview(headerView)
        headerView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            headerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            headerView.heightAnchor.constraint(equalToConstant: 60)
            
        ])
        
        headerView.addBackButtonTarget(target: self, action: #selector(previousScreen), for: .touchUpInside)
    }
    @objc private func previousScreen(){
        dismiss(animated: true)
    }
                                             
    private func setupEmailLabel(){
        view.addSubview(emailLabel)
        emailLabel.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            emailLabel.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 40),
            emailLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            emailLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
            
        ])
        

    }
    private func setupEmailTextField(){
        emailTextField.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(emailTextField)
        NSLayoutConstraint.activate([
            
            emailTextField.topAnchor.constraint(equalTo: emailLabel.bottomAnchor, constant: 10),
            emailTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            emailTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            emailTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
            emailTextField.heightAnchor.constraint(equalToConstant: 64)
        ])
        
        emailLabel2.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(emailLabel2)
        NSLayoutConstraint.activate([
            
            emailLabel2.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 10),
            emailLabel2.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            emailLabel2.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
        ])


    }
    private func setupContentsLabel(){
        //4
        //38
        // 높이 23
        view.addSubview(contentsLabel)
        contentsLabel.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            contentsLabel.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 77),
            contentsLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            contentsLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
        ])
    }
    private func setupContentsTextField(){
        view.addSubview(contentsTextField)
        contentsTextField.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            contentsTextField.topAnchor.constraint(equalTo: contentsLabel.bottomAnchor, constant: 10),
            contentsTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            contentsTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            contentsTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
            contentsTextField.heightAnchor.constraint(equalToConstant: 280)
        ])
        view.addSubview(contentsSize)
        view.addSubview(contentsError)
        contentsSize.translatesAutoresizingMaskIntoConstraints = false
        contentsError.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            contentsSize.topAnchor.constraint(equalTo: contentsTextField.bottomAnchor, constant: 8),
            contentsSize.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
            contentsError.topAnchor.constraint(equalTo: contentsTextField.bottomAnchor, constant: 8),
            contentsError.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
        ])

    }
    // 세팅 : 완료 버튼
    private func setupCompleteButton(){
        completeButton.isEnabled = false // 버튼 활성화
        view.addSubview(completeButton)
        completeButton.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            completeButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            completeButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
            completeButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            completeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            completeButton.heightAnchor.constraint(equalToConstant: 56)
        ])
        completeButton.addTarget(self, action: #selector(completeButtonTapped), for: .touchUpInside)

    }
    
    private func setTextViewPlaceholder() {
        if contentsTextField.text == "" {
            contentsTextField.text = "어떤 내용이 궁금하신가요?"
            contentsTextField.textColor = UIColor.lightGray
        } else if contentsTextField.text == "어떤 내용이 궁금하신가요?"{
            contentsTextField.text = ""
            contentsTextField.textColor = UIColor.black
        }
    }


    // MARK: - UITextViewDelegate

       func textViewDidBeginEditing(_ textView: UITextView) {
           if textView.textColor == UIColor.lightGray {
               textView.text = nil
               textView.textColor = UIColor.black
           }
       }

       func textViewDidEndEditing(_ textView: UITextView) {
           if textView.text.isEmpty {
               textView.text = "Placeholder Text"
               textView.textColor = UIColor.lightGray
               completeCheck.contentsWriten = false
               updateCompleteButtonState()
           }
           completeCheck.contentsWriten = true
           updateCompleteButtonState()
       }
    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        // 현재 텍스트뷰의 글자 수 계산
        guard let currentText = textView.text else { return false }
        let newText = (currentText as NSString).replacingCharacters(in: range, with: text)
        
        // 텍스트뷰가 비어있지 않으면 플레이스홀더 숨기기
        let textSize = newText.count
        
        
        // 글자 수가 10을 초과하면 입력을 허용하지 않음
        if newText.count > 250 {
            contentsError.textColor = .mpRed
            contentsTextField.layer.borderWidth = 1.0
            contentsTextField.layer.borderColor = UIColor.mpRed.cgColor
            completeCheck.contentsError = false
            updateCompleteButtonState()
            return false
        } else {
            contentsSize.text = "\(textSize)/250"
            contentsError.textColor = .clear
            contentsTextField.layer.borderColor = UIColor.clear.cgColor
            completeCheck.contentsError = true
            updateCompleteButtonState()
            return true
        }
    }


   
    // MARK: - UItextFieldDelegate

    
    func textField(_ textField: UITextField,shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if textField == emailTextField {
            // 현재 텍스트필드의 텍스트
            let currentText = textField.text ?? ""
            // 새로 입력되는 문자열
            let newText = (currentText as NSString).replacingCharacters(in: range, with: string)
            let textSize = newText.count
            if textSize > 0 {
                completeCheck.emailWriten = true
                updateCompleteButtonState()
            }
            else{
                completeCheck.emailWriten = false
                updateCompleteButtonState()
            }
            // 이메일 형식 검사
            if isValidEmail(email: newText) {
                // 올바른 이메일 형식
                emailTextField.layer.borderColor = UIColor.clear.cgColor
                emailLabel2.textColor = .mpDarkGray
                emailLabel2.text = "문의에 대한 답변을 이메일로 보내드려요."
                completeCheck.emailError = true
                updateCompleteButtonState()


            } else {
                // 잘못된 이메일 형식

                emailTextField.layer.borderColor = UIColor.mpRed.cgColor
                emailTextField.layer.borderWidth = 1.0
                emailLabel2.textColor = .mpRed
                emailLabel2.text = "이메일 형식이 올바르지 않습니다."
                completeCheck.emailError = false
                updateCompleteButtonState()
            }
        }

        return true
    }
    
    // 이메일 형식 검사 함수
        func isValidEmail(email: String) -> Bool {
            let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
            let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
            return emailPredicate.evaluate(with: email)
        }
    
    @objc
    private func completeButtonTapped(){
        print("문의하기가 완료되었습니다..")
      // api 연결
        let completeVC = PopupViewController() // 로그아웃 완료 팝업 띄우기
        completeVC.titleLabel.text = "문의하기가 완료되었습니다."
        completeVC.contentLabel.text = "1:1 문의에 대한 답변은 이메일로 보내드려요"
        completeVC.contentLabel.font = .mpFont16M()
        completeVC.delegate = self
        present(completeVC, animated: true)
        //dismiss(animated: true, completion: nil)
    }
    
    struct compeleBtnCheck {
        var contentsWriten : Bool = false
        var emailWriten : Bool = false
        var emailError : Bool = true
        var contentsError : Bool = true

        
    }
    // 완료 버튼 상태 갱신
        private func updateCompleteButtonState() {
            // email, contents 입력 여부 및 에러 상태에 따라 버튼 활성화/비활성화 결정
            let isButtonEnabled = completeCheck.emailWriten && completeCheck.contentsWriten && completeCheck.emailError && completeCheck.contentsError
            print(isButtonEnabled)
            print(completeCheck.emailWriten )
            print(completeCheck.emailError )
            print(completeCheck.contentsWriten )
            print(completeCheck.contentsError )

            completeButton.isEnabled = isButtonEnabled
        }
}

 

AskViewController에서는 사용자의 문의 사항을 받기 위해 이메일 주소 입력을 위한 UITextField와 문의 내용 입력을 위한 UITextView를 사용합니다.

  • 이메일 필드 (UITextField) 설정: 사용자가 자신의 이메일 주소를 입력할 수 있는 텍스트 필드입니다. 이메일 필드는 적절한 키보드 유형(.emailAddress)을 설정하여 이메일 입력에 최적화된 키보드를 제공하고, 입력된 이메일의 유효성을 검사하는 기능을 포함합니다.
  • 문의 내용 필드 (UITextView) 설정: 사용자가 자신의 문의 사항을 자유롭게 입력할 수 있는 텍스트 뷰입니다. 여러 줄의 텍스트 입력이 가능하며, 사용자가 입력한 내용에 따라 동적으로 크기가 조절됩니다. UITextView는 사용자에게 보다 풍부한 텍스트 입력 경험을 제공합니다.

4. 구현 포인트

  • 플레이스홀더: UITextField는 placeholder 속성을 통해 간단히 플레이스홀더를 설정할 수 있습니다. 반면, UITextView에는 내장된 플레이스홀더 속성이 없어, 사용자가 입력을 시작할 때 플레이스홀더 텍스트를 제거하고, 텍스트가 비어있을 때 다시 표시하는 방식으로 구현해야 합니다.
  • 입력 검증: 이메일 필드에서는 입력된 텍스트가 이메일 형식에 맞는지 검증하여 사용자에게 피드백을 줍니다. 이는 textField(_:shouldChangeCharactersIn:replacementString:) 델리게이트 메서드 내에서 구현됩니다.
  • 텍스트 길이 제한: 문의 내용을 입력하는 UITextView에서는 입력 가능한 최대 텍스트 길이를 제한하여, 초과 입력 시 사용자에게 시각적 피드백을 제공합니다.

5. 결론

UITextField와 UITextView는 UIKit에서 제공하는 기본 UI 컴포넌트로, 각각의 용도와 특성에 맞게 활용할 수 있습니다. 단순한 텍스트 입력은 UITextField로, 복잡하거나 여러 줄의 텍스트 입력이 필요한 경우 UITextView를 사용하는 것이 좋습니다. AskViewController 예제를 통해 차이점을 이해해보세요!

 
 
지금까지 개발감자였습니다!

 

 
728x90
반응형