본문 바로가기

프로젝트/Twitter Clone App (w∕Firebase)

05. 기본 UI 구성하기 #5

기본 UI 구성하기 #5
SideMenuView & ContentView


SideMenuView
| UserStatsView

var userInfoDetails: some View {
    VStack(alignment: .leading, spacing: 4) {
        HStack {
            Text("Archimedes")
                .font(.title2).bold()

            Image(systemName: "checkmark.seal.fill")
                .foregroundColor(Color(.systemBlue))
        }

        Text("@mathematician")
            .font(.subheadline)
            .foregroundColor(.gray)

        Text("Eureka!")
            .font(.subheadline)
            .padding(.vertical)

        HStack(spacing: 24) {
            HStack {
                Image(systemName: "mappin.and.ellipse")

                Text("Syracuse, Italia")
            }

            HStack {
                Image(systemName: "link")

                Text("www.chillog.page")
            }
        }
        .font(.caption)
        .foregroundColor(.gray)

        HStack(spacing: 24) {
            HStack(spacing: 4) {
                Text("777")
                    .font(.subheadline)
                    .fontWeight(.bold)

                Text("Following")
                    .font(.caption)
                    .foregroundColor(.gray)
            }

            HStack(spacing: 4) {
                Text("9M")
                    .font(.subheadline)
                    .fontWeight(.bold)

                Text("Followers")
                    .font(.caption)
                    .foregroundColor(.gray)
            }
        }
        .padding(.vertical
        )
    }
    .padding(.horizontal)
}

이전에 작성했던 ProfileView의 코드이다.

HStack(spacing: 24) {
    HStack(spacing: 4) {
        Text("777")
            .font(.subheadline)
            .fontWeight(.bold)

        Text("Following")
            .font(.caption)
            .foregroundColor(.gray)
    }

    HStack(spacing: 4) {
        Text("9M")
            .font(.subheadline)
            .fontWeight(.bold)

        Text("Followers")
            .font(.caption)
            .foregroundColor(.gray)
    }
}
.padding(.vertical

이 중 해당 유저의 Follow 정보를 불러오는 부분은 재사용할 수 있다.

struct UserStatsView: View {
    var body: some View {
        HStack(spacing: 24) {
            HStack(spacing: 4) {
                Text("777")
                    .font(.subheadline)
                    .fontWeight(.bold)

                Text("Following")
                    .font(.caption)
                    .foregroundColor(.gray)
            }

            HStack(spacing: 4) {
                Text("9M")
                    .font(.subheadline)
                    .fontWeight(.bold)

                Text("Followers")
                    .font(.caption)
                    .foregroundColor(.gray)
            }
        }
    }
}

이를 UserStatsView로 별도로 분리하면 SideMenu를 구성할 때 작업의 양이 줄어든다.

SideMenuView
| SideMenuRowViewModel

enum SideMenuViewModel: Int, CaseIterable {
    case profile
    case lists
    case bookmarks
    case logout

    var title: String {
        switch self {
        case .profile: return "Profile"
        case .lists: return "Lists"
        case .bookmarks: return "Bookmarks"
        case .logout: return "Logout"
        }
    }

    var imageName: String {
        switch self {
        case .profile: return "person"
        case .lists: return "list.bullet"
        case .bookmarks: return "bookmark"
        case .logout: return "arrow.left.square"
        }
    }
}

이전의 TweetFilterView를 구성할 때와 마찬가지로 같은 UI를 가지는 '메뉴'들의 경우
enum과 ForEach를 사용한 열거 방식으로 반복된 작업을 크게 줄일 수 있다.
case에 따라 각각의 속성이 적절한 값을 반환할 수 있도록 구현한다.

SideMenuView
| SideMenuRowView

struct SideMenuRowView: View {
    let viewModel: SideMenuViewModel
    var body: some View {
        HStack(spacing: 16) {
            Image(systemName: viewModel.imageName)
                .font(.headline)
                .foregroundColor(.gray)

            Text(viewModel.title)
                .foregroundColor(.black)
                .font(.subheadline)

            Spacer()
        }
        .frame(height: 40)
        .padding(.horizontal)
    }
}

SideMenuRowView는 SideMenuView가 SideMenuRowViewModel를 열거해 만들 UI를 구성한다.
HStack을 사용해 메뉴의 픽토그램을 앞에 배치하고 이름을 뒤에 배치한다.

SideMenuView
| SideMenuView

SideMenu의 상단에는 현재 접속 중인 계정의 간략한 정보를 표시한다.
프로필 사진과 유저명, 계정명, 팔로우 정보를 사용한다.

VStack(alignment: .leading) {
    Circle()
        .frame(width: 48, height: 48)

    VStack(alignment: .leading, spacing: 4) {
        Text("Marcus")
            .font(.headline)

        Text("@Marcus00")
            .font(.caption)
            .foregroundColor(.gray)
    }

    UserStatsView()
        .padding(.vertical)
}
.padding(.leading)

VStack으로 구성하고, 앞서 별도로 분리해 둔 UserStatsView를 재사용해 팔로우 정보를 표시한다.

하단에는 계정 메뉴를 표시한다.

ForEach(SideMenuViewModel.allCases, id: \.rawValue) { viewModel in
    if viewModel == .profile {
        NavigationLink(destination: ProfileView()) {
            SideMenuRowView(viewModel: viewModel)
        }
    } else if viewModel == .logout {
        Button {
            print("Handel logout")
        } label: {
            SideMenuRowView(viewModel: viewModel)
        }
    } else {
        SideMenuRowView(viewModel: viewModel)
    }
}

앞서 생성한 enum을 ForEach를 사용해 열거한다.
각각의 버튼은 동일한 UI를 가졌지만 Profile 버튼과 Logout 버튼은 각각의 View나 기능을 갖는다.
각각의 케이스에 맞게 분기해 기능을 연결함과 동시에 SideMenuRowView를 사용해 UI를 구성한다.

 

ContentView
| MainTabView와 SideMenuView 연결하기

ContentView는 MainTabView와 SideMenuView를 연결하는 역할을 하게 된다.
기본적으로 버튼을 누르면 옆에 나타났다가 다시 사라지는 형태를 갖게 된다.

@State private var showMenu = false

이를 위해 표시 상태를 변경할 State 변수가 하나 필요하다.

ZStack(alignment: .topLeading) {
    MainTabView()

    if showMenu {
        ZStack {
            withAnimation(.easeInOut) {
                Color(.black)
                    .opacity(showMenu ? 0.25 : 0.0)
            }
        }.onTapGesture {
            withAnimation(.easeInOut) {
                showMenu = false
            }
        }
        .ignoresSafeArea()
    }

    SideMenuView()
        .frame(width: 300)
        .offset(x: showMenu ? 0 : -300, y: 0)
}

SideMenu는 평면상으로는 MainTabView의 옆에 위치하지만,
입체적으로는 MainTabView의 위에 위치한다.
따라서 이 둘은 ZStack에 Embed 되고, 정렬은 왼쪽 상단 모서리를 기준으로 하기 위해 topLeading을 사용한다.

showMenu 변수가 true라면 sideMenu가 표시되고, 이 때는 기존의 화면에 Dimming 효과를 주도록 구현했다.

MainTabVIew 위에 표시되는 SideMenuView는 showMenu 변수에 따라 x축을 변경한다.

struct ContentView: View {
    @State private var showMenu = false
    var body: some View {
        ZStack(alignment: .topLeading) {
            MainTabView()
                .toolbar(showMenu ? .hidden : .visible)

            if showMenu {
                ZStack {
                    withAnimation(.easeInOut) {
                        Color(.black)
                            .opacity(showMenu ? 0.25 : 0.0)
                    }
                }.onTapGesture {
                    withAnimation(.easeInOut) {
                        showMenu = false
                    }
                }
                .ignoresSafeArea()
            }

            SideMenuView()
                .frame(width: 300)
                .offset(x: showMenu ? 0 : -300, y: 0)
        }
        .navigationTitle("Home")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar{
            ToolbarItem(placement: .navigationBarLeading) {
                Button {
                    withAnimation(.easeInOut) {
                        showMenu.toggle()
                    }
                } label: {
                    Circle()
                        .frame(width: 32, height: 32)
                }
            }
        }
        .onAppear {
            showMenu = false
        }
    }
}

모든 동작은 showMenu 변수에 의해 결정된다.
toolbar에 프로필 사진을 표시하고, 터치하면 showMenu를 toggle 하도록 구현했다.

MainTabView()
    .toolbar(showMenu ? .hidden : .visible)

마지막으로 MainTabView에 존재하는 툴바는 SideMenu가 나타날 때 숨을 수 있도록 shoeMenu에 따라 상태를 분기한다.

toolbar가 숨지 않으면 왼쪽과 같이 SideMenu에 겹쳐서 표시되는 문제가 생긴다.

MainApp

@main
struct twitterClone_SwiftUIApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationView {
                ContentView()
            }
        }
    }
}

이렇게 완성된 ContentView는 이제 앱의 메인 화면으로 사용된다.
내부적으로 모든 View의 전환을 통제할 수 있도록 NavigationView에 Embed 했다,

 

'프로젝트 > Twitter Clone App (w∕Firebase)' 카테고리의 다른 글

07. 기본 UI 구성하기 #7  (0) 2022.12.02
06. 기본 UI 구성하기 #6  (0) 2022.12.01
04. 기본 UI 구성하기 #4  (0) 2022.11.24
03. 기본 UI 구성하기 #3  (0) 2022.11.23
02. 기본 UI 구성하기 #2  (0) 2022.11.22