SKScene with bindings in a SpriteView - swiftui

I've created a SpriteKit Scene file and the corresponding SKScene object. That scene object includes an #Binding property.
When I use SpriteView in my SwiftUI file, I can't figure out how to initialize the scene so that it loads from the sks file and also assigns a binding.
I'm wanting something like this:
class MyScene: SKScene {
#Binding var foo: CGFloat
init(foo: Binding<CGFloat>) {
_foo = foo
super.init(fileNamed: "MyScene")
}
}
That doesn't work though because init(fileNamed:) is a convenience initializer, not a designated initializer.

A possible workaround using ObservableObject to subscribe to foo. I pass a fileName so I have an excuse to invoke my convenience initializer.
class MyScene: SKScene, ObservableObject {
#Published var foo: CGFloat = 0
convenience init(fileName: String) {
self.init(fileNamed: fileName)!
}
}
And then hold a reference to it on your view (or elsewhere).
struct MyView: View {
#ObservedObject var scene = MyScene(fileName: "MyScene")
var body: some View {
VStack {
FooView($scene.foo)
SpriteView(scene: scene)
}
}
}

Related

SwiftUI pass Binding by ref to a child ViewModel

In SwiftUI, I am trying to create some binding between a parent ViewModel and a child ViewModel, here is a simplified example of my scenario:
The parent component:
class ParentViewModel : ObservableObject {
#Published var name = "John Doe"
func updateName() {
self.name = "Jonnie Deer"
}
}
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
ChildView(name: $viewModel.name)
// tapping the button the text on parent view is updated but not in child view
Button("Update", action: viewModel.updateName)
}
}
}
The child component:
class ChildViewModel : ObservableObject {
var name: Binding<String>
var displayName: String {
get {
return "Hello " + name.wrappedValue
}
}
init(name: Binding<String>) {
self.name = name
}
}
struct ChildView: View {
#StateObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
init(name: Binding<String>) {
_viewModel = StateObject(wrappedValue: ChildViewModel(name: name))
}
}
So, as stated in the comments, when I tap the button on the parent component the name is not getting updated in ChildView, as if the binding is lost...
Is there any other way to update view model with the updated value? say something like getDerivedStateFromProps in React (becuase when tapping the button the ChildView::init method is called with the new name.
Thanks.
Apple is very big on the concept of a Single Source of Truth(SSoT), and keeping it in mind will keep you from getting into the weeds in code like this. The problem you are having is that while you are using a Binding to instantiate the child view, you are turning around and using it as a #StateObject. When you do that, you are breaking the connection as #StateObject is supposed to sit at the top of the SSoT hierarchy. It designates your SSoT. Otherwise, you have two SSoTs, so you can only update one. The view model in ChildView should be an #ObservedObject so that it connects back up the hierarchy. Also, you can directly instantiate the ChildViewModel when you call ChildView. The initializer just serves to decouple things. Your views would look like this:
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
// You can directly use the ChildViewModel to instantiate the ChildView
ChildView(viewModel: ChildViewModel(name: $viewModel.name))
Button("Update", action: viewModel.updateName)
}
}
}
struct ChildView: View {
// Make this an #ObservedObject not a #StateObject
#ObservedObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
}
Neither view model is changed.
Get rid of the view model objects and do #State var name = “John” in ParentView and #Binding var name: String in ChildView. And pass $name into ChildView’s init which gives you write access as if ParentView was a view model object.
By using #State and #Binding you get the reference type semantics you want inside a value type which is the power of SwiftUI. If you just use objects you lose that benefit and have more work to do.
We usually only use ObservableObject for model data but we can also use it for loaders/fetchers where we want to tie some controller behaviour to the view lifecycle but for data transient to a view we always use #State and #Binding. You can extract related vars into their own struct and use mutating funcs for other logic and thus have a single #State struct used by body instead of multiple. This way it can still be testable like a view model object in UIKit would be.

Is there a way decouple views from view models like the following?

My target is 2 thing:
1. to make a view depending on a view model protocol not a concrete class.
2. a sub view gets the view model from the environment instead of passing it through the view hierarchy
I've mentioned my goals so if there's a totally different way to achieve them, I'm open to suggestion.
Here's what've tried and failed of course and raised weird error:
struct ContentView: View {
var body: some View {
NavigationView {
MyView()
}
}
}
struct MyView: View {
#EnvironmentObject var viewModel: some ViewModelProtocol
var body: some View {
HStack {
TextField("Enter something...", text:$viewModel.text)
Text(viewModel.greetings)
}
}
}
//MARK:- View Model
protocol ViewModelProtocol: ObservableObject {
var greetings: String { get }
var text: String { get set }
}
class ConcreteViewModel: ViewModelProtocol {
var greetings: String { "Hello everyone..!" }
#Published var text = ""
}
//MARK:- Usage
let parent = ContentView().environmentObject(ConcreteViewModel())
Yes there is, but it's not very pretty.
You're running into issues, since the compiler can't understand how it's ever supposed to infer what type that that some protocol should be.
The reason why some works in declaring your view, is that it's inferred from the type of whatever you supply to it.
If you make your view struct take a generic viewmodel type, then you can get this up and compiling.
struct MyView<ViewModel: ViewModelProtocol>: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
Text(viewModel.greetings)
}
}
the bummer here, is that you now have to declare the type of viewmodel whenever you use this view, like so:
let test: MyView<ConcreteViewModel> = MyView()

EnvironmentObject not found for child Shape in SwiftUI

I am encountering the following SwiftUI error with #EnvironmentObject when used with a custom Shape, :
Fatal error: No ObservableObject of type MyObject found. A View.environmentObject(_:) for MyObject may be missing as an ancestor of this view.: file SwiftUI, line 0
It only happens when I use any Shape method that returns a new copy of the instance like stroke().
Here is a Swift playground example to reproduce:
import SwiftUI
import PlaygroundSupport
class MyObject: ObservableObject {
#Published var size: Int = 100
}
struct MyShape: Shape {
#EnvironmentObject var envObj: MyObject
func path(in rect: CGRect) -> Path {
let path = Path { path in
path.addRect(CGRect(x: 0, y: 0,
width: envObj.size, height: envObj.size))
}
return path
}
}
struct MyView: View {
var body: some View {
MyShape().stroke(Color.red) // FAIL: no ObservableObject found
// MyShape() // OK: it works
}
}
let view = MyView().environmentObject(MyObject())
PlaygroundPage.current.setLiveView(view)
As it looks like environment field is not copied, I've also tried to do it explicitly like this:
struct MyView: View {
#EnvironmentObject var envObj: MyObject
var body: some View {
MyShape().stroke(Color.red).environmentObject(self.envObj)
}
}
It still fails. As a SwiftUI beginner, I don't know whether this is the expected behavior, not inheriting the view hierarchy environment, and how to handle it - other than not using the environment.
Any idea?
The problem actually is that .stroke is called right after constructor, so before environmentObject injected (you can test that it works if you comment out stroke). But .stroke cannot be added after environment object injected, because .stroke is Shape-only modifier.
The solution is to inject dependency during construction as below. Tested with Xcode 11.4 / iOS 13.4
struct MyShape: Shape {
#ObservedObject var envObj: MyObject
...
}
struct MyView: View {
#EnvironmentObject var envObj: MyObject
var body: some View {
MyShape(envObj: self.envObj).stroke(Color.red)
}
}

Classes and Observable Object

I'm trying to make a data model class that can be referenced by different views. The data model has a function that can modify one of its published variables. However, this function is called inside one view, the change it makes to the published variable is not reflected in other views which also reference the class. The most simple example I can come up with is this:
struct ContentView: View {
var body: some View {
VStack {
TextView()
ButtonView()
}
}
}
struct TextView: View {
#ObservedObject var data = Data()
var body: some View {
Text(data.currentWord)
}
}
struct ButtonView: View {
#ObservedObject var data = Data()
var body: some View {
Button(action: {self.data.randomWord()}) {
Text("Random word")
}
}
}
class Data: ObservableObject {
#Published var currentWord = "Cat"
func randomWord() {
let word = ["Cat", "Dog", "Mouse", "Horse"].randomElement()!
print(word)
currentWord = word
}
}
Both the ButtonView and TextView reference the same class, and the ButtonView calls the 'Data' class's method 'randomWord' which modifies its 'currentWord' published variable. However, the change to this variable is not reflected in the Text of the TextView which also references the 'Data' class.
I think I'm not understanding something about classes and observableObject correctly. Would anyone be kind enough to point out my mistake here?
You create two different instance of Data in your subviews, instead you need to share one, so create it in ContentView and pass to subviews as below
struct ContentView: View {
#ObservedObject var data = Data()
var body: some View {
VStack {
TextView(data: data)
ButtonView(data: data)
}
}
}
struct TextView: View {
#ObservedObject var data: Data
var body: some View {
Text(data.currentWord)
}
}
struct ButtonView: View {
#ObservedObject var data: Data
var body: some View {
Button(action: {self.data.randomWord()}) {
Text("Random word")
}
}
}
Also, as variant, for such scenario can be used EnvironmentObject pattern. There are a lot of examples here on SO you can find about environment objects usage - just search by keywords.

How could I initialize the #State variable in the init function in SwiftUI?

Let's see the simple source code:
import SwiftUI
struct MyView: View {
#State var mapState: Int
init(inputMapState: Int)
{
mapState = inputMapState //Error: 'self' used before all stored properties are initialized
} //Error: Return from initializer without initializing all stored properties
var body: some View {
Text("Hello World!")
}
}
I need the init function here because I want to do some data loading here, but there is one problem, the #State variable could not be initialize here! How could I do with that?
Maybe it's a very simple question, but I don't know how to do.
Thanks very much!
Property wrappers are generating some code for you. What you need to know is the actual generated stored property is of the type of the wrapper, hence you need to use its constructors, and it is prefixed with a _. In your case this means var _mapState: State<Int>, so following your example:
import SwiftUI
struct MyView: View {
#State var mapState: Int
init(inputMapState: Int)
{
_mapState = /*State<Int>*/.init(initialValue: inputMapState)
}
var body: some View {
Text("Hello World!")
}
}
We can inject the data that the view needs.
Used a model which has access to the data that you wanted. Make a map view and use that instance of it in your parent view. This will also help to unit test the model.
Used the property wrapper #Binding to pass the data from the parent view to MapView and used _mapState which holds the value of mapState.
struct Model {
//some data
}
struct MapView {
private let model: Model
#Binding var mapState: Int
init(model: Model, mapState: Binding<Int>) {
self.model = model
self._mapState = mapState
}
}
extension MapView: View {
var body: some View {
Text("Map Data")
}
}
I think that it would better to initialize when you write the code, just like:
#State var mapState = 0
or, if you want to binding the value with another view, use #Binding.
You have more information at https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-binding-property-wrapper