iosswiftuilocalizedcollation

Getting error "unrecognized selector sent to instance" while using UILocalizedIndexedCollation to create tableView section Indexes in Swift 4.2


I'm trying to build an app modeled off of iPhone's Contacts app. In that process that I'm trying to put section indexes down the right side of my tableView of contacts. Apple's doc Table View Programming Guide for iOS recommends using UILocalizedIndexedCollation class. The problem seems to be with the collationStringSelector. In Swift 4 selectors need to be exposed to @objc, I can't find anyway to do that and everything I have tried comes back with this error message:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSTaggedPointerString localizedTitle]: unrecognized selector sent to instance

I tried to put the selector object into its own class so that I could expose it to @objc, but that did not work. Here is the code I tried:

class SelectorObj: NSObject {

    @objc var selector: Selector

    init(selector: Selector) {
        self.selector = selector
    }
}

Back to the baseline code. Here is the sample code I'm trying to make work.

class ObjectTableViewController: UITableViewController {

    let collation = UILocalizedIndexedCollation.current()
    var sections: [[AnyObject]] = []
    var objects: [AnyObject] = [] {
        didSet {
            let selector: Selector = #selector(getter: UIApplicationShortcutItem.localizedTitle)
            sections = Array(repeating: [], count: collation.sectionTitles.count)

            let sortedObjects = collation.sortedArray(from: objects, collationStringSelector: selector)
            for object in sortedObjects {
                let sectionNumber = collation.section(for: object, collationStringSelector: selector)
                sections[sectionNumber].append(object as AnyObject)
            }

            self.tableView.reloadData()
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        objects = (["One", "Two", "Three"] as [AnyObject])
    }

    // MARK: UITableViewDelegate

    override func numberOfSections(in tableView: UITableView) -> Int {
        return sections.count
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return sections[section].count

    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        let cellData = sections[indexPath.section][indexPath.row]
        cell.textLabel!.text = (cellData as! String)
        return cell
    }

    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return collation.sectionTitles[section]
    }

    override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
        return collation.sectionIndexTitles
    }

    override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
        return collation.section(forSectionIndexTitle: index)
    }
}

The expected results is to get the section indexes (A...Z, #) down the right hand side of the tableView list of contacts, like in iPhone's Contacts App.


Solution

  • The selector you pass to sortedArray(from:collationStringSelector:) needs to be a selector found on the type of objects being sorted. You don't have a localizedTitle property on the objects in your array.

    Normally you would have an array of classes or structs that you want to sort and you would pass the selector of some property of the class or struct.

    But your array is an array of String. So you need a property of String that returns the string to be used for sorting.

    And since this use of selectors is based on old Objective-C frameworks, the property needs to be exposed to Objective-C.

    One thing that will work with an array of String is to use the selector NSString.description. This works because Swift String can be bridged to NSString.

    The other thing you need is to change your array type from AnyObject to what it really is - String.

    var sections: [[String]] = []
    var objects: [String] = [] {
        didSet {
            let selector: Selector = #selector(NSString.description)
            sections = Array(repeating: [], count: collation.sectionTitles.count)
    
            let sortedObjects = collation.sortedArray(from: objects, collationStringSelector: selector)
            for object in sortedObjects {
                let sectionNumber = collation.section(for: object, collationStringSelector: selector)
                sections[sectionNumber].append(object)
            }
    
            self.tableView.reloadData()
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        objects = ["One", "Two", "Three"]
    }
    

    Update your other code that tries to cast these values as String since that won't be needed any more.