Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moving items in a RealmSwift.List to a higher index results in wrong position after move #7956

Closed
phranck opened this issue Sep 14, 2022 · 8 comments
Assignees

Comments

@phranck
Copy link

phranck commented Sep 14, 2022

How frequently does the bug occur?

All the time

Description

Take this code for demonstration:

final class MyImage: Object {
    @Persisted(primaryKey: true) public var _id: ObjectId
    @Persisted var name: String = ""
    @Persisted var path: String = ""
}

final class Item: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) public var _id: ObjectId
    @Persisted var name: String = ""
    @Persisted var images: RealmSwift.List<MyImage> = .init()
}

struct MyView: View {
    @ObservedRealmObject var item: Item
	
    var body: some View {
	VStack {
	    List {
		ForEach(item) { image in
		    Text(image.name)
		}
		.onMove(perform: $item.images.move)
	    }
	}
    }
}

We have two model objects, an MyImage and an Item. The Item contains a Realm list of MyImage's. The MyView creates a list with image names that can be moved to another position. When I drag an item to a lower index (e.g. from 3 to 0 everything is fine. But when I drag it from 0 to 3, in the result it is placed at 4.

So, every time I drag an item to a higher destination index the result is draggedDestIndex + 1 (for moving only one item).

My dirty quick fix is an override of the move function in a BundleCollection extension.

internal extension BoundCollection where Value == RealmSwift.List<Element> {
    func move(fromOffsets offsets: IndexSet, toOffset destination: Int) {
        // This is the fix
        var dest = destination
        if let firstOffset = offsets.first {
            if destination > firstOffset {
                dest = destination - 1
            }
        }

        // proceed with original call of function
        safeWrite(self.wrappedValue) { list in
            list.move(fromOffsets: offsets, toOffset: dest)
        }
    }

    private func safeWrite<Value>(_ value: Value, _ block: (Value) -> Void) where Value: ThreadConfined {
        let thawed = value.realm == nil ? value : value.thaw() ?? value
        if let realm = thawed.realm, !realm.isInWriteTransaction {
            try! realm.write {
                block(thawed)
            }
        } else {
            block(thawed)
        }
    }
}

Stacktrace & log output

No response

Can you reproduce the bug?

Yes, always

Reproduction Steps

To reproduce the bug please checkout this sample project and let it run.

Version

10.29.0

What SDK flavour are you using?

Local Database only

Are you using encryption?

No, not using encryption

Platform OS and version(s)

macOS, iOS

Build environment

Xcode version: Version 14.0 (14A309)
Dependency manager and version: SPM

@phranck phranck added the T-Bug label Sep 14, 2022
@phranck phranck changed the title Moving items in a RealmSwift.List to a higher index results wrong position after move Moving items in a RealmSwift.List to a higher index results in wrong position after move Sep 14, 2022
@dianaafanador3
Copy link
Collaborator

I created a PR which may solve this issue #7960

@sonisan
Copy link

sonisan commented Sep 29, 2022

Is this issue linked to other problematic use cases I encountered using List, such as this one which provokes a crash (index out of bound):

ForEach(listOfUsersId, id: \.self) { id in
        Text(realmManager.getUserNameBy(id))
	        .swipeActions {
		        Button("Unblock") {
			        showUnblockAlert = true // upon alert's confirmation, item will be removed in listOfUsersId
			        selectedId = id
		        }
	        }
}

In the meantime, replacing the list by an array seems to solve this: ForEach(Array(listOfUsersId)), id: \.self) { id in ...

@dianaafanador3
Copy link
Collaborator

dianaafanador3 commented Sep 29, 2022

HI @sonisan are you using one of our property wrappers?, can you please share the code for the View for closer examination?, I may add a test for this to make sure this is fixed or not

@sonisan
Copy link

sonisan commented Sep 29, 2022

Hello @dianaafanador3, no I am not using Realm's ones for this specific view:

import SwiftUI
import RealmSwift

struct BlockedUsersView: View {
	
	@EnvironmentObject var realmManager: RealmManager
	@State private var showUnblockAlert = false
	@State private var selectedId: ObjectId?
	
	var body: some View {
		VStack {
			List {
				if let currentUser = realmManager.getCurrentUser() {
					ForEach(currentUser.blockedUserId, id: \.self) { id in
						Text(realmManager.getUserNameBy(id: id))
							.swipeActions {
								Button("Unblock") {
									showUnblockAlert = true
									selectedId = id
								}
								.tint(.green)
							}
					}
				}
			}
		}
		.alert("Do you want to unblock this user?", isPresented: $showUnblockAlert, actions: {
			Button("Confirm", role: .destructive, action: {
				if let selectedId {
					realmManager.unblock(selectedId) // equivalent to user.blockedUserId.remove(at)
				}
			})
		})
	}
}

and the list property is:

@Persisted var blockedUserId = RealmSwift.List<ObjectId>()

@dianaafanador3
Copy link
Collaborator

@sonisan And how do you calculate the index to be used in user.blockedUserId.remove(at:)

@sonisan
Copy link

sonisan commented Sep 30, 2022

@dianaafanador3 Please note that it works with a list containing one element only (crashes if > 1).

@MainActor
	func unblock(_ id: ObjectId) {
		if let realm {
			try! realm.write {
				if let currentUser, let userIndex = currentUser.blockedUserId.firstIndex(of: id) {
					currentUser.blockedUserId.remove(at: userIndex)
					print("Unblocked user \(getUserNameBy(id: id))")
				}
			}
		}
	}
	

@dianaafanador3
Copy link
Collaborator

dianaafanador3 commented Sep 30, 2022

Hi @sonisan first-able I think your issue is not related to the original issue from the post, which is related to List move(fromOffsets:, toOffset:).
By the looks of your error, this issue may be caused because you are using EnvironmentObject as a datasource for your View, after you remove your blocked list, SwiftUI is trying to update the view after the list is changed, and it is not finding an item at the index
You may wanna try something similar to the following code snippet, which will also help you simplify your code.

// Navigate to the view passing the list of blocked ids
if let currentUser = realmManager.getCurrentUser() {
    DetailView(blockedUserId: currentUser.blockedUserId)
}
// State Realm object acts like a `live object` updating the view in cases the list changes, and you can append or remove to the list without having to use a write block, and the view will update its state after each change.
struct BlockedUsersView: View {
    @StateRealmObject var blockedUserId: RealmSwift.List<ObjectId>
    @State var showUnblockAlert = false
    @State var selectedId: ObjectId?

    var body: some View {
        VStack {
            List {
                ForEach(blockedUserId, id: \.self) { spec in
                    Text(spec.stringValue)
                        .swipeActions {
                            Button("Unblock") {
                                showUnblockAlert = true
                                selectedId = spec
                            }
                        }
                }
            }
        }
        .alert("Do you want to unblock this user?", isPresented: $showUnblockAlert, actions: {
            Button("Confirm", role: .destructive, action: {
                if let selectedId = selectedId,
                    let index = blockedUserId.firstIndex(of: selectedId)  {
                    $blockedUserId.remove(at: index)
                }
            })
        })
    }
}

@sonisan
Copy link

sonisan commented Sep 30, 2022

Hi @dianaafanador3 oh.. my apologies then!! Thank you for your suggestion, I will give it a shot!

@sync-by-unito sync-by-unito bot closed this as completed Feb 14, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 14, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants