From 89bdf513949d1ed0b0707f4304cb44b7c2765b47 Mon Sep 17 00:00:00 2001 From: Jan Gorman Date: Sun, 6 May 2018 17:25:54 +0200 Subject: [PATCH] Spring Cleaning --- .gitignore | 69 +-- .travis.yml | 3 +- Agrume.podspec | 6 +- Agrume.xcodeproj/project.pbxproj | 42 +- .../xcshareddata/xcschemes/Agrume.xcscheme | 4 +- .../xcshareddata/IDEWorkspaceChecks.plist | 8 + Agrume/Agrume.swift | 484 +++++------------- Agrume/AgrumeCell.swift | 50 +- Agrume/AgrumeDataSource.swift | 18 + Agrume/AgrumeImage.swift | 27 + Agrume/Background.swift | 13 + Agrume/Foundation+Agrume.swift | 11 + Agrume/ImageDownloader.swift | 11 +- Agrume/UIKit+Agrume.swift | 73 +++ Agrume/UIViewExtensions.swift | 17 - AgrumeTests/AgrumeServiceLocatorTests.swift | 66 --- .../Agrume Example.xcodeproj/project.pbxproj | 18 +- ...ltipleImagesCollectionViewController.swift | 9 +- ...MultipleURLsCollectionViewController.swift | 6 +- ...geImageBackgroundColorViewController.swift | 10 +- .../SingleImageModalViewController.swift | 6 +- .../SingleImageViewController.swift | 11 +- .../SingleURLViewController.swift | 6 +- README.md | 48 +- 24 files changed, 442 insertions(+), 574 deletions(-) create mode 100644 Agrume.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Agrume/AgrumeDataSource.swift create mode 100644 Agrume/AgrumeImage.swift create mode 100644 Agrume/Background.swift create mode 100644 Agrume/Foundation+Agrume.swift create mode 100644 Agrume/UIKit+Agrume.swift delete mode 100644 Agrume/UIViewExtensions.swift delete mode 100644 AgrumeTests/AgrumeServiceLocatorTests.swift diff --git a/.gitignore b/.gitignore index 4a3aa2b..7c44851 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,20 @@ -### https://raw.github.com/github/gitignore/c70e357bfde8a842faca6574f1dfc6ad416dfc2a/Global/Xcode.gitignore +### https://raw.github.com/github/gitignore/80a96ba508a32d425af842bffd7e2fe8ef978c6e/Global/Xcode.gitignore +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) build/ +DerivedData/ +*.moved-aside *.pbxuser !default.pbxuser *.mode1v3 @@ -9,43 +23,32 @@ build/ !default.mode2v3 *.perspectivev3 !default.perspectivev3 -xcuserdata -*.xccheckout -*.moved-aside -DerivedData -*.xcuserstate - - -### https://raw.github.com/github/gitignore/c70e357bfde8a842faca6574f1dfc6ad416dfc2a/Global/JetBrains.gitignore -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm -## Directory-based project format -.idea/ -# if you remove the above rule, at least ignore user-specific stuff: -# .idea/workspace.xml -# .idea/tasks.xml -# and these sensitive or high-churn files: -# .idea/dataSources.ids -# .idea/dataSources.xml -# .idea/sqlDataSources.xml -# .idea/dynamic.xml +### https://raw.github.com/github/gitignore/18e28746b0862059dbee8694fd366a679cb812fb/Global/Xcode.gitignore -## File-based project format -*.ipr -*.iml -*.iws +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore -## Additional for IntelliJ -out/ +## User settings +xcuserdata/ -# generated by mpeltonen/sbt-idea plugin -.idea_modules/ - -# generated by JIRA plugin -atlassian-ide-plugin.xml +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout -# generated by Crashlytics plugin (for Android Studio and Intellij) -com_crashlytics_export_strings.xml +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 diff --git a/.travis.yml b/.travis.yml index 3a7f157..e34e1b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: objective-c -osx_image: xcode9 +osx_image: xcode9.3 before_script: - brew update && brew upgrade swiftlint script: - - bundle exec fastlane scan --project "Agrume.xcodeproj" - bundle exec danger \ No newline at end of file diff --git a/Agrume.podspec b/Agrume.podspec index cc3e53d..18f528d 100644 --- a/Agrume.podspec +++ b/Agrume.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "Agrume" - s.version = "4.0.4" + s.version = "5.0.0" s.summary = "An iOS image viewer written in Swift." s.description = <<-DESC @@ -13,9 +13,9 @@ Pod::Spec.new do |s| s.license = { :type => "MIT", :file => "LICENSE" } s.author = { "Jan Gorman" => "gorman.jan@gmail.com" } - s.social_media_url = "http://twitter.com/JanGorman" + s.social_media_url = "https://twitter.com/JanGorman" - s.platform = :ios, "8.0" + s.platform = :ios, "9.0" s.source = { :git => "https://github.com/JanGorman/Agrume.git", :tag => s.version} diff --git a/Agrume.xcodeproj/project.pbxproj b/Agrume.xcodeproj/project.pbxproj index 1d010fb..b32c1d0 100644 --- a/Agrume.xcodeproj/project.pbxproj +++ b/Agrume.xcodeproj/project.pbxproj @@ -8,13 +8,16 @@ /* Begin PBXBuildFile section */ 94318E541D32612D0096215A /* AgrumeServiceLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94318E531D32612D0096215A /* AgrumeServiceLocator.swift */; }; - 94318E561D3261640096215A /* AgrumeServiceLocatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94318E551D3261640096215A /* AgrumeServiceLocatorTests.swift */; }; + F2609E23209F047200E0E93D /* AgrumeDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2609E22209F047200E0E93D /* AgrumeDataSource.swift */; }; + F2609E26209F06F800E0E93D /* AgrumeImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2609E25209F06F800E0E93D /* AgrumeImage.swift */; }; + F2609E28209F2BC600E0E93D /* Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2609E27209F2BC600E0E93D /* Background.swift */; }; + F2609E2A209F2E0200E0E93D /* UIKit+Agrume.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2609E29209F2E0200E0E93D /* UIKit+Agrume.swift */; }; F2A51FF41B10E00700924912 /* Agrume.h in Headers */ = {isa = PBXBuildFile; fileRef = F2A51FF31B10E00700924912 /* Agrume.h */; settings = {ATTRIBUTES = (Public, ); }; }; F2A51FFA1B10E00700924912 /* Agrume.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F2A51FEE1B10E00700924912 /* Agrume.framework */; }; F2A5200B1B10E29B00924912 /* Agrume.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2A5200A1B10E29B00924912 /* Agrume.swift */; }; F2D9598C1B1A108800073772 /* AgrumeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D9598B1B1A108800073772 /* AgrumeCell.swift */; }; - F2DC79D41B17012300818A8C /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2DC79D31B17012300818A8C /* UIViewExtensions.swift */; }; F2DC79D61B170C4B00818A8C /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2DC79D51B170C4B00818A8C /* ImageDownloader.swift */; }; + F2EE29AE209F31B800F281A2 /* Foundation+Agrume.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2EE29AD209F31B800F281A2 /* Foundation+Agrume.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -29,7 +32,10 @@ /* Begin PBXFileReference section */ 94318E531D32612D0096215A /* AgrumeServiceLocator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgrumeServiceLocator.swift; sourceTree = ""; }; - 94318E551D3261640096215A /* AgrumeServiceLocatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgrumeServiceLocatorTests.swift; sourceTree = ""; }; + F2609E22209F047200E0E93D /* AgrumeDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgrumeDataSource.swift; sourceTree = ""; }; + F2609E25209F06F800E0E93D /* AgrumeImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgrumeImage.swift; sourceTree = ""; }; + F2609E27209F2BC600E0E93D /* Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Background.swift; sourceTree = ""; }; + F2609E29209F2E0200E0E93D /* UIKit+Agrume.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIKit+Agrume.swift"; sourceTree = ""; }; F2A51FEE1B10E00700924912 /* Agrume.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Agrume.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F2A51FF21B10E00700924912 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F2A51FF31B10E00700924912 /* Agrume.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Agrume.h; sourceTree = ""; }; @@ -37,8 +43,8 @@ F2A51FFF1B10E00700924912 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F2A5200A1B10E29B00924912 /* Agrume.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Agrume.swift; sourceTree = ""; }; F2D9598B1B1A108800073772 /* AgrumeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgrumeCell.swift; sourceTree = ""; }; - F2DC79D31B17012300818A8C /* UIViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = ""; }; F2DC79D51B170C4B00818A8C /* ImageDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = ""; }; + F2EE29AD209F31B800F281A2 /* Foundation+Agrume.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Foundation+Agrume.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -84,12 +90,16 @@ isa = PBXGroup; children = ( F2A51FF31B10E00700924912 /* Agrume.h */, - F2A51FF11B10E00700924912 /* Supporting Files */, F2A5200A1B10E29B00924912 /* Agrume.swift */, + F2D9598B1B1A108800073772 /* AgrumeCell.swift */, + F2609E22209F047200E0E93D /* AgrumeDataSource.swift */, + F2609E25209F06F800E0E93D /* AgrumeImage.swift */, 94318E531D32612D0096215A /* AgrumeServiceLocator.swift */, - F2DC79D31B17012300818A8C /* UIViewExtensions.swift */, F2DC79D51B170C4B00818A8C /* ImageDownloader.swift */, - F2D9598B1B1A108800073772 /* AgrumeCell.swift */, + F2A51FF11B10E00700924912 /* Supporting Files */, + F2609E27209F2BC600E0E93D /* Background.swift */, + F2609E29209F2E0200E0E93D /* UIKit+Agrume.swift */, + F2EE29AD209F31B800F281A2 /* Foundation+Agrume.swift */, ); path = Agrume; sourceTree = ""; @@ -105,7 +115,6 @@ F2A51FFD1B10E00700924912 /* AgrumeTests */ = { isa = PBXGroup; children = ( - 94318E551D3261640096215A /* AgrumeServiceLocatorTests.swift */, F2A51FFE1B10E00700924912 /* Supporting Files */, ); path = AgrumeTests; @@ -178,7 +187,7 @@ attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0900; + LastUpgradeCheck = 0930; ORGANIZATIONNAME = Schnaub; TargetAttributes = { F2A51FED1B10E00700924912 = { @@ -249,8 +258,12 @@ buildActionMask = 2147483647; files = ( F2D9598C1B1A108800073772 /* AgrumeCell.swift in Sources */, - F2DC79D41B17012300818A8C /* UIViewExtensions.swift in Sources */, + F2609E26209F06F800E0E93D /* AgrumeImage.swift in Sources */, + F2609E2A209F2E0200E0E93D /* UIKit+Agrume.swift in Sources */, + F2609E28209F2BC600E0E93D /* Background.swift in Sources */, + F2EE29AE209F31B800F281A2 /* Foundation+Agrume.swift in Sources */, F2DC79D61B170C4B00818A8C /* ImageDownloader.swift in Sources */, + F2609E23209F047200E0E93D /* AgrumeDataSource.swift in Sources */, F2A5200B1B10E29B00924912 /* Agrume.swift in Sources */, 94318E541D32612D0096215A /* AgrumeServiceLocator.swift in Sources */, ); @@ -260,7 +273,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 94318E561D3261640096215A /* AgrumeServiceLocatorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -287,12 +299,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -321,7 +335,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -344,12 +358,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -371,7 +387,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/Agrume.xcodeproj/xcshareddata/xcschemes/Agrume.xcscheme b/Agrume.xcodeproj/xcshareddata/xcschemes/Agrume.xcscheme index 62421ae..3d092ab 100644 --- a/Agrume.xcodeproj/xcshareddata/xcschemes/Agrume.xcscheme +++ b/Agrume.xcodeproj/xcshareddata/xcschemes/Agrume.xcscheme @@ -1,6 +1,6 @@ + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Agrume/Agrume.swift b/Agrume/Agrume.swift index 1dcc45b..8cb7122 100644 --- a/Agrume/Agrume.swift +++ b/Agrume/Agrume.swift @@ -4,32 +4,13 @@ import UIKit -public protocol AgrumeDataSource { - - /// The number of images contained in the data source - var numberOfImages: Int { get } - - /// Return the image for the passed in index - /// - /// - Parameter index: The index (collection view item) being displayed - /// - Parameter completion: The completion that returns the image to be shown at the index - func image(forIndex index: Int, completion: @escaping (UIImage?) -> Void) - -} - public final class Agrume: UIViewController { - fileprivate static let transitionAnimationDuration: TimeInterval = 0.3 - fileprivate static let initialScalingToExpandFrom: CGFloat = 0.6 - fileprivate static let maxScalingForExpandingOffscreen: CGFloat = 1.25 - fileprivate static let reuseIdentifier = "reuseIdentifier" - - fileprivate var images: [UIImage]! - fileprivate var imageUrls: [URL]! - private var startIndex: Int? - private let backgroundBlurStyle: UIBlurEffectStyle? - private let backgroundColor: UIColor? - fileprivate let dataSource: AgrumeDataSource? + private var images: [AgrumeImage]! + private let startIndex: Int + private let background: Background + + private weak var dataSource: AgrumeDataSource? public typealias DownloadCompletion = (_ image: UIImage?) -> Void @@ -52,151 +33,115 @@ public final class Agrume: UIViewController { /// Initialize with a single image /// /// - Parameter image: The image to present - /// - Parameter backgroundBlurStyle: The UIBlurEffectStyle to apply to the background when presenting - /// - Parameter backgroundColor: The background color when presenting - public convenience init(image: UIImage, backgroundBlurStyle: UIBlurEffectStyle? = nil, backgroundColor: UIColor? = nil) { - self.init(image: image, imageUrl: nil, backgroundBlurStyle: backgroundBlurStyle, backgroundColor: backgroundColor) + /// - Parameter background: The background configuration + public convenience init(image: UIImage, background: Background = .colored(.black)) { + self.init(images: [image], background: background) } /// Initialize with a single image url /// - /// - Parameter imageUrl: The image url to present - /// - Parameter backgroundBlurStyle: The UIBlurEffectStyle to apply to the background when presenting - /// - Parameter backgroundColor: The background color when presenting - public convenience init(imageUrl: URL, backgroundBlurStyle: UIBlurEffectStyle? = .dark, backgroundColor: UIColor? = nil) { - self.init(image: nil, imageUrl: imageUrl, backgroundBlurStyle: backgroundBlurStyle, backgroundColor: backgroundColor) + /// - Parameter url: The image url to present + /// - Parameter background: The background configuration + public convenience init(url: URL, background: Background = .colored(.black)) { + self.init(urls: [url], background: background) } /// Initialize with a data source /// /// - Parameter dataSource: The `AgrumeDataSource` to use /// - Parameter startIndex: The optional start index when showing multiple images - /// - Parameter backgroundBlurStyle: The UIBlurEffectStyle to apply to the background when presenting - /// - Parameter backgroundColor: The background color when presenting - public convenience init(dataSource: AgrumeDataSource, startIndex: Int? = nil, - backgroundBlurStyle: UIBlurEffectStyle? = .dark, backgroundColor: UIColor? = nil) { - self.init(image: nil, images: nil, dataSource: dataSource, startIndex: startIndex, - backgroundBlurStyle: backgroundBlurStyle, backgroundColor: backgroundColor) + /// - Parameter background: The background configuration + public convenience init(dataSource: AgrumeDataSource, startIndex: Int = 0, background: Background = .colored(.black)) { + self.init(dataSource: dataSource, startIndex: startIndex, background: background) } /// Initialize with an array of images /// /// - Parameter images: The images to present /// - Parameter startIndex: The optional start index when showing multiple images - /// - Parameter backgroundBlurStyle: The UIBlurEffectStyle to apply to the background when presenting - /// - Parameter backgroundColor: The background color when presenting - public convenience init(images: [UIImage], startIndex: Int? = nil, backgroundBlurStyle: UIBlurEffectStyle? = .dark, - backgroundColor: UIColor? = nil) { - self.init(image: nil, images: images, startIndex: startIndex, backgroundBlurStyle: backgroundBlurStyle, - backgroundColor: backgroundColor) + /// - Parameter background: The background configuration + public convenience init(images: [UIImage], startIndex: Int = 0, background: Background = .colored(.black)) { + self.init(images: images, urls: nil, startIndex: startIndex, background: background) } /// Initialize with an array of image urls /// - /// - Parameter imageUrls: The image urls to present + /// - Parameter urls: The image urls to present /// - Parameter startIndex: The optional start index when showing multiple images - /// - Parameter backgroundBlurStyle: The UIBlurEffectStyle to apply to the background when presenting - /// - Parameter backgroundColor: The background color when presenting - public convenience init(imageUrls: [URL], startIndex: Int? = nil, backgroundBlurStyle: UIBlurEffectStyle? = .dark, - backgroundColor: UIColor? = nil) { - self.init(image: nil, imageUrls: imageUrls, startIndex: startIndex, backgroundBlurStyle: backgroundBlurStyle, - backgroundColor: backgroundColor) - } - - private init(image: UIImage? = nil, imageUrl: URL? = nil, images: [UIImage]? = nil, - dataSource: AgrumeDataSource? = nil, imageUrls: [URL]? = nil, startIndex: Int? = nil, - backgroundBlurStyle: UIBlurEffectStyle? = nil, backgroundColor: UIColor? = nil) { - switch (backgroundBlurStyle, backgroundColor) { - case (let blur, .none): - self.backgroundBlurStyle = blur - self.backgroundColor = nil - case (.none, let color): - self.backgroundColor = color - self.backgroundBlurStyle = nil + /// - Parameter background: The background configuration + public convenience init(urls: [URL], startIndex: Int = 0, background: Background = .colored(.black)) { + self.init(images: nil, urls: urls, startIndex: startIndex, background: background) + } + + private init(images: [UIImage]? = nil, urls: [URL]? = nil, dataSource: AgrumeDataSource? = nil, startIndex: Int, + background: Background) { + switch (images, urls) { + case (let images?, nil): + self.images = images.map { AgrumeImage(image: $0) } + case (_, let urls?): + self.images = urls.map { AgrumeImage(url: $0) } default: - self.backgroundBlurStyle = .dark - self.backgroundColor = nil - } - - self.images = images - if let image = image { - self.images = [image] + assert(dataSource != nil, "No images or URLs passed. You must provide an AgrumeDataSource in that case.") } - self.imageUrls = imageUrls - if let imageURL = imageUrl { - self.imageUrls = [imageURL] - } - - self.dataSource = dataSource + self.startIndex = startIndex + self.background = background super.init(nibName: nil, bundle: nil) - UIDevice.current.beginGeneratingDeviceOrientationNotifications() - NotificationCenter.default.addObserver(self, selector: #selector(orientationDidChange), - name: .UIDeviceOrientationDidChange, object: nil) + self.dataSource = dataSource ?? self + + modalPresentationStyle = .custom + modalPresentationCapturesStatusBarAppearance = true } deinit { downloadTask?.cancel() - UIDevice.current.endGeneratingDeviceOrientationNotifications() - NotificationCenter.default.removeObserver(self) } required public init?(coder aDecoder: NSCoder) { fatalError("Not implemented") } - private func frameForCurrentDeviceOrientation() -> CGRect { - let bounds = view.bounds - if UIDeviceOrientationIsLandscape(currentDeviceOrientation()) { - if bounds.width / bounds.height > bounds.height / bounds.width { - return bounds - } else { - return CGRect(origin: bounds.origin, size: CGSize(width: bounds.height, height: bounds.width)) - } - } - return bounds - } - - private func currentDeviceOrientation() -> UIDeviceOrientation { - return UIDevice.current.orientation - } - private var backgroundSnapshot: UIImage! private var backgroundImageView: UIImageView! - fileprivate var _blurContainerView: UIView? - fileprivate var blurContainerView: UIView { + private var _blurContainerView: UIView? + private var blurContainerView: UIView { if _blurContainerView == nil { let view = UIView(frame: self.view.frame) view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - view.backgroundColor = backgroundColor ?? .clear + if case .colored(let color) = background { + view.backgroundColor = color + } else { + view.backgroundColor = .clear + } _blurContainerView = view } return _blurContainerView! } - fileprivate var _blurView: UIVisualEffectView? + private var _blurView: UIVisualEffectView? private var blurView: UIVisualEffectView { - if _blurView == nil { - let blurView = UIVisualEffectView(effect: UIBlurEffect(style: self.backgroundBlurStyle!)) - blurView.frame = self.view.frame - blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - _blurView = blurView + guard case .blurred(let style) = background, _blurView == nil else { + return _blurView! } + let blurView = UIVisualEffectView(effect: UIBlurEffect(style: style)) + blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + blurView.frame = view.frame + _blurView = blurView return _blurView! } - fileprivate var _collectionView: UICollectionView? - fileprivate var collectionView: UICollectionView { + private var _collectionView: UICollectionView? + private var collectionView: UICollectionView { if _collectionView == nil { let layout = UICollectionViewFlowLayout() layout.minimumInteritemSpacing = 0 layout.minimumLineSpacing = 0 layout.scrollDirection = .horizontal - layout.itemSize = self.view.frame.size + layout.itemSize = view.frame.size - let collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout) - collectionView.register(AgrumeCell.self, forCellWithReuseIdentifier: Agrume.reuseIdentifier) + let collectionView = UICollectionView(frame: view.frame, collectionViewLayout: layout) + collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + collectionView.register(AgrumeCell.self, forCellWithReuseIdentifier: String(describing: AgrumeCell.self)) collectionView.dataSource = self - collectionView.delegate = self collectionView.isPagingEnabled = true collectionView.backgroundColor = .clear collectionView.delaysContentTouches = false @@ -205,59 +150,52 @@ public final class Agrume: UIViewController { } return _collectionView! } - fileprivate var _spinner: UIActivityIndicatorView? - fileprivate var spinner: UIActivityIndicatorView { + private var _spinner: UIActivityIndicatorView? + private var spinner: UIActivityIndicatorView { if _spinner == nil { - let activityIndicatorStyle: UIActivityIndicatorViewStyle = self.backgroundBlurStyle == .dark ? .whiteLarge : .gray - let spinner = UIActivityIndicatorView(activityIndicatorStyle: activityIndicatorStyle) - spinner.center = self.view.center + let indicatorStyle: UIActivityIndicatorViewStyle + switch background { + case .blurred(let style): + indicatorStyle = style == .dark ? .whiteLarge : .gray + case .colored(let color): + indicatorStyle = color.isLight ? .gray : .whiteLarge + } + let spinner = UIActivityIndicatorView(activityIndicatorStyle: indicatorStyle) + spinner.center = view.center spinner.startAnimating() spinner.alpha = 0 _spinner = spinner } return _spinner! } - fileprivate var downloadTask: URLSessionDataTask? - - override public func viewDidLoad() { - super.viewDidLoad() - view.autoresizingMask = [.flexibleHeight, .flexibleWidth] - backgroundImageView = UIImageView(frame: view.frame) - backgroundImageView.image = backgroundSnapshot - view.addSubview(backgroundImageView) - } - - private var lastUsedOrientation: UIDeviceOrientation? - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - lastUsedOrientation = currentDeviceOrientation() - } - - fileprivate func deviceOrientationFromStatusBarOrientation() -> UIDeviceOrientation { - return UIDeviceOrientation(rawValue: UIApplication.shared.statusBarOrientation.rawValue)! - } - - fileprivate var initialOrientation: UIDeviceOrientation! + private var downloadTask: URLSessionDataTask? public func showFrom(_ viewController: UIViewController, backgroundSnapshotVC: UIViewController? = nil) { backgroundSnapshot = (backgroundSnapshotVC ?? viewControllerForSnapshot(fromViewController: viewController))?.view.snapshot() - view.frame = frameForCurrentDeviceOrientation() view.isUserInteractionEnabled = false addSubviews() - initialOrientation = deviceOrientationFromStatusBarOrientation() - updateLayoutsForCurrentOrientation() showFrom(viewController) } + override public func viewDidLoad() { + super.viewDidLoad() + addSubviews() + } + private func addSubviews() { - if backgroundBlurStyle != nil { + view.autoresizingMask = [.flexibleHeight, .flexibleWidth] + backgroundImageView = UIImageView(frame: view.frame) + backgroundImageView.image = backgroundSnapshot + view.addSubview(backgroundImageView) + + if case .blurred(_) = background { blurContainerView.addSubview(blurView) } view.addSubview(blurContainerView) view.addSubview(collectionView) - if let index = startIndex { - collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: [], animated: false) + if startIndex > 0 { + collectionView.scrollToItem(at: IndexPath(item: startIndex, section: 0), at: [], animated: false) } view.addSubview(spinner) } @@ -267,24 +205,24 @@ public final class Agrume: UIViewController { self.blurContainerView.alpha = 1 self.collectionView.alpha = 0 self.collectionView.frame = self.view.frame - let scaling = Agrume.initialScalingToExpandFrom + let scaling: CGFloat = .initialScalingToExpandFrom self.collectionView.transform = CGAffineTransform(scaleX: scaling, y: scaling) - + viewController.present(self, animated: false) { - UIView.animate(withDuration: Agrume.transitionAnimationDuration, + UIView.animate(withDuration: .transitionAnimationDuration, delay: 0, options: .beginFromCurrentState, - animations: { [weak self] in - self?.collectionView.alpha = 1 - self?.collectionView.transform = .identity - }, completion: { [weak self] _ in - self?.view.isUserInteractionEnabled = true + animations: { + self.collectionView.alpha = 1 + self.collectionView.transform = .identity + }, completion: { _ in + self.view.isUserInteractionEnabled = true }) } } } - fileprivate func viewControllerForSnapshot(fromViewController viewController: UIViewController) -> UIViewController? { + private func viewControllerForSnapshot(fromViewController viewController: UIViewController) -> UIViewController? { var presentingVC = viewController.view.window?.rootViewController while presentingVC?.presentedViewController != nil { presentingVC = presentingVC?.presentedViewController @@ -293,7 +231,7 @@ public final class Agrume: UIViewController { } public func dismiss() { - self.dismissAfterFlick() + dismissAfterFlick() } public func showImage(atIndex index : Int) { @@ -309,153 +247,56 @@ public final class Agrume: UIViewController { public override var prefersStatusBarHidden: Bool { return hideStatusBar } - - // MARK: Rotation - - @objc - private func orientationDidChange() { - let orientation = currentDeviceOrientation() - guard let lastOrientation = lastUsedOrientation else { return } - let landscapeToLandscape = UIDeviceOrientationIsLandscape(orientation) && UIDeviceOrientationIsLandscape(lastOrientation) - let portraitToPortrait = UIDeviceOrientationIsPortrait(orientation) && UIDeviceOrientationIsPortrait(lastOrientation) - guard (landscapeToLandscape || portraitToPortrait) && orientation != lastUsedOrientation else { return } - lastUsedOrientation = orientation - UIView.animate(withDuration: 0.6) { [weak self] in - self?.updateLayoutsForCurrentOrientation() - } - } - + public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - coordinator.animate(alongsideTransition: { [weak self] _ in - self?.updateLayoutsForCurrentOrientation() - }, completion: { [weak self] _ in - self?.lastUsedOrientation = self?.deviceOrientationFromStatusBarOrientation() - }) - } - - private func updateLayoutsForCurrentOrientation() { - let transform = newTransform() - - backgroundImageView.center = view.center - backgroundImageView.transform = transform.concatenating(CGAffineTransform(scaleX: 1, y: 1)) - - spinner.center = view.center - - collectionView.performBatchUpdates({ [unowned self] in - self.collectionView.collectionViewLayout.invalidateLayout() - self.collectionView.frame = self.view.frame - let width = self.collectionView.frame.width - let page = Int((self.collectionView.contentOffset.x + (0.5 * width)) / width) - let updatedOffset = CGFloat(page) * self.collectionView.frame.width - self.collectionView.contentOffset = CGPoint(x: updatedOffset, y: self.collectionView.contentOffset.y) + coordinator.animate(alongsideTransition: nil) { _ in + guard let layout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return } + layout.itemSize = size + layout.invalidateLayout() - let layout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout - layout?.itemSize = self.view.frame.size - }, completion: { _ in - for visibleCell in self.collectionView.visibleCells as! [AgrumeCell] { - visibleCell.updateScrollViewAndImageViewForCurrentMetrics() + self.collectionView.visibleCells.forEach { cell in + (cell as! AgrumeCell).recenterImage(size: size) } - }) - } - - private func newTransform() -> CGAffineTransform { - switch initialOrientation { - case .portrait: - return transformPortrait() - case .portraitUpsideDown: - return transformPortraitUpsideDown() - case .landscapeLeft: - return transformLandscapeLeft() - case .landscapeRight: - return transformLandscapeRight() - default: - return .identity } + super.viewWillTransition(to: size, with: coordinator) } - private func transformPortrait() -> CGAffineTransform { - switch currentDeviceOrientation() { - case .landscapeLeft: - return CGAffineTransform(rotationAngle: .pi / 2) - case .landscapeRight: - return CGAffineTransform(rotationAngle: -(.pi / 2)) - case .portraitUpsideDown: - return CGAffineTransform(rotationAngle: .pi) - default: - return .identity - } - } - - private func transformPortraitUpsideDown() -> CGAffineTransform { - switch currentDeviceOrientation() { - case .landscapeLeft: - return CGAffineTransform(rotationAngle: -(.pi / 2)) - case .landscapeRight: - return CGAffineTransform(rotationAngle: .pi / 2) - case .portrait: - return CGAffineTransform(rotationAngle: .pi) - default: - return .identity - } - } +} - private func transformLandscapeLeft() -> CGAffineTransform { - switch currentDeviceOrientation() { - case .landscapeRight: - return CGAffineTransform(rotationAngle: .pi) - case .portrait: - return CGAffineTransform(rotationAngle: -(.pi / 2)) - case .portraitUpsideDown: - return CGAffineTransform(rotationAngle: .pi / 2) - default: - return .identity - } +extension Agrume: AgrumeDataSource { + + public var numberOfImages: Int { + return images.count } - - private func transformLandscapeRight() -> CGAffineTransform { - switch currentDeviceOrientation() { - case .landscapeLeft: - return CGAffineTransform(rotationAngle: .pi) - case .portrait: - return CGAffineTransform(rotationAngle: .pi / 2) - case .portraitUpsideDown: - return CGAffineTransform(rotationAngle: -(.pi / 2)) - default: - return .identity + + public func image(forIndex index: Int, completion: @escaping (UIImage?) -> Void) { + if let handler = AgrumeServiceLocator.shared.downloadHandler, let url = images[index].url { + handler(url, completion) + } else if let url = images[index].url { + downloadTask = ImageDownloader.downloadImage(url, completion: completion) + } else { + completion(images[index].image) } } - + } extension Agrume: UICollectionViewDataSource { public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - if let dataSource = dataSource { - return dataSource.numberOfImages - } - if let images = images { - return !images.isEmpty ? images.count : imageUrls.count - } - return imageUrls.count + return dataSource?.numberOfImages ?? 0 } - public func collectionView(_ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Agrume.reuseIdentifier, - for: indexPath) as! AgrumeCell - if let images = images { - cell.image = images[indexPath.row] - } else if let dataSource = dataSource { - spinner.alpha = 1 - let index = indexPath.row - - dataSource.image(forIndex: index) { [weak self] image in - DispatchQueue.main.async { - cell.image = image - self?.spinner.alpha = 0 - } + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell: AgrumeCell = collectionView.dequeue(indexPath: indexPath) + + spinner.alpha = 1 + dataSource?.image(forIndex: indexPath.item) { [weak self] image in + DispatchQueue.main.async { + cell.image = image + self?.spinner.alpha = 0 } - } + } // Only allow panning if horizontal swiping fails. Horizontal swiping is only active for zoomed in images collectionView.panGestureRecognizer.require(toFail: cell.swipeGesture) cell.delegate = self @@ -464,65 +305,6 @@ extension Agrume: UICollectionViewDataSource { } -extension Agrume: UICollectionViewDelegate { - - public func collectionView(_ collectionView: UICollectionView, - willDisplay cell: UICollectionViewCell, - forItemAt indexPath: IndexPath) { - didScroll?(indexPath.row) - - if let imageUrls = imageUrls { - let completion: DownloadCompletion = { [weak self] image in - (cell as! AgrumeCell).image = image - self?.spinner.alpha = 0 - } - - if let download = download { - download(imageUrls[indexPath.row], completion) - } else if let download = AgrumeServiceLocator.shared.downloadHandler { - spinner.alpha = 1 - download(imageUrls[indexPath.row], completion) - } else { - spinner.alpha = 1 - downloadImage(imageUrls[indexPath.row], completion: completion) - } - } - - if let dataSource = dataSource { - let collectionViewCount = collectionView.numberOfItems(inSection: 0) - let dataSourceCount = dataSource.numberOfImages - - if isDataSourceCountUnchanged(dataSourceCount: dataSourceCount, collectionViewCount: collectionViewCount) { - return - } - - if isIndexPathOutOfBounds(indexPath, count: dataSourceCount) { - showImage(atIndex: dataSourceCount - 1) - } - reload() - } - } - - private func downloadImage(_ url: URL, completion: @escaping DownloadCompletion) { - downloadTask = ImageDownloader.downloadImage(url) { image in - completion(image) - } - } - - private func isDataSourceCountUnchanged(dataSourceCount: Int, collectionViewCount: Int) -> Bool { - return collectionViewCount == dataSourceCount - } - - private func isIndexPathOutOfBounds(_ indexPath: IndexPath, count: Int) -> Bool { - return indexPath.item >= count - } - - private func isLastElement(atIndexPath indexPath: IndexPath, count: Int) -> Bool { - return indexPath.item == count - } - -} - extension Agrume: AgrumeCellDelegate { private func dismissCompletion(_ finished: Bool) { @@ -546,10 +328,10 @@ extension Agrume: AgrumeCellDelegate { } func dismissAfterFlick() { - UIView.animate(withDuration: Agrume.transitionAnimationDuration, + UIView.animate(withDuration: .transitionAnimationDuration, delay: 0, options: .beginFromCurrentState, - animations: { [unowned self] in + animations: { self.collectionView.alpha = 0 self.blurContainerView.alpha = 0 }, completion: dismissCompletion) @@ -558,25 +340,19 @@ extension Agrume: AgrumeCellDelegate { func dismissAfterTap() { view.isUserInteractionEnabled = false - UIView.animate(withDuration: Agrume.transitionAnimationDuration, + UIView.animate(withDuration: .transitionAnimationDuration, delay: 0, options: .beginFromCurrentState, animations: { self.collectionView.alpha = 0 self.blurContainerView.alpha = 0 - let scaling = Agrume.maxScalingForExpandingOffscreen + let scaling: CGFloat = .maxScalingForExpandingOffscreen self.collectionView.transform = CGAffineTransform(scaleX: scaling, y: scaling) }, completion: dismissCompletion) } func isSingleImageMode() -> Bool { - if let images = images, !images.isEmpty { - return images.count == 1 - } - if let dataSource = dataSource { - return dataSource.numberOfImages == 1 - } - return imageUrls.count == 1 + return dataSource?.numberOfImages == 1 } } diff --git a/Agrume/AgrumeCell.swift b/Agrume/AgrumeCell.swift index 8939d26..e483fcb 100644 --- a/Agrume/AgrumeCell.swift +++ b/Agrume/AgrumeCell.swift @@ -4,7 +4,7 @@ import UIKit -protocol AgrumeCellDelegate: class { +protocol AgrumeCellDelegate: AnyObject { func dismissAfterFlick() func dismissAfterTap() @@ -14,12 +14,9 @@ protocol AgrumeCellDelegate: class { final class AgrumeCell: UICollectionViewCell { - private static let targetZoomForDoubleTap: CGFloat = 3 - private static let minFlickDismissalVelocity: CGFloat = 800 - private static let highScrollVelocity: CGFloat = 1600 - private lazy var scrollView: UIScrollView = { - let scrollView = UIScrollView(frame: self.contentView.bounds) + let scrollView = UIScrollView(frame: contentView.bounds) + scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight] scrollView.delegate = self scrollView.zoomScale = 1 scrollView.maximumZoomScale = 8 @@ -29,7 +26,7 @@ final class AgrumeCell: UICollectionViewCell { return scrollView }() private lazy var imageView: UIImageView = { - let imageView = UIImageView(frame: self.contentView.bounds) + let imageView = UIImageView(frame: contentView.bounds) imageView.contentMode = .scaleAspectFit imageView.isUserInteractionEnabled = true imageView.clipsToBounds = true @@ -49,7 +46,7 @@ final class AgrumeCell: UICollectionViewCell { override init(frame: CGRect) { super.init(frame: frame) - backgroundColor = UIColor.clear + backgroundColor = .clear contentView.addSubview(scrollView) scrollView.addSubview(imageView) setupGestureRecognizers() @@ -69,7 +66,7 @@ final class AgrumeCell: UICollectionViewCell { private lazy var singleTapGesture: UITapGestureRecognizer = { let singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(singleTap)) - singleTapGesture.require(toFail: self.doubleTapGesture) + singleTapGesture.require(toFail: doubleTapGesture) singleTapGesture.delegate = self return singleTapGesture }() @@ -140,11 +137,11 @@ extension AgrumeCell: UIGestureRecognizerDelegate { } @objc - func doubleTap(_ sender: UITapGestureRecognizer) { + private func doubleTap(_ sender: UITapGestureRecognizer) { let point = scrollView.convert(sender.location(in: sender.view), from: sender.view) if notZoomed() { - zoom(to: point, scale: AgrumeCell.targetZoomForDoubleTap) + zoom(to: point, scale: .targetZoomForDoubleTap) } else { zoom(to: .zero, scale: 1) } @@ -208,7 +205,7 @@ extension AgrumeCell: UIGestureRecognizerDelegate { } @objc - private func singleTap(_ gesture: UITapGestureRecognizer) { + private func singleTap() { dismiss() } @@ -246,7 +243,7 @@ extension AgrumeCell: UIGestureRecognizerDelegate { } } } else { - if vectorDistance > AgrumeCell.minFlickDismissalVelocity { + if vectorDistance > .minFlickDismissalVelocity { if isDraggingImage { dismissWithFlick(velocity) } else { @@ -297,36 +294,39 @@ extension AgrumeCell: UIGestureRecognizerDelegate { usingSpringWithDamping: 0.7, initialSpringVelocity: 0, options: [.allowUserInteraction, .beginFromCurrentState], - animations: { [unowned self] in + animations: { guard !self.isDraggingImage else { return } self.imageView.transform = CGAffineTransform.identity if !self.scrollView.isDragging && !self.scrollView.isDecelerating { - self.imageView.center = CGPoint(x: self.scrollView.contentSize.width / 2, - y: self.scrollView.contentSize.height / 2) + self.recenterImage(size: self.scrollView.contentSize) self.updateScrollViewAndImageViewForCurrentMetrics() } - }, completion: nil) + }) } } + + func recenterImage(size: CGSize) { + imageView.center = CGPoint(x: size.width / 2, y: size.height / 2) + } - func updateScrollViewAndImageViewForCurrentMetrics() { - scrollView.frame = contentView.bounds + private func updateScrollViewAndImageViewForCurrentMetrics() { + scrollView.frame = contentView.frame if let image = imageView.image { - imageView.frame = resizedFrameForSize(image.size) + imageView.frame = resizedFrame(forSize: image.size) } scrollView.contentSize = imageView.frame.size scrollView.contentInset = contentInsetForScrollView(atScale: scrollView.zoomScale) } - private func resizedFrameForSize(_ imageSize: CGSize) -> CGRect { - var frame = contentView.bounds + private func resizedFrame(forSize size: CGSize) -> CGRect { + var frame = contentView.frame let screenWidth = frame.width * scrollView.zoomScale let screenHeight = frame.height * scrollView.zoomScale var targetWidth = screenWidth var targetHeight = screenHeight - let nativeWidth = max(imageSize.width, screenWidth) - let nativeHeight = max(imageSize.height, screenHeight) + let nativeWidth = max(size.width, screenWidth) + let nativeHeight = max(size.height, screenHeight) if nativeHeight > nativeWidth { if screenHeight / screenWidth < nativeHeight / nativeWidth { @@ -409,7 +409,7 @@ extension AgrumeCell: UIScrollViewDelegate { } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - let highVelocity = AgrumeCell.highScrollVelocity + let highVelocity: CGFloat = .highScrollVelocity let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView.panGestureRecognizer.view) if notZoomed() && (fabs(velocity.x) > highVelocity || fabs(velocity.y) > highVelocity) { dismiss() diff --git a/Agrume/AgrumeDataSource.swift b/Agrume/AgrumeDataSource.swift new file mode 100644 index 0000000..ab6fb4e --- /dev/null +++ b/Agrume/AgrumeDataSource.swift @@ -0,0 +1,18 @@ +// +// Copyright © 2018 Schnaub. All rights reserved. +// + +import UIKit + +public protocol AgrumeDataSource: AnyObject { + + /// The number of images contained in the data source + var numberOfImages: Int { get } + + /// Return the image for the passed in index + /// + /// - Parameter index: The index (collection view item) being displayed + /// - Parameter completion: The completion that returns the image to be shown at the index + func image(forIndex index: Int, completion: @escaping (UIImage?) -> Void) + +} diff --git a/Agrume/AgrumeImage.swift b/Agrume/AgrumeImage.swift new file mode 100644 index 0000000..3a226fa --- /dev/null +++ b/Agrume/AgrumeImage.swift @@ -0,0 +1,27 @@ +// +// Copyright © 2018 Schnaub. All rights reserved. +// + +import UIKit + +public struct AgrumeImage: Equatable { + + public let image: UIImage? + public let url: URL? + public let title: NSAttributedString? + + private init(image: UIImage?, url: URL?, title: NSAttributedString?) { + self.image = image + self.url = url + self.title = title + } + + public init(image: UIImage, title: NSAttributedString? = nil) { + self.init(image: image, url: nil, title: title) + } + + public init(url: URL, title: NSAttributedString? = nil) { + self.init(image: nil, url: url, title: title) + } + +} diff --git a/Agrume/Background.swift b/Agrume/Background.swift new file mode 100644 index 0000000..84cdca1 --- /dev/null +++ b/Agrume/Background.swift @@ -0,0 +1,13 @@ +// +// Copyright © 2018 Schnaub. All rights reserved. +// + +import UIKit + +/// The background type +public enum Background { + /// Overlay with a color + case colored(UIColor) + /// Overlay with a UIBlurEffectStyle + case blurred(UIBlurEffectStyle) +} diff --git a/Agrume/Foundation+Agrume.swift b/Agrume/Foundation+Agrume.swift new file mode 100644 index 0000000..388a9c0 --- /dev/null +++ b/Agrume/Foundation+Agrume.swift @@ -0,0 +1,11 @@ +// +// Copyright © 2018 Schnaub. All rights reserved. +// + +import Foundation + +extension TimeInterval { + + static let transitionAnimationDuration: TimeInterval = 0.3 + +} diff --git a/Agrume/ImageDownloader.swift b/Agrume/ImageDownloader.swift index 647603b..789da87 100644 --- a/Agrume/ImageDownloader.swift +++ b/Agrume/ImageDownloader.swift @@ -7,7 +7,12 @@ import UIKit final class ImageDownloader { static func downloadImage(_ url: URL, completion: @escaping (_ image: UIImage?) -> Void) -> URLSessionDataTask? { - let dataTask = URLSession.shared.dataTask(with: url) { data, _, error in + var configuration = URLSessionConfiguration.default + if #available(iOS 11.0, *) { + configuration.waitsForConnectivity = true + } + let session = URLSession(configuration: configuration) + let task = session.dataTask(with: url) { data, _, error in var image: UIImage? defer { DispatchQueue.main.async { @@ -17,8 +22,8 @@ final class ImageDownloader { guard let data = data, error == nil else { return } image = UIImage(data: data) } - dataTask.resume() - return dataTask + task.resume() + return task } } diff --git a/Agrume/UIKit+Agrume.swift b/Agrume/UIKit+Agrume.swift new file mode 100644 index 0000000..0e43da1 --- /dev/null +++ b/Agrume/UIKit+Agrume.swift @@ -0,0 +1,73 @@ +// +// Copyright © 2018 Schnaub. All rights reserved. +// + +import UIKit + +extension CGFloat { + + static let initialScalingToExpandFrom: CGFloat = 0.6 + static let maxScalingForExpandingOffscreen: CGFloat = 1.25 + static let targetZoomForDoubleTap: CGFloat = 3 + static let minFlickDismissalVelocity: CGFloat = 800 + static let highScrollVelocity: CGFloat = 1600 + +} + +extension UIView { + + final func snapshot() -> UIImage { + UIGraphicsBeginImageContextWithOptions(bounds.size, true, 0) + drawHierarchy(in: bounds, afterScreenUpdates: true) + let snapshot = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return snapshot! + } + + func snapshotView() -> UIView? { + UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, 0) + defer { + UIGraphicsEndImageContext() + } + guard let context = UIGraphicsGetCurrentContext() else { + return nil + } + layer.render(in: context) + return UIImageView(image: UIGraphicsGetImageFromCurrentImageContext()) + } + + func translatedCenter(toContainerView containerView: UIView) -> CGPoint { + guard let superView = superview else { return .zero } + + var centerPoint = center + if let scrollView = superView as? UIScrollView, scrollView.zoomScale != 1 { + centerPoint.x += (scrollView.bounds.width - scrollView.contentSize.width) / 2 + scrollView.contentOffset.x + centerPoint.y += (scrollView.bounds.height - scrollView.contentSize.height) / 2 + scrollView.contentOffset.y + } + return superView.convert(centerPoint, to: containerView) + } + +} + +extension UIColor { + + var isLight: Bool { + var white: CGFloat = 0 + getWhite(&white, alpha: nil) + return white > 0.5 + } + +} + +extension UICollectionView { + + func dequeue(indexPath: IndexPath) -> T { + let id = String(describing: T.self) + return dequeue(id: id, indexPath: indexPath) + } + + func dequeue(id: String, indexPath: IndexPath) -> T { + return dequeueReusableCell(withReuseIdentifier: id, for: indexPath) as! T + } + +} diff --git a/Agrume/UIViewExtensions.swift b/Agrume/UIViewExtensions.swift deleted file mode 100644 index 4eed177..0000000 --- a/Agrume/UIViewExtensions.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright © 2016 Schnaub. All rights reserved. -// - -import UIKit - -extension UIView { - - final func snapshot() -> UIImage { - UIGraphicsBeginImageContextWithOptions(bounds.size, true, 0) - drawHierarchy(in: bounds, afterScreenUpdates: true) - let snapshot = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return snapshot! - } - -} diff --git a/AgrumeTests/AgrumeServiceLocatorTests.swift b/AgrumeTests/AgrumeServiceLocatorTests.swift deleted file mode 100644 index f4a24d8..0000000 --- a/AgrumeTests/AgrumeServiceLocatorTests.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// Copyright © 2016 Schnaub. All rights reserved. -// - -import XCTest -@testable import Agrume - -class AgrumeServiceLocatorTests: XCTestCase { - - fileprivate let mockViewController = UIViewController() - fileprivate var agrume: Agrume! - - override func setUp() { - super.setUp() - - agrume = Agrume(imageUrl: URL(string: "https://dl.dropboxusercontent.com/u/512759/MapleBacon.png")!) - } - - override func tearDown() { - AgrumeServiceLocator.shared.removeDownloadHandler() - agrume.download = nil - - super.tearDown() - } - - func testAgrumeUsesDownloadHandlerWhenSet() { - var callCount = 0 - AgrumeServiceLocator.shared.setDownloadHandler { _, _ in - callCount += 1 - } - - agrume.showFrom(mockViewController) - - XCTAssertEqual(1, callCount) - } - - func testAgrumeFallsBackToInternalWhenHandlerUnset() { - var callCount = 0 - AgrumeServiceLocator.shared.setDownloadHandler { _, _ in - callCount += 1 - } - - AgrumeServiceLocator.shared.removeDownloadHandler() - agrume.showFrom(mockViewController) - - XCTAssertEqual(0, callCount) - } - - func testAgrumePrefersClosureOverServiceLocator() { - var callCount = 0 - AgrumeServiceLocator.shared.setDownloadHandler { _, _ in - callCount += 1 - } - - var closureCallCount = 0 - agrume.download = { _, _ in - closureCallCount += 1 - } - - agrume.showFrom(mockViewController) - - XCTAssertEqual(0, callCount) - XCTAssertEqual(1, closureCallCount) - } - -} diff --git a/Example/Agrume Example.xcodeproj/project.pbxproj b/Example/Agrume Example.xcodeproj/project.pbxproj index e556c0f..63db497 100644 --- a/Example/Agrume Example.xcodeproj/project.pbxproj +++ b/Example/Agrume Example.xcodeproj/project.pbxproj @@ -117,18 +117,18 @@ isa = PBXGroup; children = ( F2A5201A1B130C7E00924912 /* AppDelegate.swift */, - F2A5201C1B130C7E00924912 /* ViewController.swift */, - F2A5201E1B130C7E00924912 /* Main.storyboard */, + F2D959941B1A15ED00073772 /* DemoCell.swift */, F2A520211B130C7E00924912 /* Images.xcassets */, F2A520231B130C7E00924912 /* LaunchScreen.xib */, - F2A520181B130C7E00924912 /* Supporting Files */, - F2D9598D1B1A133800073772 /* SingleImageViewController.swift */, + F2A5201E1B130C7E00924912 /* Main.storyboard */, + F2D959921B1A153F00073772 /* MultipleImagesCollectionViewController.swift */, + F2D959961B1A199F00073772 /* MultipleURLsCollectionViewController.swift */, 94D6B2111E1411B100927735 /* SingeImageBackgroundColorViewController.swift */, E77809E21D17821400CC60F1 /* SingleImageModalViewController.swift */, + F2D9598D1B1A133800073772 /* SingleImageViewController.swift */, F2D959901B1A140200073772 /* SingleURLViewController.swift */, - F2D959921B1A153F00073772 /* MultipleImagesCollectionViewController.swift */, - F2D959941B1A15ED00073772 /* DemoCell.swift */, - F2D959961B1A199F00073772 /* MultipleURLsCollectionViewController.swift */, + F2A520181B130C7E00924912 /* Supporting Files */, + F2A5201C1B130C7E00924912 /* ViewController.swift */, ); path = "Agrume Example"; sourceTree = ""; @@ -393,7 +393,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.3; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -439,7 +439,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.3; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/Example/Agrume Example/MultipleImagesCollectionViewController.swift b/Example/Agrume Example/MultipleImagesCollectionViewController.swift index ad97b32..7bd3c16 100644 --- a/Example/Agrume Example/MultipleImagesCollectionViewController.swift +++ b/Example/Agrume Example/MultipleImagesCollectionViewController.swift @@ -16,9 +16,8 @@ final class MultipleImagesCollectionViewController: UICollectionViewController { override func viewDidLoad() { super.viewDidLoad() - let layout = collectionView?.collectionViewLayout as! UICollectionViewFlowLayout - layout.itemSize = CGSize(width: view.bounds.width, height: view.bounds.height) + layout.itemSize = CGSize(width: view.frame.width, height: view.frame.height) } // MARK: UICollectionViewDataSource @@ -29,16 +28,16 @@ final class MultipleImagesCollectionViewController: UICollectionViewController { override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as! DemoCell - cell.imageView.image = images[indexPath.row] + cell.imageView.image = images[indexPath.item] return cell } // MARK: UICollectionViewDelegate override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let agrume = Agrume(images: images, startIndex: indexPath.row, backgroundBlurStyle: .light) + let agrume = Agrume(images: images, startIndex: indexPath.item, background: .blurred(.regular)) agrume.didScroll = { [unowned self] index in - self.collectionView?.scrollToItem(at: IndexPath(row: index, section: 0), at: [], animated: false) + self.collectionView?.scrollToItem(at: IndexPath(item: index, section: 0), at: [], animated: false) } agrume.showFrom(self) } diff --git a/Example/Agrume Example/MultipleURLsCollectionViewController.swift b/Example/Agrume Example/MultipleURLsCollectionViewController.swift index 6c3f8d9..0957e67 100644 --- a/Example/Agrume Example/MultipleURLsCollectionViewController.swift +++ b/Example/Agrume Example/MultipleURLsCollectionViewController.swift @@ -34,7 +34,7 @@ final class MultipleURLsCollectionViewController: UICollectionViewController { override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as! DemoCell - cell.imageView.image = images[indexPath.row].image + cell.imageView.image = images[indexPath.item].image return cell } @@ -42,9 +42,9 @@ final class MultipleURLsCollectionViewController: UICollectionViewController { override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let urls = images.map { $0.url } - let agrume = Agrume(imageUrls: urls, startIndex: indexPath.row, backgroundBlurStyle: .extraLight) + let agrume = Agrume(urls: urls, startIndex: indexPath.item, background: .blurred(.extraLight)) agrume.didScroll = { [unowned self] index in - self.collectionView?.scrollToItem(at: IndexPath(row: index, section: 0), at: [], animated: false) + self.collectionView?.scrollToItem(at: IndexPath(item: index, section: 0), at: [], animated: false) } agrume.showFrom(self) } diff --git a/Example/Agrume Example/SingeImageBackgroundColorViewController.swift b/Example/Agrume Example/SingeImageBackgroundColorViewController.swift index 3029e3c..e40294e 100644 --- a/Example/Agrume Example/SingeImageBackgroundColorViewController.swift +++ b/Example/Agrume Example/SingeImageBackgroundColorViewController.swift @@ -7,13 +7,11 @@ import Agrume final class SingleImageBackgroundColorViewController: UIViewController { - var agrume: Agrume! - - override func viewDidLoad() { - super.viewDidLoad() - agrume = Agrume(image: #imageLiteral(resourceName: "MapleBacon"), backgroundColor: .black) + private lazy var agrume: Agrume = { + let agrume = Agrume(image: #imageLiteral(resourceName: "MapleBacon"), background: .colored(.black)) agrume.hideStatusBar = true - } + return agrume + }() @IBAction private func openImage(_ sender: Any) { agrume.showFrom(self) diff --git a/Example/Agrume Example/SingleImageModalViewController.swift b/Example/Agrume Example/SingleImageModalViewController.swift index 4c5fef9..d416ee8 100644 --- a/Example/Agrume Example/SingleImageModalViewController.swift +++ b/Example/Agrume Example/SingleImageModalViewController.swift @@ -13,12 +13,12 @@ final class SingleImageModalViewController: UIViewController { navigationController?.navigationBar.barTintColor = .red } - @IBAction func openImage(_ sender: Any) { - let agrume = Agrume(image: #imageLiteral(resourceName: "MapleBacon")) + @IBAction private func openImage(_ sender: Any) { + let agrume = Agrume(image: #imageLiteral(resourceName: "MapleBacon"), background: .blurred(.regular)) agrume.showFrom(self) } - @IBAction func close(_ sender: Any) { + @IBAction private func close(_ sender: Any) { presentingViewController?.dismiss(animated: true, completion: nil) } diff --git a/Example/Agrume Example/SingleImageViewController.swift b/Example/Agrume Example/SingleImageViewController.swift index 142d875..b79006c 100644 --- a/Example/Agrume Example/SingleImageViewController.swift +++ b/Example/Agrume Example/SingleImageViewController.swift @@ -7,14 +7,11 @@ import Agrume final class SingleImageViewController: UIViewController { - var agrume: Agrume! - - override func viewDidLoad() { - super.viewDidLoad() - agrume = Agrume(image: #imageLiteral(resourceName: "MapleBacon")) - } + private lazy var agrume: Agrume = { + return Agrume(image: #imageLiteral(resourceName: "MapleBacon"), background: .blurred(.regular)) + }() - @IBAction func openImage(_ sender: Any) { + @IBAction private func openImage(_ sender: Any) { agrume.showFrom(self) } diff --git a/Example/Agrume Example/SingleURLViewController.swift b/Example/Agrume Example/SingleURLViewController.swift index e5ab31c..a8d6cb4 100644 --- a/Example/Agrume Example/SingleURLViewController.swift +++ b/Example/Agrume Example/SingleURLViewController.swift @@ -7,9 +7,9 @@ import Agrume final class SingleURLViewController: UIViewController { - @IBAction func openURL(_ sender: Any) { - let agrume = Agrume(imageUrl: URL(string: "https://www.dropbox.com/s/mlquw9k6ogvspox/MapleBacon.png?raw=1")!, - backgroundBlurStyle: .light) + @IBAction private func openURL(_ sender: Any) { + let agrume = Agrume(url: URL(string: "https://www.dropbox.com/s/mlquw9k6ogvspox/MapleBacon.png?raw=1")!, + background: .blurred(.regular)) agrume.showFrom(self) } diff --git a/README.md b/README.md index 12cd6fe..45fa569 100644 --- a/README.md +++ b/README.md @@ -12,19 +12,19 @@ An iOS image viewer written in Swift with support for multiple images. ## Requirements -- Swift 4 (for Swift 3 support, use version 3.x) -- iOS 8.0+ +- Swift 4.1 (for Swift 3 support, use version 3.x) +- iOS 9.0+ - Xcode 9+ ## Installation -The easiest way is through [CocoaPods](http://cocoapods.org). Simply add the dependency to your `Podfile` and then `pod install`: +The easiest way is via [CocoaPods](http://cocoapods.org). Add the dependency to your `Podfile` and then run `pod install`: ```ruby pod 'Agrume', :git => 'https://github.com/JanGorman/Agrume.git' ``` -Or [Carthage](https://github.com/Carthage/Carthage). Add the dependency to your `Cartfile` and then `carthage update`: +Or [Carthage](https://github.com/Carthage/Carthage). Add the dependency to your `Cartfile` and then run `carthage update`: ```ogdl github "JanGorman/Agrume" @@ -32,36 +32,38 @@ github "JanGorman/Agrume" ## How -There are multiple ways you can use the image viewer (and the included Example project shows them all). +There are multiple ways you can use the image viewer (and the included sample project shows them all). For just a single image it's as easy as ### Basic ```swift + import Agrume @IBAction func openImage(_ sender: Any) { - if let image = UIImage(named: "…") { - let agrume = Agrume(image: image) - agrume.showFrom(self) - } + guard let image = UIImage(named: "…") else { return } + + let agrume = Agrume(image: image) + // Present Agrume like any regular UIViewController + present(agrume, animated: true) } + ``` You can also pass in a `URL` and Agrume will take care of the download for you. -### Background Color +### Background Configuration -Agrume defaults to blurring the background view controller but you can also pass in a background color instead and it will use that: +Agrume has different background configurations. You can have it blur the view it's covering or supply a background color: ```swift -@IBAction func openImage(_ sender: Any) { - let image = UIImage(named: "…")! - let agrume = Agrume(image: Image, backgroundColor: .black) - agrume.hideStatusBar = true - agrume.showFrom(self) -} + +let agrume = Agrume(image: UIImage(named: "…")!, background: .blurred(.regular)) +// or +let agrume = Agrume(image: UIImage(named: "…")!, background: .colored(.green)) + ``` ### Multiple Images @@ -69,6 +71,7 @@ Agrume defaults to blurring the background view controller but you can also pass If you're displaying a `UICollectionView` and want to add support for zooming, you can also call Agrume with an array of either images or URLs. ```swift + let agrume = Agrume(images: images, startIndex: indexPath.row, backgroundBlurStyle: .light) agrume.didScroll = { [unowned self] index in self.collectionView?.scrollToItem(at: IndexPath(row: index, section: 0), @@ -76,6 +79,7 @@ agrume.didScroll = { [unowned self] index in animated: false) } agrume.showFrom(self) + ``` This shows a way of keeping the zoomed library and the one in the background synced. @@ -85,11 +89,12 @@ This shows a way of keeping the zoomed library and the one in the background syn If you want to take control of downloading images (e.g. for caching), you can also set a download closure that calls back to Agrume to set the image. For example, let's use [MapleBacon](https://github.com/JanGorman/MapleBacon). ```swift + import Agrume import MapleBacon @IBAction func openURL(_ sender: Any) { - let agrume = Agrume(imageUrl: URL(string: "https://dl.dropboxusercontent.com/u/512759/MapleBacon.png")!, backgroundBlurStyle: .light) + let agrume = Agrume(imageUrl: URL(string: "https://dl.dropboxusercontent.com/u/512759/MapleBacon.png")!) agrume.download = { url, completion in Downloader.default.download(url) { image in completion(image) @@ -104,10 +109,11 @@ import MapleBacon Instead of having to define a handler on a per instance basis you can instead set a handler on the `AgrumeServiceLocator`. Agrume will use this handler for all downloads unless overriden on an instance as described above: ```swift + import Agrume AgrumeServiceLocator.shared.setDownloadHandler { url, completion in - // Download data, cache it and remember to call the completion + // Download data, cache it and call the completion } // Some other place @@ -120,6 +126,7 @@ agrume.showFrom(self) For more dynamic library needs you can implement the `AgrumeDataSource` protocol that supplies images to Agrume. Agrume will query the data source for the number of images and if that number changes, reload it's scrolling image view. ```swift + import Agrume let dataSource: AgrumeDataSource = MyDataSourceImplementation() @@ -134,6 +141,7 @@ agrume.showFrom(self) When showing the Agrume view controller, it'll default to taking a snapshot of the root view and blurring that. You can customize this behaviour by passing in a different view that it will blur and display: ```swift + let agrume = Agrume(image: image) agrume.showFrom(self, backgroundSnapshotVC: self) @@ -144,9 +152,11 @@ agrume.showFrom(self, backgroundSnapshotVC: self) You can customize the status bar appearance when displaying the zoomed in view. `Agrume` has a `statusBarStyle` property: ```swift + let agrume = Agrume(image: image) agrume.statusBarStyle = .lightContent agrume.showFrom(self) + ``` ## Licence