diff --git a/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonTestViewController.swift b/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonTestViewController.swift index 4635c1a7..467d360e 100644 --- a/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonTestViewController.swift +++ b/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonTestViewController.swift @@ -1,4 +1,4 @@ -// Copyright © 2025 Booket. All rights reserved +// Copyright © 2026 Booket. All rights reserved import BKDesign import SnapKit @@ -52,6 +52,9 @@ public final class BKButtonTestViewController: UIViewController { setupButtons(for: .medium) setupButtons(for: .small) setupButtons(for: .rounded) + + addSectionSeparator() + setupTextButtons() } private func setupButtons(for size: BKButtonSize) { @@ -66,6 +69,39 @@ public final class BKButtonTestViewController: UIViewController { addIconButton("양쪽 아이콘", style: .primary, size: size, left: BKImage.Icon.apple, right: BKImage.Icon.kakao) } + // MARK: - Text Button Tests + + private func setupTextButtons() { + addSectionHeader("Text Buttons") + + addTextButton("텍스트 버튼1", size: .small) + addTextButton("텍스트 버튼2", size: .medium) + addTextButton("텍스트 버튼3", size: .large) + addTextButton("아주 긴 내용의 텍스트 버튼을 만들어봅시다", size: .large) + addDisabledTextButton("비활성화 텍스트", size: .small) + } + + private func addTextButton(_ title: String, size: BKButtonSize) { + let button = BKTextButton(title: "[\(size.label)] \(title)", size: size) + containerView.addArrangedSubview(wrapForLeftAlign(button)) + } + + private func addDisabledTextButton(_ title: String, size: BKButtonSize) { + let button = BKTextButton(title: "[\(size.label)] \(title)", size: size) + button.isDisabled = true + containerView.addArrangedSubview(wrapForLeftAlign(button)) + } + + /// 텍스트 버튼은 intrinsic size를 사용하므로 왼쪽 정렬 래퍼 필요 + private func wrapForLeftAlign(_ view: UIView) -> UIView { + let wrapper = UIView() + wrapper.addSubview(view) + view.snp.makeConstraints { + $0.leading.verticalEdges.equalToSuperview() + } + return wrapper + } + // MARK: - Button Builders private func addButton(_ title: String, style: BKButtonStyle, size: BKButtonSize) { @@ -95,41 +131,26 @@ public final class BKButtonTestViewController: UIViewController { containerView.addArrangedSubview(button) } - private func setupIndependentButtons() { - let sampleView = UIView() - scrollView.addSubview(sampleView) - sampleView.snp.makeConstraints { - $0.top.equalTo(containerView.snp.bottom).offset(40) - $0.centerX.equalToSuperview() - $0.bottom.lessThanOrEqualToSuperview() - } - - let buttons: [BKButton] = [ - .primary(title: "[Free] Apple 로그인", size: .large), - .secondary(title: "[Free] Secondary", size: .large), - .tertiary(title: "[Free] Tertiary", size: .large), - .primary(title: "[Free] Apple 로그인", size: .medium), - .secondary(title: "[Free] Secondary", size: .small), - .tertiary(title: "[Free] Tertiary", size: .rounded) - ] - - buttons[0].leftIcon = BKImage.Icon.apple - buttons[2].rightIcon = BKImage.Icon.kakao - - var last: UIView? - for button in buttons { - sampleView.addSubview(button) - button.snp.makeConstraints { - $0.centerX.equalToSuperview() - $0.top.equalTo(last?.snp.bottom ?? sampleView.snp.top).offset(16) - } - last = button - } + // MARK: - Section Helpers + + private func addSectionHeader(_ title: String) { + let label = UILabel() + label.text = title + label.font = .systemFont(ofSize: 18, weight: .bold) + label.textColor = .darkGray + containerView.addArrangedSubview(label) + } + + private func addSectionSeparator() { + let separator = UIView() + separator.backgroundColor = .systemGray4 + separator.snp.makeConstraints { $0.height.equalTo(1) } + containerView.addArrangedSubview(separator) + containerView.setCustomSpacing(24, after: separator) } - } - +// MARK: - BKButtonSize Extension public extension BKButtonSize { var label: String { switch self { diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKTextButton.swift b/src/Projects/BKDesign/Sources/Components/Button/BKTextButton.swift new file mode 100644 index 00000000..fe5be441 --- /dev/null +++ b/src/Projects/BKDesign/Sources/Components/Button/BKTextButton.swift @@ -0,0 +1,172 @@ +// Copyright © 2025 Booket. All rights reserved + +import SnapKit +import UIKit + +/// 밑줄이 있는 텍스트 버튼 컴포넌트입니다. +/// +/// 링크 스타일의 텍스트 버튼으로, 배경 없이 텍스트와 밑줄만 표시됩니다. +/// `BKButtonGroup`과 함께 사용할 수 있습니다. +public final class BKTextButton: UIControl { + + // MARK: - Public Properties + + /// 버튼에 표시될 텍스트 + public var title: String? { + didSet { + titleLabel.text = title + invalidateIntrinsicContentSize() + } + } + + /// 버튼의 비활성화 여부 + public var isDisabled: Bool = false { + didSet { + isEnabled = !isDisabled + updateColors() + } + } + + /// 버튼 크기 (폰트 크기에 영향) + public var size: BKButtonSize = .small { + didSet { + titleLabel.font = size.font + invalidateIntrinsicContentSize() + } + } + + /// 텍스트 및 밑줄 색상 설정 + public var foregroundColors: BKButtonColorSet = BKButtonColorSet( + normal: .bkContentColor(.brand), + pressed: .bkContentColor(.brand), + disabled: .bkContentColor(.disable) + ) { + didSet { + updateColors() + } + } + + // MARK: - Override Properties + + public override var isHighlighted: Bool { + didSet { + updateColors() + animatePressedState() + } + } + + public override var isEnabled: Bool { + didSet { + updateColors() + } + } + + // MARK: - Private Properties + + private let titleLabel = UILabel() + private let underlineView = UIView() + + private let animationDuration: TimeInterval = 0.15 + + private var currentState: BKButtonState { + BKButtonState(isEnabled: isEnabled, isHighlighted: isHighlighted) + } + + // MARK: - Initialization + + public init(title: String? = nil, size: BKButtonSize = .small) { + super.init(frame: .zero) + self.title = title + self.size = size + setupViews() + updateColors() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + setupViews() + updateColors() + } + + // MARK: - Setup + + private func setupViews() { + setupTitleLabel() + setupUnderlineView() + } + + private func setupTitleLabel() { + addSubview(titleLabel) + + titleLabel.text = title + titleLabel.font = size.font + titleLabel.textAlignment = .center + titleLabel.numberOfLines = 1 + + titleLabel.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + } + } + + private func setupUnderlineView() { + addSubview(underlineView) + + underlineView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(1) + $0.leading.trailing.equalTo(titleLabel) + $0.height.equalTo(1) + $0.bottom.equalToSuperview() + } + } + + // MARK: - Update Methods + + private func updateColors() { + let color = foregroundColors.color(for: currentState) + titleLabel.textColor = color + underlineView.backgroundColor = color + } + + // MARK: - Animation + + private func animatePressedState() { + let targetAlpha: CGFloat = isHighlighted ? 0.6 : 1.0 + UIView.animate( + withDuration: animationDuration, + delay: 0, + options: [.allowUserInteraction, .beginFromCurrentState], + animations: { + self.titleLabel.alpha = targetAlpha + self.underlineView.alpha = targetAlpha + } + ) + } + + // MARK: - Intrinsic Content Size + + public override var intrinsicContentSize: CGSize { + let labelSize = titleLabel.intrinsicContentSize + // 라벨 높이 + 밑줄 오프셋(1) + 밑줄 높이(1) + return CGSize( + width: labelSize.width, + height: labelSize.height + 2 + ) + } +} + +// MARK: - Factory Methods +extension BKTextButton { + /// 텍스트 버튼 생성 + public static func text(title: String, size: BKButtonSize = .small) -> BKTextButton { + BKTextButton(title: title, size: size) + } +} + +/* 커스텀 색상세트 사용 시 + let customButton = BKTextButton(title: "커스텀", size: .small) + customButton.foregroundColors = BKButtonColorSet( + normal: .systemBlue, + pressed: .systemBlue, + disabled: .systemGray + ) + */