Property wrappers and SwiftUI environment: how can a property wrapper access the environment of its enclosing object? - swift

The #FetchRequest property wrapper that ships with SwiftUI helps declaring properties that are auto-updated whenever a Core Data storage changes. You only have to provide a fetch request:
struct MyView: View {
#FetchRequest(fetchRequest: /* some fetch request */)
var myValues: FetchedResults<MyValue>
}
The fetch request can't access the storage without a managed object context. This context has to be passed in the view's environment.
And now I'm quite puzzled.
Is there any public API that allows a property wrapper to access the environment of its enclosing object, or to have SwiftUI give this environment to the property wrapper?

We don't know the exact internals of how SwiftUI is implemented, but we can make some educated guesses based on the information we have available.
First, #propertyWrappers do not get automatic access to any kind of context from their containing struct/class. You can check out the spec for evidence of that. This was discussed a few times during the evolution process, but not accepted.
Therefore, we know that something has to happen at runtime for the framework to inject the #EnvironmentObject(here the NSManagedObjectContext) into the #FetchRequest. For an example of how to do something like that via the Mirror API, you can see my answer in this question. (By the way, that was written before #Property was available, so the specific example is no longer useful).
However, this article suggests a sample implementation of #State and speculates (based on
an assembly dump) that rather than using the Mirror API, SwiftUI is using TypeMetadata for speed:
Reflection without Mirror
There is still a way to get fields without using Mirror. It's using metadata.
Metadata has Field Descriptor which contains accessors for fields of the type. It's possible to get fields by using it.
My various experiments result AttributeGraph.framework uses metadata internally. AttributeGraph.framework is a private framework that SwiftUI use internally for constructing ViewGraph.
You can see it by the symbols of the framework.
$ nm /System/Library/PrivateFrameworks/AttributeGraph.framework/AttributeGraph
There is AG::swift::metadata_visitor::visit_field in the list of symbols. i didn't analysis the whole of assembly code but the name implies that AttributeGraph use visitor pattern to parse metadata.

