In a part of my app I want to present a set of photos to the user, similar to the Photos app (swipe left/right to change between photos, and delete).
Heavily inspired from Paul's example (https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-scrolling-pages-of-content-using-tabviewstyle), this is my code
public class ImageProvider: ObservableObject {
@Published private(set) var images: [UIImage]
init(images: [UIImage]) {
self.images = images
}
func remove(at index: Int) {
images.remove(at: index)
}
}
struct ContentView: View {
@ObservedObject var imageProvider: ImageProvider
@State private var selectionIndex: Int = 0
var body: some View {
ZStack(alignment: .topTrailing) {
Button {
withAnimation {
imageProvider.remove(at: selectionIndex)
}
} label: {
Image(systemName: "trash")
}
.zIndex(+1)
.font(.title)
.padding()
TabView(selection: $selectionIndex) {
ForEach(imageProvider.images, id: \.self) { uiImage in
Image(uiImage: uiImage)
.resizable()
.aspectRatio(nil, contentMode: .fit)
.tag(tag(for: uiImage))
}
}
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
private func tag(for uiImage: UIImage) -> Int {
return imageProvider.images.firstIndex { $0 == uiImage }!
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(imageProvider: createTestImageProvider())
}
}
public func createTestImageProvider() -> ImageProvider {
let images = (1...4)
.map { "image-\($0)"}
.map { UIImage(named: $0, in: Bundle.main, compatibleWith: nil)! }
return ImageProvider(images: images)
}
When I tap on the trash icon, and the tab view is showing any but the LAST view, the app crashes with following output
2021-11-05 22:20:55.219094+0100 TabViewCrashOnDelete[12304:741757] *** Assertion failure in -[_TtC7SwiftUIP33_8825076C2763A50452A210CBE1FA4AF020PagingCollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:animator:collectionViewAnimator:], UICollectionView.m:7110
2021-11-05 22:20:55.220326+0100 TabViewCrashOnDelete[12304:741757] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete and reload the same index path (<NSIndexPath: 0xae460e6c909291e8> {length = 2, path = 0 - 2})'
*** First throw call stack:
(0x1a0c5f928 0x1b49fe480 0x1a0b6d380 0x1a1ea5ef8 0x1a2c986b4 0x1a2ca3644 0x1a7193a24 0x1a71929f8 0x1a6ebda9c 0x1a6dc8830 0x1a6dc79c4 0x1a7580018 0x1a6dc12d4 0x1a6db8ed4 0x1a6f468c0 0x1c8229108 0x1c8229518 0x1c8232048 0x1a761bbb0 0x1a761e510 0x1a761d07c 0x1a761e468 0x1a716c300 0x1a7580660 0x1a757cb44 0x1a7168b64 0x1a761e448 0x1a761f5d4 0x1a718ef40 0x1a6fede54 0x1a6feddbc 0x1a6fe94a8 0x1c540ef50 0x1a6fedda0 0x1a6fededc 0x1a0bddc40 0x1a0bd8270 0x1a0bd880c 0x1a0bd7ed0 0x1b7323570 0x1a35052d0 0x1a350a84c 0x1a75b3530 0x1a75b34c0 0x1a7164870 0x10243e0b8 0x10243e158 0x1a08b6140)
libc++abi: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete and reload the same index path (<NSIndexPath: 0xae460e6c909291e8> {length = 2, path = 0 - 2})'
terminating with uncaught exception of type NSException
Any ideas or hints would be deeply appreciated.
I did some online search and found a work around, posting here my findings for future reference in case someone finds this useful.
Based on Paul's forum reply (https://www.hackingwithswift.com/forums/swiftui/how-do-i-append-a-new-page-into-tabview-pagetabviewstyle/2583/3449) and the associated SO question (https://stackoverflow.com/questions/63499707/tabview-with-pagetabviewstyle-does-not-update-its-content-when-state-var-cha/63500070#63500070), it seems that by marking the TabView
with a new id forces a refresh without a crash.
...
TabView(selection: $selectionIndex) {
ForEach(imageProvider.images, id: \.self) { uiImage in
Image(uiImage: uiImage)
.resizable()
.aspectRatio(nil, contentMode: .fit)
.tag(tag(for: uiImage))
}
}
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
.id(imageProvider.images.count) // <--- this fixed the crash
...
The reload animation is not perfect, but at least I can live with it for the time being.