Repository: airbnb/lottie-ios
File: Example/Example/AnimationPreviewView.swift (Swift)

Code before:
      #endif
    }
    .navigationTitle(animationSource.name.components(separatedBy: "/").last!)
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(Color.secondaryBackground)
    .onReceive(timer) { _ in
      updateIndex()
    }
  }

  // MARK: Private

  /// Used for remote animations only, when more than one URL is provided we loop over the urls every 2 seconds.
  private let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
  private let urls: [URL]

  @State private var animationPlaying = true
  @State private var sliderValue: AnimationProgressTime = 0
  @State private var currentURLIndex: Int

  private var fetchTrigger: Binding<AnyHashable> {
    switch animationSource {
    case .local:
      return .constant(currentURLIndex)
    case .remote:
      return $currentURLIndex.mapGetter(transform: AnyHashable.init)
    }
  }

  private func lottieSource() async throws -> LottieAnimationSource? {
    switch animationSource {
    case .local(let name):
      if let animation = LottieAnimation.named(name) {
        return .lottieAnimation(animation)
      } else {
        let lottie = try await DotLottieFile.named(name)
        return .dotLottieFile(lottie)
      }
    case .remote:
      let animation = await LottieAnimation.loadedFrom(url: urls[currentURLIndex])
      return animation.map(LottieAnimationSource.lottieAnimation)
    }
  }

  private func updateIndex() {
    let currentIndex = currentURLIndex
    let nextIndex = currentIndex == urls.index(before: urls.endIndex) ? urls.startIndex : currentIndex + 1
    currentURLIndex = nextIndex
  }
}


Review comment (refactor):
I wonder if using `func loadAnimationTrigger<T: Hashable>(_ binding: Binding<Hashable>) -> Self)` instead of `AnyHashable` would let us avoid having to map this binding to an `AnyHashable` in the consumer code and instead let us pass in a `Binding<Int>` and remove this computed `fetchTrigger` property. That would be a nice callsite simplification.

Code after:
      .padding(.all, 16)
      #endif
    }
    .navigationTitle(animationSource.name.components(separatedBy: "/").last!)
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(Color.secondaryBackground)
    .onReceive(timer) { _ in
      updateIndex()
    }
  }

  // MARK: Private

  /// Used for remote animations only, when more than one URL is provided we loop over the urls every 2 seconds.
  private let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
  private let urls: [URL]

  @State private var animationPlaying = true
  @State private var sliderValue: AnimationProgressTime = 0
  @State private var currentURLIndex: Int

  private func lottieSource() async throws -> LottieAnimationSource? {
    switch animationSource {
    case .local(let name):
      if let animation = LottieAnimation.named(name) {
        return .lottieAnimation(animation)
      } else {
        let lottie = try await DotLottieFile.named(name)
        return .dotLottieFile(lottie)
      }
    case .remote:
      let animation = await LottieAnimation.loadedFrom(url: urls[currentURLIndex])
      return animation.map(LottieAnimationSource.lottieAnimation)
    }
  }

  private func updateIndex() {
    let currentIndex = currentURLIndex
    let nextIndex = currentIndex == urls.index(before: urls.endIndex) ? urls.startIndex : currentIndex + 1
    currentURLIndex = nextIndex
  }
}

extension Color {
  static var secondaryBackground: Color {
    #if os(iOS)
    Color(uiColor: .secondarySystemBackground)
    #else
      .clear
    #endif
  }