Search code examples
iosswiftrealmrealm-list

Realm - Removing and adding objects from List


This is a follow-up to this question since I may be doing something wrong.

I have a video player in my app. There is a list of videos in a collection view called FavoritesVC. If you tap on one of the cells, a new view controller called PlayerVC appears to play the selected video. Also, you can cycle through all of the videos from the collection view in this new view controller PlayerVC because the entire list is passed.

Everything works fine here.

The problem is when I want to favorite/unfavorite a video. From the question I posted above, I decided to use this from the answer:

Add an isDeleted property to the Video class. When the user unfavorites the video, remove the Video object from FavoriteList.videos, set that property to true, but leave it in Realm. Later on (either when the app quits or the view controller is dismissed), you can then do a general query for all objects where isDeleted is true and delete them then (This solves the headless problem).

I can only get this to work if in FavoritesVC when a cell is tapped, I convert the realm List to a Swift array and use this array to power the PlayerVC. If I don't do this, and I remove an object from the List and the I try to cycle through the List, I get an Index out of range error....

The solution I have to convert the List to a Swift array and pass that to PlayerVC works but seems wrong when using Realm. Best practice is to never do this conversion but in this case I don't know how to simply use the List

This is my code for the list:

/// 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"
    }
}

I convert the List to an array and create the PlayerVC like so:

class FavoritesViewController: UIViewController {

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

        if let playerVC = self.storyboard?.instantiateViewController(withIdentifier: "playerVC") as? PlayerViewController {

            // I convert the `List` to a Swift `array` here.
            playerVC.videos = Array(favoritesManager.favoriteList.videos)
            playerVC.currentVideoIndex = indexPath.row
            self.parent?.present(playerVC, animated: true, completion: nil)
        }
    }
}

This is a snippet of the PlayerVC. This gets created when a cell in FavoritesVC is tapped:

class PlayerViewControllerr: UIViewController {
    // Array passed from `FavoritesVC`. I converted this from the `List`.
    var videos = [Video]()

    var currentVideoIndex: Int!
    var currentVideo: Video {
        return videos[currentVideoIndex]
    }

    // HELP
    // Example of how to cycle through videos. This cause crash if I use the `List` instead of Swift `array`.
    func playNextVideo() {
        if (currentVideoIndex + 1) >= videos.count {
            currentVideoIndex = 0
        } else {
            currentVideoIndex = currentVideoIndex + 1
        }

        videoPlaybackManager.video = currentVideo
    }

    // This is where I add and remove videos from the `List`
    @IBAction func didToggleFavoriteButton(_ sender: UIButton) {
        favoritesManager.handleFavoriting(currentVideo, from: location)     { [weak self] error in
            if let error = error {
            self?.present(UIAlertController.handleErrorWithMessage(error.localizedDescription, error: error), animated: true, completion: nil)
            }
        }
    }
}

Finally, the code to add and remove an object from the List:

class FavoritesManager {
    let favoriteList: FavoriteList

    init(favoriteList: FavoriteList) {
        self.favoriteList = favoriteList
    }

    /// Adds or removes a `Video` from the `FavoriteList`.
    private func handleAddingAndRemovingVideoFromFavoriteList(_ video: Video) {
        if isFavorite(video) {
            removeVideoFromFavoriteList(video)
        } else {
            addVideoToFavoriteList(video)
        }
    }

    /// Adds a `Video` to the `FavoriteList`.
    ///
    /// Modifies `Video` `isDeleted` property to false so no garbage collection is needed.
    private func addVideoToFavoriteList(_ video: Video) {
        let realm = try! Realm()
        try! realm.write {
            favoriteList.videos.append(video)
            video.isDeleted = false
        }
    }

    /// Removes a `Video` from the `FavoriteList`.
    ///
    /// Modifies `Video` `isDeleted` property to true so garbage collection is needed.
    /// Does not delete the `Video` from Realm or delete it's files.
    private func removeVideoFromFavoriteList(_ video: Video) {
        let predicate = NSPredicate(format: "objectId = %@", video.objectId)
        guard let index = favoriteList.videos.index(matching: predicate) else { return }

        let realm = try! Realm()
        try! realm.write {
            favoriteList.videos.remove(objectAtIndex: index)
            video.isDeleted = true
        }
    }
}

Any thoughts?


Solution

  • This is still hard to answer concisely since I'm still not sure what you want to happen when a user 'unfavorites' a video.

    For the best user experience, I would assume that unfavoriting a video will still let it play onscreen, but once the view controller has been dismissed, or the next video starts playing, the video has then been completely removed from the favoriteList object. This means that even when it is removed from videos, the Video object needs to stick around until the user has explicitly dismissed it.

    I'm guessing the reason why converting videos to a Swift array works is because even if the video is deleted from the videos, its reference remains in the Swift array, so the currentVideoIndex value remains in sync.

    It should be possible to directly use the videos object, but since you're mutating it when the user taps the 'unfavorite' button, you can't reliably store the 'currentIndex' as that will obviously change.

    In this case, it's probably better to work out which videos to play ahead of time, and then store a strong reference to them. That way, even if videos mutates, we can still intelligently work out where we are in the playlist and the upcoming and previous videos we will play.

    The player view controller just needs the favorites manager and the current video the user tapped

    class FavoritesViewController: UIViewController {
    
        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    
            if let playerVC = self.storyboard?.instantiateViewController(withIdentifier: "playerVC") as? PlayerViewController {
                playerVC.favoritesManager = favoritesManager
                playerVC.currentVideo = favoritesManager.favoritesList.videos[indexPath.row]
                self.parent?.present(playerVC, animated: true, completion: nil)
            }
        }
    }
    

    And the player will logically work out which videos to queue up, even if they get removed from the videos list later on:

    class PlayerViewControllerr: UIViewController {
        var favoritesManager: FavoriteManager
    
        var currentVideo: Video? {
            didSet { setUpVideos() }
        }
        var nextVideo: Video?
        var previousVideo: Video?
    
        func playNextVideo() {
            currentVideo = nextVideo
            videoPlaybackManager.video = currentVideo
        }
    
        func playPreviousVideo() {
            currentVideo = previousVideo
            videoPlaybackManager.video = currentVideo
        }
    
        func setUpVideos() {
            let videos = favoritesManager.favoritesList.videos
            let index = videos.index(of: currentVideo)
    
            var previousIndex = index - 1
            if previousIndex < 0 { previousIndex = videos.count - 1 }
            previousVideo = videos[previousIndex]
    
            var nextIndex = index + 1
            if nextIndex >= videos.count { nextIndex = 0 }
            nextVideo = videos[nextIndex]
        }
    }
    

    This works on the assumption that the user 'may' end up deleting currentVideo from videos, but the user can't delete nextVideo or previousVideo until they've become currentVideo too. In any case, now that the view controller itself is strongly referencing these video object, separately from the index, this should hopefully make copying videos to a Swift array unnecessary.