diff --git a/Distrib/MoaDistrib.swift b/Distrib/MoaDistrib.swift index 5636024..f45ed7a 100644 --- a/Distrib/MoaDistrib.swift +++ b/Distrib/MoaDistrib.swift @@ -50,6 +50,11 @@ The class can be instantiated and used without an image view: public final class Moa { private var imageDownloader: MoaImageDownloader? private weak var imageView: MoaImageView? + /// Image download did start at this time. + private var imageDownloadStart: Date? + + /// If the request finishes before this time, changes are not animated (e.g. by fading). + private static let animateChangeLimit: TimeInterval = 0.1 /// Image download settings. public static var settings = MoaSettings() { @@ -117,6 +122,7 @@ public final class Moa { public func cancel() { imageDownloader?.cancel() imageDownloader = nil + imageDownloadStart = nil } /** @@ -192,19 +198,30 @@ public final class Moa { */ public static var errorImage: MoaImage? + #if os(iOS) || os(tvOS) + /// Set a value greater than zero to make Moa fade in the image over the given time after downloading it from the network. + public var imageFadeDuration: TimeInterval? + + /// Set a value greater than zero to make Moa fade in images over the given time after downloading them from the network. + public static var imageFadeDuration: TimeInterval? + #endif + private func startDownload(_ url: String) { cancel() let simulatedDownloader = MoaSimulator.createDownloader(url) imageDownloader = simulatedDownloader ?? MoaHttpImageDownloader(logger: Moa.logger) let simulated = simulatedDownloader != nil - + + imageDownloadStart = Date() + imageDownloader?.startDownload(url, onSuccess: { [weak self] image in self?.handleSuccessAsync(image, isSimulated: simulated) }, onError: { [weak self] error, response in self?.handleErrorAsync(error, response: response, isSimulated: simulated) + self?.imageDownloadStart = nil } ) } @@ -247,9 +264,47 @@ public final class Moa { if let onSuccess = onSuccess, let image = image { imageForView = onSuccess(image) } - + + #if os(iOS) || os(tvOS) + if + let imageView = self.imageView, + imageView.image != imageForView, + let imageFadeDuration = self.shouldFadeWithDuration() + { + UIView.transition( + with: imageView, + duration: imageFadeDuration, + options: .transitionCrossDissolve, + animations: { imageView.image = imageForView } + ) + } else { + imageView?.image = imageForView + } + #else imageView?.image = imageForView + #endif + + self.imageDownloadStart = nil + } + + #if os(iOS) || os(tvOS) + /// A fade duration that is greater than zero. Only provided when the download time surpasses animateChangeLimit. + private func shouldFadeWithDuration() -> TimeInterval? { + + let downloadTime: TimeInterval = imageDownloadStart.map { Date().timeIntervalSince($0) } ?? .zero + guard downloadTime > Moa.animateChangeLimit else { + return nil + } + + if let duration = self.imageFadeDuration, duration > 0 { + return duration + } + if let duration = Moa.imageFadeDuration, duration > 0 { + return duration + } + return nil } + #endif /** diff --git a/Moa/Moa.swift b/Moa/Moa.swift index d5eca4b..a0e378e 100644 --- a/Moa/Moa.swift +++ b/Moa/Moa.swift @@ -35,6 +35,11 @@ The class can be instantiated and used without an image view: public final class Moa { private var imageDownloader: MoaImageDownloader? private weak var imageView: MoaImageView? + /// Image download did start at this time. + private var imageDownloadStart: Date? + + /// If the request finishes before this time, changes are not animated (e.g. by fading). + private static let animateChangeLimit: TimeInterval = 0.1 /// Image download settings. public static var settings = MoaSettings() { @@ -102,6 +107,7 @@ public final class Moa { public func cancel() { imageDownloader?.cancel() imageDownloader = nil + imageDownloadStart = nil } /** @@ -177,19 +183,30 @@ public final class Moa { */ public static var errorImage: MoaImage? + #if os(iOS) || os(tvOS) + /// Set a value greater than zero to make Moa fade in the image over the given time after downloading it from the network. + public var imageFadeDuration: TimeInterval? + + /// Set a value greater than zero to make Moa fade in images over the given time after downloading them from the network. + public static var imageFadeDuration: TimeInterval? + #endif + private func startDownload(_ url: String) { cancel() let simulatedDownloader = MoaSimulator.createDownloader(url) imageDownloader = simulatedDownloader ?? MoaHttpImageDownloader(logger: Moa.logger) let simulated = simulatedDownloader != nil - + + imageDownloadStart = Date() + imageDownloader?.startDownload(url, onSuccess: { [weak self] image in self?.handleSuccessAsync(image, isSimulated: simulated) }, onError: { [weak self] error, response in self?.handleErrorAsync(error, response: response, isSimulated: simulated) + self?.imageDownloadStart = nil } ) } @@ -232,9 +249,47 @@ public final class Moa { if let onSuccess = onSuccess, let image = image { imageForView = onSuccess(image) } - + + #if os(iOS) || os(tvOS) + if + let imageView = self.imageView, + imageView.image != imageForView, + let imageFadeDuration = self.shouldFadeWithDuration() + { + UIView.transition( + with: imageView, + duration: imageFadeDuration, + options: .transitionCrossDissolve, + animations: { imageView.image = imageForView } + ) + } else { + imageView?.image = imageForView + } + #else imageView?.image = imageForView + #endif + + self.imageDownloadStart = nil + } + + #if os(iOS) || os(tvOS) + /// A fade duration that is greater than zero. Only provided when the download time surpasses animateChangeLimit. + private func shouldFadeWithDuration() -> TimeInterval? { + + let downloadTime: TimeInterval = imageDownloadStart.map { Date().timeIntervalSince($0) } ?? .zero + guard downloadTime > Moa.animateChangeLimit else { + return nil + } + + if let duration = self.imageFadeDuration, duration > 0 { + return duration + } + if let duration = Moa.imageFadeDuration, duration > 0 { + return duration + } + return nil } + #endif /** diff --git a/MoaTests/Utils/MoaTimeTests.swift b/MoaTests/Utils/MoaTimeTests.swift index eb63c89..5de1377 100644 --- a/MoaTests/Utils/MoaTimeTests.swift +++ b/MoaTests/Utils/MoaTimeTests.swift @@ -4,7 +4,8 @@ import XCTest class MoaTimeTests: XCTestCase { func testLogTimeDate() { - let calendar = Calendar.current + var calendar = Calendar.current + calendar.timeZone = TimeZone(secondsFromGMT: 0)! let components = NSDateComponents() components.year = 2027 components.month = 11 @@ -17,6 +18,6 @@ class MoaTimeTests: XCTestCase { let result = MoaTime.logTime(date) - XCTAssertEqual("2027-11-21 02:51:41.000", result) + XCTAssertEqual("2027-11-21 13:51:41.000", result) } } diff --git a/moa.podspec b/moa.podspec index 20ab65e..4be6bfc 100644 --- a/moa.podspec +++ b/moa.podspec @@ -1,8 +1,8 @@ Pod::Spec.new do |s| s.name = "moa" - s.version = "12.0.0" + s.version = "12.1.0" s.license = { :type => "MIT" } - s.homepage = "https://github.com/evgenyneu/moa" + s.homepage = "https://github.com/r-dent/moa" s.summary = "An image download extension for image view written in Swift." s.description = <<-DESC Moa is an image download library written in Swift for iOS, tvOS and macOS. @@ -16,8 +16,8 @@ Pod::Spec.new do |s| * Includes unit testing mode for faking network responses. DESC s.authors = { "Evgenii Neumerzhitckii" => "sausageskin@gmail.com" } - s.source = { :git => "https://github.com/evgenyneu/moa.git", :tag => s.version } - s.screenshots = "https://raw.githubusercontent.com/evgenyneu/moa/master/Graphics/Hunting_Moa.jpg" + s.source = { :git => "https://github.com/r-dent/moa.git", :tag => s.version } + s.screenshots = "https://raw.githubusercontent.com/r-dent/moa/master/Graphics/Hunting_Moa.jpg" s.source_files = "Moa/**/*.swift" s.ios.deployment_target = "8.0" s.osx.deployment_target = "10.9"