YamamotoDesu / PageCurlSwipeAnimation

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

PageCurlSwipeAnimation

https://www.youtube.com/watch?v=eV9ybRJpuB8&list=TLGGZ5MvezILnkcyOTAzMjAyMw

スクリーンショット 2023-03-29 22 00 24

Initial Set up

スクリーンショット 2023-03-29 15 35 23

Home.swift

import SwiftUI

struct Home: View {
    /// Sample Model for Displaying Images
    @State private var images: [ImageModel] = [
    ]
    var body: some View {
        ScrollView(.vertical, showsIndicators: false) {
            VStack(spacing: 20) {
                ForEach(images) { image in
                    PeelEffect {
                        CardView(image)
                    } onDelete: {
                        
                    }
                }
            }
            .padding(15)
        }
        .onAppear {
            for index in 1...4 {
                images.append(.init(assetName: "Pic \(index)"))
            }
        }
    }
    
    @ViewBuilder
    func CardView(_ imageModel: ImageModel) -> some View {
        GeometryReader {
            let size = $0.size
            
            ZStack {
                Image(imageModel.assetName)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: size.width, height: size.height)
                    .clipShape(RoundedRectangle(cornerRadius: 15, style: .continuous))
            }
        }
        .frame(height: 130)
        .contentShape(Rectangle())
    }
}

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

struct ImageModel: Identifiable {
    var id: UUID = .init()
    var assetName: String
}

PeelEffect.swift

import SwiftUI

/// Custom View Builder
struct PeelEffect<Content: View>: View {
    var content: Content
    /// Delete Callback for MainView, When Delete is Clicked
    var onDelete: () -> ()
    
    init(@ViewBuilder content: @escaping () -> Content, onDelete: @escaping () -> () ) {
        self.content = content()
        self.onDelete = onDelete
    }
    /// View Properties
    @State private var dragProgress: CGFloat = 0
    var body: some View {
        content
        /// Masking Original Content
            .mask {
                GeometryReader {
                    let rect = $0.frame(in: .global)
                    
                    Rectangle()
                    /// Swipe: Right to Left
                    /// Thus Masking from Right to Left ( Trailing)
                        .padding(.trailing, dragProgress * rect.width)
                }
            }
            .overlay {
                GeometryReader {
                    let rect = $0.frame(in: .global)
                    let size = $0.size
                    
                    content
                        .offset(x: size.width)
                        .contentShape(Rectangle())
                        .gesture(
                            DragGesture()
                                .onChanged({ value in
                                    /// Right to Left Swipe: Negative Value
                                    var translationX = value.translation.width
                                    translationX = max(-translationX, 0)
                                    /// Converting Translation Into Progress [0 - 1]
                                    let progress = max(1, translationX / size.width)
                                    dragProgress = progress
                                }).onEnded({ value in
                                    /// Smooth Ending Animation
                                    withAnimation(.spring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)) {
                                        dragProgress = .zero
                                    }
                                })
                        )
                }
            }
    }
}

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

スクリーンショット 2023-03-29 15 35 23

