9

SwiftUI has implicit animations with .animate(), and explicit ones using .withAnimation(). However, I can't figure out how to animate an image change:

struct ImageViewWidget : View {
  @ObjectBinding var imageLoader: ImageLoader

  init(imageURL: URL) {
    imageLoader = ImageLoader(imageURL: imageURL)
  }

  var body: some View {
    Image(uiImage:
      (imageLoader.data.count == 0) ? UIImage(named: "logo-old")! :  UIImage(data: imageLoader.data)!)
        .resizable()
        .cornerRadius(5)
        .frame(width: 120, height:120)
  }
}

This Image's uiImage argument is passed the old-logo (placeholder) if there's no data in imageLoader (a BindableObject), and replaces it with the correct one once that's asynchronously loaded:

class ImageLoader : BindableObject {
  let didChange = PassthroughSubject<Data, Never>()

  var data = Data() {
    didSet {
      didChange.send(data)
    }
  }

  init(imageURL: URL) {
    print("Image loader being initted!")
    let url = imageURL

    URLSession.shared.dataTask(with: url) { (data, _, _) in
      guard let data = data else { return }
      DispatchQueue.main.async {
        self.data = data
      }
      }.resume()

  }
}

How can I animate this change, the moment where data.count stops being 0, and we have the image? say I want a fade out-in animation..

zerohedge
  • 3,185
  • 4
  • 28
  • 63

2 Answers2

2

If you want to use explicit animations based on environment objects (or observable objects), you need to create some state in your view.

You can react to changes to an observable in your view using onReceive, and then modify your state using explicit animation.

struct ImageViewWidget: View {
    @ObservedObject var imageLoader: ImageLoader
    @State var uiImage: UIImage = UIImage(named: "logo-old")!

    init(imageURL: URL) {
        imageLoader = ImageLoader(imageURL: imageURL)
    }

    var body: some View {
        Image(uiImage: uiImage)
            .resizable()
            .cornerRadius(5)
            .frame(width: 120, height: 120)
            .onReceive(imageLoader.$data) { data in
                if data.count != 0 {
                    withAnimation {
                        self.uiImage = UIImage(data: data)!
                    }
                }
            }
    }
}
Chris Pearce
  • 666
  • 5
  • 16
-2

You don't necessarily have to call .animate() or .withAnimation() because you are simply switching the images, you can use .transition() instead. Assuming you have already successfully updated your image with your @ObjectBinding(@ObservedObject in Beta5), you can do this:

var body: some View {
    if imageLoader.data.count == 0 {
        Image(uiImage: UIImage(named: "logo-old")!)
        .resizable()
        .cornerRadius(5)
        .frame(width: 120, height:120)
        .transition(.opacity)
        .animation(.easeInOut(duration:1))
    } else {
        Image(uiImage: UIImage(data: imageLoader.data)!)
        .resizable()
        .cornerRadius(5)
        .frame(width: 120, height:120)
        .transition(.opacity)
        .animation(.easeInOut(duration:1))
    }
}

or you can use a custom view modifier if you want to make the transition fancier:

struct ScaleAndFade: ViewModifier {
    /// True when the transition is active.
    var isEnabled: Bool

    // fade the content view while transitioning in and
    // out of the container.
    func body(content: Content) -> some View {
        return content
            .scaleEffect(isEnabled ? 0.1 : 1)
            .opacity(isEnabled ? 0 : 1)
            //any other properties you want to transition
    }
}


extension AnyTransition {
    static let scaleAndFade = AnyTransition.modifier(
        active: ScaleAndFade(isEnabled: true),
        identity: ScaleAndFade(isEnabled: false))
}

and then inside your ImageViewWidget, add .transition(.scaleAndFade) to your Image as its view modifier

ada10086
  • 292
  • 5
  • 8
  • 1
    This (your first suggestion) doesn't work. The image is immediately replaced without any effects. I also think it (somehow) makes the images load synchronously. – zerohedge Aug 25 '19 at 14:32
  • sorry, I forgot to add ` .animation(.easeInOut(duration:1))` after .transition, works for me – ada10086 Aug 26 '19 at 16:18
  • 4
    It still doesn't work. Nor animation and transition takes no effect – chudin26 May 03 '20 at 12:48