Fix iOS 13 compatibility

Revert back the onPlatformAppear to fix iOS 14+ behavior
Use backport for all OSs
This commit is contained in:
DreamPiggy 2022-09-20 20:26:35 +08:00
parent 83d46c08b5
commit e1c32aea7d
5 changed files with 122 additions and 66 deletions

View File

@ -101,15 +101,7 @@ final class AnimatedImageConfiguration: ObservableObject {
/// A Image View type to load image from url, data or bundle. Supports animated and static image format.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct AnimatedImage : PlatformViewRepresentable {
@SwiftUI.StateObject var imageModel_SwiftUI = AnimatedImageModel()
@Backport.StateObject var imageModel_Backport = AnimatedImageModel()
var imageModel: AnimatedImageModel {
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
return imageModel_SwiftUI
} else {
return imageModel_Backport
}
}
@ObservedObject var imageModel: AnimatedImageModel
@ObservedObject var imageHandler = AnimatedImageHandler()
@ObservedObject var imageLayout = AnimatedImageLayout()
@ObservedObject var imageConfiguration = AnimatedImageConfiguration()
@ -186,11 +178,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
init(imageModel: AnimatedImageModel, isAnimating: Binding<Bool>) {
self._isAnimating = isAnimating
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
_imageModel_SwiftUI = SwiftUI.StateObject(wrappedValue: imageModel)
} else {
_imageModel_Backport = Backport.StateObject(wrappedValue: imageModel)
}
_imageModel = ObservedObject(wrappedValue: imageModel)
}
#if os(macOS)

View File

