LottieButton.swift 4.0 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
// Created by Cal Stephens on 8/14/23.
// Copyright © 2023 Airbnb Inc. All rights reserved.

#if canImport(SwiftUI)
import SwiftUI

/// A wrapper which exposes Lottie's `AnimatedButton` to SwiftUI
public struct LottieButton: UIViewConfiguringSwiftUIView {

  // MARK: Lifecycle

  public init(animation: LottieAnimation?, action: @escaping () -> Void) {
    self.animation = animation
    self.action = action
  }

  // MARK: Public

  public var body: some View {
    AnimatedButton.swiftUIView {
      let button = AnimatedButton(animation: animation, configuration: configuration)
      button.performAction = action
      return button
    }
    .configure { context in
      // We check referential equality of the animation before updating as updating the
      // animation has a side-effect of rebuilding the animation layer, and it would be
      // prohibitive to do so on every state update.
      if animation !== context.view.animationView.animation {
        context.view.animationView.animation = animation
      }

      #if os(macOS)
      // Disable the intrinsic content size constraint on the inner animation view,
      // or the Epoxy `SwiftUIMeasurementContainer` won't size this view correctly.
      context.view.animationView.isVerticalContentSizeConstraintActive = false
      context.view.animationView.isHorizontalContentSizeConstraintActive = false
      #endif
    }
    .configurations(configurations)
  }

  /// 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 (AnimatedButton) -> Void) -> Self {
    var copy = self
    copy.configurations.append { context in
      configure(context.view)
    }
    return copy
  }

  /// Returns a copy of this view with its `LottieConfiguration` updated to the given value.
  public func configuration(_ configuration: LottieConfiguration) -> Self {
    var copy = self
    copy.configuration = configuration

    copy = copy.configure { view in
      if view.animationView.configuration != configuration {
        view.animationView.configuration = configuration
      }
    }

    return copy
  }

  /// Returns a copy of this view configured to animate between the
  /// given progress values when the given event is triggered
  public func animate(
    fromProgress: AnimationProgressTime,
    toProgress: AnimationProgressTime,
    on event: LottieControlEvent)
    -> Self
  {
    configure { view in
      // `setPlayRange` just modifies a dictionary,
      // so we can just call it on every state update without diffing
      view.setPlayRange(fromProgress: fromProgress, toProgress: toProgress, event: event)
    }
  }

  /// Returns a copy of this view configured to animate between the
  /// given markers when the given event is triggered
  public func animate(
    fromMarker: String,
    toMarker: String,
    on event: LottieControlEvent)
    -> Self
  {
    configure { view in
      // `setPlayRange` just modifies a dictionary,
      // so we can just call it on every state update without diffing
      view.setPlayRange(fromMarker: fromMarker, toMarker: toMarker, event: event)
    }
  }

  /// Returns a copy of this view using the given value provider for the given keypath.
  /// The value provider must be `Equatable` to avoid unnecessary state updates / re-renders.
  public func valueProvider<ValueProvider: AnyValueProvider & Equatable>(
    _ valueProvider: ValueProvider,
    for keypath: AnimationKeypath)
    -> Self
  {
    configure { view in
      if (view.animationView.valueProviders[keypath] as? ValueProvider) != valueProvider {
        view.animationView.setValueProvider(valueProvider, keypath: keypath)
      }
    }
  }

  // MARK: Internal

  var configurations = [SwiftUIView<AnimatedButton, Void>.Configuration]()

  // MARK: Private

  private let animation: LottieAnimation?
  private let action: () -> Void
  private var configuration: LottieConfiguration = .shared
}
#endif