Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions Handy/Handy-Storybook/Atom/HandySearchBarViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// 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.rightIcon = false
return uiSearchBar
}()
private let searchBar3: HandySearchBar = {
let uiSearchBar = HandySearchBar()
uiSearchBar.leftIcon = false
return uiSearchBar
}()
private let searchBar4: HandySearchBar = {
let uiSearchBar = HandySearchBar()
uiSearchBar.leftIcon = false
uiSearchBar.rightIcon = false
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")
}
}

2 changes: 1 addition & 1 deletion Handy/Handy-Storybook/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
8 changes: 8 additions & 0 deletions Handy/Handy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -122,6 +124,8 @@
E51FBF9A2C5399A00097B0DA /* CheckBoxViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckBoxViewController.swift; sourceTree = "<group>"; };
E51FBF9F2C54CB260097B0DA /* HandyRadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyRadioButton.swift; sourceTree = "<group>"; };
E51FBFA12C54CD350097B0DA /* RadioButtonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonViewController.swift; sourceTree = "<group>"; };
E555E4C52D2522EA0091CF15 /* HandySearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandySearchBar.swift; sourceTree = "<group>"; };
E555E4C72D2529F10091CF15 /* HandySearchBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandySearchBarViewController.swift; sourceTree = "<group>"; };
E5650D422C4D326D002790CC /* HandyCheckBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyCheckBox.swift; sourceTree = "<group>"; };
E5650D442C4E366F002790CC /* HandyIcon.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = HandyIcon.xcassets; sourceTree = "<group>"; };
E5650D462C512B07002790CC /* HandyIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyIcon.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -177,6 +181,7 @@
E51FBF9A2C5399A00097B0DA /* CheckBoxViewController.swift */,
E51FBFA12C54CD350097B0DA /* RadioButtonViewController.swift */,
02697A252C99DDA30027A362 /* HansySwitchViewController.swift */,
E555E4C72D2529F10091CF15 /* HandySearchBarViewController.swift */,
);
path = Atom;
sourceTree = "<group>";
Expand Down Expand Up @@ -237,6 +242,7 @@
E5650D422C4D326D002790CC /* HandyCheckBox.swift */,
02697A232C99D7230027A362 /* HandySwitch.swift */,
02150E492CC8D7AB00EE690E /* HandySnackbar.swift */,
E555E4C52D2522EA0091CF15 /* HandySearchBar.swift */,
);
path = Atom;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
188 changes: 188 additions & 0 deletions Handy/Handy/Source/Atom/HandySearchBar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
//
// HandySearchBar.swift
// Handy
//
// Created by 성현주 on 1/1/25.
//

import UIKit
import SnapKit

final public class HandySearchBar: UIView {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 컴포넌트를 final로 규정짓는 것은 살짝 위험할 수도 있다고 생각해요!
HandySearchBar를 상속받아서 더 꾸밀 수도 있다고 생각하기 때문에 final public보다는 필요한 곳에 open 접근제어를 걸어두는 것도 좋다고 생각합니다~!!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 맞습니다. 추가적인 커스텀이 필요한 상황이 분명 발생할거기 때문에 final은 과하다라는 생각이 듭니다. 필요한 부분에 open으로 수정했습니다!


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

/**
왼쪽 버튼의 활성화 여부를 설정합니다.
*/
@Invalidating(wrappedValue: true, .layout) public var leftIcon: Bool {
didSet {
leftButton.isHidden = !leftIcon
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

layout을 무효화(Invalidating) 시키면 setNeedsLayout()가 불리게 됩니다. (출처)

setNeedsLayout()은 다음 UI 업데이트 사이클에 layoutSubviews() 함수를 부르게 해줍니다.
그래서, @invalidating(.layout) 에 따라서 다시 subview들을 배치해야 하는 상황일 때 추가적인 작업은 layoutSubviews() 을 오버라이딩 해서 넣는거죠.

여기에서는 @InvalidatingdidSet 중 하나는 굳이 필요하지 않을 것 같아요. @Invalidating을 사용한다면 didSet보다는 layoutSubviews() 함수를 오버라이딩해서 leftButton과 rightButton의 hidden을 결정하면 될 것 같구요, didSet을 사용한다면 @Invalidating 방식은 아예 사용하지 않는 편이 좋을 것 같아요.

더 편한 방식을 고민해서 사용해주시면 될 것 같습니다~!!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HandySearchBarViewController 부분 (디버깅하는 뷰)에 아무 버튼을 하나 만들고 searchBar1.leftIcon.toggle() 을 해보시면 잘 동작하는지 확인할 수 있어요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

흠 말씀해주신 내용을 바탕으로 다시 고민해보니, invalidating 접근은 레이아웃을 전반적으로 수정해야 하는 경우에 적합하지만, 버튼의 hidden 속성과 같이 간단한 UI 요소 변경에는 과도할 것 같다는 생각이 드네요!
버튼과 텍스트 필드의 레이아웃도 스택뷰로 관리하고 있으니 굳이 무효화가 필요없기 때문에 간단히 didset으로 변경해도 충분할것 같습니다!
didset으로 변경하는 방향으로 수정하겟습니다~! 좋은 답변 감사드립니다!


/**
오른쪽 버튼의 활성화 여부를 설정합니다.
*/
@Invalidating(wrappedValue: true, .layout) public var rightIcon: Bool {
didSet {
rightButton.isHidden = !rightIcon
}
}

/**
텍스트 필드의 플레이스홀더를 설정합니다.
*/
@Invalidating(wrappedValue: "", .layout) public var placeholder: String {
didSet {
textField.placeholder = placeholder
}
}

// 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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading