본문 바로가기

카테고리 없음

SwiftUI Showcase - Kavsoft (iOS 인터렉티브 애니메이션)

728x90
반응형

결과물🤟🏻: 

이용자들에게 어플리케이션을 사용하기 전, 기본 UI의 기능을 소개하는 튜토리얼 애니메이션

Result.gif

이번 애니메이션을 학습하게된 계기 🔫:

UI/UX의 디자인 및 애니메이션은 시각적인 화려함과 재미에 대해 중요도를 높이 평가할 수 있다. 또한, 이용자들에게 별도의 사용설명서 혹은 지침을 제공하지 않더라도 디자인 혹은 애니메이션을 통해 어플리케이션의 기능을 자연스럽게 이용 및 기능을 사용할 수 있도록 유도하는 것이 매우 중요하다.

Reference ✂️ : 

https://youtu.be/I9v-zqrE8gI

과정 🎩:

한개의 모델과 2개의 View로 구성하였으며 사용된 코드 중 학습한 코드에 대해 공부하고자 한다.

기본 View를 구성하는 것은 Mapkit과 TabView를 활용하여 구성하였다.

이번 클론코딩 학습을 통해 Highlight View에서 새로운 점들을 배울 수 있었기 때문에 Highlight View의 코드를 뜯어 보고자 한다.

import SwiftUI

// Custom Show Case View Extensions

extension View {
    @ViewBuilder
    func showCase(order: Int, title: String, cornerRadius: CGFloat, style: RoundedCornerStyle = .continuous, scale: CGFloat = 1) -> some View {
        self
        // Storing it in Anchor Preference
            .anchorPreference(key: HighlightAnchorKey.self, value: .bounds) { anchor in
                let highlight = Highlight(anchor: anchor, title: title, cornerRadius: cornerRadius, style: style, scale: scale)
                return [order: highlight]
            }
    }
}

// ShowCase Root
struct ShowCaseRoot: ViewModifier {
    var showHighlights: Bool
    var onFinished: () -> ()
    
    // View Properties
    @State private var highlightOrder: [Int] = []
    @State private var currentHighlight: Int = 0
    @State private var showView: Bool = true
    // Popover
    @State private var showTitle: Bool = false
    // NameSapce ID, for smooth Shape Transitions
    @Namespace private var animation
    
    func body(content: Content) -> some View {
        content
            .onPreferenceChange(HighlightAnchorKey.self) { value in
                highlightOrder = Array(value.keys).sorted()
            }
            .overlayPreferenceValue(HighlightAnchorKey.self) { preferences in
                if highlightOrder.indices.contains(currentHighlight), showHighlights, showView {
                    if let highlight = preferences[highlightOrder[currentHighlight]] {
                        HighlightView(highlight)
                    }
                }
            }
    }
    
    // Highlight View
    @ViewBuilder
    func HighlightView(_ highlight: Highlight) -> some View {
        // Geometry Reader for Extracting Highlight Frame Rects
        GeometryReader { proxy in
            let highlightRect = proxy[highlight.anchor]
            let safeArea = proxy.safeAreaInsets
            
            Rectangle()
                .fill(.black.opacity(0.5))
                .reverseMask {
                    Rectangle()
                        .matchedGeometryEffect(id: "HIGHLIGHTSHAPE", in: animation)
                        // Adding Border
                        // Simply Extend it's Size
                    
                        .frame(width: highlightRect.width + 5, height: highlightRect.height + 5)
                        .clipShape(RoundedRectangle(cornerRadius: highlight.cornerRadius, style: highlight.style))
                        .scaleEffect(highlight.scale)
                        .offset(x: highlightRect.minX - 2.5, y: highlightRect.minY + safeArea.top - 2.5)
                }
                .ignoresSafeArea()
                .onTapGesture {
                    if currentHighlight >= highlightOrder.count - 1 {
                        // Hiding the Highlight View, because it's the Last Highlight
                        withAnimation(.easeInOut(duration: 0.25)) {
                            showView = false
                        }
                        onFinished()
                    } else {
                        // Moving to next highlight
                        withAnimation(.interactiveSpring(response: 0.3, dampingFraction: 0.7, blendDuration: 0.7)) {
                            showTitle = false
                            currentHighlight += 1
                        }
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                            showTitle = true
                        }
                    }
                }
                .onAppear {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1){
                        showTitle = true
                    }
                }
            Rectangle()
                .foregroundColor(.clear)
                // Adding Border
                // Simply Extend it's Size
            
                .frame(width: highlightRect.width + 20, height: highlightRect.height + 20)
                .clipShape(RoundedRectangle(cornerRadius: highlight.cornerRadius, style: highlight.style))
                .popover(isPresented: $showTitle) {
                    Text(highlight.title)
                        .padding(.horizontal, 10)
                        .presentationCompactAdaptation(.popover)
                        .interactiveDismissDisabled()
                }
                .scaleEffect(highlight.scale)
                .offset(x: highlightRect.minX - 10, y: highlightRect.minY - 10)
                
        }
    }
}

// Custom View Modifier for Inner/Reverse Mask
extension View {
    @ViewBuilder
    func reverseMask<Content: View>(alignment: Alignment = .topLeading, @ViewBuilder content: @escaping () -> Content) -> some View {
        self
            .mask{
                Rectangle()
                    .overlay(alignment: .topLeading) {
                        content()
                            .blendMode(.destinationOut)
                    }
            }
    }
}


// Anchor Key
fileprivate struct HighlightAnchorKey: PreferenceKey {
    static var defaultValue: [Int: Highlight] = [:]
    
    static func reduce(value: inout [Int : Highlight], nextValue: () -> [Int : Highlight]) {
        value.merge(nextValue()) { $1 }
    }
}

struct ShowCaseHelper_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

 

 ViewBuilder Attribute

정의: Closure에서 View를 구성하는 custom parameter attribute이다. -> 클로져에서 View(Child)를 구성한다.

함수형 빌더이며 ViewBuilder를 사용하면 함수의 반환 값으로 여러 개의 View를 반환하는 것이 가능해진다. View 계층 구조를 쉽게 작성할 수 있다.

주로 반복문, 제어문에서 View를 반환할 때 유용하게 사용된다.

func myView<Content: View>(@ViewBuilder content: () -> Content) -> some View {
    VStack {
        Text("This is my View")
        content()
    }
}
 
struct ContentView: View {
    var body: some View {
        myView {
            Text("Hello World!")
        }
    }
}

 

 

 

학습하며 느낀점 🧑🏻‍🏫:

구글링을 통해 학습하면 Apple이 제공하는 Document보다는 전문가(나보다 훨씬 고수님들)분들이 작성하신 블로그를 우선적으로 클릭하게 된다. 한글로 읽는 것이 편하고 편하고.. 또 편하니까. 그러다 산을 만나게 되었는데 .popover Modifier의 .presentationCompactAdaptiation Modifier는 iOS 16.4 (작성 기준 현 iOS 16.6) 업데이트 때 ㄷㄷㄷㅈ한 놈이다.

당연히 누군가 적어주었겠지 했지만... 아직 고수님들이 안계신듯.. 하여 document를 보며 고심하고 영어 블로그로 가게 되었다... 

역시 한글사랑 나라사랑. 광복절 대한민국 만세. (그래도 document를 참고 버릇 하....자..?)

반응형