@ -28,37 +28,24 @@ public final class ImageManager : ObservableObject {
/// true means during incremental loading
@Published public var isIncremental: Bool = false
var manager: SDWebImageManager?
weak var currentOperation: SDWebImageOperation? = nil
var url: URL?
var options: SDWebImageOptions = []
var context: [SDWebImageContextOption : Any]? = nil
var successBlock: ((PlatformImage, Data?, SDImageCacheType) -> Void)?
var failureBlock: ((Error) -> Void)?
var progressBlock: ((Int, Int) -> Void)?
/// Create a image manager for loading the specify url, with custom options and context.
public init() {}
/// Start to load the url operation
/// - Parameter url: The image url
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
/// - Parameter context: A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
self.url = url
self.options = options
self.context = context
if let manager = context?[.customManager] as? SDWebImageManager {
self.manager = manager
public func load(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
let manager: SDWebImageManager
if let customManager = context?[.customManager] as? SDWebImageManager {
manager = customManager
} else {
self.manager = .shared
}
}
init() {}
/// Start to load the url operation
public func load() {
guard let manager = manager else {
return
manager = .shared
}
if currentOperation != nil {
return

View File

@ -0,0 +1,84 @@
/*
* This file is part of the SDWebImage package.
* (c) DreamPiggy <lizhuoli1126@126.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import Foundation
import SwiftUI
#if os(iOS) || os(tvOS) || os(macOS)
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
struct PlatformAppear: PlatformViewRepresentable {
let appearAction: () -> Void
let disappearAction: () -> Void
#if os(iOS) || os(tvOS)
func makeUIView(context: Context) -> some UIView {
let view = PlatformAppearView()
view.appearAction = appearAction
view.disappearAction = disappearAction
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) {}
#endif
#if os(macOS)
func makeNSView(context: Context) -> some NSView {
let view = PlatformAppearView()
view.appearAction = appearAction
view.disappearAction = disappearAction
return view
}
func updateNSView(_ nsView: NSViewType, context: Context) {}
#endif
}
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
class PlatformAppearView: PlatformView {
var appearAction: () -> Void = {}
var disappearAction: () -> Void = {}
#if os(iOS) || os(tvOS)
override func willMove(toWindow newWindow: UIWindow?) {
if newWindow != nil {
appearAction()
} else {
disappearAction()
}
}
#endif
#if os(macOS)
override func viewWillMove(toWindow newWindow: NSWindow?) {
if newWindow != nil {
appearAction()
} else {
disappearAction()
}
}
#endif
}
#endif
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension View {
/// Used UIKit/AppKit behavior to detect the SwiftUI view's visibility.
/// This hack is because of SwiftUI 1.0/2.0 buggy behavior. The built-in `onAppear` and `onDisappear` is so massive on some cases. Where UIKit/AppKit is solid.
/// - Parameters:
/// - appear: The action when view appears
/// - disappear: The action when view disappears
/// - Returns: Some view
func onPlatformAppear(appear: @escaping () -> Void = {}, disappear: @escaping () -> Void = {}) -> some View {
#if os(iOS) || os(tvOS) || os(macOS)
return self.background(PlatformAppear(appearAction: appear, disappearAction: disappear))
#else
return self.onAppear(perform: appear).onDisappear(perform: disappear)
#endif
}
}

View File

@ -9,6 +9,15 @@
import SwiftUI
import SDWebImage
/// Data Binding Object, only properties in this object can support changes from user with @State and refresh
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
final class WebImageModel : ObservableObject {
/// URL image
@Published var url: URL?
@Published var webOptions: SDWebImageOptions = []
@Published var webContext: [SDWebImageContextOption : Any]? = nil
}
/// Completion Handler Binding Object, supports dynamic @State changes
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
final class WebImageHandler: ObservableObject {
@ -43,6 +52,9 @@ public struct WebImage : View {
/// True to start animation, false to stop animation.
@Binding public var isAnimating: Bool
/// A observed object to pass through the image model to manager
@ObservedObject var imageModel: WebImageModel
/// A observed object to pass through the image handler to manager
@ObservedObject var imageHandler = WebImageHandler()
@ -52,25 +64,10 @@ public struct WebImage : View {
/// A observed object to pass through the image manager loading status to indicator
@ObservedObject var indicatorStatus = IndicatorStatus()
@SwiftUI.StateObject var imagePlayer_SwiftUI = ImagePlayer()
@Backport.StateObject var imagePlayer_Backport = ImagePlayer()
var imagePlayer: ImagePlayer {
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
return imagePlayer_SwiftUI
} else {
return imagePlayer_Backport
}
}
@ObservedObject var imagePlayer = ImagePlayer()
@SwiftUI.StateObject var imageManager_SwiftUI = ImageManager()
@Backport.StateObject var imageManager_Backport = ImageManager()
var imageManager: ImageManager {
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
return imageManager_SwiftUI
} else {
return imageManager_Backport
}
}
// FIXME: Use SwiftUI StateObject and remove onPlatformAppear once drop iOS 13 support
@Backport.StateObject var imageManager = ImageManager()
/// Create a web image with url, placeholder, custom options and context. Optional can support animated image using Binding.
/// - Parameter url: The image url
@ -86,11 +83,11 @@ public struct WebImage : View {
context[.animatedImageClass] = SDAnimatedImage.self
}
}
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
_imageManager_SwiftUI = SwiftUI.StateObject(wrappedValue: ImageManager(url: url, options: options, context: context))
} else {
_imageManager_Backport = Backport.StateObject(wrappedValue: ImageManager(url: url, options: options, context: context))
}
let imageModel = WebImageModel()
imageModel.url = url
imageModel.webOptions = options
imageModel.webContext = context
_imageModel = ObservedObject(wrappedValue: imageModel)
}
/// Create a web image with url, placeholder, custom options and context.
@ -128,24 +125,24 @@ public struct WebImage : View {
}
} else {
setupPlaceholder()
.onAppear {
.onPlatformAppear(appear: {
self.imageManager.successBlock = self.imageHandler.successBlock
self.imageManager.failureBlock = self.imageHandler.failureBlock
self.imageManager.progressBlock = self.imageHandler.progressBlock
// Load remote image when first appear
self.imageManager.load()
self.imageManager.load(url: imageModel.url, options: imageModel.webOptions, context: imageModel.webContext)
guard self.imageConfiguration.retryOnAppear else { return }
// When using prorgessive loading, the new partial image will cause onAppear. Filter this case
if self.imageManager.image == nil && !self.imageManager.isIncremental {
self.imageManager.load()
self.imageManager.load(url: imageModel.url, options: imageModel.webOptions, context: imageModel.webContext)
}
}.onDisappear {
}, disappear: {
guard self.imageConfiguration.cancelOnDisappear else { return }
// When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case
if self.imageManager.image == nil && !self.imageManager.isIncremental {
self.imageManager.cancel()
}
}.onReceive(imageManager.objectWillChange) { _ in
}).onReceive(imageManager.objectWillChange) { _ in
indicatorStatus.isLoading = imageManager.isLoading
indicatorStatus.progress = imageManager.progress
}
@ -228,7 +225,7 @@ public struct WebImage : View {
// Don't use `Group` because it will trigger `.onAppear` and `.onDisappear` when condition view removed, treat placeholder as an entire component
if let placeholder = placeholder {
// If use `.delayPlaceholder`, the placeholder is applied after loading failed, hide during loading :)
if imageManager.options.contains(.delayPlaceholder) && imageManager.isLoading {
if imageModel.webOptions.contains(.delayPlaceholder) && imageManager.isLoading {
return AnyView(configure(image: .empty))
} else {
return placeholder

View File

@ -18,7 +18,7 @@ class ImageManagerTests: XCTestCase {
func testImageManager() throws {
let expectation = self.expectation(description: "ImageManager usage with Combine")
let imageUrl = URL(string: "https://via.placeholder.com/500x500.jpg")
let imageManager = ImageManager(url: imageUrl)
let imageManager = ImageManager()
imageManager.setOnSuccess { image, cacheType, data in
XCTAssertNotNil(image)
expectation.fulfill()
@ -29,7 +29,7 @@ class ImageManagerTests: XCTestCase {
imageManager.setOnProgress { receivedSize, expectedSize in
}
imageManager.load()
imageManager.load(url: imageUrl)
XCTAssertNotNil(imageManager.currentOperation)
let sub = imageManager.objectWillChange
.subscribe(on: RunLoop.main)