Create an account

Very important

  • To access the important data of the forums, you must be active in each forum and especially in the leaks and database leaks section, send data and after sending the data and activity, data and important content will be opened and visible for you.
  • You will only see chat messages from people who are at or below your level.
  • More than 500,000 database leaks and millions of account leaks are waiting for you, so access and view with more activity.
  • Many important data are inactive and inaccessible for you, so open them with activity. (This will be done automatically)


Thread Rating:
  • 847 Vote(s) - 3.44 Average
  • 1
  • 2
  • 3
  • 4
  • 5
How can I animate changes to an @ObservedObject?

#1
My view is determined by state stored in a `ViewModel`. Sometimes the view might call a function on its `ViewModel`, causing an asynchronous state change.

How can I animate the effect of that state change in the `View`?

Here's a contrived example, where the call to `viewModel.change()` will cause the view to change colour.

- Expected behaviour: slow dissolve from blue to red.
- Actual behaviour: immediate change from blue to red.

```swift
class ViewModel: ObservableObject {

@Published var color: UIColor = .blue

func change() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.color = .red
}
}
}

struct ContentView: View {

@ObservedObject var viewModel = ViewModel()

var body: some View {
Color(viewModel.color).onAppear {
withAnimation(.easeInOut(duration: 1.0)) {
self.viewModel.change()
}
}
}
}
```

If I remove the `ViewModel` and store the state in the view itself, everything works as expected. That's not a great solution, however, because I want to encapsulate state in the `ViewModel`.

```swift
struct ContentView: View {

@State var color: UIColor = .blue

var body: some View {
Color(color).onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
withAnimation(.easeInOut(duration: 1.0)) {
self.color = .red
}
}
}
}
}
```
Reply

#2
Update @Seb Jachec's answer for Xcode 14.2, to fix the deprecated warning:


Color(viewModel.color)
.onAppear {
viewModel.change()
}
.animation(.easeInOut(duration: 1), value: viewModel.color)
Reply

#3
# Using .animation()

You can use `.animation(...)` on a body content or on any subview but it will animate all changes of the view.

Let's consider an example when we have two API calls through the ViewModel and use `.animation(.default)` on body content:

```swift
import SwiftUI
import Foundation

class ArticleViewModel: ObservableObject {
@Published var title = ""
@Published var content = ""

func fetchArticle() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.title = "Article Title"
}
}

func fetchContent() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.content = "Content"
}
}
}

struct ArticleView: View {
@ObservedObject var viewModel = ArticleViewModel()

var body: some View {
VStack(alignment: .leading) {
if viewModel.title.isEmpty {
Button("Load Article") {
self.viewModel.fetchArticle()
}
} else {
Text(viewModel.title).font(.title)

if viewModel.content.isEmpty {
Button("Load content...") {
self.viewModel.fetchContent()
}
.padding(.vertical, 5)
.frame(maxWidth: .infinity, alignment: .center)
} else {
Rectangle()
.foregroundColor(Color.blue.opacity(0.2))
.frame(height: 80)
.overlay(Text(viewModel.content))
}
}
}
.padding()
.frame(width: 300)
.background(Color.gray.opacity(0.2))
.animation(.default) // animate all changes of the view
}
}
```

The result would be next:

[![Animation modifier on body][1]][1]

You can see that we have animation on both actions. It might be preferred behavior but in some cases, you may want to **control each action** separately.

# Using .onReceive()

Let say we want animate the view after first API call (`fetchArticle`), but on the second (`fetchContent`) - just redraw view without animation. In other words - animate the view when the `title` received but does not animate view when the `content` received.

To implement this we need:

1. Create a separate property `@State var title = ""` in the View.
2. Use this new property all over the view instead of
`viewModel.title`.
3. Declare `.onReceive(viewModel.$title) { newTitle in ... }`. This closure will execute when the publisher (`viewModel.$title`) sends a new value. On this step, we have control over properties in the View. In our case, we will update the `title` property of the View.
4. Use `withAnimation {...}` inside the closure to animate the changes.

So we will have animation when the `title` updates. While receiving a new `content` value of the ViewModel our View just updates without animation.

```swift
struct ArticleView: View {
@ObservedObject var viewModel = ArticleViewModel()
// 1
@State var title = ""

var body: some View {
VStack(alignment: .leading) {
// 2
if title.isEmpty {
Button("Load Article") {
self.viewModel.fetchArticle()
}
} else {
// 2
Text(title).font(.title)

if viewModel.content.isEmpty {
Button("Load content...") {
self.viewModel.fetchContent()
}
.padding(.vertical, 5)
.frame(maxWidth: .infinity, alignment: .center)
} else {
Rectangle()
.foregroundColor(Color.blue.opacity(0.2))
.frame(height: 80)
.overlay(Text(viewModel.content))
}
}
}
.padding()
.frame(width: 300)
.background(Color.gray.opacity(0.2))
// 3
.onReceive(viewModel.$title) { newTitle in
// 4
withAnimation {
self.title = newTitle
}
}
}
}
```

The result would be next:

[![Useing onReceive][2]][2]

[1]:

[2]:
Reply

#4
It looks as though using `withAnimation` inside an async closure causes the color not to animate, but instead to change instantly.

Either removing the wrapping `asyncAfter`, or removing the `withAnimation` call and adding an `animation` modifier in the `body` of your `ContentView` (as follows) should fix the issue:

Color(viewModel.color).onAppear {
self.viewModel.change()
}.animation(.easeInOut(duration: 1))

Tested locally (iOS 13.3, Xcode 11.3) and this also appears to dissolve/fade from blue to red as you intend.
Reply



Forum Jump:


Users browsing this thread:
1 Guest(s)

©0Day  2016 - 2023 | All Rights Reserved.  Made with    for the community. Connected through