온보딩화면을 만들어보자! - Part 01. 기본스타일

xohxe (김소혜)· December 21, 2023
수입푸드UI 톺아보기
아이폰 기본 앱에서 많이 사용되는 스타일로 온보딩화면을 만들어보았다.
온보딩 화면은 앱을 최초로 실행했을 때, 앱 소개와 사용법 안내 등을 위해 사용되는 화면으로 대부분의 앱에서 빼놓지 않고 활용되는 만큼 짚고 넘어가보자 내용을 정리해봤다.
완성화면
프로젝트 구조 살펴보기
먼저 프로젝트 파일 구조를 살펴봅시다.
MVVM 패턴에 익숙해지고자, 아래와 같이 코드를 작성해보았다.
@AppStorage
@State 속성을 포함하여 데이터 저장까지 한 번에 할 수 있는 property wrapper @AppStorage를 사용하여, 사용자가 앱을 처음으로 켰는지 여부를 확인해봅시다.
ContentView.swift
import SwiftUI
struct ContentView: View {
// 사용자 안내 온보딩 페이지를 앱 설치 후, 최초 실행할 때만 띄우도록 하는 변수
// AppStorage에 저장되어 앱 종료 후에도 유지된다.
@AppStorage("isFirstLaunch") var isFirstLaunch: Bool = true
var body: some View {
VStack{
Text("Main View 가 들어갈 곳!")
Text("앱 재 설치시에만 보임.")
}
.popover(isPresented: $isFirstLaunch){
OnboardingView()
}
}
}사용자가 앱을 처음으로 켰는지 체크하고 popover로 온보딩 모달뷰를 노출시켜주자.
만약 화면을 꽉 채우고 싶다면, popover 대신 fullScreenCover 로 바꿔주시면 된다.
온보딩뷰 그려보기
앞의 작업이 끝났다면, 온보딩 뷰는 간단하다.
OnboardingView.swift
import SwiftUI
struct OnboardingView : View {
@StateObject var viewModel: HowToUseViewModel = .init()
var body: some View {
VStack{
VStack{
Text("Apple Podcast의 새로운 기능")
.multilineTextAlignment(.center)
.font(.largeTitle)
.fontWeight(.bold)
}
.padding(.vertical,48)
VStack(spacing:24){
ForEach(viewModel.howToUseList){ index in
HStack{
Image(systemName: "\(index.systemName)")
.resizable()
.scaledToFit()
.foregroundColor(.accentColor)
.frame(width:36,height: 36)
.padding(8)
VStack{
Text("\(index.title)")
.font(.semibold16)
.fontWeight(.bold)
.frame(maxWidth: .infinity,alignment: .leading)
.padding(.bottom,0.25)
Text("\(index.content)")
.font(.regular14)
.foregroundColor(.gray)
.lineLimit(3)
.frame(maxWidth: .infinity,alignment: .leading)
}
}.padding(.horizontal, 16)
}
Spacer()
VStack{
Image(systemName: "person.badge.shield.checkmark")
.resizable()
.scaledToFit()
.symbolRenderingMode(.hierarchical)
.foregroundColor(.accentColor)
.frame(width:24,height: 24)
Text("Apple Podcasts에서 구입한 팟캐스트 구독이 사용자의 Apple ID와 연계됩니다. 사용자의 기기 신뢰도 정보는 사기를 방지하는 데 사용됩니다. 사용자의 팟캐스트 청취 및 상호 작용 관련 데이터가 서비스를 향상하고 개인 맞춤화하기 위해 수집되며, 사용자의 Apple ID에 연계되지는 않습니다. ")
.font(.caption)
.foregroundColor(.gray)
.multilineTextAlignment(.center)
Text("사용자의 데이터가 어떻게 관리되는지 보기...")
.font(.caption)
.foregroundColor(.accentColor)
}
Button{
} label: {
Text("계속")
.font(.semibold16)
.padding(.horizontal)
.padding(.vertical, 6)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.padding(.bottom,24)
}
}.padding()
}
}Model
사실 View와 ViewModel은 온보딩화면에서까지 필요할까 싶었다. 간단한 데이터 구조이니 View와 Model 까지 구성하는 것만으로 알아볼 수 있지만, 장단점을 좀 더 경험보기 위해 MVVM을 사용했다.
막상 작성해놓으니 ViewModel에서만 데이터를 갈아치울 때, 매우 편할 것 같다. (하단에 MVVM 패턴을 적용하기전의 OnboardingView 코드도 첨부해놨으니, 궁금하신 분은 참고하시길...)
HowToUseModel.swift
import Foundation
struct HowToUse : Identifiable, Hashable {
var id : Int
var systemName: String = ""
var title: String = ""
var content : String = ""
}ViewModel
HowToUseModel.swift
import Foundation
import Combine
final class HowToUseViewModel: ObservableObject{
@Published var howToUseList: [HowToUse] = []
init(){
fetchModel()
}
public func fetchModel(){
self.howToUseList = [
HowToUse(id:1, systemName: "play.circle", title:"향상된 재생 제어기" , content:"재생 대기 목록, 재생 속도, 잠자기 타이머에 쉽게 접근할 수 있습니다."),
HowToUse(id:2, systemName: "list.bullet.rectangle.portrait.fill", title:"새로워진 '재생 대기목록' 디자인" , content:"이제 에피소드 표지와 함께 좋아하는 프로그램을 빠르게 재생할 수 있습니다."),
HowToUse(id:3, systemName: "square.3.layers.3d.top.filled", title:"구독 연결" , content:"Apple Music, Apple News 및 인기 앱에서 팟캐스트를 받을 수 있습니다.")
]
}
}MVVM 패턴 적용 전, 온보딩뷰
아래 코드는 MVVM 디자인 패턴 적용 전의 코드이다.
Onboarding.swift
import SwiftUI
struct HowToUse{
var id = UUID()
var systemName: String = ""
var title: String = ""
var content : String = ""
}
struct OnboardingView : View {
@State private var showingPopover = true
var howToUse = [
HowToUse(systemName: "clock", title:"제목01" , content:"본문내용입니다. 본문내용본문내용입니다.본문내용본문내용본문내용본문내용본문내용본문내용본문내용본문내용"),
HowToUse(systemName: "scribble.variable", title:"제목02" , content:"본문내용입니다. 본문내용본문내용입니다.본문내용본문내용본문내용본문내용본문내용본문내용본문내용본문내용"),
HowToUse(systemName: "pencil.and.outline", title:"제목03" , content:"본문내용입니다. 본문내용본문내용입니다.본문내용본문내용본문내용본문내용본문내용본문내용본문내용본문내용"),
]
var body: some View {
VStack{
Text("What's TimeBlock")
.font(.largeTitle)
.fontWeight(.bold)
.padding(.vertical,32)
VStack(spacing:24){
ForEach(0..<howToUse.count,id:\.self){index in
HStack{
Image(systemName: "\(howToUse[index].systemName)")
.resizable()
.scaledToFit()
.foregroundColor(.blue)
.frame(width:36,height: 36)
.padding()
VStack{
Text("\(howToUse[index].title)")
.font(.headline)
.fontWeight(.bold)
.frame(maxWidth: .infinity,alignment: .leading)
.padding(.bottom,0.5)
Text("\(howToUse[index].content)")
.font(.subheadline)
.foregroundColor(.gray)
.lineLimit(3)
.frame(maxWidth: .infinity,alignment: .leading)
}
}.padding(.horizontal,8)
}
Spacer()
Button(action: {}){
Text("계속하기")
.frame(maxWidth: .infinity)
.padding()
}
.buttonStyle(.borderedProminent)
.padding(.bottom,24)
}
}.padding()
}
}다음 게시물
Swift에서 프로토콜(Protocol)이란 무엇이며, 어떻게 사용하나요?