Mohsen
Mohsen

Reputation: 65785

Change the value that is being set in variable's willSet block

I'm trying to sort the array that is being set before setting it but the argument of willSet is immutable and sort mutates the value. How can I overcome this limit?

var files:[File]! = [File]() {
    willSet(newFiles) {
        newFiles.sort { (a:File, b:File) -> Bool in
            return a.created_at > b.created_at
        }
    }
}

To put this question out of my own project context, I made this gist:

class Person {
    var name:String!
    var age:Int!

    init(name:String, age:Int) {
        self.name = name
        self.age = age
    }
}

let scott = Person(name: "Scott", age: 28)
let will = Person(name: "Will", age: 27)
let john = Person(name: "John", age: 32)
let noah = Person(name: "Noah", age: 15)

var sample = [scott,will,john,noah]



var people:[Person] = [Person]() {
    willSet(newPeople) {
        newPeople.sort({ (a:Person, b:Person) -> Bool in
            return a.age > b.age
        })

    }
}

people = sample

people[0]

I get the error stating that newPeople is not mutable and sort is trying to mutate it.

Upvotes: 45

Views: 34700

Answers (3)

Slipp D. Thompson
Slipp D. Thompson

Reputation: 34913

Another solution for people who like abstracting away behavior like this (especially those who are used to features like C#'s custom attributes) is to use a Property Wrapper, available since Swift 5.1 (Xcode 11.0).

First, create a new property wrapper struct that can sort Comparable elements:

@propertyWrapper
public struct Sorting<V : MutableCollection & RandomAccessCollection>
    where V.Element : Comparable
{
    var value: V
    
    public init(wrappedValue: V) {
        value = wrappedValue
        value.sort()
    }
    
    public var wrappedValue: V {
        get { value }
        set {
            value = newValue
            value.sort()
        }
    }
}

and then assuming you implement Comparable-conformance for Person:

extension Person : Comparable {
    static func < (lhs: Person, rhs: Person) -> Bool {
        lhs.age < lhs.age
    }
    static func == (lhs: Person, rhs: Person) -> Bool {
        lhs.age == lhs.age
    }
}

you can declare your property like this and it will be auto-sorted on init or set:

struct SomeStructOrClass
{
    @Sorting var people: [Person]
}


// … (given `someStructOrClass` is an instance of `SomeStructOrClass`)

someStructOrClass.people = sample

let oldestPerson = someStructOrClass.people.last

Caveat: Property wrappers are not allowed (as of time of writing, Swift 5.7.1) in top-level code— they need to be applied to a property var in a struct, class, or enum.


To more literally follow your sample code, you could easily also create a ReverseSorting property wrapper:

@propertyWrapper
public struct ReverseSorting<V : MutableCollection & RandomAccessCollection & BidirectionalCollection>
    where V.Element : Comparable
{
    // Implementation is almost the same, except you'll want to also call `value.reverse()`:
    //   value = …
    //   value.sort()
    //   value.reverse()
}

and then the oldest person will be at the first element:

// …
    @Sorting var people: [Person]
// …

someStructOrClass.people = sample
let oldestPerson = someStructOrClass.people[0]

And even more directly, if your use-case demands using a comparison closure via sort(by:…) instead of implementing Comparable conformance, you can do that to:

@propertyWrapper
public struct SortingBy<V : MutableCollection & RandomAccessCollection>
{
    var value: V
    
    private var _areInIncreasingOrder: (V.Element, V.Element) -> Bool
    
    public init(wrappedValue: V, by areInIncreasingOrder: @escaping (V.Element, V.Element) -> Bool) {
        _areInIncreasingOrder = areInIncreasingOrder
        
        value = wrappedValue
        value.sort(by: _areInIncreasingOrder)
    }
    
    public var wrappedValue: V {
        get { value }
        set {
            value = newValue
            value.sort(by: _areInIncreasingOrder)
        }
    }
}
// …
    @SortingBy(by: { a, b in a.age > b.age }) var people: [Person] = []
// …

someStructOrClass.people = sample
let oldestPerson = someStructOrClass.people[0]

Caveat: The way SortingBy's init currently works, you'll need to specify an initial value ([]). You can remove this requirement with an additional init (see Swift docs), but that approach is much less complicated when your property wrapper works on a concrete type (e.g. if you wrote a non-generic PersonArraySortingBy property wrapper), as opposed to a generic-on-protocols property wrapper.

Upvotes: 1

rakeshbs
rakeshbs

Reputation: 24572

It's not possible to mutate the value inside willSet. If you implement a willSet observer, it is passed the new property value as a constant parameter.


What about modifying it to use didSet?

var people:[Person] = [Person]()
{
    didSet
    {
        people.sort({ (a:Person, b:Person) -> Bool in
            return a.age > b.age
        })
    }
}

willSet is called just before the value is stored.
didSet is called immediately after the new value is stored.

You can read more about property observers here https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html

You can also write a custom getter and setter like below. But didSet seems more convenient.

var _people = [Person]()

var people: [Person] {
    get {
        return _people
    }
    set(newPeople) {
        _people = newPeople.sorted({ (a:Person, b:Person) -> Bool in
            return a.age > b.age
        })
    }

}

Upvotes: 47

Jack Lawrence
Jack Lawrence

Reputation: 10772

It is not possible to change value types (including arrays) before they are set inside of willSet. You will need to instead use a computed property and backing storage like so:

var _people = [Person]()

var people: [Person] {
    get {
        return _people
    }
    set(newPeople) {
        _people = newPeople.sorted { $0.age > $1.age }
    }
}

Upvotes: 10

Related Questions