diff --git a/Handy/Handy-Storybook/Atom/TabsViewController.swift b/Handy/Handy-Storybook/Atom/TabsViewController.swift new file mode 100644 index 0000000..1f1b09a --- /dev/null +++ b/Handy/Handy-Storybook/Atom/TabsViewController.swift @@ -0,0 +1,99 @@ +// +// TabsViewController.swift +// Handy +// +// Created by chongin on 12/29/24. +// + +import Handy +import UIKit + +final class TabsViewController: BaseViewController { + var tabs: [(title: String, viewController: UIViewController)] + + private let handyTabs: HandyTabs = { + let tabs = HandyTabs(sizeType: .small) + return tabs + }() + + private let addingTabButton: HandyFab = { + let button = HandyFab() + button.iconImage = .add + return button + }() + + init(_ tabCount: Int) { + self.tabs = [ + { + let viewController = SnackbarViewController() + return ("SnackbarViewController", viewController) + }(), + { + let viewController = LabelViewController() + return ("LabelViewController", viewController) + }(), + { + let viewController = FabViewController() + return ("FabViewController", viewController) + }(), + { + let viewController = HandyBoxButtonViewController() + return ("HandyBoxButtonViewController", viewController) + }(), + { + let viewController = ChipViewController() + return ("ChipViewController", viewController) + }(), + { + let viewController = DividerViewController() + return ("DividerViewController", viewController) + }(), + { + let viewController = CheckBoxViewController() + return ("CheckBoxViewController", viewController) + }(), + { + let viewController = RadioButtonViewController() + return ("RadioButtonViewController", viewController) + }(), + { + let viewController = HansySwitchViewController() + return ("HansySwitchViewController", viewController) + }(), + ][.. Int { + switch section { + case 0: + return components.count + default: + return 0 + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch indexPath.section { + case 0: + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + let component = components[indexPath.row] + cell.textLabel?.text = component.title + return cell + default: + fatalError() + } + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch indexPath.section { + case 0: + let component = components[indexPath.row] + navigationController?.pushViewController(component.viewController, animated: true) + default: + fatalError() + } + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch section { + case 0: + return "Components" + default: + fatalError() + } + } +} + +struct HandyStorybookComponent { + let title: String + let viewController: UIViewController + + init(title: String, viewController: UIViewController) { + self.title = title + self.viewController = viewController + } + + init(_ component: (title: String, viewController: VC)) { + self.title = component.title + self.viewController = component.viewController as UIViewController + } +} diff --git a/Handy/Handy.xcodeproj/project.pbxproj b/Handy/Handy.xcodeproj/project.pbxproj index c13e8fa..6944502 100644 --- a/Handy/Handy.xcodeproj/project.pbxproj +++ b/Handy/Handy.xcodeproj/project.pbxproj @@ -41,6 +41,11 @@ 02ED764C2C57BD09001569F1 /* HandyBoxButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ED764B2C57BD09001569F1 /* HandyBoxButtonViewController.swift */; }; 2D41E8142C5A21930043161D /* FabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D41E8132C5A21930043161D /* FabViewController.swift */; }; 2D41E8162C5A21B50043161D /* HandyFab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D41E8152C5A21B50043161D /* HandyFab.swift */; }; + 6F1A18D02D26606D004C6083 /* CALayer+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F1A18CF2D26606A004C6083 /* CALayer+.swift */; }; + 6FD1A1802D213129001E6F2E /* TabsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1A17F2D21311C001E6F2E /* TabsViewController.swift */; }; + 6FE73E942D23C52F00E06422 /* HandyTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE73E932D23C52C00E06422 /* HandyTabs.swift */; }; + 6FE73E962D23C55900E06422 /* HandyTabCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE73E952D23C55900E06422 /* HandyTabCell.swift */; }; + 6FE73E982D23D7A900E06422 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE73E972D23D6E500E06422 /* MainViewController.swift */; }; A56B3DE22C4E51D300C3610A /* HandyChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56B3DE12C4E51D300C3610A /* HandyChip.swift */; }; A5A12A7E2C57A6D900996916 /* ChipViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A12A7C2C57A6C200996916 /* ChipViewController.swift */; }; A5A12A7F2C57A92000996916 /* HandySematic.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D02AFC2C46C5A70056CE7B /* HandySematic.swift */; }; @@ -121,6 +126,11 @@ 02ED764B2C57BD09001569F1 /* HandyBoxButtonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyBoxButtonViewController.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 = ""; }; + 6F1A18CF2D26606A004C6083 /* CALayer+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+.swift"; sourceTree = ""; }; + 6FD1A17F2D21311C001E6F2E /* TabsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsViewController.swift; sourceTree = ""; }; + 6FE73E932D23C52C00E06422 /* HandyTabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyTabs.swift; sourceTree = ""; }; + 6FE73E952D23C55900E06422 /* HandyTabCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyTabCell.swift; sourceTree = ""; }; + 6FE73E972D23D6E500E06422 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; A56B3DE12C4E51D300C3610A /* HandyChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyChip.swift; sourceTree = ""; }; A5A12A7C2C57A6C200996916 /* ChipViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewController.swift; sourceTree = ""; }; A5F6D36A2C96F32D00FB961F /* HandyDivider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyDivider.swift; sourceTree = ""; }; @@ -183,6 +193,7 @@ 025776482C4EB0E700272EC6 /* Atom */ = { isa = PBXGroup; children = ( + 6FD1A17F2D21311C001E6F2E /* TabsViewController.swift */, 02150E4B2CCABAE500EE690E /* SnackbarViewController.swift */, 025776382C4EA98C00272EC6 /* LabelViewController.swift */, 2D41E8132C5A21930043161D /* FabViewController.swift */, @@ -217,6 +228,7 @@ 0257765A2C4EB9B800272EC6 /* Storybook */ = { isa = PBXGroup; children = ( + 6FE73E972D23D6E500E06422 /* MainViewController.swift */, 0257763F2C4EA98E00272EC6 /* LaunchScreen.storyboard */, 0257765B2C4EB9C700272EC6 /* Base */, ); @@ -244,7 +256,8 @@ 029E47FE2C49FD2E00D2F3B7 /* Atom */ = { isa = PBXGroup; children = ( - 02ED762F2C52849A001569F1 /* Button */, + 6FE73E922D23C51600E06422 /* HandyTabs */, + 02ED762F2C52849A001569F1 /* HandyButton */, 029E47FC2C49FD1A00D2F3B7 /* HandyLabel.swift */, 2D41E8152C5A21B50043161D /* HandyFab.swift */, A56B3DE12C4E51D300C3610A /* HandyChip.swift */, @@ -325,11 +338,21 @@ 02ED76482C577998001569F1 /* Extension */ = { isa = PBXGroup; children = ( + 6F1A18CF2D26606A004C6083 /* CALayer+.swift */, 02ED76492C5779C3001569F1 /* UIImage+.swift */, ); path = Extension; sourceTree = ""; }; + 6FE73E922D23C51600E06422 /* HandyTabs */ = { + isa = PBXGroup; + children = ( + 6FE73E952D23C55900E06422 /* HandyTabCell.swift */, + 6FE73E932D23C52C00E06422 /* HandyTabs.swift */, + ); + path = HandyTabs; + sourceTree = ""; + }; E5650D412C4D30B9002790CC /* Asset */ = { isa = PBXGroup; children = ( @@ -479,12 +502,14 @@ A5A12A7E2C57A6D900996916 /* ChipViewController.swift in Sources */, A5A12A7F2C57A92000996916 /* HandySematic.swift in Sources */, A5F6D36D2C97099C00FB961F /* DividerViewController.swift in Sources */, + 6FE73E982D23D7A900E06422 /* MainViewController.swift in Sources */, 021C77042CEA504B00AC7D7D /* HandyListViewController.swift in Sources */, 025776392C4EA98C00272EC6 /* LabelViewController.swift in Sources */, 0257765D2C4EB9EF00272EC6 /* BaseViewController.swift in Sources */, 02ED764C2C57BD09001569F1 /* HandyBoxButtonViewController.swift in Sources */, E51FBFA22C54CD350097B0DA /* RadioButtonViewController.swift in Sources */, E51FBF9B2C5399A00097B0DA /* CheckBoxViewController.swift in Sources */, + 6FD1A1802D213129001E6F2E /* TabsViewController.swift in Sources */, 025776352C4EA98C00272EC6 /* AppDelegate.swift in Sources */, 02697A262C99DDA30027A362 /* HansySwitchViewController.swift in Sources */, 025776372C4EA98C00272EC6 /* SceneDelegate.swift in Sources */, @@ -501,15 +526,18 @@ 02697A242C99D7230027A362 /* HandySwitch.swift in Sources */, 02150E4A2CC8D7AB00EE690E /* HandySnackbar.swift in Sources */, E5D02B002C480A180056CE7B /* HandyPrimitive.swift in Sources */, + 6FE73E942D23C52F00E06422 /* HandyTabs.swift in Sources */, E5D02AFD2C46C5A70056CE7B /* HandySematic.swift in Sources */, E5D02B002C480A180056CE7B /* HandyPrimitive.swift in Sources */, E51FBFA02C54CB260097B0DA /* HandyRadioButton.swift in Sources */, E5669A3F2C443E7300DABC21 /* HandyBasicColor.swift in Sources */, 02ED76312C5284BB001569F1 /* HandyButtonProtocol.swift in Sources */, 02ED76352C5284F3001569F1 /* HandyTextButton.swift in Sources */, + 6F1A18D02D26606D004C6083 /* CALayer+.swift in Sources */, 02BCB2302CDF417500D0C796 /* HandyListItem.swift in Sources */, A5F6D36B2C96F32D00FB961F /* HandyDivider.swift in Sources */, 02BDB7FC2C3E99920050FB67 /* HandyFont.swift in Sources */, + 6FE73E962D23C55900E06422 /* HandyTabCell.swift in Sources */, 02ED764A2C5779C3001569F1 /* UIImage+.swift in Sources */, 029E48002C49FD4000D2F3B7 /* HandyTypography.swift in Sources */, E5650D432C4D326D002790CC /* HandyCheckBox.swift in Sources */, diff --git a/Handy/Handy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Handy/Handy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 747f717..0000000 --- a/Handy/Handy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,15 +0,0 @@ -{ - "originHash" : "4cdad746817ac9ed37e44dbe90a2ff25fc018954899b7d513e41a4bfc70f065a", - "pins" : [ - { - "identity" : "snapkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SnapKit/SnapKit", - "state" : { - "revision" : "2842e6e84e82eb9a8dac0100ca90d9444b0307f4", - "version" : "5.7.1" - } - } - ], - "version" : 3 -} diff --git a/Handy/Handy/Source/Atom/HandyTabs/HandyTabCell.swift b/Handy/Handy/Source/Atom/HandyTabs/HandyTabCell.swift new file mode 100644 index 0000000..0e5263e --- /dev/null +++ b/Handy/Handy/Source/Atom/HandyTabs/HandyTabCell.swift @@ -0,0 +1,123 @@ +// +// HandyTabCell.swift +// Handy +// +// Created by chongin on 12/31/24. +// + +import UIKit + +/// ``HandyTabs``의 Header를 구성하는 cell입니다. +open class HandyTabCell: UICollectionViewCell { + // MARK: - open & public properties + + /// 글씨 크기를 결정합니다. + open var sizeType: SizeType = .large { + didSet { + setConfiguration() + } + } + + /// 해당 탭을 대표할 이름입니다. + open var title: String = "Tab" { + didSet { + setTitleLabel() + } + } + + /// 현재 탭이 선택되었는지를 나타내는 변수입니다. + open override var isSelected: Bool { + didSet { + if oldValue == isSelected { return } + setConfiguration() + } + } + + // MARK: - internal & private properties + + /// title이 표시될 label입니다. + internal let titleLabel: HandyLabel = { + let label = HandyLabel(style: .B1Sb16) + label.alignment = .center + return label + }() + + /// 현재 선택됨을 알려주는, 아래쪽에 표시되는 검은색 바입니다. + internal let selectedIndicator = UIView() + + // MARK: - init + public override init(frame: CGRect) { + super.init(frame: frame) + initializeViewHierarchy() + initializeConstraints() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - layoutSubviews + open override func layoutSubviews() { + super.layoutSubviews() + layer.addBorder([.bottom], color: HandySemantic.lineBasicLight, width: 1) + } + + // MARK: - private methods + + private func initializeViewHierarchy() { + // Set View Hierarchy + self.addSubview(titleLabel) + self.addSubview(selectedIndicator) + } + + private func initializeConstraints() { + // Set Constraints + titleLabel.snp.makeConstraints { + $0.leading.trailing.equalToSuperview().inset(16).priority(999) + $0.centerY.equalToSuperview() + $0.height.equalTo(48) + } + + selectedIndicator.snp.makeConstraints { + $0.bottom.equalToSuperview().priority(999) + $0.leading.trailing.equalToSuperview().inset(18).priority(999) + $0.height.equalTo(2) + } + } + + private func setConfiguration() { + setTitleLabel() + setIndicatorBar() + } + + private func setTitleLabel() { + titleLabel.text = title + titleLabel.style = switch sizeType { + case .small: + String.HandyTypoStyle.B3Sb14 + case .large: + String.HandyTypoStyle.B1Sb16 + } + titleLabel.textColor = switch isSelected { + case true: HandySemantic.textBasicPrimary + case false: HandySemantic.textBasicTertiary + } + } + + private func setIndicatorBar() { + selectedIndicator.backgroundColor = HandySemantic.bgBasicBlack + selectedIndicator.layer.cornerRadius = 1 + selectedIndicator.isHidden = !isSelected + } +} + +extension HandyTabCell { + public enum SizeType { + case small + case large + } +} + +extension HandyTabCell { + static let reuseIdentifier = "HandyTabCell" +} diff --git a/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift b/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift new file mode 100644 index 0000000..0bb56cb --- /dev/null +++ b/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift @@ -0,0 +1,218 @@ +// +// HandyTabs.swift +// Handy +// +// Created by chongin on 12/31/24. +// + +import UIKit + +open class HandyTabs: UIViewController { + // MARK: - open & public properties + /// `HandyTabCell`의 크기를 결정합니다. + open var sizeType: HandyTabCell.SizeType + + + /// 보여질 탭의 정보를 의미합니다. + /// 탭의 개수가 1~3개라면 스크롤 없는 고정된 형태로 표현되며, + /// 탭의 개수가 4개 이상이라면 스크롤이 생기는 가변 형태로 표현됩니다. + /// ``HandyTabsType``를 참고해주세요. + open var tabs: [(title: String, viewController: UIViewController)] = [] { + didSet { + if tabs.isEmpty { + selectedIndex = nil + } else if selectedIndex == nil { + selectedIndex = 0 + } else if selectedIndex! >= tabs.count { + selectedIndex = tabs.count - 1 + } + + tabsHeader.reloadData() // reload 후 layout을 수정해야 정상적으로 작동합니다. + + updateTabsHeaderLayout() + } + } + + /// 현재 선택되어있는 탭의 인덱스입니다. [0] 부터 시작합니다. + /// * `nil`인 경우 : 탭이 0개이거나 초기 상태입니다. + open var selectedIndex: Int? = nil { + didSet { + if let oldValue { + let previousViewController = tabs[oldValue].viewController + previousViewController.removeFromParent() + previousViewController.view.snp.removeConstraints() + previousViewController.view.removeFromSuperview() + } + + if let selectedVC = selectedTab?.viewController { + self.addChild(selectedVC) + self.tabsContent.addSubview(selectedVC.view) + selectedVC.view.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + } + } + + /// 현재 선택된 Tab의 정보입니다. + open var selectedTab: (title: String, viewController: UIViewController)? { + guard let selectedIndex else { return nil } + return tabs.indices.contains(selectedIndex) + ? tabs[selectedIndex] + : nil + } + + // MARK: - private properties + + /// Tab의 정보 중 `title` 부분이 표현되는 헤더 영역입니다. + /// `FlowLayout`으로 구성되어 있습니다. cell은 ``HandyTabCell``만 등록되어 있습니다. + private var tabsHeader: UICollectionView! + + /// Tab의 정보 중 `ViewController` 부분이 표현되는 콘텐츠 영역입니다. + /// 헤더 뷰를 제외한 ``HandyTabs``의 나머지 영역을 모두 포함합니다. + private var tabsContent: UIView! + + /// 현재 탭의 타입을 의미하는 계산 프로퍼티입니다. + private var tabsType: HandyTabsType { + switch self.tabs.count { + case 1...3: + return .fixed(viewCount: self.tabs.count) + default: + return .scrollable + } + } + + // MARK: - Init + public init(sizeType: HandyTabCell.SizeType) { + self.sizeType = sizeType + super.init(nibName: nil, bundle: nil) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open override func viewDidLoad() { + super.viewDidLoad() + setTabsHeader() + setTabsContent() + } + + // MARK: - private methods + + /// Tabs Header 부분을 만드는 메소드입니다. + /// + /// collectionView를 만들고, addSubview()를 수행하고, constraints를 설정하고, cell을 등록하며, delegate를 지정합니다. + private func setTabsHeader() { + // configure collectionView + tabsHeader = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + tabsHeader.isScrollEnabled = true + tabsHeader.allowsMultipleSelection = false + tabsHeader.showsHorizontalScrollIndicator = false + tabsHeader.showsVerticalScrollIndicator = false + + self.view.addSubview(tabsHeader) + + tabsHeader.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + $0.height.equalTo(48) + } + + // register cells + tabsHeader.register(HandyTabCell.self, forCellWithReuseIdentifier: HandyTabCell.reuseIdentifier) + + // set delegates + tabsHeader.delegate = self + tabsHeader.dataSource = self + } + + /// Tabs Content 부분을 만드는 메소드입니다. + /// + /// content 영역을 만들고, addSubview()를 수행하고, constraints를 설정합니다. + private func setTabsContent() { + tabsContent = UIView() + + self.view.addSubview(tabsContent) + + tabsContent.snp.makeConstraints { + $0.top.equalTo(tabsHeader.snp.bottom) + $0.leading.trailing.bottom.equalToSuperview() + } + } + + /// 헤더 영역의 layout을 수정하는 메소드입니다. + /// + /// tabs의 정보가 바뀌면 그 개수에 따라 고정형 or 가변형으로 해야 합니다. + /// + /// 고정형(fixed)일 때 : 개수에 따라 width를 계산하여 지정합니다. + /// 가변형(scrollable)일 때 : estimatedItemSize만 결정하여 텍스트의 크기만큼 너비가 조절되도록 해둡니다. + private func updateTabsHeaderLayout() { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumLineSpacing = 0.0 + layout.minimumInteritemSpacing = 0.0 + + switch tabsType { + case .scrollable: + layout.estimatedItemSize = CGSize(width: 10, height: 48) + + self.tabsHeader.isScrollEnabled = true + case .fixed(let tabCount): + let itemWidth = self.view.bounds.width / CGFloat(tabCount) + layout.itemSize = CGSize(width: itemWidth, height: 48) + + self.tabsHeader.isScrollEnabled = false + } + + tabsHeader.collectionViewLayout = layout + tabsHeader.collectionViewLayout.invalidateLayout() + } +} + +extension HandyTabs: UICollectionViewDelegate, UICollectionViewDataSource { + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + self.tabs.count + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HandyTabCell.reuseIdentifier, for: indexPath) as! HandyTabCell + cell.sizeType = sizeType + cell.title = tabs[indexPath.row].title + return cell + } + + // 새로운 cell을 선택했을 때 이전 cell은 선택 해제해줍니다. + // content를 선택한 ViewController로 바꿔줍니다. + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard selectedIndex != indexPath.row else { return } + + let previousIndex = selectedIndex + selectedIndex = indexPath.row + + collectionView.performBatchUpdates { + if let previousIndex = previousIndex { + collectionView.reloadItems(at: [IndexPath(item: previousIndex, section: 0)]) + } + collectionView.reloadItems(at: [indexPath]) + } completion: { _ in + UIView.animate(withDuration: 0.3) { + if let selectedCell = collectionView.cellForItem(at: indexPath) as? HandyTabCell { + selectedCell.isSelected = true + } + } + } + } + + // cell의 선택 속성은 willDisplay에서 결정해야 합니다. + public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + cell.isSelected = indexPath.row == self.selectedIndex + } +} + +extension HandyTabs { + public enum HandyTabsType { + case scrollable + case fixed(viewCount: Int) + } +} + diff --git a/Handy/Handy/Source/Extension/CALayer+.swift b/Handy/Handy/Source/Extension/CALayer+.swift new file mode 100644 index 0000000..82c3774 --- /dev/null +++ b/Handy/Handy/Source/Extension/CALayer+.swift @@ -0,0 +1,36 @@ +// +// CALayer+.swift +// Handy +// +// Created by chongin on 1/2/25. +// + +import UIKit + +extension CALayer { + + /// view의 layer에서 특정 모서리에만 border를 적용시킵니다. + /// - Parameters: + /// - edges: 적용하고 싶은 모서리의 방향입니다. `.top`, `.bottom,` `.left`, `.right`를 사용할 수 있습니다. `.all`은 사용할 수 없습니다. + /// - color: border의 색상입니다. + /// - width: border의 두께입니다. + func addBorder(_ edges: [UIRectEdge], color: UIColor, width: CGFloat) { + edges.forEach { edge in + let border = CALayer() + switch edge { + case .top: + border.frame = CGRect(x: 0, y: 0, width: frame.width, height: width) + case .bottom: + border.frame = CGRect(x: 0, y: frame.height - width, width: frame.width, height: width) + case .left: + border.frame = CGRect(x: 0, y: 0, width: width, height: frame.height) + case .right: + border.frame = CGRect(x: frame.width - width, y: 0, width: width, height: frame.height) + default: + break + } + border.backgroundColor = color.cgColor + addSublayer(border) + } + } +}