I am trying to use environmentObject in a watchOS6 app to bind my data model to my view.
I have created a simple, stand-alone Watch app in Xcode 11.
I created a new DataModel class
import Combine
import Foundation
import SwiftUI
final class DataModel: BindableObject {
let didChange = PassthroughSubject<DataModel,Never>()
var aString: String = "" {
didSet {
didChange.send(self)
}
}
}
In my ContentView struct I bind this class using #EnvironmentObject -
struct ContentView : View {
#EnvironmentObject private var dataModel: DataModel
var body: some View {
Text($dataModel.aString.value)
}
}
Finally, I attempt to inject an instance of the DataModel into the environment in the HostingController class -
class HostingController : WKHostingController<ContentView> {
override var body: ContentView {
return ContentView().environmentObject(DataModel())
}
}
But, I get an error:
Cannot convert return expression of type '_ModifiedContent<ContentView, _EnvironmentKeyWritingModifier<DataModel?>>' to return type 'ContentView'
The error is because the WKHostingController is a generic that needs a concrete type - WKHostingController<ContentView> in this case.
A similar approach works perfectly with UIHostingController in an iOS app because UIHostingController isn't a generic class.
Is there some other way to inject the environment to a watchOS view?
You can use type erasure, AnyView in the case of SwiftUI View.
I would refactor WKHostingController to return AnyView.
This seems to compile fine on my end.
class HostingController : WKHostingController<AnyView> {
override var body: AnyView {
return AnyView(ContentView().environmentObject(DataModel()))
}
}
For anyone like Brett (in the comments) who was getting
"Property 'body' with type 'AnyView' cannot override a property with type 'ContentView'"
I got the same error because I hadn't replaced the return value and wrapped the ContentView being returned.
ie. this is what my first attempt looked like.. notice the
WKHostingController<ContentView>
that should be
WKHostingController<AnyView>
class HostingController : WKHostingController<ContentView> {
override var body: AnyView {
return AnyView(ContentView().environmentObject(DataModel()))
}
}
Adding to Matteo's awesome answer,
If you want to use delegate then use like this:
class HostingController : WKHostingController<AnyView> {
override var body: AnyView {
var contentView = ContentView()
contentView.environmentObject(DataModel())
contentView.delegate = self
let contentWrapperView = AnyView(contentView)
return contentWrapperView
}
}
Related
Context
I have a SwiftUI View which gets initialised with a ViewModel (Observable Object). This ViewModel itself has a Generic Type.
I am now trying to use the Generic Type of the ViewModel inside the View itself, however, can't get hold of it.
Code
class ComponentViewModel<C: Component>: ObservableObject { ... }
struct SomeView: View {
#ObservedObject var componentVM: ComponentViewModel
init(with componentVM: ComponentViewModel) {
componentVM = componentVM
}
var body: some View {
switch componentVM.C.self { ... } // This does not work.
}
Question
How can I use the Generic Type of Types Property, in this case of componentVM?
You need to make your View generic as well
struct SomeView<ModelType: Component>: View
and then you use ModelType in your code to refer to the generic type of the view model
struct SomeView<ModelType: Component>: View {
#ObservedObject var componentVM: ComponentViewModel<ModelType>
init(with componentVM: ComponentViewModel<ModelType>) {
self.componentVM = componentVM
}
var body: some View {
switch ModelType.self {
//...
}
}
}
You should declare your componentVM like that:
#ObservedObject var componentVM: ComponentViewModel<SomeSolideType>
SomeSolidType should be some class / struct conforming to your Component protocol.
protocol ErrorableViewProtocol: View {
var error: Error? { get set }
}
class ObservableError: ObservableObject {
#Published var error: Error?
}
struct ErrorableView<T: ErrorableViewProtocol>: View {
var errorable: T
var body: some View {
if let error = errorable.error {
ErrorView(error: error)
} else {
errorable
}
}
}
Where did I stray off the righeous path?
ObservableError conforms to ObservableObject. You can't say the same about Optional<ObservableError> (aka ObservableError?). When you use #StateObject, you should always instantiate the class in the same struct.
However, when you want to pass in an object from another struct, use #ObservedObject.
Because my actual code is a bit more complicated, here is a simplified class structure with which I can reproduce the same unexpected behavior.
This is my base data object which I subclass:
class People: Identifiable {
var name: String
required init(name: String) {
self.name = name
}
}
class Men: People {
}
And then I use another class which acts also as superclass, but also uses a generic type of People.
class SuperMankind<PlayerType: People> {
var people: [PlayerType] = []
}
class Mankind: SuperMankind<Men> {
}
Now I want to use this this Mankind subclass in my ViewModel, which is an ObservableObject.
class ViewModel: ObservableObject {
#Published var mankind: Mankind
init(_ m: Mankind) {
mankind = m
}
}
struct TestView: View {
#StateObject var viewModel = ViewModel(Mankind())
var body: some View {
VStack {
Button("Add") {
viewModel.mankind.people.append(Men(name: Int.random(in: 0...1000).description))
}
List {
ForEach(viewModel.mankind.people) {
Text($0.name)
}
}
}
}
}
But my view does not update if I click the add button and I don't know why. I figured out that if I add the following code to my button action the view updates. But this manual call should not be necessary in my opinion so I assume I do something wrong.
viewModel.objectWillChange.send()
ObservableObject requires that its fields are structs, not classes.
I changed your code slightly and it worked:
protocol SuperMankind {
associatedtype PlayerType
var people: [PlayerType] { get set }
}
struct Mankind: SuperMankind {
var people: [Men] = []
}
Screenshot here
Re your solution (since I can't comment):
Array<Men> is a struct, despite the array holding class references. This is why your code works now, as before you were directly holding a reference to a class in your ObservableObject (which therefore did not update the view).
#SwiftSharp thanks for your answer and the associatedType I didn't thought about this. But that #Published fields need to be structs is incorrect think about this solution, which I will choose for now, because I don't want to make all my functions mutating.
class People: Identifiable {
var name: String
required init(name: String) {
self.name = name
}
}
class Men: People {
}
class SuperMankind<PlayerType: People>: ObservableObject {
#Published var people: [PlayerType] = []
}
class Mankind: SuperMankind<Men> {
}
struct TestView: View {
#StateObject var viewModel = Mankind()
var body: some View {
VStack {
Button("Add") {
viewModel.people.append(Men(name: Int.random(in: 0...1000).description))
}
List {
ForEach(viewModel.people) {
Text($0.name)
}
}
}
}
}
My problem was that the ViewModel class was not necessary and that my superclass, which holds the people array was not an ObervableObject.
Edit just to be complete here:
This would also fix my initial code problem, with the usage of the ViewModel class, but instead of subclassing ObservableObject I would subclass from Mankind, which already conforms to ObservableObject by subclassing SuperMankind:
class ViewModel: Mankind {
}
How to pass data between UIViewController and struct ContentView?
I tried with ObservableObject but I can't get the data up to date.
To pass data from a UIViewController to an SwiftUI-Struct inside an UIHostingController you can attach an environmentObject to the SwiftUI rootView:
let vc = UIHostingController(rootView: YourContentView().environmentObject(yourEnvironmentObject))
Of course you'll need to create an ObservableObject and add it to your SwiftUI-Struct.
Create the ObservableObject:
class TypeOfEnvironmentObject: ObservableObject {
#Published var data = "myData"
}
Add it to your struct:
#EnvironmentObject var yourEnvironmentObject: TypeOfEnvironmentObject
I found the existing answers confusing/incomplete, perhaps something changed around generic inference in Swift 5.3 etc. While you can add an environment object to the UIHostingController's view this seems to conflict with the types (i.e. the generic parameter to UIHostingController needs a concrete type). Adding AnyView resolves this:
import UIKit
import SwiftUI
struct TutorialView: View {
#EnvironmentObject private var integration: TutorialIntegrationService
var body: some View {
Text("Hi").navigationBarTitle("test: \(integration.id)")
}
}
class TutorialIntegrationService: ObservableObject {
#Published var id: Int = 0
}
class TutorialViewController: UIHostingController<AnyView> {
let integration = TutorialIntegrationService()
required init?(coder: NSCoder) {
super.init(coder: coder,rootView: AnyView(TutorialView().environmentObject(integration)));
}
}
Add class myclassname: ObservableObject
In the class create a variable with #Published var myvar and add:
init(myvar: type) {
self.myvar = myvar
}
In UIViewController add:
private var model = myclassname(myvar: XXX)`
and in viewWillAppear add:
let vc = myclassname(myvar: myvar)
let childView = UIHostingController(rootView: ContentView(model: vc))
In the struct add:
#ObservedObject var model: myclassname
I have two reusable views, Ex1 and Ex2. I am trying to show one of them depends on a condition alternately but I could not it.
ContentvIew:
struct ContentView: View {
#State var selector = false
var cvc = ContentViewController()
var body: some View {
ZStack { // ERROR: Protocol type 'Any' cannot conform to 'View' because only concrete types can conform to protocols
cvc.getView(t: selector)
Button(action: {
self.selector.toggle()
print(self.selector)
}) {
Text("Button")
}
}
}
}
Ex1 :
import SwiftUI
struct Ex1: View {
var body: some View {
Text("Ex 1")
}
}
Ex2 :
import SwiftUI
struct Ex2: View {
var body: some View {
Text("Ex 2")
}
}
ContentViewController :
import Foundation
class ContentViewController {
let a = Ex1()
let b = Ex2()
func getView (t: Bool) ->(Any){
if t {
return a
}
else {
return b
}
}
}
I think it is very simple but not for me, for now. Help for two things, please.
I want to understand this problem, and the solution.
Best way for alternate two view in a layout.
Thanks in advance.
As the error suggests the return type specified in ContentViewController's getView method does not conform to the protocols.
In SwiftUI everything you specified in body{} clause must be a type of View if you do not know what kind of view available at runtime.
You can specify AnyView type for unknown views.
So your error will be removed by changing the ContentViewController's code.
class ContentViewController {
let a = Ex1()
let b = Ex2()
func getView (t: Bool) -> (AnyView) {
if t {
return AnyView(a)
}
else {
return AnyView(b)
}
}
}