Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
SwiftUI/SwiftUI(Basic)

[53] 온보딩뷰 만들기

by Toughie 2023. 5. 8.

⭐️온보딩뷰 만들기⭐️

Onboarding

 

지금까지 배운 개념들을 활용해서 온보딩뷰를 만들어 보자.

간단하게 살펴보면

첫 화면

이름 입력 화면(입력값 체크)

나이 입력 화면(슬라이더)

성별 입력(피커)

그리고 화면별 트랜지션,

AppStorage 활용에 초점을 두면 된다.(앱 껐다 켜도 데이터 남아있음)

 

인트로 화면

//  Created by Toughie on 2023/05/07.
//

import SwiftUI

struct IntroView: View {
    //앱스토리지에 저장해둔 데이터를 통해 온보딩뷰 or 프로필뷰 표시
    @AppStorage("signed_in") var currentUsersignedIn: Bool = false
    
    //AppStorage 프로퍼티를 통해 트랜지션 애니메이션을 구현하려 했으나,
    //상태변수가 아니라 적용이 안되는 문제가 있었음.
    //따라서 AppStorage 프로퍼티와 동기화 시킨 @State 프로퍼티를 만들어줌.
    @State private var isSignedIn = false
    
    var body: some View {
        ZStack {
        //배경 그레디언트
            RadialGradient(
                gradient: Gradient(colors: [Color.white, Color.blue]),
                center: .topLeading,
                startRadius: 50,
                endRadius: UIScreen.main.bounds.height
            )
            .ignoresSafeArea()
            
            //만약 온보딩이 끝났다면 프로필뷰를 보여줌
            if isSignedIn {
                ProfileView()
                //등장 -> 아래에서 올라옴 / 사라짐 -> 위로 올라감
                    .transition(
                        .asymmetric(
                            insertion: .move(edge: .bottom).animation(.easeOut(duration: 1)),
                            removal: .move(edge: .top).animation(.easeInOut(duration: 1)))
                    )
            } else {
            //온보딩이 끝나지 않았다면(첫 실행 등) 온보딩뷰를 보여줌
                OnboardingView()
                //마찬가지로 트랜지션 애니메이션 추가
                    .transition(
                        .asymmetric(
                            insertion: .move(edge: .top).animation(.easeInOut(duration: 1)),
                            removal: .move(edge: .bottom).animation(.easeInOut(duration: 1))))
            }
        }
        //앱스토리지 프로퍼티와 상태 프로퍼티 바인딩(동기화)
        .onAppear {
            withAnimation(.spring()) {
                isSignedIn = currentUsersignedIn
            }

        }
        //currentUsersignedIn이 변하면 isSignedIn에 할당(동기화)
        .onChange(of: currentUsersignedIn) { newValue in
            withAnimation(.spring()) {
                isSignedIn = newValue
            }
        }
    }
}

온보딩 화면

*코드의 양이 많아서 extension을 활용함

//  Created by Toughie on 2023/05/07.
//

import SwiftUI

struct OnboardingView: View {
    
    // Onboarding states:
    /*
     0 - welcome screen
     1 - Add name
     2 - Add age
     3 - Add gender
     */
     
     //온보딩 상태를 저장하기 위한 상태 프로퍼티
    @State var onboardingState: Int = 0
    
    //온보딩 뷰 전환 트렌지션
    let transition: AnyTransition = .asymmetric(
        insertion: .move(edge: .trailing),
        removal: .move(edge: .leading))

    //Inputs
    //필요한 데이터들(유저입력/선택)
    @State var name: String = ""
    @State var age: Double = 30
    @State var gender: String = ""
    
    //Alert
    //다이나믹하게 알럿을 사용하기 위한 상태 프로퍼티
    @State var alertTitle: String = ""
    @State var showAlert: Bool = false
    
    //AppStorage
    //입력받은 데이터를 앱스토리지에 저장하기 위한 프로퍼티들
    @AppStorage("name") var currentUserName: String?
    @AppStorage("age") var currentUserAge: Int?
    @AppStorage("gender") var currentUserGender: String?
    @AppStorage("signed_in") var currentUsersignedIn: Bool = false
    
