2

Given an NSTableView that has an array of structures as its datasource. A user can click on any column heading to sort by that column. The column identifiers match the property names of the properties within the structure.

Given a structure

struct MyStructure {       
     var col0data = ""  //name matches the column identifier
     var col1data = ""
}

and an array of structures

var myArray = [MyStructure]()

The goal is that when a column heading is clicked, use that column's identifier to sort the array of structures by that column identifier/property

With an array of dictionaries, it was easy...

self.myArrayOfDictionaries.sortInPlace {
     (dictOne, dictTwo) -> Bool in
     let d1 = dictOne[colIdentifier]! as String;
     let d2 = dictTwo[colIdentifier]! as String;

     return d1 < d2 //or return d1 > d2 for reverse sort
}

The question is how to access the properties of the Structure dynamically, something like

let struct = myArray[10] as! MyStructure //get the 10th structure in the array
let value = struct["col0data"] as! String //get the value of the col0data property

If there is a better way, suggestions would be appreciated.

I should also note that the structure may have 50 properties so this is an effort to reduce the amount of code needed to sort the array by any one of those properties.

edit:

One solution is to change the structure to a class derived from NSObject. Then the properties could be accessed via .valueForKey("some key"). However, I am trying to keep this Swifty.

NSGod
  • 22,699
  • 3
  • 58
  • 66
Jay
  • 34,438
  • 18
  • 52
  • 81
  • Why are you using a struct here? With 50 properties, an array or dictionary seems like a more appropriate option. If you need to encapsulate extra logic, then you could have an array or dictionary as a property of your struct. – Hamish Jul 01 '16 at 18:46
  • @originaluser2 I started off with an Array of Dictionaries which worked wonderfully for sorting etc. However, as the project grew I needed to add object logic hence moving to a struct or class. Either way, they still need to be sortable by one of their properties which is derived from the tableView header click. I could brute force it with a bunch of if..then statements (if headerClick = "col0" then sort by x, if headerClick = "col1" sort by y etc) but that seems very inelegant. – Jay Jul 01 '16 at 21:59
  • So why not simply have the dictionary as a property of the struct? That way you get both encapsulation and easy lookup of the values. – Hamish Jul 02 '16 at 13:03
  • @originaluser2 This is an array of structures that's used as a datasource for a tableView. So they (the structs) need to be sortable by one of the properties (whichever one matches the header of the column that was clicked). If the dictionary is embedded inside the structure, there wouldn't be a way to sort the array of structures. However, if there is a way, it's a great solution so post it as an answer! – Jay Jul 02 '16 at 13:13
  • Use `NSArrayController` and Cocoa bindings then you get everything for free. But that requires a class rather than a struct as model. – vadian Jul 02 '16 at 13:29

4 Answers4

2

Maybe I have a solution to your problem. The advantage of this code over your solution is here you don't need to add a subscript method to your struct to create an hardcoded String-Property-Value map via code.

Here's my extension

extension _ArrayType {
    func sortedBy(propertyName propertyName: String) -> [Self.Generator.Element] {
        let mirrors = self.map { Mirror(reflecting: $0) }
        let propertyValues = mirrors.map { $0.children.filter { $0.label == propertyName }.first?.value }
        let castedValues = propertyValues.map { $0 as? String }

        let sortedArray = zip(self, castedValues).sort { (left, right) -> Bool in
            return left.1 < right.1
        }.map { $0.0 }
        return sortedArray
    }
}

Usage

struct Animal {
    var name: String
    var type: String
}

let animals = [
    Animal(name: "Jerry", type: "Mouse"),
    Animal(name: "Tom", type: "Cat"),
    Animal(name: "Sylvester", type: "Cat")
]

animals.sortedBy(propertyName: "name")
// [{name "Jerry", type "Mouse"}, {name "Sylvester", type "Cat"}, {name "Tom", type "Cat"}]

animals.sortedBy(propertyName: "type")
// [{name "Tom", type "Cat"}, {name "Sylvester", type "Cat"}, {name "Jerry", type "Mouse"}]

Limitations

The worst limitation of this solutions is that it works only for String properties. It can be change to work with any types of property by it must be at compile time. Right now I have not a solution to make it work with any king of property type without changing the code.

I already asked help for the core of the problem here.

Community
  • 1
  • 1
Luca Angeletti
  • 58,465
  • 13
  • 121
  • 148
1

Here's an answer (but not the best answer); use subscripts to return the correct property, and set which property you are sorting by within the array.sort:

struct MyStructure {       
     var col0data = ""  //name matches the column identifier
     var col1data = ""

