Re-implements the aspectRatio support on AnimatedImage, fix issue like cornerRadius

Use the correct way to override invalidateIntrinsicContentSize to keep aspect ratio to UIKit/SwiftUI engine
This commit is contained in:
DreamPiggy 2024-06-27 16:37:09 +08:00
parent 03c468bac2
commit 1edee7f019
3 changed files with 45 additions and 83 deletions

View File

@ -17,6 +17,17 @@ class UserSettings: ObservableObject {
#endif #endif
} }
struct ContentView4: View {
var url = URL(string: "https://github.com/SDWebImage/SDWebImageSwiftUI/assets/97430818/72d27f90-e9d8-48d7-b144-82ada828a027")!
var body: some View {
AnimatedImage(url: url)
.resizable()
.scaledToFit()
// .aspectRatio(nil, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50, style: .continuous))
}
}
// Test Switching nil url // Test Switching nil url
struct ContentView3: View { struct ContentView3: View {
@State var isOn = false @State var isOn = false

View File

@ -275,6 +275,9 @@ public struct AnimatedImage : PlatformViewRepresentable {
self.imageModel.placeholderView?.isHidden = false self.imageModel.placeholderView?.isHidden = false
self.imageHandler.failureBlock?(error ?? NSError()) self.imageHandler.failureBlock?(error ?? NSError())
} }
// Finished loading
configureView(view, context: context)
layoutView(view, context: context)
} }
} }
@ -361,20 +364,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
break // impossible break // impossible
} }
#if os(macOS) // Finished loading
if self.isAnimating != view.wrapped.animates {
view.wrapped.animates = self.isAnimating
}
#else
if self.isAnimating != view.wrapped.isAnimating {
if self.isAnimating {
view.wrapped.startAnimating()
} else {
view.wrapped.stopAnimating()
}
}
#endif
configureView(view, context: context) configureView(view, context: context)
layoutView(view, context: context) layoutView(view, context: context)
if let viewUpdateBlock = imageHandler.viewUpdateBlock { if let viewUpdateBlock = imageHandler.viewUpdateBlock {
@ -442,9 +432,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
#endif #endif
// Resizable // Resizable
if let _ = imageLayout.resizingMode { view.resizingMode = imageLayout.resizingMode
view.resizable = true
}
// Animated Image does not support resizing mode and rendering mode // Animated Image does not support resizing mode and rendering mode
if let image = view.wrapped.image { if let image = view.wrapped.image {
@ -587,6 +575,21 @@ public struct AnimatedImage : PlatformViewRepresentable {
} else { } else {
view.wrapped.playbackMode = .normal view.wrapped.playbackMode = .normal
} }
// Animation
#if os(macOS)
if self.isAnimating != view.wrapped.animates {
view.wrapped.animates = self.isAnimating
}
#else
if self.isAnimating != view.wrapped.isAnimating {
if self.isAnimating {
view.wrapped.startAnimating()
} else {
view.wrapped.stopAnimating()
}
}
#endif
} }
} }
@ -630,68 +633,6 @@ extension AnimatedImage {
} }
} }
// Aspect Ratio
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension AnimatedImage {
func setImageLayoutAspectRatio(_ aspectRatio: CGFloat?, contentMode: ContentMode) {
self.imageLayout.aspectRatio = aspectRatio
self.imageLayout.contentMode = contentMode
}
/// Constrains this view's dimensions to the specified aspect ratio.
/// - Parameters:
/// - aspectRatio: The ratio of width to height to use for the resulting
/// view. If `aspectRatio` is `nil`, the resulting view maintains this
/// view's aspect ratio.
/// - contentMode: A flag indicating whether this view should fit or
/// fill the parent context.
/// - Returns: A view that constrains this view's dimensions to
/// `aspectRatio`, using `contentMode` as its scaling algorithm.
@ViewBuilder
public func aspectRatio(_ aspectRatio: CGFloat? = nil, contentMode: ContentMode) -> some View {
// The `SwifUI.View.aspectRatio(_:contentMode:)` says:
// If `aspectRatio` is `nil`, the resulting view maintains this view's aspect ratio
// But 1: there are no public API to declare what `this view's aspect ratio` is
// So, if we don't override this method, SwiftUI ignore the content mode on actual ImageView
// To workaround, we want to call the default `SwifUI.View.aspectRatio(_:contentMode:)` method
// But 2: there are no way to call a Protocol Extention default implementation in Swift 5.1
// So, we directly call the implementation detail modifier instead
// Fired Radar: FB7413534
let _ = self.setImageLayoutAspectRatio(aspectRatio, contentMode: contentMode)
if let aspectRatio {
self.modifier(_AspectRatioLayout(aspectRatio: aspectRatio, contentMode: contentMode))
} else {
self
}
}
/// Constrains this view's dimensions to the aspect ratio of the given size.
/// - Parameters:
/// - aspectRatio: A size specifying the ratio of width to height to use
/// for the resulting view.
/// - contentMode: A flag indicating whether this view should fit or
/// fill the parent context.
/// - Returns: A view that constrains this view's dimensions to
/// `aspectRatio`, using `contentMode` as its scaling algorithm.
public func aspectRatio(_ aspectRatio: CGSize, contentMode: ContentMode) -> some View {
return self.aspectRatio(aspectRatio.width / aspectRatio.height, contentMode: contentMode)
}
/// Scales this view to fit its parent.
/// - Returns: A view that scales this view to fit its parent,
/// maintaining this view's aspect ratio.
public func scaledToFit() -> some View {
return self.aspectRatio(nil, contentMode: .fit)
}
/// Scales this view to fill its parent.
/// - Returns: A view that scales this view to fit its parent,
/// maintaining this view's aspect ratio.
public func scaledToFill() -> some View {
return self.aspectRatio(nil, contentMode: .fill)
}
}
// AnimatedImage Modifier // AnimatedImage Modifier
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension AnimatedImage { extension AnimatedImage {

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import SDWebImage import SDWebImage
import SwiftUI
#if !os(watchOS) #if !os(watchOS)
@ -18,7 +19,7 @@ public class AnimatedImageViewWrapper : PlatformView {
public var wrapped = SDAnimatedImageView() public var wrapped = SDAnimatedImageView()
var interpolationQuality = CGInterpolationQuality.default var interpolationQuality = CGInterpolationQuality.default
var shouldAntialias = false var shouldAntialias = false
var resizable = false var resizingMode: Image.ResizingMode?
public override func draw(_ rect: CGRect) { public override func draw(_ rect: CGRect) {
#if os(macOS) #if os(macOS)
@ -48,11 +49,20 @@ public class AnimatedImageViewWrapper : PlatformView {
public override var intrinsicContentSize: CGSize { public override var intrinsicContentSize: CGSize {
/// Match the behavior of SwiftUI.Image, only when image is resizable, use the super implementation to calculate size /// Match the behavior of SwiftUI.Image, only when image is resizable, use the super implementation to calculate size
if resizable { let imageSize = wrapped.intrinsicContentSize
return super.intrinsicContentSize if let _ = resizingMode {
/// Keep aspect ratio
let noIntrinsicMetric = AnimatedImageViewWrapper.noIntrinsicMetric
if (imageSize.width > 0 && imageSize.height > 0) {
let ratio = imageSize.width / imageSize.height
let size = CGSize(width: ratio, height: 1)
return size
} else {
return CGSize(width: noIntrinsicMetric, height: noIntrinsicMetric)
}
} else { } else {
/// Not resizable, always use image size, like SwiftUI.Image /// Not resizable, always use image size, like SwiftUI.Image
return wrapped.intrinsicContentSize return imageSize
} }
} }