Question re: 5.3 Function builder enhancement: let is allowed but not assignment

Previously:

var body: some View {
    let x = 123
    return VStack {
        Text("\(x)")
    }
}

Now can become:

@ViewBuilder
var body: some View {
    let x = 123
    VStack {
        Text("\(x)")
    }
}

Are these completely equivalent?

How come I cannot:

@ViewBuilder
var body: some View {
    Self.x = 123
    VStack {
        Text("\(x)")
    }
}

Where is x declared? Self.x has to refer to a variable, in this case, a static variable for whatever type Self is referring to.

x in my code is:

static let dateFormatter: DateFormatter ...

I need to set its timeZone to show the Date value in that timezone with assignment:

var body: some View {
    Self.dateFormatter.timeZone = activeTimeZone
    return VStact {
        ...
    }
}

I'm wondering why a let declaration/assignment is okay in @​ViewBuilder, but not plain assignment?

Your let is assigning a literal constant, which the compiler can optimize away. It's not clear if you were assigning something else that hasty be computed, say a constructor of a type, that you would run into the same error. Move the dateFormatter assignment outside the body computed property.

I chose to make the dateFormatter static to share one copy across all instances instead of making one per each View instance. I thought this way is better because according to the doc, formatter are expensive to create.

If I make the dateFormatter non-static, I can get rid of the assignment in body.

First, you can't reassign a constant.

Second, is this declared in some View? SwiftUI's View.self is immutable.

You haven't provided the whole source, or the error you get, which is why everyone is so confused. I guess it's something like this:

struct Foo {
    static var x = 0
    @ViewBuilder
    var body: some View {
        Self.x = 123 // Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
        VStack {
            Text("\(x)")
        }
    }
}

ViewBuilder expects that every expression is an instance of a View

foo = bar is an expression just like normal operators. Every expression in swift has to return something. Usually when you think that a function doesn't return anything, it returns Void instead. Just like 2+2 returns an Int, Self.x = 123 returns Void

Void is an empty tuple ()

Empty tuple is not a View which is why you get the error you get.


It seems you are trying to mix imperative paradigm with SwiftUI which is very declarative which is not a good idea. I would suggest to put your formatter into Environment instead, and change the time zone of it every time activeTimeZone changes

3 Likes

Note that Function Builder, and by extension ViewBuilder, is not part of the language (yet) so the behavior is not exactly stable. Which means that

  • Any explanation here may not apply in the future, and
  • What can and can’t be done may also change in the future, though chances are, you can do more, not less,

Semantically, there’s no notable a distinction between let x = ... and x = .... Both are assignment statements with special operator =, though the former also declares a new variable. So I’ll guess that they specifically allow let variable declaration to transform in a special way, which seems reasonable (remember, each line is transform by ViewBuilder).


Now on the SwiftUI side, I did say it before that you are not to mutate states inside the view (See the rant part at the end of Function Calls in Swiftui - #3 by Lantua). This applies to all objects, not just @State variable (just that it’d have annoying message specifically with @State). SwiftUI reserves its right to call body or just part of it more, or less than you’d expect.

2 Likes

I'm sorry I didn't provide full code illustration. Your guess is correct. I tried to avoid making DateFormatter per each instance of my View, instead make one shared static one:

struct  MyView: View {
    /// Expensive to create, so make a static one
    static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.setLocalizedDateFormatFromTemplate("E MMM d y")
        return formatter
    }()

    let activeTimeZone: TimeZone          /// provided by parent view
    let date: Date

    // this compile and run fine now, no warning about mutating state in body
    var body: some View {
        // No good? Should not mutate this here?
        Self.dateFormatter.timeZone = activeTimeZone
        return Text(Self.dateFormatter.string(from: date))
    }
}

My question 1: previously this:

var body: some View {
    let x = 123
    return VStack {
        Text("\(x)")
    }
}

Can become this:

@ViewBuilder
var body: some View {
    let x = 123
    VStack {
        Text("\(x)")
    }
}

Are these two exactly equivalent? I'm going through my code and wonder if it's a good idea to change them.

My 2nd question is let x = 123 is allowed in @ViewBuilder, but not simple assignment in my case:

Self.dateFormatter.timeZone = activeTimeZone

But this is not allowed per @Lantua's comment. I better not do this and not use one static shared one.

Since you want to share dataFormatter, there's a good chance you can pass it down from above, where activeTimeZone is.

An easy way would be to attach dataFormatter together with activeTimeZone (if activeTimZone is a state, make dataFormatter a state at the same scope). And update the former whenever the latter changes. Most SwiftUI-compatible state supports pattern similar to that.

