소개

iOS 기본 앱인 Safari 브라우저(아이폰)을 보면 밑에 있는 URL 도구창 + 툴바가 스크롤 상태에 따라 변하는 모습을 볼 수 있습니다. 애니메이션 및 어떻게 돌아가는지 분석해보려고 합니다.

이번 포스트는 URL 도구창 + 툴바가 확대/축소하는 과정만 알아보겠습니다.

 

동작 분석

하단의 상태는 크게 둘로 나뉩니다.

  • 상태 1: URL 도구창이 확대되었고 툴바가 보이는 상태
  • 상태 2: URL 도구창이 축소되었고 툴바가 가려진 상태

 

 

그리고 스크롤 뷰의 동작에 따라 위의 두 상태중 하나가 됩니다.

(1) URL 입력창과 툴바가 보이는 상태에서 아래로 스크롤 및 확대를 한 경우

Animated GIF - Find & Share on GIPHY

  • 툴바가 사라지고, 도구창이 축소됩니다.
  • 스크롤뷰를 확대한 경우는 따로 찍지 않았지만 위와 동작이 동일합니다.

 

(2) URL 입력창과 툴바가 숨겨진 상태에서 위로 스크롤한 경우

Animated GIF - Find & Share on GIPHY

  • 축소되었던 도구창이 다시 확대되고, 툴바가 다시 나타납니다.

 

(3) URL 입력창과 툴바가 숨겨진 상태 + 최하단까지 스크롤된 상태에서 한 번 더 아래로 스크롤 한 경우

Animated GIF - Find & Share on GIPHY

  • 스크롤 맨 아래까지 스크롤 했을 때, 첫번째에서는 축소된 상태에서 따로 반응이 없습니다.
  • 이 상태에서 한번 더 아래로 스와이프하면 도구창이 확대되고 툴바가 나타납니다.

 

구현

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:))를 추가해 세로축으로 애니메이션 되도록 합니다.

 

스크린샷

Animated GIF - Find & Share on GIPHY

 

다음 포스트에서는 WebKitViewUIRepresentableView로 추가하고 위의 SwittUI 뷰와 연동하여 스크롤 상태에 따라 하단 바를 변형시키는 과정에 대해 다루겠습니다.

 

사실 애니메이션이 그렇게 똑같아 보이진 않긴 한데 계속 분석해서 더 비슷해지면 포스트를 업데이트하겠습니다.

문의 | 코멘트 또는 yoonbumtae@gmail.com


카테고리: Swift


0개의 댓글

답글 남기기

Avatar placeholder

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다