diff --git a/Handy/Handy-Storybook/Atom/HandySearchBarViewController.swift b/Handy/Handy-Storybook/Atom/HandySearchBarViewController.swift new file mode 100644 index 0000000..b202e7f --- /dev/null +++ b/Handy/Handy-Storybook/Atom/HandySearchBarViewController.swift @@ -0,0 +1,87 @@ +// +// HandySearchBarViewController.swift +// Handy-Storybook +// +// Created by 성현주 on 1/1/25. +// + +import Handy +import UIKit + +final class HandySearchBarViewController: BaseViewController { + + private let searchBar1: HandySearchBar = { + let uiSearchBar = HandySearchBar() + return uiSearchBar + }() + private let searchBar2: HandySearchBar = { + let uiSearchBar = HandySearchBar() + uiSearchBar.isRightButtonHidden = true + return uiSearchBar + }() + private let searchBar3: HandySearchBar = { + let uiSearchBar = HandySearchBar() + uiSearchBar.isLeftButtonHidden = true + return uiSearchBar + }() + private let searchBar4: HandySearchBar = { + let uiSearchBar = HandySearchBar() + uiSearchBar.isLeftButtonHidden = true + uiSearchBar.isRightButtonHidden = true + uiSearchBar.placeholder = "Custom" + return uiSearchBar + }() + + override func viewDidLoad() { + super.viewDidLoad() + + self.view.backgroundColor = .white + setupButtonTargets() + } + + override func setViewHierarchies() { + self.view.addSubview(searchBar1) + self.view.addSubview(searchBar2) + self.view.addSubview(searchBar3) + self.view.addSubview(searchBar4) + } + + override func setViewLayouts() { + searchBar1.snp.makeConstraints { + $0.bottom.equalTo(searchBar2.snp.top).offset(-20) + $0.horizontalEdges.equalToSuperview().inset(20) + } + searchBar2.snp.makeConstraints { + $0.center.equalToSuperview() + $0.horizontalEdges.equalToSuperview().inset(20) + } + searchBar3.snp.makeConstraints { + $0.top.equalTo(searchBar2.snp.bottom).offset(20) + $0.horizontalEdges.equalToSuperview().inset(20) + } + searchBar4.snp.makeConstraints { + $0.top.equalTo(searchBar3.snp.bottom).offset(20) + $0.horizontalEdges.equalToSuperview().inset(20) + } + } + private func setupButtonTargets() { + // searchBar1 + searchBar1.leftButton.addTarget(self, action: #selector(leftButtonTapped(_:)), for: .touchUpInside) + searchBar1.rightButton.addTarget(self, action: #selector(rightButtonTapped(_:)), for: .touchUpInside) + + // searchBar2 + searchBar2.leftButton.addTarget(self, action: #selector(leftButtonTapped(_:)), for: .touchUpInside) + + // searchBar3 + searchBar3.rightButton.addTarget(self, action: #selector(rightButtonTapped(_:)), for: .touchUpInside) + } + + @objc private func leftButtonTapped(_ sender: UIButton) { + print("Left button tapped") + } + + @objc private func rightButtonTapped(_ sender: UIButton) { + print("Right button tapped") + } +} + diff --git a/Handy/Handy-Storybook/SceneDelegate.swift b/Handy/Handy-Storybook/SceneDelegate.swift index dd9feef..ea347ee 100644 --- a/Handy/Handy-Storybook/SceneDelegate.swift +++ b/Handy/Handy-Storybook/SceneDelegate.swift @@ -16,7 +16,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(frame: UIScreen.main.bounds) window?.windowScene = windowScene - window?.rootViewController = HansySwitchViewController() + window?.rootViewController = HandySearchBarViewController() window?.makeKeyAndVisible() } diff --git a/Handy/Handy.xcodeproj/project.pbxproj b/Handy/Handy.xcodeproj/project.pbxproj index fd42298..f6b07e2 100644 --- a/Handy/Handy.xcodeproj/project.pbxproj +++ b/Handy/Handy.xcodeproj/project.pbxproj @@ -50,6 +50,8 @@ E51FBF9D2C539BC30097B0DA /* HandyIcon.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E5650D442C4E366F002790CC /* HandyIcon.xcassets */; }; E51FBFA02C54CB260097B0DA /* HandyRadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51FBF9F2C54CB260097B0DA /* HandyRadioButton.swift */; }; E51FBFA22C54CD350097B0DA /* RadioButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51FBFA12C54CD350097B0DA /* RadioButtonViewController.swift */; }; + E555E4C62D2522EA0091CF15 /* HandySearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E555E4C52D2522EA0091CF15 /* HandySearchBar.swift */; }; + E555E4C82D2529F10091CF15 /* HandySearchBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E555E4C72D2529F10091CF15 /* HandySearchBarViewController.swift */; }; E5650D432C4D326D002790CC /* HandyCheckBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5650D422C4D326D002790CC /* HandyCheckBox.swift */; }; E5650D452C4E366F002790CC /* HandyIcon.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E5650D442C4E366F002790CC /* HandyIcon.xcassets */; }; E5650D472C512B07002790CC /* HandyIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5650D462C512B07002790CC /* HandyIcon.swift */; }; @@ -122,6 +124,8 @@ E51FBF9A2C5399A00097B0DA /* CheckBoxViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckBoxViewController.swift; sourceTree = ""; }; E51FBF9F2C54CB260097B0DA /* HandyRadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyRadioButton.swift; sourceTree = ""; }; E51FBFA12C54CD350097B0DA /* RadioButtonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonViewController.swift; sourceTree = ""; }; + E555E4C52D2522EA0091CF15 /* HandySearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandySearchBar.swift; sourceTree = ""; }; + E555E4C72D2529F10091CF15 /* HandySearchBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandySearchBarViewController.swift; sourceTree = ""; }; E5650D422C4D326D002790CC /* HandyCheckBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyCheckBox.swift; sourceTree = ""; }; E5650D442C4E366F002790CC /* HandyIcon.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = HandyIcon.xcassets; sourceTree = ""; }; E5650D462C512B07002790CC /* HandyIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyIcon.swift; sourceTree = ""; }; @@ -177,6 +181,7 @@ E51FBF9A2C5399A00097B0DA /* CheckBoxViewController.swift */, E51FBFA12C54CD350097B0DA /* RadioButtonViewController.swift */, 02697A252C99DDA30027A362 /* HansySwitchViewController.swift */, + E555E4C72D2529F10091CF15 /* HandySearchBarViewController.swift */, ); path = Atom; sourceTree = ""; @@ -237,6 +242,7 @@ E5650D422C4D326D002790CC /* HandyCheckBox.swift */, 02697A232C99D7230027A362 /* HandySwitch.swift */, 02150E492CC8D7AB00EE690E /* HandySnackbar.swift */, + E555E4C52D2522EA0091CF15 /* HandySearchBar.swift */, ); path = Atom; sourceTree = ""; @@ -462,6 +468,7 @@ A5A12A7E2C57A6D900996916 /* ChipViewController.swift in Sources */, A5A12A7F2C57A92000996916 /* HandySematic.swift in Sources */, A5F6D36D2C97099C00FB961F /* DividerViewController.swift in Sources */, + E555E4C82D2529F10091CF15 /* HandySearchBarViewController.swift in Sources */, 025776392C4EA98C00272EC6 /* LabelViewController.swift in Sources */, 0257765D2C4EB9EF00272EC6 /* BaseViewController.swift in Sources */, 02ED764C2C57BD09001569F1 /* HandyBoxButtonViewController.swift in Sources */, @@ -489,6 +496,7 @@ E5669A3F2C443E7300DABC21 /* HandyBasicColor.swift in Sources */, 02ED76312C5284BB001569F1 /* HandyButtonProtocol.swift in Sources */, 02ED76352C5284F3001569F1 /* HandyTextButton.swift in Sources */, + E555E4C62D2522EA0091CF15 /* HandySearchBar.swift in Sources */, A5F6D36B2C96F32D00FB961F /* HandyDivider.swift in Sources */, 02BDB7FC2C3E99920050FB67 /* HandyFont.swift in Sources */, 02ED764A2C5779C3001569F1 /* UIImage+.swift in Sources */, diff --git a/Handy/Handy/Source/Atom/HandySearchBar.swift b/Handy/Handy/Source/Atom/HandySearchBar.swift new file mode 100644 index 0000000..52373ad --- /dev/null +++ b/Handy/Handy/Source/Atom/HandySearchBar.swift @@ -0,0 +1,196 @@ +// +// HandySearchBar.swift +// Handy +// +// Created by 성현주 on 1/1/25. +// + +import UIKit +import SnapKit + +public class HandySearchBar: UIView { + + // MARK: - 외부에서 설정할 수 있는 속성 + + /** + 왼쪽 버튼의 활성화 여부를 설정합니다. + */ + public var isLeftButtonHidden: Bool = false { + didSet { + leftButton.isHidden = isLeftButtonHidden + } + } + + /** + 오른쪽 버튼의 활성화 여부를 설정합니다. + */ + public var isRightButtonHidden: Bool = false { + didSet { + rightButton.isHidden = isRightButtonHidden + } + } + + /** + 텍스트 필드의 플레이스홀더를 설정합니다. + */ + open var placeholder: String = "" { + didSet { + textField.placeholder = placeholder + } + } + + /** + 텍스트 필드의 입력 값을 외부에서 가져올 수 있는 프로퍼티입니다. + */ + open var text: String? { + return textField.text + } + + + // MARK: - Properties + + public let leftButton: UIButton = { + let button = UIButton() + button.setImage(HandyIcon.arrowsChevronLeftFilled.withRenderingMode(.alwaysTemplate), for: .normal) + button.tintColor = HandySemantic.iconBasicSecondary + return button + }() + + public let rightButton: UIButton = { + let button = UIButton() + button.setImage(HandyIcon.filterBarFilled.withRenderingMode(.alwaysTemplate), for: .normal) + button.tintColor = HandySemantic.iconBasicSecondary + return button + }() + + private let clearButton: UIButton = { + let button = UIButton(type: .system) + button.setImage(HandyIcon.cancelFilled, for: .normal) + button.tintColor = HandySemantic.iconBasicTertiary + button.isHidden = true + return button + }() + + private let searchIconImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = HandyIcon.searchLine.withRenderingMode(.alwaysTemplate) + imageView.tintColor = HandySemantic.iconBasicTertiary + imageView.contentMode = .scaleAspectFit + return imageView + }() + + private let textField: UITextField = { + let textField = UITextField() + textField.placeholder = "Text input" + textField.font = HandyFont.B1Rg16 + textField.textColor = HandySemantic.textBasicPrimary + textField.borderStyle = .none + textField.backgroundColor = HandySemantic.bgBasicLight + textField.layer.cornerRadius = HandySemantic.radiusM + textField.leftViewMode = .always + textField.rightViewMode = .whileEditing + return textField + }() + + private let stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.alignment = .center + stackView.distribution = .fill + stackView.spacing = 8 + return stackView + }() + + // MARK: - Initialization + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + // MARK: - Private Methods + + private func setupView() { + setupLeftView() + setupRightView() + + stackView.addArrangedSubview(leftButton) + stackView.addArrangedSubview(textField) + stackView.addArrangedSubview(rightButton) + addSubview(stackView) + + // 기본 레이아웃 설정 + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + leftButton.snp.makeConstraints { + $0.width.height.equalTo(24) + } + + rightButton.snp.makeConstraints { + $0.width.height.equalTo(24) + } + + textField.snp.makeConstraints { + $0.height.equalTo(48) + } + + textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged) + clearButton.addTarget(self, action: #selector(clearText), for: .touchUpInside) + } + + /** + 텍스트 필드의 왼쪽에 표시되는 아이콘 뷰를 설정합니다. + */ + private func setupLeftView() { + let leftViewContainer = UIView() + leftViewContainer.addSubview(searchIconImageView) + searchIconImageView.snp.makeConstraints { + $0.center.equalToSuperview() + $0.width.height.equalTo(20) + } + leftViewContainer.snp.makeConstraints { + $0.width.height.equalTo(40) + } + textField.leftView = leftViewContainer + } + + /** + 텍스트 필드의 오른쪽에 표시되는 clearButton 뷰를 설정합니다. + */ + private func setupRightView() { + let rightViewContainer = UIView() + rightViewContainer.addSubview(clearButton) + clearButton.snp.makeConstraints { + $0.center.equalToSuperview() + $0.width.height.equalTo(20) + } + rightViewContainer.snp.makeConstraints { + $0.width.height.equalTo(40) + } + textField.rightView = rightViewContainer + } +} + +private extension HandySearchBar { + /** + 텍스트 필드의 내용을 지우고, clearButton을 숨깁니다. + */ + @objc func clearText() { + textField.text = "" + clearButton.isHidden = true + } + + /** + 텍스트 필드의 내용이 변경될 때 clearButton의 표시 여부를 업데이트합니다. + */ + @objc func textDidChange() { + clearButton.isHidden = textField.text?.isEmpty ?? true + } +} diff --git a/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_ArrowsChevronLeft_filled.imageset/Contents.json b/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_ArrowsChevronLeft_filled.imageset/Contents.json new file mode 100644 index 0000000..533c485 --- /dev/null +++ b/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_ArrowsChevronLeft_filled.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "size=36, filled=true.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_ArrowsChevronLeft_filled.imageset/size=36, filled=true.svg b/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_ArrowsChevronLeft_filled.imageset/size=36, filled=true.svg new file mode 100644 index 0000000..31ea8cf --- /dev/null +++ b/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_ArrowsChevronLeft_filled.imageset/size=36, filled=true.svg @@ -0,0 +1,3 @@ + + + diff --git a/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_filterBar_filled.imageset/Contents.json b/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_filterBar_filled.imageset/Contents.json new file mode 100644 index 0000000..533c485 --- /dev/null +++ b/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_filterBar_filled.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "size=36, filled=true.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_filterBar_filled.imageset/size=36, filled=true.svg b/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_filterBar_filled.imageset/size=36, filled=true.svg new file mode 100644 index 0000000..5075d48 --- /dev/null +++ b/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_filterBar_filled.imageset/size=36, filled=true.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_search_line.imageset/Contents.json b/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_search_line.imageset/Contents.json new file mode 100644 index 0000000..777366d --- /dev/null +++ b/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_search_line.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "size=36, filled=false.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_search_line.imageset/size=36, filled=false.svg b/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_search_line.imageset/size=36, filled=false.svg new file mode 100644 index 0000000..cca0b70 --- /dev/null +++ b/Handy/Handy/Source/Foundation/Asset/HandyIcon.xcassets/ic_search_line.imageset/size=36, filled=false.svg @@ -0,0 +1,3 @@ + + + diff --git a/Handy/Handy/Source/Foundation/HandyIcon.swift b/Handy/Handy/Source/Foundation/HandyIcon.swift index 672c404..65c48b9 100644 --- a/Handy/Handy/Source/Foundation/HandyIcon.swift +++ b/Handy/Handy/Source/Foundation/HandyIcon.swift @@ -34,6 +34,10 @@ public enum HandyIcon { public static var wifiLine: UIImage { .load(name: "ic_wifi_line") } public static var closeFilled: UIImage { .load(name: "ic_close_filled") } public static var closeLine: UIImage { .load(name: "ic_close_line") } + public static var searchLine: UIImage { .load(name: "ic_search_line") } + public static var arrowsChevronLeftFilled: UIImage { .load(name: "ic_ArrowsChevronLeft_filled") } + public static var filterBarFilled: UIImage { .load(name: "ic_filterBar_filled") } + public static var checkBoxFilled: UIImage { .load(name: "checkBoxFilled") } public static var checkBoxLine: UIImage { .load(name: "checkBoxLine") }