Dynamically Passing Closure with Keypaths to a Sorting Function

Dynamically passing closure with keypaths to a sorting function

You can simply pass a predicate to return a comparable property from the element of a sequence and another one to check if both elements are in increasing other:

extension Sequence {
public func sorted<T: Comparable>(
_ predicate: (Element) throws -> T, by areInIncreasingOrder: (T, T) throws -> Bool
) rethrows -> [Element] {
try sorted { try areInIncreasingOrder(predicate($0), predicate($1)) }
}
func sorted<T: Comparable>(_ predicate: (Element) throws -> T) rethrows -> [Element] {
try sorted(predicate, by: <)
}
}


extension MutableCollection where Self: RandomAccessCollection {
public mutating func sort<T: Comparable>(
_ predicate: (Element) throws -> T, by areInIncreasingOrder: (T, T) throws -> Bool
) rethrows {
try sort { try areInIncreasingOrder(predicate($0), predicate($1)) }
}
public mutating func sort<T: Comparable>(_ predicate: (Element) throws -> T) rethrows {
try sort(predicate, by: <)
}

}

To suport multiple criteria (secondary, tertiary, and quaternary) you just need to add more generic types to your method:

extension Sequence {
public func sorted<T: Comparable, U: Comparable>(
_ primary: ((Element) throws -> T, (T, T) throws -> Bool),
_ secondary: ((Element) throws -> U, (U, U) throws -> Bool)
) rethrows -> [Element] {
try sorted {
let lhs = try primary.0($0)
let rhs = try primary.0($1)
guard lhs == rhs else {
return try primary.1(lhs, rhs)
}
return try secondary.1(secondary.0($0), secondary.0($1))
}
}
public func sorted<T: Comparable, U: Comparable, V: Comparable>(
_ primary: ((Element) throws -> T, (T, T) throws -> Bool),
_ secondary: ((Element) throws -> U, (U, U) throws -> Bool),
_ terciary: ((Element) throws -> V, (V, V) throws -> Bool)
) rethrows -> [Element] {
try sorted {
let lhs1 = try primary.0($0)
let rhs1 = try primary.0($1)
guard lhs1 == rhs1 else {
return try primary.1(lhs1, rhs1)
}
let lhs2 = try secondary.0($0)
let rhs2 = try secondary.0($1)
guard lhs2 == rhs2 else {
return try secondary.1(lhs2, rhs2)
}
return try terciary.1(terciary.0($0), terciary.0($1))
}
}
public func sorted<T: Comparable, U: Comparable, V: Comparable, W: Comparable>(
_ primary: ((Element) throws -> T, (T, T) throws -> Bool),
_ secondary: ((Element) throws -> U, (U, U) throws -> Bool),
_ terciary: ((Element) throws -> V, (V, V) throws -> Bool),
_ quaternary: ((Element) throws -> W, (W, W) throws -> Bool)
) rethrows -> [Element] {
try sorted {
let lhs1 = try primary.0($0)
let rhs1 = try primary.0($1)
guard lhs1 == rhs1 else {
return try primary.1(lhs1, rhs1)
}
let lhs2 = try secondary.0($0)
let rhs2 = try secondary.0($1)
guard lhs2 == rhs2 else {
return try secondary.1(lhs2, rhs2)
}
let lhs3 = try terciary.0($0)
let rhs3 = try terciary.0($1)
guard lhs3 == rhs3 else {
return try terciary.1(lhs3, rhs3)
}
return try quaternary.1(quaternary.0($0), quaternary.0($1))
}
}
}

Now if you would like to create the mutating version of those methods you need to extend MutableCollection and constrain Selfto RandomAccessCollection:

