https://www.youtube.com/watch?v=eV9ybRJpuB8&list=TLGGZ5MvezILnkcyOTAzMjAyMw
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()
}
}
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
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
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()
}
.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(
.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
}
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)
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)
}
/// 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)
}
/// 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)
/// 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)
}
.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
}
}
})
)
}
@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)
}
}
})
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)
}
}
.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)
}
})
.onTapGesture {
withAnimation(.spring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)) {
dragProgress = .zero
}
}
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()
}
}