Skip to content

Commit

Permalink
[#21] HandyTextField 생성
Browse files Browse the repository at this point in the history
  • Loading branch information
wjdalswl committed Jan 2, 2025
1 parent a80d94f commit fb0df99
Show file tree
Hide file tree
Showing 5 changed files with 553 additions and 0 deletions.
85 changes: 85 additions & 0 deletions Handy/Handy-Storybook/Atom/TextFieldViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//
// TextFieldViewController.swift
// Handy
//
// Created by 정민지 on 11/18/24.
//

import UIKit
import SnapKit

import Handy

final class TextFieldViewController: BaseViewController {

private let defaultField: HandyTextFieldView = {
let textField = HandyTextFieldView()
textField.placeholder = "Input text"
textField.fieldLabelText = "Label"
textField.helperLabelText = "Helper text"
return textField
}()

private let filledField: HandyTextFieldView = {
let textField = HandyTextFieldView()
textField.text = "Text Inputting"
textField.placeholder = "Input text"
textField.fieldLabelText = "Label"
textField.helperLabelText = "Helper text"
return textField
}()

private let errorField: HandyTextFieldView = {
let textField = HandyTextFieldView()
textField.placeholder = "Input text"
textField.fieldLabelText = "Label"
textField.helperLabelText = "Helper text"
textField.isNegative = true
return textField
}()

private let disabledField: HandyTextFieldView = {
let textField = HandyTextFieldView()
textField.placeholder = "Input text"
textField.fieldLabelText = "Label"
textField.helperLabelText = "Helper text"
textField.isDisabled = true
return textField
}()

override func viewDidLoad() {
super.viewDidLoad()
setViewLayouts()
}

override func setViewHierarchies() {
[
defaultField,
filledField,
errorField,
disabledField
].forEach {
view.addSubview($0)
}
}

override func setViewLayouts() {
defaultField.snp.makeConstraints {
$0.bottom.equalTo(filledField.snp.top).offset(-16)
$0.horizontalEdges.equalToSuperview().inset(20)
}
filledField.snp.makeConstraints {
$0.centerY.equalToSuperview().offset(-50)
$0.top.equalTo(defaultField.snp.bottom).offset(16)
$0.horizontalEdges.equalToSuperview().inset(20)
}
errorField.snp.makeConstraints {
$0.top.equalTo(filledField.snp.bottom).offset(16)
$0.horizontalEdges.equalToSuperview().inset(20)
}
disabledField.snp.makeConstraints {
$0.top.equalTo(errorField.snp.bottom).offset(16)
$0.horizontalEdges.equalToSuperview().inset(20)
}
}
}
24 changes: 24 additions & 0 deletions Handy/Handy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
02ED764C2C57BD09001569F1 /* HandyBoxButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ED764B2C57BD09001569F1 /* HandyBoxButtonViewController.swift */; };
2D41E8142C5A21930043161D /* FabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D41E8132C5A21930043161D /* FabViewController.swift */; };
2D41E8162C5A21B50043161D /* HandyFab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D41E8152C5A21B50043161D /* HandyFab.swift */; };
2D8811892D2642A900B0B517 /* HandyTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8811882D2642A800B0B517 /* HandyTextFieldView.swift */; };
2D88118B2D2642BD00B0B517 /* HandyTextFieldConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D88118A2D2642BC00B0B517 /* HandyTextFieldConstants.swift */; };
2D88118D2D2642CE00B0B517 /* HandyBaseTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D88118C2D2642CE00B0B517 /* HandyBaseTextField.swift */; };
2D88118F2D2642F900B0B517 /* TextFieldViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D88118E2D2642F900B0B517 /* TextFieldViewController.swift */; };
A56B3DE22C4E51D300C3610A /* HandyChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56B3DE12C4E51D300C3610A /* HandyChip.swift */; };
A5A12A7E2C57A6D900996916 /* ChipViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A12A7C2C57A6C200996916 /* ChipViewController.swift */; };
A5A12A7F2C57A92000996916 /* HandySematic.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D02AFC2C46C5A70056CE7B /* HandySematic.swift */; };
Expand Down Expand Up @@ -115,6 +119,10 @@
02ED764B2C57BD09001569F1 /* HandyBoxButtonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyBoxButtonViewController.swift; sourceTree = "<group>"; };
2D41E8132C5A21930043161D /* FabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FabViewController.swift; sourceTree = "<group>"; };
2D41E8152C5A21B50043161D /* HandyFab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyFab.swift; sourceTree = "<group>"; };
2D8811882D2642A800B0B517 /* HandyTextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyTextFieldView.swift; sourceTree = "<group>"; };
2D88118A2D2642BC00B0B517 /* HandyTextFieldConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyTextFieldConstants.swift; sourceTree = "<group>"; };
2D88118C2D2642CE00B0B517 /* HandyBaseTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyBaseTextField.swift; sourceTree = "<group>"; wrapsLines = 0; };
2D88118E2D2642F900B0B517 /* TextFieldViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldViewController.swift; sourceTree = "<group>"; };
A56B3DE12C4E51D300C3610A /* HandyChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyChip.swift; sourceTree = "<group>"; };
A5A12A7C2C57A6C200996916 /* ChipViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewController.swift; sourceTree = "<group>"; };
A5F6D36A2C96F32D00FB961F /* HandyDivider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyDivider.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -173,6 +181,7 @@
2D41E8132C5A21930043161D /* FabViewController.swift */,
02ED764B2C57BD09001569F1 /* HandyBoxButtonViewController.swift */,
A5A12A7C2C57A6C200996916 /* ChipViewController.swift */,
2D88118E2D2642F900B0B517 /* TextFieldViewController.swift */,
A5F6D36C2C97099C00FB961F /* DividerViewController.swift */,
E51FBF9A2C5399A00097B0DA /* CheckBoxViewController.swift */,
E51FBFA12C54CD350097B0DA /* RadioButtonViewController.swift */,
Expand Down Expand Up @@ -228,6 +237,7 @@
029E47FE2C49FD2E00D2F3B7 /* Atom */ = {
isa = PBXGroup;
children = (
2D8811872D26428500B0B517 /* HandyTextField */,
02ED762F2C52849A001569F1 /* HandyButton */,
029E47FC2C49FD1A00D2F3B7 /* HandyLabel.swift */,
2D41E8152C5A21B50043161D /* HandyFab.swift */,
Expand Down Expand Up @@ -313,6 +323,16 @@
path = Extension;
sourceTree = "<group>";
};
2D8811872D26428500B0B517 /* HandyTextField */ = {
isa = PBXGroup;
children = (
2D8811882D2642A800B0B517 /* HandyTextFieldView.swift */,
2D88118C2D2642CE00B0B517 /* HandyBaseTextField.swift */,
2D88118A2D2642BC00B0B517 /* HandyTextFieldConstants.swift */,
);
path = HandyTextField;
sourceTree = "<group>";
};
E5650D412C4D30B9002790CC /* Asset */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -455,6 +475,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2D88118F2D2642F900B0B517 /* TextFieldViewController.swift in Sources */,
02150E4C2CCABAEB00EE690E /* SnackbarViewController.swift in Sources */,
2D41E8142C5A21930043161D /* FabViewController.swift in Sources */,
A5A12A812C57A93C00996916 /* HandyPrimitive.swift in Sources */,
Expand Down Expand Up @@ -486,6 +507,7 @@
E5D02AFD2C46C5A70056CE7B /* HandySematic.swift in Sources */,
E5D02B002C480A180056CE7B /* HandyPrimitive.swift in Sources */,
E51FBFA02C54CB260097B0DA /* HandyRadioButton.swift in Sources */,
2D88118B2D2642BD00B0B517 /* HandyTextFieldConstants.swift in Sources */,
E5669A3F2C443E7300DABC21 /* HandyBasicColor.swift in Sources */,
02ED76312C5284BB001569F1 /* HandyButtonProtocol.swift in Sources */,
02ED76352C5284F3001569F1 /* HandyTextButton.swift in Sources */,
Expand All @@ -494,10 +516,12 @@
02ED764A2C5779C3001569F1 /* UIImage+.swift in Sources */,
029E48002C49FD4000D2F3B7 /* HandyTypography.swift in Sources */,
E5650D432C4D326D002790CC /* HandyCheckBox.swift in Sources */,
2D88118D2D2642CE00B0B517 /* HandyBaseTextField.swift in Sources */,
029E47FD2C49FD1A00D2F3B7 /* HandyLabel.swift in Sources */,
A56B3DE22C4E51D300C3610A /* HandyChip.swift in Sources */,
E5650D472C512B07002790CC /* HandyIcon.swift in Sources */,
E5650D472C512B07002790CC /* HandyIcon.swift in Sources */,
2D8811892D2642A900B0B517 /* HandyTextFieldView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
226 changes: 226 additions & 0 deletions Handy/Handy/Source/Atom/HandyTextField/HandyBaseTextField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
//
// HandyBaseTextField.swift
// Handy
//
// Created by 정민지 on 11/18/24.
//

import UIKit

public class HandyBaseTextField: UITextField {

// MARK: - 외부에서 지정할 수 있는 속성

/**
텍스트 필드를 비활성화 시킬 때 사용합니다.
*/
@Invalidating(.layout) public var isDisabled: Bool = false {
didSet {
updateState()
}
}

/**
텍스트 필드의 오류 상태를 나타낼 때 사용합니다.
*/
@Invalidating(.layout) public var isNegative: Bool = false {
didSet {
updateState()
}
}

// MARK: - 내부에서 사용되는 뷰

/**
텍스트 필드 내의 입력을 초기화할 때 사용하는 Clear 버튼입니다.
*/
private let clearButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(HandyIcon.cancelFilled, for: .normal)
button.tintColor = HandySemantic.iconBasicTertiary
button.isHidden = true
return button
}()