     subscript(key: String) -> String? { //the key will be the col identifier
        get {
            if key == "col0data" {
                return col0data
            } else if key == "col1data" {
                return col1data
            }

            return nil
        }
    }
}

And then here's how the sort works:

let identifier = the column identifier string,say col0data in this case

myArray.sortInPlace ({

    let my0 = $0[identifier]!  //the identifier from the table col header
    let my1 = $1[identifier]!

    return my0 < my1
})
Jay
  • 34,438
  • 18
  • 52
  • 81
  • If you're going to go with this approach – you could improve it by using a `switch` statement on `key`, then just list the different column identifiers as cases. – Hamish Jul 02 '16 at 16:15
  • @originaluser2 Yes! Good call. – Jay Jul 02 '16 at 16:20
  • @Jay: But with this solution you just moved the hardcoded part from the closure to the subscript method... – Luca Angeletti Jul 02 '16 at 16:36
  • @appzYourLife I agree. Moving that code to a single location reduced a lot of duplicate code in the app but as you pointed out it's still not a very elegant solution. It works but the other answers are far better. – Jay Jul 02 '16 at 16:47
1

I would definitely recommend simply embedding your dictionary into your struct. A dictionary is a much more suitable data structure for 50 key-value pairs than 50 properties – and you've said that this would be an acceptable solution.

Embedding the dictionary in your struct will give you the best of both worlds – you can easily encapsulate logic & you have have easy lookup of the values for each column ID.

You can now simply sort your array of structures like this:

struct MyStructure {

    var dict = [String:String]()

    init(col0Data:String, col1Data:String) {
        dict["col0data"] = col0Data
        dict["col1data"] = col1Data
    }
}


var myArray = [MyStructure(col0Data: "foo", col1Data: "bar"), MyStructure(col0Data: "bar", col1Data: "foo")]

var column = "col0data"
myArray.sort {
    $0.dict[column] < $1.dict[column]
}

print(myArray) // [MyStructure(dict: ["col0data": "bar", "col1data": "foo"]), MyStructure(dict: ["col0data": "foo", "col1data": "bar"])]

column = "col1data"
myArray.sort {
    $0.dict[column] < $1.dict[column]
}

print(myArray) // MyStructure(dict: ["col0data": "foo", "col1data": "bar"])], [MyStructure(dict: ["col0data": "bar", "col1data": "foo"])
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • Brilliant Brilliant Brilliant. You definitely didn't miss anything - I misread your comment. This is on point and works beautifuly. Very much appreciated! – Jay Jul 02 '16 at 16:36
  • @Jay Happy to help :) – Hamish Jul 02 '16 at 16:38
-1

If you do not know what types the values of MyStructure can be you will have a hard time comparing them to sort them. If you had a function that can compare all types you can have in MyStructure then something like this should work

struct OtherTypeNotComparable {

}

struct MyStructure {
    var col0data = "cat"  //name matches the column identifier
    var col1data: OtherTypeNotComparable
}

let structures = [MyStructure(), MyStructure()]

let sortBy = "col1data"

func yourCompare(a: Any, b: Any) -> Bool {
    return true
}

var expanded : [[(String, Any, MyStructure)]] 
    = structures.map { s in Mirror(reflecting: s).children.map { ($0!, $1, s) } }

expanded.sortInPlace { (a, b) -> Bool in
    let aMatch = a.filter { $0.0 == sortBy }.first!.1
    let bMatch = b.filter { $0.0 == sortBy }.first!.1
    return yourCompare(aMatch, b: bMatch)
}

source: https://developer.apple.com/library/watchos/documentation/Swift/Reference/Swift_Mirror_Structure/index.html

Mr Flynn
  • 415
  • 2
  • 10
  • You are assuming the type of the property to be `String`. But at compile time you don't know it. – Luca Angeletti Jul 01 '16 at 19:56
  • 1
    Is the intent of this answer to sort the properties of the structure? If so, that's not the original question. The question is to how to sort the structures in the array by a specific property which would be determined at run time. i.e. user clicks a table column heading, then we know which property to sort the array by. The property name would be a string derived from the column identifier. I do like that you included the Mirror function through... – Jay Jul 01 '16 at 20:09
  • The problem is comparing unknown types at run time which may or may not be comparable. [similar](http://stackoverflow.com/questions/25901105/compare-anyobjects-in-swift-without-casting-them-to-a-specific-type). I updated my answer to use a function that you can write that compares two 'Any' – Mr Flynn Jul 01 '16 at 20:39