Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading