Array Function of MPMediaItem Very Slow - swift

I'm trying to edit the queue of my music player using the applicationQueuePlayer and the perform method (details here). However, whenever I apply any array function (map, filter etc.), it takes many seconds to complete, leading to (I think) data races and crashes when the user, for example, removes two tracks immediately after each other.
var musicPlayerController = MPMusicPlayerController.applicationQueuePlayer
self.musicPlayerController.perform { (currentQueue) in
let items = currentQueue.items
let itemsToRemove = items.filter { $0.artist == "Some artist" } // this takes multiple seconds
if let item = itemsToRemove.first {
currentQueue.remove(item)
}
} completionHandler: { (newQueue, error) in
if let e = error {
print(e)
} else {
tracks = items.map { Track(item: $0) } // this takes multiple seconds
}
}
The issue is arising as I'm going through an MPMediaItem array. I don't think this is an issue with the MPMediaItem class though, as I'm able to complete a map of [MPMediaItem] in other places in the app e.g. when getting items from a playlist (a similar sized array to the queue items).
The issue happens solely when the MPMediaItems are taken from the MPMusicPlayerControllerMutableQueue and MPMusicPlayerControllerQueue
Is this just a bug with MusicKit API?

Related

OperationQueue - crash when editing the same array from multiple operations

I have an OperationQueue with multiple custom Operations which all append to the same array on completion (each operation downloads a file from user's iCloud and when it's done it appends the file to the array)
This, sometimes, causes the app to crash, because several operations try to edit the array at the same time.
How can I prevent this and only edit the array 1 operation at a time but running all operations simultaneously?
I must use OperationQueue because I need the operations to be cancelable.
func convertAssetsToMedias(assets: [PHAsset],
completion: #escaping (_ medias: [Media]) ->()) {
operationQueue = OperationQueue()
var medias: [Media] = []
operationQueue?.progress.totalUnitCount = Int64(assets.count)
for asset in assets {
// For each asset we start a new operation
let convertionOperation = ConvertPHAssetToMediaOperation(asset)
convertionOperation.qualityOfService = .userInteractive
convertionOperation.completionBlock = { [unowned convertionOperation] in
let media = convertionOperation.media
medias.append(media) // CRASH HERE (sometimes)
self.operationQueue?.progress.completedUnitCount += 1
if let progress = self.operationQueue?.progress.fractionCompleted {
self.delegate?.onICloudProgressUpdate(progress: progress)
}
convertionOperation.completionBlock = nil
}
operationQueue?.addOperation(convertionOperation)
}
operationQueue?.addBarrierBlock {
completion(medias)
}
}
Edit 1:
The Media file itself is nothing big, just a bunch of metadata and a url to an actual file at documents directory. There are usually about 24 medias max at 1 run. The memory is barely increasing during those operations. The crash never occured due to a lack of memory.
The operation ConvertPHAssetToMediaOperation is a subclass of AsyncOperation where isAsynchronous propery is set to true.
That's how I construct the Media object in the end of each operation:
self.media = Media(type: mediaType, url: resultURL, creationDate: date)
self.finish()
Edit 2: The crash is always the same:

How to use Combine to assign the number of elements returned from a Core Data fetch request?

