본문 바로가기

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

02. 기본 UI 구성하기 #2

기본 UI 구성하기 #2
MainTabView & ProfileView


MainTabView

이전에 구현한 FeedView를 시작으로,
이후에 구현할 SearchView, NotificationView, MessageView를 포함한 TabView를 추가한다.

더보기

Source

struct MainTabView: View {
    @State private var selectedIndex = 0
    var body: some View {
        TabView(selection: $selectedIndex) {
            FeedView()
                .onTapGesture {
                    self.selectedIndex = 0
                }
                .tabItem {
                    Image(systemName: "house")
                }.tag(0)

            ExploreView()
                .onTapGesture {
                    self.selectedIndex = 1
                }
                .tabItem {
                    Image(systemName: "magnifyingglass")
                }.tag(1)

            NotificationView()
                .onTapGesture {
                    self.selectedIndex = 2
                }
                .tabItem {
                    Image(systemName: "bell")
                }.tag(2)

            MessageView()
                .onTapGesture {
                    self.selectedIndex = 3
                }
                .tabItem {
                    Image(systemName: "envelope")
                }.tag(3)
        }
    }
}

tabItem은 보통은 Label로 구성하지만 지금처럼 제목을 붙이지 않을 결루 Image를 사용하는 것도 가능하다.
항상 tabItem을 선택하지 않고, 다른 경로로도 접근할 수 있도록 tag를 사용했다.

ProfileView

ProfileView는 크게 headerView와 actionButtons, userIntoDetails, tweetFilterBar로 구분된다.

ProfileView
| headerView

화면의 상단을 차지하고 있는 headerView는 뒤로 가기를 위한 Button과 프로필 사진을 표시하기 위한 원형의 View가 존재한다.
NavigationView의 뒤로 가기 버튼은 커스텀이 불가하기 때문에 navigationbar를 숨기고 별도의 버튼을 만들었다.

프로필 사진을 표시하기 위한 원형 View는 파란색의 배경과 headerView 아래에 위치하게 될 View의 경계에 존재한다.
따라서 offset을 사용해 위치를 변경했다.
offset을 사용할 때의 주의점은 ParentView들의 적용 사항 혹은
이후에 적용되는 modifier들을 충분히 고려해 순서를 정해야 한다는 점이다.
순서가 잘못된다면 이벤트를 발생시키는 위치가 다르거나 시각적으로 기대와는 다른 결과를 보여줄 수 있다.

프로필 사진과 뒤로 가기 버튼은 VStack에 Embed 돼 좌측 상단에 위치한다.

더보기

Source

var headerView: some View  {
    ZStack(alignment: .bottomLeading) {
        Color(.systemBlue)
            .ignoresSafeArea()

        VStack {
            Button {
                dismiss()
            } label: {
                Image(systemName: "arrow.left")
                    .resizable()
                    .frame(width: 20, height: 16)
                    .foregroundColor(.white)
            }
            .offset(x: 16, y: 12)

            Circle()
                .frame(width: 72, height: 72)
            .offset(x: 16, y: 24)
        }
    }
    .frame(height: 96)
    .toolbar(.hidden, for: .navigationBar)
}

ProfileView
| actionButtons

위치상 headerView의 아래에 위치하게 되는 View다.
두 개의 버튼을 가지는 HStack이다.

버튼들의 테두리를 위해 overlay를 사용한 것이 특징이다.

더보기

Source

var actionButtons: some View {
    HStack(spacing: 12) {
        Spacer()

        Image(systemName: "bell.badge")
            .font(.title3)
            .padding(6)
            .overlay {
                Circle()
                    .stroke(Color.gray, lineWidth: 0.75)
            }

        Button {

        } label: {
            Text("Edit Profile")
                .font(.subheadline).bold()
                .frame(width: 120, height: 32)
                .overlay {
                    RoundedRectangle(cornerRadius: 20)
                        .stroke(Color.gray, lineWidth: 0.75)
                }
        }
    }
    .padding(.trailing)
}

ProfileView
| userInfoDetails

더보기

Source

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)
}

userInfoDetails View는
계정 이름, 인증 패치, 계정, 소개글, 위치, 링크, 팔로잉 정보를 표시하는 VStack으로 이뤄져 있다.

정보들이 왼쪽을 기준으로 표시될 수 있도록 VStack의 alignment를 leading으로 설정하고, 간격을 위해 spacing을 4로 설정했다.
소개글의 구분을 위해 해당 TextView에는 vertical padding이 적용됐다.

 

코드 작성 방식

이렇게 하나의 View에 재사용이 불가능한 코드가 많아지게 되면 자연스럽게 가독성이 나빠진다.
fold/unfold 기능으로 간략하게 볼 수는 있지만 그럼에도 불편한 것이 사실이다.
따라서 영상에서는 다음과 같이 코드의 본문을 extension으로 따로 분리하는 방법을 소개한다.

extension ProfileView {
    var headerView: some View  {
        ZStack(alignment: .bottomLeading) {
            Color(.systemBlue)
                .ignoresSafeArea()

            VStack {
                Button {

                } label: {
                    Image(systemName: "arrow.left")
                        .resizable()
                        .frame(width: 20, height: 16)
                        .foregroundColor(.white)
                        .offset(x: 16, y: 12)
                }

                Circle()
                    .frame(width: 72, height: 72)
                .offset(x: 16, y: 24)
            }
        }
        .frame(height: 96)
    }

    var actionButtons: some View {
        HStack(spacing: 12) {
            Spacer()

            Image(systemName: "bell.badge")
                .font(.title3)
                .padding(6)
                .overlay {
                    Circle()
                        .stroke(Color.gray, lineWidth: 0.75)
                }

            Button {

            } label: {
                Text("Edit Profile")
                    .font(.subheadline).bold()
                    .frame(width: 120, height: 32)
                    .overlay {
                        RoundedRectangle(cornerRadius: 20)
                            .stroke(Color.gray, lineWidth: 0.75)
                    }
            }
        }
        .padding(.trailing)
    }

    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의 body에 들어가 있을 코드이지만,
이렇게 extension으로 분리한 뒤

struct ProfileView: View {
    var body: some View {
        VStack(alignment: .leading) {
            headerView

            actionButtons

            userInfoDetails

            Spacer()
        }
    }
}

body에서는 extension을 호출하는 방식으로 코드를 정리한다.
재사용이 불가능한 View들이긴 하지만 해당 View내에서 모듈화해 각각을 관리하기 쉽고,
body에서는 지금 보고 있는 View가 어떤 구조로 되어있는지 한눈에 확인할 수 있다.

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

05. 기본 UI 구성하기 #5  (0) 2022.11.30
04. 기본 UI 구성하기 #4  (0) 2022.11.24
03. 기본 UI 구성하기 #3  (0) 2022.11.23
01. 기본 UI 구성하기 #1  (0) 2022.11.21
00. 시작하며  (0) 2022.11.03