diff --git a/src/Projects/BKDesign/PreviewApp/Info.plist b/src/Projects/BKDesign/PreviewApp/Info.plist new file mode 100644 index 00000000..912bf849 --- /dev/null +++ b/src/Projects/BKDesign/PreviewApp/Info.plist @@ -0,0 +1,68 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + + UILaunchStoryboardName + LaunchScreen + + + UIDeviceFamily + + 1 + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + BKDesignPreviewApp.SceneDelegate + + + + + + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + + + UIAppFonts + + Pretendard-Regular.otf + Pretendard-Medium.otf + Pretendard-SemiBold.otf + Pretendard-Bold.otf + + + diff --git a/src/Projects/BKDesign/PreviewApp/Resources/LaunchScreen.storyboard b/src/Projects/BKDesign/PreviewApp/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000..dd79351e --- /dev/null +++ b/src/Projects/BKDesign/PreviewApp/Resources/LaunchScreen.storyboard @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Projects/BKDesign/PreviewApp/Sources/AppDelegate.swift b/src/Projects/BKDesign/PreviewApp/Sources/AppDelegate.swift new file mode 100644 index 00000000..d7723cda --- /dev/null +++ b/src/Projects/BKDesign/PreviewApp/Sources/AppDelegate.swift @@ -0,0 +1,23 @@ +// Copyright ยฉ 2025 Booket. All rights reserved + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return true + } + + // MARK: UISceneSession Lifecycle + + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } +} diff --git a/src/Projects/BKDesign/PreviewApp/Sources/SceneDelegate.swift b/src/Projects/BKDesign/PreviewApp/Sources/SceneDelegate.swift new file mode 100644 index 00000000..29e6c422 --- /dev/null +++ b/src/Projects/BKDesign/PreviewApp/Sources/SceneDelegate.swift @@ -0,0 +1,24 @@ +// Copyright ยฉ 2025 Booket. All rights reserved + +import BKDesign +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let windowScene = scene as? UIWindowScene else { return } + + let window = UIWindow(windowScene: windowScene) + let viewController = BKButtonTestViewController() +// let viewController = BKButtonGroupDemoViewController() + window.rootViewController = UINavigationController(rootViewController: viewController) + window.makeKeyAndVisible() + + self.window = window + } +} diff --git a/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonGroupDemoViewController.swift b/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonGroupDemoViewController.swift new file mode 100644 index 00000000..f31b030f --- /dev/null +++ b/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonGroupDemoViewController.swift @@ -0,0 +1,105 @@ +// Copyright ยฉ 2025 Booket. All rights reserved + +import BKDesign +import SnapKit +import UIKit + +public final class BKButtonGroupDemoViewController: UIViewController { + + private let scrollView = UIScrollView() + private let containerView = UIStackView() + + public override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + setupScrollView() + setupDemoGroups() + } + + private func setupScrollView() { + view.addSubview(scrollView) + scrollView.snp.makeConstraints { $0.edges.equalToSuperview() } + + scrollView.addSubview(containerView) + containerView.axis = .vertical + containerView.spacing = 16 + containerView.alignment = .fill + containerView.distribution = .fill + containerView.snp.makeConstraints { + $0.edges.equalToSuperview().inset(20) + $0.width.equalToSuperview().inset(20) + } + } + + private func setupDemoGroups() { + addSection(title: "Custom ๊ตฌ์„ฑ (์ง์ ‘ ๋งŒ๋“  ๊ทธ๋ฃน)") + containerView.addArrangedSubview(BKButtonGroup( + buttons: [ + makeButton(title: "[Custom] S", size: .small), + makeButton(title: "[Custom] M", size: .medium), + makeButton(title: "[Custom] L", size: .large) + ], + layout: .horizontal + )) + + addDivider() + + addSection(title: "TwoButtonGroup") + containerView.addArrangedSubview(BKButtonGroup.twoButtonGroup( + leftTitle: "์ทจ์†Œ", rightTitle: "ํ™•์ธ", + leftAction: { print("์ทจ์†Œ tapped") }, + rightAction: { print("ํ™•์ธ tapped") } + )) + + addDivider() + + addSection(title: "ThreeButtonGroup") + containerView.addArrangedSubview(BKButtonGroup.threeButtonGroup( + leftTitle: "์ด์ „", centerTitle: "์ค‘๊ฐ„", rightTitle: "๋‹ค์Œ", + leftAction: { print("์ด์ „ tapped") }, + centerAction: { print("์ค‘๊ฐ„ tapped") }, + rightAction: { print("๋‹ค์Œ tapped") } + )) + + addDivider() + + addSection(title: "SingleFullButton") + containerView.addArrangedSubview(BKButtonGroup.singleFullButton( + title: "๊ณ„์†ํ•˜๊ธฐ", + action: { print("๊ณ„์†ํ•˜๊ธฐ tapped") } + )) + + addDivider() + + addSection(title: "VerticalGroup (Rounded ๋ฒ„ํŠผ)") + containerView.addArrangedSubview(BKButtonGroup.verticalButtonGroup(buttons: [ + makeButton(title: "๋‘ฅ๊ธ€1", size: .rounded), + makeButton(title: "๋‘ฅ๊ธ€2", size: .rounded) + ])) + } + + private func makeButton(title: String, size: BKButtonSize) -> BKButton { + let button = BKButton.primary(title: title, size: size) + button.addAction(UIAction { _ in + print("Tapped: \(title)") + }, for: .touchUpInside) + return button + } + + private func addSection(title: String) { + let label = UILabel() + label.text = title + label.font = UIFont.systemFont(ofSize: 14, weight: .bold) + label.textColor = .darkGray + containerView.addArrangedSubview(label) + } + + private func addDivider() { + let divider = UIView() + divider.backgroundColor = UIColor.lightGray.withAlphaComponent(0.4) + divider.snp.makeConstraints { make in + make.height.equalTo(1) + } + containerView.addArrangedSubview(divider) + } +} diff --git a/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonTestViewController.swift b/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonTestViewController.swift new file mode 100644 index 00000000..83f4ee84 --- /dev/null +++ b/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonTestViewController.swift @@ -0,0 +1,133 @@ +// Copyright ยฉ 2025 Booket. All rights reserved + +import BKDesign +import SnapKit +import UIKit + +public final class BKButtonTestViewController: UIViewController { + + // MARK: - UI Components + private let scrollView = UIScrollView() + private let containerView = UIStackView() + + // MARK: - Lifecycle + + public override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + setupScrollView() +// setupIndependentButtons() + setupStackView() + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + setupAllTestButtons() + } + + // MARK: - Setup Scroll & Stack + + private func setupScrollView() { + view.addSubview(scrollView) + scrollView.snp.makeConstraints { $0.edges.equalToSuperview() } + + scrollView.addSubview(containerView) + containerView.snp.makeConstraints { + $0.edges.equalToSuperview().inset(20) + $0.width.equalToSuperview().inset(20) + } + } + + private func setupStackView() { + containerView.axis = .vertical + containerView.spacing = 16 + containerView.alignment = .fill + containerView.distribution = .equalSpacing + } + + // MARK: - Setup Buttons by Size + + private func setupAllTestButtons() { + setupButtons(for: .large) + setupButtons(for: .medium) + setupButtons(for: .small) + setupButtons(for: .rounded) + } + + private func setupButtons(for size: BKButtonSize) { + addButton("Primary", style: .primary, size: size) + addButton("Secondary", style: .secondary, size: size) + addButton("Tertiary", style: .tertiary, size: size) + + addIconButton("Apple ๋กœ๊ทธ์ธ", style: .primary, size: size, left: .appleLogo) + addIconButton("์นด์นด์˜ค ๋กœ๊ทธ์ธ", style: .primary, size: size, right: .kakaoLogo) + addIconButton("์–‘์ชฝ ์•„์ด์ฝ˜", style: .primary, size: size, left: .appleLogo, right: .kakaoLogo) + } + + // MARK: - Button Builders + + private func addButton(_ title: String, style: BKButtonStyle, size: BKButtonSize) { + let button = BKButton(style: style, size: size) + button.title = "[\(size.label)] \(title)" + containerView.addArrangedSubview(button) + } + + private func addIconButton( + _ title: String, + style: BKButtonStyle, + size: BKButtonSize, + left: BKIcon? = nil, + right: BKIcon? = nil + ) { + let button = BKButton(style: style, size: size) + button.title = "[\(size.label)] \(title)" + button.leftIcon = left?.image + button.rightIcon = right?.image + 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 = BKIcon.appleLogo.image + buttons[2].rightIcon = BKIcon.kakaoLogo.image + + 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 + } + } + +} + + +public extension BKButtonSize { + var label: String { + switch self { + case .large: return "Large" + case .medium: return "Medium" + case .small: return "Small" + case .rounded: return "Rounded" + } + } +} diff --git a/src/Projects/BKDesign/Project.swift b/src/Projects/BKDesign/Project.swift index 90fd3d14..9a6de982 100644 --- a/src/Projects/BKDesign/Project.swift +++ b/src/Projects/BKDesign/Project.swift @@ -14,7 +14,8 @@ let project = Project.project( swiftLintScript ], dependencies: [ - .core() + .core(), + .external(dependency: .SnapKit) ] ), Target.target( @@ -25,6 +26,35 @@ let project = Project.project( dependencies: [ .design() ] + ), + Target.target( + name: "BKDesignPreviewApp", + product: .app, + bundleId: "designpreview." + Project.bundleID, + infoPlist: .file(path: .relativeToRoot("Projects/BKDesign/PreviewApp/Info.plist")), // ๋ณต์‚ฌ๋ณธ! + sources: ["PreviewApp/Sources/**"], + resources: [ + "PreviewApp/Resources/**", + .glob(pattern: .relativeToRoot("Projects/BKDesign/Resources/Font/**")) + ], + scripts: [ + swiftLintScript + ], + dependencies: [ + .design() // BKDesign ๋ชจ๋“ˆ ์˜์กด์„ฑ + ], + settings: .settings( + base: [ + "DEVELOPMENT_LANGUAGE": "ko", + "CODE_SIGN_STYLE": "Automatic" // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์•ฑ์€ ์ž๋™์œผ๋กœ + ], + configurations: [ + .debug(name: "Debug", xcconfig: .relativeToRoot("SupportingFiles/Booket/Debug.xcconfig")), + ] + ) ) + + ] ) + diff --git a/src/Projects/BKDesign/Sources/Components/BKButton.swift b/src/Projects/BKDesign/Sources/Components/BKButton.swift deleted file mode 100644 index 55807205..00000000 --- a/src/Projects/BKDesign/Sources/Components/BKButton.swift +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright ยฉ 2025 Booket. All rights reserved - -import Foundation diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButton.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButton.swift new file mode 100644 index 00000000..fe453e76 --- /dev/null +++ b/src/Projects/BKDesign/Sources/Components/Button/BKButton.swift @@ -0,0 +1,373 @@ +// Copyright ยฉ 2025 Booket. All rights reserved + +import SnapKit +import UIKit + +public class BKButton: UIButton, BKButtonProtocol { + + @SetNeeds(.layout, .display) + public var style: BKButtonStyle = .primary { + didSet { + updateButtonStyle() + } + } + + @SetNeeds(.layout) + public var size: BKButtonSize = .medium { + didSet { + updateButtonSize() + } + } + + @SetNeeds(wrappedValue: nil, .layout) + public var title: String? { + didSet { + setTitle("", for: .normal) // UIButton ๊ธฐ๋ณธ ํƒ€์ดํ‹€ ์ œ๊ฑฐ + customTitleLabel.text = title + updateLayout() + } + } + + @SetNeeds(wrappedValue: nil, .layout) + public var leftIcon: UIImage? { + didSet { + updateLeftIcon() + updateLayout() + } + } + + @SetNeeds(wrappedValue: nil, .layout) + public var rightIcon: UIImage? { + didSet { + updateRightIcon() + updateLayout() + } + } + + @SetNeeds(.layout, .display) + public var isDisabled: Bool = false { + didSet { + isEnabled = !isDisabled + updateButtonState() + } + } + + public var isFullWidth: Bool = false { + didSet { + invalidateIntrinsicContentSize() + } + } + + // MARK: - Override UIButton Properties + public override var isHighlighted: Bool { + didSet { + updateButtonState() + animatePressedState() + } + } + + public override var isEnabled: Bool { + didSet { + updateButtonState() + } + } + + // MARK: - Private Properties + // ์ปค์Šคํ…€ ๋ ˆ์ด์•„์›ƒ์„ ์œ„ํ•œ ๋ทฐ + private let customContainerView = UIView() + private let leftIconView = UIImageView() + private let customTitleLabel = UILabel() + private let rightIconView = UIImageView() + private let stackView = UIStackView() + + // Animation properties + private let pressedScale: CGFloat = 0.96 + private let animationDuration: TimeInterval = 0.15 + private let animationSpringDamping: CGFloat = 0.7 + private let animationSpringVelocity: CGFloat = 0.5 + + private var currentState: BKButtonState { + return BKButtonState(isEnabled: isEnabled, isHighlighted: isHighlighted) + } + + // MARK: - Initialization + public init(style: BKButtonStyle = .primary, size: BKButtonSize = .medium) { + super.init(frame: .zero) + self.style = style + self.size = size + setupButton() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + setupButton() + } + + private func setupButton() { + _style.configure(with: self) + _size.configure(with: self) + _title.configure(with: self) + _leftIcon.configure(with: self) + _rightIcon.configure(with: self) + _isDisabled.configure(with: self) + + // UIButton์˜ ๊ธฐ๋ณธ ์š”์†Œ๋“ค์„ ์ˆจ๊น€ + setTitle("", for: .normal) + setImage(nil, for: .normal) + + // iOS 15.0+ Configuration ๋น„ํ™œ์„ฑํ™” + if #available(iOS 15.0, *) { + configuration = nil + } + + setupCustomViews() + updateButtonStyle() + updateButtonSize() + updateLayout() + } + + private func setupCustomViews() { + addSubview(customContainerView) + customContainerView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + customContainerView.isUserInteractionEnabled = false + + customContainerView.addSubview(stackView) + stackView.axis = .horizontal + stackView.alignment = .center + stackView.distribution = .equalCentering + stackView.isUserInteractionEnabled = false + + stackView.addArrangedSubview(leftIconView) + stackView.addArrangedSubview(customTitleLabel) + stackView.addArrangedSubview(rightIconView) + + stackView.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.left.greaterThanOrEqualToSuperview().offset(size.horizontalPadding) + make.right.lessThanOrEqualToSuperview().offset(-size.horizontalPadding) + + make.centerY.equalToSuperview() + make.top.greaterThanOrEqualToSuperview().offset(size.verticalPadding) + make.bottom.lessThanOrEqualToSuperview().offset(-size.verticalPadding) + } + + setupIconViews() + setupTitleLabel() + } + + private func setupIconViews() { + [leftIconView, rightIconView].forEach { iconView in + iconView.contentMode = .scaleAspectFit + iconView.isHidden = true + iconView.isUserInteractionEnabled = false + iconView.setContentHuggingPriority(.required, for: .horizontal) + iconView.setContentCompressionResistancePriority(.required, for: .horizontal) + } + + leftIconView.snp.makeConstraints { make in + make.width.height.equalTo(size.iconSize.width) + } + + rightIconView.snp.makeConstraints { make in + make.width.height.equalTo(size.iconSize.width) + } + } + + private func setupTitleLabel() { + customTitleLabel.textAlignment = .center + customTitleLabel.numberOfLines = 1 + customTitleLabel.isUserInteractionEnabled = false + customTitleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + customTitleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + } + + // MARK: - Update Methods + private func updateButtonState() { + updateColors() + } + + private func updateButtonStyle() { + updateColors() + } + + private func updateButtonSize() { + customTitleLabel.font = size.font + updateCornerRadius() + updateIconSizes() + invalidateIntrinsicContentSize() + } + + private func updateColors() { + let backgroundColors = style.backgroundColors + let foregroundColors = style.foregroundColors + + backgroundColor = backgroundColors.color(for: currentState) + let foregroundColor = foregroundColors.color(for: currentState) + + customTitleLabel.textColor = foregroundColor + leftIconView.tintColor = foregroundColor + rightIconView.tintColor = foregroundColor + } + + private func updateCornerRadius() { + if size != .rounded { + layer.cornerRadius = size.cornerRadius + } + layer.masksToBounds = true + } + + private func updateIconSizes() { + let iconSize = size.iconSize.width + leftIconView.snp.updateConstraints { make in + make.width.height.equalTo(iconSize) + } + rightIconView.snp.updateConstraints { make in + make.width.height.equalTo(iconSize) + } + } + + private func updateLayout() { + stackView.spacing = 0 + stackView.setCustomSpacing(0, after: leftIconView) + stackView.setCustomSpacing(0, after: customTitleLabel) + + if leftIcon != nil, let title = title, !title.isEmpty { + stackView.setCustomSpacing(size.iconSpacing, after: leftIconView) + } + + if rightIcon != nil, let title = title, !title.isEmpty { + stackView.setCustomSpacing(size.iconSpacing, after: customTitleLabel) + } + } + + private func updateLeftIcon() { + if let icon = leftIcon { + let resizedIcon = icon.resize(to: size.iconSize) + leftIconView.image = resizedIcon?.withRenderingMode(.alwaysTemplate) + leftIconView.isHidden = false + } else { + leftIconView.image = nil + leftIconView.isHidden = true + } + } + + private func updateRightIcon() { + if let icon = rightIcon { + let resizedIcon = icon.resize(to: size.iconSize) + rightIconView.image = resizedIcon?.withRenderingMode(.alwaysTemplate) + rightIconView.isHidden = false + } else { + rightIconView.image = nil + rightIconView.isHidden = true + } + } + + // MARK: - Animation + private func animatePressedState() { + if isHighlighted { + UIView.animate( + withDuration: animationDuration, + delay: 0, + usingSpringWithDamping: animationSpringDamping, + initialSpringVelocity: animationSpringVelocity, + options: [.allowUserInteraction, .beginFromCurrentState], + animations: { + self.transform = CGAffineTransform(scaleX: self.pressedScale, y: self.pressedScale) + } + ) + } else { + UIView.animate( + withDuration: animationDuration, + delay: 0, + usingSpringWithDamping: animationSpringDamping, + initialSpringVelocity: animationSpringVelocity, + options: [.allowUserInteraction, .beginFromCurrentState], + animations: { + self.transform = .identity + } + ) + } + } + + + // MARK: - Layout + override public func layoutSubviews() { + super.layoutSubviews() + + // 'Rounded'์ผ ๋•Œ๋งŒ ๋™์ ์œผ๋กœ ๋ฐ˜์ง€๋ฆ„ ์„ค์ • + if size == .rounded { + let height = bounds.height + let width = bounds.width + + let minimumRadius = height / 2 + let dynamicRadius = width / 2 + + layer.cornerRadius = min(minimumRadius, dynamicRadius) + } + + } + + public override var intrinsicContentSize: CGSize { + let stackSize = stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + + let minimumTotalPadding = size.horizontalPadding * 2 + + let currentButtonWidth = bounds.width + let calculatedPadding = max((currentButtonWidth - stackSize.width), minimumTotalPadding) + + let intrinsicWidth = stackSize.width + calculatedPadding + + if isFullWidth { + return CGSize(width: UIView.noIntrinsicMetric, height: size.height) + } + + return CGSize(width: intrinsicWidth, height: size.height) + } + +} + +// MARK: - Factory Methods +extension BKButton { + public static func primary( + title: String, + size: BKButtonSize = .medium + ) -> BKButton { + let button = BKButton(style: .primary, size: size) + button.title = title + return button + } + + public static func secondary( + title: String, + size: BKButtonSize = .medium + ) -> BKButton { + let button = BKButton(style: .secondary, size: size) + button.title = title + return button + } + + public static func tertiary( + title: String, + size: BKButtonSize = .medium + ) -> BKButton { + let button = BKButton(style: .tertiary, size: size) + button.title = title + return button + } +} + +// MARK: - Configuration Support +extension BKButton { + public func configure(with configuration: BKButtonConfiguration) { + style = configuration.style + size = configuration.size + title = configuration.title + leftIcon = configuration.leftIcon + rightIcon = configuration.rightIcon + isDisabled = !configuration.isEnabled + isFullWidth = configuration.isFullWidth + } +} diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButtonConfiguration.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButtonConfiguration.swift new file mode 100644 index 00000000..0782b259 --- /dev/null +++ b/src/Projects/BKDesign/Sources/Components/Button/BKButtonConfiguration.swift @@ -0,0 +1,110 @@ +// Copyright ยฉ 2025 Booket. All rights reserved + +import UIKit + +/// `BKButton`์— ์ ์šฉํ•  ์™ธ๋ถ€ ๊ตฌ์„ฑ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ๊ตฌ์กฐ์ฒด์ž…๋‹ˆ๋‹ค. +/// +/// ๋ฒ„ํŠผ ์Šคํƒ€์ผ, ํฌ๊ธฐ, ํƒ€์ดํ‹€, ์•„์ด์ฝ˜, ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์ƒํƒœ ๋“ฑ์„ ํ†ตํ•ฉ ๊ด€๋ฆฌํ•˜๋ฉฐ +/// ์„ค์ •์„ ์ฒด์ด๋‹ ๋ฐฉ์‹์œผ๋กœ ์†์‰ฝ๊ฒŒ ์กฐํ•ฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +public struct BKButtonConfiguration { + + /// ๋ฒ„ํŠผ ์Šคํƒ€์ผ (๊ธฐ๋ณธ๊ฐ’: `.primary`) + var style: BKButtonStyle + + /// ๋ฒ„ํŠผ ์‚ฌ์ด์ฆˆ (๊ธฐ๋ณธ๊ฐ’: `.medium`) + var size: BKButtonSize + + /// ๋ฒ„ํŠผ ํƒ€์ดํ‹€ ํ…์ŠคํŠธ + var title: String? + + /// ์™ผ์ชฝ์— ํ‘œ์‹œํ•  ์•„์ด์ฝ˜ + var leftIcon: UIImage? + + /// ์˜ค๋ฅธ์ชฝ์— ํ‘œ์‹œํ•  ์•„์ด์ฝ˜ + var rightIcon: UIImage? + + /// ๋ฒ„ํŠผ ํ™œ์„ฑํ™” ์ƒํƒœ (๊ธฐ๋ณธ๊ฐ’: `true`) + var isEnabled: Bool + + /// ๊ฐ€๋กœ๋กœ ๊ฝ‰ ์ฐจ๋Š”์ง€ ์—ฌ๋ถ€ (๊ธฐ๋ณธ๊ฐ’: `false`) + var isFullWidth: Bool + + /// ์ดˆ๊ธฐํ™” ๋ฉ”์„œ๋“œ + /// + /// - Parameters: + /// - style: ๋ฒ„ํŠผ ์Šคํƒ€์ผ + /// - size: ๋ฒ„ํŠผ ํฌ๊ธฐ + /// - title: ๋ฒ„ํŠผ ํ…์ŠคํŠธ + /// - leftIcon: ์ขŒ์ธก ์•„์ด์ฝ˜ ์ด๋ฏธ์ง€ + /// - rightIcon: ์šฐ์ธก ์•„์ด์ฝ˜ ์ด๋ฏธ์ง€ + /// - isEnabled: ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ + /// - isFullWidth: ๊ฐ€๋กœ ๊ฝ‰ ์ฑ„์šฐ๊ธฐ ์—ฌ๋ถ€ + public init( + style: BKButtonStyle = .primary, + size: BKButtonSize = .medium, + title: String? = nil, + leftIcon: UIImage? = nil, + rightIcon: UIImage? = nil, + isEnabled: Bool = true, + isFullWidth: Bool = false + ) { + self.style = style + self.size = size + self.title = title + self.leftIcon = leftIcon + self.rightIcon = rightIcon + self.isEnabled = isEnabled + self.isFullWidth = isFullWidth + } + + // MARK: - Immutable Modifier Helpers + + /// ์Šคํƒ€์ผ์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์„ค์ • ๋ฐ˜ํ™˜ + public func withStyle(_ style: BKButtonStyle) -> BKButtonConfiguration { + var config = self + config.style = style + return config + } + + /// ์‚ฌ์ด์ฆˆ๋ฅผ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์„ค์ • ๋ฐ˜ํ™˜ + public func withSize(_ size: BKButtonSize) -> BKButtonConfiguration { + var config = self + config.size = size + return config + } + + /// ํƒ€์ดํ‹€์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์„ค์ • ๋ฐ˜ํ™˜ + public func withTitle(_ title: String?) -> BKButtonConfiguration { + var config = self + config.title = title + return config + } + + /// ์™ผ์ชฝ ์•„์ด์ฝ˜์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์„ค์ • ๋ฐ˜ํ™˜ + public func withLeftIcon(_ icon: UIImage?) -> BKButtonConfiguration { + var config = self + config.leftIcon = icon + return config + } + + /// ์˜ค๋ฅธ์ชฝ ์•„์ด์ฝ˜์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์„ค์ • ๋ฐ˜ํ™˜ + public func withRightIcon(_ icon: UIImage?) -> BKButtonConfiguration { + var config = self + config.rightIcon = icon + return config + } + + /// ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋ฅผ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์„ค์ • ๋ฐ˜ํ™˜ + public func withEnabled(_ enabled: Bool) -> BKButtonConfiguration { + var config = self + config.isEnabled = enabled + return config + } + + /// ๊ฐ€๋กœ ์ฑ„์šฐ๊ธฐ ์—ฌ๋ถ€๋ฅผ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์„ค์ • ๋ฐ˜ํ™˜ + public func withFullWidth(_ fullWidth: Bool) -> BKButtonConfiguration { + var config = self + config.isFullWidth = fullWidth + return config + } +} diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButtonGroup.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButtonGroup.swift new file mode 100644 index 00000000..d717c544 --- /dev/null +++ b/src/Projects/BKDesign/Sources/Components/Button/BKButtonGroup.swift @@ -0,0 +1,198 @@ +// Copyright ยฉ 2025 Booket. All rights reserved + +import SnapKit +import UIKit + +public class BKButtonGroup: UIView { + + // MARK: - Types + public enum Layout { + case horizontal + case vertical + case fullWidth + } + + // MARK: - Properties + private let stackView = UIStackView() + private var buttons: [BKButton] = [] + + public var layout: Layout = .horizontal { + didSet { + updateLayout() + } + } + + public var spacing: CGFloat = BKSpacing.spacing2 { + didSet { + stackView.spacing = spacing + } + } + + // MARK: - Initialization + + public init( + buttons: [BKButton], + layout: Layout = .horizontal, + spacing: CGFloat = BKSpacing.spacing2 + ) { + self.buttons = buttons + self.layout = layout + self.spacing = spacing + super.init(frame: .zero) + setupView() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + // MARK: - Setup + private func setupView() { + addSubview(stackView) + stackView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + updateLayout() + updateButtons() + } + + // MARK: - Public Methods + public func setButtons(_ buttons: [BKButton]) { + self.buttons = buttons + updateButtons() + } + + public func addButton(_ button: BKButton) { + buttons.append(button) + updateButtons() + } + + public func removeButton(_ button: BKButton) { + if let index = buttons.firstIndex(of: button) { + buttons.remove(at: index) + updateButtons() + } + } + + public func removeAllButtons() { + buttons.removeAll() + updateButtons() + } + + // MARK: - Private Methods + private func updateLayout() { + switch layout { + case .horizontal: + stackView.axis = .horizontal + stackView.distribution = .fillProportionally + stackView.alignment = .fill + buttons.forEach { $0.isFullWidth = false } + + case .vertical: + stackView.axis = .vertical + stackView.distribution = .fill + stackView.alignment = .fill + buttons.forEach { $0.isFullWidth = true } + + case .fullWidth: + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.alignment = .fill + buttons.forEach { $0.isFullWidth = true } + } + + stackView.spacing = spacing + } + + private func updateButtons() { + stackView.arrangedSubviews.forEach { view in + stackView.removeArrangedSubview(view) + view.removeFromSuperview() + } + + buttons.forEach { button in + button.setContentHuggingPriority(.required, for: .horizontal) + stackView.addArrangedSubview(button) + } + + updateLayout() + } + +} + +// MARK: - Convenience Initializers +extension BKButtonGroup { + public static func twoButtonGroup( + leftTitle: String = "ํ™•์ธ", + rightTitle: String = "์ทจ์†Œ", + leftAction: (() -> Void)? = nil, + rightAction: (() -> Void)? = nil + ) -> BKButtonGroup { + let leftButton = BKButton.secondary(title: leftTitle) + let rightButton = BKButton.primary(title: rightTitle) + + if let action = leftAction { + leftButton.addAction(UIAction { _ in action() }, for: .touchUpInside) + } + + if let action = rightAction { + rightButton.addAction(UIAction { _ in action() }, for: .touchUpInside) + } + + return BKButtonGroup(buttons: [leftButton, rightButton], layout: .fullWidth) + } + + public static func singleFullButton( + title: String = "๋‹ค์Œ", + action: (() -> Void)? = nil + ) -> BKButtonGroup { + let nextButton = BKButton.primary(title: title, size: .large) + + if let action = action { + nextButton.addAction(UIAction { _ in action() }, for: .touchUpInside) + } + + return BKButtonGroup(buttons: [nextButton], layout: .vertical) + } + + /// 3๊ฐœ ๋ฒ„ํŠผ ์ˆ˜ํ‰ ๊ทธ๋ฃน + public static func threeButtonGroup( + leftTitle: String, + centerTitle: String, + rightTitle: String, + leftAction: (() -> Void)? = nil, + centerAction: (() -> Void)? = nil, + rightAction: (() -> Void)? = nil + ) -> BKButtonGroup { + let leftButton = BKButton.tertiary(title: leftTitle) + let centerButton = BKButton.secondary(title: centerTitle) + let rightButton = BKButton.primary(title: rightTitle) + + if let action = leftAction { + leftButton.addAction(UIAction { _ in action() }, for: .touchUpInside) + } + + if let action = centerAction { + centerButton.addAction(UIAction { _ in action() }, for: .touchUpInside) + } + + if let action = rightAction { + rightButton.addAction(UIAction { _ in action() }, for: .touchUpInside) + } + + return BKButtonGroup(buttons: [leftButton, centerButton, rightButton], layout: .horizontal) + } + + /// ์ˆ˜์ง ๋ฒ„ํŠผ ๊ทธ๋ฃน + /// + /// - Warning: ์ž…๋ ฅ๋œ ๋ฒ„ํŠผ๋“ค์˜ isFullWidth ์†์„ฑ์ด true๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. + public static func verticalButtonGroup( + buttons: [BKButton], + spacing: CGFloat = BKSpacing.spacing2 + ) -> BKButtonGroup { + buttons.forEach { $0.isFullWidth = true } + return BKButtonGroup(buttons: buttons, layout: .vertical, spacing: spacing) + } +} diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButtonProtocol.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButtonProtocol.swift new file mode 100644 index 00000000..5bc3fff5 --- /dev/null +++ b/src/Projects/BKDesign/Sources/Components/Button/BKButtonProtocol.swift @@ -0,0 +1,34 @@ +// Copyright ยฉ 2025 Booket. All rights reserved + +import UIKit + +/// BKButton ์Šคํƒ€์ผ ๋ฒ„ํŠผ์„ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. +/// +/// ์ด ํ”„๋กœํ† ์ฝœ์€ ๋ฒ„ํŠผ์˜ ์ƒํƒœ, ์Šคํƒ€์ผ, ํฌ๊ธฐ, ํ…์ŠคํŠธ ๋ฐ ์•„์ด์ฝ˜ ์†์„ฑ์„ ์ •์˜ํ•˜์—ฌ +/// ์ผ๊ด€๋œ UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค. +protocol BKButtonProtocol: AnyObject { + + /// ๋ฒ„ํŠผ์˜ ํ™œ์„ฑํ™” ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + /// + /// `true`์ธ ๊ฒฝ์šฐ ๋ฒ„ํŠผ์€ ๋น„ํ™œ์„ฑํ™” ์ƒํƒœ์ด๋ฉฐ, ์‚ฌ์šฉ์ž์™€์˜ ์ƒํ˜ธ์ž‘์šฉ์ด ์ฐจ๋‹จ๋ฉ๋‹ˆ๋‹ค. + var isDisabled: Bool { get set } + + /// ๋ฒ„ํŠผ์˜ ์Šคํƒ€์ผ์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. + /// + /// ์Šคํƒ€์ผ์— ๋”ฐ๋ผ ๋ฐฐ๊ฒฝ์ƒ‰, ํ…์ŠคํŠธ ์ƒ‰์ƒ ๋“ฑ์ด ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค. + var style: BKButtonStyle { get set } + + /// ๋ฒ„ํŠผ์˜ ํฌ๊ธฐ๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. + /// + /// ํฌ๊ธฐ์— ๋”ฐ๋ผ ๋†’์ด, ํฐํŠธ, ํŒจ๋”ฉ ๊ฐ’ ๋“ฑ์ด ์กฐ์ •๋ฉ๋‹ˆ๋‹ค. + var size: BKButtonSize { get set } + + /// ๋ฒ„ํŠผ์— ํ‘œ์‹œ๋  ํ…์ŠคํŠธ์ž…๋‹ˆ๋‹ค. + var title: String? { get set } + + /// ๋ฒ„ํŠผ์˜ ์™ผ์ชฝ์— ํ‘œ์‹œ๋  ์•„์ด์ฝ˜ ์ด๋ฏธ์ง€์ž…๋‹ˆ๋‹ค. + var leftIcon: UIImage? { get set } + + /// ๋ฒ„ํŠผ์˜ ์˜ค๋ฅธ์ชฝ์— ํ‘œ์‹œ๋  ์•„์ด์ฝ˜ ์ด๋ฏธ์ง€์ž…๋‹ˆ๋‹ค. + var rightIcon: UIImage? { get set } +} diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButtonSize.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButtonSize.swift new file mode 100644 index 00000000..f6f00ad7 --- /dev/null +++ b/src/Projects/BKDesign/Sources/Components/Button/BKButtonSize.swift @@ -0,0 +1,121 @@ +// Copyright ยฉ 2025 Booket. All rights reserved + +import UIKit + +/// `BKButton`์˜ ํฌ๊ธฐ ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. +/// +/// ๋ฒ„ํŠผ์˜ ๋†’์ด, ํŒจ๋”ฉ, ํฐํŠธ, ์•„์ด์ฝ˜ ํฌ๊ธฐ, ๋ชจ์„œ๋ฆฌ ๋ฐ˜๊ฒฝ ๋“ฑ ๋ ˆ์ด์•„์›ƒ ๊ด€๋ จ ์†์„ฑ์— ์˜ํ–ฅ์„ ์ค๋‹ˆ๋‹ค. +public enum BKButtonSize { + /// ์ž‘์€ ๋ฒ„ํŠผ + case small + + /// ์ค‘๊ฐ„ ํฌ๊ธฐ ๋ฒ„ํŠผ + case medium + + /// ๋Œ€ํ˜• ๋ฒ„ํŠผ + case large + + /// ๋‘ฅ๊ทผ ๋ฒ„ํŠผ + case rounded + + /// ๋ฒ„ํŠผ ๋†’์ด ๊ฐ’ + var height: CGFloat { + switch self { + case .small, .rounded: + 40 + case .medium: + 48 + case .large: + 52 + } + } + + /// ๋ฒ„ํŠผ ์ขŒ์šฐ ํŒจ๋”ฉ ๊ฐ’ + var horizontalPadding: CGFloat { + switch self { + case .small, .rounded: + BKSpacing.spacing3 + case .medium: + BKSpacing.spacing4 + case .large: + BKSpacing.spacing5 + } + } + + /// ๋ฒ„ํŠผ ์ƒํ•˜ ํŒจ๋”ฉ ๊ฐ’ + var verticalPadding: CGFloat { + switch self { + case .small, .rounded: + BKSpacing.spacing2 + case .medium, .large: + BKSpacing.spacing3 + } + } + + /// ๋ฒ„ํŠผ ํ…์ŠคํŠธ์— ์‚ฌ์šฉํ•  ํฐํŠธ + var font: UIFont { + switch self { + case .rounded, .small, .medium: + BKTextStyle + .label1(weight: .medium).uiFont ?? + .systemFont( + ofSize: BKTextStyle.label1(weight: .medium).fontAttributes.fontSize.rawValue, + weight: .medium + ) + case .large: + BKTextStyle + .body1(weight: .medium).uiFont ?? + .systemFont( + ofSize: BKTextStyle.body1(weight: .medium).fontAttributes.fontSize.rawValue, + weight: .medium + ) + } + } + + /// ์•„์ด์ฝ˜์˜ ํฌ๊ธฐ + var iconSize: CGSize { + switch self { + case .large: + CGSize(width: 24, height: 24) + default: + CGSize(width: 22, height: 22) + } + } + + /// ์•„์ด์ฝ˜๊ณผ ํ…์ŠคํŠธ ์‚ฌ์ด์˜ ๊ฐ„๊ฒฉ + var iconSpacing: CGFloat { + switch self { + case .rounded, .small, .medium: + BKSpacing.spacing1 + case .large: + BKSpacing.spacing2 + } + } + + /// ๊ธฐ๋ณธ ๋ชจ์„œ๋ฆฌ ๋ฐ˜๊ฒฝ + var cornerRadius: CGFloat { + switch self { + case .small: + BKRadius.xsmall + case .medium, .large: + BKRadius.small + case .rounded: + BKRadius.full + } + } + + /// ๋ฒ„ํŠผ์˜ ๋„“์ด๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ณ„์‚ฐ๋œ ๋‘ฅ๊ทผ corner radius ๋ฐ˜ํ™˜ + /// + /// - Parameter width: ๋ฒ„ํŠผ์˜ ์‹ค์ œ ๋„“์ด + /// - Returns: radius ๊ฐ’ + public func cornerRadius(for width: CGFloat) -> CGFloat { + switch self { + case .small: + return BKRadius.xsmall + case .medium, .large: + return BKRadius.small + case .rounded: + return width / 2 + } + } +} diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButtonState.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButtonState.swift new file mode 100644 index 00000000..41f0be73 --- /dev/null +++ b/src/Projects/BKDesign/Sources/Components/Button/BKButtonState.swift @@ -0,0 +1,33 @@ +// Copyright ยฉ 2025 Booket. All rights reserved + +import UIKit + +/// `BKButton`์˜ ์‹œ๊ฐ์  ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. +/// +/// ๋ฒ„ํŠผ์˜ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ ๋ฐ ๊ฐ•์กฐ ์ƒํƒœ์— ๋”ฐ๋ผ ๊ฒฐ์ •๋˜๋ฉฐ, ์Šคํƒ€์ผ ๋ Œ๋”๋ง์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. +public enum BKButtonState { + /// ๊ธฐ๋ณธ(normal) ์ƒํƒœ + case normal + + /// ํ„ฐ์น˜ ๊ฐ•์กฐ(pressed) ์ƒํƒœ + case pressed + + /// ๋น„ํ™œ์„ฑํ™”(disabled) ์ƒํƒœ + case disabled + + /// ๋ฒ„ํŠผ์˜ `isEnabled`์™€ `isHighlighted` ์†์„ฑ์„ ๋ฐ”ํƒ•์œผ๋กœ + /// ์ ์ ˆํ•œ ์ƒํƒœ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + /// + /// - Parameters: + /// - isEnabled: ๋ฒ„ํŠผ์ด ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋Š”์ง€ ์—ฌ๋ถ€ + /// - isHighlighted: ๋ฒ„ํŠผ์ด ๊ฐ•์กฐ ์ƒํƒœ์ธ์ง€ ์—ฌ๋ถ€ + public init(isEnabled: Bool, isHighlighted: Bool) { + if !isEnabled { + self = .disabled + } else if isHighlighted { + self = .pressed + } else { + self = .normal + } + } +} diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButtonStyle.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButtonStyle.swift new file mode 100644 index 00000000..c59dbdea --- /dev/null +++ b/src/Projects/BKDesign/Sources/Components/Button/BKButtonStyle.swift @@ -0,0 +1,123 @@ +// Copyright ยฉ 2025 Booket. All rights reserved + +import UIKit + +/// ๋ฒ„ํŠผ์˜ ์‹œ๊ฐ์  ์Šคํƒ€์ผ์„ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. +/// +/// ๊ฐ ์Šคํƒ€์ผ์€ ๊ณ ์œ ํ•œ ๋ฐฐ๊ฒฝ์ƒ‰ ๋ฐ ํ…์ŠคํŠธ ์ƒ‰์ƒ์„ ๊ฐ€์ง€๋ฉฐ, +/// ๋ฒ„ํŠผ์˜ ๋ชฉ์ ๊ณผ ์‚ฌ์šฉ ๋งฅ๋ฝ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์‹œ๊ฐ์  ํ‘œํ˜„์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. +public enum BKButtonStyle: Equatable { + /// ๊ฐ•ํ•œ ๊ฐ•์กฐ์˜ ๊ธฐ๋ณธ ๋ฒ„ํŠผ ์Šคํƒ€์ผ + case primary + + /// ์ค‘๊ฐ„ ๊ฐ•์กฐ์˜ ๋ณด์กฐ ๋ฒ„ํŠผ ์Šคํƒ€์ผ + case secondary + + /// ๊ฐ€์žฅ ๋‚ฎ์€ ๊ฐ•์กฐ์˜ ํ…์ŠคํŠธ ์ค‘์‹ฌ ์Šคํƒ€์ผ + case tertiary + + /// ์ง์ ‘ ์ƒ‰์ƒ์„ ์ •์˜ํ•˜๋Š” ์ปค์Šคํ…€ ์Šคํƒ€์ผ + case custom(background: BKButtonColorSet, foreground: BKButtonColorSet) + + /// ์Šคํƒ€์ผ์— ๋”ฐ๋ฅธ ๋ฐฐ๊ฒฝ์ƒ‰ ์„ธํŠธ์ž…๋‹ˆ๋‹ค. + /// + /// ๋ฒ„ํŠผ ์ƒํƒœ๋ณ„ ์ƒ‰์ƒ(normal, pressed, disabled)์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + var backgroundColors: BKButtonColorSet { + switch self { + case .primary: + return BKButtonColorSet( + normal: .bkBackgroundColor(.primary), + pressed: .bkBackgroundColor(.primaryPressed), + disabled: .bkBackgroundColor(.disable) + ) + + case .secondary: + return BKButtonColorSet( + normal: .bkBackgroundColor(.secondary), + pressed: .bkBackgroundColor(.secondaryPressed), + disabled: .bkBackgroundColor(.disable) + ) + + case .tertiary: + return BKButtonColorSet( + normal: .bkBackgroundColor(.tertiary), + pressed: .bkBackgroundColor(.tertiaryPressed), + disabled: .bkBackgroundColor(.disable) + ) + + case .custom(let background, _): + return background + } + } + + /// ์Šคํƒ€์ผ์— ๋”ฐ๋ฅธ ํ…์ŠคํŠธ ์ƒ‰์ƒ ์„ธํŠธ์ž…๋‹ˆ๋‹ค. + /// + /// ๋ฒ„ํŠผ ์ƒํƒœ๋ณ„ ์ƒ‰์ƒ(normal, pressed, disabled)์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + var foregroundColors: BKButtonColorSet { + switch self { + case .primary: + return BKButtonColorSet( + normal: .bkContentColor(.inverse), + pressed: .bkContentColor(.inverse), + disabled: .bkContentColor(.disable) + ) + + case .secondary: + return BKButtonColorSet( + normal: .bkContentColor(.primary), + pressed: .bkContentColor(.primary), + disabled: .bkContentColor(.disable) + ) + + case .tertiary: + return BKButtonColorSet( + normal: .bkContentColor(.brand), + pressed: .bkContentColor(.brand), + disabled: .bkContentColor(.disable) + ) + + case .custom(_, let foreground): + return foreground + } + } +} + +/// ๋ฒ„ํŠผ์˜ ์ƒํƒœ๋ณ„ ์ƒ‰์ƒ ์„ธํŠธ๋ฅผ ์ •์˜ํ•˜๋Š” ๊ตฌ์กฐ์ฒด์ž…๋‹ˆ๋‹ค. +public struct BKButtonColorSet { + /// ๊ธฐ๋ณธ(normal) ์ƒํƒœ์—์„œ์˜ ์ƒ‰์ƒ + let normal: UIColor + + /// ๋ˆŒ๋ฆผ(pressed) ์ƒํƒœ์—์„œ์˜ ์ƒ‰์ƒ + let pressed: UIColor + + /// ๋น„ํ™œ์„ฑํ™”(disabled) ์ƒํƒœ์—์„œ์˜ ์ƒ‰์ƒ + let disabled: UIColor + + /// ๋ฒ„ํŠผ์˜ ์ƒํƒœ์— ๋งž๋Š” ์ƒ‰์ƒ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + /// + /// - Parameter state: ๋ฒ„ํŠผ์˜ ํ˜„์žฌ ์ƒํƒœ + /// - Returns: ํ•ด๋‹น ์ƒํƒœ์— ๋งž๋Š” ์ƒ‰์ƒ + public func color(for state: BKButtonState) -> UIColor { + switch state { + case .normal: return normal + case .pressed: return pressed + case .disabled: return disabled + } + } + + /// ๋‹จ์ผ ์ƒ‰์ƒ์„ ๋ชจ๋“  ์ƒํƒœ์— ์ ์šฉํ•˜๋Š” ๊ฐ„๋‹จ ์ƒ์„ฑ์ž + public static func solid(_ color: UIColor) -> BKButtonColorSet { + BKButtonColorSet(normal: color, pressed: color, disabled: color) + } +} + +extension BKButtonColorSet: Equatable { + public static func == ( + lhs: BKButtonColorSet, + rhs: BKButtonColorSet + ) -> Bool { + return lhs.normal.isEqual(to: rhs.normal) && + lhs.pressed.isEqual(to: rhs.pressed) && + lhs.disabled.isEqual(to: rhs.disabled) + } +} diff --git a/src/Projects/BKDesign/Sources/Extensions/SetNeeds.swift b/src/Projects/BKDesign/Sources/Extensions/SetNeeds.swift new file mode 100644 index 00000000..72e02987 --- /dev/null +++ b/src/Projects/BKDesign/Sources/Extensions/SetNeeds.swift @@ -0,0 +1,57 @@ +// Copyright ยฉ 2025 Booket. All rights reserved + +import Foundation +import UIKit + +@propertyWrapper +public struct SetNeeds { + enum Need { + case layout + case display + } + + private var value: Value + private let needs: Set + private weak var view: UIView? + + // ์ดˆ๊ธฐํ™” + init( + wrappedValue: Value, + _ needs: Need... + ) { + self.value = wrappedValue + self.needs = Set(needs) + self.view = nil + } + + // ์‹ค์ œ ๊ฐ’์— ์ ‘๊ทผํ•  ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ํ”„๋กœํผํ‹ฐ + public var wrappedValue: Value { + get { value } + set { + let oldValue = value + value = newValue + + // ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ๋งŒ UI ์—…๋ฐ์ดํŠธ + if oldValue != newValue { + updateView() + } + } + } + + // ๋ทฐ๋ฅผ ์„ค์ •ํ•˜๋Š” ๋ฉ”์„œ๋“œ + public mutating func configure(with view: UIView) { + self.view = view + } + + private func updateView() { + guard let view = view else { return } + + // needs์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ + if needs.contains(.layout) { + view.setNeedsLayout() // ๋ ˆ์ด์•„์›ƒ ์žฌ๊ณ„์‚ฐ ํ•„์š” + } + if needs.contains(.display) { + view.setNeedsDisplay() // ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ ํ•„์š” + } + } +} diff --git a/src/Projects/BKDesign/Sources/Extensions/UIColor+.swift b/src/Projects/BKDesign/Sources/Extensions/UIColor+.swift index eb718ee9..f31e15e0 100644 --- a/src/Projects/BKDesign/Sources/Extensions/UIColor+.swift +++ b/src/Projects/BKDesign/Sources/Extensions/UIColor+.swift @@ -101,3 +101,10 @@ public extension UIColor { } } } + +// UIColor ๋น„๊ต๋ฅผ ์œ„ํ•œ ํ™•์žฅ +extension UIColor { + func isEqual(to color: UIColor) -> Bool { + return self.cgColor.__equalTo(color.cgColor) + } +} diff --git a/src/Projects/BKDesign/Sources/Extensions/UIImage+.swift b/src/Projects/BKDesign/Sources/Extensions/UIImage+.swift new file mode 100644 index 00000000..70b55730 --- /dev/null +++ b/src/Projects/BKDesign/Sources/Extensions/UIImage+.swift @@ -0,0 +1,17 @@ +// Copyright ยฉ 2025 Booket. All rights reserved + +import UIKit + +extension UIImage { + /// Asset ์ด๋ฏธ์ง€๋ฅผ ๋ฒ„ํŠผ ํฌ๊ธฐ์— ๋งž๊ฒŒ ์กฐ์ • + func resize(to targetSize: CGSize) -> UIImage? { + let renderer = UIGraphicsImageRenderer( + size: targetSize, + format: UIGraphicsImageRendererFormat.preferred() + ) + + return renderer.image { context in + self.draw(in: CGRect(origin: .zero, size: targetSize)) + } + } +}