Repository: airbnb/lottie-ios
File: Sources/Public/Animation/LottieView.swift (Swift)

Code before:
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {

  // MARK: Lifecycle

  /// Creates a `LottieView` that displays the given animation
  public init(animation: LottieAnimation?) where Placeholder == EmptyView {
    _animationSource = State(initialValue: animation.map(LottieAnimationSource.lottieAnimation))
    placeholder = nil
    loadAnimationTrigger = .constant(LoadReference.constantSource)
  }

  /// Creates a `LottieView` that displays the given `DotLottieFile`
  public init(dotLottieFile: DotLottieFile?) where Placeholder == EmptyView {
    _animationSource = State(initialValue: dotLottieFile.map(LottieAnimationSource.dotLottieFile))
    placeholder = nil
    loadAnimationTrigger = .constant(LoadReference.constantSource)
  }

  /// Creates a `LottieView` that asynchronously loads and displays the given `LottieAnimation`.
  /// The `loadAnimation` closure is called exactly once in `onAppear`.
  /// - Parameters:
  ///   - loadAnimationTrigger: `Binding` that  triggers a new call to `loadAnimation` when a new value is propagated
  ///   - loadAnimation: Closure that provides an animation asynchronously
  public init(
    loadAnimationTrigger: Binding<AnyHashable> = .constant(LoadReference.constantSource),
    _ loadAnimation: @escaping () async throws -> LottieAnimation?)
    where Placeholder == EmptyView
  {
    self.init(
      loadAnimationTrigger: loadAnimationTrigger,
      loadAnimation,
      placeholder: EmptyView.init)
  }

  /// Creates a `LottieView` that asynchronously loads and displays the given `LottieAnimation`.
  /// The `loadAnimation` closure is called exactly once in `onAppear`.
  /// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`.
  /// - Parameters:
  ///   - loadAnimationTrigger: `Binding` that  triggers a new call to `loadAnimation` when a new value is propagated
  ///   - loadAnimation: Closure that provides an animation asynchronously
  public init(
    loadAnimationTrigger: Binding<AnyHashable> = .constant(LoadReference.constantSource),
    _ loadAnimation: @escaping () async throws -> LottieAnimation?,
    @ViewBuilder placeholder: @escaping (() -> Placeholder))
  {
    self.init(loadAnimationTrigger: loadAnimationTrigger) {
      try await loadAnimation().map(LottieAnimationSource.lottieAnimation)
    } placeholder: {
      placeholder()
    }

Review comment (suggestion):
Instead of adding this an an argument to all of the `init` values, could we add a builder function for this (e.g. `func loadAnimationTrigger(_ binding: Binding<AnyHashable>) -> Self`)? 

This is optional and won't be used in all use cases. Anything optional seems better to provide using builder functions rather than in each individual `init`. 

Then we can just mention this in the doc comment of all of the `loadAnimation` initializers, like "By default the `loadAnimation` closure is called exactly once in `onAppear`. To trigger the closure to be called again at a later time, configure `.loadAnimationTrigger($reloadAnimation)` with a binding.

Code after:
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {

  // MARK: Lifecycle

  /// Creates a `LottieView` that displays the given animation
  public init(animation: LottieAnimation?) where Placeholder == EmptyView {
    _animationSource = State(initialValue: animation.map(LottieAnimationSource.lottieAnimation))
    placeholder = nil
  }

  /// Creates a `LottieView` that displays the given `DotLottieFile`
  public init(dotLottieFile: DotLottieFile?) where Placeholder == EmptyView {
    _animationSource = State(initialValue: dotLottieFile.map(LottieAnimationSource.dotLottieFile))
    placeholder = nil
  }

  /// Creates a `LottieView` that asynchronously loads and displays the given `LottieAnimation`.
  /// The `loadAnimation` closure is called exactly once in `onAppear`.
  /// If you wish to call `loadAnimation` again at a different time, you can use `.loadAnimationTrigger(...)`.
  public init(_ loadAnimation: @escaping () async throws -> LottieAnimation?) where Placeholder == EmptyView {
    self.init(loadAnimation, placeholder: EmptyView.init)
  }

  /// Creates a `LottieView` that asynchronously loads and displays the given `LottieAnimation`.
  /// The `loadAnimation` closure is called exactly once in `onAppear`.
  /// If you wish to call `loadAnimation` again at a different time, you can use `.loadAnimationTrigger(...)`.
  /// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`.
  public init(
    _ loadAnimation: @escaping () async throws -> LottieAnimation?,
    @ViewBuilder placeholder: @escaping (() -> Placeholder))
  {
    self.init {
      try await loadAnimation().map(LottieAnimationSource.lottieAnimation)
    } placeholder: {
      placeholder()
    }
  }

  /// Creates a `LottieView` that asynchronously loads and displays the given `DotLottieFile`.
  /// The `loadDotLottieFile` closure is called exactly once in `onAppear`.
  /// If you wish to call `loadAnimation` again at a different time, you can use `.loadAnimationTrigger(...)`.
  public init(_ loadDotLottieFile: @escaping () async throws -> DotLottieFile?) where Placeholder == EmptyView {
    self.init(loadDotLottieFile, placeholder: EmptyView.init)
  }

  /// Creates a `LottieView` that asynchronously loads and displays the given `DotLottieFile`.
  /// The `loadDotLottieFile` closure is called exactly once in `onAppear`.
  /// If you wish to call `loadAnimation` again at a different time, you can use `.loadAnimationTrigger(...)`.
  /// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`.
  public init(