-Preview-

SideMenuViewOperation

-ViewReview-

SideMenuView
이번에 구현한 뷰는 SideMenuView로, 버튼을 누르면 오른쪽에서 메뉴 뷰(일명 햄버거 메뉴 뷰)가 나오도록 하고 리스트로 구성되어 있는 버튼을 누르면 메인 뷰가 해당 화면으로 이동하는 기능을 구현하였다.
해당 기능을 구현하면서 어떤 방법으로 사이드메뉴를 구현할 것인지 여러 정보를 찾아보고 조사해보며 자신이 가장 잘 할 수 있을 것 같은 방법으로 구현하였다.
별도의 뷰에 사이드메뉴에 들어갈 코드를 작성하고, 메인 뷰에서 ZStack으로 가장 상단의 뷰에 위치하게 한 뒤에 offSet을 이용하여 평소에는 화면 밖에 있다가 버튼 트리거를 이용하여 메인뷰에 나타나도록 구성하였다.

핵심코드

사이드메뉴를 오픈하는 조건을 작성한 코드

SideMenuView(presentSideMenu: $presentSideMenu, id: $id)
	.zIndex(2)
	.offset(x: presentSideMenu ? 150 : 600, y: 0)
	.animation(.default, value: presentSideMenu)

1. 사이드메뉴 뷰 초기 세팅

사이드메뉴 뷰에서 활용할 Binding값과 데이터를 불러올 SwiftData값을 선언한다.

import SwiftUI

struct SideMenuView: View {
    
    private var swiftData: [SwiftData] = swiftDataSet // swiftData를 불러오기
    @Binding private var presentSideMenu: Bool // 사이드메뉴가 열리는 조건
    @Binding private var id: Int // 메인 뷰의 현재 값을 변동시키기 위한 조건
    
    // Binding값의 기본값을 지정
    init(presentSideMenu: Binding<Bool> = .constant(false), id: Binding<Int> = .constant(0)) {
        _presentSideMenu = presentSideMenu
        _id = id
    }

2. 사이드메뉴 뷰 내용 구성

사이드메뉴뷰의 내용으로 타이틀과 데이터 리스트를 만들었는데, 리스트는 SwiftData를 가진 배열을 가져와 ForEach문을 통해 생성해 주었다.

ScrollView {
	// SwiftData 배열값을 사용하여 ForEach문으로 리스트 작성
	ForEach((swiftData), id: \.self) { data in
		ZStack() {
			RoundedRectangle(cornerRadius: 10)
				.frame(width: 210, height: 50)
				.foregroundStyle(Color.parsta)
                            
			RoundedRectangle(cornerRadius: 10)
				.frame(width: 205, height: 45)
				.foregroundStyle(Color.white)
                // 리스트(버튼)을 클릭했을 때, 사이드 메뉴가 닫히고 메인 뷰의 값을 변경시킨다
				.onTapGesture {
					self.presentSideMenu = false
					self.id = data.id
				}
				.overlay {
					Text("\(data.id + 1). \(data.title)")
						.font(.system(size: 15))
						.fontWeight(.medium)
						.foregroundStyle(Color.parsta)
						.lineLimit(1)
						.padding(.horizontal, 10)
				}
			}
			.padding(.bottom, 15)
		}
		.padding(.top, 10)
	}
	.scrollIndicators(.hidden) // 스크롤 바 숨기기

3. 기종에 따른 Padding값 변경 설정

사이드 메뉴를 구현하며 테스트를 진행하던 중 기종을 바꾸는 경우(iPhone16 pro -> iPhoneSE 3rd) 디스플레이 크기가 줄어드는 탓에 사이드메뉴가 가려지거나 보기에 좋지 않은 경우가 발생했다. 이를 해결하기 위해 기종에 따른 값 차이를 두어 해결을 하였다.
아래 코드의 경우 SafeAreaBottom값에 따른 변화를 주기 때문에 홈버튼이 있는 기종일 때는 패딩을 20, 없는 기종일 경우 30만큼 값을 주도록 하였는데, 다른 스크린이 작은 기종(iPhone 13 mini 등)으로 테스트를 해보며 문제가 발생할 경우 GeometryUIScreen값을 활용하는 방향으로 수정을 진행할 예정이다.

private func safeAreaBottomSizeCheck() -> CGFloat {
    let scenes = UIApplication.shared.connectedScenes
    let windowScene = scenes.first as? UIWindowScene
    let window = windowScene?.windows.first
    
    let safeAreaBottomSize = window?.safeAreaInsets.bottom
    let paddingValue = CGFloat(safeAreaBottomSize == 0 ? 20 : 30)
    
    return paddingValue
// 활용 예시: .padding(.leading, safeAreaBottomSizeCheck())
}

4. 메인 뷰에 사이드메뉴 뷰 버튼 구현

메인 뷰에서 ZStack을 이용하여 사이드메뉴 뷰를 불러올 트리거 버튼을 구현한다.

Button(action: {
	// 버튼을 누르면 사이드메뉴 뷰가 나타난다
	presentSideMenu = true
}, label: {
	Image(systemName: "line.3.horizontal")
		.font(.system(size: 25))
		.foregroundStyle(Color.parstaGray)
	})
	.padding(.trailing, 35)

5. 사이드메뉴 뷰 호출 시 메인 뷰 가리기

사이드메뉴 뷰는 화면 전체를 가리는 것이 아닌 일부를 가린다. 때문에 사이드메뉴를 호출하면 메인 뷰의 화면일 일부 보이게 되는데, 이 때 사용자의 실수로 메인 뷰의 스크롤이나 버튼 등을 누르지 못하도록 Rectangle을 사용하여 가려준다. 이는 사이드 뷰와 메인 뷰의 구분감도 더해주어 가독성을 높여주는 부가적인 기능도 존재한다.

Rectangle()
	.zIndex(1) // 메인 뷰의 인덱스 = 0, 사이드메뉴 뷰의 인덱스 = 1
	.edgesIgnoringSafeArea(.all)
	.foregroundStyle(presentSideMenu ? Color.black.opacity(0.2) : .clear) // 사이드메뉴 뷰가 열렸을 때는 black 컬러가 들어가고, 닫혔을 때는 투명해진다
	.animation(.default, value: presentSideMenu)
	.onTapGesture {
    	// 사이드메뉴가 열려있을 때 빈 곳(현재 Rectangle)을 클릭하면 사이드메뉴가 닫히도록 조건 설정
		self.presentSideMenu = false
	}

6. 사이드메뉴 뷰 메인 뷰에 호출

메인 뷰 코드에 사이드메뉴 뷰를 호출하고 오픈되는 조건을 설정한다.

SideMenuView(presentSideMenu: $presentSideMenu, id: $id)
	.zIndex(2)
	.offset(x: presentSideMenu ? 150 : 600, y: 0) // 조건이 true일 때 offSet의 x값이 150이 되어 나타나고, false일 때는 600이 되어 화면 밖에 존재하게 된다
	.animation(.default, value: presentSideMenu)

ContentView 전체 코드

import SwiftUI

struct ContentView: View {
    