// MARK: - 초기화

/**
초기화 메소드입니다. 기본적인 텍스트 필드 속성과 Clear 버튼을 설정합니다.
*/
public init() {
super.init(frame: .zero)
setupTextField()
updatePlaceholderColorAndFont()
setupClearButton()
self.delegate = self
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - 설정

/**
텍스트 필드의 기본 속성을 설정합니다.
- 테두리 색상, 패딩, 기본 배경색 등을 포함합니다.
*/
private func setupTextField() {
self.tintColor = HandySemantic.lineStatusPositive
self.layer.cornerRadius = HandySemantic.radiusM
self.layer.borderWidth = 1
self.layer.borderColor = HandySemantic.bgBasicLight.cgColor
self.backgroundColor = HandySemantic.bgBasicLight
self.clipsToBounds = true
self.font = HandyFont.B1Rg16

let leftPaddingView = UIView(frame: CGRect(x: 0, y: 0, width: HandyTextFieldConstants.Dimension.leftMargin, height: 0))
self.leftView = leftPaddingView
self.leftViewMode = .always

let rightPaddingView = UIView(frame: CGRect(x: 0, y: 0, width: HandyTextFieldConstants.Dimension.rightMargin, height: 0))
self.rightView = rightPaddingView
self.rightViewMode = .always

self.snp.makeConstraints {
$0.height.greaterThanOrEqualTo(HandyTextFieldConstants.Dimension.textFieldHeight)
}
}

/**
플레이스홀더의 색상과 폰트를 업데이트합니다.
- 기본적으로 `HandyFont.B1Rg16`를 사용하며, `color` 매개변수를 통해 색상을 지정할 수 있습니다.
*/
private func updatePlaceholderColorAndFont(color: UIColor = HandySemantic.textBasicTertiary) {
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: color,
.font: HandyFont.B1Rg16
]

if let placeholder = self.placeholder {
self.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: attributes)
}
}

/**
Clear 버튼을 설정합니다.
- Clear 버튼은 텍스트 필드 오른쪽에 위치하며, 텍스트 입력 상태에 따라 표시됩니다.
*/
private func setupClearButton() {
addSubview(clearButton)
clearButton.addTarget(self, action: #selector(clearText), for: .touchUpInside)

clearButton.snp.makeConstraints {
$0.centerY.equalToSuperview()
$0.trailing.equalToSuperview().inset(HandyTextFieldConstants.Dimension.clearButtonDefaultRightMargin)
$0.width.height.equalTo(HandyTextFieldConstants.Dimension.clearButtonSize)
}

addTarget(self, action: #selector(textDidChange), for: .editingChanged)
}

// MARK: - 상태 관리

/**
텍스트 필드의 상태에 따라 UI를 업데이트합니다.
- `isDisabled`: 비활성화 상태를 나타냅니다.
- `isNegative`: 오류 상태를 나타냅니다.
*/
private func updateState() {
if isDisabled {
self.isUserInteractionEnabled = false
self.backgroundColor = HandySemantic.bgBasicLight
self.layer.borderColor = HandySemantic.bgBasicLight.cgColor
self.textColor = HandySemantic.textBasicDisabled
updatePlaceholderColorAndFont(color: HandySemantic.textBasicDisabled)
clearButton.isHidden = true
return
}

if isNegative {
self.isUserInteractionEnabled = true
self.layer.borderColor = HandySemantic.lineStatusNegative.cgColor
self.textColor = HandySemantic.textBasicSecondary
updatePlaceholderColorAndFont(color: HandySemantic.textBasicTertiary)
clearButton.isHidden = false
return
}

self.isUserInteractionEnabled = true
self.layer.borderColor = HandySemantic.bgBasicLight.cgColor
self.textColor = HandySemantic.textBasicPrimary
updatePlaceholderColorAndFont(color: HandySemantic.textBasicTertiary)
clearButton.isHidden = self.text?.isEmpty ?? true
}

// MARK: - Clear 버튼 동작

/**
텍스트 필드의 텍스트를 초기화합니다.
- Clear 버튼이 눌렸을 때 호출됩니다.
*/
@objc private func clearText() {
self.text = ""
clearButton.isHidden = true
}

/**
텍스트 필드의 텍스트 변경 시 호출됩니다.
- 텍스트가 입력되거나 삭제될 때 Clear 버튼의 표시 상태를 업데이트합니다.
*/
@objc private func textDidChange() {
clearButton.isHidden = self.text?.isEmpty ?? true
}

// MARK: - Overridden Methods

/**
Placeholder 및 텍스트 레이아웃을 설정합니다.
*/
public override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: UIEdgeInsets(
top: 0,
left: HandyTextFieldConstants.Dimension.leftMargin,
bottom: 0,
right: HandyTextFieldConstants.Dimension.rightMargin
))
}

