티스토리 뷰
리스트 형태의 뷰는 모바일 개발자라면 많이 만들게 되는 뷰일 것이다.
기존 UIKit에서는 UITableView 로 만들게 되었는데,
SwiftUI 에서도 UIKit과 메뉴를 관리하는 방식이 크게 다르지 않았다.
아래와 같이 프로필 뷰에 섹션과 메뉴가 있는 형태다. (이하 섹션, 메뉴)
이러한 구현할 때, 뷰를 일일이 나열할 수도 있지만
나는 UIKit에서부터 이러한 뷰는 enum 으로 만들어 관리하는걸 좋아한다.
SwiftUI에서도 enum 으로 관리했다.
1. Category & SubView(menu) Enum
enum MyPageCategoryTypes: CaseIterable {
case contactPoint
case myPrfile
case universityVerification
var headerTitle: String {
switch self {
case .contactPoint:
return "연락수단"
case .myPrfile:
return "내 미팅 프로필"
case .universityVerification:
return "대학교 인증"
}
}
var getSubViewTypes: [MyPageSubViewTypes] {
switch self {
case .contactPoint:
return [
.kakaoTalkId
]
case .myPrfile:
return [
.mbti,
.similarAnimal,
.physicalHeight
]
case .universityVerification:
return [
.emailVerification
]
}
}
//MARK: 하위 메뉴 타입
enum MyPageSubViewTypes: String {
case kakaoTalkId
case mbti
case similarAnimal
case physicalHeight
case emailVerification
var title: String {
switch self {
case .kakaoTalkId: return "카카오톡 ID"
case .mbti: return "성격 유형"
case .similarAnimal: return "닮은 동물"
case .physicalHeight: return "키"
case .emailVerification: return "학교 메일 인증"
}
}
var icon: Image {
switch self {
case .kakaoTalkId:
return DesignSystem.Icons.iconKakao
case .mbti:
return DesignSystem.Icons.puzzle
case .similarAnimal:
return DesignSystem.Icons.footprint
case .physicalHeight:
return DesignSystem.Icons.ruler
case .emailVerification:
return DesignSystem.Icons.eMail
}
}
}
}
구현한 모델의 형태는 MyPageCategoryTypes 열거형 내부에 MyPageSubViewTypes이 존재하는 형태다.
또한 MyPageCategoryTypes의 각 case 들은 각자 가지고 있는 [MyPageSubViewTypes] 을 리턴한다.
그럼 구현한 열거형을 뷰에 그려보자.
2. View
{
... 생략 ...
ScrollView {
VStack(spacing: 0) {
// 1. 카테고리 순회
ForEach(MyPageCategoryTypes.allCases, id: \.self) { category in
// 2. 카테고리 헤더 뷰 생성
MyPageSubViewHeaderView(headerTitle: category.headerTitle)
if let userInfo = viewStore.myUserInfo {
// 3. 카테고리 내부 SubView 순회
ForEach(0 ..< category.getSubViewTypes.count, id: \.self) { index in
// 4. SubView 생성
let viewType = category.getSubViewTypes[index]
MyPageSubSectionView(
index: index,
viewType: viewType,
userInfo: userInfo
)
.contentShape(Rectangle())
.onTapGesture {
viewStore.send(.didTappedSubViews(view: viewType))
}
}
Spacer()
.frame(height: 12)
}
}
}
.padding(.horizontal, 16)
}
... 생략 ...
}
코드는 길어서 생략했지만 대략 이런 형태다.
1. MyPageCategoryTypes 는 caseIterable을 채택했기 때문에 .allCases로 모든 case를 순회한다.
2. HeaderView를 그려주고, getSubViewTypes 프로퍼티에서 가지고 있는 메뉴의 배열을 가지고 온다.
3. 메뉴(SubView) 배열을 순회하며 하위 메뉴들을 그린다.
4. 하위 메뉴에 onTapGesture를 달아서 액션을 만든다.
( 해당 프로젝트는 TCA를 사용하였기 때문에 MyPageCategoryTypes.MyPageSubViewTypes 를 바로 액션으로 보냈다. 해당 액션을 받는 쪽에서는 switch를 통해 간편하게 분기할 수 있다. TCA가 아닌 경우라도 subViewType을 받는 함수를 만들어 사용 가능하다. )
enum Action {
case didTappedSubViews(view: MyPageCategoryTypes.MyPageSubViewTypes)
}
섹션과 메뉴의 뷰 구성은 다음과 같다.
fileprivate struct MyPageSubViewHeaderView: View {
let headerTitle: String
fileprivate var body: some View {
HStack {
Text(headerTitle)
.font(.pretendard(._600, size: 14))
.foregroundStyle(DesignSystem.Colors.textGray)
Spacer()
}
.frame(height: 54)
}
}
fileprivate struct MyPageSubSectionView: View {
let index: Int
let viewType: MyPageCategoryTypes.MyPageSubViewTypes
let userInfo: MyUserInfoModel
fileprivate var body: some View {
ZStack {
VStack(spacing: 0) {
Rectangle()
.frame(height: 1)
.foregroundStyle(DesignSystem.Colors.darkGray)
Spacer()
}
HStack {
viewType.icon
.resizable()
.frame(width: 24, height: 24)
Text(viewType.title)
.font(.pretendard(._500, size: 16))
Spacer()
Text(viewType.actionTitle(by: userInfo))
.font(.pretendard(._500, size: 14))
.foregroundStyle(viewType.foregroundColor(by: userInfo))
Image(systemName: "chevron.right")
.fontWeight(.semibold)
.foregroundStyle(DesignSystem.Colors.textGray)
}
}
.frame(height: 54)
}
}
각 메뉴에서는 MyPageSubViewTypes 열거형에 각 case별로 넣어준 연산 프로퍼티에 의해 title, icon 등을 사용한다.
3. 조건 로직
각 메뉴별로 데이터가 존재하는 경우와 아닌 경우에 따라 글자 색상과 텍스트가 달라져야 하는 요구사항이 있었다.
이 경우 MyPageSubViewTypes 내부에 함수를 넣어 로직을 만들었다.
//MARK: 하위 메뉴 타입
enum MyPageSubViewTypes: String {
case kakaoTalkId
case mbti
case similarAnimal
case physicalHeight
case emailVerification
var title: String {
switch self {
case .kakaoTalkId: return "카카오톡 ID"
case .mbti: return "성격 유형"
case .similarAnimal: return "닮은 동물"
case .physicalHeight: return "키"
case .emailVerification: return "학교 메일 인증"
}
}
var icon: Image {
switch self {
case .kakaoTalkId:
return DesignSystem.Icons.iconKakao
case .mbti:
return DesignSystem.Icons.puzzle
case .similarAnimal:
return DesignSystem.Icons.footprint
case .physicalHeight:
return DesignSystem.Icons.ruler
case .emailVerification:
return DesignSystem.Icons.eMail
}
}
func isSubMenuFilled(_ userModel: MyUserInfoModel) -> Bool {
switch self {
case .kakaoTalkId:
return userModel.kakaoId != ""
case .mbti:
return userModel.mbti != ""
case .similarAnimal:
return userModel.animalType != nil
case .physicalHeight:
return userModel.height != nil
case .emailVerification:
return userModel.isUniversityEmailVerified
}
}
func foregroundColor(by userModel: MyUserInfoModel) -> Color {
return isSubMenuFilled(userModel) ? DesignSystem.Colors.textGray : DesignSystem.Colors.defaultBlue
}
func actionTitle(by userModel: MyUserInfoModel) -> String {
if !isSubMenuFilled(userModel) {
return "30실 받기"
}
switch self {
case .kakaoTalkId: return userModel.kakaoId ?? ""
case .mbti: return userModel.mbti
case .similarAnimal: return userModel.animalType ?? ""
case .physicalHeight: return String(userModel.height ?? 0)
case .emailVerification: return "인증됨"
}
}
}
먼저, 데이터가 존재하는 상태인지를 체크하는 isSubMenuFilled 함수를 만들었다.
그리고 해당 함수는 foregroundColor 와 actionTitle 에서 각각 사용된다.
특정 조건이 필요하다면, 조건에 필요한 내용을 파라미터로 넣어 분기처리가 가능하다.
4.마무리
이렇게 enum으로 메뉴를 관리하는 이유는 유지보수 및 관리가 용이해지기 때문이다.
기획에 따라 메뉴가 추가되거나 삭제될 때 뷰의 코드를 수정하는 것이 아닌,
enum에 case를 추가하는 작은 수정으로도 메뉴를 만들 수 있다.
UIKit에서도 TableView가 enum에 의해 관리되도록 즐겨 구현했었는데,
때에 따라서는 TableView의 Click 액션까지도 관리하도록 했었다.
이처럼 열거형을 잘 이용하면 보다 명확하고, 유연한이 좋은 코드가 될 것이다..
'iOS' 카테고리의 다른 글
[iOS] 단일타겟 프로젝트를 멀티모듈로 바꾸었다. (1) | 2024.05.16 |
---|---|
[iOS] SwiftUI에서 커스텀 Alert 만들기(Animation도) (0) | 2024.05.01 |
[iOS] 서버 환경 분리하기(prod/dev, with Tuist) (0) | 2024.04.19 |
[iOS] SwiftUI 그라데이션 응용하기 (Stepper) (0) | 2024.04.17 |
[iOS] 오픈소스 기여를 해보았다. (2) | 2024.01.04 |
- Total
- Today
- Yesterday
- retry
- locale
- demical
- openapi-generator
- IOS
- 2024년
- Swift
- 회고
- 애플워치
- DateFormatter
- open-api-generator
- AVFoundation
- 소수점
- Xcode
- easy cue
- auth
- keyboardtype
- SwiftUI
- 토큰
- swift날짜
- KVO
- watch connectivity
- flo
- watchOS
- 애플워치 데이터 전송
- musicplayer
- Xcode15
- OAS
- avplayer
- TextField
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |