-
Notifications
You must be signed in to change notification settings - Fork 0
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
DMVM-192(feat): navigation 구현 #22
base: develop
Are you sure you want to change the base?
Conversation
TabHeader를 사용하여 navigation을 구현할 수 있도록 수정 헤더의 우측 icon이 0, 1, 2일 경우를 모두 하나의 컴포넌트로 사용할 수 있도록 수정
notification API가 나오기 전이므로 DUMMY Data로 구현했습니다 TODO: Mongle Brand Color 및 디자인 시스템 도입 TODO: API 연결
This comment was marked as off-topic.
This comment was marked as off-topic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
정말 많은 작업을 해주셨어요!
고생많으셨습니다!!!
Navigation 부분은 제가 이어받아 추가작업을 진행하도록 할게요!
} | ||
.padding(.trailing, 40) | ||
} // ScrollView | ||
} // VStack | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#Preview {
NavigationStack {
HomeView()
}
}
네비게이션 기능이 연결이 되었으므로 Preview에서도 네비게이션 기능을 확인할 수 있게 NavigationStack
을 넣어주면 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영했습니다~
import SwiftUI | ||
|
||
struct MainView: View { | ||
@StateObject var navigationVM = NavigationViewModel(path: []) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NavigationViewModel에 path를 빈배열로 초기화 시키는 것을 인스턴스화 하는 과정에서 하는 것이 아닌
생성할때 빈 배열로 초기화시키면 가독성이 좋을 것 같습니다.
예시
MainView
@StateObject var navigationVM = NavigationViewModel()
NavigationViewModel
public init(path: [Screen] = []) {
self.path = path
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영했습니다
.navigationDestination(for: Screen.self) { route in | ||
switch route { | ||
case .home: | ||
HomeView() | ||
case .searchCompany: | ||
SearchView() | ||
case .storeDetail: | ||
Text("test") | ||
case .notification: | ||
NotificationView() | ||
} | ||
} // navigationDestination | ||
.environmentObject(navigationVM) | ||
.onOpenURL { url in | ||
navigationVM.handleDeeplink(url: url) | ||
} // openURL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이건 개인의 생각에 따라 다를 것 같은데 가독성을 봤을 때 긴 modifier를 맨 밑으로 가면 어떨까요?
저희가 modifier 규칙을 정하면 좋을 것 같아요!
.navigationDestination(for: Screen.self) { route in | |
switch route { | |
case .home: | |
HomeView() | |
case .searchCompany: | |
SearchView() | |
case .storeDetail: | |
Text("test") | |
case .notification: | |
NotificationView() | |
} | |
} // navigationDestination | |
.environmentObject(navigationVM) | |
.onOpenURL { url in | |
navigationVM.handleDeeplink(url: url) | |
} // openURL | |
.environmentObject(navigationVM) | |
.onOpenURL { url in | |
navigationVM.handleDeeplink(url: url) | |
} // openURL | |
.navigationDestination(for: Screen.self) { route in | |
switch route { | |
case .home: | |
HomeView() | |
case .searchCompany: | |
SearchView() | |
case .storeDetail: | |
Text("test") | |
case .notification: | |
NotificationView() | |
} | |
} // navigationDestination |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UI를 구성하는 구조체들은 한칸씩 개행하는거 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이게 무슨 말일까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lint 이야기 인데 저희 한 번 이야기 해봐요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
struct Notification: Identifiable { } 을 해서
ForEach(notifications) { notification in { } 을 하는 방법도 있는데 따로 hashable하는 이유가 있는지 궁금합니다!
어떤 방법이 좋은지 저도 잘몰라서 질문드려요
} | ||
.padding(.horizontal, 20) | ||
} // VStack | ||
.navigationBarBackButtonHidden(true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UINavigationController를 사용한다면 마찬가지로 제거가 필요합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
마찬가지로 한칸씩 개행하는거 어떠신가요? 이것도 회의하여 규칙을 정하면 좋겠네요!
public init( | ||
headerText: String, | ||
firstIconImageName: Image? = nil, | ||
firstDestination: FirstDestination, | ||
secondIconName: Image? = nil, | ||
secondDestination: SecondDestination | ||
) { | ||
self.headerText = headerText | ||
self.firstIconImageName = firstIconImageName | ||
self.firstDestination = firstDestination | ||
self.secondIconName = secondIconName | ||
self.secondDestination = secondDestination | ||
} | ||
|
||
public init( | ||
headerText: String, | ||
firstIconImageName: Image? = nil, | ||
firstDestination: FirstDestination | ||
) where SecondDestination == EmptyView { | ||
self.headerText = headerText | ||
self.firstIconImageName = firstIconImageName | ||
self.firstDestination = firstDestination | ||
self.secondIconName = nil | ||
self.secondDestination = EmptyView() | ||
} | ||
|
||
public init(headerText: String) where FirstDestination == EmptyView, SecondDestination == EmptyView { | ||
self.headerText = headerText | ||
self.iconImageName = iconImageName | ||
self.action = action | ||
self.firstIconImageName = nil | ||
self.firstDestination = EmptyView() | ||
self.secondIconName = nil | ||
self.secondDestination = EmptyView() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아래 코드로 하면 문제없이 작동됩니다. 기본값으로 EmptiyView()를 넣어서 생성자 코드를 한번만 작성해도 됩니다.
public init(
headerText: String,
firstIconImageName: Image? = nil,
firstDestination: FirstDestination = EmptyView(),
secondIconName: Image? = nil,
secondDestination: SecondDestination = EmptyView()
) {
self.headerText = headerText
self.firstIconImageName = firstIconImageName
self.firstDestination = firstDestination
self.secondIconName = secondIconName
self.secondDestination = secondDestination
}
수정했어용 |
오~~ 기대하고 있습니다 ㅎㅎ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트 하셨을때 따로 문제 없었을까요?
🎫 지라 티켓 이슈
https://dogbusiness.atlassian.net/browse/DMVM-192
🛠️ 작업 내용
해당 PR에서 중점적으로 구현한 부분은 화면간 이동을 위해 navigationStack을 구현하였습니다.
화면 간 이동을 구현하면서 각 View에 대해 피그마 확인하는 과정에서 일부 화면 리팩토링 및 디테일을 수정하였습니다.
하지만, 전체적인 View에 대해 리팩토링 및 디테일 수정을 한 것이 아니므로, 피쳐 작업을 하면서 추가적으로 뷰 수정이 필요해보이며, 이점 참고하여 추후 작업 시에 @bulmang 님께서도 수정 부탁드립니다.
간략한 작업 내용
📱 작업 화면
상단의 Header 리펙토링
HomeView(홈)와 CalandarView(예약내역)에서 사용되고 있는 상단의 TabHeaderComponent를 리팩토링하였습니다.
✅ AS-IS
기존의 코드에서는 아이콘이 1개만 사용 가능하였고, 그 위에 overlay로 2번째 아이콘을 추가하는 방식이었습니다. 또한, 각 icon은 Button으로 구성되어 있어 navigation에는 적합하지 않았습니다.
✅ TO-BE
리팩토링 후 개선된 코드에서는 재사용성을 고려하여 icon과 그에 따른 action에 대해 파라미터로 받고 있으며, optional로 처리하여 icon이 0개, 1개, 2개 있는 경우 모두 해당 컴포넌트를 사용할 수 있게 구현하였습니다.
하단 tabView를 연결하여 4개의 탭 아이템 구현 및 리펙토링
하단의 TabView에 대하여 4개의 View가 정상적으로 연결되어 있지 않아, 각 탭에 해당하는 뷰를 연걸하고, TabView를 가독성 및 유지보수 측면을 고려하여 리팩토링하였습니다.
✅ AS-IS
4개의 Tab이 모두 화면에 연결되어 있지 않으며, Tab의 아이콘 및 텍스트가 적절히 구현되어 있지 않고 있습니다.
✅ TO-BE
피그마의 탭뷰의 이미지를 export하여 적용하였으며, 각 탭에 따른 뷰를 연결하였습니다.
TabView를 사용하는 화면에서 각 탭의 뷰, 아이콘, 텍스트 등을 효율적으로 관리할 수 있도록 TabItem을 enum 타입으로 정의하였습니다.
반복되는 코드를 줄이고, 탭 관련 데이터를 한 곳에서 관리하여 유지보수성을 향상시키는 것을 목표로 하였습니다.
CaseIterable을 채택하여 탭 항목을 쉽게 순회할 수 있도록 구현했습니다.
✅ 신경 쓴 부분
(가독성을 위해 일부 코드만 발췌하였습니다)
navigationstack을 사용하여 뷰간 이동 구현
navigationStack 및 navigationLink를 사용하여 화면 간 이동을 구현하였습니다.
루트뷰인 MainView를 navigationStack으로 감싸주어야 navigation을 인식하여 화면 간 이동을 할 수 있습니다.
그리고, 이동하고 싶은 뷰를 NavigationLink의 destination에 넣어주고, 클로저 부분에는 화면에 노출되는 부분을 넣어 구현합니다.
예를 들어, CompanyListItem 클릭 시 CompanyDetailView로 이동하도록 구현했습니다.
✅ AS-IS
화면 전환이 구현되어 있지 않아 리스트에서 아이템 클릭 시 이동할 수 없었음
✅ TO-BE
navigationLink를 통해 화면 간 이동 가능
✅ 신경 쓴 부분
NavigationStack 사용 위치: MainView를 네비게이션의 루트로 설정하여, 앱의 전체 네비게이션 흐름을 쉽게 관리할 수 있도록 설계했습니다.
유연한 확장성: path를 ViewModel로 관리함으로써 다중 화면 이동 시에도 일관된 상태 관리를 유지할 수 있도록 준비했습니다.
✅ 검토 요청 부분
NavigationStack 및 NavigationLink의 사용 위치와 상태 관리 방법이 적절한지 의견 부탁드립니다.
화면 이동 로직에서 개선할 점이나 추가적으로 고려해야 할 사항이 있다면 알려주세요.
deeplink 구현
외부 링크를 통해 앱 실행 시 특정 화면으로 이동할 수 있는 딥링크 기능을 구현했습니다.
딥링크 URL 예시: mongle://searchCompany
✅ AS-IS
딥링크 X
✅ TO-BE
mongle://[host] 형식의 URL을 통해 앱이 실행되고, host 값에 따라 지정된 화면으로 이동 가능
✅ 신경 쓴 부분
URLScheme 및 host 부분은 해당 이미지 참고 부탁드립니다.
딥링크를 구현하기 위해서는 3Step이 필요합니다
[Step1]
Targets > Info > URL Types에 원하는 host를 추가해줍니다. mongle을 추가해주었습니다.
[Step2]
info.plist에 scheme를 추가해줍니다.
[Step3]
코드를 구현해줍니다.
Host: 화면 이동에 사용되는 값(home, searchCompany, notification, storeDetail).
🗒️ Note (optional)
NavigationViewModel을 StateObject로 구현하였습니다.
더 좋은 방법이나, navigationPath를 활용하는 방법에 대한 의견이 있다면 공유 부탁드립니다.
TODO: Calandar 뷰에서 "다가오는 예약"이 없을 경우, "목욕업체 둘러보기" 버튼 클릭 시 Tab이 Home으로 이동되어야 함
TODO: Network 레이어 개발 및 Entity, DTO 분리 필요
TODO: Hi-Fi 작업이 완료되었으므로 전체적인 뷰의 디테일을 수정할 필요가 있어보이며, 화면 구현 및 로직 구현 또한 필요해보입니다.
[레퍼런스]
딥링크(Deep Link) - URL Scheme
SwiftUI NavigationStack Router로 깔끔하게 관리하기