diff --git a/Handy/Handy-Storybook/Atom/FabViewController.swift b/Handy/Handy-Storybook/Atom/FabViewController.swift new file mode 100644 index 0000000..3582bb6 --- /dev/null +++ b/Handy/Handy-Storybook/Atom/FabViewController.swift @@ -0,0 +1,83 @@ +// +// FabViewController.swift +// Handy-Storybook +// +// Created by 정민지 on 7/31/24. +// + +import Handy + +final class FabViewController: BaseViewController { + + let primaryFab: HandyFab = { + let checkBox = HandyFab() + return checkBox + }() + + let disabledPrimaryFab: HandyFab = { + let checkBox = HandyFab() + checkBox.isDisabled = true + return checkBox + }() + + let secondaryFab: HandyFab = { + let checkBox = HandyFab() + checkBox.type = .secondary + checkBox.size = .small + return checkBox + }() + + let disabledSecondaryFab: HandyFab = { + let checkBox = HandyFab() + checkBox.type = .secondary + checkBox.size = .small + checkBox.isDisabled = true + return checkBox + }() + + let customIconFab: HandyFab = { + let checkBox = HandyFab() + checkBox.size = .small + checkBox.iconImage = .add + return checkBox + }() + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func setViewHierarchies() { + [ + primaryFab, + disabledPrimaryFab, + secondaryFab, + disabledSecondaryFab, + customIconFab + ].forEach { + view.addSubview($0) + } + } + + override func setViewLayouts() { + primaryFab.snp.makeConstraints { + $0.center.equalToSuperview() + } + disabledPrimaryFab.snp.makeConstraints { + $0.top.equalTo(primaryFab.snp.bottom).offset(16) + $0.centerX.equalToSuperview() + } + secondaryFab.snp.makeConstraints { + $0.top.equalTo(disabledPrimaryFab.snp.bottom).offset(16) + $0.centerX.equalToSuperview() + } + disabledSecondaryFab.snp.makeConstraints { + $0.top.equalTo(secondaryFab.snp.bottom).offset(16) + $0.centerX.equalToSuperview() + } + customIconFab.snp.makeConstraints { + $0.top.equalTo(disabledSecondaryFab.snp.bottom).offset(16) + $0.centerX.equalToSuperview() + } + } +} + diff --git a/Handy/Handy.xcodeproj/project.pbxproj b/Handy/Handy.xcodeproj/project.pbxproj index d12ca7c..9fe96ec 100644 --- a/Handy/Handy.xcodeproj/project.pbxproj +++ b/Handy/Handy.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 029E48002C49FD4000D2F3B7 /* HandyTypography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029E47FF2C49FD4000D2F3B7 /* HandyTypography.swift */; }; 02BDB7F32C3E95580050FB67 /* Handy.h in Headers */ = {isa = PBXBuildFile; fileRef = 02BDB7F22C3E95580050FB67 /* Handy.h */; settings = {ATTRIBUTES = (Public, ); }; }; 02BDB7FC2C3E99920050FB67 /* HandyFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BDB7FB2C3E99920050FB67 /* HandyFont.swift */; }; + 2D41E8142C5A21930043161D /* FabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D41E8132C5A21930043161D /* FabViewController.swift */; }; + 2D41E8162C5A21B50043161D /* HandyFab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D41E8152C5A21B50043161D /* HandyFab.swift */; }; 02ED76312C5284BB001569F1 /* HandyButtonProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ED76302C5284BB001569F1 /* HandyButtonProtocol.swift */; }; 02ED76332C5284E6001569F1 /* HandyBoxButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ED76322C5284E6001569F1 /* HandyBoxButton.swift */; }; 02ED76352C5284F3001569F1 /* HandyTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ED76342C5284F3001569F1 /* HandyTextButton.swift */; }; @@ -84,6 +86,8 @@ 02BDB7EF2C3E95580050FB67 /* Handy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Handy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 02BDB7F22C3E95580050FB67 /* Handy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Handy.h; sourceTree = ""; }; 02BDB7FB2C3E99920050FB67 /* HandyFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyFont.swift; sourceTree = ""; }; + 2D41E8132C5A21930043161D /* FabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FabViewController.swift; sourceTree = ""; }; + 2D41E8152C5A21B50043161D /* HandyFab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyFab.swift; sourceTree = ""; }; 02ED76302C5284BB001569F1 /* HandyButtonProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyButtonProtocol.swift; sourceTree = ""; }; 02ED76322C5284E6001569F1 /* HandyBoxButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyBoxButton.swift; sourceTree = ""; }; 02ED76342C5284F3001569F1 /* HandyTextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyTextButton.swift; sourceTree = ""; }; @@ -134,6 +138,7 @@ isa = PBXGroup; children = ( 025776382C4EA98C00272EC6 /* LabelViewController.swift */, + 2D41E8132C5A21930043161D /* FabViewController.swift */, 02ED764B2C57BD09001569F1 /* HandyBoxButtonViewController.swift */, ); path = Atom; @@ -188,6 +193,7 @@ children = ( 02ED762F2C52849A001569F1 /* HandyButton */, 029E47FC2C49FD1A00D2F3B7 /* HandyLabel.swift */, + 2D41E8152C5A21B50043161D /* HandyFab.swift */, ); path = Atom; sourceTree = ""; @@ -401,6 +407,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2D41E8142C5A21930043161D /* FabViewController.swift in Sources */, 025776392C4EA98C00272EC6 /* LabelViewController.swift in Sources */, 0257765D2C4EB9EF00272EC6 /* BaseViewController.swift in Sources */, 02ED764C2C57BD09001569F1 /* HandyBoxButtonViewController.swift in Sources */, @@ -413,6 +420,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2D41E8162C5A21B50043161D /* HandyFab.swift in Sources */, 02ED76332C5284E6001569F1 /* HandyBoxButton.swift in Sources */, E5D02AFD2C46C5A70056CE7B /* HandySematic.swift in Sources */, E5D02B002C480A180056CE7B /* HandyPrimitive.swift in Sources */, diff --git a/Handy/Handy/Source/Atom/HandyFab.swift b/Handy/Handy/Source/Atom/HandyFab.swift new file mode 100644 index 0000000..26efbf0 --- /dev/null +++ b/Handy/Handy/Source/Atom/HandyFab.swift @@ -0,0 +1,217 @@ +// +// HandyFab.swift +// Handy +// +// Created by 정민지 on 7/31/24. +// + +import UIKit + +public class HandyFab: UIButton { + + // MARK: - 외부에서 지정할 수 있는 속성 + + /** + Fab 버튼을 비활성화 시킬 때 사용합니다. + */ + @Invalidating(.layout, .display) public var isDisabled: Bool = false { + didSet { + setConfiguration() + } + } + + /** + Fab 버튼의 Pressed / Hover 여부를 나타낼 때 사용합니다. + */ + public override var isHighlighted: Bool { + didSet { setConfiguration() } + } + + /** + Fab 버튼의 타입을 결정할 때 사용합니다. + */ + @Invalidating(.layout, .display) public var type: FabType = .primary { + didSet { + setConfiguration() + } + } + + /** + Fab 버튼의 크기를 결정할 때 사용합니다. + */ + @Invalidating(.layout, .display) public var size: FabSize = .large { + didSet { + setConfiguration() + } + } + + /** + Fab 버튼의 아이콘을 설정할 때 사용합니다. + */ + public var iconImage: UIImage? { + didSet { + setConfiguration() + } + } + + // MARK: - 외부에서 접근할 수 있는 enum + + /** + Fab 버튼의 type 종류입니다. + 각 type에 맞는 borderColor, backgroundColor를 computed property로 가지고 있습니다. + */ + public enum FabType { + case primary + case secondary + + var backgroundColor: UIColor { + switch self { + case .primary: + return HandySemantic.buttonFabPrimaryEnabled + case .secondary: + return HandySemantic.buttonFabSecondaryEnabled + } + } + + var highlightedColor: UIColor { + switch self { + case .primary: + return HandySemantic.buttonFabPrimaryPressed + case .secondary: + return HandySemantic.buttonFabSecondaryPressed + } + } + + var disabledColor: UIColor { + switch self { + case .primary: + return HandySemantic.buttonFabPrimaryDisabled + case .secondary: + return HandySemantic.buttonFabSecondaryDisabled + } + } + + var borderColor: UIColor { + switch self { + case .primary: + return .clear + case .secondary: + return HandySemantic.lineBasicLight + } + } + + var shadowColor: UIColor { + switch self { + case .primary: + return UIColor( + red: 110/255, + green: 118/255, + blue: 135/255, + alpha: 0.25 + ) + case .secondary: + return UIColor( + red: 181/255, + green: 185/255, + blue: 196/255, + alpha: 0.25 + ) + } + } + } + + /** + Fab 버튼의 size 종류입니다. + 각 size에 맞는 icon을 computed property로 가지고 있습니다. + */ + public enum FabSize { + case small + case large + + fileprivate var fabSize: CGFloat { + switch self { + case .small: + return 40 + case .large: + return 56 + } + } + + fileprivate var icon: String { + switch self { + case .small: + return "arrow.up" + case .large: + return "square.and.arrow.up" + } + } + } + + // MARK: - 메소드 + + public init() { + super.init(frame: .zero) + setConfiguration() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setConfiguration() { + NSLayoutConstraint.activate([ + widthAnchor.constraint(equalToConstant: size.fabSize), + heightAnchor.constraint(equalToConstant: size.fabSize) + ]) + self.layer.cornerRadius = size.fabSize / 2 + self.layer.borderWidth = type == .secondary ? 1 : 0 + + var configuration = UIButton.Configuration.plain() + + let iconConfig = UIImage.SymbolConfiguration(pointSize: 24, weight: .regular) + let iconImage = iconImage == nil ? UIImage( + systemName: size.icon, + withConfiguration: iconConfig + ): iconImage + +// TODO: - HandyIcon으로 아이콘 변경, pointSize 삭제, resize 적용(아래 코드로 교체) +// var iconConfig = iconImage == nil ? UIImage(systemName: size.icon): iconImage +// iconConfig = iconConfig?.resize(to: 24) + + + var iconColor: UIColor = type == .secondary ? HandySemantic.iconBasicTertiary : HandySemantic.iconBasicWhite + switch (isDisabled, isHighlighted) { + case (true, _): + self.backgroundColor = type.disabledColor + self.layer.borderColor = type.borderColor.cgColor + iconColor = type == .secondary ? HandySemantic.iconBasicDisabled : HandySemantic.iconBasicWhite + removeShadow() + + case (false, true): + self.backgroundColor = type.highlightedColor + self.layer.borderColor = type.borderColor.cgColor + applyShadow() + + case (false, false): + self.backgroundColor = type.backgroundColor + self.layer.borderColor = type.borderColor.cgColor + applyShadow() + } + + configuration.image = iconImage?.withTintColor(iconColor, renderingMode: .alwaysOriginal) + + self.isEnabled = !isDisabled + self.configuration = configuration + } + + private func applyShadow() { + layer.shadowColor = type.shadowColor.cgColor + layer.shadowOffset = CGSize(width: 0, height: 4) + layer.shadowRadius = 8 + layer.shadowOpacity = 1.0 + } + + private func removeShadow() { + layer.shadowOpacity = 0.0 + } +}