PeelEffect.swift

        content
            .offset(x: size.width)
            .contentShape(Rectangle())
            .gesture(
                DragGesture()
                    .onChanged({ value in
                        /// Right to Left Swipe: Negative Value
                        var translationX = value.translation.width
                        translationX = max(-translationX, 0)
                        /// Converting Translation Into Progress [0 - 1]
                        let progress = min(1, translationX / size.width)
                        dragProgress = progress

スクリーンショット 2023-03-29 15 35 23

PeelEffect.swift

            content
                /// Fliping Horizontallyh for Update Image
                .scaleEffect(x: -1)
                /// Moving A;long Side While Dragging
                .offset(x: size.width - (size.width * dragProgress))
                .contentShape(Rectangle())
                .gesture(
                    DragGesture()
                        .onChanged({ value in
                            /// Right to Left Swipe: Negative Value
                            var translationX = value.translation.width
                            translationX = max(-translationX, 0)
                            /// Converting Translation Into Progress [0 - 1]
                            let progress = min(1, translationX / size.width)
                            dragProgress = progress
                        }).onEnded({ value in

スクリーンショット 2023-03-29 15 35 23

            content
                /// Fliping Horizontallyh for Update Image
                .scaleEffect(x: -1)
                /// Moving A;long Side While Dragging
                .offset(x: size.width - (size.width * dragProgress))
                /// Masking Overlayed Image for Removing Outbound Visibility
                .mask {
                    Rectangle()
                }

Make it more quicker

スクリーンショット 2023-03-29 15 35 23

            .overlay {
                GeometryReader {
                    let rect = $0.frame(in: .global)
                    let size = $0.size
                    
                    content
                        /// Fliping Horizontallyh for Update Image
                        .scaleEffect(x: -1)
                        /// Moving A;long Side While Dragging
                        .offset(x: size.width - (size.width * dragProgress))
                        .offset(x: size.width * -dragProgress) // added
                        /// Masking Overlayed Image for Removing Outbound Visibility
                        .mask {
                            Rectangle()
                        }
                        .contentShape(Rectangle())
                        .gesture(

Covering up the region

スクリーンショット 2023-03-29 15 35 23

            .overlay {
                GeometryReader {
                    let rect = $0.frame(in: .global)
                    let size = $0.size
                    
                    content
                        /// Fliping Horizontallyh for Update Image
                        .scaleEffect(x: -1)
                        /// Moving A;long Side While Dragging
                        .offset(x: size.width - (size.width * dragProgress))
                        .offset(x: size.width * -dragProgress)
                        /// Masking Overlayed Image for Removing Outbound Visibility
                        .mask {
                            Rectangle()
                                .offset(x: size.width * -dragProgress) //added
                        }

Set up Deleting BK

スクリーンショット 2023-03-29 15 35 23

      content
            /// Masking Original Content
            .mask {
                GeometryReader {
                    let rect = $0.frame(in: .global)

                    Rectangle()
                    /// Swipe: Right to Left
                    /// Thus Masking from Right to Left ( Trailing)
                        .padding(.trailing, dragProgress * rect.width)
                }
            }
            .overlay {
                GeometryReader {
                    let rect = $0.frame(in: .global)
                    let size = $0.size
                    
                    content
                        /// Fliping Horizontallyh for Update Image
                        .scaleEffect(x: -1)
                        /// Moving A;long Side While Dragging
                        .offset(x: size.width - (size.width * dragProgress))
                        .offset(x: size.width * -dragProgress)
                        /// Masking Overlayed Image for Removing Outbound Visibility
                        .mask {
                            Rectangle()
                                .offset(x: size.width * -dragProgress)
                        }
                        .contentShape(Rectangle())
                        .gesture(
                            DragGesture()
                                .onChanged({ value in
                                    /// Right to Left Swipe: Negative Value
                                    var translationX = value.translation.width
                                    translationX = max(-translationX, 0)
                                    /// Converting Translation Into Progress [0 - 1]
                                    let progress = min(1, translationX / size.width)
                                    dragProgress = progress
                                }).onEnded({ value in
                                    /// Smooth Ending Animation
                                    withAnimation(.spring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)) {
                                        dragProgress = .zero
                                    }
                                })
                        )
                }
            }
            .background { //added
                RoundedRectangle(cornerRadius: 15, style: .continuous)
                    .fill(.red.gradient)
                    .overlay(alignment: .trailing) {
                        Image(systemName: "trash")
                            .font(.title)
                            .fontWeight(.semibold)
                            .padding(.trailing, 20)
                            .foregroundColor(.white)
                    }
                    .padding(.vertical, 8)

Set Overlay

スクリーンショット 2023-03-29 15 35 23

                    content
                       /// Making it Look like it's Rolling
                       .shadow(color: .black.opacity(dragProgress != 0 ? 0.1 : 0), radius: 5, x: 15, y:0)
                       .overlay {
                           Rectangle()
                               .fill(.white.opacity(0.25))
                               .mask(content)
                       }

Make Shadow

スクリーンショット 2023-03-29 15 35 23

        /// Background Shadow
        .background {
            GeometryReader {
                let rect = $0.frame(in: .global)

                Rectangle()
                    .fill(.black)
                    .shadow(color: .black.opacity(0.3), radius: 15, x: 30, y: 0)
                    /// Moving Along Side While Dragging
                    .padding(.trailing, rect.width * dragProgress)
            }
            .mask(content)
        }

Make Extra Shadow

スクリーンショット 2023-03-29 15 35 23

        /// Background Shadow
        .background {
            GeometryReader {
                let rect = $0.frame(in: .global)

                Rectangle()
                    .fill(.black)
                    .padding(.vertical, 23) // added
                    .shadow(color: .black.opacity(0.3), radius: 15, x: 30, y: 0)
                    /// Moving Along Side While Dragging
                    .padding(.trailing, rect.width * dragProgress)
            }
            .mask(content)

Make White Shadow

スクリーンショット 2023-03-29 15 35 23

         /// Making it Glow At the Back Side
            .overlay(alignment: .trailing) {
                Rectangle()
                    .fill(
                        .linearGradient(colors: [
                            .clear,
                            .white,
                            .clear,
                            .clear
                        ], startPoint: .leading, endPoint: .trailing)
                    )
                    .frame(width: 60)
                    .offset(x: 40)
                    .offset(x: -30 + (30 * opacity))
                    /// Moving Along Side While Dragging
                    .offset(x: size.width * -dragProgress)
            }

Set Fixed Dragprgress

スクリーンショット 2023-03-29 15 35 23

    .gesture(
        DragGesture()
            .onChanged({ value in
                /// Right to Left Swipe: Negative Value
                var translationX = value.translation.width
                translationX = max(-translationX, 0)
                /// Converting Translation Into Progress [0 - 1]
                let progress = min(1, translationX / size.width)
                dragProgress = progress
            }).onEnded({ value in
                /// Smooth Ending Animation
                withAnimation(.spring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)) {
                    if dragProgress > 0.25 {
                        dragProgress = 0.6
                    } else {
                        dragProgress = .zero
                    }
                }
            })
    )
}

Masking Original Content

スクリーンショット 2023-03-29 21 57 48

    @State private var dragProgress: CGFloat = 0
    var body: some View {
        content
            .hidden()
            .overlay(content: {
                GeometryReader {
                    let rect = $0.frame(in: .global)

                    Rectangle()
                    
                    content
                        .mask {
                            Rectangle()
                            /// Masking Original Content
                            /// Swipe: Right to Left
                            /// Thus Masking from Right to Left ( Trailing)
                                .padding(.trailing, dragProgress * rect.width)
                        }
                }
            })

Move Deleting BK from Background to Overlay

スクリーンショット 2023-03-29 22 00 24

        content
            .hidden()
            .overlay(content: {
                GeometryReader {
                    let rect = $0.frame(in: .global)

                    RoundedRectangle(cornerRadius: 15, style: .continuous)
                        .fill(.red.gradient)
                        .overlay(alignment: .trailing) {
                            Image(systemName: "trash")
                                .font(.title)
                                .fontWeight(.semibold)
                                .padding(.trailing, 20)
                                .foregroundColor(.white)
                        }
                        .padding(.vertical, 8)
                    
                    content
                        .mask {
                            Rectangle()
                            /// Masking Original Content
                            /// Swipe: Right to Left
                            /// Thus Masking from Right to Left ( Trailing)
                                .padding(.trailing, dragProgress * rect.width)
                        }
                }

Make Trash Icon Tappable

スクリーンショット 2023-03-29 22 00 24

            .hidden()
            .overlay(content: {
                GeometryReader {
                    let rect = $0.frame(in: .global)

                    RoundedRectangle(cornerRadius: 15, style: .continuous)
                        .fill(.red.gradient)
                        .overlay(alignment: .trailing) {
                            Button {
                                print("Tapped")
                            } label: {
                                Image(systemName: "trash")
                                    .font(.title)
                                    .fontWeight(.semibold)
                                    .padding(.trailing, 20)
                                    .foregroundColor(.white)
                            }
                            .disabled(dragProgress < 0.6)
                        }
                        .padding(.vertical, 8)
                        .contentShape(Rectangle())
                        .gesture(
                            DragGesture()
                                .onChanged({ value in
                                    /// Right to Left Swipe: Negative Value
                                    var translationX = value.translation.width
                                    translationX = max(-translationX, 0)
                                    /// Converting Translation Into Progress [0 - 1]
                                    let progress = min(1, translationX / rect.width)
                                    dragProgress = progress
                                }).onEnded({ value in
                                    /// Smooth Ending Animation
                                    withAnimation(.spring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)) {
                                        if dragProgress > 0.25 {
                                            dragProgress = 0.6
                                        } else {
                                            dragProgress = .zero
                                        }
                                    }
                                })
                        )
                    content
                        .mask {
                            Rectangle()
                            /// Masking Original Content
                            /// Swipe: Right to Left
                            /// Thus Masking from Right to Left ( Trailing)
                            .padding(.trailing, dragProgress * rect.width)
                        }
                        /// Disable Interaction
                        .allowsHitTesting(false)
                }
            })

Tap Other Than Delete Button. It will reset to initial State

スクリーンショット 2023-03-29 22 00 24

    .onTapGesture {
        withAnimation(.spring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)) {
            dragProgress = .zero
        }
    }

Completed

スクリーンショット 2023-03-29 22 00 24

Home.swift

import SwiftUI

struct Home: View {
    /// Sample Model for Displaying Images
    @State private var images: [ImageModel] = [
    ]
    var body: some View {
        ScrollView(.vertical, showsIndicators: false) {
            VStack(spacing: 20) {
                ForEach(images) { image in
                    PeelEffect {
                        CardView(image)
                    } onDelete: {
                        if let index = images.firstIndex(where: { C1 in
                            C1.id == image.id
                        }) {
                            let _ = withAnimation(.easeInOut(duration: 0.35)) {
                                images.remove(at: index)
                            }
                        }
                    }
                }
            }
            .padding(15)
        }
        .onAppear {
            for index in 1...4 {
                images.append(.init(assetName: "Pic \(index)"))
            }
        }
    }
    
    @ViewBuilder
    func CardView(_ imageModel: ImageModel) -> some View {
        GeometryReader {
            let size = $0.size
            
            ZStack {
                Image(imageModel.assetName)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: size.width, height: size.height)
                    .clipShape(RoundedRectangle(cornerRadius: 15, style: .continuous))
            }
        }
        .frame(height: 130)
        .contentShape(Rectangle())
    }
}

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

struct ImageModel: Identifiable {
    var id: UUID = .init()
    var assetName: String
}

PeelEffect.swift

import SwiftUI

/// Custom View Builder
struct PeelEffect<Content: View>: View {
    var content: Content
    /// Delete Callback for MainView, When Delete is Clicked
    var onDelete: () -> ()
    
    init(@ViewBuilder content: @escaping () -> Content, onDelete: @escaping () -> () ) {
        self.content = content()
        self.onDelete = onDelete
    }
    /// View Properties
    @State private var dragProgress: CGFloat = 0
    @State private var isExpanded: Bool = false
    var body: some View {
        content
            .hidden()
            .overlay(content: {
                GeometryReader {
                    let rect = $0.frame(in: .global)
                    let minX = rect.minX

                    RoundedRectangle(cornerRadius: 15, style: .continuous)
                        .fill(.red.gradient)
                        .overlay(alignment: .trailing) {
                            Button {
                                /// Removing Card Completelt
                                withAnimation(.spring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)) {
                                    dragProgress = 1
                                }
                                DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
                                    onDelete()
                                }
                            } label: {
                                Image(systemName: "trash")
                                    .font(.title)
                                    .fontWeight(.semibold)
                                    .padding(.trailing, 20)
                                    .foregroundColor(.white)
                            }
                            .disabled(!isExpanded)
                        }
                        .padding(.vertical, 8)
                        .contentShape(Rectangle())
                        .gesture(
                            DragGesture()
                                .onChanged({ value in
                                    /// Disabling Gesture When it's Expanded
                                    guard !isExpanded else { return }
                                    /// Right to Left Swipe: Negative Value
                                    var translationX = value.translation.width
                                    translationX = max(-translationX, 0)
                                    /// Converting Translation Into Progress [0 - 1]
                                    let progress = min(1, translationX / rect.width)
                                    dragProgress = progress
                                }).onEnded({ value in
                                    /// Disabling Gesture When it's Expanded
                                    guard !isExpanded else { return }
                                    /// Smooth Ending Animation
                                    withAnimation(.spring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)) {
                                        if dragProgress > 0.25 {
                                            dragProgress = 0.6
                                            isExpanded = true
                                        } else {
                                            dragProgress = .zero
                                            isExpanded = false
                                        }
                                    }
                                })
                        )
                       /// If we Tap Other Than Delete Button. It will reset to initial State
                        .onTapGesture {
                            withAnimation(.spring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)) {
                                dragProgress = .zero
                                isExpanded = false
                            }
                        }
                    
                    // Shadow
                    Rectangle()
                        .fill(.black)
                        .padding(.vertical, 23)
                        .shadow(color: .black.opacity(0.3), radius: 15, x: 30, y: 0)
                        /// Moving Along Side While Dragging
                        .padding(.trailing, rect.width * dragProgress)
                        .mask(content)
                        /// Diabling Interaction
                        .allowsHitTesting(false)
                        .offset(x: dragProgress == 1 ? -minX : 0)
                    
                    content
                        .mask {
                            Rectangle()
                            /// Masking Original Content
                            /// Swipe: Right to Left
                            /// Thus Masking from Right to Left ( Trailing)
                            .padding(.trailing, dragProgress * rect.width)
                        }
                        /// Disable Interaction
                        .allowsHitTesting(false)
                        .offset(x: dragProgress == 1 ? -minX : 0)

                }
            })
            .overlay {
                GeometryReader {
                    let size = $0.size
                    let minX = $0.frame(in: .global).minX
                    let minOpacity = dragProgress / 0.5
                    let opacity = min(1, minOpacity)
                    
                    content
                        /// Making it Look like it's Rolling
                        .shadow(color: .black.opacity(dragProgress != 0 ? 0.1 : 0), radius: 5, x: 15, y:0)
                        .overlay {
                            Rectangle()
                                .fill(.white.opacity(0.25))
                                .mask(content)
                        }
                        /// Making it Glow At the Back Side
                        .overlay(alignment: .trailing) {
                            Rectangle()
                                .fill(
                                    .linearGradient(colors: [
                                        .clear,
                                        .white,
                                        .clear,
                                        .clear
                                    ], startPoint: .leading, endPoint: .trailing)
                                )
                                .frame(width: 60)
                                .offset(x: 40)
                                .offset(x: -30 + (30 * opacity))
                                /// Moving Along Side While Dragging
                                .offset(x: size.width * -dragProgress)
                        }
                        /// Fliping Horizontallyh for Update Image
                        .scaleEffect(x: -1)
                        /// Moving A;long Side While Dragging
                        .offset(x: size.width - (size.width * dragProgress))
                        .offset(x: size.width * -dragProgress)
                        /// Masking Overlayed Image for Removing Outbound Visibility
                        .mask {
                            Rectangle()
                                .offset(x: size.width * -dragProgress)
                        }
                        .offset(x: dragProgress == 1 ? -minX : 0)
                }
                .allowsHitTesting(false)
            }
    }
}

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

About


Languages

Language:Swift 100.0%