Sometime, somewhere, I stumbled across a hint about SwiftUI that I had been stumped about. Consider this example:
|
1 2 3 4 5 6 |
Group { Text("All text is displayed") Text("In the same font") Text("Because the group is modified") } .font(.title) |
This is great, and very easy to figure out. But how the heck does this work:
|
1 2 3 4 5 |
Group { Text("This text is displayed") Rectangle() // <-- how do you set the font for a rectangle? } .font(.title) |
My naive approach involved creating a modifier, which applied a method to each view type I wanted to modify. That seemed like the right thing to do. But then it caused problems when I tried to apply it to *all* view types. It is possible to extend the View protocol to give a default implementation of a custom modifier method, but in the end, this made all my views run that default implementation instead of any custom implementation that class may adopt. And further, how does each view do the kind of introspection necessary for the Group to apply the Font modifier to the objects inside the Group?
It turns out that SwiftUI uses an EnvironmentValue in order to store the default font for any view in a given hierarchy.
https://developer.apple.com/documentation/swiftui/environmentvalues/font
Adopting this pattern made it easier to send information upward throughout the hierarchy of my framework MusicStaffView. For example:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
ZStack { StaffShapeView(ledgerLines: maxLedgerLines) .mask { StaffMask() } HStack { clef ForEach(elements.map { AnyMusicStaffViewElement($0) }) { element in element.body } } } .spaceWidth(self.spaceWidth(in: g)) .clef(self.clef) |
Elements in the ZStack (including the StaffShapeView, which draws the musical staff lines) depend heavily on the vertical space between the staff lines. This is how I developed a better method of resolution independence when the framework moved from rendering images to drawing paths, and it remains a core feature of MusicStaffView today. But it requires that the width of the spaces is computed before the drawing takes place. In UIKit and Cocoa, this was easy, because the rendering happened in the draw() method for a custom subclass of UIView and NSView.
In reality, SwiftUI is calling similar methods to draw the shapes of the notes and other elements, but now it relies on the environment to describe how big to make each of the elements, as well as what their offsets are (i.e. a given note is offset from the center of the staff view by a certain distance depending on which note is being represented). In this way, it can be described by the GeometryProxy in the MusicStaffView itself and propagated to the rest of the views in the staff view, allowing them to draw in the correct size.