I want my app to periodically fetch new records and stores them in Core Data. I have a label on my UI that should display the number of elements for a particular record and I want that number to be updated as more records are added into the database. As an exercise, I want to use Combine to accomplish it.
I'm able to display the number of elements in the database when the app launches, but the number doesn't get updated when new data enters into the database (I verified that new data was being added by implementing a button that would manual refresh the UI).
Here's the code that displays the correct number of elements on launch but doesn't update when new records are added:
let replayRecordFetchRequest: NSFetchRequest<ReplayRecord> = ReplayRecord.fetchRequest()
_ = try? persistentContainer.viewContext.fetch(replayRecordFetchRequest).publisher.count().map { String(format: Constants.Strings.playsText, $0) }.assign(to: \.text, on: self.playsLabel)
Here's a code snippet from the WWDC 2019 Session 230 talk that I adapted but this doesn't work at all (the subscriber is never fired):
let replayRecordFetchRequest: NSFetchRequest<ReplayRecord> = ReplayRecord.fetchRequest()
if let replayRecords = try? replayRecordFetchRequest.execute() {
_ = replayRecords.publisher.count().map { String(format: Constants.Strings.playsText, $0) }.assign(to: \.text, on: self.playsLabel)
}
So, I didn't know this until now, but not all publishers are infinitely alive.
And the problem was that the NSFetchRequest.publisher is not a long-living publisher. It simply provides a way to iterate through the sequence of elements in the fetch request. As a result, the subscriber will cancel after the elements are iterated. In my case, I was counting the elements published until cancellation then assigning that value onto the UI.
Instead, I should be subscribing to changes to the managed object context and assigning that pipeline to my UI. Here's some example code:
extension NotificationCenter.Publisher {
func context<T>(fetchRequest: NSFetchRequest<T>) -> Publishers.CompactMap<NotificationCenter.Publisher, [T]> {
return compactMap { notification -> [T]? in
let context = notification.object as! NSManagedObjectContext
var results: [T]?
context.performAndWait {
results = try? context.fetch(fetchRequest)
}
return results
}
}
}
let playFetchRequest: NSFetchRequest<ReplayRecord> = ReplayRecord.fetchRequest()
let replayVideoFetchRequest: NSFetchRequest<ReplayVideo> = ReplayVideo.fetchRequest()
let playsPublisher = contextDidSavePublisher.context(fetchRequest: playFetchRequest).map(\.count)
let replayVideoPublisher = contextDidSavePublisher.context(fetchRequest: replayVideoFetchRequest).map(\.count)
playsSubscription = playsPublisher.zip(replayVideoPublisher).map {
String(format: Constants.Strings.playsText, $0, $1)
}.receive(on: RunLoop.main).assign(to: \.text, on: self.playsLabel)

cancel filter and sorting of big data array

