Property Wrappers (Part II)

@Binding,@Environment,@EnvironmentObject,@State,@Published,@ObservedObject

Property Wrappers (Part II)

@Binding,@Environment,@EnvironmentObject,@State,@Published,@ObservedObject

Integrating Combine

  • Confirm to ObservableObject protocol for your custom object so that your data can be bound to a view.
  • By adding @Binding — Use a binding to create a two-way connection between a view and its underlying model.
  • By adding @Environment(property wrapper) we canpass custom objects into the Environment of a View hierarchy inside SwiftUI.
  • By adding @EnvironmentObject A dynamic view property that uses a bindable object supplied by an ancestor view to invalidate the current view whenever the bindable object changes.
  • By adding @Published (property wrapper) we can add a Publisher to any property, for example: @Published var password: String = ""
  • Using targetAction you can assign new values to the @Published value: password = sender.text ?? ""
  • CombineLatest — combines the most recently emitted values
  • using map you can modify received values and change Publisher's type
  • eraseToAnyPublisher() - returns an AnyPublisher of the given type and error
  • debounce(for:scheduler:) - lets you specify a window by which you’d like to receive values and not receive them faster than that. It is useful to reduce number of requests when filtering data using text fields
  • removeDuplicates() - Publishes only elements that don’t match the previous element

ObservableObject

protocol ObservableObject : AnyObject

ObservableObject is a protocol that’s part of the Combine framework. A observable object is a custom object for your data that can be bound to a view from storage in SwiftUI’s environment. SwiftUI watches for any changes to observable objects that could affect a view, and displays the correct version of the view after a change.

Declare a new model type that conforms to the ObservableObject protocol from the Combine framework. SwiftUI subscribes to your observable object, and updates any views that need refreshing when the data changes.

Assume we have a UserData class which conforms to ObservableObject Protocol, it has name & age variable. We have added the @Published attribute to these property in the model. An observable object needs to publish any changes to its data, so that its subscribers can pick up the change.

class UserData: 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
}
}

Now we need to adopt the Person Object in our views.

@EnvironmentObject var user: UserData

This user property gets its value automatically, as long as the environmentObject(_:) modifier has been applied to a parent.

@EnvironmentObject :-

Instead of passing ObservableObject via init method of our View we can implicitly inject it into Environment of our View hierarchy. By doing this, we create the opportunity for all child Views of current Environment access this ObservableObject.

As you can see, we have to pass UserData object via environmentObject modifier of our View. By doing this, we can easily access UserData by defining it with @EnvironmentObject Property Wrapper. @EnvironmentObject uses dynamic member lookup feature to find UserData class instance in the Environment, that’s why you don’t need to pass it via init method of View Class. The Environment is the right way of Dependency Injection in SwiftUI. It works like magic.

@Published :-

SwiftUI tracks the changes on ObservableObject with the help of @Published property wrapper, and as soon as a property marked as @Published changes SwiftUI rebuild all Views bound to that UserData Object.

@propertyWrapper struct Published<Value>

Publishing a property with the @Published attribute creates a publisher of this type. You access the publisher with the $ operator. @Published allows us to transform this property into a stream of its successive values.

Suppose we have a ViewController, we listen for UITextField updates, in the callback method we are updating a string variable named “username”. We set the value of a String property named “username” annotated with @Published. The @Published property wrapper makes $username be a Publisher that we can subscribe to.

Assume we have a label which need to update as user enters text in UITextField, we can use $username to bind with label, so that it can be updated as the username strings update.This first thing you might notice is the dollar sign in front of username. It allows you to access the wrapped Publisher value.

Important: The @Published attribute is class-constrained. Use it with properties of classes, not with non-class types like structures.

@State,@Binding :-

@State is a Property Wrapper which we can use to mark View’s state. SwiftUI will store it in special internal memory outside of View struct. Only the related View can access it. As soon as the value of @State property changes SwiftUI rebuilds View to respect state changes.

@Binding provides reference like access for a value type. Sometimes we need to make the state of our View accessible for its children. But we can’t simply pass that value because it is a value type and Swift will pass the copy of that value. And this is where we can use @Binding Property Wrapper.

struct FavouriteView: View {
@Binding var showFavorited: Bool
    var body: some View {
Toggle(isOn: $showFavorited) {
Text("Change favourite")
}
}
}
struct UserView: View {
let users: [User]
    @State private var showFavorited: Bool = false
var body: some View {
List {
FavouriteView(showFavorited: $showFavorited)
            ForEach(users) { user in
if !self.showFavorited || user.isFavorited {
Text(user.title)
}
}
}
}
}

We use @Binding to mark showFavorited property inside the FavouriteView. We also use $ to pass a binding reference, because without $ Swift will pass a copy of the value instead of passing bindable reference. FavouriteView can read and write the value of UserView’s showFavorited property, but it can’t observe the changes using this binding. As soon as FavouriteView changes value of showFavorited property, SwiftUI will recreate the UserView and FavouriteView as its child.

@ObservedObject

@ObservedObject work very similarly to @State Property Wrapper, but the main difference is that we can share it between multiple independent Views which can subscribe and observe changes on that object, and as soon as changes appear SwiftUI rebuilds all Views bound to this object. Let’s take a look at an example.

import Combine

final class MusicPlayer: ObservableObject {
@Published private(set) var isPlaying: Bool = false

func play() {
isPlaying = true
}

func pause() {
isPlaying = false
}
}

Here we have MusicPlayer class which we share between the screens of our app. Every screen has to show floating pause button in the case when the app is playing a TV series episode. SwiftUI tracks the changes on ObservableObject with the help of @Published property wrapper, and as soon as a property marked as @Published changes SwiftUI rebuild all Views bound to that MusicPlayer object. Here we use @ObservedObject Property Wrapper to bind our TVSeriesView to MusicPlayer class

struct TVSeriesView: View {
@ObservedObject var player: MusicPlayer
let episodes: [Episode]

var body: some View {
List {
Button(
action: {
if self.player.isPlaying {
self.player.pause()
} else {
self.player.play()
}
}, label: {
Text(player.isPlaying ? "Pause": "Play")
}
)
ForEach(episodes) { episode in
Text(episode.title)
}
}
}
}

@Environment

We can pass custom objects into the Environment of a View hierarchy inside SwiftUI. But SwiftUI already has an Environment populated with system-wide settings. We can easily access them with @Environment Property Wrapper.

struct MyCalendarView: View {
@Environment(\.calendar) var calendar: Calendar
@Environment(\.locale) var locale: Locale
@Environment(\.colorScheme) var colorScheme: ColorScheme
var body: some View {
return Text(locale.identifier)
}
}

By marking our properties with @Environment Property Wrapper, we access and subscribe to changes of system-wide settings. As soon as Locale, Calendar or ColorScheme of the system change, SwiftUI recreates our MyCalendarView.

Hope this article is useful for iOS developers, Please ❤️ to recommend this post to others 😊. Let me know your feedback. :)

Reference:

  1. https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md
  2. https://developer.apple.com/documentation/swiftui