Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

커스텀 네비게이션 바 구현 #116

Merged
merged 6 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion client/Projects/OpenList/OpenList.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
4DCF10F02B10EC4300C614B7 /* LinkedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DCF10EF2B10EC4300C614B7 /* LinkedList.swift */; };
4DD947052B044FF20054C104 /* CheckListTableItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DD947042B044FF20054C104 /* CheckListTableItem.swift */; };
4DD97D982B14934000DF73BC /* DeepLinkTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DD97D972B14934000DF73BC /* DeepLinkTarget.swift */; };
4DD97D9A2B14C9AD00DF73BC /* UIImage+Resize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DD97D992B14C9AD00DF73BC /* UIImage+Resize.swift */; };
4DD97D9C2B14D3A600DF73BC /* DetailCheckListUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DD97D9B2B14D3A600DF73BC /* DetailCheckListUseCase.swift */; };
4DD97D9E2B14D56B00DF73BC /* CheckList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DD97D9D2B14D56B00DF73BC /* CheckList.swift */; };
4DDC43262B0F0FAB00859B28 /* CRDTUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DDC43252B0F0FAB00859B28 /* CRDTUseCase.swift */; };
4DDC43292B0F10EF00859B28 /* CRDTRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DDC43282B0F10EF00859B28 /* CRDTRepository.swift */; };
4DDC432F2B0F16CC00859B28 /* WithDetailCheckListAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DDC432A2B0F16CC00859B28 /* WithDetailCheckListAction.swift */; };
Expand Down Expand Up @@ -81,6 +84,9 @@
5FA1F8DF2AFF809C00869079 /* libCustomNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5FA1F8DD2AFF809C00869079 /* libCustomNetwork.a */; };
5FA1F8E02AFF809C00869079 /* libCustomSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5FA1F8DE2AFF809C00869079 /* libCustomSocket.a */; };
5FA1F8E32AFF818E00869079 /* TabBarViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA1F8E22AFF818E00869079 /* TabBarViewFactory.swift */; };
5FA8466B2B10A08B00B90F85 /* UIDevice+safeAreaHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA8466A2B10A08B00B90F85 /* UIDevice+safeAreaHeight.swift */; };
5FCCA3C12B104ABF00496EB2 /* OpenListNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCCA3C02B104ABF00496EB2 /* OpenListNavigationBar.swift */; };
5FCCA3C32B104AE200496EB2 /* OpenListNavigationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCCA3C22B104AE200496EB2 /* OpenListNavigationItem.swift */; };
78C2EFFF2B03D58700E4EC4E /* AddTabAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C2EFFA2B03D58700E4EC4E /* AddTabAction.swift */; };
78C2F0002B03D58700E4EC4E /* AddTabRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C2EFFB2B03D58700E4EC4E /* AddTabRouter.swift */; };
78C2F0012B03D58700E4EC4E /* AddTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C2EFFC2B03D58700E4EC4E /* AddTabViewController.swift */; };
Expand Down Expand Up @@ -149,6 +155,9 @@
4DCF10EF2B10EC4300C614B7 /* LinkedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedList.swift; sourceTree = "<group>"; };
4DD947042B044FF20054C104 /* CheckListTableItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckListTableItem.swift; sourceTree = "<group>"; };
4DD97D972B14934000DF73BC /* DeepLinkTarget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkTarget.swift; sourceTree = "<group>"; };
4DD97D992B14C9AD00DF73BC /* UIImage+Resize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resize.swift"; sourceTree = "<group>"; };
4DD97D9B2B14D3A600DF73BC /* DetailCheckListUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailCheckListUseCase.swift; sourceTree = "<group>"; };
4DD97D9D2B14D56B00DF73BC /* CheckList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckList.swift; sourceTree = "<group>"; };
4DDC43252B0F0FAB00859B28 /* CRDTUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CRDTUseCase.swift; sourceTree = "<group>"; };
4DDC43282B0F10EF00859B28 /* CRDTRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CRDTRepository.swift; sourceTree = "<group>"; };
4DDC432A2B0F16CC00859B28 /* WithDetailCheckListAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithDetailCheckListAction.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -198,6 +207,9 @@
5FA1F8DD2AFF809C00869079 /* libCustomNetwork.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libCustomNetwork.a; sourceTree = BUILT_PRODUCTS_DIR; };
5FA1F8DE2AFF809C00869079 /* libCustomSocket.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libCustomSocket.a; sourceTree = BUILT_PRODUCTS_DIR; };
5FA1F8E22AFF818E00869079 /* TabBarViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewFactory.swift; sourceTree = "<group>"; };
5FA8466A2B10A08B00B90F85 /* UIDevice+safeAreaHeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+safeAreaHeight.swift"; sourceTree = "<group>"; };
5FCCA3C02B104ABF00496EB2 /* OpenListNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenListNavigationBar.swift; sourceTree = "<group>"; };
5FCCA3C22B104AE200496EB2 /* OpenListNavigationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenListNavigationItem.swift; sourceTree = "<group>"; };
78C2EFFA2B03D58700E4EC4E /* AddTabAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTabAction.swift; sourceTree = "<group>"; };
78C2EFFB2B03D58700E4EC4E /* AddTabRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTabRouter.swift; sourceTree = "<group>"; };
78C2EFFC2B03D58700E4EC4E /* AddTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTabViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -246,7 +258,6 @@
5F70C83E2B04C6CA00826B5D /* UIKit */,
4D3397FB2B02399300963664 /* Publisher+Operator.swift */,
78C2F01D2B04943E00E4EC4E /* String+.swift */,
4D967CD32B050EA60032E0D7 /* UIFont+.swift */,
4D967D162B0617260032E0D7 /* Future+.swift */,
);
path = Extension;
Expand Down Expand Up @@ -383,6 +394,7 @@
isa = PBXGroup;
children = (
4DD947042B044FF20054C104 /* CheckListTableItem.swift */,
4DD97D9D2B14D56B00DF73BC /* CheckList.swift */,
4DDC43372B0F214300859B28 /* CheckListItem.swift */,
4DDC434A2B0F432900859B28 /* Device.swift */,
4D696CBA2B0F946100873B3B /* EditText.swift */,
Expand Down Expand Up @@ -453,6 +465,9 @@
isa = PBXGroup;
children = (
5F70C83F2B04C6F000826B5D /* UITableView+Extensions.swift */,
4DD97D992B14C9AD00DF73BC /* UIImage+Resize.swift */,
4D967CD32B050EA60032E0D7 /* UIFont+.swift */,
5FA8466A2B10A08B00B90F85 /* UIDevice+safeAreaHeight.swift */,
);
path = UIKit;
sourceTree = "<group>";
Expand Down Expand Up @@ -563,6 +578,7 @@
78C2F01B2B0490D300E4EC4E /* ValidCheckUseCase.swift */,
78C2F0212B049A8A00E4EC4E /* PersistenceUseCase.swift */,
4DDC43252B0F0FAB00859B28 /* CRDTUseCase.swift */,
4DD97D9B2B14D3A600DF73BC /* DetailCheckListUseCase.swift */,
);
path = UseCases;
sourceTree = "<group>";
Expand Down Expand Up @@ -650,6 +666,8 @@
children = (
78C2F0062B04657600E4EC4E /* ConfirmButton.swift */,
78C2F02D2B05427E00E4EC4E /* OpenListTextField.swift */,
5FCCA3C02B104ABF00496EB2 /* OpenListNavigationBar.swift */,
5FCCA3C22B104AE200496EB2 /* OpenListNavigationItem.swift */,
);
path = Sources;
sourceTree = "<group>";
Expand Down Expand Up @@ -830,6 +848,7 @@
78C2F0222B049A8A00E4EC4E /* PersistenceUseCase.swift in Sources */,
4D8AEBDA2B122AA400292AF3 /* CRDTRequestDTO.swift in Sources */,
5F70C84F2B0600B800826B5D /* DefaultCheckListStorage.swift in Sources */,
4DD97D9A2B14C9AD00DF73BC /* UIImage+Resize.swift in Sources */,
5F8615E22B0224E100CF2686 /* ViewBindable.swift in Sources */,
4DDC43332B0F16CC00859B28 /* WithDetailCheckListViewModel.swift in Sources */,
5FA1F8D32AFF802900869079 /* TabBarPage.swift in Sources */,
Expand All @@ -840,13 +859,15 @@
4DDC434B2B0F432900859B28 /* Device.swift in Sources */,
5FA1F8A92AFF7F6700869079 /* AppDelegate.swift in Sources */,
4D967CF12B05AF940032E0D7 /* CoreDataStorage.swift in Sources */,
4DD97D9C2B14D3A600DF73BC /* DetailCheckListUseCase.swift in Sources */,
4DDC43492B0F39EB00859B28 /* DefaultCRDTStorage.swift in Sources */,
4D8AEBD82B121F3A00292AF3 /* WithCheckListItem.swift in Sources */,
78C2F00A2B0487B400E4EC4E /* UITextField+Combine.swift in Sources */,
5F70C83B2B04BF9A00826B5D /* CheckListItemButton.swift in Sources */,
4DDC432F2B0F16CC00859B28 /* WithDetailCheckListAction.swift in Sources */,
5F70C8462B05DC5100826B5D /* CheckListHeaderView.swift in Sources */,
4DDC43262B0F0FAB00859B28 /* CRDTUseCase.swift in Sources */,
4DD97D9E2B14D56B00DF73BC /* CheckList.swift in Sources */,
4D33980C2B023BE800963664 /* CheckListTableViewFactory.swift in Sources */,
5FA1F8E32AFF818E00869079 /* TabBarViewFactory.swift in Sources */,
4D3398082B023BE800963664 /* CheckListTableAction.swift in Sources */,
Expand All @@ -861,7 +882,9 @@
78C2F0022B03D58700E4EC4E /* AddTabViewFactory.swift in Sources */,
4DDC43292B0F10EF00859B28 /* CRDTRepository.swift in Sources */,
5FA1F8D12AFF802900869079 /* RecommendTabViewController.swift in Sources */,
5FCCA3C12B104ABF00496EB2 /* OpenListNavigationBar.swift in Sources */,
5F70C8392B04BF7A00826B5D /* LocalCheckListItem.swift in Sources */,
5FA8466B2B10A08B00B90F85 /* UIDevice+safeAreaHeight.swift in Sources */,
5FA1F8A62AFF7E6C00869079 /* AppComponent.swift in Sources */,
4DDC43362B0F19F700859B28 /* WithDetailCheckListDiffableDataSource.swift in Sources */,
4DDC43312B0F16CC00859B28 /* WithDetailCheckListViewController.swift in Sources */,
Expand All @@ -885,6 +908,7 @@
4DD947052B044FF20054C104 /* CheckListTableItem.swift in Sources */,
4DDC43302B0F16CC00859B28 /* WithDetailCheckListRouter.swift in Sources */,
4D8AEBDC2B122AAD00292AF3 /* CRDTResponseDTO.swift in Sources */,
5FCCA3C32B104AE200496EB2 /* OpenListNavigationItem.swift in Sources */,
78C2F01C2B0490D300E4EC4E /* ValidCheckUseCase.swift in Sources */,
78C2F01E2B04943E00E4EC4E /* String+.swift in Sources */,
78C2F02E2B05427E00E4EC4E /* OpenListTextField.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion client/Projects/OpenList/OpenList/App/DeepLinkTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ import Foundation

enum DeepLinkTarget {
/// openlist://shared-checklists?shared-checklistId=\(id)
case routeToSharedCheckList(id: String)
case routeToSharedCheckList(id: UUID)
}
8 changes: 6 additions & 2 deletions client/Projects/OpenList/OpenList/App/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,15 @@ private extension SceneDelegate {
}

func executeSharedCheckListDeepLink(_ url: URL) {
let components = URLComponents(url: url, resolvingAgainstBaseURL: true)
guard url.host() == SharedCheckListDeepLinkConstant.host else { return }
guard let scheme = url.scheme, scheme == SharedCheckListDeepLinkConstant.scheme else { return }
let components = URLComponents(url: url, resolvingAgainstBaseURL: true)
guard let queryItems = components?.queryItems else { return }
guard let id = queryItems.first(where: { $0.name == SharedCheckListDeepLinkConstant.query })?.value else { return }
guard let idString = queryItems.first(
where: { $0.name == SharedCheckListDeepLinkConstant.query }
)?.value
else { return }
guard let id = UUID(uuidString: idString) else { return }
deepLinkSubject.send(.routeToSharedCheckList(id: id))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,22 @@ extension DefaultCheckListRepository: CheckListRepository {
)
}
}

func fetchCheckList(id: UUID) async throws -> CheckList {
let result = try await checkListStorage.fetchCheckList(id: id)
return .init(
id: result.checklistId,
title: result.title,
createdAt: result.createdAt,
updatedAt: result.updatedAt,
progress: result.progress,
items: result.items.map {
.init(
itemId: $0.itemId,
title: $0.content,
isChecked: $0.isChecked
)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ import Foundation
protocol CheckListStorage {
func saveCheckList(id: UUID, title: String) async throws
func fetchAllCheckList() async throws -> [LocalStorageCheckListResponseDTO]
func fetchCheckList(id: UUID) async throws -> LocalStorageCheckListResponseDTO
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//
// OpenListNavigationBar.swift
// OpenList
//
// Created by 김영균 on 11/24/23.
//

import UIKit

protocol OpenListNavigationBarDelegate: AnyObject {
func openListNavigationBar(_ navigationBar: OpenListNavigationBar, didTapBackButton button: UIButton)
func openListNavigationBar(
_ navigationBar: OpenListNavigationBar,
didTapBarItem item: OpenListNavigationBarItem
)
}

final class OpenListNavigationBar: UIView {
enum Constant {
static let barHeight: CGFloat = 56
static let horizontalPadding: CGFloat = 20
static let itemSpacing: CGFloat = 24
}

// MARK: - Properties
weak var delegate: OpenListNavigationBarDelegate?

// MARK: - UI Components
private let leftView: UIStackView = .init()
private let rightView: UIStackView = .init()
internal private(set) var backButtonItem: OpenListNavigationBackButtonItem?
internal private(set) var leftItems: [OpenListNavigationBarItem]?
internal private(set) var rightItems: [OpenListNavigationBarItem]?

// MARK: - Initializers
init(
isBackButtonHidden: Bool = true,
backButtonTitle: String? = "",
leftItems: [OpenListNavigationItemType] = [],
rightItems: [OpenListNavigationItemType] = []
) {
self.leftItems = leftItems.map(OpenListNavigationBarItem.init(type:))
self.rightItems = rightItems.map(OpenListNavigationBarItem.init(type:))
if !isBackButtonHidden {
backButtonItem = OpenListNavigationBackButtonItem(backButtonTitle: backButtonTitle)
}

super.init(
frame: .init(
origin: .init(x: 0, y: UIDevice.safeAreaTopHeight),
size: .init(width: UIScreen.main.bounds.width, height: Constant.barHeight)
)
)

setViewAttributes()
setViewHierarchies()
setViewConstraints()
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

// MARK: - 네비게이션 바 아이템 추가 및 삭제 메서드
extension OpenListNavigationBar {
func appendLeftItem(_ item: OpenListNavigationBarItem) {
leftItems?.append(item)
leftView.addArrangedSubview(item)
}

func appendRightItem(_ item: OpenListNavigationBarItem) {
rightItems?.append(item)
rightView.addArrangedSubview(item)
}

func insertLeftItem(_ item: OpenListNavigationBarItem, at index: Int) {
leftItems?.insert(item, at: index)
leftView.insertArrangedSubview(item, at: index)
}

func insertRightItem(_ item: OpenListNavigationBarItem, at index: Int) {
rightItems?.insert(item, at: index)
rightView.insertArrangedSubview(item, at: index)
}

func removeLeftItem(_ item: OpenListNavigationBarItem) {
leftItems?.removeAll(where: { $0 == item })
leftView.removeArrangedSubview(item)
}

func removeRightItem(_ item: OpenListNavigationBarItem) {
rightItems?.removeAll(where: { $0 == item })
rightView.removeArrangedSubview(item)
}
}

// MARK: - UI 관련 메서드
private extension OpenListNavigationBar {
// MARK: View Attributes
func setViewAttributes() {
backgroundColor = .background
setLeftViewAttributes()
setRightViewAttributes()
setActionAttributes()
}

func setLeftViewAttributes() {
leftView.translatesAutoresizingMaskIntoConstraints = false
leftView.axis = .horizontal
leftView.alignment = .center
leftView.spacing = 24
}

func setRightViewAttributes() {
rightView.translatesAutoresizingMaskIntoConstraints = false
rightView.axis = .horizontal
rightView.alignment = .center
rightView.spacing = 24
}

func setActionAttributes() {
backButtonItem?.addTarget(self, action: #selector(didTapBackButton), for: .touchUpInside)
leftItems?.forEach {
$0.addTarget(self, action: #selector(didTapBarItem), for: .touchUpInside)
}
rightItems?.forEach {
$0.addTarget(self, action: #selector(didTapBarItem), for: .touchUpInside)
}
}

// MARK: View Hierarchies
func setViewHierarchies() {
if let backButtonItem = backButtonItem {
leftView.addArrangedSubview(backButtonItem)
}
if let leftItems = leftItems {
leftItems.forEach(leftView.addArrangedSubview)
}
if let rightItems = rightItems {
rightItems.forEach(rightView.addArrangedSubview)
}
addSubview(leftView)
addSubview(rightView)
}

// MARK: View Constratins
func setViewConstraints() {
setBackButtonConstraints()
setLeftViewConstraints()
setRightViewConstraints()
}

func setBackButtonConstraints() {
guard let backButtonItem = backButtonItem,
let textWidth = backButtonItem.titleLabel?.intrinsicContentSize.width
else { return }
backButtonItem.translatesAutoresizingMaskIntoConstraints = false
backButtonItem.widthAnchor.constraint(equalToConstant: 24 + textWidth).isActive = true
backButtonItem.heightAnchor.constraint(equalToConstant: 24).isActive = true
}

func setLeftViewConstraints() {
NSLayoutConstraint.activate([
leftView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constant.horizontalPadding),
leftView.centerYAnchor.constraint(equalTo: centerYAnchor)
])
if let leftItems = leftItems {
leftItems.forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.widthAnchor.constraint(equalToConstant: 24).isActive = true
$0.heightAnchor.constraint(equalToConstant: 24).isActive = true
}
}
}

func setRightViewConstraints() {
NSLayoutConstraint.activate([
rightView.leadingAnchor.constraint(
greaterThanOrEqualTo: leftView.trailingAnchor,
constant: Constant.itemSpacing
),
rightView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constant.horizontalPadding),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅎㅎ 위랑 아래랑 형식이 조금 다르네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 다음 커밋에서 수정했습니다 ㅎㅎ

rightView.centerYAnchor.constraint(equalTo: centerYAnchor)
])

if let rightItems = rightItems {
rightItems.forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.widthAnchor.constraint(equalToConstant: 24).isActive = true
$0.heightAnchor.constraint(equalToConstant: 24).isActive = true
}
}
}

// MARK: View Action Methods
@objc func didTapBackButton(_ button: UIButton) {
delegate?.openListNavigationBar(self, didTapBackButton: button)
}

@objc func didTapBarItem(_ item: OpenListNavigationBarItem) {
delegate?.openListNavigationBar(self, didTapBarItem: item)
}
}
Loading