    private var swiftData: [SwiftData] = swiftDataSet
    @State private var id: Int = 0
    @State private var presentSideMenu: Bool = false
    
    var body: some View {
        
        ZStack {
            
            Rectangle()
                .zIndex(1)
                .edgesIgnoringSafeArea(.all)
                .foregroundStyle(presentSideMenu ? Color.black.opacity(0.2) : .clear)
                .animation(.default, value: presentSideMenu)
                .onTapGesture {
                    self.presentSideMenu = false
                }
            
            SideMenuView(presentSideMenu: $presentSideMenu, id: $id)
                .zIndex(2)
                .offset(x: presentSideMenu ? 150 : 600, y: 0)
                .animation(.default, value: presentSideMenu)
            
            VStack(alignment: .trailing) {
                HStack {
                    
                    Spacer()
                    
                    Button(action: {
                        presentSideMenu = true
                    }, label: {
                        Image(systemName: "line.3.horizontal")
                            .font(.system(size: 25))
                            .foregroundStyle(Color.parstaGray)
                    })
                    .padding(.trailing, 35)
                }
                Spacer()
            }
            
            VStack(spacing: 0) {
                
                Rectangle()
                    .frame(height: 50)
                    .opacity(0)
                
                Text(swiftData[self.id].title)
                    .font(.system(size: 20))
                    .fontWeight(.medium)
                    .padding(.bottom, 30)
                
                ScrollView {
                    Text(swiftData[self.id].content)
                        .font(.system(size: 15))
                        .fontWeight(.light)
                        .lineSpacing(2)
                        .padding(10)
                        .padding(.horizontal, 20)
                }
                .frame(width: UIScreen.main.bounds.width, height: 350)
                .padding(.bottom, 10)
                .scrollIndicators(.hidden)
                
                HStack(spacing: 0) {
                    Text("< Prev")
                        .font(.system(size: 15))
                        .fontWeight(.semibold)
                        .foregroundStyle(Color.parsta)
                        .onTapGesture {
                            withAnimation {
                                if self.id <= 0 {
                                    self.id = swiftData.count - 1
                                } else {
                                    self.id -= 1
                                }
                            }
                        }
                    
                    Spacer()
                    
                    Text("Next >")
                        .font(.system(size: 15))
                        .fontWeight(.semibold)
                        .foregroundStyle(Color.parsta)
                        .onTapGesture {
                            withAnimation {
                                if self.id >= swiftData.count - 1 {
                                    self.id = 0
                                } else {
                                    self.id += 1
                                }
                            }
                        }
                }
                .padding(.horizontal, 20)
                
                Spacer()
            }
            .padding(.horizontal, 20)
        }
    }
 }

-구현 결과물-


-오늘의 학습 후기-

오늘은 처음으로 사이드메뉴 뷰를 구현해 보았다.
처음에 조사를 시작할 때는 시행착오도 많이 겪고, 너무 어려워 보였지만 결국 원리를 파악하니 내 방식대로 제작할 수가 있었다.
내 방법이 정답이 아니고 다른 더욱 편한 방법이 있을 수 있지만, 이번엔 그저 내 힘으로 스스로 구현한 것에 자축을 하기로 했다.
이번엔 간단한 모양이었기에 어렵지 않았지만 다음에는 더 예쁘게 커스텀하여 커스텀 사이드메뉴 뷰를 구현해보고 싶다고 생각했다.
profile
이유있는 코드를 쓰자!!

0개의 댓글