안녕하세요! 개발감자입니다 :)
이번 글에서는 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 예제를 통해 차이점을 이해해보세요!
지금까지 개발감자였습니다!
'iOS > UIKIt' 카테고리의 다른 글
UIViewController와 관련 개념 정리: HIG, LifeCycle, ContainViewController, AppDelegate, MVC (0) | 2024.10.06 |
---|---|
[iOS/UIKit] UITextField 이메일 형식 올바른지 확인하기 (0) | 2024.03.08 |
[ iOS/UIkit ] UITableView로 마이페이지 구현하기 (0) | 2024.03.06 |
iOS 앱 개발: 탭바에 동그란 커스텀 버튼 추가하기 - 완벽 가이드 (1) | 2024.02.09 |
[iOS] MVVM 모델로 RxSwift API 연결하기 (0) | 2024.02.08 |