I'm building vocabulary app using realm. I have several objects of Vocabulary, which contains list of words. One vocabulary contains 45000 words
UI is build such way, that user can search by "BEGINSWITH", "CONTAINS" or "ENDSWITH" through word's title, if corresponding tab is selected.
As, there are several vocabularies, there are some words, that appear in several vocabularies, and I need to remove "duplicates" from UI.
When I do this filtering duplicates on resulted objects + sorting them alphabetically the UI of app freezes, till process completes.
My question is:
1) How can I cancel previous filter and realm filtering request, if tab changed (for example from Contains to Ends"?
2) How can I do all these filter/sorting requests in background, so UI will not freeze?
My code:
let vocabularyPredicate = NSPredicate(format: "enabled == 1 AND lang_from CONTAINS[c] %#", self.language.value)
self.vocabularies = Array(realm.objects(Vocabulary.self).filter(vocabularyPredicate).sorted(byKeyPath: "display_order"))
let result = List<Word>()
for object in self.vocabularies {
let predicate = NSPredicate(format: "title \(selectedFilter.value)[c] %#", self.query.value.lowercased())
result.append(objectsIn: object.words.filter(predicate))
}
self.words = Array(result).unique{$0.title}.sorted {
(s1, s2) -> Bool in return s1.title.localizedStandardCompare(s2.title) == .orderedAscending
}
selectedFilter.value is selected tab value: "BEGINSWITH", "CONTAINS" or "ENDSWITH"
self.query.value.lowercased() - search query.
unique{$0.title} is extension method for array
extension Array {
func unique<T:Hashable>(map: ((Element) -> (T))) -> [Element] {
var set = Set<T>() //the unique list kept in a Set for fast retrieval
var arrayOrdered = [Element]() //keeping the unique list of elements but ordered
for value in self {
if !set.contains(map(value)) {
set.insert(map(value))
arrayOrdered.append(value)
}
}
return arrayOrdered
}
}
Actually, realm search is pretty fast, but because of looping through vocabularies and filtering duplicates + sorting alphabetically operations through array of objects - request is freezing for 1-2 seconds.
UPDATE, based on EpicPandaForce and Manuel advices:
I have lurked one more time, and it appeared, that .distinct(by: [keypath]) is already presented in Results in new version of RealmSwift.
I have changed filter/sorting request to
realm.objects(Word.self).filter(vocabularyPredicate).distinct(by: ["title"]).sorted(byKeyPath: "title", ascending: true)
works better know, but I want to ensure, UI will not freeze anyway, by passing objects bettween background thread and UI thread. I have updated adviced construction to:
DispatchQueue.global(qos: .background).async {
let realm = try! Realm()
let cachedWords = CashedWords()
let predicate = NSPredicate(format: "enabled == 1")
let results = realm.objects(Word.self).filter(predicate).distinct(by: ["title"]).sorted(byKeyPath: "title", ascending: true)
cachedWords.words.append(objectsIn: results)
try! realm.write {
realm.add(cachedWords)
}
let wordsRef = ThreadSafeReference(to: cachedWords)
DispatchQueue.main.async {
let realm = try! Realm()
guard let wordsResult = realm.resolve(wordsRef) else {
return
}
self.words = Array(wordsResult.words)
if ((self.view.window) != nil) {
self.tableView.reloadData()
}
}
print("data reload finalized")
}
1) How can I cancel previous filter and realm filtering request, if tab changed (for example from Contains to Ends"?
You could create an NSOperation to perform the task and check if it's been cancelled between each of the steps (fetch, check isCancelled, filter, check isCancelled, sort). You won't get to cancel it immediately, but it could improve your performance. It also depends on which of those three steps (fetch, filter, sort) is taking longer...
2) How can I do all these filter/sorting requests in background, so UI will not freeze?
You could run that operation inside a new NSOperationQueue.
Or just use GCD, dispatch a block to a background queue, create a Realm instance in the block and run your code there, then dispatch the results back to the main queue to update the UI.
Something like this:
DispatchQueue.global(qos: .userInitiated).async {
guard let realm = try? Realm() else {
return // maybe pass an empty array back to the main queue?
}
// ...
// your code here
// ...
let words = Array(result).unique{$0.title}.sorted {
(s1, s2) -> Bool in return s1.title.localizedStandardCompare(s2.title) == .orderedAscending
}
// Can't pass Realm objects directly across threads
let wordReferences = words.map { ThreadSafeReference(to: $0) }
DispatchQueue.main.async {
// Resolve references on main thread
let realm = try! Realm()
let mainThreadWords = wordReferences.flatMap { realm.resolve($0) }
// Do something with words
self.words = mainThreadWords
}
}
Additionally, you should try to optimize your query:
let predicate = NSPredicate(format: "vocabulary.enabled == 1 AND vocabulary.lang_from CONTAINS[c] %# AND title \(selectedFilter.value)[c] %#", self.language.value, self.query.value.lowercased())
let words = realm.objects(Word.self).filter(predicate).sorted(byKeyPath: "title")
let wordsReference = ThreadSafeReference(words)
// resolve this wordsReference in the main thread

Difference between generate and append when querying and adding data to array?