1 Like

I rather not make the parent above aware of how this view format the date. I'll keep just one static as "template" and make per instance copy of it (hoping to avoid the expensive create part):

struct  MyView: View {
    /// Expensive to create, so make a static one
    static let dateFormatterTemplate: DateFormatter = {
        let formatter = DateFormatter()
        formatter.setLocalizedDateFormatFromTemplate("E MMM d y")
        return formatter
    }()

    let activeTimeZone: TimeZone          // provided by parent view
    let date: Date
    let dateFormatter: DateFormatter

    init(activeTimeZone: TimeZone, date: Date) {
        self.activeTimeZone = activeTimeZone          // needed for other things not shown in this example
        self.date = date
        // copy and configure, but no good, DateFormatter is a class :(
        self.dateFormatter = Self.dateFormatterTemplate
        self.dateFormatter.timeZone = activeTimeZone
    }


    var body: some View {
        // No need to do this anymore
        // Self.dateFormatter.timeZone = activeTimeZone
        Text(self.dateFormatter.string(from: date))
    }
}

What do you think of this approach?

I just look at DateFormatter source and it's a class...a reference type. How to make a copy of it?

You could also make a global memoized cache of formatters for specific timezones if avoiding the allocation is important for your performance. That is a special case of global state that would be acceptable to mutate in the body getter, since it's referentially transparent (i.e. it doesn't create problems if there are interleaved uses of body). If body can only be called from the UI thread (which I think is true, but I'm not certain), you don't need synchronization and can just do this:

extension DateFormatter {
  private var timeZoneCache = [TimeZone: DateFormatter]()
  public static func singleton(for timeZone: TimeZone) -> DateFormatter {
    timeZoneCache[timeZone, orInsert: {
      let formatter = DataFormatter()
      formatter.setLocalizedDateFormatterFromTemplate("E MMM d y")
      formatter.timeZone = timeZone
      return formatter
    }()]
  }
}

This unfortunately relies on a method that doesn't currently exist on Dictionary that you'd have to add:

extension Dictionary {
  public subscript(key: Key, orInsert value: @autoclosure () -> Value) -> Value {
    mutating get {
      // We have to "mutate" the result of subscript[_:default:] in order
      // to ensure an entry gets added, so we pass it inout to a function.
      return { $0 }(&self[key, default: value()])
    }
  }
}

(Credit to Nate Cook, Joe Groff, and Ben Cohen for most of the details here)

11 Likes

This would be a great alternative to have on Dictionary. I had no idea the default-taking subscript didn't store the default value.

But this will be very useful for many things.

:+1:

I just could not figure out what's going on here and why it works out.

I re-read the doc for Dictionary.subscript(key:default:), first, there is a doc error:

    ///     let message = "Hello, Elle!"
    ///     var letterCounts: [Character: Int] = [:]
    ///     for letter in message {
    ///         letterCounts[letter, defaultValue: 0] += 1    <==!!! should be default, not defaultValue
    ///     }
    ///     // letterCounts == ["H": 1, "e": 2, "l": 4, "o": 1, ...]

So this expression:

letterCounts[letter, default: 0] += 1

does different thing depending on whether letter exist or not: if exist, return just the value, if not, call default and call += 1 on that and store the result at key. What does the source code look like? I don't know how to write this myself if I need something similar.

So how does this work:

{ $0 }(&self[key, default: value()])

is it basically doing this:

dictionary[key, default: value] = { value }()

??

When you write to that subscript (setter), it works like normal subscript. The difference is when you read from it. If the value exists, it returns the value, otherwise, it evaluate closure and return the result.

So if the value exist, say letterCount[letter] == 4, it'd just return 4, get incremented (by += 1), then is assigned back to the letterCount[letter, default: 0] (remember, when writing, this works like normal subscript). In this case, you're writing 5 into the subscript.

If the value doesn't exist, it'll evaluate the default closure (0 in this case), then the value gets incremented, then assigned back to the subscript. In this case, you're writing 1 into the subscript.

This design does have advantage that the getter remains non-mutating, but I digress.

It's a neat inout trick. Let's first name the unnamed (closure).

func doTrick<T>(_ t: inout T) -> T {
  return t
}

...
return doTrick(&self[key, default: value()])

The semantic of Swift regarding its inout variable, is the copy-in-copy-out (if only I remember the source). So what happens, is that, when you call `doSomething(&x)

  • you read x
  • make a mutable copy
  • pass the copy into doSomething, and let it mutate the copy
  • *copy the new value of the copy back to x
  • *return the new value

Of course compiler can optimise away a lot of things here, but it needs to make it appear to behave like this.

So when you call it doSomething(&self[key, default: value]),

  • you read self[key, default: value], triggering the subscript getter
  • make a mutable copy
  • pass the copy into doSomething, and let it mutate (which it does nothing)
  • copy the new value of the copy back to self[key, default: value], triggering the subscript getter

The first and the last one is important, and is what we need.

You need to read from self[...], then assign to it. Because if the value doesn't exist in the dictionary, the new value is not written back (as per documentation).
So you're essentially doing this:

let copy = self[key, default: value()]
let result = copy
self[key, default: value()]
return result

in one line.

* Not sure which of the two comes first. It doesn't matter here anyway.

2 Likes

Here's how I think about it. dict[key, default: <expression>] is a "storage reference" expression. In all cases, evaluating it works with the existing entry if there is one and otherwise evaluates the default-value expression. But how that actually works out depends on what you do with the storage reference.

If you're modifying the reference, but there isn't actually an entry already in the dictionary, there's only one reasonable choice for the behavior: the default value becomes the initial value of the new entry before the modification.

If you're not modifying the reference, only reading from it, there are two reasonable choices for the behavior if there's no existing entry. In either case, what you read is the default value, but in one option you also store that value into the dictionary for the key, and in the other option you leave the dictionary alone. Storing the value into the dictionary is useful for this kind of memoizing cache, but it's bad for other use cases because it means every lookup potentially grows the dictionary, so you could end up with a trivial entry for every key you've ever looked up this way. Plus, being able to store the value into the dictionary would mean all of these accesses would be potential modifications, which has other costs and would create surprising performance and semantics differences between dict[key, default: <expression>] and dict[key] ?? <expression>. So Dictionary has instead chosen not to store the default value, which isn't as ideal for this caching use case, but ends up working out better overall.

The decision about whether the storage reference is modified is (intentionally) very local and static: Swift just looks at how the storage reference expression is immediately used, and if that falls into one of a set of specific cases that might modify the element, the whole thing is formally considered a modification. One of the things that counts for this is passing the storage reference as an inout argument. Crucially, what the program actually does dynamically isn't considered, just what might happen based on the types of things. So it doesn't matter that the {$0} closure in my code never actually assigns to its parameter, just that it takes it inout, and that's sufficient to make this a formal modification of the element, which provokes the behavior we want of caching the default value.

This is all pretty subtle reasoning, which is why it'd be best to relegate it to the details of the implementation and just have a method that does the right thing in the first place.

5 Likes

I have not encountered that term before. How does it compare with “lvalue”?

Is every subscript a “storage reference”, or is there something special which makes it so?

What about other members?

  • Stored properties
  • Computed properties
  • Instance methods
  • Type methods
  • …anything else?

It's from the ownership manifesto, which is the closest we have to a formal specification for most of the behavior of storage declarations. "lvalue" is basically the same concept from other languages.

All subscripts are storage declarations, as are var, let, parameters, case payloads, and anything we ever add in the same general category, regardless of whether they actually create new storage. (No subscript declarations create storage.) Methods are not storage declarations.

3 Likes

There is yet another wrinkle here. Localised formatters, like DateFormatter, cache information about the current locale. If you cache a formatter, you must flush that cache when the locale changes. currentLocaleDidChangeNotification is your friend.

This also applies to the formatter’s result. For example, in a traditional AppKit app, the result of the formatter might be stored in an NSTextField, and you have to refresh that when the locale changes. I’ve not tested this with SwiftUI. I suspect it’ll be less of a problem there but it’s worth thinking about.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

1 Like

I found:

Text.init(_:formatter:)

and for formatter: DateFormatter, the Text view use the timezone from Environment (so I do not need to specify the timzeone on the DateFormatter), which is just what I need:

// one copy shared among all instance
static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.setLocalizedDateFormatFromTemplate("E MMM d y")
    return formatter
}()

let timezone: TimeZone    // pass in from parent
let value: Date               // from parent
var body: some View {
    VStack {
         Text(value, Self.dateFormatter)
        // some other stuffs all specific to this TimeZone
    }
    // Text()/DatePicker() (WheelStyle) all respect this environment value (new Graphical one do not at this time)
    .environment(\.timeZone, timezone)
}

I wonder, do I still need to deal with locale switching? Yes most likely.

1 Like