기본 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 |