My previous answer, although it works, does not do its best possible Swift validator. It also switches between types that can be used in one centralized location, which limits extensibility for the frame owner.
The following approach solves these problems. (Please forgive me for not having a heart to delete my previous answer, let's say that these limitations are instructive ...)
As before, we will start with the target API:
struct Thing : ThingType { let properties: [String:Sortable] subscript(key: String) -> Sortable? { return properties[key] } } let data: [[String:Sortable]] = [ ["id": 1, "description": "one"], ["id": 2, "description": "two"], ["id": 3, "description": "three"], ["id": 4, "description": "four"], ["id": 4, "description": "four"] ] var things = data.map(Thing.init) things.sortInPlaceBy("id") things .map{ $0["id"]! } // [1, 2, 3, 4] things.sortInPlaceBy("description") things .map{ $0["description"]! } // ["four", "one", "three", "two"]
To make this possible, we must have this ThingType protocol and an extension for mutable collections (which will work for both sets and arrays):
protocol ThingType { subscript(_: String) -> Sortable? { get } } extension MutableCollectionType where Index : RandomAccessIndexType, Generator.Element : ThingType { mutating func sortInPlaceBy(key: String, ascending: Bool = true) { sortInPlace { guard let lhs = $0[key], let rhs = $1[key] else { return false // TODO: nil handling } guard let b = (try? lhs.isOrderedBefore(rhs, ascending: ascending)) else { return false // TODO: handle SortableError } return b } } }
Obviously, the whole idea revolves around this Sortable protocol:
protocol Sortable { func isOrderedBefore(_: Sortable, ascending: Bool) throws -> Bool }
... that can be matched independently by whatever type we want to work with:
import Foundation extension NSNumber : Sortable { func isOrderedBefore(other: Sortable, ascending: Bool) throws -> Bool { try throwIfTypeNotEqualTo(other) let f: (Double, Double) -> Bool = ascending ? (<) : (>) return f(doubleValue, (other as! NSNumber).doubleValue) } } extension NSString : Sortable { func isOrderedBefore(other: Sortable, ascending: Bool) throws -> Bool { try throwIfTypeNotEqualTo(other) let f: (String, String) -> Bool = ascending ? (<) : (>) return f(self as String, other as! String) } } // TODO: make more types Sortable (including those that do not conform to NSObject or even AnyObject)!
This throwIfTypeNotEqualTo method is simply an extension of the Sortable convenience:
enum SortableError : ErrorType { case TypesNotEqual } extension Sortable { func throwIfTypeNotEqualTo(other: Sortable) throws { guard other.dynamicType == self.dynamicType else { throw SortableError.TypesNotEqual } } }
What is it. Now we can map new types to Sortable even outside the box, and type checking checks the source data [[String:Sortable]] at compile time. Also, if Thing expands to match Hashable , then Set<Thing> will also sort by key ...
Note that although Sortable itself has no limitations (which is surprising), the data and Thing properties source can be bound to dictionaries with NSObject or AnyObject , if required, using a protocol, for example:
protocol SortableNSObjectType : Sortable, NSObjectProtocol { }
... or more, declaring data and Thing properties as:
let _: [String : protocol<Sortable, NSObjectProtocol>]