Skip to content

Instantly share code, notes, and snippets.

@verebes1
Last active March 6, 2024 19:47
Show Gist options
  • Save verebes1/02950e46fff91456f2ad359b3f3ec3d9 to your computer and use it in GitHub Desktop.
Save verebes1/02950e46fff91456f2ad359b3f3ec3d9 to your computer and use it in GitHub Desktop.
Realm Cascade Deletion in Swift
import RealmSwift
import Realm
protocol CascadeDeleting {
func delete<S: Sequence>(_ objects: S, cascading: Bool) where S.Iterator.Element: Object
func delete<Entity: Object>(_ entity: Entity, cascading: Bool)
}
extension Realm: CascadeDeleting {
func delete<S: Sequence>(_ objects: S, cascading: Bool) where S.Iterator.Element: Object {
for obj in objects {
delete(obj, cascading: cascading)
}
}
func delete<Entity: Object>(_ entity: Entity, cascading: Bool) {
if cascading {
cascadeDelete(entity)
} else {
delete(entity)
}
}
}
private extension Realm {
private func cascadeDelete(_ entity: RLMObjectBase) {
guard let entity = entity as? Object else { return }
var toBeDeleted = Set<RLMObjectBase>()
toBeDeleted.insert(entity)
while !toBeDeleted.isEmpty {
guard let element = toBeDeleted.removeFirst() as? Object,
!element.isInvalidated else { continue }
resolve(element: element, toBeDeleted: &toBeDeleted)
}
}
private func resolve(element: Object, toBeDeleted: inout Set<RLMObjectBase>) {
element.objectSchema.properties.forEach {
guard let value = element.value(forKey: $0.name) else { return }
if let entity = value as? RLMObjectBase {
toBeDeleted.insert(entity)
} else if let list = value as? RLMSwiftCollectionBase {
for index in 0..<list._rlmCollection.count {
if let entity = list._rlmCollection.object(at: index) as? RLMObjectBase {
toBeDeleted.insert(entity)
}
}
}
}
delete(element)
}
}
@X901
Copy link

X901 commented Jan 8, 2020

I did Clean my project and I did also Clean Build Folder
nothing change same error
I did drag it to my project which have Realm already

see the image
https://a.cl.ly/qGuDrYEn

Could you try to update RealmSwift to last version ?
because I'm on last version

@X901
Copy link

X901 commented Jan 8, 2020

I just remove :class from protocol
and it working fine

from

protocol CascadeDeleting: class {
    func delete<S: Sequence>(_ objects: S, cascading: Bool) where S.Iterator.Element: Object
    func delete<Entity: Object>(_ entity: Entity, cascading: Bool)
}

to

protocol CascadeDeleting {
    func delete<S: Sequence>(_ objects: S, cascading: Bool) where S.Iterator.Element: Object
    func delete<Entity: Object>(_ entity: Entity, cascading: Bool)
}

but still get error
RLMArray has been invalidated or the containing object has been deleted
this error not related to the delete itself

I did notice the Realm file and see them both deleted successfuly

Thank you it's helpfull and makes our life easier !

@verebes1
Copy link
Author

verebes1 commented Jan 8, 2020 via email

@X901
Copy link

X901 commented Jan 8, 2020

If I change it to AnyObject
the error message return back

'CascadeDeleting' requires that 'Realm' be a class type
Non-class type 'Realm' cannot conform to class protocol 'CascadeDeleting'

The reason that I remove class
it I notce the Realm is of type struct

and I remembers on version v4.0.0
on release note they said :

The following Swift types have changed from final class to struct:
AnyRealmCollection
LinkingObjects
ObjectiveCSupport
Realm
Results
SyncSubscription
ThreadSafeReference
There is no intended change in semantics from this, but certain edge cases
may behave differently.

@verebes1
Copy link
Author

verebes1 commented Jan 8, 2020

That makes sense as the class modifier and later AnyObject modifier restricts the protocol to class only types since you say that Realm changed it's libraries to Structs the modifier is causing an error I will update it on my gist as well. Thanks for the input.

@stefanrenne
Copy link

stefanrenne commented Aug 25, 2020

I refactored this into a more functional solution, hope it helps someone:

extension Realm {

