I am still struggling to understand how view modifiers work in SwiftUI.
As far as I know modifiers do not directly change the corresponding view but instead embed the view within a new view which applies the desired changes. For example Text(...).padding()
does NOT change some properties of the Text
view to create the padding but instead embeds the Text
view inside a new view which takes care of the paddings. So modifiers do not modify the view but create a new view with the desired changes.
OK, this marks perfect sense.
But how do modifiers work, which (seem to) change only specific properties of a view? For example the .font
or .tint
modifierers. While I understand how a surrounding view can change "external" properties like background, padding, scale, etc. I do not understand how the same is possible for "internal" properties like the font? How can the Text
view know from its surrounding view, which font it should use? Or how could a surrounding view re-create the Text
view with all its properties but only a changed font?
Could I create a custom Text("Some example").capitalize("aoe")
which creates a Text with all the same properties like font, foreground color, etc. but with "SOmE ExAmplE"?
It's unclear what other view modifiers that you consider as "changes internal properties", so I will be only considering font
and tint
. Other view modifiers may be implemented differently.
Both font
and tint
are implemented by setting environment values. Both of these are inlinable, so their implementations can be found in the .swiftinterface files of SwiftUI.
// from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/SwiftUICore.framework/Modules/SwiftUICore.swiftmodule/arm64e-apple-macos.swiftinterface
@inlinable nonisolated public func tint<S>(_ tint: S?) -> some SwiftUICore.View where S : SwiftUICore.ShapeStyle {
return environment(\.tint, tint.map(AnyShapeStyle.init))
}
@inlinable nonisolated public func font(_ font: SwiftUICore.Font?) -> some SwiftUICore.View {
return environment(\.font, font)
}
\.font
here is the public font
environment value, but \.tint
refers to an internal
environment value, so you cannot use that in your own code.
So views like Text
is just reading the environment value and changing its appearance accordingly.
Even without looking at the .swiftinterface files, you can often infer how something is implemented by print
ing or dump
ing a view with that modifier. e.g.
print(Text("").tint(.red))
The output mentions a _EnvironmentKeyWritingModifier
, so it can be inferred that it is modifying the environment.
Note that there is another modifier called font
that returns a Text
(as opposed to some View
). This modifier is declared in Text
itself, so it can just create a copy of self
, modify some properties, then return the modified copy.
So the reason why font
/tint
can change the "internals" of a view is because those views are reading the environment values the modifiers set. There is no magic. You cannot create such a capitalize
modifier in the same way (by setting your own environment value), because Text
does not read your custom environment value.
Of course, you can always create your own view that reads the environment value:
struct ContentView: View {
var body: some View {
CapitalizableText("Some example").capitalize("aoe")
}
}
import RegexBuilder
struct CapitalizableText: View {
let text: String
@Environment(\.capitalizedCharacters) var capChars
init(_ text: String) {
self.text = text
}
var body: some View {
Text(capitalized())
}
func capitalized() -> String {
text.replacing(.anyOf(capChars)) { match in
match.output.localizedUppercase
}
}
}
extension EnvironmentValues {
@Entry var capitalizedCharacters: String = ""
}
extension View {
func capitalize(_ chars: String) -> some View {
environment(\.capitalizedCharacters, chars)
}
}