Skip to content
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
94 changes: 83 additions & 11 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 Down Expand Up @@ -101,6 +160,19 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult {
}
}

private final class LazyReference<Value> {
private let factory: () -> Value
private lazy var storedValue = self.factory()

var value: Value {
self.storedValue
}

init(factory: @escaping () -> Value) {
self.factory = factory
}
}

public extension AsyncSwiftUIImageView {
func data(_ data: ImageViewData) -> Self {
var view = self
Expand Down
110 changes: 110 additions & 0 deletions AsyncImageViewTests/AsyncSwiftUIImageViewStateSpec.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import Quick
import Nimble
import SwiftUI
import UIKit

import ReactiveSwift

@testable import AsyncImageView

class AsyncSwiftUIImageViewStateSpec: QuickSpec {
override class func spec() {
describe("AsyncSwiftUIImageView state") {
it("preserves every scheduler initializer combination") {
let renderer = StateTestRenderer()

_ = StateTestImageView(renderer: renderer)
_ = StateTestImageView(renderer: renderer, uiScheduler: ImmediateScheduler())
_ = StateTestImageView(renderer: renderer, imageCreationScheduler: ImmediateScheduler())
_ = StateTestImageView(
renderer: renderer,
uiScheduler: ImmediateScheduler(),
imageCreationScheduler: ImmediateScheduler()
)
}

it("creates its retained schedulers only once across parent updates") {
let schedulerFactory = SchedulerFactory()
let renderer = StateTestRenderer()
let initialView = StateTestContainer(
generation: 0,
renderer: renderer,
schedulerFactory: schedulerFactory
)
let viewController = UIHostingController(rootView: initialView)
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 100, height: 100)))
window.rootViewController = viewController
window.makeKeyAndVisible()
viewController.view.frame = window.bounds
viewController.view.layoutIfNeeded()

for generation in 1...100 {
viewController.rootView = StateTestContainer(
generation: generation,
renderer: renderer,
schedulerFactory: schedulerFactory
)
viewController.view.layoutIfNeeded()
}

expect(schedulerFactory.uiSchedulerCount.value) == 1
expect(schedulerFactory.imageSchedulerCount.value) == 1
}
}
}
}

private struct StateTestContainer: View {
let generation: Int
let renderer: StateTestRenderer
let schedulerFactory: SchedulerFactory

var body: some View {
StateTestImageView(
renderer: self.renderer,
placeholderRenderer: nil,
uiSchedulerFactory: self.schedulerFactory.makeUIScheduler,
imageCreationSchedulerFactory: self.schedulerFactory.makeImageScheduler
)
.frame(width: 100, height: 100)
.accessibilityIdentifier("generation-\(self.generation)")
}
}

private typealias StateTestImageView = AsyncSwiftUIImageView<
StateTestRenderData,
StateTestViewData,
StateTestRenderer,
StateTestRenderer
>

private final class SchedulerFactory {
let uiSchedulerCount = Atomic(0)
let imageSchedulerCount = Atomic(0)

func makeUIScheduler() -> ReactiveSwift.Scheduler {
self.uiSchedulerCount.modify { $0 += 1 }
return ImmediateScheduler()
}

func makeImageScheduler() -> ReactiveSwift.Scheduler {
self.imageSchedulerCount.modify { $0 += 1 }
return ImmediateScheduler()
}
}

private struct StateTestViewData: ImageViewDataType {
func renderDataWithSize(_ size: CGSize) -> StateTestRenderData {
StateTestRenderData(size: size)
}
}

private struct StateTestRenderData: RenderDataType {
let size: CGSize
}

private final class StateTestRenderer: RendererType {
func renderImageWithData(_ data: StateTestRenderData) -> SignalProducer<UIImage, Never> {
SignalProducer(value: UIImage())
}
}
Loading