I have LazyVStack view that contains a list of views. Each one of the views has a different color and there is 8 points space between them. Threrefore, I can not use List.
So I am trying to build a custom trailing swipe that functions similar to the onDelete method of List. This is my code and it is not perfect, but I am on the right directin, I think.
Test Data - List of countries
class Data: ObservableObject {
@Published var countries: [String]
init() {
self.countries = NSLocale.isoCountryCodes.map { (code:String) -> String in
let id = NSLocale.localeIdentifier(fromComponents: [NSLocale.Key.countryCode.rawValue: code])
return NSLocale(localeIdentifier: "en_US").displayName(forKey: NSLocale.Key.identifier, value: id) ?? "Country not found for code: \(code)"
}
}
}
ContentView
struct ContentView: View {
@ObservedObject var data: Data = Data()
var body: some View {
ScrollView {
LazyVStack {
ForEach(data.countries, id: \.self) { country in
VStack {
SwipeView(content: {
VStack(spacing: 0) {
Spacer()
Text(country)
.frame(minWidth: 0, maxWidth: .infinity)
Spacer()
}
.background(Color.yellow)
}, trailingActionView: {
Image(systemName: "trash")
.foregroundColor(.white)
}) {
self.data.countries.removeAll {$0 == country}
}
}
.clipShape(Rectangle())
}
}
}
.padding(.vertical, 16)
}
}
Custom SwipeView
struct SwipeView<Content: View, TrailingActionView: View>: View {
let width = UIScreen.main.bounds.width - 32
@State private var height: CGFloat = .zero
@State var offset: CGFloat = 0
let content: Content
let trailingActionView: TrailingActionView
var onDelete: () -> ()
init(@ViewBuilder content: () -> Content,
@ViewBuilder trailingActionView: () -> TrailingActionView,
onDelete: @escaping () -> Void) {
self.content = content()
self.trailingActionView = trailingActionView()
self.onDelete = onDelete
}
var body: some View {
ZStack {
HStack(spacing: 0) {
Button(action: {
withAnimation {
self.onDelete()
}
}) {
trailingActionView
}
.frame(minHeight: 0, maxHeight: .infinity)
.frame(width: 60)
Spacer()
}
.background(Color.red)
.frame(width: width)
.offset(x: width + self.offset)
content
.frame(width: width)
.contentShape(Rectangle())
.offset(x: self.offset)
.gesture(DragGesture().onChanged(onChanged).onEnded { value in
onEnded(value: value, width: width)
})
}
.background(Color.white)
}
private func onChanged(value: DragGesture.Value) {
let translation = value.translation.width
if translation < 0 {
self.offset = translation
} else {
}
}
private func onEnded(value: DragGesture.Value,width: CGFloat) {
withAnimation(.easeInOut) {
let translation = -value.translation.width
if translation > width - 16 {
self.onDelete()
self.offset = -(width * 2)
}
else if translation > 50 {
self.offset = -50
}
else {
self.offset = 0
}
}
}
}
It has one annoying problem: If you swipe a row and do not delete it. And if you swipe another views, they don not reset. All the trailing Delete Views are visible. But I want to reset/ swipe back if you tap anywhere outside the Delete View.
I want to swipe back if you tap anywhere outside the Delete View. So how to do it?
SwipeView, because it is needed internally react on external conditions, so state holding exclusiveness of activity should be located externally and injected inside via binding. It requires a lot of changes in your code, so try it at first by yourself, if such idea appropriate for you. - Asperi