Observing a generic class - swift

I am new to SwiftUI.
I have a button struct that contains the following property:
struct CircleTextButton: View {
#ObservedObject var controlModel:MyModelA
// bla bla
I would like to be able to reuse this button. So, I need this controlModel property to be able to assume other values, beyond MyModelA. For example, MyModelB
How should I declare this?
I have tried
#ObservedObject var controlModel:Any
but I get
Property type 'Any' does not match that of the 'wrappedValue' property of its wrapper type 'ObservedObject'
Any ideas?

You need to make your view generic over the type of controlModel and constraint Model to be ObservableObject.
struct CircleTextButton<Model: ObservableObject>: View {
#ObservedObject var controlModel: Model
...
To create a specific button with a specific model, you need to do
let button = CircleTextButton(controlModel: MyModelA())

Related

Cannot assign value of type 'Binding<Bool>' to type 'Bool'

I'm having trouble with initialising a Bool, it keeps giving me errors and I can't seem to find the solution. The error I'm getting with the below code is "Cannot assign value of type 'Binding' to type 'Bool'"
Any ideas?
struct ProfileView: View {
#ObservedObject var viewModel: ProfileViewModel
#Binding var isFollowed: Bool
init(user: User) {
self.viewModel = ProfileViewModel(user: user)
// error below
self.isFollowed = $isFollowed
// error above
}
I'm not clear what you want to do, but the $ notation is when you pass a property wrapper, such as #State or #Published to someone else.
For example if you want to initialize your #Binding property with some value passed on initialization:
First you need a corresponding argument in the init, and you initialize its value by using the "special" syntax with underscore:
init(...,
isFollowed: Binding<Bool>) {
// This is how binding is initialized
self._isFollowed = isFollowed
Now we assume that some other class or struct (lets call it Other), which has some sort of state or published property:
#Published var isProfileFollowed = false
So from that Other class/struct you can create an instance of ProfileView like this:
ProfileView(...,
isFollowed: $isProfileFollowed)
That is not just passing a current value of isProfileFollowed, but binding a isFollowed of ProfileView to isProfileFollowed of class/struct Other, so that any change in isProfileFollowed is also visible to a binded property isFollowed.
So this is just an explanation of what's not working.

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 Do You Pass A View Input Argument To #State Variable?

I have a basic SwiftUI question - I have a view that takes argument, in my case "symbolName" for which I would like to fetch prices. I have a class function that does this, but passing the view argument to the FetchPrice as an argument does not work. When I use a fixed string, such as "GE", it works. I am sure there is a right way to do this, thanks for any hints and tips!
Error:
Cannot use instance member 'symbolNameV' within property initializer;
property initializers run before 'self' is available
import SwiftUI
struct SymbolRow2: View {
var symbolNameV: String
#ObservedObject var fetchPrice = FetchPrice(symbolName:symbolNameV)
...
property initializers run before 'self' is available
By calling
FetchPrice(symbolName: symbolNameV)
you're accessing self. The code above is actually:
FetchPrice(symbolName: self.symbolNameV)
To solve this you can create a custom init:
struct SymbolRow2: View {
private var symbolNameV: String
#ObservedObject private var fetchPrice: FetchPrice
init(symbolNameV: String) {
self.symbolNameV = symbolNameV
self.fetchPrice = FetchPrice(symbolName: symbolNameV)
}
...
}

How to persist data using MVVM in SwiftUI?

I’m practicing MVVM and SwiftUI by making a simple practice app. The main view of the app is a list where presents a title (in each cell) that can change by user input (text field). By selecting the cell, the app presents you the detail view where it presents another text.
I managed to change the cell´s title but I can’t figure out how to change the text in the detail view and make it stay that way. When I change the text in the detail view and go back to the main view, after entering again, the text doesn’t stay the same.
How can I make the text in the detail view maintain the text of whatever the user writes?
Your Sandwish is a struct which means when you pass it around it's copied (See structure vs class in swift language). This also means that when you pass a sandwish:
CPCell(sandwish: sandwish)
...
struct CPCell: View {
#State var sandwish: Sandwish
...
}
a sandwish is copied - any changes you make on this copy will not apply to the original sandwish.
When you do $sandwish.name in CPCell you're already binding to a copy. And in the NavigationLink you're copying it again.
struct CPCell: View {
...
var body: some View {
NavigationLink(destination: SandwishDetail(sandwish: sandwish)) {
TextField("Record", text: $sandwish.name)
}
}
}
So in the SandwishDetail you're already using a copy of a copy of your original sandwish.
struct SandwishDetail: View {
#State var sandwish: Sandwish // <- this is not the same struct as in `ContentView`
...
}
One way is to make Sandwish a class. Another, maybe better, solution is to use #Binding.
Note that the change from #State var sandwish to #Binding is not enough. CPCell expects the sandwish parameter to be Binding<Sandwish>, so you can't just pass a struct of type Sandwish.
One of the solutions is to use an index to access a binding from the SandwishStore:
ForEach (0..<store.sandwishes.count, id:\.self) { index in
CPCell(sandwish: self.$store.sandwishes[index])
}
...
struct CPCell: View {
#Binding var sandwish: Sandwish
...
}
Also you should do the same for all other places where the compiler expects Binding<Sandwish> and you originally passed Sandwish.
Change #State to #Binding in Sandwish.swift and in your CPCell struct in ContentView.swift

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.