티스토리 뷰

리스트 형태의 뷰는 모바일 개발자라면 많이 만들게 되는 뷰일 것이다.

기존 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 액션까지도 관리하도록 했었다.

이처럼 열거형을 잘 이용하면 보다 명확하고, 유연한이 좋은 코드가 될 것이다..

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
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
글 보관함