By Mattt, translator: Hale; Proofreading: Numbbbbb, PMST, Yousanflics; Finalized: Forelax

By the 1930s, Rube Goldberg had become a household name, synonymous with bizarrely complex and whimsical inventions depicted in comics like “Proprietary napkins.” Around the same time, Albert Einstein was critical of Niels Bohr’s popular interpretation of quantum mechanics, from which he coined the term “spooky tele-action.”

Nearly a century later, modern software development has come to be seen as a model for what could become a Goldbergian device — the spooky realm that quantum computers believe we’re ever getting closer to.

As software developers, we advocate minimizing remote operations in our code. This is based on well-known normative principles such as the single responsibility principle, the least accidents principle, and The Demeter rule. Although they may have some side effects on the code, more often than not these principles make the code logic clear.

This is the focus of this week’s Swift attributes observation, which proposes a built-in lightweight alternative for more formal solutions such as Model-View-View model (MVVM) functional Responsive programming (FRP).

There are two types of properties in Swift: storage properties, which associate states with objects; Compute property, and perform a calculation based on that state. For example,

struct S {
    // Store attributes
    var stored: String = "stored"

    // Calculate attributes
    var computed: String {
        return "computed"}}Copy the code

When you declare a stored property, you can define a property observer using a closure that executes the code when the property is set. The willSet observer is run before the property is assigned a new value, and the didSet observer is run after the property is assigned a new value. They are executed regardless of whether the new value is equal to the old value of the property.

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("didSet was called")
            print("stored is now equal to \ [self.stored)")
            print("stored was previously set to \(oldValue)")}}}Copy the code

For example, running the following code on the console produces the following output:

var s = S(stored: "first")
s.stored = "second"
Copy the code
  • 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

Note that the observer code does not fire when the property is assigned in the initialization method. Starting with Swift4.2, you can fix this by wrapping the assignment logic in the defer block, but this is a problem that will be fixed soon, so you don’t need to rely on this behavior.

Swift’s attribute viewer has been part of the language since the beginning. To better understand how it works, let’s take a quick look at how it works in Objective-C.

Properties in Objective-C

In some sense, all properties in Objective-C are computed. Each time a property is accessed through dot syntax, it is converted to an equivalent getter or setter method call. These calls are eventually compiled to send messages, followed by methods that read or write instance variables.

// click syntax access
person.name = @"Johnny";

/ /... Is equivalent to
[person setName:@"Johnny"];

/ /... It's compiled into
objc_msgSend(person, @selector(setName:), @"Johnny");

/ /... Finally realize
person->_name = @"Johnny";
Copy the code

We usually want to avoid introducing side effects during programming because they make it difficult to infer the behavior of the program. But many Objective-C developers already rely on this feature, injecting all sorts of extra behavior into getters or setters as needed.

Swift’s attribute design standardizes these patterns and distinguishes between the side effects of decorating state access (storing properties) and redirecting state access (calculating properties). For storage properties, willSet and didSet observers will replace the code you used when you accessed ivar. For computed properties, get and set accessors may replace some of the @dynamic properties implemented in Objective-C.

Because of this, we can achieve more consistent semantics and better guarantee attribute interaction mechanisms such as key-value observation (KVO) and key-value coding (KVC).

So what can you do with the Swift attribute viewer? Here are some ideas for you to consider:

Standardize or validate values

Sometimes you want to impose additional constraints on values that are typed.

For example, if you are developing an application to interact with a government agency, you need to make sure that the user has filled out all the required fields and does not contain illegal values before submitting the form.

If a form requires the name field to be uppercase and not accented, you can use the didSet property viewer to automatically remove the accented character and convert to uppercase.

var name: String? {
    didSet {
        self.name = self.name?
                        .applyingTransform(.stripDiacritics,
                                            reverse: false)?
                        .uppercased()
    }
}
Copy the code

Fortunately, setting properties inside the observer does not trigger additional callbacks, so there is no infinite loop in the code above. We don’t use the willSet observer because even if we do any assignment in its callback, it will be overridden when the property is given newValue.

While this approach solves the one-time problem, business logic that requires repeated use like this can be encapsulated in a type.

A better design would be to create a NormalizedText type that encapsulates the rules for text to be entered in this 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
    }
}
Copy the code

An initialization method that throws an exception can send an error message to the caller, something the didSet observer cannot do. Now what can we do for a troublemaker like Johnny of Ranvelpur Guingirgorgli Huir Ndrobl Rantisilio Gogoh! (In other words, it’s better to communicate errors in a reasonable way than to provide invalid data.)

Propagation dependent state

Another potential use case for property observers is propagating state to components that depend on view controllers.

Consider the following example of a Track model and a TrackViewController that renders it:

struct Track {
    var title: String
    var audioURL: URL
}

class TrackViewController: UIViewController {
    var player: AVPlayer?

    var track: Track? {
        willSet {
            self.player? .pause() }didSet {
            guard let track = self.track else {
                return
            }

            self.title = track.title

            let item = AVPlayerItem(url: track.audioURL)
            self.player = AVPlayer(playerItem: item)
            self.player? .play() } } }Copy the code

When the track property of the view controller is assigned, the following automatically happens:

  • The audio from the previous tracks will be suspended
  • View controller’stitleIs set to the title of the new track object
  • The audio message for the new track object is loaded and played

Cool, right?

You can even cascade behavior with multiple observed attributes, as depicted in the Rat Hunt.

Of course, observers also have the side effect of making some complex behaviors harder to infer, something we need to avoid in programming. This also needs to be taken care of when using this feature in the future.

At the top of this crumbling tower of abstraction, however, a certain amount of systemic chaos is tempting and sometimes worthwhile. It was Bohr’s theory, not Einstein’s, that followed the rules.

This article is translated by SwiftGG translation team and has been authorized to be translated by the authors. Please visit swift.gg for the latest articles.