Property Wrappers
Property wrappers are a Swift 5.1 language feature that gives you the ability to defined a wrapped
property. I’ll examine
some common property wrappers used in SwiftUI/Combine and also describe how to create your own property wrappers. Some common
property wrapers include
@Published
@State
@Binding
@ObservedObject
@EnvironmentObject
@Published
This struct wraps a value and provides access to a publisher that is backed by an internal Subject
. The definition is
/// Adds a `Publisher` to a property.
///
/// Properties annotated with `@Published` contain both the stored value and a publisher which sends any new values after the property value has been sent. New subscribers will receive the current value of the property first.
/// Note that the `@Published` property is class-constrained. Use it with properties of classes, not with non-class types like structures.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct Published<Value> {
/// Initialize the storage of the Published property as well as the corresponding `Publisher`.
public init(initialValue: Value)
/// A publisher for properties marked with the `@Published` attribute.
public struct Publisher : Publisher {
/// The kind of values published by this publisher.
public typealias Output = Value
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Never
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where Value == S.Input, S : Subscriber, S.Failure == Published<Value>.Publisher.Failure
}
/// The property that can be accessed with the `$` syntax and allows access to the `Publisher`
public var projectedValue: Published<Value>.Publisher { mutating get }
}
The struct contains an internal structure which is of type Publisher
and it also contains a projectedValue
property that
returns the internal struct Published<Value>.Publisher
To define an @Published
property just prefix the property defintion with @Published
@Published var players: [Player] = []
To get/set the value just reference the propery normally
let x: [Players] = players
players = [ ... ]
To obtain the internal Subject as a Publisher, use the $
syntax (which accesses the projectedValue
).
let playersPublisher: Published<[Player]>.Publisher = self.viewModel.$players
playersPublisher
.print("self.frc.objectsPublisher")
.sink{ players in
players.forEach{ print("----> \($0)")}
}
.store(in: &cancellableSet)
I use the $
syntax to obtain the internal struct Publisher
which I can then subscribe to via sink
We could imagine that the @Published
property wrapper replaces this
final class ContentViewModel: ObservableObject {
// Would be replaced by @Published var testText: String = ""
private let testTextPropertySubject = PassthroughSubject<String, Never>()
var testText : String = "" {
willSet {
self.objectWillChange.send()
}
didSet {
self.testTextPropertySubject.send(oldValue)
}
}
var testTextPublisher: AnyPublisher<String, Never> {
get { self.testTextPropertySubject.eraseToAnyPublisher() }
}
}
I would think that SwiftUI would inspect all @ObservedObject
wrappers and subscribe to their objectWillChange
publishers. If any events are fired, evaluate all views that depend on any @Published
properties of the observed object and refresh the views if necessary.
You may find that
objectWillChange
fires multiple times but no views are refreshed. This is normal SwiftUI behavior.
To access different parts of the property wrapper
@Published var test: Int = 0
let y: Int = self.test
let x: Published<Int> = self._test
let z: Published<Int>.Publisher = self.$test
The
$
accesses theprojectedValue
of the property wrapper while_
retrieves the instance of the property wrapper itself.
@State
This property wrapper is intended to be used within SwiftUI views to track view state. It is defined as
/// A linked View property that instantiates a persistent state
/// value of type `Value`, allowing the view to read and update its
/// value.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct State<Value> : DynamicProperty {
/// Initialize with the provided initial value.
public init(wrappedValue value: Value)
/// Initialize with the provided initial value.
public init(initialValue value: Value)
/// The current state value.
public var wrappedValue: Value { get nonmutating set }
/// Produces the binding referencing this state value
public var projectedValue: Binding<Value> { get }
}
To define a @State
property just prefix the property defintion with @State
@State private var showPopover: Bool = false
It is suggested that
@State
properties are markedprivate
, only the current view should have access to its@State
properties
To get/set the value just reference the propery normally
.blur(isBlurred: self.showPopover, radius: 2.0) // conditionally blur
self.showPopover = true
To obtain the binding for the state Binding<Value>
, use the $
syntax (which accesses the projectedValue
).
let viewModel = PopoverViewModel<Player>(showOverlay: self.$showPopover)
@State private var type = 0
let x: Int = type
let y: Binding<Int> = $type
let z: State<Int> = _type
@Binding
This property wrapper is used to provide bi-directional binding to @State
properties. If view A contains an @State
property that it wants to provide read/write access to view B, view A would pass the @State
property to view B as a Bindinb<Value>
type. This type is accessed by applying the $
syntax to the @State
property. The defintion is
/// A value and a means to mutate it.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper @dynamicMemberLookup public struct Binding<Value> {
/// The transaction used for any changes to the binding's value.
public var transaction: Transaction
/// Initializes from functions to read and write the value.
public init(get: @escaping () -> Value, set: @escaping (Value) -> Void)
/// Initializes from functions to read and write the value.
public init(get: @escaping () -> Value, set: @escaping (Value, Transaction) -> Void)
/// Creates a binding with an immutable `value`.
public static func constant(_ value: Value) -> Binding<Value>
/// The value referenced by the binding. Assignments to the value
/// will be immediately visible on reading (assuming the binding
/// represents a mutable location), but the view changes they cause
/// may be processed asynchronously to the assignment.
public var wrappedValue: Value { get nonmutating set }
/// The binding value, as "unwrapped" by accessing `$foo` on a `@Binding` property.
public var projectedValue: Binding<Value> { get }
/// Creates a new `Binding` focused on `Subject` using a key path.
public subscript<Subject>(dynamicMember keyPath: WritableKeyPath<Value, Subject>) -> Binding<Subject> { get }
}
To define a @Binding
property just prefix the property defintion with @Binding
@Binding var showOverlay: Bool
To get/set the value just reference the propery normally
.onTapGesture { self.viewModel.showOverlay = false }
setting the property will update the @State
property that is associated with the binding (updating the @Binding
property
on view B will update the @State
property on view A).
To access the projectedValue
use the $
syntax
let x: Binding<Bool> = self.viewModel.$showOverlay
@Binding var addViewDisplayed: Bool
let y: Bool = self.viewModel.addViewDisplayed
let t: Binding<Bool> = self.viewModel.$addViewDisplayed
@ObservedObject
This wrapper is used to define properties of objects that adopt the ObservableObject
protocol. It is mainly used to
invalidate the view when the property changes. This is key to getting views to refresh from updates to @Published
properties on objects wrapped by @ObservedObject
. The definition is
/// A dynamic view property that subscribes to a `ObservableObject` automatically invalidating the view
/// when it changes.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct ObservedObject<ObjectType> : DynamicProperty where ObjectType : ObservableObject {
/// A wrapper of the underlying `ObservableObject` that can create
/// `Binding`s to its properties using dynamic member lookup.
@dynamicMemberLookup public struct Wrapper {
/// Creates a `Binding` to a value semantic property of a
/// reference type.
///
/// If `Value` is not value semantic, the updating behavior for
/// any views that make use of the resulting `Binding` is
/// unspecified.
public subscript<Subject>(dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>) -> Binding<Subject> { get }
}
public init(initialValue: ObjectType)
public init(wrappedValue: ObjectType)
public var wrappedValue: ObjectType
public var projectedValue: ObservedObject<ObjectType>.Wrapper { get }
}
To define an @ObservedObject
property just prefix the property definition with @ObservedObject
@ObservedObject private var viewModel: ContentViewModel
If the ContentViewModel
adopts ObservableObject
AND contains any @Published
properties, whenever any @Published
property changes, any views that reference the @Published
properties will refresh themselves. The ObservableObject
protocol is defined as
/// A type of object with a publisher that emits before the object has changed.
///
/// By default an `ObservableObject` will synthesize an `objectWillChange`
/// publisher that emits before any of its `@Published` properties changes:
///
/// class Contact: ObservableObject {
/// @Published var name: String
/// @Published var age: Int
///
/// init(name: String, age: Int) {
/// self.name = name
/// self.age = age
/// }
///
/// func haveBirthday() -> Int {
/// age += 1
/// return age
/// }
/// }
///
/// let john = Contact(name: "John Appleseed", age: 24)
/// john.objectWillChange.sink { _ in print("\(john.age) will change") }
/// print(john.haveBirthday())
/// // Prints "24 will change"
/// // Prints "25"
///
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ObservableObject : AnyObject {
/// The type of publisher that emits before the object has changed.
associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never
/// A publisher that emits before the object has changed.
var objectWillChange: Self.ObjectWillChangePublisher { get }
}
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher {
/// A publisher that emits before the object has changed.
public var objectWillChange: ObservableObjectPublisher { get }
}
It uses an ObservableObjectPublisher
/// The default publisher of an `ObservableObject`.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
final public class ObservableObjectPublisher : Publisher {
/// The kind of values published by this publisher.
public typealias Output = Void
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Never
public init()
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
final public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == ObservableObjectPublisher.Failure, S.Input == ObservableObjectPublisher.Output
final public func send()
}
Adopters of this protocol will raise an event BEFORE any of the @Published
objects will change. This is possible because behind the scenes we have something like this:
class Observable<T>: ObservableObject, Identifiable {
let id = UUID()
let publisher = PassthroughSubject<T, Never>()
var value: T {
willSet { objectWillChange.send() } // provided automatically by adopting ObservableObject
didSet { publisher.send(value) }
}
init(_ initValue: T) { self.value = initValue }
}
Internally the @Published
property wrapper contains a subject. When the proerty is set the willSet
will notify the
class it is contained in via a call to objectWillChange.send()
. It also updates its own internal subject in the didSet
call.
final class ContentViewModel: ObservableObject {
// list of players
@Published var players: [Player] = []
}
The ContentViewModel
has an objectWillChange
property public var objectWillChange: ObservableObjectPublisher { get }
.
When the @Published var players
willSet
is called, the ContentViewModel.objectWillChange.send()
function is called. This function will notifiy all dependent views to invalidate and refresh themselves. Internally the didSet
property observer will update the value & subject within the @Published var players
property.
Manually pushing updates
The property wrappers are a nice way to remove bolierplate code from you application. Given what we know, we could do all the updates by doing the following
final class ContentViewModel: ObservableObject {
// Would be replaced by @Published var testText: String = ""
private let testTextPropertySubject = PassthroughSubject<String, Never>()
var testText : String = "" {
willSet {
self.objectWillChange.send()
}
didSet {
self.testTextPropertySubject.send(oldValue)
}
}
var testTextPublisher: AnyPublisher<String, Never> {
get { self.testTextPropertySubject.eraseToAnyPublisher() }
}
init() {
self.testText = "Phil"
}
func addPlayer(name: String) {
player.name = name
let msg = "Added \(player.name)"
print(msg)
self.testText = msg
}
}
Simple view model with an exposed String property, a publisher and an addPlayer() function.
struct ContentView: View {
@ObservedObject private var viewModel: ContentViewModel
init() {
self.viewModel.objectWillChange
.print("PAK: objectWillChange")
.sink{ _ in
print("PAK: objectWillChange event")
}
.store(in: &cancellableSet)
self.viewModel.testTextPublisher
.print("PAK: testTextPublisher")
.sink{
print("PAK: testText event: \($0)")
}
.store(in: &cancellableSet)
}
var body: some View {
Text("Test Text: \(self.viewModel.testText)")
}
}
Using the objectWillChange
publisher on the view model we can get notifications when the view model changes (i.e. any
@Published properties) in response to changing the testText
@FetchRequest
Used to fetch data from CoreData.
/// Property wrapper to help Core Data clients drive views from the results of
/// a fetch request. The managed object context used by the fetch request and
/// its results is provided by @Environment(\.managedObjectContext).
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct FetchRequest<Result> : DynamicProperty where Result : NSFetchRequestResult {
/// The current collection of fetched results.
public var wrappedValue: FetchedResults<Result> { get }
/// Creates an instance by defining a fetch request based on the parameters.
/// - Parameters:
/// - entity: The kind of modeled object to fetch.
/// - sortDescriptors: An array of sort descriptors defines the sort
/// order of the fetched results.
/// - predicate: An NSPredicate defines a filter for the fetched results.
/// - animation: The animation used for any changes to the fetched
/// results.
public init(entity: NSEntityDescription, sortDescriptors: [NSSortDescriptor], predicate: NSPredicate? = nil, animation: Animation? = nil)
/// Creates an instance from a fetch request.
/// - Parameters:
/// - fetchRequest: The request used to produce the fetched results.
/// - animation: The animation used for any changes to the fetched
/// results.
public init(fetchRequest: NSFetchRequest<Result>, animation: Animation? = nil)
/// Creates an instance from a fetch request.
/// - Parameters:
/// - fetchRequest: The request used to produce the fetched results.
/// - transaction: The transaction used for any changes to the fetched
/// results.
public init(fetchRequest: NSFetchRequest<Result>, transaction: Transaction)
/// Called immediately before the view's body() function is
/// executed, after updating the values of any dynamic properties
/// stored in `self`.
public mutating func update()
}
https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-a-core-data-fetch-request-using-fetchrequest