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?
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.