extension MutableCollection where Self: RandomAccessCollection {
public mutating func sort<T: Comparable, U: Comparable>(
_ primary: ((Element) throws -> T, (T, T) throws -> Bool),
_ secondary: ((Element) throws -> U, (U, U) throws -> Bool)
) rethrows {
try sort {
let lhs = try primary.0($0)
let rhs = try primary.0($1)
guard lhs == rhs else {
return try primary.1(lhs, rhs)
}
return try secondary.1(secondary.0($0), secondary.0($1))
}
}
public mutating func sort<T: Comparable, U: Comparable, V: Comparable>(
_ primary: ((Element) throws -> T, (T, T) throws -> Bool),
_ secondary: ((Element) throws -> U, (U, U) throws -> Bool),
_ terciary: ((Element) throws -> V, (V, V) throws -> Bool)
) rethrows {
try sort {
let lhs1 = try primary.0($0)
let rhs1 = try primary.0($1)
guard lhs1 == rhs1 else {
return try primary.1(lhs1, rhs1)
}
let lhs2 = try secondary.0($0)
let rhs2 = try secondary.0($1)
guard lhs2 == rhs2 else {
return try secondary.1(lhs2, rhs2)
}
return try terciary.1(terciary.0($0), terciary.0($1))
}
}
public mutating func sort<T: Comparable, U: Comparable, V: Comparable, W: Comparable>(
_ primary: ((Element) throws -> T, (T, T) throws -> Bool),
_ secondary: ((Element) throws -> U, (U, U) throws -> Bool),
_ terciary: ((Element) throws -> V, (V, V) throws -> Bool),
_ quaternary: ((Element) throws -> W, (W, W) throws -> Bool)
) rethrows {
try sort {
let lhs1 = try primary.0($0)
let rhs1 = try primary.0($1)
guard lhs1 == rhs1 else {
return try primary.1(lhs1, rhs1)
}
let lhs2 = try secondary.0($0)
let rhs2 = try secondary.0($1)
guard lhs2 == rhs2 else {
return try secondary.1(lhs2, rhs2)
}
let lhs3 = try terciary.0($0)
let rhs3 = try terciary.0($1)
guard lhs3 == rhs3 else {
return try terciary.1(lhs3, rhs3)
}
return try quaternary.1(quaternary.0($0), quaternary.0($1))
}
}
}

Playground testing:

struct User: Equatable {
let name: String
let age: Int
}

var users: [User] = [
.init(name: "Liza", age: 19),
.init(name: "John", age: 19),
.init(name: "Steve", age: 51)
]

Single property criteria sort:

let sorted = users.sorted(\.age)  // [{name "Liza", age 19}, {name "John", age 19}, {name "Steve", age 51}]
users.sort(\.age) // [{name "Liza", age 19}, {name "John", age 19}, {name "Steve", age 51}]
users == sorted // true

Multiple property criteria sort:

let sorted = users.sorted((\.age, >),(\.name, <)) // [{name "Steve", age 51}, {name "John", age 19}, {name "Liza", age 19}]

users.sort((\.age, >),(\.name, <)) // [{name "Steve", age 51}, {name "John", age 19}, {name "Liza", age 19}]
users == sorted // true

For Xcode 13.0+, iOS 15.0+, iPadOS 15.0+, macOS 12.0+, Mac Catalyst 15.0+, tvOS 15.0+, watchOS 8.0+ you can use KeyPathComparator:

Usage:

let sorted1 = users.sorted(using: [KeyPathComparator(\.age)])  // [{name "Liza", age 19}, {name "John", age 19}, {name "Steve", age 51}]
let sorted2 = users.sorted(using: [KeyPathComparator(\.age), KeyPathComparator(\.name)]) // [{name "John", age 19}, {name "Liza", age 19}, {name "Steve", age
let sorted3 = users.sorted(using: [KeyPathComparator(\.age, order: .reverse), KeyPathComparator(\.name)]) // [{name "Steve", age 51}, {name "John", age 19}, {name "Liza", age 19}]

How to sort an array of custom objects by property value in Swift

First, declare your Array as a typed array so that you can call methods when you iterate:

var images : [imageFile] = []

Then you can simply do:

Swift 2

images.sorted({ $0.fileID > $1.fileID })

Swift 3

images.sorted(by: { $0.fileID > $1.fileID })

Swift 5

images.sorted { $0.fileId > $1.fileID }

The example above gives the results in descending order.

Swift 4: Find value in nested, dynamic DictionaryString, Any recursively

A more compact recursive solution might be:

func search(key:String, in dict:[String:Any], completion:((Any) -> ())) {
if let foundValue = dict[key] {
completion(foundValue)
} else {
dict.values.enumerated().forEach {
if let innerDict = $0.element as? [String:Any] {
search(key: key, in: innerDict, completion: completion)
}
}
}
}

