ly, <code>GeometryProxy</code> is a public struct, but it doesn’t have any public initializers.</p><p id="330b">How can we construct a value without a factory?</p><p id="c502">Value types, as opposed to objects, don't require storing pointers to the parent class for self-identification, meaning they remain functional without <code>isa</code> pointers inside ... I had one crazy idea in my mind, and I decided to try it.</p><p id="f825">At first, I wanted to find out the number of bytes that <code>GeometryProxy</code> takes. Swift provides <code>MemoryLayout</code> for this purpose:</p><div id="fad8"><pre><span class="hljs-title class_">MemoryLayout</span><<span class="hljs-title class_">GeometryProxy</span>>.size
<span class="hljs-meta prompt_">>></span> <span class="hljs-number">48</span></pre></div><p id="f88d">There are two options of where you can allocate the memory: on the stack and on the heap.</p><p id="e9b7">The latter is more flexible, as you can just specify the number of bytes you need:</p><div id="f3af"><pre>let <span class="hljs-keyword">pointer</span> = UnsafeMutableRawBufferPointer
.<span class="hljs-built_in">allocate</span>(byteCount: <span class="hljs-number">48</span>, alignment: <span class="hljs-number">8</span>)</pre></div><p id="6579">But dynamic memory requires manual deallocation with <code>deallocate()</code> and is slower than allocation on the stack, so I decided to go with the first option, which is more exotic.</p><p id="df9d">I needed to declare a value type that’d take the same amount of bytes: 48. I called <code>MemoryLayout</code> for a <code>Double</code> and, expectedly, got the following:</p><div id="401f"><pre><span class="hljs-title class_">MemoryLayout</span><<span class="hljs-title class_">Double</span>>.size
<span class="hljs-meta prompt_">>></span> <span class="hljs-number">8</span></pre></div><p id="319c">So if I declared a struct, for example, that kept six doubles. Its total memory size should be 48:</p><div id="a618"><pre><span class="hljs-title">struct</span> <span class="hljs-type">Allocator</span> {
<span class="hljs-keyword">let</span> <span class="hljs-class"><span class="hljs-keyword">data</span>: (<span class="hljs-type">Double</span>, <span class="hljs-type">Double</span>, <span class="hljs-type">Double</span>,
<span class="hljs-type">Double</span>, <span class="hljs-type">Double</span>, <span class="hljs-type">Double</span>) = (0, 0, 0, 0, 0, 0)</span>
}
<span class="hljs-type">MemoryLayout</span><<span class="hljs-type">Allocator</span>>.size
>> <span class="hljs-number">48</span></pre></div><p id="6aaa">Great! The last step was to cast the types:</p><div id="7655"><pre><span class="hljs-keyword">let</span> proxy = unsafeBitCast(Allocator(), <span class="hljs-keyword">to</span>: GeometryProxy.<span class="hljs-built_in">self</span>)</pre></div><p id="da08">It's alive. Alive!</p><p id="7f21">Of course, there were no guarantees the fake <code>GeometryProxy</code> would work correctly, as the inner variables may not expect to be zeros, but, fortunately, this worked well:</p><div id="b0fd"><pre>proxy.size
<span class="hljs-meta prompt_">>></span> <span class="hljs-title class_">CGSize</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>)</pre></div><p id="e493">I had an idea to find the position of bytes responsible for storing the <code>CGSize</code> and to initialize them with the custom value. But reflection showed that <code>size</code>, just like all the other public vars on <code>GeometryProxy</code> are computed, so there was no way to achieve this.</p><p id="f7c7">So after I called the factory closure on the <code>GeometryReader</code> with this Frankenstein struct, I got the contained views with no issues. Of course, the layout of the views is screwed, but at least the values, like the string on <code>Text</code>, could be safely extracted.</p><h1 id="365a">Casting to an Unknown Generic Type</h1><p id="cfc6">Another notable case was with <code>ForEach</code>. To explore the internals, I made a simple setup with an array of strings transformed to <code>Text</code> views:</p><div id="e792"><pre>let<span class="hljs-built_in"> array </span>= [<span class="hljs-string">"0"</span>, <span class="hljs-string">"1"</span>, <span class="hljs-string">"2"</span>]
let view = ForEach(array, id: .self) { Text($0) }</pre></div><p id="a039">My BFG10K function <code>attributesTree(value:)</code> showed the following:</p><div id="ad46"><pre><span class="hljs-string">"view"</span> <span class="hljs-keyword">of</span> <span class="hljs-keyword">type</span> ForEach<<span class="hljs-keyword">Array</span><<span class="hljs-built_in">String</span>>, <span class="hljs-built_in">String</span>, <span class="hljs-literal">Text</span>>
↳ <span class="hljs-string">"data"</span> <span class="hljs-keyword">of</span> <span class="hljs-keyword">type</span> <span class="hljs-keyword">Array</span><<span class="hljs-built_in">String</span>>
↳ value = [<span class="hljs-string">"0"</span>, <span class="hljs-string">"1"</span>, <span class="hljs-string">"2"</span>]
↳ <span class="hljs-string">"content"</span> <span class="hljs-keyword">of</span> <span class="hljs-keyword">type</span> (<span class="hljs-built_in">String</span>) -> <span class="hljs-literal">Text</span>
↳ <span class="hljs-string">"idGenerator"</span> <span class="hljs-keyword">of</span> <span class="hljs-keyword">type</span> WritableKeyPath<<span class="hljs-built_in">String</span>, <span class="hljs-built_in">String</span>>
↳ value = WritableKeyPath<<span class="hljs-built_in">String</span>, <span class="hljs-built_in">String</span>>
↳ <span class="hljs-string">"contentID"</span> <span class="hljs-keyword">of</span> <span class="hljs-keyword">type</span> Int
↳ value = <span class="hljs-number">0</span></pre></div><p id="91f6">So I could extract the <code>Text</code> views using the content-builder closure <code>content: (String) -> Text</code> by providing it with an element of the <code>data: [String]</code> array.</p><p id="4135">All I needed to do was cast <code>data</code> and <code>content</code> to correct types from reflection's default type <code>Any</code>:</p>
<figure id="0809">
<div>
<div>
<iframe class="gist-iframe" src="/gist/nalexn/6cb7ff34e654599d7356a1edf61f584a.js" allowfullscreen="" frameborder="0" height="undefined" width="undefined">
</div>
</div>
</figure></iframe></div></div></figure><p id="d680">Hardcoding types <code>String</code> and <code>Text</code>, of course, wouldn't work for an arbitrary <code>ForEach</code>, so I needed to get the types from elsewhere.</p><p id="b69a">A naive attempt to obtain the type dynamically with <code>type(of: value)</code> didn’t make the compiler happy — it needs to know the types in compile time. Basically, this is not a valid code: <code>let casted = value as? type(of: value)</code>.</p><p id="aaed">OK, the type information should be known at compile time. Where can we get it from?</p><p id="46e7">The first workable solution I came up with was to provide the types from the caller side:</p>
<figure id="486c">
<div>
<div>
<iframe class="gist-iframe" src="/gist/nalexn/f2be0ab828a3f81e63dbb5350b241b4a.js" allowfullscreen="" frameborder="0" height="undefined" width="undefined">
</div>
</div>
</figure></iframe></div></div></figure><p id="e618">I didn’t like this approach because it was bulky and inconvenient to use, so I appealed to the following hack.</p><p id="c635">I’ve declared a type-erased middleware protocol and extended the <code>ForEach</code> to conform to that protocol. The trick is that in the extension of the<code>ForEach</code> we have the inner type information required for the content extraction:</p>
<figure id="dfaa">
<div>
<div>
<iframe class="gist-iframe" src="/gist/nalexn/5a99e0e65c67a008495118dda234ab6a.js" allowfullscreen="" frameborder="0" height="undefined" width="undefined">
</div>
</div>
</figure></iframe></div></div></figure><p id="8beb">So the original extraction function now just needed to cast the <code>view: Any</code> to the middleware protocol and call <code>extractContent()</code>. Since <code>ForEach</code> now conforms to that protocol, the cast succeeds, and the extraction works as expected:</p>
<figure id="918c">
<div>
<div>
<iframe class="gist-iframe" src="/gist/nalexn/fb7743003dd56fc76c5644255e742a98.js" allowfullscreen="" frameborder="0" height="undefined" width="undefined">
</div>
</div>
</figure></iframe></div></div></figure><h1 id="130c">SwiftUI’s Native Environment Injection</h1><p id="c2cf">SwiftUI has a very handy dependency injection mechanism through <code>@ObservedObject</code>, <code>@EnvironmentObject</code>, and <code>@Environment</code> attributes.</p><p id="e400">While there was no practical problem with supporting <code>@ObservedObject</code> in the inspection framework, I had to spend quite some time trying to figure out how to inject <code>@EnvironmentObject</code>.</p><p id="684e">When a view receives a traditional DI injection through <code>.environmentObject(...)</code>, it gets wrapped into a view of the type <code>ModifiedContent</code>. This type of view is widely used throughout SwiftUI for applying variou
Options
s tweaks to the view, such as <code>.padding()</code>, <code>.blur(radius:)</code>, etc.</p><p id="4f21"><code>ModifiedContent</code> is quite transparent — one of its attributes, called <code>content</code>, provides the enclosed view, which could be easily extracted.</p><p id="1a49">The problem was with the other attribute, <code>modifier</code>, which usually refers to the value of a <i>semiprivate</i> type, such as <code>_PaddingLayout</code>. I called them semiprivate because Xcode recognizes these types if you paste them in the source code, but their symbols are excluded from the public headers. If you control-click and select "Jump to Definition," Xcode won't be able to locate them.</p><p id="bb68">For some types, Xcode Autocomplete shows a few instance vars — for example, <code>_PaddingLayout</code> has <code>var edges: Edge.Set</code> and <code>var insets: EdgeInsets?</code>.</p><p id="35ea">So going back to the problem of injecting <code>@EnvironmentObject</code>: The view gets wrapped in a <code>ModifiedContent</code> in which the <code>modifier</code> has the type <code>_EnvironmentKeyWritingModifier<InjectedObject?></code>.</p><p id="3f76">That modifier has no public methods, and here’s what reflection shows for it when we inject an object of type <code>InjectedObject</code>:</p><div id="099e"><pre><span class="hljs-string">"modifier"</span> <span class="hljs-keyword">of</span> <span class="hljs-keyword">type</span> <span class="hljs-type">_EnvironmentKeyWritingModifier<InjectedObject?>
</span>↳ <span class="hljs-string">"keyPath"</span> <span class="hljs-keyword">of</span> <span class="hljs-keyword">type</span> <span class="hljs-type">WritableKeyPath
</span>↳ value = WritableKeyPath<EnvironmentValues, InjectedObject?>
↳ <span class="hljs-string">"value"</span> <span class="hljs-keyword">of</span> <span class="hljs-keyword">type</span> <span class="hljs-type">InjectedObject?
</span>↳ value = InjectedObject(...)</pre></div><p id="1d96">It does keep the reference to the <code>InjectedObject</code> and also has a <code>WritableKeyPath</code> for <code>EnvironmentValues</code>.</p><p id="6b9b">Those <code>EnvironmentValues</code> are very mysterious. So far I know both <code>@EnvironmentObject</code> and <code>@Environment</code> are using it for storing the values used by the SwiftUI views, but my experiments showed that the <code>EnvironmentValues</code> are provided to the view hierarchy only at the render time – and withdrawn after.</p><p id="705c">Try running the following code:</p>
<figure id="d61f">
<div>
<div>
<iframe class="gist-iframe" src="/gist/nalexn/71d5e47ef119842a7d23856f922569f7.js" allowfullscreen="" frameborder="0" height="undefined" width="undefined">
</div>
</div>
</figure></iframe></div></div></figure><p id="9f7e">You’ll see the asynchronous reading of <code>@EnvironmentObject</code> outside of the rendering cycle is prohibited — you'll get the same crash as if you never provided <code>InjectedObject</code> in the <code>.environmentObject(...)</code> call.</p><h1 id="6a59">Design Decisions Behind the Inspection Framework</h1><p id="e869">I wanted to make the library safe and convenient to use. All I had was just an idea how the syntax on the caller side should look like: It should be chained to calls like <code>view.anyView.hStack.button</code>.</p><p id="74ee">It was clear each intermediate element should return a statically typed value that’d restrict the available options: There’s no sense of calling <code>.tap()</code> for <code>AnyView</code> or <code>.hStack</code> on a <code>Text</code>.</p><p id="975b">One of the options was to create an object-oriented hierarchy of classes, but after using functional and protocol-oriented programming for a few years, I developed a <a href="https://readmedium.com/goodbye-object-oriented-programming-a59cda4c0e53">strong allergy to OOP</a>.</p><p id="d081">So I decided to use a unified struct, <code>InspectableView</code>, and encapsulate the polymorphic behavior in its generic parameter <code>View</code>:</p><div id="1e19"><pre><span class="hljs-keyword">struct</span> <span class="hljs-type">InspectableView</span><<span class="hljs-type">View</span>> {
<span class="hljs-keyword">let</span> <span class="hljs-built_in">view</span>: Any
}</pre></div><p id="3035">At first, I thought I’d be using SwiftUI views as the <code>View</code> parameter, but I quickly realized most of the SwiftUI views have generic parameters as well. Constructions like <code>InspectableView<HStack<VStach<Text>>></code> would be too cumbersome and fragile to operate.</p><p id="e436">Instead, I’ve created an empty <code>struct ViewType { }</code> that served as the base namespace for future view type — <code>ViewType.Button</code> being a representative for the <code>Button</code> view, for example.</p><p id="dc31">I thought the user of the library could falsely assume they can substitute SwiftUI views in that parameter. In order to help them quickly identify this is the wrong path, I’ve put a restriction on the generic type to conform to a simple protocol <code>KnownViewType</code>, which SwiftUI views don't conform to by default:</p>
<figure id="c09f">
<div>
<div>
<iframe class="gist-iframe" src="/gist/nalexn/15871ba20fb7d6cca694fa14eaf1c5b1.js" allowfullscreen="" frameborder="0" height="undefined" width="undefined">
</div>
</div>
</figure></iframe></div></div></figure><p id="1f7a">Now it was all ready to start building the polymorphic behavior with generics.</p><p id="944e">The views in SwiftUI can either contain a single view (<code>AnyView</code>), a collection of views (<code>HStack</code>), or no other views (<code>Text</code>).</p><p id="f266">In order to encapsulate this behavior, I defined two protocols: <code>SingleViewContent</code>and <code>MultipleViewContent</code>.</p>
<figure id="d63b">
<div>
<div>
<iframe class="gist-iframe" src="/gist/nalexn/3f0294a3bc84ce14f1e6bda592c23654.js" allowfullscreen="" frameborder="0" height="undefined" width="undefined">
</div>
</div>
</figure></iframe></div></div></figure><p id="08f9">Now any <code>ViewType</code> was able to adopt the content-extraction strategy based on its nature:</p>
<figure id="4912">
<div>
<div>
<iframe class="gist-iframe" src="/gist/nalexn/3e004dae2b51d7408113e279eb6e06a9.js" allowfullscreen="" frameborder="0" height="undefined" width="undefined">
</div>
</div>
</figure></iframe></div></div></figure><p id="f4ce">For views like <code>Text</code> that don't have a contained view, its companion <code>ViewType.Text</code> simply opts out of conforming to either of these protocols.</p><p id="7bb5">Now every <code>ViewType</code> could declare its strategy of extracting the content.</p><p id="77bf">The last piece of the puzzle was to add methods, such as <code>.hStack</code> for extraction <i>from</i> the parent.</p><p id="ba8a">This one was easy — I just extended <code>InspectableView where View: SingleViewContent</code> with a method named after the type of view intended for extraction, allowing such views to continue the chain with <code>.hStack</code>, for example:</p>
<figure id="1f42">
<div>
<div>
<iframe class="gist-iframe" src="/gist/nalexn/47850e75a3bd0504d9aeb3b1a3592136.js" allowfullscreen="" frameborder="0" height="undefined" width="undefined">
</div>
</div>
</figure></iframe></div></div></figure><p id="0616">A similar extension was defined for <code>MultipleViewContent</code> as well.</p><p id="56c9">Finally, for types like <code>ViewType.Button</code>, I could add exclusive support of the methods like <code>.tap()</code>.</p>
<figure id="acf6">
<div>
<div>
<iframe class="gist-iframe" src="/gist/nalexn/b46bd38546ff4d7e4fa8ebf1905f91d9.js" allowfullscreen="" frameborder="0" height="undefined" width="undefined">
</div>
</div>
</figure></iframe></div></div></figure><p id="983a">With this approach <code>InspectableView</code> obtained a controlled set of methods available for a particular <code>ViewType</code>, eliminating the possible logical errors when working with the view-extraction library.</p><h1 id="2bb1">Conclusion</h1><p id="57c2">That’s the story behind creating the ViewInspector framework. If you have a SwiftUI project you want to cover with unit yests — consider trying it out.</p><div id="6e44" class="link-block">
<a href="https://github.com/nalexn/ViewInspector">
<div>
<div>
<h2>nalexn/ViewInspector</h2>
<div><h3>ViewInspector is a library for unit testing SwiftUI-based projects. It allows for traversing SwiftUI view hierarchy in…</h3></div>
<div><p>github.com</p></div>
</div>
<div>
<div style="background-image: url(https://miro.readmedium.com/v2/resize:fit:320/0*RYm8QI7FPmvr4-kB)"></div>
</div>
</div>
</a>
</div><p id="8be7">If you want to better understand the inner SwiftUI mechanisms used under the hood, take that function <code>attributesTree(value:)</code>, and crack those black-boxed views.</p></article></body>
Who said we cannot unit test SwiftUI views?
The story behind creating a unit-testing framework
Viktor Chernomyrdin, a Russian politician from the ‘90s, once said:
“Such has never happened before, and here it is again.”
This reminds me of the situation we find ourselves in with SwiftUI: We have a brand-new, exciting technology — but with stability issues, an incomplete API, and scarce documentation. Such has never happened before, and here it is again.
Anyway, things are not as bad as they could be, and teams have already started adopting SwiftUI in production projects. Still, one of the main arguments against using it in production is a complete inability to unit test the UI.
A function of state should be really straightforward to test — with one if . We need to have access to the function’s output.
Views in SwiftUI are nested inside one another, forming a statically typed hierarchy of structs without an API to inspect the view’s content.
One day Apple may release its unit testing tool for SwiftUI, but who knows whether/when this will happen.
So I decided to build one.
The Tool
Since there’s no access to the inner shadow attribute graph of SwiftUI, I tried to use Swift’s reflection API. Xcode uses it for printing out the contents of variables when we stop on a breakpoint in the debugger. And I was surprised by how much information was available inside the SwiftUI views.
It turns out SwiftUI views have a very ramified inner structure, so the first thing I had to implement was a recursive traversing of inner attributes:
If you call this function for a simple view hierarchy, like this one:
… you’d get a pretty long output. However, this could be restructured in a more readable and concise way:
"view"oftypeAnyView
↳ "storage"oftypeAnyViewStorage<Text>
↳ "view"oftypeText
↳ "modifiers"oftypeArray<Modifier>
↳ value = []
↳ "storage"oftypeStorage
↳ "verbatim"oftypeString
↳ value = "Hello, world!"
I had a gut feeling that it just can’t be that simple — there had to be a wall I won’t be able to get through with using just reflection, but I was curious how far I could dig.
And as it turned out, there were many pitfalls waiting for me on the way:
All types in reflection are erased to Any
Computed properties, such as var body: some View, are not available in reflection
There are generic private structs and function types, which are tricky to cast the value to
It’s hard to initialize a struct for which all init methods are private
SwiftUI dependency needs an injection through Environment
There are significant variations to the hierarchy after a tiny tweak of the input — for example, Text("Hi") vs. Text(hiValue)
Overall there’s a lot of obscurities and a lack of information about the private structures
In this article, I’ll talk about interesting use cases I encountered and the ways I addressed the challenges, but before that, let me show you what I’ve got after a few days of trial and error:
With this library, you can extract your custom views from the hierarchy and evaluate its state in unit tests:
You can read the actual values from the standard SwiftUI views, such as theString value of Text.
And it’s also possible to programmatically trigger side effects on behalf of the user:
By now, the framework supports the majority of views available in SwiftUI for iOS and macOS, as well as views ported from UIKit with UIViewRepresentable:
I did eventually hit a few unbreakable walls, but overall, I’m satisfied with the result.
OK, it’s time for some hacky stories.
Creating a Struct Without Calling init()
There is one interesting SwiftUI view that provides information about the view’s container size: GeometryReader.
The reflection showed this view doesn’t store the contained view directly. Instead, it holds a closure for building the enclosed views. The closure takes one parameter — the GeometryProxy value.
This means the only way to obtain the Text view in the example above is to call that closure with a GeometryProxy.
OK, fortunately, GeometryProxy is a public struct, but it doesn’t have any public initializers.
How can we construct a value without a factory?
Value types, as opposed to objects, don't require storing pointers to the parent class for self-identification, meaning they remain functional without isa pointers inside ... I had one crazy idea in my mind, and I decided to try it.
At first, I wanted to find out the number of bytes that GeometryProxy takes. Swift provides MemoryLayout for this purpose:
MemoryLayout<GeometryProxy>.size
>>48
There are two options of where you can allocate the memory: on the stack and on the heap.
The latter is more flexible, as you can just specify the number of bytes you need:
let pointer = UnsafeMutableRawBufferPointer
.allocate(byteCount: 48, alignment: 8)
But dynamic memory requires manual deallocation with deallocate() and is slower than allocation on the stack, so I decided to go with the first option, which is more exotic.
I needed to declare a value type that’d take the same amount of bytes: 48. I called MemoryLayout for a Double and, expectedly, got the following:
MemoryLayout<Double>.size
>>8
So if I declared a struct, for example, that kept six doubles. Its total memory size should be 48:
let proxy = unsafeBitCast(Allocator(), to: GeometryProxy.self)
It's alive. Alive!
Of course, there were no guarantees the fake GeometryProxy would work correctly, as the inner variables may not expect to be zeros, but, fortunately, this worked well:
proxy.size
>>CGSize(0, 0)
I had an idea to find the position of bytes responsible for storing the CGSize and to initialize them with the custom value. But reflection showed that size, just like all the other public vars on GeometryProxy are computed, so there was no way to achieve this.
So after I called the factory closure on the GeometryReader with this Frankenstein struct, I got the contained views with no issues. Of course, the layout of the views is screwed, but at least the values, like the string on Text, could be safely extracted.
Casting to an Unknown Generic Type
Another notable case was with ForEach. To explore the internals, I made a simple setup with an array of strings transformed to Text views:
let array = ["0", "1", "2"]
let view = ForEach(array, id: \.self) { Text($0) }
My BFG10K function attributesTree(value:) showed the following:
"view"oftype ForEach<Array<String>, String, Text>
↳ "data"oftypeArray<String>
↳ value = ["0", "1", "2"]
↳ "content"oftype (String) -> Text
↳ "idGenerator"oftype WritableKeyPath<String, String>
↳ value = WritableKeyPath<String, String>
↳ "contentID"oftype Int
↳ value = 0
So I could extract the Text views using the content-builder closure content: (String) -> Text by providing it with an element of the data: [String] array.
All I needed to do was cast data and content to correct types from reflection's default type Any:
Hardcoding types String and Text, of course, wouldn't work for an arbitrary ForEach, so I needed to get the types from elsewhere.
A naive attempt to obtain the type dynamically with type(of: value) didn’t make the compiler happy — it needs to know the types in compile time. Basically, this is not a valid code: let casted = value as? type(of: value).
OK, the type information should be known at compile time. Where can we get it from?
The first workable solution I came up with was to provide the types from the caller side:
I didn’t like this approach because it was bulky and inconvenient to use, so I appealed to the following hack.
I’ve declared a type-erased middleware protocol and extended the ForEach to conform to that protocol. The trick is that in the extension of theForEach we have the inner type information required for the content extraction:
So the original extraction function now just needed to cast the view: Any to the middleware protocol and call extractContent(). Since ForEach now conforms to that protocol, the cast succeeds, and the extraction works as expected:
SwiftUI’s Native Environment Injection
SwiftUI has a very handy dependency injection mechanism through @ObservedObject, @EnvironmentObject, and @Environment attributes.
While there was no practical problem with supporting @ObservedObject in the inspection framework, I had to spend quite some time trying to figure out how to inject @EnvironmentObject.
When a view receives a traditional DI injection through .environmentObject(...), it gets wrapped into a view of the type ModifiedContent. This type of view is widely used throughout SwiftUI for applying various tweaks to the view, such as .padding(), .blur(radius:), etc.
ModifiedContent is quite transparent — one of its attributes, called content, provides the enclosed view, which could be easily extracted.
The problem was with the other attribute, modifier, which usually refers to the value of a semiprivate type, such as _PaddingLayout. I called them semiprivate because Xcode recognizes these types if you paste them in the source code, but their symbols are excluded from the public headers. If you control-click and select "Jump to Definition," Xcode won't be able to locate them.
For some types, Xcode Autocomplete shows a few instance vars — for example, _PaddingLayout has var edges: Edge.Set and var insets: EdgeInsets?.
So going back to the problem of injecting @EnvironmentObject: The view gets wrapped in a ModifiedContent in which the modifier has the type _EnvironmentKeyWritingModifier<InjectedObject?>.
That modifier has no public methods, and here’s what reflection shows for it when we inject an object of type InjectedObject:
"modifier"oftype_EnvironmentKeyWritingModifier<InjectedObject?>
↳ "keyPath"oftypeWritableKeyPath
↳ value = WritableKeyPath<EnvironmentValues, InjectedObject?>
↳ "value"oftypeInjectedObject?
↳ value = InjectedObject(...)
It does keep the reference to the InjectedObject and also has a WritableKeyPath for EnvironmentValues.
Those EnvironmentValues are very mysterious. So far I know both @EnvironmentObject and @Environment are using it for storing the values used by the SwiftUI views, but my experiments showed that the EnvironmentValues are provided to the view hierarchy only at the render time – and withdrawn after.
Try running the following code:
You’ll see the asynchronous reading of @EnvironmentObject outside of the rendering cycle is prohibited — you'll get the same crash as if you never provided InjectedObject in the .environmentObject(...) call.
Design Decisions Behind the Inspection Framework
I wanted to make the library safe and convenient to use. All I had was just an idea how the syntax on the caller side should look like: It should be chained to calls like view.anyView.hStack.button.
It was clear each intermediate element should return a statically typed value that’d restrict the available options: There’s no sense of calling .tap() for AnyView or .hStack on a Text.
One of the options was to create an object-oriented hierarchy of classes, but after using functional and protocol-oriented programming for a few years, I developed a strong allergy to OOP.
So I decided to use a unified struct, InspectableView, and encapsulate the polymorphic behavior in its generic parameter View:
structInspectableView<View> {
letview: Any
}
At first, I thought I’d be using SwiftUI views as the View parameter, but I quickly realized most of the SwiftUI views have generic parameters as well. Constructions like InspectableView<HStack<VStach<Text>>> would be too cumbersome and fragile to operate.
Instead, I’ve created an empty struct ViewType { } that served as the base namespace for future view type — ViewType.Button being a representative for the Button view, for example.
I thought the user of the library could falsely assume they can substitute SwiftUI views in that parameter. In order to help them quickly identify this is the wrong path, I’ve put a restriction on the generic type to conform to a simple protocol KnownViewType, which SwiftUI views don't conform to by default:
Now it was all ready to start building the polymorphic behavior with generics.
The views in SwiftUI can either contain a single view (AnyView), a collection of views (HStack), or no other views (Text).
In order to encapsulate this behavior, I defined two protocols: SingleViewContentand MultipleViewContent.
Now any ViewType was able to adopt the content-extraction strategy based on its nature:
For views like Text that don't have a contained view, its companion ViewType.Text simply opts out of conforming to either of these protocols.
Now every ViewType could declare its strategy of extracting the content.
The last piece of the puzzle was to add methods, such as .hStack for extraction from the parent.
This one was easy — I just extended InspectableView where View: SingleViewContent with a method named after the type of view intended for extraction, allowing such views to continue the chain with .hStack, for example:
A similar extension was defined for MultipleViewContent as well.
Finally, for types like ViewType.Button, I could add exclusive support of the methods like .tap().
With this approach InspectableView obtained a controlled set of methods available for a particular ViewType, eliminating the possible logical errors when working with the view-extraction library.
Conclusion
That’s the story behind creating the ViewInspector framework. If you have a SwiftUI project you want to cover with unit yests — consider trying it out.
If you want to better understand the inner SwiftUI mechanisms used under the hood, take that function attributesTree(value:), and crack those black-boxed views.