    var body: some View {
        ZStack {
            // content
            ZStack {
            //onboardingState에 따라 뷰 전환
                switch onboardingState {
                case 0:
                    welcomeSection
                        .transition(transition)
                case 1:
                    addNameSection
                        .transition(transition)
                case 2:
                    addAgeSection
                        .transition(transition)
                case 3:
                    addGenderSection
                        .transition(transition)
                default:
                    RoundedRectangle(cornerRadius: 25)
                        .foregroundColor(.green)
                }
            }
            //button
            VStack {
                Spacer()
                //버튼뷰 따로 분리
                bottomButton
            }
            .padding(30)
        }
        //알럿
        .alert(isPresented: $showAlert) {
            return Alert(title: Text(alertTitle))
        }
    }
}

온보딩뷰 확장 (컴포넌트, 서브뷰)

extension OnboardingView {
    //하단 버튼 
    private var bottomButton: some View {
    	//다이나믹하게 버튼 내 텍스트 변화
        Text(
            onboardingState == 0 ? "로그인" :
                onboardingState == 3 ? "시작하기" :
                "다음"
        )
        .font(.headline)
        .foregroundColor(.blue)
        .frame(height: 55)
        .frame(maxWidth: .infinity)
        .background(.white)
        .cornerRadius(10)
        //텍스트 전환 애니메이션 비활성화
        .animation(nil, value: onboardingState)
        //탭 제스쳐 추가
        .onTapGesture {
        // 함수 모아놓은 확장에서 설명
        // 버튼이 눌렀을 때 동작
            handleNextButtonPressed()
        }
    }
    
    //시작 안내 화면
    private var welcomeSection: some View {
        VStack(spacing: 40) {
            Spacer()
            Image(systemName: "figure.dance")
                .resizable()
                .scaledToFit()
                .frame(width: UIScreen.main.bounds.width * 0.3)
                .foregroundColor(.white)
            
            Text("터피와 댄스!")
                .font(.largeTitle)
                .fontWeight(.semibold)
                .foregroundColor(.white)
                //밑줄
                .overlay (
                    Capsule(style: .continuous)
                        .frame(height: 3)
                        .offset(y: 5)
                        .foregroundColor(.white)
                    
                    ,alignment: .bottom
                )
            Text("어디서든 터피의 춤을 배워보세요.이 앱은 근심걱정들을 없애고 행운을 가져다 줍니다.")
                .fontWeight(.medium)
                .foregroundColor(.white)
            Spacer()
            Spacer()
        }
        .multilineTextAlignment(.center)
        .padding(30)
    }
    
    // 이름 입력 뷰
    private var addNameSection: some View {
        VStack(spacing: 20) {
            Spacer()
            
            Text("이름을 알려주세요!")
                .font(.largeTitle)
                .fontWeight(.semibold)
                .foregroundColor(.white)
                
            //텍스트필드, name 바인딩
            TextField("이름", text: $name)
                .font(.headline)
                .frame(height: 55)
                .padding(.horizontal)
                .background(Color.white)
                .cornerRadius(10)
            
            //스페이서를 여러개 쓰면 균등하게 사이즈를 나눈다는 사실.. 알고 계셨나요?
            Spacer()
            Spacer()
        }
        .padding(30)
    }
    
    //나이 선택 뷰
    private var addAgeSection: some View {
        VStack(spacing: 20) {
            Spacer()
            
            Text("나이를 알려주세요!")
                .font(.largeTitle)
                .fontWeight(.semibold)
                .foregroundColor(.white)
            //문자열 포메팅
            Text("\(String(format: "%.0f", age))")
                .font(.largeTitle)
                .fontWeight(.semibold)
                .foregroundColor(.white)
                //슬라이더 value(시작값), in(범위), step(단계)
            Slider(value: $age, in: 18...50, step: 1)
                .accentColor(.white)
            
            Spacer()
            Spacer()
        }
        .padding(30)
    }
    