/**
텍스트 입력 시 레이아웃을 설정합니다.
*/
public override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: UIEdgeInsets(
top: 0,
left: HandyTextFieldConstants.Dimension.leftMargin,
bottom: 0,
right: HandyTextFieldConstants.Dimension.rightMargin
))
}
}

// MARK: - UITextFieldDelegate

extension HandyBaseTextField: UITextFieldDelegate {
/**
텍스트 필드가 편집을 시작할 때 호출됩니다.
- isNegative 상태가 아닐 경우, 테두리 색상을 긍정 상태 색상으로 변경합니다.
- 편집 중일 때 시각적 피드백을 제공합니다.
- 호출 시점: 사용자가 텍스트 필드에 포커스를 줄 때.
*/
public func textFieldDidBeginEditing(_ textField: UITextField) {
if !isNegative {
self.layer.borderColor = HandySemantic.lineStatusPositive.cgColor
}
}

/**
텍스트 필드의 편집이 종료될 때 호출됩니다.
- 상태를 다시 업데이트하여 현재 상태에 맞는 UI를 반영합니다.
- 호출 시점: 사용자가 텍스트 필드의 포커스를 해제할 때.
*/
public func textFieldDidEndEditing(_ textField: UITextField) {
updateState()
}
}

Loading

0 comments on commit fb0df99

Please sign in to comment.