Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ concurrency:

jobs:
test:
runs-on: macos-latest
runs-on: macos-26
timeout-minutes: 30
env:
CI: true
Expand Down
4 changes: 1 addition & 3 deletions AsyncImageView/AsyncImageLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult {
requestsSignal: Signal<Data?, Never>,
renderer: Renderer,
placeholderRenderer: PlaceholderRenderer?,
uiScheduler: ReactiveSwift.Scheduler,
imageCreationScheduler: ReactiveSwift.Scheduler
uiScheduler: ReactiveSwift.Scheduler
) -> Signal<Renderer.RenderResult?, Never> {
return requestsSignal.skipRepeats(==)
.observe(on: imageCreationScheduler)
.flatMap(.latest) { data -> SignalProducer<Renderer.RenderResult?, Never> in
let prefixSignal: SignalProducer<Renderer.RenderResult?, Never> = .init(value: nil)

Expand Down
11 changes: 7 additions & 4 deletions AsyncImageView/AsyncImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ open class AsyncImageView<
requestsSignal: self.requestsSignal,
renderer: renderer,
placeholderRenderer: placeholderRenderer,
uiScheduler: uiScheduler,
imageCreationScheduler: imageCreationScheduler
uiScheduler: uiScheduler
)
.observeValues { [weak self] result in
self?.updateImage(result)
Expand All @@ -77,13 +76,17 @@ open class AsyncImageView<

open override var frame: CGRect {
didSet {
self.requestNewImageIfReady()
if self.frame.size != oldValue.size {
self.requestNewImageIfReady()
}
}
}

open override var bounds: CGRect {
didSet {
self.requestNewImageIfReady()
if self.bounds.size != oldValue.size {
self.requestNewImageIfReady()
}
}
}

Expand Down
129 changes: 89 additions & 40 deletions AsyncImageView/AsyncSwiftUIImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,82 @@ public struct AsyncSwiftUIImageView<
Renderer.Error == Never,
PlaceholderRenderer.Data == Data,
PlaceholderRenderer.Error == Never,
Renderer.RenderResult == PlaceholderRenderer.RenderResult {
Renderer.RenderResult == PlaceholderRenderer.RenderResult {
private typealias ViewModel = AsyncSwiftUIImageViewModel<Data, ImageViewData, Renderer, PlaceholderRenderer>

@State private var viewModel: ViewModel
@State private var viewModelReference: LazyReference<ViewModel>

private var viewModel: ViewModel {
self.viewModelReference.value
}

public init(
renderer: Renderer,
placeholderRenderer: PlaceholderRenderer? = nil
) {
self.init(
renderer: renderer,
placeholderRenderer: placeholderRenderer,
uiSchedulerFactory: { UIScheduler() },
imageCreationSchedulerFactory: { QueueScheduler() }
)
}

public init(
renderer: Renderer,
placeholderRenderer: PlaceholderRenderer? = nil,
uiScheduler: ReactiveSwift.Scheduler
) {
self.init(
renderer: renderer,
placeholderRenderer: placeholderRenderer,
uiSchedulerFactory: { uiScheduler },
imageCreationSchedulerFactory: { QueueScheduler() }
)
}

public init(
renderer: Renderer,
placeholderRenderer: PlaceholderRenderer? = nil,
imageCreationScheduler: ReactiveSwift.Scheduler
) {
self.init(
renderer: renderer,
placeholderRenderer: placeholderRenderer,
uiSchedulerFactory: { UIScheduler() },
imageCreationSchedulerFactory: { imageCreationScheduler }
)
}

public init(
renderer: Renderer,
placeholderRenderer: PlaceholderRenderer? = nil,
uiScheduler: ReactiveSwift.Scheduler = UIScheduler(),
imageCreationScheduler: ReactiveSwift.Scheduler = QueueScheduler()) {
_viewModel = State(
initialValue: ViewModel(
renderer: renderer,
placeholderRenderer: placeholderRenderer,
uiScheduler: uiScheduler,
imageCreationScheduler: imageCreationScheduler
)
uiScheduler: ReactiveSwift.Scheduler,
imageCreationScheduler: ReactiveSwift.Scheduler
) {
self.init(
renderer: renderer,
placeholderRenderer: placeholderRenderer,
uiSchedulerFactory: { uiScheduler },
imageCreationSchedulerFactory: { imageCreationScheduler }
)
}

internal init(
renderer: Renderer,
placeholderRenderer: PlaceholderRenderer?,
uiSchedulerFactory: @escaping () -> ReactiveSwift.Scheduler,
imageCreationSchedulerFactory: @escaping () -> ReactiveSwift.Scheduler
) {
_viewModelReference = State(
initialValue: LazyReference {
ViewModel(
renderer: renderer,
placeholderRenderer: placeholderRenderer,
uiScheduler: uiSchedulerFactory(),
imageCreationScheduler: imageCreationSchedulerFactory()
)
}
)
}

Expand All @@ -58,12 +117,11 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult {
public var body: some View {
ZStack {
self.imageView

Color.clear
.modifier(SizeModifier())
.onPreferenceChange(ImageSizePreferenceKey.self) { imageSize in
self.size = imageSize
}
}
.onGeometryChange(for: CGSize.self) { geometry in
geometry.size
} action: { imageSize in
self.size = imageSize
}
.onAppear {
self.viewModel.start()
Expand Down Expand Up @@ -101,33 +159,25 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult {
}
}

public extension AsyncSwiftUIImageView {
func data(_ data: ImageViewData) -> Self {
var view = self
view.data = data
private final class LazyReference<Value> {
private let factory: () -> Value
private lazy var storedValue = self.factory()

return view
var value: Value {
self.storedValue
}
}

private struct ImageSizePreferenceKey: PreferenceKey {
typealias Value = CGSize

static var defaultValue: CGSize = .zero

static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
init(factory: @escaping () -> Value) {
self.factory = factory
}
}

private struct SizeModifier: ViewModifier {
func body(content: Content) -> some View {
content.background(
GeometryReader { geometry in
Color.clear
.preference(key: ImageSizePreferenceKey.self, value: geometry.size)
}
)
public extension AsyncSwiftUIImageView {
func data(_ data: ImageViewData) -> Self {
var view = self
view.data = data

return view
}
}

Expand Down Expand Up @@ -178,8 +228,7 @@ private final class AsyncSwiftUIImageViewModel<
requestsSignal: self.requestsSignal,
renderer: self.renderer,
placeholderRenderer: self.placeholderRenderer,
uiScheduler: self.uiScheduler,
imageCreationScheduler: self.imageCreationScheduler
uiScheduler: self.uiScheduler
)
.observeValues { [weak self] result in
self?.renderResult = result
Expand Down
20 changes: 18 additions & 2 deletions AsyncImageView/Caching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,22 @@ public protocol NSDataConvertible {

/// Encodes the receiver in `NSData`. Returns `nil` if failed.
var data: Data? { get }

/// Creates an instance from its disk-cache representation.
static func valueFromCacheData(_ data: Data) -> Self?

/// Encodes the receiver for storage in `DiskCache`. Returns `nil` if failed.
var cacheData: Data? { get }
}

public extension NSDataConvertible {
static func valueFromCacheData(_ data: Data) -> Self? {
Self(data: data)
}

var cacheData: Data? {
self.data
}
}

/// Returns the directory where all `DiskCache` caches are stored
Expand Down Expand Up @@ -129,7 +145,7 @@ public final class DiskCache<K: DataFileType, V: NSDataConvertible>: CacheType {

public func valueForKey(_ key: K) -> V? {
return withLock { (try? Data(contentsOf: self.filePathForKey(key))) }
.flatMap(V.init)
.flatMap(V.valueFromCacheData)
}

public func setValue(_ value: V?, forKey key: K) {
Expand All @@ -138,7 +154,7 @@ public final class DiskCache<K: DataFileType, V: NSDataConvertible>: CacheType {
self.withLock {
self.guaranteeDirectoryExists(url.deletingLastPathComponent())

if let data = value.flatMap({ $0.data }) {
if let data = value.flatMap({ $0.cacheData }) {
// swiftlint:disable:next force_try
try! data.write(to: url, options: .atomicWrite)
} else if self.fileManager.fileExists(atPath: url.path) {
Expand Down
52 changes: 52 additions & 0 deletions AsyncImageView/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ extension UIImage: NSDataConvertible {
public var data: Data? {
return self.pngData()
}

public static func valueFromCacheData(_ data: Data) -> Self? {
CachedImageSerialization.image(from: data) as? Self
}

public var cacheData: Data? {
CachedImageSerialization.data(for: self)
}
}

extension ImageResult: NSDataConvertible {
Expand All @@ -31,6 +39,50 @@ extension ImageResult: NSDataConvertible {
public var data: Data? {
return self.image.data
}

public static func valueFromCacheData(_ data: Data) -> ImageResult? {
UIImage.valueFromCacheData(data).map { image in
ImageResult(image: image, cacheHit: false)
}
}

public var cacheData: Data? {
self.image.cacheData
}
}

private enum CachedImageSerialization {
private static let currentVersion = 1

private struct Payload: Codable {
let version: Int
let pngData: Data
let scale: Double
}

static func data(for image: UIImage) -> Data? {
guard let pngData = image.pngData() else { return nil }

let payload = Payload(
version: self.currentVersion,
pngData: pngData,
scale: Double(image.scale)
)
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary

return try? encoder.encode(payload)
}

static func image(from data: Data) -> UIImage? {
guard let payload = try? PropertyListDecoder().decode(Payload.self, from: data),
payload.version == self.currentVersion,
payload.scale.isFinite,
payload.scale > 0,
let image = UIImage(data: payload.pngData, scale: CGFloat(payload.scale)) else { return nil }

return image
}
}

extension Result {
Expand Down
Loading
Loading