Since RAC 3.0, ReactiveSwift has a clear split between mutable properties and read-only properties, as represented by MutableProperty and AnyProperty. To achieve encapsulation, the pattern of wrapping a private MutableProperty in a public AnyProperty is usually encouraged.
public class Library {
public private(set) lazy var books: Property<[Book]> = .init(self._books)
private let _books = MutableProperty<[Book]>([])
}
However, it does not interoperate well with the native Swift experience of defining stored properties with access control modifiers.
Therefore, here comes a proposal (with its caveat) to address this issue. A special thanks to @robertjpayne who had brought it up before.
The new unified Property type:
- is a
struct; but - does not have pure value semantics.
It behaves like a reference type, except with an exception to interoperate with the immutable model:
- A
Propertyinstance can have an immutable backing, or a mutable backing. - Copies of a
Propertyall refers to the same backing with one exception (rule 3). - Mutating a copy with an immutable backing would replace its backing with a mutable one that observes the original immutable one.
var books = Property<[Book]>([])
var books2 = book
let books3 = books
book2.value.append(Book())
assert(books3.value.count) == 1
Notice that:
- it is
varinstead oflet— we are taking advantage of the Swift immutable model here. - No matter how many copies are made, they all share the same backing.
let count = Property(constant: 1)
var count2 = count
count2.value += 2
This is just an optimization hint. If one mades a copy of count and mutate it, count still remains unchanged, while the copy creates a new mutable backing.
let price = viewModel.flatMap { $0.price }.map(priceFormatter.format)
var price2 = price
price2.value = "$0.00 FAKE SALE"
Similar to constant properties, mutating a copy of price would not change price in any way. The copy would create instead a new mutable backing that observes price.
class Library {
public private(set) var books = Property<[Book]>([])
}
It kinda behaves like stored properties now.
For example, create a shadow copy and mutate the copy.
func decrement(_ books: Property<[Book]>) {
var books = books
books.append(Book())
}
The local books mutation would affect the Property if the property passed in has a mutable backing. The problem can be partially offset by encouraging the use of inout for passing mutable properties:
func decrement(_ books: inout Property<[Book]>) {
books.append(Book())
}
This clarifies the intention.
The problem of implicit sharing still exists, however. We would have to educate users not to mutate properties that you do not own, and are not passed to you inout.
- Property behaviors with "out-of-band" behaviour members.
- Leave it as is.