소개
iOS 기본 앱인 Safari 브라우저(아이폰)을 보면 밑에 있는 URL 도구창 + 툴바가 스크롤 상태에 따라 변하는 모습을 볼 수 있습니다. 애니메이션 및 어떻게 돌아가는지 분석해보려고 합니다.
이번 포스트는 URL 도구창 + 툴바가 확대/축소하는 과정만 알아보겠습니다.
동작 분석
하단의 상태는 크게 둘로 나뉩니다.
-
상태 1: URL 도구창이 확대되었고 툴바가 보이는 상태

-
상태 2: URL 도구창이 축소되었고 툴바가 가려진 상태

그리고 스크롤 뷰의 동작에 따라 위의 두 상태중 하나가 됩니다.
(1) URL 입력창과 툴바가 보이는 상태에서 아래로 스크롤 및 확대를 한 경우
- 툴바가 사라지고, 도구창이 축소됩니다.
- 스크롤뷰를 확대한 경우는 따로 찍지 않았지만 위와 동작이 동일합니다.
(2) URL 입력창과 툴바가 숨겨진 상태에서 위로 스크롤한 경우
- 축소되었던 도구창이 다시 확대되고, 툴바가 다시 나타납니다.
(3) URL 입력창과 툴바가 숨겨진 상태 + 최하단까지 스크롤된 상태에서 한 번 더 아래로 스크롤 한 경우
- 스크롤 맨 아래까지 스크롤 했을 때, 첫번째에서는 축소된 상태에서 따로 반응이 없습니다.
- 이 상태에서 한번 더 아래로 스와이프하면 도구창이 확대되고 툴바가 나타납니다.
구현
Step 1: SwiftUI로 사파리 하단 부분을 최대한 비슷하게 그리기
struct LikeSafariView: View {
// ... 상태변수 추가 ... //
// 하단 툴바 배경색 강제적용
init() {
let toolbarAppearance = UIToolbarAppearance()
toolbarAppearance.configureWithOpaqueBackground()
toolbarAppearance.backgroundColor = .clear
toolbarAppearance.shadowColor = .clear
UIToolbar.appearance().standardAppearance = toolbarAppearance
UIToolbar.appearance().scrollEdgeAppearance = toolbarAppearance
}
/// URL 창: 확대된 상태
private var urlLargeArea: some View {
VStack(spacing: 0) {
// 테두리(위)
Rectangle()
.fill(Color(white: 238/255))
.frame(height: 1)
HStack {
// 하얀색 둥근 사각형
RoundedRectangle(cornerRadius: 10)
.fill(Color(white: 253/255))
.frame(height: 50)
.padding()
.shadow(color: .init(red: 0.8, green: 0.8, blue: 0.8), radius: 10, y: 5)
.overlay {
// 둥근 사각형 안의 내용
HStack {
Image(systemName: "character")
Spacer()
// URL 부분
HStack {
Image(systemName: "lock.fill")
.foregroundStyle(.gray)
Text("en.m.wikipedia.org")
}
Spacer()
Image(systemName: "arrow.clockwise")
}
.padding(30)
}
}
.padding(.bottom, -15)
.background(Color(white: 247/255))
// ...트랜지션 추가...
}
}
/// URL 창: 축소된 상태
private var urlShrinkedArea: some View {
ZStack(alignment: .center) {
// 배경색
Color(red: 247/255, green: 247/255, blue: 247/255).ignoresSafeArea()
VStack(spacing: 0) {
// 테두리(위)
Rectangle()
.fill(Color(white: 238/255))
.frame(height: 1)
HStack(alignment: .center) {
Image(systemName: "lock.fill")
.foregroundStyle(.gray)
Text("en.m.wikipedia.org")
}
.padding(.top, 15)
.font(.system(size: 15))
}
}
.frame(height: 10)
// ...트랜지션 추가...
}
/// Body
var body: some View {
NavigationStack {
VStack(spacing: 0) {
// ... 메인 부분 ...
}
// 툴바 그리기
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
HStack {
Button {} label: {
Image(systemName: "chevron.left")
}
Spacer()
Button {} label: {
Image(systemName: "chevron.right")
}
Spacer()
Button {} label: {
Image(systemName: "square.and.arrow.up")
}
Spacer()
Button {} label: {
Image(systemName: "book")
}
Spacer()
Button {} label: {
Image(systemName: "square.on.square")
}
}
.padding(10)
.padding(.top, 22)
}
}
}
}
}


- SwiftUI의 기본 컴포넌트와 SF Symbol 등을 활용해 최대한 비슷하게 흉내내봅니다.
- 분량상 자세한 설명은 생략합니다.
Step 2: 하단바 상태를 토글할 상태변수(@State) 추가
struct LikeSafariView: View {
@State private var showToolBar = true
// ... //
}
showToolBar: Bool 값에 따라true인 경우 확대모드,false인 경우 축소모드로 사용합니다.
Step 3: showToolBar에 따라 하단바 토글하는 부분 추가
struct LikeSafariView: View {
@State private var showToolBar = true
/// URL 창: 확대된 상태
private var urlLargeArea: some View { // ... // }
/// URL 창: 축소된 상태
private var urlShrinkedArea: some View { // ... // }
/// Body
var body: some View {
NavigationStack {
VStack(spacing: 0) {
VStack(alignment: .leading) {
Text("WebKit View가 들어갈 예정")
.padding(.top, 20)
.font(.title)
Text("- WebKitView를 Representable로 연결")
Text("- 스크롤 상태를 뷰모델을 통해 받아옴")
Text("- 스크롤 상태에 따라 URL 창 크기 조절 및 \n툴바의 표시 여부 결정")
Divider()
Button("State Toggle") {
showToolBar.toggle()
}
}
.padding([.leading, .trailing], 20)
Spacer()
if showToolBar {
urlLargeArea
} else {
urlShrinkedArea
}
}
// 툴바(맨 아래 아이콘 5개 부분) 보이기 여부
.toolbar(showToolBar ? .visible : .hidden, for: .bottomBar)
// 툴바 그리기
.toolbar { ... }
}
}
}
"State Toggle"버튼을 누르면showToolBar가 토글됩니다.showToolBar의 값 여부에 따라 어떻게 보여질지 정해지게 됩니다.true인 경우urlLargeArea뷰를 표시하고, 툴바를 보여줍(.visible)니다.false인 경우urlShrinkedArea뷰를 표시하고, 툴바를 가립(.hidden)니다.
Step 4: 애니메이션(Transition, withAnimaiton) 추가 [전체 코드]
import SwiftUI
struct LikeSafariView: View {
@State private var showToolBar = true
// 하단 툴바 배경색 강제적용
init() {
let toolbarAppearance = UIToolbarAppearance()
toolbarAppearance.configureWithOpaqueBackground()
toolbarAppearance.backgroundColor = .clear
toolbarAppearance.shadowColor = .clear
UIToolbar.appearance().standardAppearance = toolbarAppearance
UIToolbar.appearance().scrollEdgeAppearance = toolbarAppearance
}
/// URL 창: 확대된 상태
private var urlLargeArea: some View {
VStack(spacing: 0) {
// 테두리(위)
Rectangle()
.fill(Color(white: 238/255))
.frame(height: 1)
HStack {
// 하얀색 둥근 사각형
RoundedRectangle(cornerRadius: 10)
.fill(Color(white: 253/255))
.frame(height: 50)
.padding()
.shadow(color: .init(red: 0.8, green: 0.8, blue: 0.8), radius: 10, y: 5)
.overlay {
// 둥근 사각형 안의 내용
HStack {
Image(systemName: "character")
Spacer()
// URL 부분
HStack {
Image(systemName: "lock.fill")
.foregroundStyle(.gray)
Text("en.m.wikipedia.org")
}
Spacer()
Image(systemName: "arrow.clockwise")
}
.padding(30)
}
}
.padding(.bottom, -15)
.background(Color(white: 247/255))
.transition(.offset(y: 100))
}
}
/// URL 창: 축소된 상태
private var urlShrinkedArea: some View {
ZStack(alignment: .center) {
// 배경색
Color(red: 247/255, green: 247/255, blue: 247/255).ignoresSafeArea()
VStack(spacing: 0) {
// 테두리(위)
Rectangle()
.fill(Color(white: 238/255))
.frame(height: 1)
HStack(alignment: .center) {
Image(systemName: "lock.fill")
.foregroundStyle(.gray)
Text("en.m.wikipedia.org")
}
.padding(.top, 15)
.font(.system(size: 15))
}
}
.frame(height: 10)
.transition(.offset(y: -75))
}
/// Body
var body: some View {
NavigationStack {
VStack(spacing: 0) {
VStack(alignment: .leading) {
Text("WebKit View가 들어갈 예정")
.padding(.top, 20)
.font(.title)
Text("- WebKitView를 Representable로 연결")
Text("- 스크롤 상태를 뷰모델을 통해 받아옴")
Text("- 스크롤 상태에 따라 URL 창 크기 조절 및 \n툴바의 표시 여부 결정")
Divider()
Button("State Toggle") {
withAnimation(.bouncy(duration: 0.2, extraBounce: -0.2)) {
showToolBar.toggle()
}
}
}
.padding([.leading, .trailing], 20)
Spacer()
if showToolBar {
urlLargeArea
} else {
urlShrinkedArea
}
}
// 툴바(맨 아래 아이콘 5개 부분) 보이기 여부
.toolbar(showToolBar ? .visible : .hidden, for: .bottomBar)
// 툴바 그리기
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
HStack {
Button {} label: {
Image(systemName: "chevron.left")
}
Spacer()
Button {} label: {
Image(systemName: "chevron.right")
}
Spacer()
Button {} label: {
Image(systemName: "square.and.arrow.up")
}
Spacer()
Button {} label: {
Image(systemName: "book")
}
Spacer()
Button {} label: {
Image(systemName: "square.on.square")
}
}
.padding(10)
.padding(.top, 22)
}
}
}
}
}
showToolBar토글시withAnimaiton {...}으로 감싸 애니메이션이 동작하도록 합니다.- URL 도구창 뷰마다
transition(.offset(y:))를 추가해 세로축으로 애니메이션 되도록 합니다.
스크린샷
다음 포스트에서는 WebKitView를 UIRepresentableView로 추가하고 위의 SwittUI 뷰와 연동하여 스크롤 상태에 따라 하단 바를 변형시키는 과정에 대해 다루겠습니다.
사실 애니메이션이 그렇게 똑같아 보이진 않긴 한데 계속 분석해서 더 비슷해지면 포스트를 업데이트하겠습니다.









0개의 댓글