    func cascadeDelete(_ entity: [Object]) {

        var toBeDeleted = Set(entity)

        while let element = toBeDeleted.popFirst() {
            guard !element.isInvalidated, element.realm != nil else { continue }
            resolve(element: element, toBeDeleted: &toBeDeleted)
            delete(element)
        }
    }

    private func resolve(element: Object, toBeDeleted: inout Set<Object>) {

        func foundChild(entity: Object) {
            toBeDeleted.insert(entity)
        }

        func foundChild(list: RealmSwift.ListBase) {
            (0..<list._rlmArray.count)
                .map(list._rlmArray.object)
                .compactMap { $0 as? Object }
                .forEach(foundChild)
        }

        element
            .objectSchema
            .properties
            .map(\.name)
            .compactMap(element.value(forKey:))
            .forEach { value in
                if let entity = value as? Object {
                    foundChild(entity: entity)
                } else if let list = value as? RealmSwift.ListBase {
                    foundChild(list: list)
                }
            }
    }
}

@verebes1
Copy link
Author

@stefanrenne Thanks for sharing your solution.

@georgescumihai
Copy link

georgescumihai commented Aug 26, 2020

@stefanrenne and @verebes, because you are both using a Set for toBeDeleted, you are missing the objects which have the same hash.
I modified your solution to work with Objective-C Realm and changed it to use an Array instead of Set.
https://gist.github.com/georgescumihai/5c7e93322c59e6808c671bc65beaa221

Thanks for the source.

Example

/// Birthday.
@objcMembers public class Birthday: Object {
    /// The brith day.
    public static let day: Int = 1
    /// The brith day.
    public static let month: Int = 1
    /// The brith day.
    public static let year: Int = 2010
}

This is true for objects without a primary key.

@jhoanarango
Copy link

jhoanarango commented Aug 2, 2021

Hello, does anyone has a solution for this ? Just updated Realm to the latest version and now I get this message "No type named 'ListBase' in module 'RealmSwift'".

    private func resolve(element: Object, toBeDeleted: inout Set<RLMObjectBase>) {
        element.objectSchema.properties.forEach {
            guard let value = element.value(forKey: $0.name) else { return }
            if let entity = value as? RLMObjectBase {
                toBeDeleted.insert(entity)
            } else if let list = value as? RealmSwift.ListBase { /// <--- Error appears here !
                for index in 0 ..< list._rlmArray.count {
                    if let realmObject = list._rlmArray.object(at: index) as? RLMObjectBase {
                        toBeDeleted.insert(realmObject)
                    }
                }
            }
        }
        delete(element)
    }

Thank you !

@niralishaha25
Copy link

faced same problem. Looking for solution for "No type named 'ListBase' in module 'RealmSwift'".

@georgescumihai
Copy link

Probably won't fix all the situations, but you can solve some of the issue by using Embedded Objects.

Realm Uses Cascading Deletes for Embedded Objects
When you delete a Realm object, Realm automatically deletes any embedded objects referenced by that object. Any objects that your application must persist after the deletion of their parent object should use relationships instead.

@verebes1
Copy link
Author

I haven't used Realm for a while now. but I can point to the right place in Realm Docs
Two things come to my mind trying RealmSwift.List instead RealmSwift.ListBase
And cleaning your derived data folder from Xcode.
Again these are only some ideas to go forward. I haven't tried them.

@aleyooop
Copy link

aleyooop commented Jan 25, 2022

@jhoanarango @niralishaha25 as mentioned before, you can use Embedded Objects for automated cascade deleting. But if you still want to use the snippet on realm v10+, that should work as follows:

private func resolve(element: Object, toBeDeleted: inout Set<RLMObjectBase>) {
    element.objectSchema.properties.forEach {
        guard let value = element.value(forKey: $0.name) else { return }
        if let entity = value as? RLMObjectBase {
            toBeDeleted.insert(entity)
        } else if let list = value as? RLMSwiftCollectionBase {
            for index in 0..<list._rlmCollection.count {
                if let entity = list._rlmCollection.object(at: index) as? RLMObjectBase {
                    toBeDeleted.insert(entity)
                }
            }
        }
    }
    delete(element)
}

@verebes1
Copy link
Author

@aleyooop Thanks for your input. I've updated the Gist with your suggestion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment