diff --git a/AsyncImageView/AsyncImageView.swift b/AsyncImageView/AsyncImageView.swift index 9e57ae4..b8907dd 100644 --- a/AsyncImageView/AsyncImageView.swift +++ b/AsyncImageView/AsyncImageView.swift @@ -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 + ImageViewData.RenderData == Data, + Renderer.Data == Data, + Renderer.Error == Never, + PlaceholderRenderer.Data == Data, + PlaceholderRenderer.Error == Never, + Renderer.RenderResult == PlaceholderRenderer.RenderResult { + private typealias ImageLoader = AsyncImageLoader private let requestsSignal: Signal private let requestsObserver: Signal.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 { @@ -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) } } @@ -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 + } } } diff --git a/AsyncImageView/AsyncSwiftUIImageView.swift b/AsyncImageView/AsyncSwiftUIImageView.swift index fd654c4..0261dab 100644 --- a/AsyncImageView/AsyncSwiftUIImageView.swift +++ b/AsyncImageView/AsyncSwiftUIImageView.swift @@ -29,15 +29,14 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult { private let uiScheduler: ReactiveSwift.Scheduler private let requestsSignal: Signal private let requestsObserver: Signal.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 @@ -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 { @@ -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 @@ -111,7 +110,7 @@ Renderer.RenderResult == PlaceholderRenderer.RenderResult { Color.clear } } - + private func requestImage() { guard self.size.width > 0 && self.size.height > 0 else { return @@ -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() } diff --git a/AsyncImageView/Renderers/AnyRenderer.swift b/AsyncImageView/Renderers/AnyRenderer.swift index c88f0ef..7058e98 100644 --- a/AsyncImageView/Renderers/AnyRenderer.swift +++ b/AsyncImageView/Renderers/AnyRenderer.swift @@ -20,8 +20,7 @@ public final class AnyRenderer< /// Creates an `AnyRenderer` based on another `RendererType`. public convenience init(_ 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) } diff --git a/AsyncImageView/Renderers/CacheRenderer.swift b/AsyncImageView/Renderers/CacheRenderer.swift index fe75a5c..5c791ef 100644 --- a/AsyncImageView/Renderers/CacheRenderer.swift +++ b/AsyncImageView/Renderers/CacheRenderer.swift @@ -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 @@ -51,8 +50,7 @@ public final class CacheRenderer< extension RendererType { /// Surrounds this renderer with a layer of caching. public func withCache(_ cache: Cache) -> CacheRenderer - where Cache.Key == Self.Data, Cache.Value == Self.RenderResult - { + where Cache.Key == Self.Data, Cache.Value == Self.RenderResult { return CacheRenderer(renderer: self, cache: cache) } } @@ -90,4 +88,3 @@ private extension UIImage { ) } } - diff --git a/AsyncImageView/Renderers/ContextRenderer.swift b/AsyncImageView/Renderers/ContextRenderer.swift index d2b0881..3fa5dc2 100644 --- a/AsyncImageView/Renderers/ContextRenderer.swift +++ b/AsyncImageView/Renderers/ContextRenderer.swift @@ -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: 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. @@ -33,13 +33,13 @@ public final class ContextRenderer: 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) } diff --git a/AsyncImageView/Renderers/ErrorIgnoringRenderer.swift b/AsyncImageView/Renderers/ErrorIgnoringRenderer.swift index a71254a..9378dfc 100644 --- a/AsyncImageView/Renderers/ErrorIgnoringRenderer.swift +++ b/AsyncImageView/Renderers/ErrorIgnoringRenderer.swift @@ -15,9 +15,9 @@ import ReactiveSwift /// if you're already providing a placeholder renderer. public final class ErrorIgnoringRenderer: RendererType { private let renderer: Renderer - private let handler: ((Renderer.Error) -> ())? + private let handler: ((Renderer.Error) -> Void)? - public init(renderer: Renderer, handler: ((Renderer.Error) -> ())?) { + public init(renderer: Renderer, handler: ((Renderer.Error) -> Void)?) { self.renderer = renderer self.handler = handler } @@ -27,7 +27,7 @@ public final class ErrorIgnoringRenderer: RendererType { .renderImageWithData(data) .flatMapError { [handler = self.handler] error in handler?(error) - + return .empty } } @@ -38,9 +38,9 @@ extension RendererType { public func ignoreErrors() -> ErrorIgnoringRenderer { return ErrorIgnoringRenderer(renderer: self, handler: nil) } - + /// Returns a new `RendererType` that will ignore any errors emitted by the receiver. - public func logAndIgnoreErrors(handler: @escaping (Self.Error) -> ()) -> ErrorIgnoringRenderer { + public func logAndIgnoreErrors(handler: @escaping (Self.Error) -> Void) -> ErrorIgnoringRenderer { return ErrorIgnoringRenderer(renderer: self, handler: handler) } } diff --git a/AsyncImageView/Renderers/FallbackRenderer.swift b/AsyncImageView/Renderers/FallbackRenderer.swift index e8d3552..4e717ac 100644 --- a/AsyncImageView/Renderers/FallbackRenderer.swift +++ b/AsyncImageView/Renderers/FallbackRenderer.swift @@ -28,8 +28,7 @@ public final class FallbackRenderer< R1.RenderResult == RR1, R2.RenderResult == RR2, R1.Error == E1, - R2.Error == E2 - { + R2.Error == E2 { self.primaryRenderer = AnyRenderer(primaryRenderer) self.fallbackRenderer = AnyRenderer(fallbackRenderer) } @@ -52,8 +51,7 @@ extension RendererType { /// Uses the given `RendererType` whenever `self` produces an error. public func fallback (_ fallbackRenderer: Other) -> FallbackRenderer - where Self.Data == Other.Data - { + where Self.Data == Other.Data { return FallbackRenderer(primaryRenderer: self, fallbackRenderer: fallbackRenderer) } } diff --git a/AsyncImageView/Renderers/ImageInflaterRenderer.swift b/AsyncImageView/Renderers/ImageInflaterRenderer.swift index a352c14..f904472 100644 --- a/AsyncImageView/Renderers/ImageInflaterRenderer.swift +++ b/AsyncImageView/Renderers/ImageInflaterRenderer.swift @@ -36,9 +36,7 @@ public final class ImageInflaterRenderer: RendererType { public func renderImageWithData(_ data: Data) -> SignalProducer { return self.renderer.renderImageWithData(data) - .map { [screenScale = self.screenScale, - opaque = self.opaque, - contentMode = self.contentMode] result in + .map { [screenScale = self.screenScale, opaque = self.opaque, contentMode = self.contentMode] result in let inflatedImage = result.image.inflate( withSize: data.size, scale: screenScale, @@ -55,10 +53,10 @@ public final class ImageInflaterRenderer: RendererType { public enum ImageInflaterRendererContentMode { case aspectFill case aspectFit - + // For backwards compatibility public static let defaultMode: ImageInflaterRendererContentMode = .aspectFill - + fileprivate func drawingRectForRendering(imageSize: CGSize, inSize canvasSize: CGSize) -> CGRect { switch self { case .aspectFill: @@ -94,9 +92,8 @@ extension UIImage { scale: CGFloat, opaque: Bool, contentMode: ImageInflaterRendererContentMode, - renderingBlock: (_ image: UIImage, _ context: CGContext, _ contextSize: CGSize, _ imageDrawing: () -> ()) -> ()) - -> UIImage - { + renderingBlock: (_ image: UIImage, _ context: CGContext, _ contextSize: CGSize, _ imageDrawing: () -> Void) -> Void) + -> UIImage { precondition(size.width > 0 && size.height > 0, "Invalid size: \(size.width)x\(size.height)") let colorSpace = CGColorSpaceCreateDeviceRGB() @@ -150,8 +147,8 @@ extension RendererType { public struct InflaterSizeCalculator { public static func drawingRectForRenderingWithAspectFill(imageSize: CGSize, inSize canvasSize: CGSize) -> CGRect { - if (imageSize == canvasSize || - abs(imageSize.aspectRatio - canvasSize.aspectRatio) < CGFloat.ulpOfOne) { + if imageSize == canvasSize || + abs(imageSize.aspectRatio - canvasSize.aspectRatio) < CGFloat.ulpOfOne { return CGRect(origin: .zero, size: canvasSize) } else { let destScale = max( @@ -168,24 +165,24 @@ public struct InflaterSizeCalculator { return CGRect(x: dWidth, y: dHeight, width: newWidth, height: newHeight) } } - + // TODO: write tests for this public static func drawingRectForRenderingWithAspectFit(imageSize: CGSize, inSize canvasSize: CGSize) -> CGRect { - if (imageSize == canvasSize || - abs(imageSize.aspectRatio - canvasSize.aspectRatio) < CGFloat.ulpOfOne) { + if imageSize == canvasSize || + abs(imageSize.aspectRatio - canvasSize.aspectRatio) < CGFloat.ulpOfOne { return CGRect(origin: .zero, size: canvasSize) } else { let destScale = min( canvasSize.width / imageSize.width, canvasSize.height / imageSize.height ) - + let newWidth = imageSize.width * destScale let newHeight = imageSize.height * destScale - + let dWidth = ((canvasSize.width - newWidth) / 2.0) let dHeight = ((canvasSize.height - newHeight) / 2.0) - + return CGRect(x: dWidth, y: dHeight, width: newWidth, height: newHeight) } } @@ -197,7 +194,7 @@ private extension CGSize { } } -private func *(lhs: CGSize, rhs: CGFloat) -> CGSize { +private func * (lhs: CGSize, rhs: CGFloat) -> CGSize { return CGSize( width: lhs.width * rhs, height: lhs.height * rhs diff --git a/AsyncImageView/Renderers/ImageProcessingRenderer.swift b/AsyncImageView/Renderers/ImageProcessingRenderer.swift index fa9dac9..c00b07e 100644 --- a/AsyncImageView/Renderers/ImageProcessingRenderer.swift +++ b/AsyncImageView/Renderers/ImageProcessingRenderer.swift @@ -12,7 +12,7 @@ import ReactiveSwift /// `RendererType` decorator that allows rendering a new image derived from the original one. public final class ImageProcessingRenderer: RendererType { - public typealias Block = (_ image: UIImage, _ context: CGContext, _ contextSize: CGSize, _ data: Renderer.Data, _ imageDrawingBlock: () -> ()) -> () + public typealias Block = (_ image: UIImage, _ context: CGContext, _ contextSize: CGSize, _ data: Renderer.Data, _ imageDrawingBlock: () -> Void) -> Void private let renderer: Renderer private let scale: CGFloat @@ -42,15 +42,10 @@ public final class ImageProcessingRenderer: RendererType return self.renderer.renderImageWithData(data) .observe(on: self.schedulerCreator()) .map { $0.image } - .map { [ - scale = self.scale, - opaque = self.opaque, - block = self.renderingBlock, - contentMode = self.contentMode - ] image in - image.processImageWithBitmapContext( - withSize: data.size, - scale: scale, + .map { [scale = self.scale, opaque = self.opaque, block = self.renderingBlock, contentMode = self.contentMode] image in + image.processImageWithBitmapContext( + withSize: data.size, + scale: scale, opaque: opaque, contentMode: contentMode, renderingBlock: { image, context, contextSize, imageDrawingBlock in diff --git a/AsyncImageView/Renderers/MulticastedRenderer.swift b/AsyncImageView/Renderers/MulticastedRenderer.swift index 7e6ffe0..5de27ff 100644 --- a/AsyncImageView/Renderers/MulticastedRenderer.swift +++ b/AsyncImageView/Renderers/MulticastedRenderer.swift @@ -21,10 +21,9 @@ public final class MulticastedRenderer< >: RendererType where Renderer.Data == Data, - Renderer.Error == Never -{ + Renderer.Error == Never { private let renderer: Renderer - private let cache: Atomic<[Data : ImageProperty]> + private let cache: Atomic<[Data: ImageProperty]> #if !os(watchOS) private let memoryWarningDisposable: Disposable @@ -75,7 +74,7 @@ public final class MulticastedRenderer< } #if !os(watchOS) - private static func clearCacheOnMemoryWarning(_ cache: Atomic<[Data : ImageProperty]>) -> Disposable { + private static func clearCacheOnMemoryWarning(_ cache: Atomic<[Data: ImageProperty]>) -> Disposable { return NotificationCenter.default .reactive.notifications(forName: UIApplication.didReceiveMemoryWarningNotification, object: nil) .observe(on: QueueScheduler()) diff --git a/AsyncImageView/Renderers/RemoteOrLocalImageRenderer.swift b/AsyncImageView/Renderers/RemoteOrLocalImageRenderer.swift index cd64a00..2dd2a3c 100644 --- a/AsyncImageView/Renderers/RemoteOrLocalImageRenderer.swift +++ b/AsyncImageView/Renderers/RemoteOrLocalImageRenderer.swift @@ -21,21 +21,21 @@ public enum RemoteOrLocalRenderData: RendererType { public typealias Data = RemoteOrLocalRenderData - + private let remoteRenderer: RemoteImageRenderer private let localRenderer: LocalImageRenderer - + public init(session: URLSession, scheduler: Scheduler = QueueScheduler()) { self.remoteRenderer = RemoteImageRenderer(session: session) self.localRenderer = LocalImageRenderer(scheduler: scheduler) } - + public func renderImageWithData(_ data: Data) -> SignalProducer { switch data { case let .remote(data): return self.remoteRenderer .renderImageWithData(data) - + case let .local(data): return self.localRenderer .renderImageWithData(data) @@ -51,16 +51,16 @@ extension RemoteOrLocalRenderData { case let .remote(data): hasher.combine(data) } } - + public static func == (lhs: RemoteOrLocalRenderData, rhs: RemoteOrLocalRenderData) -> Bool { switch (lhs, rhs) { case let (.local(lhs), .local(rhs)): return lhs == rhs case let (.remote(lhs), .remote(rhs)): return lhs == rhs - + default: return false } } - + public var size: CGSize { switch self { case let .local(data): return data.size diff --git a/AsyncImageView/Renderers/SimpleImageProcessingRenderer.swift b/AsyncImageView/Renderers/SimpleImageProcessingRenderer.swift index 6c1e1a7..ee26e19 100644 --- a/AsyncImageView/Renderers/SimpleImageProcessingRenderer.swift +++ b/AsyncImageView/Renderers/SimpleImageProcessingRenderer.swift @@ -27,7 +27,7 @@ public final class SimpleImageProcessingRenderer: Render ) { self.renderer = renderer self.renderingBlock = renderingBlock - + self.schedulerCreator = schedulerCreator } diff --git a/AsyncImageView/Renderers/ViewRenderer.swift b/AsyncImageView/Renderers/ViewRenderer.swift index e637d05..48a9e16 100644 --- a/AsyncImageView/Renderers/ViewRenderer.swift +++ b/AsyncImageView/Renderers/ViewRenderer.swift @@ -14,13 +14,13 @@ import ReactiveSwift #if !os(watchOS) /// `RendererType` which generates a `UIImage` from a UIView. -@available(iOS 10.0, tvOSApplicationExtension 10.0, *) +@available(iOS 10.0, tvOSApplicationExtension 10.0, *) public final class ViewRenderer: RendererType { public typealias Block = (_ data: Data) -> UIView - + private let format: UIGraphicsImageRendererFormat private let viewCreationBlock: 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. @@ -29,7 +29,7 @@ public final class ViewRenderer: RendererType { self.format.opaque = opaque self.viewCreationBlock = viewCreationBlock } - + public func renderImageWithData(_ data: Data) -> SignalProducer { return createProducer( data, @@ -39,7 +39,7 @@ public final class ViewRenderer: RendererType { size: data.size, format: self.format ) - + return renderer.image { context in draw(view: view, inContext: context.cgContext) } @@ -51,10 +51,10 @@ public final class ViewRenderer: RendererType { /// `RendererType` which generates a `UIImage` from a UIView. public final class OldViewRenderer: RendererType { public typealias Block = (_ data: Data) -> UIView - + private let opaque: Bool private let viewCreationBlock: 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. @@ -62,18 +62,18 @@ public final class OldViewRenderer: RendererType { self.opaque = opaque self.viewCreationBlock = viewCreationBlock } - + public func renderImageWithData(_ data: Data) -> SignalProducer { return createProducer( data, viewCreationBlock: self.viewCreationBlock, renderBlock: { view in - + UIGraphicsBeginImageContextWithOptions(data.size, self.opaque, 0) defer { UIGraphicsEndImageContext() } - + draw(view: view, inContext: UIGraphicsGetCurrentContext()!) - + return UIGraphicsGetImageFromCurrentImageContext()! } ) @@ -90,7 +90,7 @@ fileprivate func createProducer( view.frame.origin = .zero view.bounds.size = data.size view.layoutIfNeeded() - + // Make the CA renderer wait "until all the post-commit triggers fire". // We can't take a snapshot right away because the view has not been commited to the render server yet. DispatchQueue.main.async { diff --git a/AsyncImageViewTests/AsyncImageViewSpec.swift b/AsyncImageViewTests/AsyncImageViewSpec.swift index 2447163..2939445 100644 --- a/AsyncImageViewTests/AsyncImageViewSpec.swift +++ b/AsyncImageViewTests/AsyncImageViewSpec.swift @@ -22,11 +22,11 @@ class AsyncImageViewSpec: QuickSpec { // and we can verify image is reset before that let uiScheduler = QueueScheduler(targeting: DispatchQueue.main) var window: UIWindow! - + beforeEach { window = UIWindow() } - + context("No placeholder") { typealias ViewType = AsyncImageView @@ -45,14 +45,18 @@ class AsyncImageViewSpec: QuickSpec { window.addSubview(view) } - func verifyView(file: FileString = #file, - line: UInt = #line) { - verifyImage(view.image, - withSize: view.frame.size, - data: view.data, - file: file, - line: line) - } + func verifyView( + file: FileString = #file, + line: UInt = #line + ) { + verifyImage( + view.image, + withSize: view.frame.size, + data: view.data, + file: file, + line: line + ) + } it("has no image initially") { expect(view.image).to(beNil()) @@ -186,22 +190,31 @@ class AsyncImageViewSpec: QuickSpec { window.addSubview(view) } - func verifyRealImage(file: FileString = #file, - line: UInt = #line) { - verifyImage(view.image, - withSize: view.frame.size, - data: view.data!, - file: file, line: line) - } + func verifyRealImage( + file: FileString = #file, + line: UInt = #line + ) { + verifyImage( + view.image, + withSize: view.frame.size, + data: view.data!, + file: file, + line: line + ) + } - func verifyPlaceholder(file: FileString = #file, - line: UInt = #line) { - verifyImage(view.image, - withSize: view.frame.size, - expectedScale: view.data!.placeholderScale, - file: file, - line: line) - } + func verifyPlaceholder( + file: FileString = #file, + line: UInt = #line + ) { + verifyImage( + view.image, + withSize: view.frame.size, + expectedScale: view.data!.placeholderScale, + file: file, + line: line + ) + } it("has no image initially") { expect(view.image).to(beNil()) @@ -291,7 +304,7 @@ class AsyncImageViewSpec: QuickSpec { view.data = nil expect(view.image).to(beNil()) // image should be reset immediately } - + it("shows placeholder if renderer fails first") { view.frame.size = CGSize(width: 1, height: 1) @@ -304,11 +317,11 @@ class AsyncImageViewSpec: QuickSpec { view.data = data renderer.failAndComplete(renderData) - + placeholderRenderer.emitImageForData(renderData, scale: data.placeholderScale) verifyPlaceholder() } - + it("does not reset placeholder if renderer fails after") { view.frame.size = CGSize(width: 1, height: 1) @@ -332,12 +345,12 @@ class AsyncImageViewSpec: QuickSpec { } private final class ManualRenderer: RendererType { - var signals: [TestRenderData : (output: Signal, input: Signal.Observer)] = [:] + var signals: [TestRenderData: (output: Signal, input: Signal.Observer)] = [:] func addRenderSignal(_ data: TestRenderData) { signals[data] = Signal.pipe() } - + private func observer(forData data: TestRenderData) -> Signal.Observer { return signals[data]!.input } @@ -351,14 +364,14 @@ private final class ManualRenderer: RendererType { observer.send(value: image) observer.sendCompleted() } - + func failAndComplete(_ data: TestRenderData) { // Errors aren't allowed in AsyncImageView (they must be handled prior), // so they instead simply cause the signal to complete. self.observer(forData: data).sendCompleted() } - - func renderImageWithData(_ data: TestRenderData) -> SignalProducer { + + func renderImageWithData(_ data: TestRenderData) -> SignalProducer { guard let signal = signals[data]?.output else { XCTFail("Signal not created for \(data)") return .empty diff --git a/AsyncImageViewTests/ImageInflaterRendererSpec.swift b/AsyncImageViewTests/ImageInflaterRendererSpec.swift index 8a9f3f8..82b0acc 100644 --- a/AsyncImageViewTests/ImageInflaterRendererSpec.swift +++ b/AsyncImageViewTests/ImageInflaterRendererSpec.swift @@ -22,149 +22,149 @@ class ImageInflaterRendererSpec: QuickSpec { imageSize: size, inSize: size ) - + expect(result) == CGRect(origin: CGPoint.zero, size: size) } - + it("reduces size if aspect ratio matches, but canvas is smaller") { let imageSize = CGSize.random() let canvasSize = CGSize(width: imageSize.width * 0.4, height: imageSize.height * 0.4) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFit( imageSize: imageSize, inSize: canvasSize ) - + expect(result) == CGRect(origin: CGPoint.zero, size: canvasSize) } - + it("scales up size if aspect ratio matches, but canvas is bigger") { let imageSize = CGSize.random() let canvasSize = CGSize(width: imageSize.width * 2, height: imageSize.height * 2) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFit( imageSize: imageSize, inSize: canvasSize ) - + expect(result) == CGRect(origin: CGPoint.zero, size: canvasSize) } - + it("scales and centers image vertically if height matches, but canvas width is smaller") { let imageSize = CGSize(width: 1242, height: 240) let canvasSize = CGSize(width: 750, height: 240) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFit( imageSize: imageSize, inSize: canvasSize ) - + let expectedHeight = canvasSize.height * (canvasSize.width / imageSize.width) // preserve aspect ratio - + expect(result.origin) == CGPoint(x: 0, y: (expectedHeight - canvasSize.height) / -2.0) expect(result.size.width).to(beCloseTo(canvasSize.width)) expect(result.size.height).to(beCloseTo(expectedHeight)) } - + it("centers horizontally if height matches, but canvas width is bigger") { let imageSize = CGSize(width: 1242, height: 240) let canvasSize = CGSize(width: 1334, height: 240) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFit( imageSize: imageSize, inSize: canvasSize ) - + expect(result.origin) == CGPoint(x: (imageSize.width - canvasSize.width) / -2.0, y: 0) expect(result.size) == imageSize } - + it("scales and centers image horizontally if width matches, but canvas height is smaller") { let imageSize = CGSize(width: 1242, height: 240) let canvasSize = CGSize(width: 1242, height: 100) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFit( imageSize: imageSize, inSize: canvasSize ) - + let expectedWidth = canvasSize.width * (canvasSize.height / imageSize.height) // preserve aspect ratio - + expect(result.origin) == CGPoint(x: (expectedWidth - canvasSize.width) / -2.0, y: 0) expect(result.size.width).to(beCloseTo(expectedWidth)) expect(result.size.height).to(beCloseTo(canvasSize.height)) } - + it("centers vertically if width matches, but canvas height is bigger") { let imageSize = CGSize(width: 1242, height: 240) let canvasSize = CGSize(width: 1242, height: 300) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFit( imageSize: imageSize, inSize: canvasSize ) - + expect(result.origin) == CGPoint(x: 0, y: (imageSize.height - canvasSize.height) / -2.0) expect(result.size) == imageSize } - + context("aspect ratio and image size are different") { context("image size is smaller") { it("image aspect ratio is smaller") { let imageSize = CGSize(width: 30, height: 40) let canvasSize = CGSize(width: 50, height: 60) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFit( imageSize: imageSize, inSize: canvasSize ) - + expect(result.origin.x).to(beCloseTo(2.5)) expect(result.origin.y).to(beCloseTo(0)) expect(result.size.width).to(beCloseTo(45)) expect(result.size.height).to(beCloseTo(60)) } - + it("image aspect ratio is bigger") { let imageSize = CGSize(width: 50, height: 60) let canvasSize = CGSize(width: 60, height: 80) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFit( imageSize: imageSize, inSize: canvasSize ) - + expect(result.origin.x).to(beCloseTo(0)) expect(result.origin.y).to(beCloseTo(4)) expect(result.size.width).to(beCloseTo(60)) expect(result.size.height).to(beCloseTo(72)) } } - + context("image size is bigger") { it("image aspect ratio is smaller") { let imageSize = CGSize(width: 60, height: 80) let canvasSize = CGSize(width: 50, height: 60) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFit( imageSize: imageSize, inSize: canvasSize ) - + expect(result.origin.x).to(beCloseTo(2.5)) expect(result.origin.y).to(beCloseTo(0)) expect(result.size.width).to(beCloseTo(45)) expect(result.size.height).to(beCloseTo(60)) } - + it("image aspect ratio is bigger") { let imageSize = CGSize(width: 100, height: 120) let canvasSize = CGSize(width: 60, height: 80) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFit( imageSize: imageSize, inSize: canvasSize ) - + expect(result.origin.x).to(beCloseTo(0)) expect(result.origin.y).to(beCloseTo(4)) expect(result.size.width).to(beCloseTo(60)) @@ -173,7 +173,7 @@ class ImageInflaterRendererSpec: QuickSpec { } } } - + context("Aspect Fill") { it("returns identity frame if sizes match") { let size = CGSize.random() @@ -181,153 +181,153 @@ class ImageInflaterRendererSpec: QuickSpec { imageSize: size, inSize: size ) - + expect(result) == CGRect(origin: CGPoint.zero, size: size) } - + it("reduces size if aspect ratio matches, but canvas is smaller") { let imageSize = CGSize.random() let canvasSize = CGSize(width: imageSize.width * 0.4, height: imageSize.height * 0.4) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFill( imageSize: imageSize, inSize: canvasSize ) - + expect(result) == CGRect(origin: CGPoint.zero, size: canvasSize) } - + it("scales up size if aspect ratio matches, but canvas is bigger") { let imageSize = CGSize.random() let canvasSize = CGSize(width: imageSize.width * 2, height: imageSize.height * 2) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFill( imageSize: imageSize, inSize: canvasSize ) - + expect(result) == CGRect(origin: CGPoint.zero, size: canvasSize) } - + it("centers image horizontally if height matches, but canvas width is smaller") { let imageSize = CGSize(width: 1242, height: 240) let canvasSize = CGSize(width: 750, height: 240) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFill( imageSize: imageSize, inSize: canvasSize ) - + expect(result.origin) == CGPoint(x: (imageSize.width - canvasSize.width) / -2.0, y: 0) expect(result.size) == imageSize } - + it("scales image and centers horizontally if height matches, but canvas width is bigger") { let imageSize = CGSize(width: 1242, height: 240) let canvasSize = CGSize(width: 1334, height: 240) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFill( imageSize: imageSize, inSize: canvasSize ) - + let expectedHeight = canvasSize.height * (canvasSize.width / imageSize.width) // preserve aspect ratio - + expect(result.origin.x).to(beCloseTo(0)) expect(result.origin.y).to(beCloseTo((canvasSize.height - expectedHeight) / 2.0)) expect(result.size.width).to(beCloseTo(canvasSize.width)) expect(result.size.height).to(beCloseTo(expectedHeight)) } - + it("centers image vertically if width matches, but canvas height is smaller") { let imageSize = CGSize(width: 1242, height: 240) let canvasSize = CGSize(width: 1242, height: 100) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFill( imageSize: imageSize, inSize: canvasSize ) - + expect(result.origin) == CGPoint(x: 0, y: (imageSize.height - canvasSize.height) / -2.0) expect(result.size) == imageSize } - + it("scales image and centers vertically if width matches, but canvas height is bigger") { let imageSize = CGSize(width: 1242, height: 240) let canvasSize = CGSize(width: 1242, height: 300) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFill( imageSize: imageSize, inSize: canvasSize ) - + let expectedWidth = canvasSize.width * (canvasSize.height / imageSize.height) // preserve aspect ratio - + // TODO: write matcher for `CGRect`. expect(result.origin.x).to(beCloseTo((canvasSize.width - expectedWidth) / 2.0)) expect(result.origin.y).to(beCloseTo(0)) expect(result.size.width).to(beCloseTo(expectedWidth)) expect(result.size.height).to(beCloseTo(canvasSize.height)) } - + context("aspect ratio and image size are different") { context("image size is smaller") { it("image aspect ratio is smaller") { let imageSize = CGSize(width: 30, height: 40) let canvasSize = CGSize(width: 50, height: 60) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFill( imageSize: imageSize, inSize: canvasSize ) - + expect(result.origin.x).to(beCloseTo(0)) expect(result.origin.y).to(beCloseTo(-3.3333)) expect(result.size.width).to(beCloseTo(50)) expect(result.size.height).to(beCloseTo(66.6666)) } - + it("image aspect ratio is bigger") { let imageSize = CGSize(width: 50, height: 60) let canvasSize = CGSize(width: 60, height: 80) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFill( imageSize: imageSize, inSize: canvasSize ) - + expect(result.origin.x).to(beCloseTo(-3.3333)) expect(result.origin.y).to(beCloseTo(0)) expect(result.size.width).to(beCloseTo(66.6666)) expect(result.size.height).to(beCloseTo(80)) } } - + context("image size is bigger") { it("image aspect ratio is smaller") { let imageSize = CGSize(width: 60, height: 80) let canvasSize = CGSize(width: 50, height: 60) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFill( imageSize: imageSize, inSize: canvasSize ) - + expect(result.origin.x).to(beCloseTo(0)) expect(result.origin.y).to(beCloseTo(-3.3333)) expect(result.size.width).to(beCloseTo(50)) expect(result.size.height).to(beCloseTo(66.6666)) } - + it("image aspect ratio is bigger") { let imageSize = CGSize(width: 100, height: 120) let canvasSize = CGSize(width: 60, height: 80) - + let result = InflaterSizeCalculator.drawingRectForRenderingWithAspectFill( imageSize: imageSize, inSize: canvasSize ) - + expect(result.origin.x).to(beCloseTo(-3.3333)) expect(result.origin.y).to(beCloseTo(0)) expect(result.size.width).to(beCloseTo(66.6666)) diff --git a/AsyncImageViewTests/MulticastedRendererSpec.swift b/AsyncImageViewTests/MulticastedRendererSpec.swift index c54eca2..e8007ce 100644 --- a/AsyncImageViewTests/MulticastedRendererSpec.swift +++ b/AsyncImageViewTests/MulticastedRendererSpec.swift @@ -76,7 +76,6 @@ class MulticastedRendererSpec: QuickSpec { var cacheHitRenderer: CacheHitRenderer! - func getProducerForData(_ data: TestData, _ size: CGSize) -> SignalProducer { return renderer.renderImageWithData(data.renderDataWithSize(size)) } @@ -150,7 +149,7 @@ private final class CacheHitRenderer: RendererType { private let testRenderer = TestRenderer() - func renderImageWithData(_ data: TestRenderData) -> SignalProducer { + func renderImageWithData(_ data: TestRenderData) -> SignalProducer { return testRenderer.renderImageWithData(data) .map { return RenderResult( diff --git a/AsyncImageViewTests/TestRenderData.swift b/AsyncImageViewTests/TestRenderData.swift index e4b05b0..bca29d7 100644 --- a/AsyncImageViewTests/TestRenderData.swift +++ b/AsyncImageViewTests/TestRenderData.swift @@ -42,7 +42,7 @@ internal struct TestRenderData: RenderDataType { } } -internal func ==(lhs: TestRenderData, rhs: TestRenderData) -> Bool { +internal func == (lhs: TestRenderData, rhs: TestRenderData) -> Bool { return (lhs.data == rhs.data && lhs.size == rhs.size) } @@ -59,7 +59,7 @@ internal final class TestRenderer: RendererType { }) } - @available(iOS 10.0, tvOSApplicationExtension 10.0, *) + @available(iOS 10.0, tvOSApplicationExtension 10.0, *) static func rendererForSize(_ size: CGSize, scale: CGFloat) -> ContextRenderer { precondition(size.width > 0 && size.height > 0, "Should not attempt to render with invalid size: \(size)") diff --git a/Package.swift b/Package.swift index 3ca76f8..f8de81b 100644 --- a/Package.swift +++ b/Package.swift @@ -5,12 +5,12 @@ let package = Package( name: "AsyncImageView", platforms: [.iOS(.v13), .tvOS(.v13), .watchOS(.v9)], products: [ - .library(name: "AsyncImageView", targets: ["AsyncImageView"]), + .library(name: "AsyncImageView", targets: ["AsyncImageView"]) ], dependencies: [ .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", from: "7.1.0"), .package(url: "https://github.com/Quick/Quick.git", from: "7.4.0"), - .package(url: "https://github.com/Quick/Nimble.git", from: "12.2.0"), + .package(url: "https://github.com/Quick/Nimble.git", from: "12.2.0") ], targets: [ .target( @@ -23,10 +23,10 @@ let package = Package( dependencies: [ "AsyncImageView", "Quick", - "Nimble", + "Nimble" ], path: "AsyncImageViewTests" - ), + ) ], swiftLanguageVersions: [.v5] )