airbnb / lottie-ios

An iOS library to natively render After Effects vector animations
http://airbnb.io/lottie/
Apache License 2.0
25.67k stars 3.74k forks source link

SwiftUI does not respect scaling #1050

Closed sieren closed 4 years ago

sieren commented 4 years ago

Check these before submitting:

This issue is a:

Which Version of Lottie are you using?

Lottie 3.0

What Platform are you on?

What Language are you in?

Expected Behavior

When wrapping an AnimationView inside an UIViewRepresentable to interface with SwiftUI it is expected that the frame size is applied correctly to AnimationView.

Actual Behavior

The scale of the AnimationView is based on the animation bounds and gets overwritten. This is because SwiftUI prefers the intrinsicContentSize over any specified frames.

Code Example

Temporary workaround is modifying lottie-ios in AnimationView.swift to report back the frame and not the animation bounds:

  // MARK: - Public (UIView Overrides)

  override public var intrinsicContentSize: CGSize {
    if let animation = animation {
      return self.frame.size
   //   return animation.bounds.size
    }
    return .zero
  }

Example:

struct ContentView: View {
  @EnvironmentObject var appContext: AppContext

    var body: some View {
      VStack {
        VStack {
          AnimationLottieView(animation: "door").aspectRatio(contentMode: .fit).frame(minWidth: 0, maxWidth: 20, minHeight:0, maxHeight: 100)
        Spacer()
      }
    }
}

AnimationView in UIViewRepresentable:

import Lottie
import SwiftUI

struct AnimationLottieView: UIViewRepresentable {
  var animation: String!
  let animationView = AnimationView()

  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }

  func makeUIView(context: UIViewRepresentableContext<AnimationLottieView>) -> UIView {
    let animationLoop = Animation.named(animation)
    animationView.animation = animationLoop
    animationView.loopMode = .loop
    animationView.contentMode = .scaleAspectFit
    animationView.play()
    return animationView
  }

  func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<AnimationLottieView>) {
  // swiftlint:disable:next force_cast
    let animView = uiView as! AnimationView
  }

  class Coordinator: NSObject {
      var parent: AnimationLottieView

      init(_ animationView: AnimationLottieView) {
        self.parent = animationView
        super.init()
      }
    }
  }

Animation JSON

howlingblast commented 4 years ago

I had the same problem but following solution did work for me without the need to change Lottie's code.

I created a container view and constrained the AnimationView to the container via Auto Layout. Hope that helps.

struct LottieView: UIViewRepresentable {

    var name: String!

    func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
        let view = UIView()

        let animationView = AnimationView()
        animationView.animation = Animation.named(name)
        animationView.contentMode = .scaleAspectFit

        animationView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(animationView)

        NSLayoutConstraint.activate([
            animationView.widthAnchor.constraint(equalTo: view.widthAnchor),
            animationView.heightAnchor.constraint(equalTo: view.heightAnchor)
        ])

        return view
    }

    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieView>) {
    }
}
davidpasztor commented 4 years ago

@reversepanda while that workaround does solve the problem, it isn't a real solution and hence makes the interface of the UIViewRepresentable incorrect. If you actually need to implement updateUIView to interact with the LottieView, you now need to iterate over uiView.subviews to find the LottieView instead of being able to directly interact with it. Moreover, UIViewType should be LottieView, not UIView.

For this reason, the fix should be done inside Lottie instead of requiring a workaround outside of Lottie.

sumit-anantwar commented 3 years ago

@davidpasztor check this implementation for your concern. I am also adding support for AutoPlay

//
//  LottieAnimationView.swift
//  TabView_POC
//
//  Created by Sumit Anantwar on 30/04/2021.
//

import SwiftUI
import UIKit
import Lottie

struct LottieAnimationView : UIViewRepresentable {

    @State private var isPlayingDefault = true

    let filename: String
    let loopMode: LottieLoopMode
    let isPlaying: Binding<Bool>?
    init(filename: String, loopMode: LottieLoopMode = .loop, isPlaying: Binding<Bool>? = nil) {
        self.filename = filename
        self.loopMode = loopMode
        self.isPlaying = isPlaying
    }

    func makeUIView(context: Context) -> AnimationViewProxy {
        let playing = self.isPlaying ?? $isPlayingDefault
        return AnimationViewProxy(
            filename: filename, loopMode: loopMode, isPlaying: playing.wrappedValue
        )
    }

    func updateUIView(_ animationView: AnimationViewProxy, context: Context) {
        let playing = self.isPlaying ?? $isPlayingDefault
        if playing.wrappedValue {
            animationView.play()
        } else {
            animationView.stop()
        }
    }

    final class AnimationViewProxy : UIView  {

        private let animationView = AnimationView()
        private var isAnimationPlaying: Bool = true

        init(filename: String, loopMode: LottieLoopMode, isPlaying: Bool = true) {
            super.init(frame: .zero)

            animationView.animation = Animation.named(filename)
            animationView.contentMode = .scaleAspectFit
            animationView.loopMode = loopMode
            self.isAnimationPlaying = isPlaying
            if isPlaying {
                self.play()
            }

            self.addSubview(animationView)

            animationView.translatesAutoresizingMaskIntoConstraints = false

            NSLayoutConstraint.activate([
                animationView.widthAnchor.constraint(equalTo: self.widthAnchor),
                animationView.heightAnchor.constraint(equalTo: self.heightAnchor)
            ])

            NotificationCenter.default.addObserver(self,
                                                   selector: #selector(applicationWillEnterForeground),
                                                   name: UIApplication.willEnterForegroundNotification,
                                                   object: nil)
        }

        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        func play() {
            self.isAnimationPlaying = true
            self.animationView.play()
        }

        func stop() {
            self.isAnimationPlaying = false
            self.animationView.stop()
        }

        @objc private func applicationWillEnterForeground() {
            if self.isAnimationPlaying {
                self.animationView.play()
            }
        }

        override func willMove(toWindow newWindow: UIWindow?) {
            guard let _ = newWindow else {
                self.animationView.stop()
                return
            }

            if self.isAnimationPlaying {
                self.animationView.play()
            }
        }
    }
}