0

I'm using Swift 4, iOS 11

I'm clearly trying to do something beyond my knowledge level here, but I'm hoping someone can help.

I am implementing a table view search that fetches results from CoreData. The search became too slow, so I am now trying to run it asynchronously to avoid UI lag.

On my main VC (table view), I have the following logic that runs on textDidChange:

DispatchQueue.global(qos: .background).async {

    DataManager.searchAllTables(searchText: searchText){  (returnResults) -> () in

        DispatchQueue.main.async {

            // LOGIC TO GIVE RETURNED RESULTS TO TABLE

            self.resultsTable.reloadData()
        }
    }
}

Within my DataManager in the function searchAllTables, I have the following logic run on each CoreData relationship:

// Get Disease
if let disease = bug.related_disease?.allObjects {
    if !disease.isEmpty {
        if disease.contains(where: { (($0 as AnyObject).name?.lowercased().contains(searchTerm.lowercased()))! || (($0 as AnyObject).name?.lowercased().contains(self.convertAcronyms(searchTerm: searchTerm)))! }){
            // LOGIC TO DETERMINE POINTS
        }
    }
}

It was working well before trying to make it async, but now it gives me the error

Thread 5: EXC_BAD_ACCESS (code=1, address=0x1a1b40cc771)

for the line of code

