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

Code before:
    @ViewBuilder placeholder: @escaping () -> Placeholder)
  {
    self.loadAnimation = loadAnimation
    self.placeholder = placeholder
    _animationSource = State(initialValue: nil)
  }

  // MARK: Public

  public var body: some View {
    LottieAnimationView.swiftUIView {
      LottieAnimationView(
        animationSource: animationSource,
        imageProvider: imageProviderConfiguration?.imageProvider,
        textProvider: textProvider,
        fontProvider: fontProvider,
        configuration: configuration,
        logger: logger)
    }
    .sizing(sizing)
    .configure { context in
      applyCurrentAnimationConfiguration(to: context.view)
    }
    .configurations(configurations)
    .overlay {
      placeholder?()
        .opacity(animationSource == nil ? 1 : 0)
    }
    .onAppear {
      loadAnimationIfNecessary()
    }
    .valueChanged(value: reloadAnimationTrigger) { _ in
      reloadAnimationTriggerDidChange()
    }
  }

  /// Returns a copy of this `LottieView` updated to have the given closure applied to its
  /// represented `LottieAnimationView` whenever it is updated via the `updateUIView(…)`
  /// or `updateNSView(…)` method.
  public func configure(_ configure: @escaping (LottieAnimationView) -> Void) -> Self {
    var copy = self
    copy.configurations.append { context in
      configure(context.view)
    }
    return copy
  }

  /// Returns a copy of this view that can be resized by scaling its animation to fit the size
  /// offered by its parent.
  public func resizable() -> Self {
    var copy = self

Review comment (suggestion):
This is a behavior change that will probably affect existing consumers (see comparison below) but I think this is ok because the updated behavior seems more expected and predictable

```swift
LottieView {
  try await loadAnimation()
} placeholder: {
  LoadingIndicator()
    .frame(width: 50, height: 50)
}
```

| Before | After |
| ---- | ---- |
| <img width="731" alt="Screenshot 2024-01-19 at 4 29 11 PM" src="https://github.com/airbnb/lottie-ios/assets/1811727/0258de34-9b9b-447d-8f92-eb9f1ff4edb2"> | <img width="731" alt="Screenshot 2024-01-19 at 4 31 28 PM" src="https://github.com/airbnb/lottie-ios/assets/1811727/110e5e0c-4827-497f-b6ee-22ec7586513b"> |

Code after:
    @ViewBuilder placeholder: @escaping () -> Placeholder)
  {
    self.loadAnimation = loadAnimation
    self.placeholder = placeholder
    _animationSource = State(initialValue: nil)
  }

  // MARK: Public

  public var body: some View {
    LottieAnimationView.swiftUIView {
      LottieAnimationView(
        animationSource: animationSource,
        imageProvider: imageProviderConfiguration?.imageProvider,
        textProvider: textProvider,
        fontProvider: fontProvider,
        configuration: configuration,
        logger: logger)
    }
    .sizing(sizing)
    .configure { context in
      applyCurrentAnimationConfiguration(to: context.view)
    }
    .configurations(configurations)
    .opacity(animationSource == nil ? 0 : 1)
    .overlay {
      placeholder?()
        .opacity(animationSource == nil ? 1 : 0)
    }
    .onAppear {
      loadAnimationIfNecessary()
    }
    .valueChanged(value: reloadAnimationTrigger) { _ in
      reloadAnimationTriggerDidChange()
    }
  }

  /// Returns a copy of this `LottieView` updated to have the given closure applied to its
  /// represented `LottieAnimationView` whenever it is updated via the `updateUIView(…)`
  /// or `updateNSView(…)` method.
  public func configure(_ configure: @escaping (LottieAnimationView) -> Void) -> Self {
    var copy = self
    copy.configurations.append { context in
      configure(context.view)
    }
    return copy
  }

  /// Returns a copy of this view that can be resized by scaling its animation to fit the size
  /// offered by its parent.
  public func resizable() -> Self {