I am currently working on a project, where I use SpriteView to display game content and normal Views to display menus and navigation in general. I am able to create and load SpriteViews when pressing buttons in the normal view but the communication does not work the other way. I want to be able to change State variables in the parent View-Element by using buttons/SKShapeNodes of the child SpriteView-Element.
I tried Binding variables between the two instances and using callback-functions. But I wasn't able to change any content in the View-Element.
Is there a simple and effective way to send requests from a child-SpriteView to the parent-View ?
You can use the ObservableObject - #Published pattern for this.
Make your GameScene conform to the ObservableObject protocol and publish the properties that you are interested to send the values of into the SwiftUI views, something like this:
class GameScene: SKScene, ObservableObject {
#Published var updates = 0
#Published var isPressed = false
// ...
}
Then, in your SwiftUI view, use a #StateObject property (or an #ObservedObject or an #EnvironmentObject) to store the GameScene instance. You can then use the GameScene's published properties in your SwiftUI view:
struct ContentView: View {
#StateObject private var scene: GameScene = {
let scene = GameScene()
scene.size = CGSize(width: 300, height: 400)
return scene
}()
var body: some View {
ZStack {
SpriteView(scene: scene).ignoresSafeArea()
VStack {
Text("Updates from SKScene: \(scene.updates)")
if scene.isPressed {
Text("isPressed is true inside GameScene")
}
}
}
}
}
When you "press buttons in the normal view", change the value of the published properties, and they will change the SwiftUI views that uses them.
Related
I have a view which has a function fadeInOut() that fades a square in and out.
struct FadingSquare: View {
#State var fading = false
var body: some View {
Rectangle()
.frame(width: 20, height: 20)
.foregroundColor(.primary)
.opacity(fading ? 1 : 0)
.onAppear {
fadeInOut()
}
}
func fadeInOut() {
withAnimation(.linear(duration: 1)) {
self.fading = true
}
withAnimation(.linear(duration: 1).delay(1)) {
self.fading = false
}
}
}
That works fine, the square fades in, then fades out.
But I want to place FadingSquare in other places, and call fadingSquare.fadeInOut() from a parent view. This is what I have attempted:
struct ContentView: View {
var fadingSquare = FadingSquare()
var body: some View {
VStack {
fadingSquare
Button {
fadingSquare.fadeInOut()
} label: {
Text("Fade")
}
}
}
}
This does not work, and making fadingSquare a #State doesn't work either. I'd rather not use Bindings, I want the FadingSquare to be responsible for its own values.
So in SwiftUI, parents can modify their children in really just two ways.
Within the parent view, (ContentView) add a #State variable to track the fade. Within the child view, (FadingSquare) change the #State variable to #Binding and remove the default value. Now, within ContentView, you can have the square update when the parent's fade variable changes by writing it in the body as FadingSquare(fading: $ourFadingState) where ourFadingState is that #State variable we created.
The parent and the child each hold a reference to the same ObservableObject. This is the most performant way to do it when dealing with complex UIs. Within the parent's button action, you'd modify the object's fading property. Within the FadingSquare, you'd have an #ObservedObject variable which would cause the square to update itself upon any changes, regardless of if they were made within the view itself, a parent, or on the moon.
By the way, for SwiftUI to work properly, you don't keep a fadingView property, you instantiate a brand new one within the body code. This is because when it's a property, it can't update until the parent gets trashed, which will never happen because ContentView is typically the root view of a scene and will never get destroyed. Look at the system SwiftUI views and notice you don't need to hold them within variables either. We do it the same way.
So instead of:
var fadingSquare = FadingSquare()
var body: some View {
fadingSquare
}
It should be:
var body: some View {
FadingSquare()
}
Remember that unlike UIKit, a SwiftUI view struct is not the view itself. It is a collection of data describing to the system what the view should contain during a given refresh, like HTML. So initializing a new FadingSquare() within body code is correct and it will be the same view, even if it is "a new" struct in code.
When using different property wrappers associated with view updates, changes in one place affect rendering of views that do not use that property.
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
#State var thirdTitle = "thirdTitle"
var body: some View {
VStack {
Text(viewModel.firstTitle)
.background(.random)
Text(viewModel.secondTitle)
.background(.random)
Text(thirdTitle)
.background(.random)
Button("Change First Title") {
viewModel.chageFirstTitle()
}
}
}
}
class MyViewModel: ObservableObject {
#Published var firstTitle = "firstTitle"
#Published var secondTitle = "secondTitle"
func chageFirstTitle() {
firstTitle = "hello world"
}
}
I understand that the reason why the Text exposing the viewModel.secondTitle is re-rendered is because the #StateObject varviewModel = MyViewModel() dependency changed when the `viewModel.firstTitle changed.
However, I don't know why Text using #State var thirdTitle = "thirdTitle" is re-rendered too. In WWDC21 session Demystify SwiftUI, I saw that the view is re-rendered only when the related dependency is updated according to the dependency graph. But, even though the thirdTitle is irrelevant to the change of the viewModel, third Text using that dependency is re-rendered and the background color is changed.
What's even more confusing is that if I seperate the third Text into a separate view ( ThirdView ) and receive the thirdTitle using #Binding, the background color does not change because it is not re-rendering at that time.
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
#State var thirdTitle = "thirdTitle"
var body: some View {
VStack {
Text(viewModel.firstTitle)
.background(.random)
Text(viewModel.secondTitle)
.background(.random)
ThirdView(text: $thirdTitle)
.background(.random)
Button("Change First Title") {
viewModel.chageFirstTitle()
}
}
}
}
struct ThirdView: View {
#Binding var text: String
var body: some View {
Text(text)
.background(.random)
}
}
Regarding the situation I explained, could you help me to understand the rendering conditions of the view?
To render SwiftUI calls body property of a view (it is computable, i.e. executes completely on call). This call is performed whenever any view dependency, i.e. dynamic property, is changed.
So, viewModel.chageFirstTitle() changes dependency for ContentView and ContentView.body is called and every primitive in it is rendered. ThirdView also created but as far as its dependency is not changed, its body is not called, so content is not re-rendered.
A few things wrong here. We don't use view model objects in SwiftUI for view data, it's quite inefficient/buggy to do so. Instead, use a struct with mutating funcs with an #State. Pass in params to sub-Views as lets for read access, #Binding is only when you need write access. In terms of rendering, first of all body is only called if the let property is different from the last time the sub-View is init, then it diffs the body from the last time it was called, if there are any differences then SwiftUI adds/removes/updates actual UIKit UIViews on your behalf, then actual rendering of those UIViews, e.g. drawRect, is done by CoreGraphics.
struct ContentViewConfig {
var firstTitle = "firstTitle"
var secondTitle = "secondTitle"
mutating func changeFirstTitle() {
firstTitle = "hello world"
}
}
struct ContentView: View {
#State var config = Config()
...
struct ThirdView: View {
let text: String
...
Combine's ObservableObject is usually only used when needing to use Combine, e.g. using combineLatest with multiple publishers or for a Store object to hold the model struct arrays in #Published properties that are not tied to a View's lifetime like #State. Your use case doesn't look like a valid use of ObservableObject.
Can I use the update function in a SKScene class to animate a view in SwiftUI?
In a project I use something like this:
class X: SKScene, ObservableObject{
#Published var x: CGFloat = 30
//...
}
struct ContentView: View{
#ObservedObject var scene = X()
var body: some View{
ZStack{
SpriteView(scene: scene)
Circle().frame(width: scene.l, height: scene.l)
}
}
}
When the update function is called, then the property "x" can be changed and then the Circle should change too. In the other project I saw that there is a growing memory usage over time. I don't know why this is happening. Perhaps there is somebody who has an idea.
i'm trying to do a simple login screen using swiftUI.
i put my isLogin into a class and set it as an EnviromentObject Variable.
How do I use it in a fullscreen cover?
My class
class AuthUser:ObservableObject {
#Published var isLogin:Bool = false
#Published var isCorrect:Bool = false
}
My View :
struct UIController: View {
#EnvironmentObject var userAuth : AuthUser
#State var showLogin:Bool = false
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $showLogin, content: AfterLogin.init)
}
}
You can use binding directly to property of environment object, like
struct UIController: View {
#EnvironmentObject var userAuth : AuthUser
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $userAuth.isLogin, content: AfterLogin.init)
}
}
The structure you're attempting to follow is called MVVM or Model-View-View-Model, which is nearly a requirement for swiftUI I believe the only thing that you're missing is the actual usage of that ViewModel, or in your case the AuthUser. So let's dig into that.
View Model
class AuthUser: ObservableObject {
#Published var isLogin = false
//Any other code, methods, constructors you want.
}
View
struct UIController: View {
#ObservedObject var userAuth = AuthUser()
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $userAuth.isLogin, content: AfterLogin.init)
}
}
What did I change? I changed your #EnvironmentObject to a ObservedObject because in your case you're not likely to actually be using an environment object. What are those special tags?
ObservableObject, this means that it can be observed for state changes.
ObservedObject, an object that is watching for state changes.
Published, an object that has public availability.
EnvironmentObject, an object that is created for the environment and accessible wherever in the app. This particular object is not immediately disposed which is why we removed it in your example.
So what's the purpose of doing things this way? Well it has to do with abstraction and readability. MVVM frameworks provide a way to Bind a view to an object. That binding means that any changes on the view change the data, and any changes to the data change the view. Swift makes this concept simple enough. In your case we're binding the isPresented to the ObservedObject and checking that objects isLogin state. If it changes, the view is also changed.
This is more a general question: I'm working on my first SwiftUI project and using MVVM for the first time as well. After programming some views I realized that I need for almost each view two different view models. Often it's the view model for the current view and the view model of the previous/mother view. Is this "normal" or is this a hint that I've designed my project wrong and abusing MVVM?
For example:
I have a view where I list all flashcard decks. For this view I have a decksViewModelthat looks like this:
class DeckListViewModel: ObservableObject{
#Published var decks = [Deck]
#Published var showDeck = false // this value will be true if i tab on a deck and the deck will shown in a detailed view. This value is checked in the list
#Published var expandButton = false
#Published var showDownloadCenter = false
#Published var showCreateDeck = false
}
Now I have a deckDetailView for the detailed view of my deck. The deckDetailViewModel stores the selected item. But to remove this view I need to change the value of the showDeck? in decksViewModel`. So I need to pass this view model as well.
I wouldn't say that's a good way to use MVVM.
Instead of storing showDeck in DeckListViewModel, you could just have it as a local #State variable in whatever view you're using it in. Or if you're using a NavigationView, just use a NavigationLink, and there'll be no need for any state variable.
struct DecksView: View {
#ObservedObject var deckListVM = DeckListViewModel()
var body: some View {
NavigationView {
VStack {
ForEach(deckListVM.decks) { deck in
NavigationLink(destination: DeckDetailView(deckDetailVM: DeckDetailViewModel(deck: deck))) {
// some view
}
}
}
}
}
}
I'm not sure how you implemented your DeckDetailViewModel, just guessing there.