With Xcode 13 (haven't tested on earlier versions) as long as your property wrapper implements DynamicProperty you can use the #Environment property wrapper.
The following example create a property wrapper that's read the lineSpacing from the current environment.
#propertyWrapper
struct LineSpacing: DynamicProperty {
#Environment(\.lineSpacing) var lineSpacing: CGFloat
var wrappedValue: CGFloat {
lineSpacing
}
}
Then you can use it just like any other property wrapper:
struct LineSpacingDisplayView: View {
#LineSpacing private var lineSpacing: CGFloat
var body: some View {
Text("Line spacing: \(lineSpacing)")
}
}
struct ContentView: View {
var body: some View {
VStack {
LineSpacingDisplayView()
LineSpacingDisplayView()
.environment(\.lineSpacing, 99)
}
}
}
This displays:
Line spacing: 0.000000
Line spacing: 99.000000

A DynamicProperty struct can simply declare #Environment and it will be set before update is called e.g.
struct FetchRequest2: DynamicProperty {
#Environment(\.managedObjectContext) private var context
#StateObject private var controller = FetchController()
func update(){
// context will now be valid
// set the context on the controller and do some fetching.
}

Related

why doesn't this code set the #State variable in a struct in swiftui

I've got the following code, which seems very simple.
import SwiftUI
struct Tester : View
{
#State var blah : String = "blah"
func setBlah(_ val : String) {
blah = val
}
var body: some View {
Text("text")
}
}
var test = Tester()
test.setBlah("blee")
print(test.blah)
I would normally expect to see the final print statement display "blee", but it is "blah" -- the variable never changed. If I pass blah into another view via a binding, I am able to change the value of blah there. Any insight here would be appreciated -- including rtfm, if you can point me to the right manual -- I have looked, and haven't found an answer.
Edit: after reading #jnpdx 's answer, I have a slightly different version, but it still doesn't work -- I'm not worried about this specific code working, but trying to understand the magic that #jnpdx refers to, in terms of how #State works, and why a binding passed to another view is able to modify the original #State variable, while I am unable to within the struct itself. I am guessing there is some part of SwiftUI that needs to be instantiated that the #State property's communicate with in order to store the variables outside of the struct, as the apple documentation says. New version follows:
import Foundation
import SwiftUI
struct Tester : View
{
#State var blah : String
func setBlah(_ val : String) {
$blah.wrappedValue = val
}
var body: some View {
Text("smoe text")
}
}
var test = Tester(blah: "blah")
test.setBlah("blee") // expect to see blee printed, but get nil instead
print(test.blah)
Thanks :)
#State in SwiftUI doesn't work like simple mutating functions on a struct -- it's more like a separate layer of state that gets stored alongside the view hierarchy.
Let's look at what this would have to look like if it were not SwiftUI/#State:
struct Tester
{
var blah : String = "blah"
mutating func setBlah(_ val : String) {
blah = val
}
}
var test = Tester()
test.setBlah("blee") // prints correctly
print(test.blah)
Note that above, setBlah has to be marked mutating because it mutates the struct. Whereas in your example, the compiler doesn't require it, because the struct itself is not actually mutating -- the #State property wrapper is doing some behind-the-scenes magic.
Check out the documentation on State: https://developer.apple.com/documentation/swiftui/state
In particular:
Don’t initialize a state property of a view at the point in the view hierarchy where you instantiate the view, because this can conflict with the storage management that SwiftUI provides. To avoid this, always declare state as private, and place it in the highest view in the view hierarchy that needs access to the value. Then share the state with any child views that also need access, either directly for read-only access, or as a binding for read-write access.
By marking #State as private, you can prevent things like outside entities trying to manipulate it directly. However, in your example, you've circumvented this a bit by making a setter function that would avoid the private issue even if it were included. So, really, setBlah should be marked private as well.

Do I have to use an ObservableObject in SwiftUI?

I would like to use a struct instead of a class as a state for my View, and as you may know, ObservableObject is a protocol only classes can conform to.
Do I have to wrap my struct in a ViewModel or some other similar type of object ? What happens if I don't ?
A sample on what that looks like now :
import Foundation
import SwiftUI
struct Object3D {
var x : Int
var y : Int
var z : Int
var visible : Bool
}
struct NumberView<Number : Strideable> : View {
var label : String
#State var number : Number
var body : some View {
HStack {
TextField(
self.label,
value: self.$number,
formatter: NumberFormatter()
)
Stepper("", value: self.$number)
.labelsHidden()
}
}
}
struct ObjectInspector : View {
#State var object : Object3D
var body : some View {
VStack {
Form {
Toggle("Visible", isOn: $object.visible)
}
Divider()
Form {
HStack {
NumberView(label: "X:", number: object.x)
NumberView(label: "Y:", number: object.y)
NumberView(label: "Z:", number: object.z)
}
}
}
.padding()
}
}
struct ObjectInspector_Previews: PreviewProvider {
static var previews: some View {
ObjectInspector(object: Object3D(x: 0, y: 0, z: 0, visible: true))
}
}
You don't have to use #ObservedObject to ensure that updates to your model object are updating your view.
If you want to use a struct as your model object, you can use #State and your view will be updated correctly whenever your #State struct is updated.
There are lots of different property wrappers that you can use to update your SwiftUI views whenever your model object is updated. You can use both value and reference types as your model objects, however, depending on your choice, you have to use different property wrappers.
#State can only be used on value types and #State properties can only be updated from inside the view itself (hence they must be private).
#ObservedObject (and all other ...Object property wrappers, such as #EnvironmentObject and #StateObject) can only be used with classes that conform to ObservableObject. If you want to be able to update your model objects from both inside and outside your view, you must use an ObservableObject conformant type with the appropriate property wrapper, you cannot use #State in this case.
So think about what sources your model objects can be updated from (only from user input captured directly inside your View or from outside the view as well - such as from other views or from the network), whether you need value or reference semantics and make the appropriate choice for your data model accordingly.
For more information on the differences between #ObservedObject and #StateObject, see What is the difference between ObservedObject and StateObject in SwiftUI.
I would like to use a struct instead of a class as a state for my View, and as you may know, ObservableObject is a protocol only classes can conform to.
A model is usually shared among whichever parts of the app need it, so that they're all looking at the same data all the time. For that, you want a reference type (i.e. a class), so that everybody shares a single instance of the model. If you use a value type (i.e. a struct), your model will be copied each time you assign it to something. To make that work, you'd need to copy the updated info back to wherever it belongs whenever you finish updating it, and then arrange for every other part of the app that might use it to get an updated copy. It's usually a whole lot easier and safer to share one instance than to manage that sort of updating.
Do I have to wrap my struct in a ViewModel or some other similar type of object ? What happens if I don't ?
It's your code -- you can do whatever you like. ObservableObject provides a nice mechanism for communicating the fact that your model has changed to other parts of your program. It's not the only possible way to do that, but it's the way that SwiftUI does it, so if you go another route you're going to lose out on a lot of support that's built into SwiftUI.
The View is a struct already, it cannot be a class. It holds the data that SwiftUI diffs to update the actual UIViews and NSViews on screen. It uses the #State and #Binding property wrappers to make it behave like a class, i.e. so when the hierarchy of View structs is recreated they are given back their property values from last time. You can refactor groups of vars into their own testable struct and include mutating funcs.
You usually only need an ObservableObject if you are using Combine, it's part of the Combine framework.
I recommend watching Data Flow through SwiftUI WWDC 2019 for more detail.

How to bind an array and List if the array is a member of ObservableObject?

I want to create MyViewModel which gets data from network and then updates the arrray of results. MyView should subscribe to the $model.results and show List filled with the results.
Unfortunately I get an error about "Type of expression is ambiguous without more context".
How to properly use ForEach for this case?
import SwiftUI
import Combine
class MyViewModel: ObservableObject {
#Published var results: [String] = []
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.results = ["Hello", "World", "!!!"]
}
}
}
struct MyView: View {
#ObservedObject var model: MyViewModel
var body: some View {
VStack {
List {
ForEach($model.results) { text in
Text(text)
// ^--- Type of expression is ambiguous without more context
}
}
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(model: MyViewModel())
}
}
P.S. If I replace the model with #State var results: [String] all works fine, but I need have separate class MyViewModel: ObservableObject for my purposes
The fix
Change your ForEach block to
ForEach(model.results, id: \.self) { text in
Text(text)
}
Explanation
SwiftUI's error messages aren't doing you any favors here. The real error message (which you will see if you change Text(text) to Text(text as String) and remove the $ before model.results), is "Generic parameter 'ID' could not be inferred".
In other words, to use ForEach, the elements that you are iterating over need to be uniquely identified in one of two ways.
If the element is a struct or class, you can make it conform to the Identifiable protocol by adding a property var id: Hashable. You don't need the id parameter in this case.
The other option is to specifically tell ForEach what to use as a unique identifier using the id parameter. Update: It is up to you to guarentee that your collection does not have duplicate elements. If two elements have the same ID, any change made to one view (like an offset) will happen to both views.
In this case, we chose option 2 and told ForEach to use the String element itself as the identifier (\.self). We can do this since String conforms to the Hashable protocol.
What about the $?
Most views in SwiftUI only take your app's state and lay out their appearance based on it. In this example, the Text views simply take the information stored in the model and display it. But some views need to be able to reach back and modify your app's state in response to the user:
A Toggle needs to update a Bool value in response to a switch
A Slider needs to update a Double value in response to a slide
A TextField needs to update a String value in response to typing
The way we identify that there should be this two-way communication between app state and a view is by using a Binding<SomeType>. So a Toggle requires you to pass it a Binding<Bool>, a Slider requires a Binding<Double>, and a TextField requires a Binding<String>.
This is where the #State property wrapper (or #Published inside of an #ObservedObject) come in. That property wrapper "wraps" the value it contains in a Binding (along with some other stuff to guarantee SwiftUI knows to update the views when the value changes). If we need to get the value, we can simply refer to myVariable, but if we need the binding, we can use the shorthand $myVariable.
So, in this case, your original code contained ForEach($model.results). In other words, you were telling the compiler, "Iterate over this Binding<[String]>", but Binding is not a collection you can iterate over. Removing the $ says, "Iterate over this [String]," and Array is a collection you can iterate over.

LSP and SwiftUI

In order to make my code testable, I'm trying to adhere to Liskov's substitution principle by having my SwiftUI views depend on protocols rather than concrete types. This allows me to easily swap implementations and allows me to easily build mocks for testing. Here's an example of what I'm trying to do:
protocol DashboardViewModel: ObservableObject {
var orders: [Order] { get }
}
My DashboardViewModel needs to communicate changes back to its dependents, so I've also attached ObservableObject as a transitive requirement.
This seems to be a problem. You cannot achieve LSP if you have associated type requirements. Here's the error I got from my SwiftUI view class that depended on my view model:
struct DashboardView: View {
#ObservedObject var viewModel: DashboardViewModel
}
Protocol 'DatastoreProtocol' can only be used as a generic constraint because it has Self or associated type requirements
I ended up doing this instead:
protocol DashboardViewModel {
var orders: [Order] { get }
var objectWillChange: AnyPublisher<Void, Never> { get }
}
This also requires dependents to do additional work to observe for state changes. This takes away the conveniences of using property wrappers - mainly the ability for dependents to observe for state changes using #ObservedObject. Using this alternative leads us to code like this:
struct DashboardView: View {
let viewModel: DashboardViewModel
var viewModelSubscriber: AnyCancellable!
// MARK: - Used only to force a re-render of this view
#State private var reload = false
init(viewModel: DashboardViewModel) {
self.viewModel = viewModel
viewModelSubscriber = viewModel.objectWillChange.sink { _ in
self.reload.toggle()
}
}
}
This is a tad distasteful to look at:
Created a #State variable that is only used to force an update of the view, because we cannot utilize SwiftUI property wrappers to observe for state changes
Created a AnyCancellable! variable to hold a subscription to objectWillChange from the view model. This is necessary to detect state changes from the DashboardViewModel
Added the subscription call in the initializer that only toggles the #State variable to force a retrieval of new data from the view model
I feel like there should be a much better way of handling this. Looking for help!
One way to solve this is to make your view generic:
protocol DashboardViewModel: ObservableObject {
var orders: [Order] { get }
}
struct DashboardView<Model: DashboardViewModel>: View {
#ObservedObject var viewModel: Model
}

Class or struct for hierarchy model?

I understand difference between class and struct in Swift. Now I'm wondering what to use for hierarchy model.
To define a class is pretty simple (setting connections on properties set is now irrelevant).
class XYClass {
var title: String
var subinstances: [XYClass]
weak var superinstance: XYClass?
}
But it looks like pretty fine model for struct. Especially if I need to instantiate a lots of these and frequently. But I'm wondering if I can somehow safely point to superinstance or I need to store whole object graph to every instance on every change... Should I use class or struct and if struct, how to define it?
You are making a linked list. If you were to try to form a linked list of structs of a single type, memory management would not be feasible, and the compiler would stop you dead in your tracks. This won't compile:
struct XYClass {
var title: String
var subinstances: [XYClass]
var superinstance: XYClass?
}
The compiler has spotted the problem. You cannot refer to an instance of a struct as a property of that struct. (The compiler calls this a "recursive value type".)
Thus, for your situation, you must use a class, because only then can you get a weak reference and avoid a retain cycle. Only a reference to a class can be weak (and only if the reference is typed as an Optional).
This will compile, and will give your linked list coherent memory management:
class XYClass {
var title: String = ""
var subinstances: [XYClass] = []
weak var superinstance: XYClass?
}