Best way to achieve animation in SwiftUI with a CurrentValueSubject value change - ios

I’m currently building out a new MVVM app in SwiftUI and wanted to see whether there was a cleaner way of assigning a value from a CurrentValueSubject to my ViewModel, whilst still achieving animations? 

I’m currently leaning toward solution 2 below as that keeps the animation code neatly within the View, but it’s quite annoying having to setup another State for every animated change. Hoping there’s another way!
Solution 1 - use sink and withAnimation together in the ViewModel:
ViewModel:
authenticationService.authenticationState
.receive(on: DispatchQueue.main)
.sink { [unowned self] state in
withAnimation(.easeInOut(duration: Constants.Animation.slideSpeed)) {
self.authenticationState = state
}
}
.store(in: &cancellables)
Solution 2 - Use assign in the ViewModel and onReceive (+ withAnimation) in the view
ViewModel:
authenticationService.authenticationState
.receive(on: DispatchQueue.main)
.assign(to: &$authenticationState)
View:
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
#State private var authenticationState: AuthenticationState = .loggedOut
private var shiftAnimation: AnyTransition {
.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
}
var body: some View {
ZStack {
switch authenticationState {
case .loggedOut:
LoginView()
.transition(shiftAnimation)
case .loggedIn:
AdminView()
.transition(shiftAnimation)
}
if viewModel.showProgressView {
ProgressView()
}
}
.onReceive(viewModel.$authenticationState) { state in
withAnimation(.easeInOut(duration: Constants.Animation.slideSpeed)) {
self.authenticationState = state
}
}
}
}

Related

Passed in published property not firing onReceive in iOS 14 but is in iOS 16 [duplicate]

I have a custom ViewModifier which simply returns the same content attached with a onReceive modifier, the onReceive is not triggered, here is a sample code that you can copy, paste and run in XCode:
import SwiftUI
import Combine
class MyViewModel: ObservableObject {
#Published var myProperty: Bool = false
}
struct ContentView: View {
#ObservedObject var viewModel: MyViewModel
var body: some View {
Text("Hello, World!")
.modifier(MyOnReceive(viewModel: viewModel))
.onTapGesture {
self.viewModel.myProperty = true
}
}
}
struct MyOnReceive: ViewModifier {
#ObservedObject var viewModel: MyViewModel
func body(content: Content) -> some View {
content
.onReceive(viewModel.$myProperty) { theValue in
print("The Value is \(theValue)") // <--- this is not executed
}
}
}
is SwiftUI designed to disallow onReceive to execute inside a ViewModifier or is it a bug ? I have a view in my real life project that gets bigger with some business logic put inside onReceive, so I need to clean that view by separating it from onReceive.
ok, this works for me:
func body(content: Content) -> some View {
content
.onAppear() // <--- this makes it work
.onReceive(viewModel.$myProperty) { theValue in
print("-----> The Value is \(theValue)") // <--- this will be executed
}
}
ObservableObject and #Published are part of the Combine framework if you aren't using Combine then you shouldn't be using a class for your view data. Instead, you should be using SwiftUI as designed and avoid heavy objects and either put the data in the efficient View data struct or make a custom struct as follows:
import SwiftUI
struct MyConfig {
var myProperty: Bool = false
mutating func myMethod() {
myProperty = !myProperty
}
}
struct ContentView: View {
#State var config = MyConfig()
var body: some View {
Text("Hello, World!")
.onTapGesture {
config.myMethod()
}
}
}
Old answer:
Try onChange instead
https://developer.apple.com/documentation/swiftui/scrollview/onchange(of:perform:)
.onChange(of: viewModel.myProperty) { newValue in
print("newValue \(newValue)")
}
But please don't use the View Model object pattern in SwiftUI, try to force yourself to use value types like structs for all your data as SwiftUI was designed. Property wrappers like #State will give you the reference semantics you are used to with objects.

Swift UI overwhelmed by high-frequency #StateObject updates?

