LottieSwitch.swift 4.7 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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
// Created by Cal Stephens on 8/11/23.
// Copyright © 2023 Airbnb Inc. All rights reserved.

#if canImport(SwiftUI)
import SwiftUI

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

  // MARK: Lifecycle

  public init(animation: LottieAnimation?) {
    self.animation = animation
  }

  // MARK: Public

  public var body: some View {
    AnimatedSwitch.swiftUIView {
      let animatedSwitch = AnimatedSwitch(animation: animation, configuration: configuration)
      animatedSwitch.isOn = isOn?.wrappedValue ?? false
      return animatedSwitch
    }
    .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

      if let isOn = isOn?.wrappedValue, isOn != context.view.isOn {
        context.view.setIsOn(isOn, animated: true)
      }
    }
    .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 (AnimatedSwitch) -> 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 with the given `Binding` reflecting the `isOn` state of the switch.
  public func isOn(_ binding: Binding<Bool>) -> Self {
    var copy = self
    copy.isOn = binding
    return copy.configure { view in
      view.stateUpdated = { isOn in
        DispatchQueue.main.async {
          binding.wrappedValue = isOn
        }
      }
    }
  }

  /// Returns a copy of this view with the "on" animation configured
  /// to start and end at the given progress values.
  /// Defaults to playing the entire animation forwards (0...1).
  public func onAnimation(
    fromProgress onStartProgress: AnimationProgressTime,
    toProgress onEndProgress: AnimationProgressTime)
    -> Self
  {
    configure { view in
      if onStartProgress != view.onStartProgress || onEndProgress != view.onEndProgress {
        view.setProgressForState(
          fromProgress: onStartProgress,
          toProgress: onEndProgress,
          forOnState: true)
      }
    }
  }

  /// Returns a copy of this view with the "on" animation configured
  /// to start and end at the given progress values.
  /// Defaults to playing the entire animation backwards (1...0).
  public func offAnimation(
    fromProgress offStartProgress: AnimationProgressTime,
    toProgress offEndProgress: AnimationProgressTime)
    -> Self
  {
    configure { view in
      if offStartProgress != view.offStartProgress || offEndProgress != view.offEndProgress {
        view.setProgressForState(
          fromProgress: offStartProgress,
          toProgress: offEndProgress,
          forOnState: false)
      }
    }
  }

  /// 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<AnimatedSwitch, Void>.Configuration]()

  // MARK: Private

  private let animation: LottieAnimation?
  private var configuration: LottieConfiguration = .shared
  private var isOn: Binding<Bool>?

}
#endif