the usage is:

search(key: "child3SubChild2Key", in: theDictionary, completion: { print($0) }) 

which gives:

["comment": "child3SubChild2Comment", "value": "child3SubChild2Subchild1Value"]

alternatively, if you don't want to use closures, you might use the following:

extension Dictionary {
func search(key:String, in dict:[String:Any] = [:]) -> Any? {
guard var currDict = self as? [String : Any] else { return nil }
currDict = !dict.isEmpty ? dict : currDict

if let foundValue = currDict[key] {
return foundValue
} else {
for val in currDict.values {
if let innerDict = val as? [String:Any], let result = search(key: key, in: innerDict) {
return result
}
}
return nil
}
}
}

usage is:

let result = theDictionary.search(key: "child4SubChild3SubChild1Key")
print(result) // ["comment": "child4SubChild3SubChild1Comment", "value": "child4SubChild3SubChild1Value"]

Detect objects inside closure (in Linux Swift)

Well, I think the easiest way will be passing captured object to read function and use KVO for catching updated values:

class ReadableObject: NSObject {
weak var reader: Reader?
}

class Reader: NSObject {
private var observerContext = 0

func doSomething(with value: Any?, of property: String) {
print("value of \(property) modified to: '\(value ?? "")'")
}

func read(capturedObjects:[ReadableObject], block: (() -> Void)) {
capturedObjects.forEach({ object in
object.reader = self
let properties = Mirror(reflecting: object).children.compactMap { $0.label }
properties.forEach({ (property) in
object.addObserver(self, forKeyPath: property, options: [.new], context: &observerContext)
})
})
block()
capturedObjects.forEach({$0.reader = nil})
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard context == &observerContext, let kp = keyPath, let obj = object as? ReadableObject else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}

let value = obj.value(forKeyPath: kp)
let className = NSStringFromClass(type(of: obj)).components(separatedBy: ".").last ?? ""
obj.reader?.doSomething(with: value, of: "\(className).\(kp)")
}
}

Then inherit your classes from ReadableObject and mark all readable properties as "@obj dynamic" (in order to make them observable).

class Dog: ReadableObject {
@objc dynamic var name: String = ""
}

class Cat: ReadableObject {
@objc dynamic var name: String = ""
}

Usage:

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

let reader = Reader()
let dog = Dog()
let cat = Cat()
reader.read(capturedObjects: [dog, cat]) {
dog.name = "Bob"
cat.name = "Tom"
}
}
}

Console log:

value of Dog.name modified to: 'Bob'

value of Cat.name modified to: 'Tom'

=========================

EDIT

If you don't want to use KVO then the alternative will be the usage of didSet block:

class Dog: ReadableObject {
var name: String = "" {
didSet {
if let reader = reader { //If reader is not nil, then the property was modified inside "read" block
reader?.doSomething(with: name, of: "Dog.name")
}
}
}
}

class Cat: ReadableObject {
var name: String = "" {
didSet {
if let reader = reader {
reader?.doSomething(with: name, of: "Cat.name")
}
}
}
}

How to create an NSSortDescriptor using the comparator signature?

Example:

class C : NSObject {
var id = 0
}

let desc = NSSortDescriptor(key: "id", ascending: true) { // comparator function
id1, id2 in
if (id1 as! Int) < (id2 as! Int) { return .orderedAscending }
if (id1 as! Int) > (id2 as! Int) { return .orderedDescending }
return .orderedSame
}

// test:

let c1 = C(); let c2 = C(); let c3 = C()
c1.id = 100; c2.id = 50; c3.id = 25
let arr = [c1,c2,c3]
let arr2 = (arr as NSArray).sortedArray(using: [desc])

Swift 4 approach for observeValue(forKeyPath:...)

Swift 4 introduced a family of concrete Key-Path types, a new Key-Path Expression to produce them and a new closure-based observe function available to classes that inherit NSObject.

Using this new set of features, your particular example can now be expressed much more succinctly:

self.observation = object.observe(\.keyPath) { 
[unowned self] object, change in
self.someFunction()
}

Types Involved

  • observation:NSKeyValueObservation
  • change:NSKeyValueObservedChange
  • \.keyPath: An instance of a KeyPath class produced at compile time.

