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
6 changes: 6 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ jobs:
action: test
warnings-as-errors: false
verbosity: xcbeautify

- name: Install SwiftLint
run: brew install swiftlint

- name: Run SwiftLint
run: swiftlint --config .swiftlint.yml --reporter github-actions-logging --strict
30 changes: 30 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
included:
- AsyncImageView
- AsyncImageViewTests
- Package.swift

excluded:
- .build
- .swiftpm
- Docs

opt_in_rules:
- empty_count
- explicit_init
- overridden_super_call
- redundant_nil_coalescing

analyzer_rules:
- unused_import
- unused_optional_binding

disabled_rules:
- force_cast
- function_body_length
- function_name_whitespace
- identifier_name
- line_length
- nesting
- private_over_fileprivate
- todo
- type_body_length
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Agent Instructions

- Always run `swiftlint --config .swiftlint.yml --strict` from the repository root before submitting changes. This lints the AsyncImageView codebase with the shared configuration.
117 changes: 58 additions & 59 deletions AsyncImageView/AsyncImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,60 +21,59 @@ public protocol ImageViewDataType {

/// A `UIImageView` that can render asynchronously.
open class AsyncImageView<
Data: RenderDataType,
Data: RenderDataType,
ImageViewData: ImageViewDataType,
Renderer: RendererType,
PlaceholderRenderer: RendererType>: UIImageView
where
ImageViewData.RenderData == Data,
Renderer.Data == Data,
Renderer.Error == Never,
PlaceholderRenderer.Data == Data,
PlaceholderRenderer.Error == Never,
Renderer.RenderResult == PlaceholderRenderer.RenderResult
{
private typealias ImageLoader = AsyncImageLoader<Data, ImageViewData, Renderer, PlaceholderRenderer>
ImageViewData.RenderData == Data,
Renderer.Data == Data,
Renderer.Error == Never,
PlaceholderRenderer.Data == Data,
PlaceholderRenderer.Error == Never,
Renderer.RenderResult == PlaceholderRenderer.RenderResult {
private typealias ImageLoader = AsyncImageLoader<Data, ImageViewData, Renderer, PlaceholderRenderer>

private let requestsSignal: Signal<Data?, Never>
private let requestsObserver: Signal<Data?, Never>.Observer

private let imageCreationScheduler: ReactiveSwift.Scheduler
private let imageCreationScheduler: ReactiveSwift.Scheduler

private var disposable: Disposable?

private var disposable: Disposable?

public init(
initialFrame: CGRect,
renderer: Renderer,
placeholderRenderer: PlaceholderRenderer? = nil,
uiScheduler: ReactiveSwift.Scheduler = UIScheduler(),
imageCreationScheduler: ReactiveSwift.Scheduler = QueueScheduler())
{
(self.requestsSignal, self.requestsObserver) = Signal.pipe()
self.imageCreationScheduler = imageCreationScheduler

super.init(frame: initialFrame)

self.backgroundColor = nil
self.disposable = ImageLoader.createSignal(
requestsSignal: self.requestsSignal,
renderer: renderer,
placeholderRenderer: placeholderRenderer,
uiScheduler: uiScheduler,
imageCreationScheduler: imageCreationScheduler
)
.observeValues { [weak self] result in
self?.updateImage(result)
}
uiScheduler: ReactiveSwift.Scheduler = UIScheduler(),
imageCreationScheduler: ReactiveSwift.Scheduler = QueueScheduler()
) {
(self.requestsSignal, self.requestsObserver) = Signal.pipe()
self.imageCreationScheduler = imageCreationScheduler

super.init(frame: initialFrame)

self.backgroundColor = nil

self.disposable = ImageLoader.createSignal(
requestsSignal: self.requestsSignal,
renderer: renderer,
placeholderRenderer: placeholderRenderer,
uiScheduler: uiScheduler,
imageCreationScheduler: imageCreationScheduler
)
.observeValues { [weak self] result in
self?.updateImage(result)
}
}

public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

deinit {
self.disposable?.dispose()
}
deinit {
self.disposable?.dispose()
}

open override var frame: CGRect {
didSet {
Expand All @@ -93,19 +92,19 @@ open class AsyncImageView<
self.requestNewImageIfReady()
}
}
open override func didMoveToWindow() {
super.didMoveToWindow()
if self.window != nil {
self.requestNewImageIfReady()
}
}

open override func didMoveToWindow() {
super.didMoveToWindow()

if self.window != nil {
self.requestNewImageIfReady()
}
}

// MARK: -

private func requestNewImageIfReady() {
if self.window != nil && self.bounds.size.width > 0 && self.bounds.size.height > 0 {
if self.window != nil && self.bounds.size.width > 0 && self.bounds.size.height > 0 {
self.requestNewImage(self.bounds.size, data: self.data)
}
}
Expand All @@ -121,21 +120,21 @@ open class AsyncImageView<
// MARK: -

private func updateImage(_ result: Renderer.RenderResult?) {
if let result = result {
if result.cacheHit {
self.image = result.image
} else {
UIView.transition(
with: self,
duration: fadeAnimationDuration,
options: [.curveEaseOut, .transitionCrossDissolve],
animations: { self.image = result.image },
completion: nil
)
}
} else {
self.image = nil
}
if let result = result {
if result.cacheHit {
self.image = result.image
} else {
UIView.transition(
with: self,
duration: fadeAnimationDuration,
options: [.curveEaseOut, .transitionCrossDissolve],
animations: { self.image = result.image },
completion: nil
)
}
} else {
self.image = nil
}
}
}

Expand Down
21 changes: 10 additions & 11 deletions AsyncImageView/AsyncSwiftUIImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,14 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult {
private let uiScheduler: ReactiveSwift.Scheduler
private let requestsSignal: Signal<Data?, Never>
private let requestsObserver: Signal<Data?, Never>.Observer

private let imageCreationScheduler: ReactiveSwift.Scheduler

public init(
renderer: Renderer,
placeholderRenderer: PlaceholderRenderer? = nil,
uiScheduler: ReactiveSwift.Scheduler = UIScheduler(),
imageCreationScheduler: ReactiveSwift.Scheduler = QueueScheduler())
{
imageCreationScheduler: ReactiveSwift.Scheduler = QueueScheduler()) {
self.renderer = renderer
self.placeholderRenderer = placeholderRenderer
self.uiScheduler = uiScheduler
Expand All @@ -48,13 +47,13 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult {

@State private var renderResult: Renderer.RenderResult?
@State private var disposable: Disposable?

public var data: ImageViewData? {
didSet {
self.requestImage()
}
}

@State
private var size: CGSize = .zero {
didSet {
Expand All @@ -63,11 +62,11 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult {
}
}
}

public var body: some View {
ZStack {
self.imageView

Color.clear
.modifier(SizeModifier())
.onPreferenceChange(ImageSizePreferenceKey.self) { imageSize in
Expand Down Expand Up @@ -111,7 +110,7 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult {
Color.clear
}
}

private func requestImage() {
guard self.size.width > 0 && self.size.height > 0 else {
return
Expand All @@ -134,9 +133,9 @@ public extension AsyncSwiftUIImageView {

private struct ImageSizePreferenceKey: PreferenceKey {
typealias Value = CGSize

static var defaultValue: CGSize = .zero

static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
Expand Down
4 changes: 4 additions & 0 deletions AsyncImageView/Caching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public protocol NSDataConvertible {
/// Returns the directory where all `DiskCache` caches are stored
/// by default.
public func diskCacheDefaultCacheDirectory() -> URL {
// swiftlint:disable:next force_try
return try! FileManager()
.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("AsyncImageView", isDirectory: true)
Expand Down Expand Up @@ -138,8 +139,10 @@ public final class DiskCache<K: DataFileType, V: NSDataConvertible>: CacheType {
self.guaranteeDirectoryExists(url.deletingLastPathComponent())

if let data = value.flatMap({ $0.data }) {
// swiftlint:disable:next force_try
try! data.write(to: url, options: .atomicWrite)
} else if self.fileManager.fileExists(atPath: url.path) {
// swiftlint:disable:next force_try
try! self.fileManager.removeItem(at: url)
}
}
Expand All @@ -165,6 +168,7 @@ public final class DiskCache<K: DataFileType, V: NSDataConvertible>: CacheType {
}

private func guaranteeDirectoryExists(_ url: URL) {
// swiftlint:disable:next force_try
try! self.fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
}
}
3 changes: 1 addition & 2 deletions AsyncImageView/Renderers/AnyRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ public final class AnyRenderer<

/// Creates an `AnyRenderer` based on another `RendererType`.
public convenience init<R: RendererType>(_ renderer: R)
where R.Data == Data, R.RenderResult == RenderResult, R.Error == Error
{
where R.Data == Data, R.RenderResult == RenderResult, R.Error == Error {
self.init(renderBlock: renderer.renderImageWithData)
}

Expand Down
7 changes: 2 additions & 5 deletions AsyncImageView/Renderers/CacheRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ public final class CacheRenderer<
>: RendererType
where
Cache.Key == Renderer.Data,
Cache.Value == Renderer.RenderResult
{
Cache.Value == Renderer.RenderResult {
private let renderer: Renderer
private let cache: Cache

Expand Down Expand Up @@ -51,8 +50,7 @@ public final class CacheRenderer<
extension RendererType {
/// Surrounds this renderer with a layer of caching.
public func withCache<Cache: CacheType>(_ cache: Cache) -> CacheRenderer<Self, Cache>
where Cache.Key == Self.Data, Cache.Value == Self.RenderResult
{
where Cache.Key == Self.Data, Cache.Value == Self.RenderResult {
return CacheRenderer(renderer: self, cache: cache)
}
}
Expand Down Expand Up @@ -90,4 +88,3 @@ private extension UIImage {
)
}
}

12 changes: 6 additions & 6 deletions AsyncImageView/Renderers/ContextRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import CoreGraphics
#if !os(watchOS)

/// `SynchronousRendererType` which generates a `UIImage` by rendering into a context.
@available(iOS 10.0, tvOSApplicationExtension 10.0, *)
@available(iOS 10.0, tvOSApplicationExtension 10.0, *)
public final class ContextRenderer<Data: RenderDataType>: SynchronousRendererType {
public typealias Block = (_ context: CGContext, _ data: Data) -> ()
public typealias Block = (_ context: CGContext, _ data: Data) -> Void

private let format: UIGraphicsImageRendererFormat
private let imageSize: CGSize?
private let renderingBlock: Block

/// - opaque: A Boolean flag indicating whether the bitmap is opaque.
/// If you know the bitmap is fully opaque, specify YES to ignore the
/// alpha channel and optimize the bitmap’s storage.
Expand All @@ -33,13 +33,13 @@ public final class ContextRenderer<Data: RenderDataType>: SynchronousRendererTyp
self.imageSize = imageSize
self.renderingBlock = renderingBlock
}

public func renderImageWithData(_ data: Data) -> UIImage {
let renderer = UIGraphicsImageRenderer(
size: self.imageSize ?? data.size,
format: self.format
)

return renderer.image { context in
self.renderingBlock(context.cgContext, data)
}
Expand Down
Loading