    //성별 선택 뷰
    private var addGenderSection: some View {
        VStack(spacing: 20) {
            Spacer()
            
            //처음에 gender가 ""로 초기화 되어 있기에 삼항연산자를 통해 안내문구를 표시함
            //피커를 통해 값이 선택되면 해당 값을 표시
            Text(gender.count > 1 ? gender : "성별을 알려주세요!")
                .font(.largeTitle)
                .fontWeight(.semibold)
                .foregroundColor(.white)
            
            Picker(selection: $gender) {
                Text("남자").tag("남자")
                Text("여자").tag("여자")
                Text("비공개").tag("비공개")
            } label: {
                Text("성별을 알려주세요")
            }
            .pickerStyle(.wheel)
            Spacer()
            Spacer()
        }
        .padding(30)
    }
}

온보딩뷰 확장 (메서드)

extension OnboardingView {
    
   	//버튼 동작 메서드 (탭 제스쳐)
    func handleNextButtonPressed() {
        // Check Input
        //onboardingState 즉 온보딩 단계에 맞게 적절한 알럿 표시
        switch onboardingState {
        //이름 입력 뷰
        case 1:
            guard name.count >= 2 else {
                showAlert(title: "두 글자 이상 입력해 주세요. 😓")
                return
            }
        case 3:
            guard gender.count > 1 else {
                showAlert(title: "성별을 선택해 주세요.")
                return
            }
        default:
            break
        }
        
        //Go To Next Section
        //마지막 온보딩뷰인 경우 singIn()메서드 실행 else 다음 온보딩뷰로 이동
        //(onboardingState += 1)
        if onboardingState == 3 {
            signIn()
        } else {
            withAnimation(.spring()) {
                onboardingState += 1
            }
        }
    }
    
    //알럿표시를 위한 메서드
    func showAlert(title: String) {
        alertTitle = title
        showAlert.toggle()
    }
    
    //마지막 온보딩 뷰에서 데이터를 앱스토리지에 저장
    //온보딩이 끝났기 때문에 currentUsersignedIn 변경
    // -> 프로필뷰로 전환될 것
    func signIn() {
        currentUserName = name
        currentUserAge = Int(age)
        currentUserGender = gender
        currentUsersignedIn = true
        
            withAnimation(.easeInOut(duration: 1)) {
                currentUsersignedIn = true
            }
    }
}

프로필뷰 (온보딩 완료 후)

//  Created by Toughie on 2023/05/07.
//

import SwiftUI

struct ProfileView: View {
	//AppStorage 변수 접근
    @AppStorage("name") var currentUserName: String?
    @AppStorage("age") var currentUserAge: Int?
    @AppStorage("gender") var currentUserGender: String?
    @AppStorage("signed_in") var currentUsersignedIn: Bool = false
    
    
    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "figure.dance")
                .resizable()
                .scaledToFit()
                .frame(width: 150, height: 150)
            Text(currentUserName ?? "USERNAME")
            Text("나이 : \(currentUserAge ?? -1)세")
            Text("성별 : \(currentUserGender ?? "UNKNOWN")")
            
            Text("Log Out")
                .foregroundColor(Color.white)
                .font(.headline)
                .frame(height: 55)
                .frame(maxWidth: .infinity)
                .background(Color.black)
                .cornerRadius(10)
        }
        .font(.title)
        .foregroundColor(.blue)
        .padding()
        .padding(.vertical, 40)
        .background(Color.white)
        .cornerRadius(10)
        .shadow(radius: 10)
        //데이터 초기화를 위한 제스쳐
        .onTapGesture {
            logOut()
        }
    }
    
    //앱스토리지 프로퍼티 nil로 초기화
    func logOut() {
        currentUserName = nil
        currentUserAge = nil
        currentUserGender = nil
        
        // 초기 안내화면으로 전환시키기 위함.
        withAnimation(.spring()) {
            currentUsersignedIn = false
        }
    }
}

 

꽤 길었다..😓

그래도 지금까지 배웠던 개념들을 총망라하는 시간이라 유익!