From 75a1b2cf505dba3e49efe8e1102ab6f99710cd0f Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 23 Feb 2021 15:29:03 +0800 Subject: [PATCH] Update watchOS demo to watchOS 7, remove the custom indicator sample and use `ProgressView` instead --- Example/Podfile | 2 +- .../project.pbxproj | 22 +- ...eSwiftUIDemo-watchOS WatchKit App.xcscheme | 25 +- .../SDWebImageSwiftUIDemo/ContentView.swift | 45 +--- Example/SDWebImageSwiftUIDemo/Espera.swift | 234 ------------------ README.md | 15 +- 6 files changed, 32 insertions(+), 311 deletions(-) delete mode 100644 Example/SDWebImageSwiftUIDemo/Espera.swift diff --git a/Example/Podfile b/Example/Podfile index a2a70d4..d570628 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -23,6 +23,6 @@ target 'SDWebImageSwiftUIDemo-tvOS' do end target 'SDWebImageSwiftUIDemo-watchOS WatchKit Extension' do - platform :watchos, '6.0' + platform :watchos, '7.0' all_pods end \ No newline at end of file diff --git a/Example/SDWebImageSwiftUI.xcodeproj/project.pbxproj b/Example/SDWebImageSwiftUI.xcodeproj/project.pbxproj index 3c33ff4..9261c3f 100644 --- a/Example/SDWebImageSwiftUI.xcodeproj/project.pbxproj +++ b/Example/SDWebImageSwiftUI.xcodeproj/project.pbxproj @@ -14,10 +14,6 @@ 320CDC3222FADB45007CF858 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 320CDC3122FADB45007CF858 /* Assets.xcassets */; }; 320CDC3522FADB45007CF858 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 320CDC3422FADB45007CF858 /* Preview Assets.xcassets */; }; 320CDC3822FADB45007CF858 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 320CDC3622FADB45007CF858 /* LaunchScreen.storyboard */; }; - 3243598423E05C3D006DF9C5 /* Espera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3243598323E05C3D006DF9C5 /* Espera.swift */; }; - 3243598523E05C3D006DF9C5 /* Espera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3243598323E05C3D006DF9C5 /* Espera.swift */; }; - 3243598623E05C3D006DF9C5 /* Espera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3243598323E05C3D006DF9C5 /* Espera.swift */; }; - 3243598723E05C3D006DF9C5 /* Espera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3243598323E05C3D006DF9C5 /* Espera.swift */; }; 326B0D712345C01900D28269 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B0D702345C01900D28269 /* DetailView.swift */; }; 32E5290C2348A0C700EA46FF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E5290B2348A0C700EA46FF /* AppDelegate.swift */; }; 32E529102348A0C900EA46FF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 32E5290F2348A0C900EA46FF /* Assets.xcassets */; }; @@ -98,7 +94,6 @@ 320CDC3422FADB45007CF858 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 320CDC3722FADB45007CF858 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 320CDC3922FADB45007CF858 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 3243598323E05C3D006DF9C5 /* Espera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Espera.swift; sourceTree = ""; }; 326B0D702345C01900D28269 /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; }; 32E529092348A0C700EA46FF /* SDWebImageSwiftUIDemo-macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SDWebImageSwiftUIDemo-macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 32E5290B2348A0C700EA46FF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -212,7 +207,6 @@ 320CDC2D22FADB44007CF858 /* SceneDelegate.swift */, 320CDC2F22FADB44007CF858 /* ContentView.swift */, 326B0D702345C01900D28269 /* DetailView.swift */, - 3243598323E05C3D006DF9C5 /* Espera.swift */, 320CDC3122FADB45007CF858 /* Assets.xcassets */, 320CDC3622FADB45007CF858 /* LaunchScreen.storyboard */, 320CDC3922FADB45007CF858 /* Info.plist */, @@ -792,7 +786,6 @@ 320CDC2C22FADB44007CF858 /* AppDelegate.swift in Sources */, 326B0D712345C01900D28269 /* DetailView.swift in Sources */, 320CDC2E22FADB44007CF858 /* SceneDelegate.swift in Sources */, - 3243598423E05C3D006DF9C5 /* Espera.swift in Sources */, 320CDC3022FADB44007CF858 /* ContentView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -804,7 +797,6 @@ 32E529622348A10B00EA46FF /* ContentView.swift in Sources */, 32E529632348A10B00EA46FF /* DetailView.swift in Sources */, 32E5290C2348A0C700EA46FF /* AppDelegate.swift in Sources */, - 3243598523E05C3D006DF9C5 /* Espera.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -815,7 +807,6 @@ 32E529652348A10B00EA46FF /* ContentView.swift in Sources */, 32E529662348A10B00EA46FF /* DetailView.swift in Sources */, 32E529232348A0D300EA46FF /* AppDelegate.swift in Sources */, - 3243598623E05C3D006DF9C5 /* Espera.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -826,7 +817,6 @@ 32E5294E2348A0DE00EA46FF /* HostingController.swift in Sources */, 32E529692348A10C00EA46FF /* DetailView.swift in Sources */, 32E529502348A0DE00EA46FF /* ExtensionDelegate.swift in Sources */, - 3243598723E05C3D006DF9C5 /* Espera.swift in Sources */, 32E529682348A10C00EA46FF /* ContentView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1102,7 +1092,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.0; + WATCHOS_DEPLOYMENT_TARGET = 7.0; }; name = Debug; }; @@ -1132,7 +1122,7 @@ SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.0; + WATCHOS_DEPLOYMENT_TARGET = 7.0; }; name = Release; }; @@ -1163,7 +1153,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.0; + WATCHOS_DEPLOYMENT_TARGET = 7.0; }; name = Debug; }; @@ -1191,7 +1181,7 @@ SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.0; + WATCHOS_DEPLOYMENT_TARGET = 7.0; }; name = Release; }; @@ -1217,6 +1207,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_VERSION = 5.0; + WATCHOS_DEPLOYMENT_TARGET = 7.0; }; name = Debug; }; @@ -1239,6 +1230,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.dreampiggy.SDWebImageSwiftUIDemo-watchOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; + WATCHOS_DEPLOYMENT_TARGET = 7.0; }; name = Release; }; @@ -1287,7 +1279,6 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -1333,7 +1324,6 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/Example/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUIDemo-watchOS WatchKit App.xcscheme b/Example/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUIDemo-watchOS WatchKit App.xcscheme index 3d1f081..65df20b 100644 --- a/Example/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUIDemo-watchOS WatchKit App.xcscheme +++ b/Example/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUIDemo-watchOS WatchKit App.xcscheme @@ -54,10 +54,8 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - + - + - + - - - - - + diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index dabbffe..16184a7 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -17,53 +17,20 @@ class UserSettings: ObservableObject { #endif } -#if os(watchOS) -// watchOS does not provide built-in indicator, use Espera's custom indicator -struct ActivityIndicator : View { - @Binding var isAnimating: Bool - var body: some View { - if isAnimating { - return AnyView(LoadingFlowerView() - .frame(width: 30, height: 30)) - } else { - return AnyView(EmptyView() - .frame(width: 30, height: 30)) - } - } -} - -struct ProgressIndicator : View { - @Binding var isAnimating: Bool - @Binding var progress: Double - var body: some View { - if isAnimating { - return AnyView(StretchProgressView(progress: $progress) - .frame(width: 140, height: 10)) - } else { - return AnyView(EmptyView() - .frame(width: 140, height: 10)) - } - } -} - -extension Indicator where T == ActivityIndicator { - /// Activity Indicator +@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *) +extension Indicator where T == ProgressView { static var activity: Indicator { - Indicator { isAnimating, _ in - ActivityIndicator(isAnimating: isAnimating) + Indicator { isAnimating, progress in + ProgressView() } } -} - -extension Indicator where T == ProgressIndicator { - /// Progress Indicator + static var progress: Indicator { Indicator { isAnimating, progress in - ProgressIndicator(isAnimating: isAnimating, progress: progress) + ProgressView(value: progress.wrappedValue) } } } -#endif struct ContentView: View { @State var imageURLs = [ diff --git a/Example/SDWebImageSwiftUIDemo/Espera.swift b/Example/SDWebImageSwiftUIDemo/Espera.swift deleted file mode 100644 index a38e10e..0000000 --- a/Example/SDWebImageSwiftUIDemo/Espera.swift +++ /dev/null @@ -1,234 +0,0 @@ -// -// Espera.swift -// Espera -// -// Created by jagcesar on 2019-12-29. -// Copyright © 2019 Ambi. All rights reserved. -// - -import SwiftUI - -public struct RotatingCircleWithGap: View { - @State private var angle: Double = 270 - @State var isAnimating = false - private let lineWidth: CGFloat = 2 - - var foreverAnimation: Animation { - Animation.linear(duration: 1) - .repeatForever(autoreverses: false) - } - - public init() { } - - public var body: some View { - Circle() - .trim(from: 0.15, to: 1) - .stroke(Color.gray, style: StrokeStyle(lineWidth: self.lineWidth, lineCap: .round, lineJoin: CGLineJoin.round)) - .rotationEffect((Angle(degrees: self.isAnimating ? 360.0 : 0))) - .padding(EdgeInsets(top: lineWidth/2, leading: lineWidth/2, bottom: lineWidth/2, trailing: lineWidth/2)) - .animation(foreverAnimation) - .onAppear { - self.isAnimating = true - } - } -} - -private struct LoadingCircle: View { - let circleColor: Color - let scale: CGFloat - let circleWidth: CGFloat - - var body: some View { - Circle() - .fill(circleColor) - .frame(width: circleWidth, height: circleWidth, alignment: .center) - .scaleEffect(scale) - } -} - -public struct LoadingFlowerView: View { - - private let animationDuration: Double = 0.6 - private var singleCircleAnimationDuration: Double { - return animationDuration/3 - } - private var foreverAnimation: Animation { - Animation.linear(duration: animationDuration) - .repeatForever(autoreverses: true) - } - - private let originalColor: Color - public init(color: Color = .white) { - self.originalColor = color - self.color = color.opacity(0.3) - } - - @State private var color = Color.white.opacity(0.3) - @State private var scale: CGFloat = 0.98 - - public var body: some View { - GeometryReader { [color, scale, singleCircleAnimationDuration, foreverAnimation] reader -> AnyView in - - let minLength = min(reader.size.width, reader.size.height) - let thirdOfMinLength = minLength / 3 - - let proportionalSpacing: CGFloat = 1 / 26 - let spacing = minLength * proportionalSpacing - - // THIS IS FINE :D - // Fix later, ok? - let leafDiameter = thirdOfMinLength - (spacing - proportionalSpacing * thirdOfMinLength) - - return AnyView( - HStack(spacing: spacing) { - VStack(spacing: spacing) { - LoadingCircle(circleColor: color, scale: scale, circleWidth: leafDiameter) - .animation(foreverAnimation.delay(singleCircleAnimationDuration*5)) - LoadingCircle(circleColor: color, scale: scale, circleWidth: leafDiameter) - .animation(foreverAnimation.delay(singleCircleAnimationDuration*4)) - } - VStack(alignment: .center, spacing: spacing) { - LoadingCircle(circleColor: color, scale: scale, circleWidth: leafDiameter) - .animation(foreverAnimation) - LoadingCircle(circleColor: .clear, scale: 1, circleWidth: leafDiameter) - LoadingCircle(circleColor: color, scale: scale, circleWidth: leafDiameter) - .animation(foreverAnimation.delay(singleCircleAnimationDuration*3)) - } - VStack(alignment: .center, spacing: spacing) { - LoadingCircle(circleColor: color, scale: scale, circleWidth: leafDiameter) - .animation(foreverAnimation.delay(singleCircleAnimationDuration*1)) - LoadingCircle(circleColor: color, scale: scale, circleWidth: leafDiameter) - .animation(foreverAnimation.delay(singleCircleAnimationDuration*2)) - } - } - ) - } - .onAppear { - self.color = self.originalColor - self.scale = 1 - } - .aspectRatio(contentMode: .fit) - .frame(idealWidth: 26) - } -} - -private class StretchyShapeModel { - var forwards = true -} - -extension StretchyShape { - enum Side { - case front, back - } - - enum Mode { - case lagged, stretchy - } -} - -private struct StretchyShape: Shape { - - var progress: Double - var mode: Mode - init(progress: Double, mode: Mode = .lagged) { - self.progress = progress - self.mode = mode - } - - private var model = StretchyShapeModel() - - func path(in rect: CGRect) -> Path { - Path { path in - - addSide(.back, to: &path, rect: rect) - addSide(.front, to: &path, rect: rect) - - if progress >= 1 { - model.forwards.toggle() - } - } - } - - var animatableData: Double { - set { progress = newValue } - get { progress } - } - - private func easeInOutQuad(_ x: CGFloat) -> CGFloat { - if x <= 0.5 { - return pow(x, 2) * 2 - } - - let x = x - 0.5 - return 2 * x * (1 - x) + 0.5 - } - - private func addSide(_ side: Side, to path: inout Path, rect: CGRect) { - let lag = 0.1 - - let laggedProgress: CGFloat - let startAngle: Angle - let endAngle: Angle - switch side { - case .front: - laggedProgress = CGFloat(progress + lag) - startAngle = Angle(degrees: 90) - endAngle = Angle(degrees: -90) - case .back: - if mode == .stretchy { - laggedProgress = 0 - } else { - laggedProgress = CGFloat(progress - lag) - } - startAngle = Angle(degrees: -90) - endAngle = Angle(degrees: 90) - } - - var progress = max(0, min(1, laggedProgress)) - - if !model.forwards { - progress = 1 - progress - } - - let radius = rect.height / 2 - let offset = easeInOutQuad(progress) * (rect.width - rect.height) - - path.addArc(center: CGPoint(x: radius + offset, y: radius), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: model.forwards) - } -} - -public struct StretchLoadingView: View { - - @State private var progress: Double = 0 - - public init() { } - - public var body: some View { - StretchyShape(progress: progress) - .animation(Animation.linear(duration: 0.6).repeatForever(autoreverses: false)) - .onAppear { - withAnimation { - self.progress = 1 - } - } - } -} - -public struct StretchProgressView: View { - - @Binding public var progress: Double - - public var body: some View { - StretchyShape(progress: progress, mode: .stretchy) - } -} - -struct Previews: PreviewProvider { - static var previews: some View { - Group { - RotatingCircleWithGap() - LoadingFlowerView() - StretchLoadingView().frame(width: 60, height: 14) - } - } -} diff --git a/README.md b/README.md index c5fb01a..1584b08 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,9 @@ var body: some View { } ``` -Note: This `WebImage` using `Image` for internal implementation, which is the best compatible for SwiftUI layout and animation system. But unlike SwiftUI's `Image` which does not support animated image or vector image, `WebImage` supports animated image as well (by defaults from v1.6.0) +Note: This `WebImage` using `Image` for internal implementation, which is the best compatible for SwiftUI layout and animation system. But unlike SwiftUI's `Image` which does not support animated image or vector image, `WebImage` supports animated image as well (by defaults from v1.6.0). + +However, The `WebImage` animation provide simple common use case, so it's still recommend to use `AnimatedImage` for advanced controls like progressive animation rendering, or vector image rendering. ```swift @State var isAnimating: Bool = true @@ -162,7 +164,16 @@ var body: some View { } ``` -Note: The `WebImage` animation provide common use case, so it's still recommend to use `AnimatedImage` for advanced controls like progressive animation rendering, or vector image rendering. In a word, `WebImage` can render animated image, but not always the best choice. +Note: For indicator, you can custom your own as well. For example, iOS 14/watchOS 7 introduce the new `ProgressView`, which can replace our built-in `ProgressIndicator/ActivityIndicator` (where watchOS does not provide). + +```swift +WebImage(url: url) +.indicator { + Indicator { _, _ in + ProgressView() + } +} +``` ### Using `AnimatedImage` to play animation