Building the user interface of an app constitutes a task that requires significant amount of time and effort; it has to be providing the best user experience, and be unique and original at the same time. There is no doubt that SwiftUI framework has come to make that process much easier and fun comparing to UIKit. Although most of the work with it is usually fast and straightforward, there are times that things might not be that obvious.
One of those things is the topic of this post. Quite often we need to trigger additional actions or update the user interface, but only after a change to a property’s value has taken place. Sometimes that’s the result of user actions, some other times it’s the result of internal processes in the app. No matter where changes come from, the fact remains the same; it’s necessary to be able to perform actions when property values change in SwiftUI views.
But since we are talking about SwiftUI, we all know that besides views there also the view modifiers; and in this case, a specific view modifier is all we need since iOS 14 and above; the onChange(_:)
.
Understanding onChange with an example
Let’s suppose that we want to create a view in a SwiftUI-based app in order to allow users to write a short review. For the sake of the example let’s set the limit of the allowed characters to 150. The bare minimum implementation for that requires a label to prompt users, and a text editor. The typed text is hold in a property marked with the @State
property wrapper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct ContentView: View { @State private var review = “” var body: some View { VStack { Label(“Write a short review:”, systemImage: “pencil.circle”) .font(.title3) TextEditor(text: $review) .frame(maxHeight: 150) .border(Color.gray, width: 0.5) } .padding(.horizontal) } } |
Even though the above presents the text editor where users can type in, there is an actual problem that is not solved by the above implementation. How can we make it possible to prevent users from typing more than 150 characters in the text editor?
In addition to that, our view is not so user-friendly right now. To change that, let’s make the decision to add two more views so users are aware of how far they’ve gone with writing; a progress view and a text view indicating the number of typed characters.
Both of the above have the same requirement; it’s necessary to know the length of the typed text. Before we get to see how to solve that, let’s update the initial code so it displays a progress and a text view as well:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
struct ContentView: View { … @State private var progress: Double = 0 var body: some View { VStack { … ProgressView(value: progress, total: 150) .padding(.top, 20) .accentColor(.pink) Text(“\(review.count) / 150″) .fontWeight(.medium) } .padding(.horizontal) } } |
progress
is another state property which holds the writing progress value as a double number. It’s given as argument to the progress view.
To the actual point of the post now, what we really need is to be notified whenever the value of the review
property gets changed. In order to achieve that, it’s necessary to use the onChange(_:)
view modifier, which we’ll apply to the text editor like so:
1 2 3 4 5 6 |
TextEditor(text: $review) .onChange(of: review) { _ in } |
There are two things to do in the above closure:
- To assign the length of the review to the
progress
property as a double value. - To check if the length of the review is more than 150 characters or not. In the former case, we’ll just drop the last characters that exceed the limit.
Converting the two points above into code is shown right next:
1 2 3 4 5 6 7 8 9 |
.onChange(of: review) { _ in progress = Double(review.count) if review.count > 150 { review = String(review.dropLast(review.count – 150)) } } |
This addition to the code is good enough to make our view work properly, and be user-friendly at the same time:
Old and new values
Although it’s not necessary in the above example, it’s often needed to know both the old and the new value of the property we are observing for changes. The closure of the onChange(_:)
provides them both like so:
1 2 3 4 5 |
.onChange(of: review) { [review] newValue in } |
The previous value of the review
property is captured by the closure -the [review]
value-, while the new one is given as argument -the newValue
-.
Notice that:
- the old value in the capture list must have the name of the property, while you can name the new value whatever you want, and,
- accessing the actual property from within the closure is now possible with the
self
keyword (self.review
).
As said, knowing both the previous and the new value of a property that gets changed can be vital in various cases. Therefore, keep the above in mind if you ever need to access them both.
Conclusion
The onChange(_:)
view modifier is a quite important one, as it’s the way to go in order to observe for changes in property values. Use it as the event that will trigger further actions in your app, and update the user interface if necessary. The example presented above was thorough enough in order to showcase how this modifier works. Thank you for reading, stay safe and take care.