Key-Path grammar

The general grammar of a Key-Path Expression follows the form \Type.keyPath where Type is a concrete type name (incl. any generic parameters), and keyPath a chain of one or more properties, subscripts, or optional chaining/forced unwrapping postfixes. In addition, if the keyPath's Type can be inferred from context, it can be elided, resulting in a most pithy \.keyPath.

These are all valid Key-Path Expressions:

\SomeStruct.someValue
\.someClassProperty
\.someInstance.someInnerProperty
\[Int].[1]
\[String].first?.count
\[SomeHashable: [Int]].["aStringLiteral, literally"]!.count.bitWidth

Ownership

You're the owner of the NSKeyValueObservation instance the observe function returns, meaning, you don't have to addObserver nor removeObserver anymore; rather, you keep a strong reference to it for as long as you need your observation observing.

You're not required to invalidate() either: it'll deinit gracefully. So, you can let it live until the instance holding it dies, stop it manually by niling the reference, or even invoke invalidate() if you need to keep your instance alive for some smelly reason.

Caveats

As you may have noticed, observation still lurks inside the confines of Cocoa's KVO mechanism, therefore it's only available to Obj-C classes and Swift classes inheriting NSObject (every Swift-dev's favorite type) with the added requirement that any value you intend to observe, must be marked as @objc (every Swift-dev's favorite attribute) and declared dynamic.

That being said, the overall mechanism is a welcomed improvement, particularly because it manages to Swiftify observing imported NSObjects from modules we may happen to be required to use (eg. Foundation), and without risking weakening the expressive power we work so hard to obtain with every keystroke.

As a side-note, Key-Path String Expressions are still required to dynamically access NSObject's properties to KVC or call value(forKey(Path):)

Beyond KVO

There's much more to Key-Path Expressions than KVO. \Type.path expressions can be stored as KeyPath objects for later reuse. They come in writable, partial and type-erased flavors. They can augment the expressive power of getter/setter functions designed for composition, not to mention the role they play in allowing those with the strongest of stomachs to delve into the world of functional concepts like Lenses and Prisms. I suggest you check the links down below to learn more about the many development doors they can open.

Links:

Key-Path Expression @ docs.swift.org

KVO docs @ Apple

Swift Evolution Smart KeyPaths proposal

Ole Begemann's Whats-new-in-Swift-4 playground with Key-Path examples

WWDC 2017 Video: What's New in Foundation 4:35 for SKP and 19:40 for KVO.

Using KVC with Singleton pattern

The key path is not correct. It’s KVOObject.fontSize. And you need to add the observer to that singleton:

 KVOObject.shared.addObserver(self, forKeyPath: #keyPath(KVOObject.fontSize), options: [.old, .new], context: nil)

As an aside, (a) you should probably use a context to identify whether you're handling this or whether it might be used by the superclass; (b) you should call the super implementation if it's not yours; and (c) make sure to remove the observer on deinit:

class ViewController: UICollectionViewController {

private var observerContext = 0

override func viewDidLoad() {
super.viewDidLoad()

KVOObject.shared.addObserver(self, forKeyPath: #keyPath(KVOObject.fontSize), options: [.new, .old], context: &observerContext)
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &observerContext {
// do something
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}

deinit {
KVOObject.shared.removeObserver(self, forKeyPath: #keyPath(KVOObject.fontSize))
}

...
}

Or, if in Swift 4, it's now much easier as it's closure-based (avoiding need for context) and is automatically removed when the NSKeyValueObservation falls out of scope:

class ViewController: UICollectionViewController {

private var token: NSKeyValueObservation?

override func viewDidLoad() {
super.viewDidLoad()

token = KVOObject.shared.observe(\.fontSize, options: [.new, .old]) { [weak self] object, change in
// do something
}
}

...
}

By the way, a few observations on the singleton:

  1. The shared property does not require @objc qualifier; only the property being observed needs that; and

  2. The init method really should be calling super; and

  3. I'd probably also declare it to be final to avoid confusion that can result in subclassing singletons.

Thus:

final class KVOObject: NSObject {
static let shared = KVOObject()

override private init() { super.init() }

@objc dynamic var fontSize: Int = 18
}


Related Topics



Leave a reply



Submit