From 6bdf6482d3682b2d6232f57398875234cdf74fef Mon Sep 17 00:00:00 2001 From: "chongin12(Mosu)" Date: Sun, 29 Dec 2024 19:29:37 +0900 Subject: [PATCH 01/13] =?UTF-8?q?[#12]=20TabComponent=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Atom/TabsViewController.swift | 122 ++++++++++++++++++ Handy/Handy-Storybook/SceneDelegate.swift | 2 +- Handy/Handy.xcodeproj/project.pbxproj | 4 + 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 Handy/Handy-Storybook/Atom/TabsViewController.swift diff --git a/Handy/Handy-Storybook/Atom/TabsViewController.swift b/Handy/Handy-Storybook/Atom/TabsViewController.swift new file mode 100644 index 0000000..3e247bc --- /dev/null +++ b/Handy/Handy-Storybook/Atom/TabsViewController.swift @@ -0,0 +1,122 @@ +// +// TabsViewController.swift +// Handy +// +// Created by chongin on 12/29/24. +// + +import Handy + +final class TabsViewController: BaseViewController { + private let tab1: HandyTabComponent = { + let tabComponent = HandyTabComponent(size: .small) + tabComponent.backgroundColor = .gray100 + tabComponent.title = "tabbbbb" + return tabComponent + }() + + private let tab2: HandyTabComponent = { + let tabComponent = HandyTabComponent(size: .large) + tabComponent.backgroundColor = .gray100 + tabComponent.title = "tabbbbb" + tabComponent.isSelected = true + return tabComponent + }() + + override func setViewHierarchies() { + self.view.addSubview(tab1) + self.view.addSubview(tab2) + } + + override func setViewLayouts() { + tab1.snp.makeConstraints { + $0.top.leading.equalToSuperview().inset(100) + } + + tab2.snp.makeConstraints { + $0.top.leading.equalToSuperview().inset(200) + } + } +} + + +import UIKit + +open class HandyTabComponent: UIControl { + public var size: SizeType = .large + + private let titleLabel = HandyLabel(style: .B1Sb16) + private let activeIndicatorBar = UIView() + + public var title: String? { + didSet { + setTitleLabel() + } + } + + public override var isSelected: Bool { + didSet { + setConfiguration() + } + } + + public init(size: SizeType) { + super.init(frame: .zero) + self.size = size + initializeViewHierarchy() + initializeConstraints() + setConfiguration() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func initializeViewHierarchy() { + // Set View Hierarchy + self.addSubview(titleLabel) + self.addSubview(activeIndicatorBar) + } + + private func initializeConstraints() { + // Set Constraints + titleLabel.snp.makeConstraints { + $0.leading.trailing.equalToSuperview().inset(16) + $0.top.bottom.equalToSuperview().inset(12) + } + + activeIndicatorBar.snp.makeConstraints { + $0.bottom.equalToSuperview() + $0.leading.trailing.equalToSuperview().inset(18) + $0.height.equalTo(2) + } + } + + private func setConfiguration() { + setTitleLabel() + setIndicatorBar() + } + + private func setTitleLabel() { + titleLabel.text = title + titleLabel.style = switch size { + case .small: + String.HandyTypoStyle.B3Sb14 + case .large: + String.HandyTypoStyle.B1Sb16 + } + } + + private func setIndicatorBar() { + activeIndicatorBar.backgroundColor = HandySemantic.bgBasicBlack + activeIndicatorBar.layer.cornerRadius = 1 + activeIndicatorBar.isHidden = !isSelected + } +} + +extension HandyTabComponent { + public enum SizeType { + case small + case large + } +} diff --git a/Handy/Handy-Storybook/SceneDelegate.swift b/Handy/Handy-Storybook/SceneDelegate.swift index 4be8af5..a10553a 100644 --- a/Handy/Handy-Storybook/SceneDelegate.swift +++ b/Handy/Handy-Storybook/SceneDelegate.swift @@ -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 = HandyListViewController() + window?.rootViewController = TabsViewController() window?.makeKeyAndVisible() } diff --git a/Handy/Handy.xcodeproj/project.pbxproj b/Handy/Handy.xcodeproj/project.pbxproj index c13e8fa..1b72111 100644 --- a/Handy/Handy.xcodeproj/project.pbxproj +++ b/Handy/Handy.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ 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 */; }; + 6FD1A1802D213129001E6F2E /* TabsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1A17F2D21311C001E6F2E /* TabsViewController.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 +122,7 @@ 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 = ""; }; + 6FD1A17F2D21311C001E6F2E /* TabsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsViewController.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 +185,7 @@ 025776482C4EB0E700272EC6 /* Atom */ = { isa = PBXGroup; children = ( + 6FD1A17F2D21311C001E6F2E /* TabsViewController.swift */, 02150E4B2CCABAE500EE690E /* SnackbarViewController.swift */, 025776382C4EA98C00272EC6 /* LabelViewController.swift */, 2D41E8132C5A21930043161D /* FabViewController.swift */, @@ -485,6 +488,7 @@ 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 */, From a2ccebf4639ef39f5940629af5144329ccd1cf16 Mon Sep 17 00:00:00 2001 From: "chongin12(Mosu)" Date: Mon, 30 Dec 2024 16:03:58 +0900 Subject: [PATCH 02/13] =?UTF-8?q?[#12]=20TabComponent=EB=A5=BC=20=EB=AA=A8?= =?UTF-8?q?=EC=9D=80=20HandyTabs=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Atom/TabsViewController.swift | 264 +++++++++++++++--- 1 file changed, 226 insertions(+), 38 deletions(-) diff --git a/Handy/Handy-Storybook/Atom/TabsViewController.swift b/Handy/Handy-Storybook/Atom/TabsViewController.swift index 3e247bc..71df5ec 100644 --- a/Handy/Handy-Storybook/Atom/TabsViewController.swift +++ b/Handy/Handy-Storybook/Atom/TabsViewController.swift @@ -8,47 +8,232 @@ import Handy final class TabsViewController: BaseViewController { - private let tab1: HandyTabComponent = { - let tabComponent = HandyTabComponent(size: .small) - tabComponent.backgroundColor = .gray100 - tabComponent.title = "tabbbbb" - return tabComponent - }() - - private let tab2: HandyTabComponent = { - let tabComponent = HandyTabComponent(size: .large) - tabComponent.backgroundColor = .gray100 - tabComponent.title = "tabbbbb" - tabComponent.isSelected = true - return tabComponent + private let tabs: HandyTabs = { + let tabs = HandyTabs(sizeType: .large) + tabs.viewControllers = [ + { + let viewController = UIViewController() + viewController.view.frame = UIScreen.main.bounds + viewController.view.backgroundColor = .red + return viewController + }(), + { + let viewController = UIViewController() + viewController.view.frame = UIScreen.main.bounds + viewController.view.backgroundColor = .green + return viewController + }(), + { + let viewController = UIViewController() + viewController.view.frame = UIScreen.main.bounds + viewController.view.backgroundColor = .red + return viewController + }(), + { + let viewController = UIViewController() + viewController.view.frame = UIScreen.main.bounds + viewController.view.backgroundColor = .green + return viewController + }(), + { + let viewController = UIViewController() + viewController.view.frame = UIScreen.main.bounds + viewController.view.backgroundColor = .red + return viewController + }(), + { + let viewController = UIViewController() + viewController.view.frame = UIScreen.main.bounds + viewController.view.backgroundColor = .green + return viewController + }(), + { + let viewController = UIViewController() + viewController.view.frame = UIScreen.main.bounds + viewController.view.backgroundColor = .red + return viewController + }(), + { + let viewController = UIViewController() + viewController.view.frame = UIScreen.main.bounds + viewController.view.backgroundColor = .green + return viewController + }(), + ] + return tabs }() override func setViewHierarchies() { - self.view.addSubview(tab1) - self.view.addSubview(tab2) + self.addChild(tabs) + self.view.addSubview(tabs.view) } override func setViewLayouts() { - tab1.snp.makeConstraints { - $0.top.leading.equalToSuperview().inset(100) + tabs.view.snp.makeConstraints { + $0.edges.equalTo(self.view.safeAreaLayoutGuide) + } + } +} + + +import UIKit + +open class HandyTabs: UIViewController { + public var sizeType: HandyTabComponent.SizeType = .large + public var viewControllers: [UIViewController] = [] { + didSet { + if viewControllers.isEmpty { + selectedIndex = nil + } else if selectedIndex == nil { + selectedIndex = 0 + } else if selectedIndex! >= viewControllers.count { + selectedIndex = viewControllers.count - 1 + } + } + } + public var selectedIndex: Int? { + didSet { + if let oldValue { + let previousViewController = viewControllers[oldValue] + previousViewController.removeFromParent() + previousViewController.view.removeFromSuperview() + } + + if let selectedVC = selectedViewController { + self.addChild(selectedVC) + self.tabsContent.addSubview(selectedVC.view) + } + } + } + public var selectedViewController: UIViewController? { + guard let selectedIndex else { return nil } + return viewControllers.indices.contains(selectedIndex) + ? viewControllers[selectedIndex] + : nil + } + + private var tabsHeader: UICollectionView! + private var tabsContent: UIScrollView! + + private var viewCount: Int { + self.viewControllers.count + } + private var tabsType: HandyTabsType { + switch viewCount { + case 1...3: + return .fixed(viewCount: self.viewControllers.count) + default: + return .scrollable + } + } + public init(sizeType: HandyTabComponent.SizeType) { + super.init(nibName: nil, bundle: nil) + self.sizeType = sizeType + setTabsHeader() + setTabsContent() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func viewDidLoad() { + super.viewDidLoad() + } + + private func setTabsHeader() { + // configure collectionView + let flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .horizontal + flowLayout.estimatedItemSize = CGSize(width: 0, height: 48) + flowLayout.minimumInteritemSpacing = 8.0 + + tabsHeader = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) + tabsHeader.isScrollEnabled = true + tabsHeader.allowsMultipleSelection = false + tabsHeader.showsHorizontalScrollIndicator = false + tabsHeader.showsVerticalScrollIndicator = false + tabsHeader.backgroundColor = .g400 + + self.view.addSubview(tabsHeader) + + tabsHeader.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + $0.height.equalTo(48) } - tab2.snp.makeConstraints { - $0.top.leading.equalToSuperview().inset(200) + // register cells + tabsHeader.register(HandyTabComponent.self, forCellWithReuseIdentifier: HandyTabComponent.reuseIdentifier) + + // set delegates + tabsHeader.delegate = self + tabsHeader.dataSource = self + } + + private func setTabsContent() { + tabsContent = UIScrollView() + tabsContent.backgroundColor = .blue + tabsContent.showsHorizontalScrollIndicator = false + + self.view.addSubview(tabsContent) + + tabsContent.snp.makeConstraints { + $0.top.equalTo(tabsHeader.snp.bottom) + $0.leading.trailing.bottom.equalToSuperview() } } } +extension HandyTabs: UICollectionViewDelegate, UICollectionViewDataSource { + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + viewCount + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HandyTabComponent.reuseIdentifier, for: indexPath) as! HandyTabComponent + cell.sizeType = sizeType + cell.title = "Tab \(indexPath.row)" + return cell + } -import UIKit + // 새로운 cell을 선택했을 때 이전 cell은 선택 해제해줍니다. + // content를 선택한 ViewController로 바꿔줍니다. + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let selectedIndex else { return } + let previousIndexPath = IndexPath(item: selectedIndex, section: 0) + if let previousCell = collectionView.cellForItem(at: previousIndexPath) as? HandyTabComponent { + previousCell.isSelected = false + } -open class HandyTabComponent: UIControl { - public var size: SizeType = .large + self.selectedIndex = indexPath.row + let selectedCell = collectionView.cellForItem(at: indexPath) as? HandyTabComponent + selectedCell?.isSelected = true + } + + // cell의 선택 속성은 willDisplay에서 결정해야 합니다. + public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + cell.isSelected = indexPath.row == selectedIndex + } +} + +extension HandyTabs { + public enum HandyTabsType { + case scrollable + case fixed(viewCount: Int) + } +} + +open class HandyTabComponent: UICollectionViewCell { + public var sizeType: SizeType = .large { + didSet { + setConfiguration() + } + } private let titleLabel = HandyLabel(style: .B1Sb16) - private let activeIndicatorBar = UIView() + private let selectedIndicator = UIView() - public var title: String? { + public var title: String = "Tab" { didSet { setTitleLabel() } @@ -56,16 +241,15 @@ open class HandyTabComponent: UIControl { public override var isSelected: Bool { didSet { + if oldValue == isSelected { return } setConfiguration() } } - public init(size: SizeType) { - super.init(frame: .zero) - self.size = size + public override init(frame: CGRect) { + super.init(frame: frame) initializeViewHierarchy() initializeConstraints() - setConfiguration() } required public init?(coder: NSCoder) { @@ -75,19 +259,19 @@ open class HandyTabComponent: UIControl { private func initializeViewHierarchy() { // Set View Hierarchy self.addSubview(titleLabel) - self.addSubview(activeIndicatorBar) + self.addSubview(selectedIndicator) } private func initializeConstraints() { // Set Constraints titleLabel.snp.makeConstraints { - $0.leading.trailing.equalToSuperview().inset(16) - $0.top.bottom.equalToSuperview().inset(12) + $0.leading.trailing.equalToSuperview().inset(16).priority(999) + $0.top.bottom.equalToSuperview().inset(12).priority(999) } - activeIndicatorBar.snp.makeConstraints { - $0.bottom.equalToSuperview() - $0.leading.trailing.equalToSuperview().inset(18) + selectedIndicator.snp.makeConstraints { + $0.bottom.equalToSuperview().priority(999) + $0.leading.trailing.equalToSuperview().inset(18).priority(999) $0.height.equalTo(2) } } @@ -99,7 +283,7 @@ open class HandyTabComponent: UIControl { private func setTitleLabel() { titleLabel.text = title - titleLabel.style = switch size { + titleLabel.style = switch sizeType { case .small: String.HandyTypoStyle.B3Sb14 case .large: @@ -108,9 +292,9 @@ open class HandyTabComponent: UIControl { } private func setIndicatorBar() { - activeIndicatorBar.backgroundColor = HandySemantic.bgBasicBlack - activeIndicatorBar.layer.cornerRadius = 1 - activeIndicatorBar.isHidden = !isSelected + selectedIndicator.backgroundColor = HandySemantic.bgBasicBlack + selectedIndicator.layer.cornerRadius = 1 + selectedIndicator.isHidden = !isSelected } } @@ -120,3 +304,7 @@ extension HandyTabComponent { case large } } + +extension HandyTabComponent { + static let reuseIdentifier = "HandyTabComponent" +} From 1a807c13395c90fc6b2762d0f117f32beb690b47 Mon Sep 17 00:00:00 2001 From: "chongin12(Mosu)" Date: Mon, 30 Dec 2024 16:48:06 +0900 Subject: [PATCH 03/13] =?UTF-8?q?[#12]=20Content=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=EC=9D=84=20ScrollView=EA=B0=80=20=EC=95=84=EB=8B=8C=20?= =?UTF-8?q?=EC=9D=BC=EB=B0=98=20UIView=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Atom/TabsViewController.swift | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/Handy/Handy-Storybook/Atom/TabsViewController.swift b/Handy/Handy-Storybook/Atom/TabsViewController.swift index 71df5ec..901e1cd 100644 --- a/Handy/Handy-Storybook/Atom/TabsViewController.swift +++ b/Handy/Handy-Storybook/Atom/TabsViewController.swift @@ -13,49 +13,21 @@ final class TabsViewController: BaseViewController { tabs.viewControllers = [ { let viewController = UIViewController() - viewController.view.frame = UIScreen.main.bounds viewController.view.backgroundColor = .red return viewController }(), { let viewController = UIViewController() - viewController.view.frame = UIScreen.main.bounds viewController.view.backgroundColor = .green return viewController }(), { let viewController = UIViewController() - viewController.view.frame = UIScreen.main.bounds viewController.view.backgroundColor = .red return viewController }(), { let viewController = UIViewController() - viewController.view.frame = UIScreen.main.bounds - viewController.view.backgroundColor = .green - return viewController - }(), - { - let viewController = UIViewController() - viewController.view.frame = UIScreen.main.bounds - viewController.view.backgroundColor = .red - return viewController - }(), - { - let viewController = UIViewController() - viewController.view.frame = UIScreen.main.bounds - viewController.view.backgroundColor = .green - return viewController - }(), - { - let viewController = UIViewController() - viewController.view.frame = UIScreen.main.bounds - viewController.view.backgroundColor = .red - return viewController - }(), - { - let viewController = UIViewController() - viewController.view.frame = UIScreen.main.bounds viewController.view.backgroundColor = .green return viewController }(), @@ -96,12 +68,16 @@ open class HandyTabs: UIViewController { if let oldValue { let previousViewController = viewControllers[oldValue] previousViewController.removeFromParent() + previousViewController.view.snp.removeConstraints() previousViewController.view.removeFromSuperview() } if let selectedVC = selectedViewController { self.addChild(selectedVC) self.tabsContent.addSubview(selectedVC.view) + selectedVC.view.snp.makeConstraints { + $0.edges.equalToSuperview() + } } } } @@ -113,7 +89,7 @@ open class HandyTabs: UIViewController { } private var tabsHeader: UICollectionView! - private var tabsContent: UIScrollView! + private var tabsContent: UIView! private var viewCount: Int { self.viewControllers.count @@ -171,9 +147,8 @@ open class HandyTabs: UIViewController { } private func setTabsContent() { - tabsContent = UIScrollView() + tabsContent = UIView() tabsContent.backgroundColor = .blue - tabsContent.showsHorizontalScrollIndicator = false self.view.addSubview(tabsContent) From 1fedc60d4b5c9b13e4c29eaec24dd3da7873d29e Mon Sep 17 00:00:00 2001 From: "chongin12(Mosu)" Date: Mon, 30 Dec 2024 21:50:09 +0900 Subject: [PATCH 04/13] =?UTF-8?q?[#12]=20=EA=B0=80=EB=B3=80=20=ED=83=AD=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Atom/TabsViewController.swift | 70 +++++++++++++------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/Handy/Handy-Storybook/Atom/TabsViewController.swift b/Handy/Handy-Storybook/Atom/TabsViewController.swift index 901e1cd..f94bdea 100644 --- a/Handy/Handy-Storybook/Atom/TabsViewController.swift +++ b/Handy/Handy-Storybook/Atom/TabsViewController.swift @@ -10,26 +10,26 @@ import Handy final class TabsViewController: BaseViewController { private let tabs: HandyTabs = { let tabs = HandyTabs(sizeType: .large) - tabs.viewControllers = [ + tabs.tabs = [ { let viewController = UIViewController() viewController.view.backgroundColor = .red - return viewController + return ("첫번째 탭입니다", viewController) }(), { let viewController = UIViewController() viewController.view.backgroundColor = .green - return viewController + return ("두번째 탭입니다", viewController) }(), { let viewController = UIViewController() viewController.view.backgroundColor = .red - return viewController + return ("세번째", viewController) }(), { let viewController = UIViewController() - viewController.view.backgroundColor = .green - return viewController + viewController.view.backgroundColor = .red + return ("네번째", viewController) }(), ] return tabs @@ -52,27 +52,29 @@ import UIKit open class HandyTabs: UIViewController { public var sizeType: HandyTabComponent.SizeType = .large - public var viewControllers: [UIViewController] = [] { + public var tabs: [(title: String, viewController: UIViewController)] = [] { didSet { - if viewControllers.isEmpty { + if tabs.isEmpty { selectedIndex = nil } else if selectedIndex == nil { selectedIndex = 0 - } else if selectedIndex! >= viewControllers.count { - selectedIndex = viewControllers.count - 1 + } else if selectedIndex! >= tabs.count { + selectedIndex = tabs.count - 1 } + + updateCollectionViewLayout() } } public var selectedIndex: Int? { didSet { if let oldValue { - let previousViewController = viewControllers[oldValue] + let previousViewController = tabs[oldValue].viewController previousViewController.removeFromParent() previousViewController.view.snp.removeConstraints() previousViewController.view.removeFromSuperview() } - if let selectedVC = selectedViewController { + if let selectedVC = selectedTab?.viewController { self.addChild(selectedVC) self.tabsContent.addSubview(selectedVC.view) selectedVC.view.snp.makeConstraints { @@ -81,10 +83,10 @@ open class HandyTabs: UIViewController { } } } - public var selectedViewController: UIViewController? { + public var selectedTab: (title: String, viewController: UIViewController)? { guard let selectedIndex else { return nil } - return viewControllers.indices.contains(selectedIndex) - ? viewControllers[selectedIndex] + return tabs.indices.contains(selectedIndex) + ? tabs[selectedIndex] : nil } @@ -92,16 +94,17 @@ open class HandyTabs: UIViewController { private var tabsContent: UIView! private var viewCount: Int { - self.viewControllers.count + self.tabs.count } private var tabsType: HandyTabsType { switch viewCount { case 1...3: - return .fixed(viewCount: self.viewControllers.count) + return .fixed(viewCount: viewCount) default: return .scrollable } } + private let tabSpacing: CGFloat = 8.0 public init(sizeType: HandyTabComponent.SizeType) { super.init(nibName: nil, bundle: nil) self.sizeType = sizeType @@ -121,8 +124,7 @@ open class HandyTabs: UIViewController { // configure collectionView let flowLayout = UICollectionViewFlowLayout() flowLayout.scrollDirection = .horizontal - flowLayout.estimatedItemSize = CGSize(width: 0, height: 48) - flowLayout.minimumInteritemSpacing = 8.0 + flowLayout.minimumInteritemSpacing = tabSpacing tabsHeader = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) tabsHeader.isScrollEnabled = true @@ -157,17 +159,37 @@ open class HandyTabs: UIViewController { $0.leading.trailing.bottom.equalToSuperview() } } + + private func updateCollectionViewLayout() { + guard let layout = tabsHeader.collectionViewLayout as? UICollectionViewFlowLayout else { return } + + switch tabsType { + case .scrollable: + layout.scrollDirection = .horizontal + layout.minimumInteritemSpacing = tabSpacing + layout.estimatedItemSize = CGSize(width: 0, height: 48) + case .fixed(let viewCount): + let totalSpacing = tabSpacing * CGFloat(viewCount - 1) + let itemWidth = (self.view.frame.width - totalSpacing) / CGFloat(viewCount) + layout.scrollDirection = .horizontal + layout.minimumInteritemSpacing = tabSpacing + layout.itemSize = CGSize(width: itemWidth, height: 48) + } + + tabsHeader.collectionViewLayout.invalidateLayout() + } } extension HandyTabs: UICollectionViewDelegate, UICollectionViewDataSource { public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { viewCount } - + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HandyTabComponent.reuseIdentifier, for: indexPath) as! HandyTabComponent cell.sizeType = sizeType - cell.title = "Tab \(indexPath.row)" + cell.title = tabs[indexPath.row].title + cell.backgroundColor = .green return cell } @@ -205,7 +227,11 @@ open class HandyTabComponent: UICollectionViewCell { } } - private let titleLabel = HandyLabel(style: .B1Sb16) + private let titleLabel: HandyLabel = { + let label = HandyLabel(style: .B1Sb16) + label.alignment = .center + return label + }() private let selectedIndicator = UIView() public var title: String = "Tab" { From 54315552326c8546f1ec0203a1496b349fbaf867 Mon Sep 17 00:00:00 2001 From: "chongin12(Mosu)" Date: Tue, 31 Dec 2024 14:32:54 +0900 Subject: [PATCH 05/13] =?UTF-8?q?[#12]=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=EC=9A=A9=20=EC=9A=94=EC=86=8C=20=EC=A0=9C=EA=B1=B0,=20?= =?UTF-8?q?=EC=B2=AB=20=ED=99=94=EB=A9=B4=EC=9D=84=20Tabs=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Atom/TabsViewController.swift | 80 +++++++++++-------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/Handy/Handy-Storybook/Atom/TabsViewController.swift b/Handy/Handy-Storybook/Atom/TabsViewController.swift index f94bdea..729a883 100644 --- a/Handy/Handy-Storybook/Atom/TabsViewController.swift +++ b/Handy/Handy-Storybook/Atom/TabsViewController.swift @@ -9,27 +9,43 @@ import Handy final class TabsViewController: BaseViewController { private let tabs: HandyTabs = { - let tabs = HandyTabs(sizeType: .large) + let tabs = HandyTabs(sizeType: .small) tabs.tabs = [ { - let viewController = UIViewController() - viewController.view.backgroundColor = .red - return ("첫번째 탭입니다", viewController) + let viewController = SnackbarViewController() + return ("SnackbarViewController", viewController) }(), { - let viewController = UIViewController() - viewController.view.backgroundColor = .green - return ("두번째 탭입니다", viewController) + let viewController = LabelViewController() + return ("LabelViewController", viewController) }(), { - let viewController = UIViewController() - viewController.view.backgroundColor = .red - return ("세번째", viewController) + let viewController = FabViewController() + return ("FabViewController", viewController) }(), { - let viewController = UIViewController() - viewController.view.backgroundColor = .red - return ("네번째", 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) }(), ] return tabs @@ -51,7 +67,7 @@ final class TabsViewController: BaseViewController { import UIKit open class HandyTabs: UIViewController { - public var sizeType: HandyTabComponent.SizeType = .large + public var sizeType: HandyTabComponent.SizeType public var tabs: [(title: String, viewController: UIViewController)] = [] { didSet { if tabs.isEmpty { @@ -62,7 +78,7 @@ open class HandyTabs: UIViewController { selectedIndex = tabs.count - 1 } - updateCollectionViewLayout() + updateTabsHeaderLayout() } } public var selectedIndex: Int? { @@ -106,8 +122,8 @@ open class HandyTabs: UIViewController { } private let tabSpacing: CGFloat = 8.0 public init(sizeType: HandyTabComponent.SizeType) { - super.init(nibName: nil, bundle: nil) self.sizeType = sizeType + super.init(nibName: nil, bundle: nil) setTabsHeader() setTabsContent() } @@ -116,22 +132,13 @@ open class HandyTabs: UIViewController { fatalError("init(coder:) has not been implemented") } - override open func viewDidLoad() { - super.viewDidLoad() - } - private func setTabsHeader() { // configure collectionView - let flowLayout = UICollectionViewFlowLayout() - flowLayout.scrollDirection = .horizontal - flowLayout.minimumInteritemSpacing = tabSpacing - - tabsHeader = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) + tabsHeader = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) tabsHeader.isScrollEnabled = true tabsHeader.allowsMultipleSelection = false tabsHeader.showsHorizontalScrollIndicator = false tabsHeader.showsVerticalScrollIndicator = false - tabsHeader.backgroundColor = .g400 self.view.addSubview(tabsHeader) @@ -150,7 +157,6 @@ open class HandyTabs: UIViewController { private func setTabsContent() { tabsContent = UIView() - tabsContent.backgroundColor = .blue self.view.addSubview(tabsContent) @@ -160,22 +166,21 @@ open class HandyTabs: UIViewController { } } - private func updateCollectionViewLayout() { - guard let layout = tabsHeader.collectionViewLayout as? UICollectionViewFlowLayout else { return } + private func updateTabsHeaderLayout() { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumInteritemSpacing = tabSpacing switch tabsType { case .scrollable: - layout.scrollDirection = .horizontal - layout.minimumInteritemSpacing = tabSpacing layout.estimatedItemSize = CGSize(width: 0, height: 48) case .fixed(let viewCount): let totalSpacing = tabSpacing * CGFloat(viewCount - 1) let itemWidth = (self.view.frame.width - totalSpacing) / CGFloat(viewCount) - layout.scrollDirection = .horizontal - layout.minimumInteritemSpacing = tabSpacing layout.itemSize = CGSize(width: itemWidth, height: 48) } + tabsHeader.collectionViewLayout = layout tabsHeader.collectionViewLayout.invalidateLayout() } } @@ -189,7 +194,6 @@ extension HandyTabs: UICollectionViewDelegate, UICollectionViewDataSource { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HandyTabComponent.reuseIdentifier, for: indexPath) as! HandyTabComponent cell.sizeType = sizeType cell.title = tabs[indexPath.row].title - cell.backgroundColor = .green return cell } @@ -275,6 +279,10 @@ open class HandyTabComponent: UICollectionViewCell { $0.leading.trailing.equalToSuperview().inset(18).priority(999) $0.height.equalTo(2) } + + self.snp.makeConstraints { + $0.height.equalTo(48) + } } private func setConfiguration() { @@ -290,6 +298,10 @@ open class HandyTabComponent: UICollectionViewCell { case .large: String.HandyTypoStyle.B1Sb16 } + titleLabel.textColor = switch isSelected { + case true: HandySemantic.textBasicPrimary + case false: HandySemantic.textBasicTertiary + } } private func setIndicatorBar() { From da43766e25ac3b5943b31105aaed019550fd2d1b Mon Sep 17 00:00:00 2001 From: "chongin12(Mosu)" Date: Tue, 31 Dec 2024 15:20:34 +0900 Subject: [PATCH 06/13] =?UTF-8?q?[#12]=20=EC=A0=91=EA=B7=BC=EC=A0=9C?= =?UTF-8?q?=EC=96=B4=EC=9E=90=20=EC=88=98=EC=A0=95,=20=EC=95=A0=EB=8B=88?= =?UTF-8?q?=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Atom/TabsViewController.swift | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/Handy/Handy-Storybook/Atom/TabsViewController.swift b/Handy/Handy-Storybook/Atom/TabsViewController.swift index 729a883..30de503 100644 --- a/Handy/Handy-Storybook/Atom/TabsViewController.swift +++ b/Handy/Handy-Storybook/Atom/TabsViewController.swift @@ -67,8 +67,8 @@ final class TabsViewController: BaseViewController { import UIKit open class HandyTabs: UIViewController { - public var sizeType: HandyTabComponent.SizeType - public var tabs: [(title: String, viewController: UIViewController)] = [] { + open var sizeType: HandyTabComponent.SizeType + open var tabs: [(title: String, viewController: UIViewController)] = [] { didSet { if tabs.isEmpty { selectedIndex = nil @@ -81,7 +81,7 @@ open class HandyTabs: UIViewController { updateTabsHeaderLayout() } } - public var selectedIndex: Int? { + open var selectedIndex: Int? { didSet { if let oldValue { let previousViewController = tabs[oldValue].viewController @@ -99,7 +99,7 @@ open class HandyTabs: UIViewController { } } } - public var selectedTab: (title: String, viewController: UIViewController)? { + open var selectedTab: (title: String, viewController: UIViewController)? { guard let selectedIndex else { return nil } return tabs.indices.contains(selectedIndex) ? tabs[selectedIndex] @@ -200,20 +200,28 @@ extension HandyTabs: UICollectionViewDelegate, UICollectionViewDataSource { // 새로운 cell을 선택했을 때 이전 cell은 선택 해제해줍니다. // content를 선택한 ViewController로 바꿔줍니다. public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let selectedIndex else { return } - let previousIndexPath = IndexPath(item: selectedIndex, section: 0) - if let previousCell = collectionView.cellForItem(at: previousIndexPath) as? HandyTabComponent { - previousCell.isSelected = false - } + guard selectedIndex != indexPath.row else { return } + + let previousIndex = selectedIndex + selectedIndex = indexPath.row - self.selectedIndex = indexPath.row - let selectedCell = collectionView.cellForItem(at: indexPath) as? HandyTabComponent - selectedCell?.isSelected = true + 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? HandyTabComponent { + selectedCell.isSelected = true + } + } + } } // cell의 선택 속성은 willDisplay에서 결정해야 합니다. public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - cell.isSelected = indexPath.row == selectedIndex + cell.isSelected = indexPath.row == self.selectedIndex } } @@ -225,7 +233,7 @@ extension HandyTabs { } open class HandyTabComponent: UICollectionViewCell { - public var sizeType: SizeType = .large { + open var sizeType: SizeType = .large { didSet { setConfiguration() } @@ -238,13 +246,13 @@ open class HandyTabComponent: UICollectionViewCell { }() private let selectedIndicator = UIView() - public var title: String = "Tab" { + open var title: String = "Tab" { didSet { setTitleLabel() } } - public override var isSelected: Bool { + open override var isSelected: Bool { didSet { if oldValue == isSelected { return } setConfiguration() From 63523ceb5b2eec9f57cd7355f20735a367f0d980 Mon Sep 17 00:00:00 2001 From: "chongin12(Mosu)" Date: Tue, 31 Dec 2024 16:37:46 +0900 Subject: [PATCH 07/13] =?UTF-8?q?[#12]=20=ED=8C=8C=EC=9D=BC=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Atom/TabsViewController.swift | 266 ------------------ Handy/Handy.xcodeproj/project.pbxproj | 18 +- .../xcshareddata/swiftpm/Package.resolved | 15 - .../Atom/HandyTabs/HandyTabComponent.swift | 120 ++++++++ .../Source/Atom/HandyTabs/HandyTabs.swift | 211 ++++++++++++++ 5 files changed, 348 insertions(+), 282 deletions(-) delete mode 100644 Handy/Handy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Handy/Handy/Source/Atom/HandyTabs/HandyTabComponent.swift create mode 100644 Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift diff --git a/Handy/Handy-Storybook/Atom/TabsViewController.swift b/Handy/Handy-Storybook/Atom/TabsViewController.swift index 30de503..f6d43b3 100644 --- a/Handy/Handy-Storybook/Atom/TabsViewController.swift +++ b/Handy/Handy-Storybook/Atom/TabsViewController.swift @@ -63,269 +63,3 @@ final class TabsViewController: BaseViewController { } } - -import UIKit - -open class HandyTabs: UIViewController { - open var sizeType: HandyTabComponent.SizeType - 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 - } - - updateTabsHeaderLayout() - } - } - open var selectedIndex: Int? { - 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() - } - } - } - } - open var selectedTab: (title: String, viewController: UIViewController)? { - guard let selectedIndex else { return nil } - return tabs.indices.contains(selectedIndex) - ? tabs[selectedIndex] - : nil - } - - private var tabsHeader: UICollectionView! - private var tabsContent: UIView! - - private var viewCount: Int { - self.tabs.count - } - private var tabsType: HandyTabsType { - switch viewCount { - case 1...3: - return .fixed(viewCount: viewCount) - default: - return .scrollable - } - } - private let tabSpacing: CGFloat = 8.0 - public init(sizeType: HandyTabComponent.SizeType) { - self.sizeType = sizeType - super.init(nibName: nil, bundle: nil) - setTabsHeader() - setTabsContent() - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - 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(HandyTabComponent.self, forCellWithReuseIdentifier: HandyTabComponent.reuseIdentifier) - - // set delegates - tabsHeader.delegate = self - tabsHeader.dataSource = self - } - - private func setTabsContent() { - tabsContent = UIView() - - self.view.addSubview(tabsContent) - - tabsContent.snp.makeConstraints { - $0.top.equalTo(tabsHeader.snp.bottom) - $0.leading.trailing.bottom.equalToSuperview() - } - } - - private func updateTabsHeaderLayout() { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - layout.minimumInteritemSpacing = tabSpacing - - switch tabsType { - case .scrollable: - layout.estimatedItemSize = CGSize(width: 0, height: 48) - case .fixed(let viewCount): - let totalSpacing = tabSpacing * CGFloat(viewCount - 1) - let itemWidth = (self.view.frame.width - totalSpacing) / CGFloat(viewCount) - layout.itemSize = CGSize(width: itemWidth, height: 48) - } - - tabsHeader.collectionViewLayout = layout - tabsHeader.collectionViewLayout.invalidateLayout() - } -} - -extension HandyTabs: UICollectionViewDelegate, UICollectionViewDataSource { - public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - viewCount - } - - public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HandyTabComponent.reuseIdentifier, for: indexPath) as! HandyTabComponent - 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? HandyTabComponent { - 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) - } -} - -open class HandyTabComponent: UICollectionViewCell { - open var sizeType: SizeType = .large { - didSet { - setConfiguration() - } - } - - private let titleLabel: HandyLabel = { - let label = HandyLabel(style: .B1Sb16) - label.alignment = .center - return label - }() - private let selectedIndicator = UIView() - - open var title: String = "Tab" { - didSet { - setTitleLabel() - } - } - - open override var isSelected: Bool { - didSet { - if oldValue == isSelected { return } - setConfiguration() - } - } - - public override init(frame: CGRect) { - super.init(frame: frame) - initializeViewHierarchy() - initializeConstraints() - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - 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.top.bottom.equalToSuperview().inset(12).priority(999) - } - - selectedIndicator.snp.makeConstraints { - $0.bottom.equalToSuperview().priority(999) - $0.leading.trailing.equalToSuperview().inset(18).priority(999) - $0.height.equalTo(2) - } - - self.snp.makeConstraints { - $0.height.equalTo(48) - } - } - - 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 HandyTabComponent { - public enum SizeType { - case small - case large - } -} - -extension HandyTabComponent { - static let reuseIdentifier = "HandyTabComponent" -} diff --git a/Handy/Handy.xcodeproj/project.pbxproj b/Handy/Handy.xcodeproj/project.pbxproj index 1b72111..32fdb98 100644 --- a/Handy/Handy.xcodeproj/project.pbxproj +++ b/Handy/Handy.xcodeproj/project.pbxproj @@ -42,6 +42,8 @@ 2D41E8142C5A21930043161D /* FabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D41E8132C5A21930043161D /* FabViewController.swift */; }; 2D41E8162C5A21B50043161D /* HandyFab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D41E8152C5A21B50043161D /* HandyFab.swift */; }; 6FD1A1802D213129001E6F2E /* TabsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1A17F2D21311C001E6F2E /* TabsViewController.swift */; }; + 6FE73E942D23C52F00E06422 /* HandyTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE73E932D23C52C00E06422 /* HandyTabs.swift */; }; + 6FE73E962D23C55900E06422 /* HandyTabComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE73E952D23C55900E06422 /* HandyTabComponent.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 */; }; @@ -123,6 +125,8 @@ 2D41E8132C5A21930043161D /* FabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FabViewController.swift; sourceTree = ""; }; 2D41E8152C5A21B50043161D /* HandyFab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyFab.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 /* HandyTabComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyTabComponent.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 = ""; }; @@ -247,7 +251,8 @@ 029E47FE2C49FD2E00D2F3B7 /* Atom */ = { isa = PBXGroup; children = ( - 02ED762F2C52849A001569F1 /* Button */, + 6FE73E922D23C51600E06422 /* HandyTabs */, + 02ED762F2C52849A001569F1 /* HandyButton */, 029E47FC2C49FD1A00D2F3B7 /* HandyLabel.swift */, 2D41E8152C5A21B50043161D /* HandyFab.swift */, A56B3DE12C4E51D300C3610A /* HandyChip.swift */, @@ -333,6 +338,15 @@ path = Extension; sourceTree = ""; }; + 6FE73E922D23C51600E06422 /* HandyTabs */ = { + isa = PBXGroup; + children = ( + 6FE73E952D23C55900E06422 /* HandyTabComponent.swift */, + 6FE73E932D23C52C00E06422 /* HandyTabs.swift */, + ); + path = HandyTabs; + sourceTree = ""; + }; E5650D412C4D30B9002790CC /* Asset */ = { isa = PBXGroup; children = ( @@ -505,6 +519,7 @@ 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 */, @@ -514,6 +529,7 @@ 02BCB2302CDF417500D0C796 /* HandyListItem.swift in Sources */, A5F6D36B2C96F32D00FB961F /* HandyDivider.swift in Sources */, 02BDB7FC2C3E99920050FB67 /* HandyFont.swift in Sources */, + 6FE73E962D23C55900E06422 /* HandyTabComponent.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/HandyTabComponent.swift b/Handy/Handy/Source/Atom/HandyTabs/HandyTabComponent.swift new file mode 100644 index 0000000..bad2be9 --- /dev/null +++ b/Handy/Handy/Source/Atom/HandyTabs/HandyTabComponent.swift @@ -0,0 +1,120 @@ +// +// HandyTabComponent.swift +// Handy +// +// Created by chongin on 12/31/24. +// + +import UIKit + +/// ``HandyTabs``의 Header를 구성하는 cell입니다. +open class HandyTabComponent: 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: - private properties + + /// title이 표시될 label입니다. + private let titleLabel: HandyLabel = { + let label = HandyLabel(style: .B1Sb16) + label.alignment = .center + return label + }() + + /// 현재 선택됨을 알려주는, 아래쪽에 표시되는 검은색 바입니다. + private 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: - 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.top.bottom.equalToSuperview().inset(12).priority(999) + } + + selectedIndicator.snp.makeConstraints { + $0.bottom.equalToSuperview().priority(999) + $0.leading.trailing.equalToSuperview().inset(18).priority(999) + $0.height.equalTo(2) + } + + self.snp.makeConstraints { + $0.height.equalTo(48) + } + } + + 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 HandyTabComponent { + public enum SizeType { + case small + case large + } +} + +extension HandyTabComponent { + static let reuseIdentifier = "HandyTabComponent" +} diff --git a/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift b/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift new file mode 100644 index 0000000..b49eebe --- /dev/null +++ b/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift @@ -0,0 +1,211 @@ +// +// HandyTabs.swift +// Handy +// +// Created by chongin on 12/31/24. +// + +import UIKit + +open class HandyTabs: UIViewController { + // MARK: - open & public properties + /// `HandyTabComponent`의 크기를 결정합니다. + open var sizeType: HandyTabComponent.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 + } + + updateTabsHeaderLayout() + } + } + + /// 현재 선택되어있는 탭의 인덱스입니다. [0] 부터 시작합니다. + /// * `nil`인 경우 : 탭이 0개이거나 초기 상태입니다. + open var selectedIndex: Int? { + 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은 ``HandyTabComponent``만 등록되어 있습니다. + 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 + } + } + + /// Tab 간 간격을 의미합니다. + private let tabSpacing: CGFloat = 8.0 + + // MARK: - Init + public init(sizeType: HandyTabComponent.SizeType) { + self.sizeType = sizeType + super.init(nibName: nil, bundle: nil) + setTabsHeader() + setTabsContent() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // 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(HandyTabComponent.self, forCellWithReuseIdentifier: HandyTabComponent.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.minimumInteritemSpacing = tabSpacing + + switch tabsType { + case .scrollable: + layout.estimatedItemSize = CGSize(width: 0, height: 48) + case .fixed(let viewCount): + let totalSpacing = tabSpacing * CGFloat(viewCount - 1) + let itemWidth = (self.view.frame.width - totalSpacing) / CGFloat(viewCount) + layout.itemSize = CGSize(width: itemWidth, height: 48) + } + + 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: HandyTabComponent.reuseIdentifier, for: indexPath) as! HandyTabComponent + 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? HandyTabComponent { + 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) + } +} + From 1f7008edc67be6197026c1315759a1efef59aa6f Mon Sep 17 00:00:00 2001 From: "chongin12(Mosu)" Date: Tue, 31 Dec 2024 17:06:22 +0900 Subject: [PATCH 08/13] =?UTF-8?q?[#12]=20Main=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=EB=B7=B0=EB=A1=9C=20=EC=9E=AC?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Handy/Handy-Storybook/SceneDelegate.swift | 2 +- .../Storybook/MainViewController.swift | 87 +++++++++++++++++++ Handy/Handy.xcodeproj/project.pbxproj | 4 + 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 Handy/Handy-Storybook/Storybook/MainViewController.swift diff --git a/Handy/Handy-Storybook/SceneDelegate.swift b/Handy/Handy-Storybook/SceneDelegate.swift index a10553a..a1095e0 100644 --- a/Handy/Handy-Storybook/SceneDelegate.swift +++ b/Handy/Handy-Storybook/SceneDelegate.swift @@ -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 = TabsViewController() + window?.rootViewController = UINavigationController(rootViewController: MainViewController()) window?.makeKeyAndVisible() } diff --git a/Handy/Handy-Storybook/Storybook/MainViewController.swift b/Handy/Handy-Storybook/Storybook/MainViewController.swift new file mode 100644 index 0000000..83e307b --- /dev/null +++ b/Handy/Handy-Storybook/Storybook/MainViewController.swift @@ -0,0 +1,87 @@ +// +// MainViewController.swift +// Handy +// +// Created by chongin on 12/31/24. +// + +import UIKit + +class MainViewController: UITableViewController { + + private let components: [HandyStorybookComponent] = [ + .init(("SnackbarViewController", SnackbarViewController())), + .init(("LabelViewController", LabelViewController())), + .init(("FabViewController", FabViewController())), + .init(("HandyBoxButtonViewController", HandyBoxButtonViewController())), + .init(("ChipViewController", ChipViewController())), + .init(("DividerViewController", DividerViewController())), + .init(("CheckBoxViewController", CheckBoxViewController())), + .init(("RadioButtonViewController", RadioButtonViewController())), + .init(("HansySwitchViewController", HansySwitchViewController())), + .init(("TabsViewController", TabsViewController())), + ] + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") + } +} + +extension MainViewController { + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> 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 32fdb98..a679080 100644 --- a/Handy/Handy.xcodeproj/project.pbxproj +++ b/Handy/Handy.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ 6FD1A1802D213129001E6F2E /* TabsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1A17F2D21311C001E6F2E /* TabsViewController.swift */; }; 6FE73E942D23C52F00E06422 /* HandyTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE73E932D23C52C00E06422 /* HandyTabs.swift */; }; 6FE73E962D23C55900E06422 /* HandyTabComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE73E952D23C55900E06422 /* HandyTabComponent.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 */; }; @@ -127,6 +128,7 @@ 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 /* HandyTabComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyTabComponent.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 = ""; }; @@ -224,6 +226,7 @@ 0257765A2C4EB9B800272EC6 /* Storybook */ = { isa = PBXGroup; children = ( + 6FE73E972D23D6E500E06422 /* MainViewController.swift */, 0257763F2C4EA98E00272EC6 /* LaunchScreen.storyboard */, 0257765B2C4EB9C700272EC6 /* Base */, ); @@ -496,6 +499,7 @@ 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 */, From 092e1bc7487fb541a5f9f9fd841210c606faaa5f Mon Sep 17 00:00:00 2001 From: "chongin12(Mosu)" Date: Wed, 1 Jan 2025 16:26:27 +0900 Subject: [PATCH 09/13] =?UTF-8?q?[#12]=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EB=94=94=ED=85=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Atom/TabsViewController.swift | 29 ++++++++++++++----- .../Storybook/MainViewController.swift | 4 ++- .../Atom/HandyTabs/HandyTabComponent.swift | 15 ++++------ .../Source/Atom/HandyTabs/HandyTabs.swift | 17 ++++++----- 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/Handy/Handy-Storybook/Atom/TabsViewController.swift b/Handy/Handy-Storybook/Atom/TabsViewController.swift index f6d43b3..949807f 100644 --- a/Handy/Handy-Storybook/Atom/TabsViewController.swift +++ b/Handy/Handy-Storybook/Atom/TabsViewController.swift @@ -6,11 +6,12 @@ // import Handy +import UIKit final class TabsViewController: BaseViewController { - private let tabs: HandyTabs = { - let tabs = HandyTabs(sizeType: .small) - tabs.tabs = [ + let tabs: [(title: String, viewController: UIViewController)] + init(_ tabCount: Int) { + self.tabs = [ { let viewController = SnackbarViewController() return ("SnackbarViewController", viewController) @@ -47,17 +48,31 @@ final class TabsViewController: BaseViewController { let viewController = HansySwitchViewController() return ("HansySwitchViewController", viewController) }(), - ] + ][.. Date: Wed, 1 Jan 2025 16:28:00 +0900 Subject: [PATCH 10/13] =?UTF-8?q?[#12]=20HandyTabComponent=20->=20HandyTab?= =?UTF-8?q?Cell=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Handy/Handy.xcodeproj/project.pbxproj | 8 ++++---- ...{HandyTabComponent.swift => HandyTabCell.swift} | 10 +++++----- Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift | 14 +++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) rename Handy/Handy/Source/Atom/HandyTabs/{HandyTabComponent.swift => HandyTabCell.swift} (93%) diff --git a/Handy/Handy.xcodeproj/project.pbxproj b/Handy/Handy.xcodeproj/project.pbxproj index a679080..6a50aef 100644 --- a/Handy/Handy.xcodeproj/project.pbxproj +++ b/Handy/Handy.xcodeproj/project.pbxproj @@ -43,7 +43,7 @@ 2D41E8162C5A21B50043161D /* HandyFab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D41E8152C5A21B50043161D /* HandyFab.swift */; }; 6FD1A1802D213129001E6F2E /* TabsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1A17F2D21311C001E6F2E /* TabsViewController.swift */; }; 6FE73E942D23C52F00E06422 /* HandyTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE73E932D23C52C00E06422 /* HandyTabs.swift */; }; - 6FE73E962D23C55900E06422 /* HandyTabComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE73E952D23C55900E06422 /* HandyTabComponent.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 */; }; @@ -127,7 +127,7 @@ 2D41E8152C5A21B50043161D /* HandyFab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyFab.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 /* HandyTabComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyTabComponent.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 = ""; }; @@ -344,7 +344,7 @@ 6FE73E922D23C51600E06422 /* HandyTabs */ = { isa = PBXGroup; children = ( - 6FE73E952D23C55900E06422 /* HandyTabComponent.swift */, + 6FE73E952D23C55900E06422 /* HandyTabCell.swift */, 6FE73E932D23C52C00E06422 /* HandyTabs.swift */, ); path = HandyTabs; @@ -533,7 +533,7 @@ 02BCB2302CDF417500D0C796 /* HandyListItem.swift in Sources */, A5F6D36B2C96F32D00FB961F /* HandyDivider.swift in Sources */, 02BDB7FC2C3E99920050FB67 /* HandyFont.swift in Sources */, - 6FE73E962D23C55900E06422 /* HandyTabComponent.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/Source/Atom/HandyTabs/HandyTabComponent.swift b/Handy/Handy/Source/Atom/HandyTabs/HandyTabCell.swift similarity index 93% rename from Handy/Handy/Source/Atom/HandyTabs/HandyTabComponent.swift rename to Handy/Handy/Source/Atom/HandyTabs/HandyTabCell.swift index a275fb7..8c27fdd 100644 --- a/Handy/Handy/Source/Atom/HandyTabs/HandyTabComponent.swift +++ b/Handy/Handy/Source/Atom/HandyTabs/HandyTabCell.swift @@ -1,5 +1,5 @@ // -// HandyTabComponent.swift +// HandyTabCell.swift // Handy // // Created by chongin on 12/31/24. @@ -8,7 +8,7 @@ import UIKit /// ``HandyTabs``의 Header를 구성하는 cell입니다. -open class HandyTabComponent: UICollectionViewCell { +open class HandyTabCell: UICollectionViewCell { // MARK: - open & public properties /// 글씨 크기를 결정합니다. @@ -105,13 +105,13 @@ open class HandyTabComponent: UICollectionViewCell { } } -extension HandyTabComponent { +extension HandyTabCell { public enum SizeType { case small case large } } -extension HandyTabComponent { - static let reuseIdentifier = "HandyTabComponent" +extension HandyTabCell { + static let reuseIdentifier = "HandyTabCell" } diff --git a/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift b/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift index dd9e5f0..342576b 100644 --- a/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift +++ b/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift @@ -9,8 +9,8 @@ import UIKit open class HandyTabs: UIViewController { // MARK: - open & public properties - /// `HandyTabComponent`의 크기를 결정합니다. - open var sizeType: HandyTabComponent.SizeType + /// `HandyTabCell`의 크기를 결정합니다. + open var sizeType: HandyTabCell.SizeType /// 보여질 탭의 정보를 의미합니다. @@ -63,7 +63,7 @@ open class HandyTabs: UIViewController { // MARK: - private properties /// Tab의 정보 중 `title` 부분이 표현되는 헤더 영역입니다. - /// `FlowLayout`으로 구성되어 있습니다. cell은 ``HandyTabComponent``만 등록되어 있습니다. + /// `FlowLayout`으로 구성되어 있습니다. cell은 ``HandyTabCell``만 등록되어 있습니다. private var tabsHeader: UICollectionView! /// Tab의 정보 중 `ViewController` 부분이 표현되는 콘텐츠 영역입니다. @@ -81,7 +81,7 @@ open class HandyTabs: UIViewController { } // MARK: - Init - public init(sizeType: HandyTabComponent.SizeType) { + public init(sizeType: HandyTabCell.SizeType) { self.sizeType = sizeType super.init(nibName: nil, bundle: nil) setTabsHeader() @@ -113,7 +113,7 @@ open class HandyTabs: UIViewController { } // register cells - tabsHeader.register(HandyTabComponent.self, forCellWithReuseIdentifier: HandyTabComponent.reuseIdentifier) + tabsHeader.register(HandyTabCell.self, forCellWithReuseIdentifier: HandyTabCell.reuseIdentifier) // set delegates tabsHeader.delegate = self @@ -169,7 +169,7 @@ extension HandyTabs: UICollectionViewDelegate, UICollectionViewDataSource { } public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HandyTabComponent.reuseIdentifier, for: indexPath) as! HandyTabComponent + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HandyTabCell.reuseIdentifier, for: indexPath) as! HandyTabCell cell.sizeType = sizeType cell.title = tabs[indexPath.row].title return cell @@ -190,7 +190,7 @@ extension HandyTabs: UICollectionViewDelegate, UICollectionViewDataSource { collectionView.reloadItems(at: [indexPath]) } completion: { _ in UIView.animate(withDuration: 0.3) { - if let selectedCell = collectionView.cellForItem(at: indexPath) as? HandyTabComponent { + if let selectedCell = collectionView.cellForItem(at: indexPath) as? HandyTabCell { selectedCell.isSelected = true } } From 8fe0d2daa44332a7fd107826f7ad1748144db110 Mon Sep 17 00:00:00 2001 From: "chongin12(Mosu)" Date: Wed, 1 Jan 2025 19:00:27 +0900 Subject: [PATCH 11/13] =?UTF-8?q?[#12]=20TabsViewController=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=83=AD=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Atom/TabsViewController.swift | 31 +++++++++++++++---- .../Source/Atom/HandyTabs/HandyTabs.swift | 4 ++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Handy/Handy-Storybook/Atom/TabsViewController.swift b/Handy/Handy-Storybook/Atom/TabsViewController.swift index 949807f..1f1b09a 100644 --- a/Handy/Handy-Storybook/Atom/TabsViewController.swift +++ b/Handy/Handy-Storybook/Atom/TabsViewController.swift @@ -9,7 +9,19 @@ import Handy import UIKit final class TabsViewController: BaseViewController { - let tabs: [(title: String, viewController: UIViewController)] + 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 = [ { @@ -58,23 +70,30 @@ final class TabsViewController: BaseViewController { override func viewDidLoad() { super.viewDidLoad() + addingTabButton.addTarget(self, action: #selector(addingTabButtonDidTap(_:)), for: .touchUpInside) self.handyTabs.tabs = self.tabs } - private let handyTabs: HandyTabs = { - let tabs = HandyTabs(sizeType: .small) - return tabs - }() + @objc private func addingTabButtonDidTap(_ sender: UIButton) { + let randomViewController = UIViewController() + randomViewController.view.backgroundColor = UIColor.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1), alpha: 1) + handyTabs.tabs.append(("newTab!!", randomViewController)) + } override func setViewHierarchies() { self.addChild(handyTabs) self.view.addSubview(handyTabs.view) + self.view.addSubview(addingTabButton) } override func setViewLayouts() { handyTabs.view.snp.makeConstraints { $0.edges.equalTo(self.view.safeAreaLayoutGuide) } + + addingTabButton.snp.makeConstraints { + $0.trailing.bottom.equalTo(self.view.safeAreaLayoutGuide).inset(32) + $0.width.height.equalTo(100) + } } } - diff --git a/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift b/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift index 342576b..8f20761 100644 --- a/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift +++ b/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift @@ -27,13 +27,15 @@ open class HandyTabs: UIViewController { selectedIndex = tabs.count - 1 } + tabsHeader.reloadData() // reload 후 layout을 수정해야 정상적으로 작동합니다. + updateTabsHeaderLayout() } } /// 현재 선택되어있는 탭의 인덱스입니다. [0] 부터 시작합니다. /// * `nil`인 경우 : 탭이 0개이거나 초기 상태입니다. - open var selectedIndex: Int? { + open var selectedIndex: Int? = nil { didSet { if let oldValue { let previousViewController = tabs[oldValue].viewController From a1d458ba1a332482ddbb453b2c832cc45c4bf7db Mon Sep 17 00:00:00 2001 From: "chongin12(Mosu)" Date: Thu, 2 Jan 2025 15:26:51 +0900 Subject: [PATCH 12/13] =?UTF-8?q?[#12]=20=EC=85=80=20=EC=95=84=EB=9E=98?= =?UTF-8?q?=EC=AA=BD=EC=97=90=20=EC=8B=A4=EC=84=A0(border)=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Handy/Handy.xcodeproj/project.pbxproj | 4 +++ .../Source/Atom/HandyTabs/HandyTabCell.swift | 6 ++++ .../Source/Atom/HandyTabs/HandyTabs.swift | 8 +++-- Handy/Handy/Source/Extension/CALayer+.swift | 36 +++++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 Handy/Handy/Source/Extension/CALayer+.swift diff --git a/Handy/Handy.xcodeproj/project.pbxproj b/Handy/Handy.xcodeproj/project.pbxproj index 6a50aef..6944502 100644 --- a/Handy/Handy.xcodeproj/project.pbxproj +++ b/Handy/Handy.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ 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 */; }; @@ -125,6 +126,7 @@ 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 = ""; }; @@ -336,6 +338,7 @@ 02ED76482C577998001569F1 /* Extension */ = { isa = PBXGroup; children = ( + 6F1A18CF2D26606A004C6083 /* CALayer+.swift */, 02ED76492C5779C3001569F1 /* UIImage+.swift */, ); path = Extension; @@ -530,6 +533,7 @@ 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 */, diff --git a/Handy/Handy/Source/Atom/HandyTabs/HandyTabCell.swift b/Handy/Handy/Source/Atom/HandyTabs/HandyTabCell.swift index 8c27fdd..0e5263e 100644 --- a/Handy/Handy/Source/Atom/HandyTabs/HandyTabCell.swift +++ b/Handy/Handy/Source/Atom/HandyTabs/HandyTabCell.swift @@ -56,6 +56,12 @@ open class HandyTabCell: UICollectionViewCell { 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() { diff --git a/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift b/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift index 8f20761..0bb56cb 100644 --- a/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift +++ b/Handy/Handy/Source/Atom/HandyTabs/HandyTabs.swift @@ -86,14 +86,18 @@ open class HandyTabs: UIViewController { public init(sizeType: HandyTabCell.SizeType) { self.sizeType = sizeType super.init(nibName: nil, bundle: nil) - setTabsHeader() - setTabsContent() } 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 부분을 만드는 메소드입니다. 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) + } + } +} From 0362c4b1c6a196680f85c4aca2e374b81821c064 Mon Sep 17 00:00:00 2001 From: "chongin12(Mosu)" Date: Thu, 9 Jan 2025 15:03:23 +0900 Subject: [PATCH 13/13] =?UTF-8?q?[#12]=20MainViewController=EC=97=90=20Han?= =?UTF-8?q?dyList=20=EC=9A=94=EC=86=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Handy/Handy-Storybook/Storybook/MainViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Handy/Handy-Storybook/Storybook/MainViewController.swift b/Handy/Handy-Storybook/Storybook/MainViewController.swift index 1ffbbb7..90b396c 100644 --- a/Handy/Handy-Storybook/Storybook/MainViewController.swift +++ b/Handy/Handy-Storybook/Storybook/MainViewController.swift @@ -22,6 +22,7 @@ class MainViewController: UITableViewController { .init(("TabsViewController(2개)", TabsViewController(2))), .init(("TabsViewController(3개)", TabsViewController(3))), .init(("TabsViewController(9개)", TabsViewController(9))), + .init(("HandyListViewController", HandyListViewController())), ] override func viewDidLoad() {