From 96d09577b9ee7f10be26b977c6cb3b6b7f9b7e26 Mon Sep 17 00:00:00 2001 From: Cal Stephens Date: Fri, 11 Aug 2023 14:46:15 -0700 Subject: [PATCH] Add cross-platform SwiftUI wrapper for AnimatedSwitch --- Example/Example.xcodeproj/project.pbxproj | 10 ++ Example/Example/AnimationListView.swift | 10 +- Example/Example/ControlsDemoView.swift | 61 ++++++++ Example/Example/LottieSwitchRow.swift | 53 +++++++ Lottie.xcodeproj/project.pbxproj | 56 +++++-- .../{iOS => Controls}/AnimatedControl.swift | 87 ++++++++++- .../{iOS => Controls}/AnimatedSwitch.swift | 66 ++++++-- Sources/Public/Controls/LottieSwitch.swift | 144 ++++++++++++++++++ Sources/Public/Controls/LottieViewType.swift | 32 ++++ Sources/Public/iOS/AnimatedButton.swift | 2 +- 10 files changed, 485 insertions(+), 36 deletions(-) create mode 100644 Example/Example/ControlsDemoView.swift create mode 100644 Example/Example/LottieSwitchRow.swift rename Sources/Public/{iOS => Controls}/AnimatedControl.swift (70%) rename Sources/Public/{iOS => Controls}/AnimatedSwitch.swift (82%) create mode 100644 Sources/Public/Controls/LottieSwitch.swift create mode 100644 Sources/Public/Controls/LottieViewType.swift diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 31b06deed8..9053655885 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 08E359922A55FFC400141956 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E359912A55FFC400141956 /* ExampleApp.swift */; }; 08E359942A55FFC400141956 /* LottieViewLayoutDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E359932A55FFC400141956 /* LottieViewLayoutDemoView.swift */; }; 08E3599F2A56004100141956 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 08E3599E2A56004100141956 /* Lottie */; }; + 08E6CF822A86C35B00A6D92F /* LottieSwitchRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF812A86C35B00A6D92F /* LottieSwitchRow.swift */; }; + 08E6CF842A86C49300A6D92F /* ControlsDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF832A86C49300A6D92F /* ControlsDemoView.swift */; }; 2E0F2FBE27602CB300B65DE3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2E0F2FBC27602CB300B65DE3 /* LaunchScreen.storyboard */; }; 2E1670C32784F009009CDED3 /* ControlsDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E1670C22784F009009CDED3 /* ControlsDemoViewController.swift */; }; 2E1670CA2784F123009CDED3 /* AnimatedButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E1670C92784F123009CDED3 /* AnimatedButtonRow.swift */; }; @@ -54,6 +56,8 @@ 08E359912A55FFC400141956 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; 08E359932A55FFC400141956 /* LottieViewLayoutDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieViewLayoutDemoView.swift; sourceTree = ""; }; 08E359972A55FFC600141956 /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; }; + 08E6CF812A86C35B00A6D92F /* LottieSwitchRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieSwitchRow.swift; sourceTree = ""; }; + 08E6CF832A86C49300A6D92F /* ControlsDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsDemoView.swift; sourceTree = ""; }; 2E0F2FB627602C1500B65DE3 /* .. */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ..; sourceTree = ""; }; 2E0F2FBD27602CB300B65DE3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 2E1670C22784F009009CDED3 /* ControlsDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsDemoViewController.swift; sourceTree = ""; }; @@ -101,6 +105,8 @@ 08E359932A55FFC400141956 /* LottieViewLayoutDemoView.swift */, 08E359972A55FFC600141956 /* Example.entitlements */, 607FACD11AFB9204008FA782 /* Products */, + 08E6CF812A86C35B00A6D92F /* LottieSwitchRow.swift */, + 08E6CF832A86C49300A6D92F /* ControlsDemoView.swift */, ); path = Example; sourceTree = ""; @@ -283,7 +289,9 @@ 08E359942A55FFC400141956 /* LottieViewLayoutDemoView.swift in Sources */, 08E359922A55FFC400141956 /* ExampleApp.swift in Sources */, 085D97872A5E0DB600C78D18 /* AnimationPreviewView.swift in Sources */, + 08E6CF822A86C35B00A6D92F /* LottieSwitchRow.swift in Sources */, 085D97852A5DF94C00C78D18 /* AnimationListView.swift in Sources */, + 08E6CF842A86C49300A6D92F /* ControlsDemoView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -430,6 +438,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = iOS/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -461,6 +470,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = iOS/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; diff --git a/Example/Example/AnimationListView.swift b/Example/Example/AnimationListView.swift index 1c19902372..9ab0c3cf46 100644 --- a/Example/Example/AnimationListView.swift +++ b/Example/Example/AnimationListView.swift @@ -35,7 +35,7 @@ struct AnimationListView: View { Text(item.name) } - case .animationList: + case .animationList, .controlsDemo: Text(item.name) .frame(height: 50) } @@ -48,6 +48,8 @@ struct AnimationListView: View { AnimationPreviewView(animationSource: .remote(urls: urls, name: name)) case .animationList(let listContent): AnimationListView(content: listContent) + case .controlsDemo: + ControlsDemoView() } } } @@ -68,7 +70,7 @@ struct AnimationListView: View { guard let url = urls.first else { return nil } return await LottieAnimation.loadedFrom(url: url)?.animationSource - case .animationList: + case .animationList, .controlsDemo: return nil } } @@ -98,6 +100,7 @@ extension AnimationListView { case animationList(AnimationListView.Content) case animation(name: String, path: String) case remoteAnimations(name: String, urls: [URL]) + case controlsDemo } var items: [Item] { @@ -151,6 +154,7 @@ extension AnimationListView { return [ .animationList(.remoteAnimationsDemo), + .controlsDemo, ] } } @@ -162,6 +166,8 @@ extension AnimationListView.Item { return animationName case .animationList(let content): return content.name + case .controlsDemo: + return "Controls Demo" } } } diff --git a/Example/Example/ControlsDemoView.swift b/Example/Example/ControlsDemoView.swift new file mode 100644 index 0000000000..3ccabd4746 --- /dev/null +++ b/Example/Example/ControlsDemoView.swift @@ -0,0 +1,61 @@ +// Created by Cal Stephens on 8/11/23. +// Copyright © 2023 Airbnb Inc. All rights reserved. + +import Lottie +import SwiftUI + +// MARK: - ControlsDemoView + +struct ControlsDemoView: View { + + var body: some View { + List { + LottieSwitchRow( + animationName: "Samples/Switch", + title: "Switch", + onTimeRange: 0.5...1.0, + offTimeRange: 0.0...0.5) + + LottieSwitchRow( + animationName: "Samples/Switch", + title: "Switch (Custom Colors)", + onTimeRange: 0.5...1.0, + offTimeRange: 0.0...0.5, + colorValueProviders: [ + "Checkmark Outlines.Group 1.Stroke 1.Color": [Keyframe(.black)], + "Checkmark Outlines 2.Group 1.Stroke 1.Color": [Keyframe(.black)], + "X Outlines.Group 1.Stroke 1.Color": [Keyframe(.black)], + "Switch Outline Outlines.Fill 1.Color": [ + Keyframe(value: LottieColor.black, time: 0), + Keyframe(value: LottieColor(r: 0.76, g: 0.76, b: 0.76, a: 1), time: 75), + Keyframe(value: LottieColor.black, time: 150), + ], + ]) + + // TODO: Add SwiftUI support for AnimatedButton + // AnimatedButtonRow( + // animationName: "Samples/TwitterHeartButton", + // title: "Twitter Heart Button", + // playRanges: [ + // .init(fromMarker: "touchDownStart", toMarker: "touchDownEnd", event: .touchDown), + // .init(fromMarker: "touchDownEnd", toMarker: "touchUpCancel", event: .touchUpOutside), + // .init(fromMarker: "touchDownEnd", toMarker: "touchUpEnd", event: .touchUpInside), + // ])) + + LottieSwitchRow( + animationName: "Samples/Issues/issue_1877", + title: "Issue #1877", + onTimeRange: nil, // use the default (0...1) + offTimeRange: nil, // use the default (1...0) + colorValueProviders: ["**.Color": [Keyframe(.black)]]) + } + .navigationTitle("Controls Demo") + } + +} + +extension LottieColor { + static var black: LottieColor { + .init(r: 0, g: 0, b: 0, a: 1) + } +} diff --git a/Example/Example/LottieSwitchRow.swift b/Example/Example/LottieSwitchRow.swift new file mode 100644 index 0000000000..a8816c5a10 --- /dev/null +++ b/Example/Example/LottieSwitchRow.swift @@ -0,0 +1,53 @@ +// Created by Cal Stephens on 8/11/23. +// Copyright © 2023 Airbnb Inc. All rights reserved. + +import Lottie +import SwiftUI + +// MARK: - LottieSwitchRow + +struct LottieSwitchRow: View { + + // MARK: Internal + + var animationName: String + var title: String + var onTimeRange: ClosedRange? + var offTimeRange: ClosedRange? + var colorValueProviders: [String: [Keyframe]] = [:] + + var body: some View { + HStack { + LottieSwitch(animation: .named(animationName)) + .isOn($isOn) + .onAnimation( + fromProgress: onTimeRange?.lowerBound ?? 0, + toProgress: onTimeRange?.upperBound ?? 1) + .offAnimation( + fromProgress: offTimeRange?.lowerBound ?? 1, + toProgress: offTimeRange?.upperBound ?? 0) + .colorValueProviders(colorValueProviders) + .frame(width: 80, height: 80) + + Text(verbatim: "\(title) (isOn=\(isOn))") + } + } + + // MARK: Private + + @State private var isOn = false +} + +extension LottieSwitch { + func colorValueProviders(_ colorValueProviders: [String: [Keyframe]]) -> Self { + var copy = self + + for (keypath, keyframes) in colorValueProviders { + copy = copy.valueProvider( + ColorValueProvider(keyframes), + for: AnimationKeypath(keypath: keypath)) + } + + return copy + } +} diff --git a/Lottie.xcodeproj/project.pbxproj b/Lottie.xcodeproj/project.pbxproj index 8e4ec15bb4..60a27e3273 100644 --- a/Lottie.xcodeproj/project.pbxproj +++ b/Lottie.xcodeproj/project.pbxproj @@ -220,6 +220,18 @@ 08E2075D2A56014E002DCE17 /* EpoxyModeled.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E206DE2A56014E002DCE17 /* EpoxyModeled.swift */; }; 08E2075E2A56014E002DCE17 /* EpoxyModeled.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E206DE2A56014E002DCE17 /* EpoxyModeled.swift */; }; 08E2075F2A56014E002DCE17 /* EpoxyModeled.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E206DE2A56014E002DCE17 /* EpoxyModeled.swift */; }; + 08E6CF892A86E26F00A6D92F /* AnimatedSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF862A86E26F00A6D92F /* AnimatedSwitch.swift */; }; + 08E6CF8A2A86E26F00A6D92F /* AnimatedSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF862A86E26F00A6D92F /* AnimatedSwitch.swift */; }; + 08E6CF8B2A86E26F00A6D92F /* AnimatedSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF862A86E26F00A6D92F /* AnimatedSwitch.swift */; }; + 08E6CF8C2A86E26F00A6D92F /* LottieSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF872A86E26F00A6D92F /* LottieSwitch.swift */; }; + 08E6CF8D2A86E26F00A6D92F /* LottieSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF872A86E26F00A6D92F /* LottieSwitch.swift */; }; + 08E6CF8E2A86E26F00A6D92F /* LottieSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF872A86E26F00A6D92F /* LottieSwitch.swift */; }; + 08E6CF8F2A86E26F00A6D92F /* AnimatedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF882A86E26F00A6D92F /* AnimatedControl.swift */; }; + 08E6CF902A86E26F00A6D92F /* AnimatedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF882A86E26F00A6D92F /* AnimatedControl.swift */; }; + 08E6CF912A86E26F00A6D92F /* AnimatedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF882A86E26F00A6D92F /* AnimatedControl.swift */; }; + 08E6CF932A86E29100A6D92F /* LottieViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF922A86E29100A6D92F /* LottieViewType.swift */; }; + 08E6CF942A86E29100A6D92F /* LottieViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF922A86E29100A6D92F /* LottieViewType.swift */; }; + 08E6CF952A86E29100A6D92F /* LottieViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF922A86E29100A6D92F /* LottieViewType.swift */; }; 08EED05028F0D2D10057D958 /* LottieColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EED04F28F0D2D10057D958 /* LottieColor.swift */; }; 08EED05128F0D2D10057D958 /* LottieColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EED04F28F0D2D10057D958 /* LottieColor.swift */; }; 08EED05228F0D2D10057D958 /* LottieColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EED04F28F0D2D10057D958 /* LottieColor.swift */; }; @@ -690,9 +702,6 @@ 2EAF5ABC27A0798700E00531 /* FilepathImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D227A0798700E00531 /* FilepathImageProvider.swift */; }; 2EAF5ABD27A0798700E00531 /* FilepathImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D227A0798700E00531 /* FilepathImageProvider.swift */; }; 2EAF5ABE27A0798700E00531 /* FilepathImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D227A0798700E00531 /* FilepathImageProvider.swift */; }; - 2EAF5ABF27A0798700E00531 /* AnimatedSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D327A0798700E00531 /* AnimatedSwitch.swift */; }; - 2EAF5AC027A0798700E00531 /* AnimatedSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D327A0798700E00531 /* AnimatedSwitch.swift */; }; - 2EAF5AC127A0798700E00531 /* AnimatedSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D327A0798700E00531 /* AnimatedSwitch.swift */; }; 2EAF5AC227A0798700E00531 /* BundleImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D427A0798700E00531 /* BundleImageProvider.swift */; }; 2EAF5AC327A0798700E00531 /* BundleImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D427A0798700E00531 /* BundleImageProvider.swift */; }; 2EAF5AC427A0798700E00531 /* BundleImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D427A0798700E00531 /* BundleImageProvider.swift */; }; @@ -708,9 +717,6 @@ 2EAF5ACE27A0798700E00531 /* AnimationSubview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D827A0798700E00531 /* AnimationSubview.swift */; }; 2EAF5ACF27A0798700E00531 /* AnimationSubview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D827A0798700E00531 /* AnimationSubview.swift */; }; 2EAF5AD027A0798700E00531 /* AnimationSubview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D827A0798700E00531 /* AnimationSubview.swift */; }; - 2EAF5AD127A0798700E00531 /* AnimatedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D927A0798700E00531 /* AnimatedControl.swift */; }; - 2EAF5AD227A0798700E00531 /* AnimatedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D927A0798700E00531 /* AnimatedControl.swift */; }; - 2EAF5AD327A0798700E00531 /* AnimatedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59D927A0798700E00531 /* AnimatedControl.swift */; }; 2EAF5AD427A0798700E00531 /* AnimationTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59DB27A0798700E00531 /* AnimationTime.swift */; }; 2EAF5AD527A0798700E00531 /* AnimationTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59DB27A0798700E00531 /* AnimationTime.swift */; }; 2EAF5AD627A0798700E00531 /* AnimationTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF59DB27A0798700E00531 /* AnimationTime.swift */; }; @@ -923,6 +929,10 @@ 08E206DC2A56014E002DCE17 /* EpoxyModelProperty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpoxyModelProperty.swift; sourceTree = ""; }; 08E206DD2A56014E002DCE17 /* EpoxyModelArrayBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpoxyModelArrayBuilder.swift; sourceTree = ""; }; 08E206DE2A56014E002DCE17 /* EpoxyModeled.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpoxyModeled.swift; sourceTree = ""; }; + 08E6CF862A86E26F00A6D92F /* AnimatedSwitch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedSwitch.swift; sourceTree = ""; }; + 08E6CF872A86E26F00A6D92F /* LottieSwitch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieSwitch.swift; sourceTree = ""; }; + 08E6CF882A86E26F00A6D92F /* AnimatedControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedControl.swift; sourceTree = ""; }; + 08E6CF922A86E29100A6D92F /* LottieViewType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieViewType.swift; sourceTree = ""; }; 08EED04F28F0D2D10057D958 /* LottieColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieColor.swift; sourceTree = ""; }; 08EF21DB289C643B0097EA47 /* KeyframeInterpolator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyframeInterpolator.swift; sourceTree = ""; }; 08F8B20C2898A7B100CB5323 /* RepeaterLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeaterLayer.swift; sourceTree = ""; }; @@ -1091,13 +1101,11 @@ 2EAF59D027A0798700E00531 /* CompatibleAnimationKeypath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompatibleAnimationKeypath.swift; sourceTree = ""; }; 2EAF59D127A0798700E00531 /* CompatibleAnimationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompatibleAnimationView.swift; sourceTree = ""; }; 2EAF59D227A0798700E00531 /* FilepathImageProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilepathImageProvider.swift; sourceTree = ""; }; - 2EAF59D327A0798700E00531 /* AnimatedSwitch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedSwitch.swift; sourceTree = ""; }; 2EAF59D427A0798700E00531 /* BundleImageProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BundleImageProvider.swift; sourceTree = ""; }; 2EAF59D527A0798700E00531 /* UIColorExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColorExtension.swift; sourceTree = ""; }; 2EAF59D627A0798700E00531 /* AnimatedButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedButton.swift; sourceTree = ""; }; 2EAF59D727A0798700E00531 /* LottieAnimationViewBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieAnimationViewBase.swift; sourceTree = ""; }; 2EAF59D827A0798700E00531 /* AnimationSubview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationSubview.swift; sourceTree = ""; }; - 2EAF59D927A0798700E00531 /* AnimatedControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedControl.swift; sourceTree = ""; }; 2EAF59DB27A0798700E00531 /* AnimationTime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationTime.swift; sourceTree = ""; }; 2EAF59DC27A0798700E00531 /* Vectors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Vectors.swift; sourceTree = ""; }; 2EAF59DF27A0798700E00531 /* Interpolatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Interpolatable.swift; sourceTree = ""; }; @@ -1342,6 +1350,17 @@ path = Internal; sourceTree = ""; }; + 08E6CF852A86E26F00A6D92F /* Controls */ = { + isa = PBXGroup; + children = ( + 08E6CF862A86E26F00A6D92F /* AnimatedSwitch.swift */, + 08E6CF872A86E26F00A6D92F /* LottieSwitch.swift */, + 08E6CF882A86E26F00A6D92F /* AnimatedControl.swift */, + 08E6CF922A86E29100A6D92F /* LottieViewType.swift */, + ); + path = Controls; + sourceTree = ""; + }; 2E80409027A0725D006E74CB = { isa = PBXGroup; children = ( @@ -1857,6 +1876,7 @@ 08AB05532A61C1F000DE86FD /* Configuration */, 2EAF59C227A0798700E00531 /* macOS */, 2EAF59C827A0798700E00531 /* Animation */, + 08E6CF852A86E26F00A6D92F /* Controls */, 6C4877E028FF20140005AF07 /* DotLottie */, 2EAF59CC27A0798700E00531 /* ImageProvider */, 2EAF59CE27A0798700E00531 /* iOS */, @@ -1909,13 +1929,11 @@ children = ( 2EAF59CF27A0798700E00531 /* Compatibility */, 2EAF59D227A0798700E00531 /* FilepathImageProvider.swift */, - 2EAF59D327A0798700E00531 /* AnimatedSwitch.swift */, 2EAF59D427A0798700E00531 /* BundleImageProvider.swift */, 2EAF59D527A0798700E00531 /* UIColorExtension.swift */, 2EAF59D627A0798700E00531 /* AnimatedButton.swift */, 2EAF59D727A0798700E00531 /* LottieAnimationViewBase.swift */, 2EAF59D827A0798700E00531 /* AnimationSubview.swift */, - 2EAF59D927A0798700E00531 /* AnimatedControl.swift */, ); path = iOS; sourceTree = ""; @@ -2285,6 +2303,7 @@ 08E207182A56014E002DCE17 /* SwiftUIView.swift in Sources */, 2E9C96932822F43100677516 /* PolygonNode.swift in Sources */, 2E9C96E42822F43100677516 /* MaskCompositionLayer.swift in Sources */, + 08E6CF8C2A86E26F00A6D92F /* LottieSwitch.swift in Sources */, 08E207092A56014E002DCE17 /* EpoxyableView+SwiftUIView.swift in Sources */, 08E207212A56014E002DCE17 /* CallbackContextEpoxyModeled.swift in Sources */, 6DB3BDBC28245A14002A276D /* CGPointExtension.swift in Sources */, @@ -2352,6 +2371,7 @@ 2E9C95E82822F43100677516 /* Merge.swift in Sources */, 2E9C96032822F43100677516 /* ImageLayerModel.swift in Sources */, 19465F52282F998B00BB2C97 /* CachedImageProvider.swift in Sources */, + 08E6CF8F2A86E26F00A6D92F /* AnimatedControl.swift in Sources */, 08F8B20D2898A7B100CB5323 /* RepeaterLayer.swift in Sources */, 0887347828F0CCDD00458627 /* LottieAnimationViewInitializers.swift in Sources */, 2E9C96BA2822F43100677516 /* KeypathSearchable.swift in Sources */, @@ -2388,6 +2408,7 @@ 2E9C95FA2822F43100677516 /* Star.swift in Sources */, 2E9C961E2822F43100677516 /* KeyedDecodingContainerExtensions.swift in Sources */, AB87F02E2A72FA3A0091D7B8 /* Binding+Map.swift in Sources */, + 08E6CF932A86E29100A6D92F /* LottieViewType.swift in Sources */, 08C001F32A46150D00AB54BA /* Archive+Helpers.swift in Sources */, 08C002022A46150D00AB54BA /* Archive+ReadingDeprecated.swift in Sources */, 2E9C96512822F43100677516 /* PreCompositionLayer.swift in Sources */, @@ -2397,6 +2418,7 @@ 2E9C96182822F43100677516 /* Mask.swift in Sources */, 2E9C97622822F43100677516 /* PathElement.swift in Sources */, 2E9C97142822F43100677516 /* KeyframeGroup+exactlyOneKeyframe.swift in Sources */, + 08E6CF892A86E26F00A6D92F /* AnimatedSwitch.swift in Sources */, 2E9C970E2822F43100677516 /* CALayer+fillBounds.swift in Sources */, 2E9C95FD2822F43100677516 /* SolidLayerModel.swift in Sources */, 2E9C970B2822F43100677516 /* ValueProviderStore.swift in Sources */, @@ -2408,7 +2430,6 @@ 2E9C95E22822F43100677516 /* Group.swift in Sources */, 2E9C97112822F43100677516 /* Keyframes+combined.swift in Sources */, 0887347B28F0CCDD00458627 /* LottieAnimationView.swift in Sources */, - 2EAF5AD127A0798700E00531 /* AnimatedControl.swift in Sources */, 2E9C966F2822F43100677516 /* LayerTextProvider.swift in Sources */, 2E9C97172822F43100677516 /* CAAnimation+TimingConfiguration.swift in Sources */, 08E2075D2A56014E002DCE17 /* EpoxyModeled.swift in Sources */, @@ -2469,7 +2490,6 @@ 08C001F62A46150D00AB54BA /* Archive.swift in Sources */, 2E9C96AB2822F43100677516 /* GradientStrokeNode.swift in Sources */, 08E2071B2A56014E002DCE17 /* EpoxySwiftUIHostingView.swift in Sources */, - 2EAF5ABF27A0798700E00531 /* AnimatedSwitch.swift in Sources */, 08E207452A56014E002DCE17 /* DidDisplayProviding.swift in Sources */, 08E2074E2A56014E002DCE17 /* ViewEpoxyModeled.swift in Sources */, 2EAF5AC227A0798700E00531 /* BundleImageProvider.swift in Sources */, @@ -2544,6 +2564,7 @@ 2E9C96372822F43100677516 /* ImageAsset.swift in Sources */, 08E206E92A56014E002DCE17 /* EpoxyableView.swift in Sources */, 08E206F52A56014E002DCE17 /* SectionedChangeset.swift in Sources */, + 08E6CF942A86E29100A6D92F /* LottieViewType.swift in Sources */, 2E9C96F72822F43100677516 /* ShapeLayer.swift in Sources */, 6C48784828FF20140005AF07 /* DotLottieManifest.swift in Sources */, 08E206EF2A56014E002DCE17 /* EpoxyLogger.swift in Sources */, @@ -2595,6 +2616,7 @@ 2E9C96432822F43100677516 /* RootAnimationLayer.swift in Sources */, 2E9C97722822F43100677516 /* AnimationContext.swift in Sources */, 2E9C96B22822F43100677516 /* NodeProperty.swift in Sources */, + 08E6CF8A2A86E26F00A6D92F /* AnimatedSwitch.swift in Sources */, 08E2071C2A56014E002DCE17 /* EpoxySwiftUIHostingView.swift in Sources */, 2E9C965E2822F43100677516 /* MainThreadAnimationLayer.swift in Sources */, AB87F02C2A72F5A80091D7B8 /* View+ValueChanged.swift in Sources */, @@ -2606,6 +2628,7 @@ 2E9C96A02822F43100677516 /* TextAnimatorNode.swift in Sources */, 2EAF5AFC27A0798700E00531 /* SizeValueProvider.swift in Sources */, 2E9C97572822F43100677516 /* MathKit.swift in Sources */, + 08E6CF902A86E26F00A6D92F /* AnimatedControl.swift in Sources */, 2E9C96912822F43100677516 /* EllipseNode.swift in Sources */, 2E9C975A2822F43100677516 /* BezierPath.swift in Sources */, 2EAF5ABA27A0798700E00531 /* CompatibleAnimationView.swift in Sources */, @@ -2704,7 +2727,6 @@ 08E2075E2A56014E002DCE17 /* EpoxyModeled.swift in Sources */, 2E9C97122822F43100677516 /* Keyframes+combined.swift in Sources */, 0887347C28F0CCDD00458627 /* LottieAnimationView.swift in Sources */, - 2EAF5AD227A0798700E00531 /* AnimatedControl.swift in Sources */, 08E206EC2A56014E002DCE17 /* BehaviorsConfigurableView.swift in Sources */, 2E9C96702822F43100677516 /* LayerTextProvider.swift in Sources */, 08C002DE2A46196300AB54BA /* Archive+MemoryFile.swift in Sources */, @@ -2761,7 +2783,6 @@ 08E2072E2A56014E002DCE17 /* DidEndDisplayingProviding.swift in Sources */, 08E206F22A56014E002DCE17 /* IndexChangeset.swift in Sources */, 2E9C96AC2822F43100677516 /* GradientStrokeNode.swift in Sources */, - 2EAF5AC027A0798700E00531 /* AnimatedSwitch.swift in Sources */, 2EAF5AC327A0798700E00531 /* BundleImageProvider.swift in Sources */, 08AB055A2A61C5B700DE86FD /* DecodingStrategy.swift in Sources */, 08E207492A56014E002DCE17 /* AnimatedProviding.swift in Sources */, @@ -2778,6 +2799,7 @@ 2E9C96A92822F43100677516 /* FillNode.swift in Sources */, 2EAF5ACC27A0798700E00531 /* LottieAnimationViewBase.swift in Sources */, 2E9C96CD2822F43100677516 /* ShapeRenderLayer.swift in Sources */, + 08E6CF8D2A86E26F00A6D92F /* LottieSwitch.swift in Sources */, 5721092029119F3100169699 /* BezierPathRoundExtension.swift in Sources */, 08E207132A56014E002DCE17 /* EpoxySwiftUIHostingController.swift in Sources */, ABF033B52A7B0ABA00F8C228 /* AnyEquatable.swift in Sources */, @@ -2860,6 +2882,7 @@ 08E2071A2A56014E002DCE17 /* SwiftUIView.swift in Sources */, 2E9C96E62822F43100677516 /* MaskCompositionLayer.swift in Sources */, 08C002E62A46196300AB54BA /* Archive+ZIP64.swift in Sources */, + 08E6CF8E2A86E26F00A6D92F /* LottieSwitch.swift in Sources */, 08E2070B2A56014E002DCE17 /* EpoxyableView+SwiftUIView.swift in Sources */, 08E207232A56014E002DCE17 /* CallbackContextEpoxyModeled.swift in Sources */, 6DB3BDBE28245A14002A276D /* CGPointExtension.swift in Sources */, @@ -2927,6 +2950,7 @@ 19465F54282F998B00BB2C97 /* CachedImageProvider.swift in Sources */, 08F8B20F2898A7B100CB5323 /* RepeaterLayer.swift in Sources */, 0887347A28F0CCDD00458627 /* LottieAnimationViewInitializers.swift in Sources */, + 08E6CF912A86E26F00A6D92F /* AnimatedControl.swift in Sources */, 2E9C96BC2822F43100677516 /* KeypathSearchable.swift in Sources */, 2E9C963E2822F43100677516 /* AssetLibrary.swift in Sources */, 2E9C97042822F43100677516 /* PreCompLayer.swift in Sources */, @@ -2963,6 +2987,7 @@ 2E9C95FC2822F43100677516 /* Star.swift in Sources */, 2E9C96202822F43100677516 /* KeyedDecodingContainerExtensions.swift in Sources */, AB87F0302A72FA3A0091D7B8 /* Binding+Map.swift in Sources */, + 08E6CF952A86E29100A6D92F /* LottieViewType.swift in Sources */, 2E9C96532822F43100677516 /* PreCompositionLayer.swift in Sources */, 2EAF5AF427A0798700E00531 /* AnyValueProvider.swift in Sources */, 2E9C96652822F43100677516 /* CoreTextRenderLayer.swift in Sources */, @@ -2972,6 +2997,7 @@ 2E9C97162822F43100677516 /* KeyframeGroup+exactlyOneKeyframe.swift in Sources */, 2E9C97102822F43100677516 /* CALayer+fillBounds.swift in Sources */, 2E9C95FF2822F43100677516 /* SolidLayerModel.swift in Sources */, + 08E6CF8B2A86E26F00A6D92F /* AnimatedSwitch.swift in Sources */, 2E9C970D2822F43100677516 /* ValueProviderStore.swift in Sources */, 6C48780128FF20140005AF07 /* DotLottieAnimation.swift in Sources */, 2E9C97282822F43100677516 /* StrokeAnimation.swift in Sources */, @@ -2982,7 +3008,6 @@ 2E9C97132822F43100677516 /* Keyframes+combined.swift in Sources */, 0887347D28F0CCDD00458627 /* LottieAnimationView.swift in Sources */, 08C002F12A46196300AB54BA /* Archive+Reading.swift in Sources */, - 2EAF5AD327A0798700E00531 /* AnimatedControl.swift in Sources */, 2E9C96712822F43100677516 /* LayerTextProvider.swift in Sources */, 2E9C97192822F43100677516 /* CAAnimation+TimingConfiguration.swift in Sources */, 6C4878622901D8C70005AF07 /* DotLottieImageProvider.swift in Sources */, @@ -3041,7 +3066,6 @@ 2E9C97012822F43100677516 /* BaseAnimationLayer.swift in Sources */, 2E9C96AD2822F43100677516 /* GradientStrokeNode.swift in Sources */, 08E206F92A56014E002DCE17 /* Collection+Diff.swift in Sources */, - 2EAF5AC127A0798700E00531 /* AnimatedSwitch.swift in Sources */, 2EAF5AC427A0798700E00531 /* BundleImageProvider.swift in Sources */, 08E2071D2A56014E002DCE17 /* EpoxySwiftUIHostingView.swift in Sources */, 2E9C976D2822F43100677516 /* InterpolatableExtensions.swift in Sources */, diff --git a/Sources/Public/iOS/AnimatedControl.swift b/Sources/Public/Controls/AnimatedControl.swift similarity index 70% rename from Sources/Public/iOS/AnimatedControl.swift rename to Sources/Public/Controls/AnimatedControl.swift index 4b764df98d..500c6dd47a 100644 --- a/Sources/Public/iOS/AnimatedControl.swift +++ b/Sources/Public/Controls/AnimatedControl.swift @@ -5,9 +5,15 @@ // Created by Brandon Withrow on 2/4/19. // -import Foundation -#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) +#if canImport(UIKit) import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + +import Foundation + +// MARK: - AnimatedControl /// Lottie comes prepacked with a two Animated Controls, `AnimatedSwitch` and /// `AnimatedButton`. Both of these controls are built on top of `AnimatedControl` @@ -24,21 +30,21 @@ import UIKit /// of its layers. /// /// NOTE: Do not initialize directly. This is intended to be subclassed. -open class AnimatedControl: UIControl { +open class AnimatedControl: LottieControlType { // MARK: Lifecycle // MARK: Initializers public init( - animation: LottieAnimation, + animation: LottieAnimation?, configuration: LottieConfiguration = .shared) { animationView = LottieAnimationView( animation: animation, configuration: configuration) - super.init(frame: animation.bounds) + super.init(frame: animation?.bounds ?? .zero) commonInit() } @@ -64,11 +70,13 @@ open class AnimatedControl: UIControl { } } + #if canImport(UIKit) open override var isSelected: Bool { didSet { updateForState() } } + #endif open override var isHighlighted: Bool { didSet { @@ -80,6 +88,7 @@ open class AnimatedControl: UIControl { animationView.intrinsicContentSize } + #if canImport(UIKit) open override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { updateForState() return super.beginTracking(touch, with: event) @@ -100,6 +109,49 @@ open class AnimatedControl: UIControl { super.cancelTracking(with: event) } + #elseif canImport(AppKit) + open override func mouseDown(with _: NSEvent) { + guard let window = window else { return } + + currentState = .highlighted + updateForState() + + // AppKit mouse-tracking loop from: + // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/HandlingMouseEvents/HandlingMouseEvents.html#//apple_ref/doc/uid/10000060i-CH6-SW4 + var keepOn = true + while + keepOn, + let event = window.nextEvent( + matching: [.leftMouseUp, .leftMouseDragged], + until: .distantFuture, + inMode: .eventTracking, + dequeue: true) + { + let mouseLocation = convert(event.locationInWindow, from: nil) + let isInside = isMousePoint(mouseLocation, in: bounds) + + if event.type == .leftMouseUp { + keepOn = false + + if isInside { + handleMouseUp() + } + } + + let expectedState = (isInside && keepOn) ? LottieNSLottieControlState.highlighted : .normal + if currentState != expectedState { + currentState = expectedState + updateForState() + } + } + } + + @objc + func handleMouseUp() { + // To be overridden in subclasses + } + #endif + open func animationDidSet() { } // MARK: Public @@ -112,7 +164,11 @@ open class AnimatedControl: UIControl { didSet { animationView.animation = animation animationView.bounds = animation?.bounds ?? .zero + #if canImport(UIKit) setNeedsLayout() + #elseif canImport(AppKit) + needsLayout = true + #endif updateForState() animationDidSet() } @@ -125,7 +181,7 @@ open class AnimatedControl: UIControl { } /// Sets which Animation Layer should be visible for the given state. - public func setLayer(named: String, forState: UIControl.State) { + public func setLayer(named: String, forState: LottieControlState) { stateMap[forState.rawValue] = named updateForState() } @@ -139,10 +195,19 @@ open class AnimatedControl: UIControl { var stateMap: [UInt: String] = [:] + #if canImport(UIKit) + var currentState: LottieControlState { + state + } + + #elseif canImport(AppKit) + var currentState = LottieControlState.normal + #endif + func updateForState() { guard let animationLayer = animationView.animationLayer else { return } if - let layerName = stateMap[state.rawValue], + let layerName = stateMap[currentState.rawValue], let stateLayer = animationLayer.layer(for: AnimationKeypath(keypath: layerName)) { for layer in animationLayer._animationLayers { @@ -159,17 +224,23 @@ open class AnimatedControl: UIControl { // MARK: Private private func commonInit() { + #if canImport(UIKit) animationView.clipsToBounds = false clipsToBounds = true + #endif + animationView.translatesAutoresizingMaskIntoConstraints = false animationView.backgroundBehavior = .forceFinish addSubview(animationView) animationView.contentMode = .scaleAspectFit + + #if canImport(UIKit) animationView.isUserInteractionEnabled = false + #endif + animationView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true animationView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true animationView.topAnchor.constraint(equalTo: topAnchor).isActive = true animationView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true } } -#endif diff --git a/Sources/Public/iOS/AnimatedSwitch.swift b/Sources/Public/Controls/AnimatedSwitch.swift similarity index 82% rename from Sources/Public/iOS/AnimatedSwitch.swift rename to Sources/Public/Controls/AnimatedSwitch.swift index ecedabb11f..03196faef5 100644 --- a/Sources/Public/iOS/AnimatedSwitch.swift +++ b/Sources/Public/Controls/AnimatedSwitch.swift @@ -6,8 +6,14 @@ // import Foundation -#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) + +#if canImport(UIKit) import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + +// MARK: - AnimatedSwitch /// An interactive switch with an 'On' and 'Off' state. When the user taps on the /// switch the state is toggled and the appropriate animation is played. @@ -18,7 +24,7 @@ open class AnimatedSwitch: AnimatedControl { // MARK: Lifecycle public override init( - animation: LottieAnimation, + animation: LottieAnimation?, configuration: LottieConfiguration = .shared) { /// Generate a haptic generator if available. @@ -28,7 +34,13 @@ open class AnimatedSwitch: AnimatedControl { hapticGenerator = NullHapticGenerator() #endif super.init(animation: animation, configuration: configuration) + + #if canImport(UIKit) isAccessibilityElement = true + #elseif canImport(AppKit) + setAccessibilityElement(true) + #endif + updateOnState(isOn: _isOn, animated: false, shouldFireHaptics: false) } @@ -39,8 +51,15 @@ open class AnimatedSwitch: AnimatedControl { #else hapticGenerator = NullHapticGenerator() #endif + super.init() + + #if canImport(UIKit) isAccessibilityElement = true + #elseif canImport(AppKit) + setAccessibilityElement(true) + #endif + updateOnState(isOn: _isOn, animated: false, shouldFireHaptics: false) } @@ -52,7 +71,12 @@ open class AnimatedSwitch: AnimatedControl { hapticGenerator = NullHapticGenerator() #endif super.init(coder: aDecoder) + + #if canImport(UIKit) isAccessibilityElement = true + #elseif canImport(AppKit) + setAccessibilityElement(true) + #endif } // MARK: Open @@ -61,12 +85,20 @@ open class AnimatedSwitch: AnimatedControl { updateOnState(isOn: _isOn, animated: animateUpdateWhenChangingAnimation, shouldFireHaptics: false) } + #if canImport(UIKit) open override func endTracking(_ touch: UITouch?, with event: UIEvent?) { super.endTracking(touch, with: event) updateOnState(isOn: !_isOn, animated: true, shouldFireHaptics: true) sendActions(for: .valueChanged) } + #elseif canImport(AppKit) + open override func handleMouseUp() { + super.handleMouseUp() + updateOnState(isOn: !_isOn, animated: true, shouldFireHaptics: true) + } + #endif + // MARK: Public /// Defines what happens when the user taps the switch while an @@ -82,10 +114,15 @@ open class AnimatedSwitch: AnimatedControl { /// If `false` the switch will not play the animation when changing between animations. public var animateUpdateWhenChangingAnimation = true + #if canImport(UIKit) public override var accessibilityTraits: UIAccessibilityTraits { set { super.accessibilityTraits = newValue } get { super.accessibilityTraits.union(.button) } } + #endif + + /// A closure that is called when the `isOn` state is updated + public var stateUpdated: ((_ isOn: Bool) -> Void)? /// The current state of the switch. public var isOn: Bool { @@ -124,6 +161,11 @@ open class AnimatedSwitch: AnimatedControl { // MARK: Internal + private(set) var onStartProgress: CGFloat = 0 + private(set) var onEndProgress: CGFloat = 1 + private(set) var offStartProgress: CGFloat = 1 + private(set) var offEndProgress: CGFloat = 0 + // MARK: Animation State func updateOnState(isOn: Bool, animated: Bool, shouldFireHaptics: Bool) { @@ -176,21 +218,27 @@ open class AnimatedSwitch: AnimatedControl { // MARK: Fileprivate - fileprivate var onStartProgress: CGFloat = 0 - fileprivate var onEndProgress: CGFloat = 1 - fileprivate var offStartProgress: CGFloat = 1 - fileprivate var offEndProgress: CGFloat = 0 - fileprivate var _isOn = false fileprivate var hapticGenerator: ImpactGenerator + fileprivate var _isOn = false { + didSet { + stateUpdated?(_isOn) + } + } + // MARK: Private private func updateAccessibilityLabel() { - accessibilityValue = _isOn ? NSLocalizedString("On", comment: "On") : NSLocalizedString("Off", comment: "Off") + let value = _isOn ? NSLocalizedString("On", comment: "On") : NSLocalizedString("Off", comment: "Off") + + #if canImport(UIKit) + accessibilityValue = value + #elseif canImport(AppKit) + setAccessibilityValue(value) + #endif } } -#endif // MARK: - ImpactGenerator diff --git a/Sources/Public/Controls/LottieSwitch.swift b/Sources/Public/Controls/LottieSwitch.swift new file mode 100644 index 0000000000..ff328e3c15 --- /dev/null +++ b/Sources/Public/Controls/LottieSwitch.swift @@ -0,0 +1,144 @@ +// Created by Cal Stephens on 8/11/23. +// Copyright © 2023 Airbnb Inc. All rights reserved. + +import SwiftUI + +/// A wrapper which exposes Lottie's `AnimatedSwitch` to SwiftUI +@available(iOS 13.0, tvOS 13.0, macOS 10.15, *) +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) -> 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: 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.Configuration]() + + // MARK: Private + + private let animation: LottieAnimation? + private var configuration: LottieConfiguration = .shared + private var isOn: Binding? + +} diff --git a/Sources/Public/Controls/LottieViewType.swift b/Sources/Public/Controls/LottieViewType.swift new file mode 100644 index 0000000000..a8e0e58e5b --- /dev/null +++ b/Sources/Public/Controls/LottieViewType.swift @@ -0,0 +1,32 @@ +// Created by Cal Stephens on 8/11/23. +// Copyright © 2023 Airbnb Inc. All rights reserved. + +#if canImport(UIKit) +import UIKit + +/// The control base type for this platform. +/// - `UIControl` on iOS / tvOS and `NSControl` on macOS. +public typealias LottieControlType = UIControl + +/// The `State` type of `LottieControlType` +/// - `UIControl.State` on iOS / tvOS and `NSControl.StateValue` on macOS. +public typealias LottieControlState = UIControl.State +#else +import AppKit + +/// The control base type for this platform. +/// - `UIControl` on iOS / tvOS and `NSControl` on macOS. +public typealias LottieControlType = NSControl + +/// The `State` type of `LottieControlType` +/// - `UIControl.State` on iOS / tvOS and `NSControl.StateValue` on macOS. +public typealias LottieControlState = LottieNSLottieControlState + +/// AppKit equivalent of `UIControl.State` for `AnimatedControl` +public enum LottieNSLottieControlState: UInt, RawRepresentable { + /// The normal, or default, state of a control where the control is enabled but neither selected nor highlighted. + case normal + /// The highlighted state of a control. + case highlighted +} +#endif diff --git a/Sources/Public/iOS/AnimatedButton.swift b/Sources/Public/iOS/AnimatedButton.swift index 987aa5a161..03bc9d0d41 100644 --- a/Sources/Public/iOS/AnimatedButton.swift +++ b/Sources/Public/iOS/AnimatedButton.swift @@ -14,7 +14,7 @@ open class AnimatedButton: AnimatedControl { // MARK: Lifecycle public override init( - animation: LottieAnimation, + animation: LottieAnimation?, configuration: LottieConfiguration = .shared) { super.init(animation: animation, configuration: configuration)