var objectarray = [PFObject]()
func populateTable() {
query.findObjectsInBackgroundWithBlock { (objects, error) in
self.objectarray.removeAll(keepCapacity: true)
self.searchTableView.reloadData()
if error == nil {
Above is the query I am doing and the below 2 codes are what I can use to use the query to populate a array.
if let objects = objects as [PFObject]! {
self.objectarray = Array(objects.generate())
}
Is there any difference with running this code above to populate my array or running the code below?
for object in objects! {
self.objectarray.append(object)
}
Doing either works to load onto my tableView. Also another question regarding Parse. After doing the above, the user doesn't download PFFiles from the background until I run
getDataInBackgroundWithBlock
right? I want to know if it'd be beneficial to save smaller versions of images onto the server.

The method does not enter for loop Parse Swift

I use parse to query current user's friend list and the friend request user and when user press each cell of the friend request, The app will add that friend back and delete the selected friend request so I query friend list and friend request and use "addedArray" as friend requests and "duplicate" as array of current user's friend list and use for loop to find the duplicate of friend list and friend request and delete that friend from addedArray so the current user will se the latest friend requests
Here's my code in swift
func queryAdded(){
let query = PFQuery(className: "Request")
let user = PFUser.currentUser()?.relationForKey("Friends")
let query2 = user?.query()
query.whereKey("To", equalTo: PFUser.currentUser()!)
query.findObjectsInBackgroundWithBlock {
(objects, error) -> Void in
if error == nil{
for object in objects! {
print("query")
let username = object.valueForKey("FromUsername") as! String
self.userCellAdded = username
self.addedArray.append(username)
print(username)
print(self.addedArray.count)
}
print("READY")
print(self.addedArray.count)
self.tableView.reloadData()
}
else{
/* dispatch_async(dispatch_get_main_queue()){
//reload the table view
query.cachePolicy = PFCachePolicy.NetworkElseCache
}*/
print("errorrrr")
}
}
query2!.findObjectsInBackgroundWithBlock{(objects,error) -> Void in
if error == nil {
for object in (objects)!{
if let username = object["username"] as? String {
self.duplicate.append(username)
print("duplicate")
print(username)
print("size")
print(self.duplicate.count)
}
}
}
}
for self.iIndex = 0 ; self.iIndex < self.addedArray.count ; ++self.iIndex {
for self.jIndex = 0 ; self.jIndex < self.duplicate.count ; ++self.jIndex {
print("in for loop")
if self.addedArray[self.iIndex] == self.duplicate[self.jIndex] {
self.addedArray.removeAtIndex(self.iIndex)
self.tableView.reloadData()
print("find")
}
}
}
}
The problem is The method queryAdded() does not run for loop for me and I don't understand why
The duplicate array and the addedArray have value and size but still it didn't go inside the for loop
Your problem is that your for loop is depending on the results of two asynchronous operations. What happens is that your app starts these two background queries and then immediately starts the for loop. Since there is no data yet from the queries, the for loop has no data to work on.
You can either solve this by creating a "pyramid hell" by nesting your operations (bad), or you can use a framework to achieve the same as Promises would provide for JavaScript (good).
Since you're using Parse, you have such a framework already; namely the Bolts Framework. You could then perform these operations sequentially using tasks (BFTask).
Example from the Bolts readme:
var query = PFQuery(className:"Student")
query.orderByDescending("gpa")
findAsync(query).continueWithSuccessBlock {
(task: BFTask!) -> BFTask in
let students = task.result() as NSArray
var valedictorian = students.objectAtIndex(0) as PFObject
valedictorian["valedictorian"] = true
return self.saveAsync(valedictorian)
}.continueWithSuccessBlock {
(task: BFTask!) -> BFTask in
var valedictorian = task.result() as PFObject
return self.findAsync(query)
}.continueWithSuccessBlock {
(task: BFTask!) -> BFTask in
let students = task.result() as NSArray
var salutatorian = students.objectAtIndex(1) as PFObject
salutatorian["salutatorian"] = true
return self.saveAsync(salutatorian)
}.continueWithSuccessBlock {
(task: BFTask!) -> AnyObject! in
// Everything is done!
return nil
}
You could then first prepare both your queries and then start the chain of tasks:
query1.findObjectsInBackground().continueWithSuccessBlock {
(task: BFTask!) -> BFTask in
var objects = task.result() as NSArray
for object in objects {
//collect your usernames
}
return query2.findObjectsInBackground()
}.continueWithSuccessBlock {
(task: BFTask!) -> AnyObject! in
var objects = task.result() as NSArray
for object in objects {
// collect your usernames from relation
}
// Call a function containing the for loop that is currently not running
return nil
}
The for loop is run
duplicate array and the addedArray have value and size - No they don't
findObjectsInBackgroundWithBlock runs the query in ... the background.
Therefore your program does the following:
start the first query
start the second query
run the for loop
the queries finish at some arbitrary point in time.
In particular when the program reaches point 3 the arrays do not contain anything, they are empty arrays, therefore the for-loop executes perfectly fine as it is supposed to be: it does nothing since there is nothing to loop over.
Solution:
Move the for loop into a function that you call after the first query and the second query finish.