Open SleepiestAdam opened 1 year ago
Any update on this / you guys been able to reproduce the issue internally?
Hi @SleepiestAdam - sorry for the delayed response. We'll take a look at this next chance we get, and definitely appreciate the reproduction project you provided!
No worries, not sure if this is related (I suspect it is), but RiveViewModel.riveView also isn't available for some reason after initialisation.
import SwiftUI
import RiveRuntime
struct Onboarding: View {
@ObservedObject var viewModel = OnboardingViewModel.shared
var splash : RiveViewModel
init() {
self.splash = RiveViewModel(fileName: "Splash", fit: .fitHeight, autoPlay: true)
self.splash.riveView!.playerDelegate = viewModel // Thiss crashes
// Trying to tap into player delegate to hide the splash.view() after it's completed playback.
}
// Rest of code inc body.
}
In RiveViewModel it looks like one of your devs might have also suspected an issue given the TODO in the RiveViewModel class... Seems like it's (maybe?) getting deallocated somehow; but I have absolutely no idea why, as as far as I can tell it is not a weak ref and if I remove the playerDelegate assignation line then it does play the Rive animation perfectly fine, so it is loading my Splash.riv without any issues, and isn't crashing at "riveModel = try! RiveModel(fileName: fileName, extension: extension
, in: bundle)" of your internal code. Bit of an odd one...
// TODO: could be a weak ref, need to look at this in more detail.
open private(set) var riveView: RiveView?
private var defaultModel: RiveModelBuffer!
public init(
fileName: String,
extension: String = ".riv",
in bundle: Bundle = .main,
stateMachineName: String?,
fit: RiveFit = .contain,
alignment: RiveAlignment = .center,
autoPlay: Bool = true,
artboardName: String? = nil
) {
self.fit = fit
self.alignment = alignment
self.autoPlay = autoPlay
super.init()
riveModel = try! RiveModel(fileName: fileName, extension: `extension`, in: bundle)
sharedInit(artboardName: artboardName, stateMachineName: stateMachineName, animationName: nil)
}
For the first issue, yes, I think this is due to var confetti = RiveViewModel(fileName: "confetti", stateMachineName: "State Machine 1")
being reinitialized on the parent re-render. RiveViewModel
holds a reference to its view (which I think you caught in your second comment) and when calling confetti.view()
in the body, creates a new RiveView
view and a reference on the newly initialized view model. So an overview of what I think is happening is:
confetti
, the RiveViewModel
confetti.view()
, and thus a new view is created and confetti
holds a reference to this view (for controlling API purposes)confetti
(let's call it confetti2
)confetti2.view()
, and thus a new view is created and confetti2
holds a reference to this new viewconfetti.view
notices an update, so its UIViewRepresentable implementation of updateUIView
is called. Right now, we're not setting the new confetti2.view
to confetti.view
because that would essentially restart the animation/state machineconfetti.view
remains face up, and any new use of confetti2.triggerInput()
or any API for that matter, would not affect the view on screen (the original view)A quick and dirty hack that's probably not viable as a long-term solution is to wrap confetti
with a @StateObject
wrapper, so that it does not get reinitialized, and the view persists. But ultimately, I think we have to re-examine how our RiveViewModel
pattern should work in a SwiftUI paradigm. It's a relatively new implementation for Rive so there's definitely some room to learn from this and would love any suggestions here - a similar issue was reported: https://github.com/rive-app/rive-ios/issues/244
As for the second issue, riveView
is not set yet as a member variable on rive view model until you call .view()
or set the view manually on the view model, so you can't yet set the playerDelegate
upon just creating the view model. We've usually seen folks wrap the RiveViewModel in another class and implement the delegates there (small example here: https://github.com/rive-app/rive-ios/blob/main/Example-iOS/Source/Examples/SwiftUI/SwiftEvents.swift#L42), but I could see why you want it separated.
@zplata Any update on improvement of the SwiftUI implementation to negate some of these issues?
We're running into issues in instances where StateObject isn't an option (e.g. when trying to load a rive assets via a dynamic web url where the object needs creating as part of an initializer where StateObject isn't available).
As we approach our internal project completion in the coming 1-2 months going to soon be blocking us producing a production build.
Description
In SwiftUI if a Rive view is hosted within a parent view that has been updated when a @Published value has changed, then it appears the RiveRuntime no longer responds to either triggerInput or setInput work.
By the looks of it's it's duplicating the view the RiveViewModel is using / there's a lifecycle issue. I've produced a very basic demo showing a version that works when the Confetti isn't within a subview (red background), right alongside one within a subview (green background) that doesn't work.
Both print a statement out when tapped, but only the one directly embedded in the SplashScreen works, any RiveViewModel.view()'s within subviews fail to respond to any triggerInput or setInput.
Minimal code to reproduce the issue is attached and is based on the rive-ios demo project with minimal changes.
This is likely causing issues for anyone using Rive in a serious way in a SwiftUI app, as very few apps are likely to host a RiveViewModel where the parent view has never been updated by an ObservableObject.
Minimal Repro
This is based on the SwiftUI Demo app. Just replace the SplashScreen with the code below. Minimal Repo also attached:
Demo-App.zip