Skip to content

Commit

Permalink
Merge pull request #324 from SDWebImage/bugfix/animatedimage_aspect_r…
Browse files Browse the repository at this point in the history
…atio_related_issues

Re-implements the aspectRatio support on AnimatedImage, fix issue like cornerRadius
  • Loading branch information
dreampiggy authored Jun 27, 2024
2 parents 03c468b + c8320d4 commit 1ba96a0
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 84 deletions.
13 changes: 13 additions & 0 deletions Example/SDWebImageSwiftUIDemo/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ class UserSettings: ObservableObject {
#endif
}

#if !os(watchOS)
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))
}
}
#endif

// Test Switching nil url
struct ContentView3: View {
@State var isOn = false
Expand Down
111 changes: 31 additions & 80 deletions SDWebImageSwiftUI/Classes/AnimatedImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ public struct AnimatedImage : PlatformViewRepresentable {
self.imageModel.placeholderView?.isHidden = false
self.imageHandler.failureBlock?(error ?? NSError())
}
// Finished loading, async
finishUpdateView(view, context: context, image: image)
}
}

Expand Down Expand Up @@ -361,22 +363,9 @@ public struct AnimatedImage : PlatformViewRepresentable {
break // impossible
}

#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
// Finished loading, sync
finishUpdateView(view, context: context, image: view.wrapped.image)

configureView(view, context: context)
layoutView(view, context: context)
if let viewUpdateBlock = imageHandler.viewUpdateBlock {
viewUpdateBlock(view.wrapped, context)
}
Expand All @@ -394,6 +383,17 @@ public struct AnimatedImage : PlatformViewRepresentable {
}
}

func finishUpdateView(_ view: AnimatedImageViewWrapper, context: Context, image: PlatformImage?) {
// Finished loading
if let imageSize = image?.size {
view.imageSize = imageSize
} else {
view.imageSize = nil
}
configureView(view, context: context)
layoutView(view, context: context)
}

func layoutView(_ view: AnimatedImageViewWrapper, context: Context) {
// AspectRatio && ContentMode
#if os(macOS)
Expand Down Expand Up @@ -442,9 +442,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
#endif

// Resizable
if let _ = imageLayout.resizingMode {
view.resizable = true
}
view.resizingMode = imageLayout.resizingMode

// Animated Image does not support resizing mode and rendering mode
if let image = view.wrapped.image {
Expand Down Expand Up @@ -587,6 +585,21 @@ public struct AnimatedImage : PlatformViewRepresentable {
} else {
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
}
}

Expand Down Expand Up @@ -630,68 +643,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
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension AnimatedImage {
Expand Down
26 changes: 22 additions & 4 deletions SDWebImageSwiftUI/Classes/ImageViewWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Foundation
import SDWebImage
import SwiftUI

#if !os(watchOS)

Expand All @@ -18,7 +19,8 @@ public class AnimatedImageViewWrapper : PlatformView {
public var wrapped = SDAnimatedImageView()
var interpolationQuality = CGInterpolationQuality.default
var shouldAntialias = false
var resizable = false
var resizingMode: Image.ResizingMode?
var imageSize: CGSize?

public override func draw(_ rect: CGRect) {
#if os(macOS)
Expand Down Expand Up @@ -48,11 +50,27 @@ public class AnimatedImageViewWrapper : PlatformView {

public override var intrinsicContentSize: CGSize {
/// Match the behavior of SwiftUI.Image, only when image is resizable, use the super implementation to calculate size
if resizable {
return super.intrinsicContentSize
var contentSize = wrapped.intrinsicContentSize
/// Sometimes, like during the transaction, the wrapped.image == nil, which cause contentSize invalid
/// Use image size as backup
/// TODO: This mixed use of UIKit/SwiftUI animation will cause visial issue because the intrinsicContentSize during animation may be changed
if let imageSize = imageSize {
if contentSize != imageSize {
contentSize = imageSize
}
}
if let _ = resizingMode {
/// Keep aspect ratio
if contentSize.width > 0 && contentSize.height > 0 {
let ratio = contentSize.width / contentSize.height
let size = CGSize(width: ratio, height: 1)
return size
} else {
return contentSize
}
} else {
/// Not resizable, always use image size, like SwiftUI.Image
return wrapped.intrinsicContentSize
return contentSize
}
}

Expand Down

0 comments on commit 1ba96a0

Please sign in to comment.