Skip to content

Instantly share code, notes, and snippets.

@evgeniyd
Last active July 16, 2018 16:59
Show Gist options
  • Save evgeniyd/d974121b94b17931480c to your computer and use it in GitHub Desktop.
Save evgeniyd/d974121b94b17931480c to your computer and use it in GitHub Desktop.
NSSortDescriptor subclass to sort objects w/ nullable key. These objects appears (unsorted) at the end of the sorted list when ascending == true. Written in Swift, this class merely extends the idea from this SO answer: http://stackoverflow.com/a/11188999/1492173
import Foundation
class UBINullableSortDescriptor: NSSortDescriptor {
override init(key: String, ascending: Bool) {
super.init(key: key, ascending: ascending)
}
required override init(key: String, ascending: Bool, selector: Selector) {
super.init(key: key, ascending: ascending, selector: selector)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func copyWithZone(zone: NSZone) -> AnyObject {
let thisClassType = self.dynamicType
return thisClassType(key: self.key!, ascending:self.ascending, selector:self.selector)
}
override func compareObject(object1: AnyObject, toObject object2: AnyObject) -> NSComparisonResult {
if self.isNull(object: object1.valueForKey(self.key!)/*object1[self.key!]*/) {
if self.isNull(object: object2.valueForKey(self.key!)/*object2[self.key!]*/) {
return .OrderedSame
}
return .OrderedDescending
}
if self.isNull(object: object2.valueForKey(self.key!)/*object2[self.key!]*/) {
return .OrderedAscending
}
return super.compareObject(object1, toObject: object2)
}
override var reversedSortDescriptor: AnyObject! {
get {
let thisClassType = self.dynamicType
let asc: Bool = self.ascending
let desc = !self.ascending
return thisClassType(key: self.key!, ascending: desc, selector:self.selector)
}
}
// Note: there is no reason to this being w/ 'internal' access level,
// but it is so we are able to test the class's internals w/ XCTest
internal func isNull(#object: AnyObject?) -> Bool {
// (a) == nil
if let notNil: AnyObject = object {
// [(a) isEqual:[NSNull null]]
let nsNullObject = NSNull()
if let derivesFromNSObject = notNil as? NSObject {
if derivesFromNSObject == nsNullObject {
return true
}
}
return false
}
else {
return true
}
}
}
import XCTest
// objects compared by NSSortDescriptor must be key-value compliant (e.g. NSObject subclasses)
// https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSSortDescriptor_Class/index.html
class TestDataItemAnyObject: NSObject {
var sortIndex: AnyObject?
var expectedIndex: Int
init(sortIndex: AnyObject?, expectedIndex: Int) {
self.sortIndex = sortIndex
self.expectedIndex = expectedIndex
}
}
class TestDataItemNSNumber: NSObject {
var sortIndex: NSNumber?
var expectedIndex: Int
init(sortIndex: NSNumber?, expectedIndex: Int) {
self.sortIndex = sortIndex
self.expectedIndex = expectedIndex
}
}
class UBINullableSortDescriptorTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
// MARK: Init/Copy Test
func testInit() {
let testSortDescriptor = UBINullableSortDescriptor(key: "testKey", ascending: true, selector: "testSelector")
XCTAssertTrue(testSortDescriptor.key == "testKey")
XCTAssertTrue(testSortDescriptor.selector==Selector("testSelector") )
XCTAssertEqual(testSortDescriptor.ascending, true)
}
func testCopy() {
let testSortDescriptor = UBINullableSortDescriptor(key: "testKey", ascending: true, selector: "testSelector")
let copyTestSortDescriptor: AnyObject = testSortDescriptor.copy()
let sameTestSortDescriptor = testSortDescriptor
XCTAssertFalse(copyTestSortDescriptor === testSortDescriptor)
XCTAssertTrue(sameTestSortDescriptor === testSortDescriptor)
}
// MARK: Archieve Test
func testArchieve() {
let thisClassType = self.dynamicType
let filePathURL = NSBundle(forClass: thisClassType).resourceURL?.URLByAppendingPathComponent("UBINullableSortDescriptor.archieve")
XCTAssertNotNil(filePathURL)
// encode
let expectedSortDescriptor = UBINullableSortDescriptor(key: "testKey", ascending: true, selector: "testSelector")
XCTAssertTrue(NSKeyedArchiver.archiveRootObject(expectedSortDescriptor, toFile: filePathURL!.path!))
// decode
//var testSortDescriptor:UBINullableSortDescriptor = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath!) as UBINullableSortDescriptor; // <== this will crash, don't know why
let testSortDescriptor:NSSortDescriptor = NSKeyedUnarchiver.unarchiveObjectWithFile(filePathURL!.path!) as! NSSortDescriptor;
XCTAssertTrue(testSortDescriptor.key == "testKey")
XCTAssertTrue(testSortDescriptor.selector==Selector("testSelector") )
XCTAssertEqual(testSortDescriptor.ascending, true)
}
// MARK: Internals Tests
func testIsNull() {
let testSortDescriptor = UBINullableSortDescriptor(key: "", ascending: true, selector: "")
XCTAssertNotNil(testSortDescriptor)
// test NSNull equality
// Note: I hope that 'NSNull()' in Swift is equal to '[NSNull null]' in Objective-C
let testObject01 = TestDataItemAnyObject(sortIndex: NSNull(), expectedIndex: 0)
XCTAssertTrue(testSortDescriptor.isNull(object: testObject01.sortIndex))
// test nil equality
let testObject02 = TestDataItemAnyObject(sortIndex: nil, expectedIndex: 0)
XCTAssertTrue(testSortDescriptor.isNull(object: testObject02.sortIndex))
// test not nil not-NSObject-descender
let testObject03 = TestDataItemAnyObject(sortIndex: "1", expectedIndex: 0)
XCTAssertFalse(testSortDescriptor.isNull(object: testObject03.sortIndex))
// test not nil objects
let testObject04 = TestDataItemAnyObject(sortIndex: NSNumber(int: 2), expectedIndex: 0)
XCTAssertFalse(testSortDescriptor.isNull(object: testObject04.sortIndex))
let testObject05 = TestDataItemNSNumber(sortIndex: NSNumber(int: 3), expectedIndex: 0)
XCTAssertFalse(testSortDescriptor.isNull(object: testObject05.sortIndex))
}
// MARK: Comparizon tests
func testReversedSortDescriptorPrimitive() {
let testSortDescriptor = UBINullableSortDescriptor(key: "testKey", ascending: true, selector: "testSelector")
XCTAssertNotNil(testSortDescriptor)
let testReversedSortDescriptor: UBINullableSortDescriptor = testSortDescriptor.reversedSortDescriptor as UBINullableSortDescriptor
XCTAssertFalse(testReversedSortDescriptor.ascending)
}
func testComparizonNSNumber() {
// 1. create test data set
let testArray: [TestDataItemNSNumber] =
[
TestDataItemNSNumber(sortIndex: nil, expectedIndex:2),
TestDataItemNSNumber(sortIndex: NSNumber(integer: 2), expectedIndex:1),
TestDataItemNSNumber(sortIndex: NSNumber(integer: 1), expectedIndex:0),
TestDataItemNSNumber(sortIndex: nil, expectedIndex:3)
]
// 2. create sort descriptor
var descriptor = UBINullableSortDescriptor(key: "sortIndex", ascending: true)
// 3. sort
let sortedArray = (testArray as NSArray).sortedArrayUsingDescriptors([descriptor])
// 4. test
for index: Int in 0...3 {
let expIndex: Int = (sortedArray[index] as TestDataItemNSNumber).expectedIndex as Int
XCTAssertTrue(expIndex == index, "object w/ sortIndex == \((sortedArray[index] as TestDataItemNSNumber).sortIndex) is expected to be at \(index) position")
}
}
func testComparizonNSNumberAllNil() {
let testArray: [TestDataItemNSNumber] =
[
TestDataItemNSNumber(sortIndex: nil, expectedIndex:0),
TestDataItemNSNumber(sortIndex: nil, expectedIndex:1),
TestDataItemNSNumber(sortIndex: nil, expectedIndex:2),
TestDataItemNSNumber(sortIndex: nil, expectedIndex:3)
]
var descriptor = UBINullableSortDescriptor(key: "sortIndex", ascending: true)
let sortedArray = (testArray as NSArray).sortedArrayUsingDescriptors([descriptor])
for index: Int in 0...3 {
let expIndex: Int = (sortedArray[index] as TestDataItemNSNumber).expectedIndex as Int
XCTAssertTrue(expIndex == index, "object w/ sortIndex == \((sortedArray[index] as TestDataItemNSNumber).sortIndex) is expected to be at \(index) position")
}
}
func testComparizonNSNumberOposite() {
let testArray: [TestDataItemNSNumber] =
[
TestDataItemNSNumber(sortIndex: nil, expectedIndex:2),
TestDataItemNSNumber(sortIndex: nil, expectedIndex:3),
TestDataItemNSNumber(sortIndex: NSNumber(integer: 15), expectedIndex:1),
TestDataItemNSNumber(sortIndex: NSNumber(integer: 12), expectedIndex:0)
]
var descriptor = UBINullableSortDescriptor(key: "sortIndex", ascending: true)
let sortedArray = (testArray as NSArray).sortedArrayUsingDescriptors([descriptor])
for index: Int in 0...3 {
let expIndex: Int = (sortedArray[index] as TestDataItemNSNumber).expectedIndex as Int
XCTAssertTrue(expIndex == index, "object w/ sortIndex == \((sortedArray[index] as TestDataItemNSNumber).sortIndex) is expected to be at \(index) position")
}
}
func testComparizonAnyObject() {
let testArray: [TestDataItemAnyObject] =
[
TestDataItemAnyObject(sortIndex: nil, expectedIndex:2),
TestDataItemAnyObject(sortIndex: nil, expectedIndex:3),
TestDataItemAnyObject(sortIndex: 1, expectedIndex:0),
TestDataItemAnyObject(sortIndex: 2, expectedIndex:1)
]
var descriptor = UBINullableSortDescriptor(key: "sortIndex", ascending: true)
let sortedArray = (testArray as NSArray).sortedArrayUsingDescriptors([descriptor])
for index: Int in 0...3 {
let expIndex: Int = (sortedArray[index] as TestDataItemAnyObject).expectedIndex as Int
XCTAssertTrue(expIndex == index, "object w/ sortIndex == \((sortedArray[index] as TestDataItemAnyObject).sortIndex) is expected to be at \(index) position")
}
}
}
@aumeunier
Copy link

This is awesome! Thanks for the link :)
I also suggest you add another one for having the null objects at the top when ascending is true.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment