⭐️온보딩뷰 만들기⭐️
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
}
}
}
꽤 길었다..😓
그래도 지금까지 배웠던 개념들을 총망라하는 시간이라 유익!
'SwiftUI > SwiftUI(Basic)' 카테고리의 다른 글
[55] Background Materials (0) | 2023.05.11 |
---|---|
[54] AsyncImage / @unknown (0) | 2023.05.11 |
[52] @AppStorage / UserDefaults (0) | 2023.05.07 |
[51] @EnvironmentObject / 뷰 계층 구조 (0) | 2023.05.07 |
[50] @Publisehd, @StateObject, @ObservedObject / MVVM (0) | 2023.05.07 |