Swift Property Observers
source link: https://www.tuicool.com/articles/hit/jaMreeu
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
By the 1930’s, Rube Goldberg had become a household name, synonymous with the fantastically complicated and whimsical inventions depicted in comic strips like “Self-Operating Napkin.” Around the same time, Albert Einstein popularized the phrase “spooky action at a distance” in his critique of the prevailing interpretation of quantum mechanics by Niels Bohr.
Nearly a century later, modern software development has become what might be seen as the quintessence of a Goldbergian contraption — sprawling ever closer into that spooky realm by way of quantum computers.
As software developers, we’re encouraged to reduce action-at-a-distance in our code whenever possible. This is codified in impressive-sounding guidelines like the Single Responsibility Principle , Principle of Least Astonishment , and Law of Demeter . Yet despite their misgivings about code that produces side effects, there are sometimes occasions where such techniques may clarify rather than confound.
Such is the focus of this week’s article about property observers in Swift, which offer a built-in, lightweight alternative to more formalized solutions like model-view-viewmodel (MVVM) functional reactive programming (FRP).
There are two kinds of properties in Swift: stored properties , which associate state with an object, and computed properties , which perform a calculation based on that state. For example,
struct S { // Stored Property var stored: String = "stored" // Computed Property var computed: String { return "computed" } }
When you declare a stored property,
you have the option to define property observers
with blocks of code to be executed when a property is set.
The willSet
observer runs before the new value is stored
and the didSet
observer runs after.
And they run regardless of whether the old value is equal to the new value.
A major caveat is that observers don’t run
when you set a property in an initializer
(however you can
force this behavior, but more on that later).
struct S { var stored: String { willSet { print("willSet was called") print("stored is now equal to \(self.stored)") print("stored will be set to \(newValue)") } didSet { print("stored is now equal to \(self.stored)") print("stored was previously set to \(oldValue)") } } }
For example, running the following code prints the resulting text to the console:
var s = S(stored: "first") s.stored = "second"
- willSet was called
- stored is now equal to first
- stored will be set to second
- didSet was called
- stored is now equal to second
- stored was previously set to first
Swift property observers have been part of the language from the very beginning. To better understand why, let’s take a quick look at how things work in Objective-C:
Properties in Objective-C
In Objective-C, all properties are, in a sense, computed. Each time a property is accessed through dot notation, the call is translated into an equivalent getter or setter method invocation. This, in turn, is compiled into a message send that executes a function that reads or writes an instance variable.
// Dot accessor person.name = @"Johnny"; // ...is equivalent to [person setName:@"Johnny"]; // ...which gets compiled to objc_msgSend(person, @selector(setName:), @"Johnny"); // ...whose synthesized implementation yields person->_name = @"Johnny";
Side effects are something you generally want to avoid in programming because they make it difficult to reason about program behavior. But many Objective-C developers had come to rely on the ability to inject additional behavior into getter or setter methods as needed.
Swift’s design for properties formalized these patterns
and created a distinction between side effects
that decorate state access (stored properties)
and those that redirect state access (computed properties).
For stored properties, the willSet
and didSet
observers
replace the code that you’d otherwise include alongside ivar access.
For computed properties, the get
and set
accessors
replace code that you might implement for @dynamic
properties in Objective-C.
As a result, we get more consistent semantics and better guarantees about mechanisms like Key-Value Observing (KVO) and Key-Value Coding (KVC) that interact with properties.
So what can you do with property observers in Swift? Here are a couple ideas for your consideration:
Validating / Normalizing Values
Sometimes you want to impose additional constraints on what values are acceptable for a type.
For example, if you were developing an app that interfaced with a government bureaucracy, you’d need to ensure that the user wouldn’t be able to submit a form if it was missing a required field, or contained an invalid value.
If, say,
a form required that names use capital letters without accents,
you could use the didSet
property observer
to automatically strip diacritics and uppercase the new value:
var name: String? { didSet { self.name = self.name? .applyingTransform(.stripDiacritics, reverse: false)? .uppercased() } }
Setting a property in the body of an observer (fortunately)
doesn’t trigger additional callbacks,
so we don’t create an infinite loop here.
This is the same reason why this won’t work as a willSet
observer;
any value set in the callback is immediately overwritten
when the property is set to its newValue
.
While this approach can work for one-off problems, repeat use like this is a strong indicator of business logic that could be formalized in a type.
A better design would be to create a NormalizedText
type
that encapsulates the requirements of text to be entered in such a form:
struct NormalizedText { enum Error: Swift.Error { case empty case excessiveLength case unsupportedCharacters } static let maximumLength = 32 var value: String init(_ string: String) throws { if string.isEmpty { throw Error.empty } guard let value = string.applyingTransform(.stripDiacritics, reverse: false)? .uppercased(), value.canBeConverted(to: .ascii) else { throw Error.unsupportedCharacters } guard value.count < NormalizedText.maximumLength else { throw Error.excessiveLength } self.value = value } }
A failable or throwing initializer
can surface errors to the caller
in a way that a didSet
observer can’t.
Now, when a troublemaker like Jøhnny
from
Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
comes a’knocking,
we can give him what’s for!
(Which is to say,
communicate errors to him in a reasonable manner
rather than failing silently or allowing invalid data)
Propagating Dependent State
Another potential use case for property observers is propagating state to dependent components in a view controller.
Consider the following example of a Track
model
and a TrackViewController
that presents it:
struct Track { var title: String var audioURL: URL } class TrackViewController: UIViewController { var player: AVPlayer? var track: Track? { willSet { self.player?.pause() } didSet { self.title = self.track.title let item = AVPlayerItem(url: self.track.audioURL) self.player = AVPlayer(playerItem: item) self.player?.play() } } }
When the track
property of the view controller is set,
the following happens automatically:
title
Pretty cool, right?
You could even cascade this behavior across multiple observed properties a la that one scene from Mousehunt .
One major shortcoming of this approach is that property observers
aren’t called during initialization…
at least not normally.
But you can force a property’s observer to run
by wrapping it in a
defer
statement:
init(track: Track) { super.init() defer { self.track = track } }
Another limitation of property observers
is that they don’t trigger for any internal state changes.
For example,
if we were to call self.track.title = "All Star"
from our view controller,
there wouldn’t be any noticeable changes.
If you want to support something like this, you can use KVO or your favorite functional reactive library instead.
As a general rule, side effects are something to avoid when programming, because they make it difficult to reason about complex behavior. Keep that in mind the next time you reach for this new tool.
And yet, from the tippy top of this teetering tower of abstraction, it can be tempting — and perhaps sometimes rewarding — to embrace the chaos of the system. Always following the rules is such a Bohr .
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK