Realm - Cannot use object after deletion
I have a video player in my application. There is a list of videos as a collection. If you click on one of the cells, a new view controller appears to play the selected video. Also, you can view all videos from the collection in this new view controller because the entire list has been passed.
The problem is this: When the user is in PlayerVC
, they may not greet a Video
. If they do, I remove the object Video
from the Realm. However, this causes:
Terminating app due to uncaught exception 'RLMException', reason: 'Object has been deleted or invalidated.'
Basically, if the user is watching the video in PlayerVC
, and he doesn't distort the video, I want them to still be able to watch the video for a while. But when they leave PlayerVC
, the collection view in FavoritesVC
should be refreshed and not show anymore Video
.
When I delete an object Video
, I use the Realm method delete
.
This is my code for storing a list of objects Video
:
/// Model class that manages the ordering of `Video` objects.
final class FavoriteList: Object {
// MARK: - Properties
/// `objectId` is set to a static value so that only
/// one `FavoriteList` object could be saved into Realm.
dynamic var objectId = 0
let videos = List<Video>()
// MARK: - Realm Meta Information
override class func primaryKey() -> String? {
return "objectId"
}
}
This is my class Video
which has the property isFavorite
:
final class Video: Object {
// MARK: - Properties
dynamic var title = ""
dynamic var descriptionText = ""
dynamic var date = ""
dynamic var videoId = ""
dynamic var category = ""
dynamic var duration = 0
dynamic var fullURL = ""
dynamic var creatorSite = ""
dynamic var creatorName = ""
dynamic var creatorURL = ""
// MARK: FileManager Properties (Files are stored on disk for `Video` object).
/*
These are file names (e.g., myFile.mp4, myFile.jpg)
*/
dynamic var previewLocalFileName: String?
dynamic var stitchedImageLocalFileName: String?
dynamic var placeholderLocalFileName: String?
/*
These are partial paths (e.g., bundleID/Feed/myFile.mp4, bundleID/Favorites/myFile.mp4)
They are used to build the full path/URL at runtime.
*/
dynamic var previewLocalFilePath: String?
dynamic var stitchedImageLocalFilePath: String?
dynamic var placeholderLocalFilePath: String?
// Other code...
}
This is my code for displaying objects Video
in the collection view (Note: I am using RealmCollectionChange
to update the collection view to delete and insert cells):
/// This view controller has a `collectioView` to show the favorites.
class FavoriteCollectionViewController: UIViewController {
// MARK: Properties
let favoriteList: FavoriteList = {
let realm = try! Realm()
return realm.objects(FavoriteList.self).first!
}()
// Realm notification token to update collection view.
var notificationToken: NotificationToken?
// MARK: Collection View
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return favoriteList.videos.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FavoritesCollectionViewCell.reuseIdentifier, for: indexPath) as! FavoritesCollectionViewCell
cell.video = favoriteList.videos[indexPath.item]
return cell
}
// I pass this lst forward to the PlayerVC
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let playerVC = self.storyboard?.instantiateViewController(withIdentifier: "PlayerViewController") as? PlayerViewController {
// I pass the videos here.
playerVC.videos = favoriteList.videos
self.parent?.present(playerVC, animated: true, completion: nil)
}
}
// MARK: Realm Notifications
func updateUI(with changes: RealmCollectionChange<List<Video>>) {
// This is code to update the collection view.
}
}
Finally, this is the code to allow the user to play and cycle through all objects Video
:
/// This view controller uses `AVFoundation` to play the videos from `FavoriteCollectionViewController`.
class PlayerViewControllerr: UIViewController {
/// This `videos` is passed from `FavoriteCollectionViewController`
var videos = List<Video>()
// HELP: The app crashes here if I unfavorite a `Video`.
@IBAction func didToggleStarButton(_ sender: UIButton) {
let realm = try! Realm()
try! realm.write {
let videoToDelete = videos[currentIndexInVideosList] /// Get the video that is currently playing
realm.delete(videoToDelete)
}
}
}
Ultimately I want the object to Video
be unusable to be completely removed from the Realm . Just not sure how to do it in this case.
Any thoughts?
Update 1
Possibility to solve this problem:
- Make an unmanaged copy of the copy
Video
and use it to enable the view controller UI.
As I see it, maybe this works:
-
PlayerVC
will get twoList
, the original one saved in Realm, and a copy of thatList
to enable the UI. Call listsfavoriteList
andcopyList
. -
So, internally,
didToggleStarButton
we would do something like this:
code:
/// This view controller uses `AVFoundation` to play the videos from `FavoriteCollectionViewController`.
class PlayerViewControllerr: UIViewController {
/// A button to allow the user to favorite and unfavorite a `Video`
@IBOutlet weak var starButton: UIButton!
/// This is passed from `FavoriteCollectionViewController`
var favoriteList: FavoriteList!
/// A copy of the `FavoriteList` videos to power the UI.
var copiedList: List<Video>!
var currentIndexOfVideoInCopiedList: Int!
override func viewDidLoad() {
super viewDidLoad()
// Make a copy of the favoriteList to power the UI.
var copiedVideos = [Video]()
for video in favoriteList.videos {
let unmanagedVideo = Video(value: video)
copiedVideos.append(unmanagedVideo)
}
self.copiedList.append(copiedVideos)
}
// HELP: The app crashes here if I unfavorite a `Video`.
@IBAction func didToggleStarButton(_ sender: UIButton) {
// Do the unfavoriting and favoriting here.
// An example of unfavoriting:
let realm = try! Realm()
try! realm.write {
let videoToDeleteFromFavoriteList = favoriteList.videos[currentIndexOfVideoInCopiedList] /// Get the video that is currently playing
realm.delete(videoToDeleteFromOriginalList)
}
// Update star button to a new image depending on if the `Video` is favorited or not.
starButton.isSelected = //... update based on if the `Video` in the `FavoriteList` or not.
}
}
Any thoughts?
source to share
This is definitely tricky for a number of architectural reasons.
You are correct that you can simply remove the object from FavoriteList.videos
and then properly remove it from the Realm when you are about to dismiss the controller, but you are correct if the user presses the home button or you get a headless video object before the application crashes. You will need to make sure you can track this.
There are several things you might want to consider.
- Add property
isDeleted
to classVideo
. When the user has disabled the video, remove the objectVideo
fromFavoriteList.videos
, set this property to a valuetrue
, but leave it in Realm. Later (either when the application is closed or the view manager is rejected), you can then run a generic query for all objects where itisDeleted
istrue
and then delete them (this solves the headless problem). - Since your architecture requires a view controller based on a model that can be removed from under it, depending on how much information you are using from that object
Video
it may be safer to make an unmanaged copyVideo
copy and use that to enable the view controller UI ... You can create a new copy of an existing Realm object by runninglet unmanagedVideo = Video(value: video)
.
source to share
Here is the solution to the problem. Check if it works.
class PlayerViewControllerr: UIViewController {
var arrayForIndex = [Int]()
var videos = List<Video>()
@IBAction func didToggleStarButton(_ sender: UIButton) {
self.arrayForIndex.append(currentIndexInVideosList)
}
@overide func viewWillDisappear(_ animated : Bool){
super.viewWillDisappear(animated)
for i in arrayForIndex{
let realm = try! Realm()
try! realm.write {
let videoToDelete = videos[i] /// Get the video that is currently playing
realm.delete(videoToDelete)
}
}
}
source to share