Scenario
A simple SwiftUI App that consists of a TabView with two tabs. The App struct has a #StateObject property, which is being repeatedly and very quickly (30 times per second) updated by simulateFastStateUpdate.
In this example, simulateFastStateUpdate is not doing any useful work, but it closely resembles a real function that quickly updates the app's state. The function does some work on a background queue for a short interval of time and then schedules a state update on the main queue. For example, when using the camera API, the app might update the preview image as frequently as 30 times per second.
Question
When the app is running, the TabView does not respond to taps. It's permanently stuck on the first tab. Removing liveController.message = "Nice" line fixes the issue.
Why is TabView stuck?
Why is updating #StateObject causing this issue?
How to adapt this simple example, so that the TabView is not stuck?
import SwiftUI
class LiveController: ObservableObject {
#Published var message = "Hello"
}
#main
struct LiveApp: App {
#StateObject var liveController = LiveController()
var body: some Scene {
WindowGroup {
TabView() {
Text(liveController.message)
.tabItem {
Image(systemName: "1.circle")
}
Text("Tab 2")
.tabItem {
Image(systemName: "2.circle")
}
}
.onAppear {
DispatchQueue.global(qos: .userInitiated).async {
simulateFastStateUpdate()
}
}
}
}
func simulateFastStateUpdate() {
DispatchQueue.main.async {
liveController.message = "Nice"
}
// waits 33 ms ~ 30 updates per second
usleep(33 * 1000)
DispatchQueue.global(qos: .userInitiated).async {
simulateFastStateUpdate()
}
}
}
You are blocking the main thread with these constant updates and the app is busy processing your UI updates and can't handle touch inputs (also received on the main thread).
Whatever creates this rapid event stream needs to be throttled. You can use Combine's throttle or debounce functionality to reduce the frequency of your UI updates.
Look at this sample, I added the class UpdateEmittingComponent producing updates with a Timer. This could be your background component updating rapidly.
In your LiveController I'm observing the result with Combine. There I added a throttle into the pipeline which will cause the message publisher to fiere once per second by dropping all in-between values.
Removing the throttle will end up in an unresponsive TabView.
import SwiftUI
import Combine
/// class simulating a component emitting constant events
class UpdateEmittingComponent: ObservableObject {
#Published var result: String = ""
private var cancellable: AnyCancellable?
init() {
cancellable = Timer
.publish(every: 0.00001, on: .main, in: .default)
.autoconnect()
.sink {
[weak self] _ in
self?.result = "\(Date().timeIntervalSince1970)"
}
}
}
class LiveController: ObservableObject {
#Published var message = "Hello"
#ObservedObject var updateEmitter = UpdateEmittingComponent()
private var cancellable: AnyCancellable?
init() {
updateEmitter
.$result
.throttle(for: .seconds(1),
scheduler: RunLoop.main,
latest: true
)
.assign(to: &$message)
}
}
#main
struct LiveApp: App {
#StateObject var liveController = LiveController()
var body: some Scene {
WindowGroup {
TabView() {
Text(liveController.message)
.tabItem {
Image(systemName: "1.circle")
}
Text("Tab 2")
.tabItem {
Image(systemName: "2.circle")
}
}
}
}
}

How can I avoid this SwiftUI + Combine Timer Publisher reference cycle / memory leak?

I have the following SwiftUI view which contains a subview that fades away after five seconds. The fade is triggered by receiving the result of a Combine TimePublisher, but changing the value of showRedView in the sink publisher's sink block is causing a memory leak.
import Combine
import SwiftUI
struct ContentView: View {
#State var showRedView = true
#State var subscriptions: Set<AnyCancellable> = []
var body: some View {
ZStack {
if showRedView {
Color.red
.transition(.opacity)
}
Text("Hello, world!")
.padding()
}
.onAppear {
fadeRedView()
}
}
func fadeRedView() {
Timer.publish(every: 5.0, on: .main, in: .default)
.autoconnect()
.prefix(1)
.sink { _ in
withAnimation {
showRedView = false
}
}
.store(in: &subscriptions)
}
}
I thought this was somehow managed behind the scenes with the AnyCancellable collection. I'm relatively new to SwiftUI and Combine, so sure I'm either messing something up here or not thinking about it correctly. What's the best way to avoid this leak?
Edit: Adding some pictures showing the leak.
Views should be thought of as describing the structure of the view, and how it reacts to data. They ought to be small, single-purpose, easy-to-init structures. They shouldn't hold instances with their own life-cycles (like keeping publisher subscriptions) - those belong to the view model.
class ViewModel: ObservableObject {
var pub: AnyPublisher<Void, Never> {
Timer.publish(every: 2.0, on: .main, in: .default).autoconnect()
.prefix(1)
.map { _ in }
.eraseToAnyPublisher()
}
}
And use .onReceive to react to published events in the View:
struct ContentView: View {
#State var showRedView = true
#ObservedObject vm = ViewModel()
var body: some View {
ZStack {
if showRedView {
Color.red
.transition(.opacity)
}
Text("Hello, world!")
.padding()
}
.onReceive(self.vm.pub, perform: {
withAnimation {
self.showRedView = false
}
})
}
}
So, it seems that with the above arrangement, the TimerPublisher with prefix publisher chain is causing the leak. It's also not the right publisher to use for your use case.
The following achieves the same result, without the leak:
class ViewModel: ObservableObject {
var pub: AnyPublisher<Void, Never> {
Just(())
.delay(for: .seconds(2.0), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
My guess is that you're leaking because you store an AnyCancellable in subscriptions and you never remove it.
The sink operator creates the AnyCancellable. Unless you store it somewhere, the subscription will be cancelled prematurely. But if we use the Subscribers.Sink subscriber directly, instead of using the sink operator, there will be no AnyCancellable for us to manage.
func fadeRedView() {
Timer.publish(every: 5.0, on: .main, in: .default)
.autoconnect()
.prefix(1)
.subscribe(Subscribers.Sink(
receiveCompletion: { _ in },
receiveValue: { _ in
withAnimation {
showRedView = false
}
}
))
}
But this is still overkill. You don't need Combine for this. You can schedule the event directly:
func fadeRedView() {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
withAnimation {
showRedView = false
}
}
}

SwiftUI: ViewModifier doesn't listen to onReceive events

I have a custom ViewModifier which simply returns the same content attached with a onReceive modifier, the onReceive is not triggered, here is a sample code that you can copy, paste and run in XCode:
import SwiftUI
import Combine
class MyViewModel: ObservableObject {
#Published var myProperty: Bool = false
}
struct ContentView: View {
#ObservedObject var viewModel: MyViewModel
var body: some View {
Text("Hello, World!")
.modifier(MyOnReceive(viewModel: viewModel))
.onTapGesture {
self.viewModel.myProperty = true
}
}
}
struct MyOnReceive: ViewModifier {
#ObservedObject var viewModel: MyViewModel
func body(content: Content) -> some View {
content
.onReceive(viewModel.$myProperty) { theValue in
print("The Value is \(theValue)") // <--- this is not executed
}
}
}
is SwiftUI designed to disallow onReceive to execute inside a ViewModifier or is it a bug ? I have a view in my real life project that gets bigger with some business logic put inside onReceive, so I need to clean that view by separating it from onReceive.
ok, this works for me:
func body(content: Content) -> some View {
content
.onAppear() // <--- this makes it work
.onReceive(viewModel.$myProperty) { theValue in
print("-----> The Value is \(theValue)") // <--- this will be executed
}
}
ObservableObject and #Published are part of the Combine framework if you aren't using Combine then you shouldn't be using a class for your view data. Instead, you should be using SwiftUI as designed and avoid heavy objects and either put the data in the efficient View data struct or make a custom struct as follows:
import SwiftUI
struct MyConfig {
var myProperty: Bool = false
mutating func myMethod() {
myProperty = !myProperty
}
}
struct ContentView: View {
#State var config = MyConfig()
var body: some View {
Text("Hello, World!")
.onTapGesture {
config.myMethod()
}
}
}
Old answer:
Try onChange instead
https://developer.apple.com/documentation/swiftui/scrollview/onchange(of:perform:)
.onChange(of: viewModel.myProperty) { newValue in
print("newValue \(newValue)")
}
But please don't use the View Model object pattern in SwiftUI, try to force yourself to use value types like structs for all your data as SwiftUI was designed. Property wrappers like #State will give you the reference semantics you are used to with objects.

Keep reference on view/data model after View update

Consider we have a RootView and a DetailView. DetailView has it's own BindableObject, let's call it DetailViewModel and we have scenario:
RootView may be updated by some kind of global event e.g. missed
internet connection or by it's own data/view model
When RootView handling event it's
content is updated and this is causes new struct of DetailView to
be created
If DetailViewModel is created by DetailView on init,
there would be another reference of DetailViewModel and it's state (e.g. selected object) will be missed
How can we avoid this situation?
Store all ViewModels as EnvironmentObjects that is basically a singleton pool. This approach is causes to store unneeded objects in memory when they are not used
Pass throw all ViewModels from RootView to it's children and to children of child (has cons as above + painfull dependencies)
Store View independent DataObjects (aka workers) as EnvironmentObjects. In that case where do we store view dependent states that corresponds to Model? If we store it in View it will end up in situation where we cross-changing #States what is forbidden by SwiftUI
Better approach?
Sorry me for not providing any code. This question is on architecture concept of Swift UI where we trying to combine declarative structs and reference objects with data.
For now I don't see da way to keep references that corresponds to appropriate view only and don't keep them in memory/environment forever in their current states.
Update:
Lets add some code to see whats happening if VM is created by it's View
import SwiftUI
import Combine
let trigger = Timer.publish(every: 2.0, on: .main, in: .default)
struct ContentView: View {
#State var state: Date = Date()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: ContentDetailView(), label: {
Text("Navigation push")
.padding()
.background(Color.orange)
})
Text("\(state)")
.padding()
.background(Color.green)
ContentDetailView()
}
}
.onAppear {
_ = trigger.connect()
}
.onReceive(trigger) { (date) in
self.state = date
}
}
}
struct ContentDetailView: View {
#ObservedObject var viewModel = ContentDetailViewModel()
#State var once = false
var body: some View {
let vmdesc = "View model uuid:\n\(viewModel.uuid)"
print("State of once: \(once)")
print(vmdesc)
return Text(vmdesc)
.multilineTextAlignment(.center)
.padding()
.background(Color.blue)
.onAppear {
self.once = true
}
}
}
class ContentDetailViewModel: ObservableObject, Identifiable {
let uuid = UUID()
}
Update 2:
It seems that if we store ObservableObject as #State in view (not as ObservedObject) View keeps reference on VM
#State var viewModel = ContentDetailViewModel()
Any negative effects? Can we use it like this?
Update 3:
It seems that if ViewModel kept in View's #State:
and ViewModel is retained by closure with strong reference - deinit will never be executed -> memory leak
and ViewModel is retained by closure with weak reference - deinit invokes every time on view update, all subs will be reseted, but properties will be the same
Mehhh...
Update 4:
This approach also allows you to keep strong references in bindings closures
import Foundation
import Combine
import SwiftUI
/**
static func instanceInView() -> UIViewController {
let vm = ContentViewModel()
let vc = UIHostingController(rootView: ContentView(viewModel: vm))
vm.bind(uiViewController: vc)
return vc
}
*/
public protocol ViewModelProtocol: class {
static func instanceInView() -> UIViewController
var bindings: Set<AnyCancellable> { get set }
func onAppear()
func onDisappear()
}
extension ViewModelProtocol {
func bind(uiViewController: UIViewController) {
uiViewController.publisher(for: \.parent)
.sink(receiveValue: { [weak self] (parent) in
if parent == nil {
self?.bindings.cancel()
}
})
.store(in: &bindings)
}
}
struct ModelView<ViewModel: ViewModelProtocol>: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<ModelView>) -> UIViewController {
return ViewModel.instanceInView()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ModelView>) {
//
}
}
struct RootView: View {
var body: some View {
ModelView<ParkingViewModel>()
.edgesIgnoringSafeArea(.vertical)
}
}
Apple says that we should use ObservableObject for the data that lives outside of SwiftUI. It means you have to manage your data source yourself.
It looks like a single state container fits best for SwiftUI architecture.
typealias Reducer<State, Action> = (inout State, Action) -> Void
final class Store<State, Action>: ObservableObject {
#Published private(set) var state: State
private let reducer: Reducer<State, Action>
init(initialState: State, reducer: #escaping Reducer<State, Action>) {
self.state = initialState
self.reducer = reducer
}
func send(_ action: Action) {
reducer(&state, action)
}
}
You can pass the instance of the store into the environment of your SwiftUI app and it will be available in all views and will store your app state without data losses.
I wrote a blog post about this approach, take a look at it for more information
https://swiftwithmajid.com/2019/09/18/redux-like-state-container-in-swiftui/

Resources