if let disease = bug.related_disease?.allObjects {

This error does NOT appear if the user types slowly. This makes me think that the background threads are crossing and something is happening with the object I'm trying to get from CoreData.

I know that EXC_BAD_ACCESS means that I'm "sending a message to an object that has already been released". Can anyone help me understand how to solve this problem?

UPDATE: Added searchAllTables below

static func searchAllTables(searchText:String, completion: @escaping ((_ returnResults:[BugElement])->())) {

    var bugElements:[BugElement] = []
    var bugElementShell:[BugElement] = []

    var returnValueRank:Int = 0
    var categoryRestriction:String = "all" // Search all categories by default
    var numSearchTerms:Int = searchText.components(separatedBy: " ").count
    let searchTerm:String = self.getSearchPhrases(searchTerms: searchText.components(separatedBy: " ")).1
    numSearchTerms = self.getSearchPhrases(searchTerms: searchText.components(separatedBy: " ")).0

    // Set category restriciton if needed
    if self.bugFilter.category_restricted[searchTerm.lowercased()] != nil{
        categoryRestriction = self.bugFilter.category_restricted[searchTerm.lowercased()]!
    }

    let fetchRequest: NSFetchRequest<Bugs> = Bugs.fetchRequest()

    // DOES THIS HELP SPEED?
    if searchTerm.count <= 3{
        let predicate = NSPredicate(format:"name beginswith[c] %@ OR ANY related_disease.name contains[c] %@ OR ANY related_general.name beginswith[c] %@ OR ANY related_gramstain.name beginswith[c] %@ OR ANY related_keypoints.name beginswith[c] %@ OR ANY related_laboratory.name beginswith[c] %@ OR ANY related_morphology.name beginswith[c] %@ OR ANY related_prevention.name beginswith[c] %@ OR ANY related_signs.name beginswith[c] %@ OR ANY related_treatments.name beginswith[c] %@ OR ANY related_type.name beginswith[c] %@", argumentArray: [searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased()])
        fetchRequest.predicate = predicate
    } else{
        let predicate = NSPredicate(format:"name contains[c] %@ OR related_disease.name contains[c] %@ OR related_general.name contains[c] %@ OR related_gramstain.name contains[c] %@ OR related_keypoints.name contains[c] %@ OR related_laboratory.name contains[c] %@ OR related_morphology.name contains[c] %@ OR related_prevention.name contains[c] %@ OR related_signs.name contains[c] %@ OR related_treatments.name contains[c] %@ OR related_type.name contains[c] %@", argumentArray: [searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased(),searchTerm.lowercased()])
        fetchRequest.predicate = predicate
    }

    do {
        let diseases = try DataManager.context.fetch(fetchRequest)

        for bug in diseases{

            // Points system (all matches past array are worth 1)
            var matchValue_name:[Int] = [10, 8, 4]
            var matchValue_disease:[Int] = [8, 4, 3]
            var matchValue_general:[Int] = [5, 3, 2]
            var matchValue_gramstain:[Int] = [5, 3, 2]
            var matchValue_keypoints:[Int] = [5, 3, 2]
            var matchValue_laboratory:[Int] = [2]
            var matchValue_morphology:[Int] = [5, 3, 2]
            var matchValue_prevention:[Int] = [2]
            var matchValue_signs:[Int] = [1]
            var matchValue_treatment:[Int] = [5, 3, 2]
            var matchValue_type:[Int] = [1]

            // Break down by word
            var matchedNameTerms:Int = 0
            var matchedDiseaseTerms:Int = 0
            var matchedGeneralTerms:Int = 0
            var matchedGramStainTerms:Int = 0
            var matchedKeyPointsTerms:Int = 0
            var matchedLaboratoryTerms:Int = 0
            var matchedMorphologyTerms:Int = 0
            var matchedPreventionTerms:Int = 0
            var matchedSignsTerms:Int = 0
            var matchedTreatmentTerms:Int = 0
            var matchedTypeTerms:Int = 0


            // BEGIN: By term

            var matchBasis = Set<String>()

            if categoryRestriction == "all" || categoryRestriction == "name"{
                // FAULT

                if bug.name.lowercased().contains(searchTerm.lowercased()){
                    matchedNameTerms += 1

                    // Matched based on disease name
                    if matchedNameTerms > (matchValue_name.count-1){
                        // Match beyond point assignment
                        returnValueRank += 1
                    } else {
                        // Matched within point assignment, add those points
                        returnValueRank += matchValue_name[(matchedNameTerms-1)]
                    }

                    // Append name if first match
                    if matchedNameTerms == 1{
                        matchBasis.insert("Name")
                    }

                }
            }

            // Get Disease
            if categoryRestriction == "all" || categoryRestriction == "disease"{
                // FAULT

                if let disease = bug.related_disease?.allObjects {
                    if !disease.isEmpty {
                        if disease.contains(where: { (($0 as AnyObject).name?.lowercased().contains(searchTerm.lowercased()))! || (($0 as AnyObject).name?.lowercased().contains(self.convertAcronyms(searchTerm: searchTerm)))! }){
                            matchedDiseaseTerms += 1

                            // Matched based on Disease
                            if matchedDiseaseTerms > (matchValue_disease.count-1){
                                // Match beyond point assignment
                                returnValueRank += 1
                            } else {
                                // Matched within point assignment, add those points
                                returnValueRank += matchValue_disease[(matchedDiseaseTerms-1)]
                            }

                            // Append Disease if first match
                            if matchedDiseaseTerms == 1{
                                matchBasis.insert("Disease")
                            }
                        }
                    }
                }
            }

            // ADDITIONAL MATCHES JUST LIKE ABOVE DISEASE BLOCK

            // END: By term

            if (matchedNameTerms + matchedDiseaseTerms + matchedGeneralTerms + matchedGramStainTerms + matchedKeyPointsTerms + matchedLaboratoryTerms + matchedMorphologyTerms + matchedPreventionTerms + matchedSignsTerms + matchedTreatmentTerms + matchedTypeTerms) > 0{

                // Create Element
                let bugElement = BugElement(rank: returnValueRank, name: bug.name, match: matchBasis) // Initialize struct
                bugElementShell.append(bugElement)


            }

            returnValueRank = 0

        }

    } catch {
        if DataManager.debug{ print("Could not get Bugs!") }
    }


    // Handle stored search
    if numSearchTerms == 0{
        // No stored search
        //print("None")
        self.storedBugElements.append(bugElementShell)
    } else if numSearchTerms > self.storedBugElements.count{
        // New search term
        //print("New")
        self.storedBugElements.append(bugElementShell)
    } else if numSearchTerms < self.storedBugElements.count{
        // Deleted search term
        //print("Delete")
        self.storedBugElements.removeLast()
    } else if numSearchTerms == self.storedBugElements.count{
        // Still typing search term
        //print("Still...")
        self.storedBugElements.removeLast()
        self.storedBugElements.append(bugElementShell)
    }

    // Handle stored search
    if self.storedBugElements.count > 0 {
        let namedElements = self.storedBugElements.joined().map { ($0.name, $0) }
        // Now combine them as you describe. Add the ranks, and merge the items
        let uniqueElements =
            Dictionary<String, BugElement>(namedElements,
                                           uniquingKeysWith: { (lhs, rhs) -> BugElement in
                                                let sum = lhs.rank + rhs.rank
                                                return BugElement(rank: sum,
                                                           name: lhs.name,
                                                           match: lhs.match.union(rhs.match))
            })

        // The result is the values of the dictionary
        let result = uniqueElements.values
        bugElements = result.sorted { $0.rank > $1.rank }

    } else{
        bugElements = bugElementShell.sorted { $0.rank > $1.rank }
    }



    completion(bugElements)

}
Coltuxumab
  • 615
  • 5
  • 16

1 Answers1

0

ManagedObjectContexts and managedObjects are not thread safe. Neither for reading or for writing. I don't need to to see what you are doing in searchAllTables to know that you are doing it wrong. There are two types of context - main queue context (that must always be accessed from the main thread) and private queue context that must only be assessed through performBlock. There is no context that is correct to run on a global background thread.

It appears to me that your problem is that you are loading all the objects into memory and testing each one instead of using a NSPredicate on a fetch request. I would recommend running the search on the main thread and fixing it so it doesn't take so long.

Jon Rose
  • 8,373
  • 1
  • 30
  • 36
  • I figured this was one way I was messing this up, thanks. I am using a predicate, but I have a complex search algorithm that assigns points to a “bug” based on which relationship is matched. At the end, it tallies the points to determine which bug should be ranked first. This is why it is slow and why I don’t THINK I can use just a predicate. Though, I may be wrong there... – Coltuxumab Jan 03 '18 at 10:02
  • If you don't have a field that you can sort by then create on and update it ever time the entity updates. – Jon Rose Jan 03 '18 at 11:28
  • Thanks Jon. Currently I am creating the rank:Int on the fly and sorting by that. I may be able to rely more heavily on the predicate and less on the rank, which may speed up the app. Just to be clear, are you suggesting I create a field to sort by within CoreData? If so, then I’m not clear on how that could work given that a user’s search would be different every time. When someone types “gr”, it matches many results, so just sorting alphabetically (for example) wouldn’t be ideal because some “gr”s are more important than others. – Coltuxumab Jan 03 '18 at 11:36