├── Cartfile
├── Cartfile.resolved
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ └── stale.yml
├── Gemfile
├── Example
├── Agrume Example
│ ├── animated.gif
│ ├── Images.xcassets
│ │ ├── Contents.json
│ │ ├── EvilBacon.imageset
│ │ │ ├── EvilBacon.png
│ │ │ └── Contents.json
│ │ ├── MapleBacon.imageset
│ │ │ ├── MapleBacon.png
│ │ │ └── Contents.json
│ │ ├── TextAndQR.imageset
│ │ │ ├── textAndQR.png
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── DemoCell.swift
│ ├── AppDelegate.swift
│ ├── CloseButtonViewController.swift
│ ├── AnimatedGifViewController.swift
│ ├── CustomCloseButtonViewController.swift
│ ├── URLUpdatedToImageViewController.swift
│ ├── LiveTextViewController.swift
│ ├── SingleImageViewController.swift
│ ├── SingleURLViewController.swift
│ ├── SingeImageBackgroundColorViewController.swift
│ ├── SwiftUIExampleViewController.swift
│ ├── SingleImageModalViewController.swift
│ ├── Info.plist
│ ├── MultipleImagesCollectionViewController.swift
│ ├── MultipleImagesCustomOverlayView.swift
│ ├── MultipleURLsCollectionViewController.swift
│ ├── OverlayView.swift
│ └── Base.lproj
│ │ └── LaunchScreen.xib
├── Agrume Example.xcodeproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── project.pbxproj
└── Agrume ExampleTests
│ └── Info.plist
├── .travis.yml
├── .gitmodules
├── Agrume.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcshareddata
│ └── xcschemes
│ │ └── Agrume.xcscheme
└── project.pbxproj
├── Agrume.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Agrume
├── Foundation+Agrume.swift
├── With.swift
├── Agrume.h
├── AgrumeDataSource.swift
├── AgrumeImage.swift
├── Info.plist
├── AgrumeServiceLocator.swift
├── ImageDownloader.swift
├── UIKit+Agrume.swift
├── AgrumeView.swift
├── Configuration.swift
├── AgrumePhotoLibraryHelper.swift
├── AgrumeOverlayView.swift
├── AgrumeCell.swift
└── Agrume.swift
├── Package.resolved
├── Dangerfile
├── Package.swift
├── AgrumeTests
└── Info.plist
├── Agrume.podspec
├── .gitignore
├── LICENSE
├── .swiftlint.yml
├── Gemfile.lock
└── README.md
/Cartfile:
--------------------------------------------------------------------------------
1 | github "kirualex/SwiftyGif"
--------------------------------------------------------------------------------
/Cartfile.resolved:
--------------------------------------------------------------------------------
1 | github "kirualex/SwiftyGif" "5.4.3"
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [JanGorman]
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "fastlane"
4 | gem "danger"
5 | gem "danger-swiftlint"
--------------------------------------------------------------------------------
/Example/Agrume Example/animated.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanGorman/Agrume/HEAD/Example/Agrume Example/animated.gif
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: objective-c
2 | osx_image: xcode11
3 | script:
4 | - git submodule init
5 | - bundle exec danger
6 |
--------------------------------------------------------------------------------
/Example/Agrume Example/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "Frameworks/SwiftyGif"]
2 | path = Frameworks/SwiftyGif
3 | url = https://github.com/kirualex/SwiftyGif
4 | shallow = true
5 |
--------------------------------------------------------------------------------
/Example/Agrume Example/Images.xcassets/EvilBacon.imageset/EvilBacon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanGorman/Agrume/HEAD/Example/Agrume Example/Images.xcassets/EvilBacon.imageset/EvilBacon.png
--------------------------------------------------------------------------------
/Example/Agrume Example/Images.xcassets/MapleBacon.imageset/MapleBacon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanGorman/Agrume/HEAD/Example/Agrume Example/Images.xcassets/MapleBacon.imageset/MapleBacon.png
--------------------------------------------------------------------------------
/Example/Agrume Example/Images.xcassets/TextAndQR.imageset/textAndQR.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanGorman/Agrume/HEAD/Example/Agrume Example/Images.xcassets/TextAndQR.imageset/textAndQR.png
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: bundler
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "04:00"
8 | open-pull-requests-limit: 10
9 |
--------------------------------------------------------------------------------
/Agrume.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Agrume.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Agrume/Foundation+Agrume.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2018 Schnaub. All rights reserved.
3 | //
4 |
5 | import Foundation
6 |
7 | extension TimeInterval {
8 |
9 | static let transitionAnimationDuration: TimeInterval = 0.3
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/Example/Agrume Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Agrume Example/DemoCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2016 Schnaub. All rights reserved.
3 | //
4 |
5 | import UIKit
6 |
7 | final class DemoCell: UICollectionViewCell {
8 |
9 | @IBOutlet private(set) var imageView: UIImageView!
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/Example/Agrume Example/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Agrume Example
4 | //
5 |
6 | import UIKit
7 |
8 | @UIApplicationMain
9 | class AppDelegate: UIResponder, UIApplicationDelegate {
10 |
11 | var window: UIWindow?
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/Agrume/With.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2019 Schnaub. All rights reserved.
3 | //
4 |
5 | import Foundation
6 |
7 | public func with(_ value: T, _ modifier: (inout T) -> Void) -> T {
8 | var value = value
9 | modifier(&value)
10 | return value
11 | }
12 |
--------------------------------------------------------------------------------
/Agrume.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Agrume.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "SwiftyGif",
6 | "repositoryURL": "https://github.com/kirualex/SwiftyGif",
7 | "state": {
8 | "branch": null,
9 | "revision": "d6d26061d6553a493781ad3df4a8e275c43fc373",
10 | "version": "5.4.4"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Example/Agrume Example/Images.xcassets/EvilBacon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images": [
3 | {
4 | "idiom": "universal",
5 | "scale": "1x",
6 | "filename": "EvilBacon.png"
7 | },
8 | {
9 | "idiom": "universal",
10 | "scale": "2x"
11 | },
12 | {
13 | "idiom": "universal",
14 | "scale": "3x"
15 | }
16 | ],
17 | "info": {
18 | "version": 1,
19 | "author": "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Agrume/Agrume.h:
--------------------------------------------------------------------------------
1 | //
2 | // Agrume.h
3 | // Agrume
4 | //
5 |
6 | #import
7 |
8 | //! Project version number for Agrume.
9 | FOUNDATION_EXPORT double AgrumeVersionNumber;
10 |
11 | //! Project version string for Agrume.
12 | FOUNDATION_EXPORT const unsigned char AgrumeVersionString[];
13 |
14 | // In this header, you should import all the public headers of your framework using statements like #import
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Example/Agrume Example/Images.xcassets/MapleBacon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images": [
3 | {
4 | "idiom": "universal",
5 | "scale": "1x",
6 | "filename": "MapleBacon.png"
7 | },
8 | {
9 | "idiom": "universal",
10 | "scale": "2x"
11 | },
12 | {
13 | "idiom": "universal",
14 | "scale": "3x"
15 | }
16 | ],
17 | "info": {
18 | "version": 1,
19 | "author": "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Example/Agrume Example/Images.xcassets/TextAndQR.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "textAndQR.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Example/Agrume Example/CloseButtonViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2018 Schnaub. All rights reserved.
3 | //
4 |
5 | import Agrume
6 | import UIKit
7 |
8 | final class CloseButtonViewController: UIViewController {
9 |
10 | private lazy var agrume = Agrume(image: UIImage(named: "MapleBacon")!, background: .blurred(.regular), dismissal: .withButton(nil))
11 |
12 | @IBAction private func showImage() {
13 | agrume.show(from: self)
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/Dangerfile:
--------------------------------------------------------------------------------
1 | # Sometimes it's a README fix, or something like that - which isn't relevant for
2 | # including in a project's CHANGELOG for example
3 | declared_trivial = github.pr_title.include? "#trivial"
4 |
5 | # Make it more obvious that a PR is a work in progress and shouldn't be merged yet
6 | warn("PR is classed as Work in Progress") if github.pr_title.include? "[WIP]"
7 |
8 | # Warn when there is a big PR
9 | warn("Big PR") if git.lines_of_code > 500
10 |
11 | swiftlint.lint_files
--------------------------------------------------------------------------------
/Example/Agrume Example/AnimatedGifViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2018 Schnaub. All rights reserved.
3 | //
4 |
5 | import Agrume
6 | import SwiftyGif
7 | import UIKit
8 |
9 | final class AnimatedGifViewController: UIViewController {
10 |
11 | @IBAction private func openImage(_ sender: Any?) {
12 | let image = try! UIImage(gifName: "animated.gif")
13 | let agrume = Agrume(image: image, background: .blurred(.regular))
14 | agrume.show(from: self)
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/Agrume/AgrumeDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2018 Schnaub. All rights reserved.
3 | //
4 |
5 | import UIKit
6 |
7 | public protocol AgrumeDataSource: AnyObject {
8 |
9 | /// The number of images contained in the data source
10 | var numberOfImages: Int { get }
11 |
12 | /// Return the image for the passed in index
13 | ///
14 | /// - Parameter index: The index (collection view item) being displayed
15 | /// - Parameter completion: The completion that returns the image to be shown at the index
16 | func image(forIndex index: Int, completion: @escaping (UIImage?) -> Void)
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Agrume",
6 | platforms: [
7 | .iOS(.v13),
8 | ],
9 | products: [
10 | .library(
11 | name: "Agrume",
12 | targets: ["Agrume"]
13 | ),
14 | ],
15 | dependencies: [
16 | .package(url: "https://github.com/kirualex/SwiftyGif", .upToNextMajor(from: "5.4.0"))
17 | ],
18 | targets: [
19 | .target(
20 | name: "Agrume",
21 | dependencies: ["SwiftyGif"],
22 | path: "./Agrume"
23 | )
24 | ]
25 | )
26 |
--------------------------------------------------------------------------------
/Example/Agrume Example/CustomCloseButtonViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2018 Schnaub. All rights reserved.
3 | //
4 |
5 | import Agrume
6 | import UIKit
7 |
8 | final class CustomCloseButtonViewController: UIViewController {
9 |
10 | private lazy var agrume: Agrume = {
11 | let button = UIBarButtonItem(barButtonSystemItem: .stop, target: nil, action: nil)
12 | button.tintColor = .red
13 | return Agrume(image: UIImage(named: "MapleBacon")!, background: .blurred(.regular), dismissal: .withButton(button))
14 | }()
15 |
16 | @IBAction private func showImage() {
17 | agrume.show(from: self)
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Agrume/AgrumeImage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2018 Schnaub. All rights reserved.
3 | //
4 |
5 | import UIKit
6 |
7 | public struct AgrumeImage: Equatable {
8 |
9 | public var image: UIImage?
10 | public var url: URL?
11 | public var title: NSAttributedString?
12 |
13 | private init(image: UIImage?, url: URL?, title: NSAttributedString?) {
14 | self.image = image
15 | self.url = url
16 | self.title = title
17 | }
18 |
19 | public init(image: UIImage, title: NSAttributedString? = nil) {
20 | self.init(image: image, url: nil, title: title)
21 | }
22 |
23 | public init(url: URL, title: NSAttributedString? = nil) {
24 | self.init(image: nil, url: url, title: title)
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Example/Agrume Example/URLUpdatedToImageViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLUpdatedToImageViewController.swift
3 | // Agrume Example
4 | //
5 | // Created by Bao Lei on 9/13/21.
6 | // Copyright © 2021 Schnaub. All rights reserved.
7 | //
8 |
9 | import Agrume
10 | import UIKit
11 |
12 | final class URLUpdatedToImageViewController: UIViewController {
13 |
14 | @IBAction private func openURL(_ sender: Any) {
15 | let agrume = Agrume(
16 | url: URL(string: "https://placekitten.com/500/500")!,
17 | background: .blurred(.regular)
18 | )
19 | agrume.show(from: self)
20 |
21 | DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
22 | agrume.updateImage(at: 0, with: URL(string: "https://placekitten.com/2500/2500")!)
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: Mark stale issues and pull requests
2 |
3 | on:
4 | schedule:
5 | - cron: "0 0 * * *"
6 |
7 | jobs:
8 | stale:
9 |
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/stale@v1
14 | with:
15 | repo-token: ${{ secrets.GITHUB_TOKEN }}
16 | days-before-close: 5
17 | days-before-stale: 30
18 | stale-issue-label: 'stale'
19 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you.'
20 | stale-pr-label: 'stale'
21 | stale-pr-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you.'
--------------------------------------------------------------------------------
/AgrumeTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Example/Agrume ExampleTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Agrume/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Agrume.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 |
3 | s.name = "Agrume"
4 | s.version = "5.8.10"
5 | s.summary = "An iOS image viewer written in Swift."
6 | s.swift_version = "5.0"
7 |
8 | s.description = <<-DESC
9 | An iOS image viewer written in Swift with support for multiple images.
10 | DESC
11 |
12 | s.homepage = "https://github.com/JanGorman/Agrume"
13 |
14 | s.license = { :type => "MIT", :file => "LICENSE" }
15 |
16 | s.author = { "Jan Gorman" => "gorman.jan@gmail.com" }
17 | s.social_media_url = "https://twitter.com/JanGorman"
18 |
19 | s.platform = :ios, "13.0"
20 |
21 | s.source = { :git => "https://github.com/JanGorman/Agrume.git", :tag => s.version}
22 |
23 | s.source_files = "Classes", "Agrume/*.swift"
24 |
25 | s.dependency "SwiftyGif"
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/Agrume/AgrumeServiceLocator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2016 Schnaub. All rights reserved.
3 | //
4 |
5 | import Foundation
6 |
7 | public class AgrumeServiceLocator {
8 |
9 | public static let shared = AgrumeServiceLocator()
10 |
11 | public typealias DownloadHandler = ((_ url: URL, _ completion: @escaping Agrume.DownloadCompletion) -> Void)
12 |
13 | var downloadHandler: DownloadHandler?
14 |
15 | /// Register a download handler with the service locator.
16 | /// Agrume will use this handler for all downloads. This can be overriden on a per call basis
17 | /// by passing in a different handler for said call.
18 | ///
19 | /// – Parameter handler: The download handler
20 | public func setDownloadHandler(_ handler: @escaping DownloadHandler) {
21 | downloadHandler = handler
22 | }
23 |
24 | /// Remove the global handler.
25 | public func removeDownloadHandler() {
26 | downloadHandler = nil
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Example/Agrume Example/LiveTextViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LiveTextViewController.swift
3 | //
4 |
5 | import Agrume
6 | import UIKit
7 | import VisionKit
8 |
9 | final class LiveTextViewController: UIViewController {
10 | @IBAction private func openImage(_ sender: Any) {
11 | if #available(iOS 16, *), ImageAnalyzer.isSupported {
12 | let agrume = Agrume(
13 | image: UIImage(named: "TextAndQR")!,
14 | enableLiveText: true
15 | )
16 | agrume.show(from: self)
17 | return
18 | }
19 |
20 | let alert = UIAlertController(
21 | title: "Not supported on this device",
22 | message: """
23 | Live Text is available for devices with iOS 16 (or above) and A12 (or above)
24 | Bionic chip (iPhone XS and later, physical device only)
25 | """,
26 | preferredStyle: .alert
27 | )
28 | alert.addAction(UIAlertAction(title: "OK", style: .cancel))
29 | present(alert, animated: true)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Example/Agrume Example/SingleImageViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2016 Schnaub. All rights reserved.
3 | //
4 |
5 | import Agrume
6 | import UIKit
7 |
8 | final class SingleImageViewController: UIViewController {
9 |
10 | private lazy var agrume = Agrume(image: UIImage(named: "MapleBacon")!, background: .blurred(.regular))
11 |
12 | @IBAction private func openImage(_ sender: Any) {
13 | let helper = makeHelper()
14 | agrume.onLongPress = helper.makeSaveToLibraryLongPressGesture
15 | agrume.show(from: self)
16 | }
17 |
18 | private func makeHelper() -> AgrumePhotoLibraryHelper {
19 | let saveButtonTitle = NSLocalizedString("Save Photo", comment: "Save Photo")
20 | let cancelButtonTitle = NSLocalizedString("Cancel", comment: "Cancel")
21 | let helper = AgrumePhotoLibraryHelper(saveButtonTitle: saveButtonTitle, cancelButtonTitle: cancelButtonTitle) { error in
22 | guard error == nil else {
23 | print("Could not save your photo")
24 | return
25 | }
26 | print("Photo has been saved to your library")
27 | }
28 | return helper
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Example/Agrume Example/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "size" : "1024x1024",
46 | "scale" : "1x"
47 | }
48 | ],
49 | "info" : {
50 | "version" : 1,
51 | "author" : "xcode"
52 | }
53 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### https://raw.github.com/github/gitignore/18e28746b0862059dbee8694fd366a679cb812fb/Global/Xcode.gitignore
2 |
3 | # Xcode
4 | #
5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
6 |
7 | ## User settings
8 | xcuserdata/
9 |
10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
11 | *.xcscmblueprint
12 | *.xccheckout
13 |
14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
15 | build/
16 | DerivedData/
17 | *.moved-aside
18 | *.pbxuser
19 | !default.pbxuser
20 | *.mode1v3
21 | !default.mode1v3
22 | *.mode2v3
23 | !default.mode2v3
24 | *.perspectivev3
25 | !default.perspectivev3
26 |
27 | ### Carthage
28 | Carthage/Checkouts/
29 | Carthage/Build/
30 |
31 | # Created by https://www.gitignore.io/api/mac
32 |
33 | .DS_Store
34 | # Created by https://www.gitignore.io/api/SwiftPM
35 | # Edit at https://www.gitignore.io/?templates=SwiftPM
36 |
37 | ### SwiftPM ###
38 | Packages
39 | .build/
40 | xcuserdata
41 | DerivedData/
42 | *.xcodeproj
43 |
44 |
45 | # End of https://www.gitignore.io/api/SwiftPM
46 |
47 | .swiftpm/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Jan Gorman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Example/Agrume Example/SingleURLViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2016 Schnaub. All rights reserved.
3 | //
4 |
5 | import Agrume
6 | import UIKit
7 |
8 | final class SingleURLViewController: UIViewController {
9 |
10 | @IBAction private func openURL(_ sender: Any) {
11 | let agrume = Agrume(
12 | url: URL(string: "https://www.dropbox.com/s/mlquw9k6ogvspox/MapleBacon.png?raw=1")!,
13 | background: .blurred(.regular)
14 | )
15 | let helper = makeHelper()
16 | agrume.onLongPress = helper.makeSaveToLibraryLongPressGesture
17 | agrume.show(from: self)
18 | }
19 |
20 | private func makeHelper() -> AgrumePhotoLibraryHelper {
21 | let saveButtonTitle = NSLocalizedString("Save Photo", comment: "Save Photo")
22 | let cancelButtonTitle = NSLocalizedString("Cancel", comment: "Cancel")
23 | let helper = AgrumePhotoLibraryHelper(saveButtonTitle: saveButtonTitle, cancelButtonTitle: cancelButtonTitle) { error in
24 | guard error == nil else {
25 | print("Could not save your photo")
26 | return
27 | }
28 | print("Photo has been saved to your library")
29 | }
30 | return helper
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Example/Agrume Example/SingeImageBackgroundColorViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2016 Schnaub. All rights reserved.
3 | //
4 |
5 | import Agrume
6 | import UIKit
7 |
8 | final class SingleImageBackgroundColorViewController: UIViewController {
9 |
10 | private lazy var agrume: Agrume = {
11 | let agrume = Agrume(image: UIImage(named: "MapleBacon")!, background: .colored(.black))
12 | agrume.hideStatusBar = true
13 | return agrume
14 | }()
15 |
16 | @IBAction private func openImage(_ sender: Any) {
17 | let helper = makeHelper()
18 | agrume.onLongPress = helper.makeSaveToLibraryLongPressGesture
19 | agrume.show(from: self)
20 | }
21 |
22 | private func makeHelper() -> AgrumePhotoLibraryHelper {
23 | let saveButtonTitle = NSLocalizedString("Save Photo", comment: "Save Photo")
24 | let cancelButtonTitle = NSLocalizedString("Cancel", comment: "Cancel")
25 | let helper = AgrumePhotoLibraryHelper(saveButtonTitle: saveButtonTitle, cancelButtonTitle: cancelButtonTitle) { error in
26 | guard error == nil else {
27 | print("Could not save your photo")
28 | return
29 | }
30 | print("Photo has been saved to your library")
31 | }
32 | return helper
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Example/Agrume Example/SwiftUIExampleViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2022 Schnaub. All rights reserved.
3 | //
4 |
5 | import Agrume
6 | import SwiftUI
7 | import UIKit
8 |
9 | final class SwiftUIExampleViewController: UIViewController {
10 |
11 | override func viewDidLoad() {
12 | super.viewDidLoad()
13 |
14 | let hostingView = UIHostingController(
15 | rootView: SwiftUIHostingExample(
16 | images: [
17 | UIImage(named: "MapleBacon")!,
18 | UIImage(named: "EvilBacon")!
19 | ]
20 | )
21 | )
22 | addChild(hostingView)
23 | hostingView.view.frame = view.frame
24 | view.addSubview(hostingView.view)
25 | hostingView.didMove(toParent: self)
26 | }
27 |
28 | }
29 |
30 | struct SwiftUIHostingExample: View {
31 |
32 | let images: [UIImage]
33 |
34 | @State var showAgrume = false
35 |
36 | var body: some View {
37 | VStack {
38 | // Hide the presenting button (or other view) whenever Agrume is shown
39 | if !showAgrume {
40 | Button("Launch Agrume from SwiftUI") {
41 | withAnimation {
42 | showAgrume = true
43 | }
44 | }
45 | }
46 |
47 | if showAgrume {
48 | AgrumeView(images: images, isPresenting: $showAgrume)
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | excluded:
2 | - Frameworks/SwiftyGif
3 | - Carthage/Checkouts
4 |
5 | identifier_name:
6 | min_length:
7 | error: 3
8 | max_length:
9 | warning: 50
10 | error: 60
11 | excluded:
12 | - id
13 |
14 | type_name:
15 | min_length:
16 | error: 3
17 | max_length:
18 | error: 50
19 | excluded:
20 | - id
21 | - i
22 |
23 | type_body_length:
24 | - 600
25 |
26 | file_length:
27 | warning: 700
28 |
29 | line_length: 140
30 |
31 | trailing_whitespace:
32 | ignores_empty_lines: true
33 |
34 | cyclomatic_complexity:
35 | warning: 15
36 |
37 | opt_in_rules:
38 | - attributes
39 | - closure_end_indentation
40 | - closure_spacing
41 | - explicit_init
42 | - fatal_error_message
43 | - first_where
44 | - object_literal
45 | - operator_usage_whitespace
46 | - operator_whitespace
47 | - overridden_super_call
48 | - prohibited_super_call
49 | - redundant_nil_coalescing
50 | - vertical_parameter_alignment_on_call
51 | - discouraged_object_literal
52 | - sorted_imports
53 | - static_operator
54 | - strong_iboutlet
55 | - switch_case_on_newline
56 | - toggle_bool
57 |
58 | disabled_rules:
59 | - force_cast
60 | - colon
61 | - unused_optional_binding
62 | - vertical_parameter_alignment
63 | - force_try
64 | - object_literal
65 |
--------------------------------------------------------------------------------
/Example/Agrume Example/SingleImageModalViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2016 Schnaub. All rights reserved.
3 | //
4 |
5 | import Agrume
6 | import UIKit
7 |
8 | final class SingleImageModalViewController: UIViewController {
9 |
10 | override func viewDidLoad() {
11 | super.viewDidLoad()
12 |
13 | navigationController?.navigationBar.barTintColor = .red
14 | }
15 |
16 | @IBAction private func openImage(_ sender: Any) {
17 | let agrume = Agrume(image: UIImage(named: "MapleBacon")!, background: .blurred(.regular))
18 | let helper = makeHelper()
19 | agrume.onLongPress = helper.makeSaveToLibraryLongPressGesture
20 | agrume.show(from: self)
21 | }
22 |
23 | @IBAction private func close(_ sender: Any) {
24 | presentingViewController?.dismiss(animated: true)
25 | }
26 |
27 | private func makeHelper() -> AgrumePhotoLibraryHelper {
28 | let saveButtonTitle = NSLocalizedString("Save Photo", comment: "Save Photo")
29 | let cancelButtonTitle = NSLocalizedString("Cancel", comment: "Cancel")
30 | let helper = AgrumePhotoLibraryHelper(saveButtonTitle: saveButtonTitle, cancelButtonTitle: cancelButtonTitle) { error in
31 | guard error == nil else {
32 | print("Could not save your photo")
33 | return
34 | }
35 | print("Photo has been saved to your library")
36 | }
37 | return helper
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Agrume/ImageDownloader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2016 Schnaub. All rights reserved.
3 | //
4 |
5 | import ImageIO
6 | import MobileCoreServices
7 | import SwiftyGif
8 | import UIKit
9 |
10 | final class ImageDownloader {
11 |
12 | static func downloadImage(_ url: URL, completion: @escaping (_ image: UIImage?) -> Void) -> URLSessionDataTask? {
13 | let session = URLSession(configuration: newConfiguration())
14 | let task = session.dataTask(with: url) { data, _, error in
15 | var image: UIImage?
16 | defer {
17 | DispatchQueue.main.async {
18 | completion(image)
19 | }
20 | }
21 | guard let data, error == nil else {
22 | return
23 | }
24 | if isAnimatedImage(data) {
25 | image = try? UIImage(gifData: data)
26 | } else {
27 | image = UIImage(data: data)
28 | }
29 | }
30 | task.resume()
31 | return task
32 | }
33 |
34 | private static func newConfiguration() -> URLSessionConfiguration {
35 | let configuration = URLSessionConfiguration.default
36 | if #available(iOS 11.0, *) {
37 | configuration.waitsForConnectivity = true
38 | }
39 | return configuration
40 | }
41 |
42 | private static func isAnimatedImage(_ data: Data) -> Bool {
43 | guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil),
44 | let imageType = CGImageSourceGetType(imageSource)
45 | else {
46 | return false
47 | }
48 | return UTTypeConformsTo(imageType, kUTTypeGIF)
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/Agrume/UIKit+Agrume.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2018 Schnaub. All rights reserved.
3 | //
4 |
5 | import UIKit
6 |
7 | extension CGFloat {
8 | static let initialScaleToExpandFrom: CGFloat = 0.6
9 | static let maxScaleForExpandingOffscreen: CGFloat = 1.25
10 | static let targetZoomForDoubleTap: CGFloat = 3
11 | static let minFlickDismissalVelocity: CGFloat = 800
12 | static let highScrollVelocity: CGFloat = 1_600
13 | }
14 |
15 | extension CGSize {
16 | static func * (size: CGSize, scale: CGFloat) -> CGSize {
17 | size.applying(CGAffineTransform(scaleX: scale, y: scale))
18 | }
19 | }
20 |
21 | extension UIView {
22 |
23 | func usesAutoLayout(_ useAutoLayout: Bool) {
24 | translatesAutoresizingMaskIntoConstraints = !useAutoLayout
25 | }
26 |
27 | var portableSafeTopInset: NSLayoutYAxisAnchor {
28 | if #available(iOS 11.0, *) {
29 | return safeAreaLayoutGuide.topAnchor
30 | }
31 | return layoutMarginsGuide.topAnchor
32 | }
33 |
34 | }
35 |
36 | extension UIColor {
37 | var isLight: Bool {
38 | var white: CGFloat = 0
39 | getWhite(&white, alpha: nil)
40 | return white > 0.5
41 | }
42 | }
43 |
44 | extension UICollectionView {
45 |
46 | func register(_ cell: T.Type) {
47 | register(cell, forCellWithReuseIdentifier: String(describing: cell))
48 | }
49 |
50 | func dequeue(indexPath: IndexPath) -> T {
51 | let id = String(describing: T.self)
52 | return dequeue(id: id, indexPath: indexPath)
53 | }
54 |
55 | func dequeue(id: String, indexPath: IndexPath) -> T {
56 | dequeueReusableCell(withReuseIdentifier: id, for: indexPath) as! T
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/Example/Agrume Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSAppTransportSecurity
6 |
7 | NSAllowsArbitraryLoads
8 |
9 |
10 | CFBundleDevelopmentRegion
11 | en
12 | CFBundleExecutable
13 | $(EXECUTABLE_NAME)
14 | CFBundleIdentifier
15 | $(PRODUCT_BUNDLE_IDENTIFIER)
16 | CFBundleInfoDictionaryVersion
17 | 6.0
18 | CFBundleName
19 | $(PRODUCT_NAME)
20 | CFBundlePackageType
21 | APPL
22 | CFBundleShortVersionString
23 | 1.0
24 | CFBundleSignature
25 | ????
26 | CFBundleVersion
27 | 1
28 | LSRequiresIPhoneOS
29 |
30 | UILaunchStoryboardName
31 | LaunchScreen
32 | UIMainStoryboardFile
33 | Main
34 | UIRequiredDeviceCapabilities
35 |
36 | armv7
37 |
38 | UISupportedInterfaceOrientations
39 |
40 | UIInterfaceOrientationPortrait
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | NSPhotoLibraryAddUsageDescription
45 | Photos access is required to save photos in your library
46 | NSPhotoLibraryUsageDescription
47 | Photos access is required to save photos in your library
48 |
49 |
50 |
--------------------------------------------------------------------------------
/Agrume/AgrumeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Schnaub. All rights reserved.
3 | //
4 |
5 | import SwiftUI
6 | import UIKit
7 |
8 | @available(iOS 14.0, *)
9 | public struct AgrumeView: View {
10 |
11 | private let images: [UIImage]
12 | @Binding private var binding: Bool
13 | @Namespace var namespace
14 |
15 | public init(image: UIImage, isPresenting: Binding) {
16 | self.init(images: [image], isPresenting: isPresenting)
17 | }
18 |
19 | public init(images: [UIImage], isPresenting: Binding) {
20 | self.images = images
21 | self._binding = isPresenting
22 | }
23 |
24 | public var body: some View {
25 | WrapperAgrumeView(images: images, isPresenting: $binding)
26 | .matchedGeometryEffect(id: "AgrumeView", in: namespace, properties: .frame, isSource: binding)
27 | .ignoresSafeArea()
28 | }
29 | }
30 |
31 | @available(iOS 13.0, *)
32 | struct WrapperAgrumeView: UIViewControllerRepresentable {
33 |
34 | private let images: [UIImage]
35 | @Binding private var binding: Bool
36 |
37 | public init(images: [UIImage], isPresenting: Binding) {
38 | self.images = images
39 | self._binding = isPresenting
40 | }
41 |
42 | public func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIViewController {
43 | let agrume = Agrume(images: images)
44 | agrume.view.backgroundColor = .clear
45 | agrume.addSubviews()
46 | agrume.addOverlayView()
47 | agrume.willDismiss = {
48 | withAnimation {
49 | binding = false
50 | }
51 | }
52 | return agrume
53 | }
54 |
55 | public func updateUIViewController(_ uiViewController: UIViewController,
56 | context: UIViewControllerRepresentableContext) {
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Agrume/Configuration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2018 Schnaub. All rights reserved.
3 | //
4 |
5 | import UIKit
6 |
7 | /// The background type
8 | ///
9 | /// - colored: Overlay with a color
10 | /// - blurred: Overlay with a UIBlurEffectStyle
11 | public enum Background {
12 | case colored(UIColor)
13 | case blurred(UIBlurEffect.Style)
14 | }
15 |
16 | /// Control the way Agrume is dismissed
17 | ///
18 | /// - withPan: Allow dragging the images and "throwing" them off screen to dismiss Agrume
19 | /// - withButton: Overlay with a close button. Pass an optional `UIBarButtonItem` to control the look
20 | /// - withPanAndButton: Combines both behaviours. Physics and the close button all in one
21 | public enum Dismissal {
22 | /// Allowed pan directions.
23 | ///
24 | /// - horizontalAndVertical: Allow panning freely along X and Y axes
25 | /// - verticalOnly: Only allow panning along the Y axis
26 | public enum PanDirections {
27 | case horizontalAndVertical
28 | case verticalOnly
29 | }
30 |
31 | public struct Physics {
32 | /// Directions in which panning will work during flick gesture.
33 | let permittedDirections: PanDirections
34 | /// Magnitude of the push an image receives after flicking to dismiss. The `nil` value is equivalent to no force, see
35 | /// `UIPushBehavior.magnitude` documentation for the intuition behind non-`nil` values.
36 | let pushMagnitude: CGFloat?
37 | /// Enables or disables image rotation during flicking.
38 | let allowsRotation: Bool
39 | /// Physics with standard (all default) settings.
40 | public static let standard = Physics()
41 |
42 | public init(permittedDirections: PanDirections = .horizontalAndVertical, pushMagnitude: CGFloat? = nil, allowsRotation: Bool = true) {
43 | self.permittedDirections = permittedDirections
44 | self.pushMagnitude = pushMagnitude
45 | self.allowsRotation = allowsRotation
46 | }
47 | }
48 |
49 | case withPan(Physics)
50 | case withButton(UIBarButtonItem?)
51 | case withPanAndButton(Physics, UIBarButtonItem?)
52 | @available(*, deprecated, message: "Use .withPan(.standard) instead.")
53 | case withPhysics
54 | @available(*, deprecated, message: "Use .withPanAndButton(.standard, ...) instead.")
55 | case withPhysicsAndButton(UIBarButtonItem?)
56 | }
57 |
--------------------------------------------------------------------------------
/Agrume/AgrumePhotoLibraryHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2020 Schnaub. All rights reserved.
3 | //
4 |
5 | import UIKit
6 |
7 | public final class AgrumePhotoLibraryHelper: NSObject {
8 |
9 | private let saveButtonTitle: String
10 | private let cancelButtonTitle: String
11 | private let saveToLibraryHandler: (_ error: Error?) -> Void
12 |
13 | /// Initialize photo library helper
14 | ///
15 | /// - Parameters:
16 | /// - saveButtonTitle: Title text to save photo to library
17 | /// - cancelButtonTitle: Cancel text to save photo to library
18 | /// - saveToLibraryHandler: saveToLibraryHandler to notify the user if it was successfull.
19 | public init(saveButtonTitle: String, cancelButtonTitle: String, saveToLibraryHandler: @escaping (_ error: Error?) -> Void) {
20 | self.saveButtonTitle = saveButtonTitle
21 | self.cancelButtonTitle = cancelButtonTitle
22 | self.saveToLibraryHandler = saveToLibraryHandler
23 | }
24 |
25 | /// Save the current photo shown in the user's photo library using Long Press Gesture
26 | /// Make sure to have NSPhotoLibraryUsageDescription (ios 10) and NSPhotoLibraryAddUsageDescription (ios 11+) in your info.plist
27 | public func makeSaveToLibraryLongPressGesture(for image: UIImage?, viewController: UIViewController) {
28 | guard let image else {
29 | return
30 | }
31 | let view = viewController.view!
32 | let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
33 | alert.popoverPresentationController?.sourceView = view
34 | alert.popoverPresentationController?.permittedArrowDirections = .up
35 | let alertPosition = CGRect(x: view.bounds.midX, y: view.bounds.maxY - view.bounds.midY / 2, width: 0, height: 0)
36 | alert.popoverPresentationController?.sourceRect = alertPosition
37 |
38 | alert.addAction(UIAlertAction(title: saveButtonTitle, style: .default) { _ in
39 | UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image), nil)
40 | })
41 | alert.addAction(UIAlertAction(title: cancelButtonTitle, style: .cancel, handler: nil))
42 |
43 | viewController.present(alert, animated: true)
44 | }
45 |
46 | @objc
47 | private func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {
48 | saveToLibraryHandler(error)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Example/Agrume Example/MultipleImagesCollectionViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2016 Schnaub. All rights reserved.
3 | //
4 |
5 | import Agrume
6 | import UIKit
7 |
8 | final class MultipleImagesCollectionViewController: UICollectionViewController {
9 |
10 | private let identifier = "Cell"
11 |
12 | private let images = [
13 | UIImage(named: "MapleBacon")!,
14 | UIImage(named: "EvilBacon")!
15 | ]
16 |
17 | override func viewDidLoad() {
18 | super.viewDidLoad()
19 | let layout = collectionView?.collectionViewLayout as! UICollectionViewFlowLayout
20 | layout.itemSize = CGSize(width: view.frame.width, height: view.frame.height)
21 | }
22 |
23 | // MARK: UICollectionViewDataSource
24 |
25 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
26 | images.count
27 | }
28 |
29 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
30 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as! DemoCell
31 | cell.imageView.image = images[indexPath.item]
32 | return cell
33 | }
34 |
35 | // MARK: UICollectionViewDelegate
36 |
37 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
38 | let agrume = Agrume(images: images, startIndex: indexPath.item, background: .blurred(.regular))
39 | agrume.didScroll = { [unowned self] index in
40 | self.collectionView?.scrollToItem(at: IndexPath(item: index, section: 0), at: [], animated: false)
41 | }
42 | let helper = makeHelper()
43 | agrume.onLongPress = helper.makeSaveToLibraryLongPressGesture
44 | agrume.show(from: self)
45 | }
46 |
47 | private func makeHelper() -> AgrumePhotoLibraryHelper {
48 | let saveButtonTitle = NSLocalizedString("Save Photo", comment: "Save Photo")
49 | let cancelButtonTitle = NSLocalizedString("Cancel", comment: "Cancel")
50 | let helper = AgrumePhotoLibraryHelper(saveButtonTitle: saveButtonTitle, cancelButtonTitle: cancelButtonTitle) { error in
51 | guard error == nil else {
52 | print("Could not save your photo")
53 | return
54 | }
55 | print("Photo has been saved to your library")
56 | }
57 | return helper
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Agrume/AgrumeOverlayView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2018 Schnaub. All rights reserved.
3 | //
4 |
5 | import UIKit
6 |
7 | protocol AgrumeCloseButtonOverlayViewDelegate: AnyObject {
8 | func agrumeOverlayViewWantsToClose(_ view: AgrumeCloseButtonOverlayView)
9 | }
10 |
11 | /// A base class for a user defined view that will overlay the image.
12 | ///
13 | /// An overlay view can be used to add navigation, actions, or information over the image.
14 | open class AgrumeOverlayView: UIView {
15 | override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
16 | if let view = super.hitTest(point, with: event), view != self {
17 | return view
18 | }
19 | return nil
20 | }
21 | }
22 |
23 | final class AgrumeCloseButtonOverlayView: AgrumeOverlayView {
24 |
25 | weak var delegate: AgrumeCloseButtonOverlayViewDelegate?
26 |
27 | private lazy var navigationBar = with(UINavigationBar()) { navigationBar in
28 | navigationBar.usesAutoLayout(true)
29 | navigationBar.backgroundColor = .clear
30 | navigationBar.isTranslucent = true
31 | navigationBar.shadowImage = UIImage()
32 | navigationBar.setBackgroundImage(UIImage(), for: .default)
33 | navigationBar.items = [navigationItem]
34 | }
35 |
36 | private lazy var navigationItem = UINavigationItem(title: "")
37 | private lazy var defaultCloseButton = UIBarButtonItem(
38 | title: NSLocalizedString("Close", comment: "Close image view"),
39 | style: .plain, target: self, action: #selector(close)
40 | )
41 |
42 | init(closeButton: UIBarButtonItem?) {
43 | super.init(frame: .zero)
44 |
45 | addSubview(navigationBar)
46 |
47 | if let closeButton = closeButton {
48 | closeButton.target = self
49 | closeButton.action = #selector(close)
50 | navigationItem.leftBarButtonItem = closeButton
51 | } else {
52 | navigationItem.leftBarButtonItem = defaultCloseButton
53 | }
54 |
55 | NSLayoutConstraint.activate([
56 | navigationBar.topAnchor.constraint(equalTo: portableSafeTopInset),
57 | navigationBar.widthAnchor.constraint(equalTo: widthAnchor),
58 | navigationBar.centerXAnchor.constraint(equalTo: centerXAnchor)
59 | ])
60 | }
61 |
62 | @available(*, unavailable)
63 | required init?(coder aDecoder: NSCoder) {
64 | fatalError("init(coder:) has not been implemented")
65 | }
66 |
67 | @objc
68 | private func close() {
69 | delegate?.agrumeOverlayViewWantsToClose(self)
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/Example/Agrume Example/MultipleImagesCustomOverlayView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2020 Schnaub. All rights reserved.
3 | //
4 |
5 | import Agrume
6 | import UIKit
7 |
8 | final class MultipleImagesCustomOverlayView: UICollectionViewController {
9 |
10 | private let identifier = "Cell"
11 |
12 | private let images = [
13 | UIImage(named: "MapleBacon")!,
14 | UIImage(named: "EvilBacon")!
15 | ]
16 |
17 | private var agrume: Agrume?
18 |
19 | private lazy var overlayView: OverlayView = {
20 | let overlay = OverlayView()
21 | overlay.delegate = self
22 | return overlay
23 | }()
24 |
25 | override func viewDidLoad() {
26 | super.viewDidLoad()
27 | let layout = collectionView?.collectionViewLayout as! UICollectionViewFlowLayout
28 | layout.itemSize = CGSize(width: view.frame.width, height: view.frame.height)
29 | }
30 |
31 | // MARK: UICollectionViewDataSource
32 |
33 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
34 | images.count
35 | }
36 |
37 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
38 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as! DemoCell
39 | cell.imageView.image = images[indexPath.item]
40 | return cell
41 | }
42 |
43 | // MARK: UICollectionViewDelegate
44 |
45 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
46 | overlayView.navigationBar.topItem?.title = "Image \(indexPath.item + 1)"
47 |
48 | agrume = Agrume(images: images, startIndex: indexPath.item, background: .blurred(.regular), overlayView: overlayView)
49 | agrume?.tapBehavior = .toggleOverlayVisibility
50 | agrume?.didScroll = { [unowned self] index in
51 | self.collectionView?.scrollToItem(at: IndexPath(item: index, section: 0), at: [], animated: false)
52 | self.overlayView.navigationBar.topItem?.title = "Image \(index + 1)"
53 | }
54 |
55 | agrume?.show(from: self)
56 | }
57 | }
58 |
59 | extension MultipleImagesCustomOverlayView: OverlayViewDelegate {
60 | func overlayView(_ overlayView: OverlayView, didSelectAction action: String) {
61 | let alert = UIAlertController(
62 | title: nil,
63 | message: "You selected \(action) for image \((agrume?.currentIndex ?? 0) + 1)",
64 | preferredStyle: .alert
65 | )
66 | alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
67 | agrume?.present(alert, animated: true)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Example/Agrume Example/MultipleURLsCollectionViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2016 Schnaub. All rights reserved.
3 | //
4 |
5 | import Agrume
6 | import UIKit
7 |
8 | final class MultipleURLsCollectionViewController: UICollectionViewController {
9 |
10 | private let identifier = "Cell"
11 |
12 | private struct ImageWithURL {
13 | let image: UIImage
14 | let url: URL
15 | }
16 |
17 | private let images = [
18 | ImageWithURL(image: UIImage(named: "MapleBacon")!, url: URL(string: "https://www.dropbox.com/s/mlquw9k6ogvspox/MapleBacon.png?raw=1")!),
19 | ImageWithURL(image: UIImage(named: "EvilBacon")!, url: URL(string: "https://www.dropbox.com/s/fwjbsuonhv1wrqu/EvilBacon.png?raw=1")!)
20 | ]
21 |
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 |
25 | let layout = collectionView?.collectionViewLayout as! UICollectionViewFlowLayout
26 | layout.itemSize = CGSize(width: view.bounds.width, height: view.bounds.height)
27 | }
28 |
29 | // MARK: UICollectionViewDataSource
30 |
31 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
32 | images.count
33 | }
34 |
35 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
36 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as! DemoCell
37 | cell.imageView.image = images[indexPath.item].image
38 | return cell
39 | }
40 |
41 | // MARK: UICollectionViewDelegate
42 |
43 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
44 | let urls = images.map { $0.url }
45 | let agrume = Agrume(urls: urls, startIndex: indexPath.item, background: .blurred(.extraLight))
46 | agrume.didScroll = { [unowned self] index in
47 | self.collectionView?.scrollToItem(at: IndexPath(item: index, section: 0), at: [], animated: false)
48 | }
49 | let helper = makeHelper()
50 | agrume.onLongPress = helper.makeSaveToLibraryLongPressGesture
51 | agrume.show(from: self)
52 | }
53 |
54 | private func makeHelper() -> AgrumePhotoLibraryHelper {
55 | let saveButtonTitle = NSLocalizedString("Save Photo", comment: "Save Photo")
56 | let cancelButtonTitle = NSLocalizedString("Cancel", comment: "Cancel")
57 | let helper = AgrumePhotoLibraryHelper(saveButtonTitle: saveButtonTitle, cancelButtonTitle: cancelButtonTitle) { error in
58 | guard error == nil else {
59 | print("Could not save your photo")
60 | return
61 | }
62 | print("Photo has been saved to your library")
63 | }
64 | return helper
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Example/Agrume Example/OverlayView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2020 Schnaub. All rights reserved.
3 | //
4 |
5 | import Agrume
6 | import UIKit
7 |
8 | protocol OverlayViewDelegate: AnyObject {
9 | func overlayView(_ overlayView: OverlayView, didSelectAction action: String)
10 | }
11 |
12 | /// Example custom image overlay
13 | final class OverlayView: AgrumeOverlayView {
14 | lazy var toolbar: UIToolbar = {
15 | let toolbar = UIToolbar()
16 | toolbar.translatesAutoresizingMaskIntoConstraints = false
17 |
18 | toolbar.setItems(
19 | [UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(selectShare)),
20 | UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(selectDelete)),
21 | UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(selectDone))],
22 | animated: false
23 | )
24 |
25 | return toolbar
26 | }()
27 |
28 | lazy var navigationBar: UINavigationBar = {
29 | let navigationBar = UINavigationBar()
30 | navigationBar.translatesAutoresizingMaskIntoConstraints = false
31 | navigationBar.pushItem(UINavigationItem(title: ""), animated: false)
32 | return navigationBar
33 | }()
34 |
35 | var portableSafeLayoutGuide: UILayoutGuide {
36 | if #available(iOS 11.0, *) {
37 | return safeAreaLayoutGuide
38 | }
39 | return layoutMarginsGuide
40 | }
41 |
42 | weak var delegate: OverlayViewDelegate?
43 |
44 | override init(frame: CGRect) {
45 | super.init(frame: frame)
46 | commonInit()
47 | }
48 |
49 | required init?(coder: NSCoder) {
50 | super.init(coder: coder)
51 | commonInit()
52 | }
53 |
54 | private func commonInit() {
55 | addSubview(toolbar)
56 |
57 | NSLayoutConstraint.activate([
58 | toolbar.bottomAnchor.constraint(equalTo: portableSafeLayoutGuide.bottomAnchor),
59 | toolbar.leadingAnchor.constraint(equalTo: leadingAnchor),
60 | toolbar.trailingAnchor.constraint(equalTo: trailingAnchor)
61 | ])
62 |
63 | addSubview(navigationBar)
64 |
65 | NSLayoutConstraint.activate([
66 | navigationBar.topAnchor.constraint(equalTo: portableSafeLayoutGuide.topAnchor),
67 | navigationBar.leadingAnchor.constraint(equalTo: leadingAnchor),
68 | navigationBar.trailingAnchor.constraint(equalTo: trailingAnchor)
69 | ])
70 | }
71 |
72 | @objc
73 | private func selectShare() {
74 | delegate?.overlayView(self, didSelectAction: "share")
75 | }
76 |
77 | @objc
78 | private func selectDelete() {
79 | delegate?.overlayView(self, didSelectAction: "delete")
80 | }
81 |
82 | @objc
83 | private func selectDone() {
84 | delegate?.overlayView(self, didSelectAction: "done")
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Example/Agrume Example/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Agrume.xcodeproj/xcshareddata/xcschemes/Agrume.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
51 |
52 |
53 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
76 |
77 |
83 |
84 |
85 |
86 |
92 |
93 |
99 |
100 |
101 |
102 |
104 |
105 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.8)
5 | abbrev (0.1.2)
6 | activesupport (8.1.1)
7 | base64
8 | bigdecimal
9 | concurrent-ruby (~> 1.0, >= 1.3.1)
10 | connection_pool (>= 2.2.5)
11 | drb
12 | i18n (>= 1.6, < 2)
13 | json
14 | logger (>= 1.4.2)
15 | minitest (>= 5.1)
16 | securerandom (>= 0.3)
17 | tzinfo (~> 2.0, >= 2.0.5)
18 | uri (>= 0.13.1)
19 | addressable (2.8.8)
20 | public_suffix (>= 2.0.2, < 8.0)
21 | artifactory (3.0.17)
22 | atomos (0.1.3)
23 | aws-eventstream (1.4.0)
24 | aws-partitions (1.1197.0)
25 | aws-sdk-core (3.240.0)
26 | aws-eventstream (~> 1, >= 1.3.0)
27 | aws-partitions (~> 1, >= 1.992.0)
28 | aws-sigv4 (~> 1.9)
29 | base64
30 | bigdecimal
31 | jmespath (~> 1, >= 1.6.1)
32 | logger
33 | aws-sdk-kms (1.118.0)
34 | aws-sdk-core (~> 3, >= 3.239.1)
35 | aws-sigv4 (~> 1.5)
36 | aws-sdk-s3 (1.208.0)
37 | aws-sdk-core (~> 3, >= 3.234.0)
38 | aws-sdk-kms (~> 1)
39 | aws-sigv4 (~> 1.5)
40 | aws-sigv4 (1.12.1)
41 | aws-eventstream (~> 1, >= 1.0.2)
42 | babosa (1.0.4)
43 | base64 (0.2.0)
44 | bigdecimal (4.0.1)
45 | claide (1.1.0)
46 | claide-plugins (0.9.2)
47 | cork
48 | nap
49 | open4 (~> 1.3)
50 | colored (1.2)
51 | colored2 (3.1.2)
52 | commander (4.6.0)
53 | highline (~> 2.0.0)
54 | concurrent-ruby (1.3.6)
55 | connection_pool (3.0.2)
56 | cork (0.3.0)
57 | colored2 (~> 3.1)
58 | csv (3.3.5)
59 | danger (9.5.3)
60 | base64 (~> 0.2)
61 | claide (~> 1.0)
62 | claide-plugins (>= 0.9.2)
63 | colored2 (>= 3.1, < 5)
64 | cork (~> 0.1)
65 | faraday (>= 0.9.0, < 3.0)
66 | faraday-http-cache (~> 2.0)
67 | git (>= 1.13, < 3.0)
68 | kramdown (>= 2.5.1, < 3.0)
69 | kramdown-parser-gfm (~> 1.0)
70 | octokit (>= 4.0)
71 | pstore (~> 0.1)
72 | terminal-table (>= 1, < 5)
73 | danger-swiftlint (0.37.4)
74 | danger
75 | rake (> 10)
76 | thor (~> 1.4)
77 | declarative (0.0.20)
78 | digest-crc (0.7.0)
79 | rake (>= 12.0.0, < 14.0.0)
80 | domain_name (0.6.20240107)
81 | dotenv (2.8.1)
82 | drb (2.2.3)
83 | emoji_regex (3.2.3)
84 | excon (0.112.0)
85 | faraday (1.10.4)
86 | faraday-em_http (~> 1.0)
87 | faraday-em_synchrony (~> 1.0)
88 | faraday-excon (~> 1.1)
89 | faraday-httpclient (~> 1.0)
90 | faraday-multipart (~> 1.0)
91 | faraday-net_http (~> 1.0)
92 | faraday-net_http_persistent (~> 1.0)
93 | faraday-patron (~> 1.0)
94 | faraday-rack (~> 1.0)
95 | faraday-retry (~> 1.0)
96 | ruby2_keywords (>= 0.0.4)
97 | faraday-cookie_jar (0.0.8)
98 | faraday (>= 0.8.0)
99 | http-cookie (>= 1.0.0)
100 | faraday-em_http (1.0.0)
101 | faraday-em_synchrony (1.0.1)
102 | faraday-excon (1.1.0)
103 | faraday-http-cache (2.5.1)
104 | faraday (>= 0.8)
105 | faraday-httpclient (1.0.1)
106 | faraday-multipart (1.1.1)
107 | multipart-post (~> 2.0)
108 | faraday-net_http (1.0.2)
109 | faraday-net_http_persistent (1.2.0)
110 | faraday-patron (1.0.0)
111 | faraday-rack (1.0.0)
112 | faraday-retry (1.0.3)
113 | faraday_middleware (1.2.1)
114 | faraday (~> 1.0)
115 | fastimage (2.4.0)
116 | fastlane (2.230.0)
117 | CFPropertyList (>= 2.3, < 4.0.0)
118 | abbrev (~> 0.1.2)
119 | addressable (>= 2.8, < 3.0.0)
120 | artifactory (~> 3.0)
121 | aws-sdk-s3 (~> 1.0)
122 | babosa (>= 1.0.3, < 2.0.0)
123 | base64 (~> 0.2.0)
124 | bundler (>= 1.12.0, < 3.0.0)
125 | colored (~> 1.2)
126 | commander (~> 4.6)
127 | csv (~> 3.3)
128 | dotenv (>= 2.1.1, < 3.0.0)
129 | emoji_regex (>= 0.1, < 4.0)
130 | excon (>= 0.71.0, < 1.0.0)
131 | faraday (~> 1.0)
132 | faraday-cookie_jar (~> 0.0.6)
133 | faraday_middleware (~> 1.0)
134 | fastimage (>= 2.1.0, < 3.0.0)
135 | fastlane-sirp (>= 1.0.0)
136 | gh_inspector (>= 1.1.2, < 2.0.0)
137 | google-apis-androidpublisher_v3 (~> 0.3)
138 | google-apis-playcustomapp_v1 (~> 0.1)
139 | google-cloud-env (>= 1.6.0, < 2.0.0)
140 | google-cloud-storage (~> 1.31)
141 | highline (~> 2.0)
142 | http-cookie (~> 1.0.5)
143 | json (< 3.0.0)
144 | jwt (>= 2.1.0, < 3)
145 | logger (>= 1.6, < 2.0)
146 | mini_magick (>= 4.9.4, < 5.0.0)
147 | multipart-post (>= 2.0.0, < 3.0.0)
148 | mutex_m (~> 0.3.0)
149 | naturally (~> 2.2)
150 | nkf (~> 0.2.0)
151 | optparse (>= 0.1.1, < 1.0.0)
152 | plist (>= 3.1.0, < 4.0.0)
153 | rubyzip (>= 2.0.0, < 3.0.0)
154 | security (= 0.1.5)
155 | simctl (~> 1.6.3)
156 | terminal-notifier (>= 2.0.0, < 3.0.0)
157 | terminal-table (~> 3)
158 | tty-screen (>= 0.6.3, < 1.0.0)
159 | tty-spinner (>= 0.8.0, < 1.0.0)
160 | word_wrap (~> 1.0.0)
161 | xcodeproj (>= 1.13.0, < 2.0.0)
162 | xcpretty (~> 0.4.1)
163 | xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
164 | fastlane-sirp (1.0.0)
165 | sysrandom (~> 1.0)
166 | gh_inspector (1.1.3)
167 | git (2.3.3)
168 | activesupport (>= 5.0)
169 | addressable (~> 2.8)
170 | process_executer (~> 1.1)
171 | rchardet (~> 1.8)
172 | google-apis-androidpublisher_v3 (0.54.0)
173 | google-apis-core (>= 0.11.0, < 2.a)
174 | google-apis-core (0.11.3)
175 | addressable (~> 2.5, >= 2.5.1)
176 | googleauth (>= 0.16.2, < 2.a)
177 | httpclient (>= 2.8.1, < 3.a)
178 | mini_mime (~> 1.0)
179 | representable (~> 3.0)
180 | retriable (>= 2.0, < 4.a)
181 | rexml
182 | google-apis-iamcredentials_v1 (0.17.0)
183 | google-apis-core (>= 0.11.0, < 2.a)
184 | google-apis-playcustomapp_v1 (0.13.0)
185 | google-apis-core (>= 0.11.0, < 2.a)
186 | google-apis-storage_v1 (0.31.0)
187 | google-apis-core (>= 0.11.0, < 2.a)
188 | google-cloud-core (1.8.0)
189 | google-cloud-env (>= 1.0, < 3.a)
190 | google-cloud-errors (~> 1.0)
191 | google-cloud-env (1.6.0)
192 | faraday (>= 0.17.3, < 3.0)
193 | google-cloud-errors (1.5.0)
194 | google-cloud-storage (1.47.0)
195 | addressable (~> 2.8)
196 | digest-crc (~> 0.4)
197 | google-apis-iamcredentials_v1 (~> 0.1)
198 | google-apis-storage_v1 (~> 0.31.0)
199 | google-cloud-core (~> 1.6)
200 | googleauth (>= 0.16.2, < 2.a)
201 | mini_mime (~> 1.0)
202 | googleauth (1.8.1)
203 | faraday (>= 0.17.3, < 3.a)
204 | jwt (>= 1.4, < 3.0)
205 | multi_json (~> 1.11)
206 | os (>= 0.9, < 2.0)
207 | signet (>= 0.16, < 2.a)
208 | highline (2.0.3)
209 | http-cookie (1.0.8)
210 | domain_name (~> 0.5)
211 | httpclient (2.9.0)
212 | mutex_m
213 | i18n (1.14.7)
214 | concurrent-ruby (~> 1.0)
215 | jmespath (1.6.2)
216 | json (2.18.0)
217 | jwt (2.10.2)
218 | base64
219 | kramdown (2.5.1)
220 | rexml (>= 3.3.9)
221 | kramdown-parser-gfm (1.1.0)
222 | kramdown (~> 2.0)
223 | logger (1.7.0)
224 | mini_magick (4.13.2)
225 | mini_mime (1.1.5)
226 | minitest (5.27.0)
227 | multi_json (1.18.0)
228 | multipart-post (2.4.1)
229 | mutex_m (0.3.0)
230 | nanaimo (0.4.0)
231 | nap (1.1.0)
232 | naturally (2.3.0)
233 | nkf (0.2.0)
234 | octokit (10.0.0)
235 | faraday (>= 1, < 3)
236 | sawyer (~> 0.9)
237 | open4 (1.3.4)
238 | optparse (0.8.1)
239 | os (1.1.4)
240 | plist (3.7.2)
241 | process_executer (1.3.0)
242 | pstore (0.2.0)
243 | public_suffix (7.0.0)
244 | rake (13.3.1)
245 | rchardet (1.10.0)
246 | representable (3.2.0)
247 | declarative (< 0.1.0)
248 | trailblazer-option (>= 0.1.1, < 0.2.0)
249 | uber (< 0.2.0)
250 | retriable (3.1.2)
251 | rexml (3.4.4)
252 | rouge (3.28.0)
253 | ruby2_keywords (0.0.5)
254 | rubyzip (2.4.1)
255 | sawyer (0.9.3)
256 | addressable (>= 2.3.5)
257 | faraday (>= 0.17.3, < 3)
258 | securerandom (0.4.1)
259 | security (0.1.5)
260 | signet (0.21.0)
261 | addressable (~> 2.8)
262 | faraday (>= 0.17.5, < 3.a)
263 | jwt (>= 1.5, < 4.0)
264 | multi_json (~> 1.10)
265 | simctl (1.6.10)
266 | CFPropertyList
267 | naturally
268 | sysrandom (1.0.5)
269 | terminal-notifier (2.0.0)
270 | terminal-table (3.0.2)
271 | unicode-display_width (>= 1.1.1, < 3)
272 | thor (1.4.0)
273 | trailblazer-option (0.1.2)
274 | tty-cursor (0.7.1)
275 | tty-screen (0.8.2)
276 | tty-spinner (0.9.3)
277 | tty-cursor (~> 0.7)
278 | tzinfo (2.0.6)
279 | concurrent-ruby (~> 1.0)
280 | uber (0.1.0)
281 | unicode-display_width (2.6.0)
282 | uri (1.1.1)
283 | word_wrap (1.0.0)
284 | xcodeproj (1.27.0)
285 | CFPropertyList (>= 2.3.3, < 4.0)
286 | atomos (~> 0.1.3)
287 | claide (>= 1.0.2, < 2.0)
288 | colored2 (~> 3.1)
289 | nanaimo (~> 0.4.0)
290 | rexml (>= 3.3.6, < 4.0)
291 | xcpretty (0.4.1)
292 | rouge (~> 3.28.0)
293 | xcpretty-travis-formatter (1.0.1)
294 | xcpretty (~> 0.2, >= 0.0.7)
295 |
296 | PLATFORMS
297 | ruby
298 |
299 | DEPENDENCIES
300 | danger
301 | danger-swiftlint
302 | fastlane
303 |
304 | BUNDLED WITH
305 | 2.1.4
306 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Agrume
2 |
3 | [](https://travis-ci.org/JanGorman/Agrume) [](https://github.com/Carthage/Carthage)
4 | [](http://cocoapods.org/pods/Agrume)
5 | [](http://cocoapods.org/pods/Agrume)
6 | [](http://cocoapods.org/pods/Agrume)
7 | [](https://swift.org/package-manager)
8 |
9 | An iOS image viewer written in Swift with support for multiple images.
10 |
11 | 
12 |
13 | ## Requirements
14 |
15 | - Swift 5.0
16 | - iOS 9.0+
17 | - Xcode 10.2+
18 |
19 | ## Installation
20 |
21 | Use [Swift Package Manager](https://swift.org/package-manager).
22 |
23 | Or [CocoaPods](http://cocoapods.org). Add the dependency to your `Podfile` and then run `pod install`:
24 |
25 | ```ruby
26 | pod "Agrume"
27 | ```
28 |
29 | Or [Carthage](https://github.com/Carthage/Carthage). Add the dependency to your `Cartfile` and then run `carthage update`:
30 |
31 | ```ogdl
32 | github "JanGorman/Agrume"
33 | ```
34 |
35 | ## Usage
36 |
37 | There are multiple ways you can use the image viewer (and the included sample project shows them all).
38 |
39 | For just a single image it's as easy as
40 |
41 | ### Basic
42 |
43 | ```swift
44 | import Agrume
45 |
46 | private lazy var agrume = Agrume(image: UIImage(named: "…")!)
47 |
48 | @IBAction func openImage(_ sender: Any) {
49 | agrume.show(from: self)
50 | }
51 | ```
52 |
53 | You can also pass in a `URL` and Agrume will take care of the download for you.
54 |
55 | ### SwiftUI
56 |
57 | Currently the SwiftUI implementation doesn't surface configurations, so can only be used as a basic image viewer - PRs welcome to extend its functionality.
58 |
59 | ```swift
60 | import Agrume
61 |
62 | struct ExampleView: View {
63 |
64 | let images: [UIImage]
65 |
66 | @State var showAgrume = false
67 |
68 | var body: some View {
69 | VStack {
70 | // Hide the presenting button (or other view) whenever Agrume is shown
71 | if !showAgrume {
72 | Button("Launch Agrume from SwiftUI") {
73 | withAnimation {
74 | showAgrume = true
75 | }
76 | }
77 | }
78 |
79 | if showAgrume {
80 | // You can pass a single or multiple images
81 | AgrumeView(images: images, isPresenting: $showAgrume)
82 | }
83 | }
84 | }
85 | }
86 | ```
87 |
88 | ### Background Configuration
89 |
90 | Agrume has different background configurations. You can have it blur the view it's covering or supply a background color:
91 |
92 | ```swift
93 | let agrume = Agrume(image: UIImage(named: "…")!, background: .blurred(.regular))
94 | // or
95 | let agrume = Agrume(image: UIImage(named: "…")!, background: .colored(.green))
96 | ```
97 |
98 | ### Multiple Images
99 |
100 | 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.
101 |
102 | ```swift
103 | // In case of an array of [UIImage]:
104 | let agrume = Agrume(images: images, startIndex: indexPath.item, background: .blurred(.light))
105 | // Or an array of [URL]:
106 | // let agrume = Agrume(urls: urls, startIndex: indexPath.item, background: .blurred(.light))
107 |
108 | agrume.didScroll = { [unowned self] index in
109 | self.collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: [], animated: false)
110 | }
111 | agrume.show(from: self)
112 | ```
113 |
114 | This shows a way of keeping the zoomed library and the one in the background synced.
115 |
116 | ### Animated gifs
117 |
118 | Agrume bundles [SwiftyGif](https://github.com/kirualex/SwiftyGif) to display animated gifs. You use SwiftyGif's custom `UIImage` initializer:
119 |
120 | ```swift
121 | let image = UIImage(gifName: "animated.gif")
122 | let agrume = Agrume(image: image)
123 | agrume.display(from: self)
124 |
125 | // Or gif using data:
126 |
127 | let image = UIImage(gifData: data)
128 | let agrume = Agrume(image: image)
129 |
130 | // Or multiple images:
131 |
132 | let images = [UIImage(gifName: "animated.gif"), UIImage(named: "foo.png")] // You can pass both animated and regular images at the same time
133 | let agrume = Agrume(images: images)
134 | ```
135 |
136 | Remote animated gifs (i.e. using the url or urls initializer) are supported. Agrume does the image type detection and displays them properly. If using Agrume from a custom `UIImageView` you may need to rebuild the `UIImage` using the original data to preserve animation vs. using the `UIImage` instance from the image view.
137 |
138 | ### Close Button
139 |
140 | Per default you dismiss the zoomed view by dragging/flicking the image off screen. You can opt out of this behaviour and instead display a close button. To match the look and feel of your app you can pass in a custom `UIBarButtonItem`:
141 |
142 | ```swift
143 | // Default button that displays NSLocalizedString("Close", …)
144 | let agrume = Agrume(image: UIImage(named: "…")!, .dismissal: .withButton(nil))
145 | // Customise the button any way you like. For example display a system "x" button
146 | let button = UIBarButtonItem(barButtonSystemItem: .stop, target: nil, action: nil)
147 | button.tintColor = .red
148 | let agrume = Agrume(image: UIImage(named: "…")!, .dismissal: .withButton(button))
149 | ```
150 |
151 | The included sample app shows both cases for reference.
152 |
153 | ### Custom Download Handler
154 |
155 | 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).
156 |
157 | ```swift
158 | import Agrume
159 | import MapleBacon
160 |
161 | private lazy var agrume = Agrume(url: URL(string: "https://dl.dropboxusercontent.com/u/512759/MapleBacon.png")!)
162 |
163 | @IBAction func openURL(_ sender: Any) {
164 | agrume.download = { url, completion in
165 | Downloader.default.download(url) { image in
166 | completion(image)
167 | }
168 | }
169 | agrume.show(from: self)
170 | }
171 | ```
172 |
173 | ### Global Custom Download Handler
174 |
175 | 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:
176 |
177 | ```swift
178 | import Agrume
179 |
180 | AgrumeServiceLocator.shared.setDownloadHandler { url, completion in
181 | // Download data, cache it and call the completion with the resulting UIImage
182 | }
183 |
184 | // Some other place
185 | agrume.show(from: self)
186 | ```
187 |
188 | ### Custom Data Source
189 |
190 | 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.
191 |
192 | ```swift
193 | import Agrume
194 |
195 | let dataSource: AgrumeDataSource = MyDataSourceImplementation()
196 | let agrume = Agrume(dataSource: dataSource)
197 |
198 | agrume.show(from: self)
199 | ```
200 |
201 | ### Status Bar Appearance
202 |
203 | You can customize the status bar appearance when displaying the zoomed in view. `Agrume` has a `statusBarStyle` property:
204 |
205 | ```swift
206 | let agrume = Agrume(image: image)
207 | agrume.statusBarStyle = .lightContent
208 | agrume.show(from: self)
209 | ```
210 |
211 | ### Long Press Gesture and Downloading Images
212 |
213 | If you want to handle long press gestures on the images, there is an optional `onLongPress` closure. This will pass an optional `UIImage` and a reference to the Agrume `UIViewController` as parameters. The project includes a helper class to easily opt into downloading the image to the user's photo library called `AgrumePhotoLibraryHelper`. First, create an instance of the helper:
214 |
215 | ```swift
216 | private func makeHelper() -> AgrumePhotoLibraryHelper {
217 | let saveButtonTitle = NSLocalizedString("Save Photo", comment: "Save Photo")
218 | let cancelButtonTitle = NSLocalizedString("Cancel", comment: "Cancel")
219 | let helper = AgrumePhotoLibraryHelper(saveButtonTitle: saveButtonTitle, cancelButtonTitle: cancelButtonTitle) { error in
220 | guard error == nil else {
221 | print("Could not save your photo")
222 | return
223 | }
224 | print("Photo has been saved to your library")
225 | }
226 | return helper
227 | }
228 | ```
229 |
230 | and then pass this helper's long press handler to `Agrume` as follows:
231 |
232 | ```swift
233 | let helper = makeHelper()
234 | agrume.onLongPress = helper.makeSaveToLibraryLongPressGesture
235 | ```
236 |
237 | ### Custom Overlay View
238 |
239 | You can customise the look and functionality of the image views. To do so, you need create a class that inherits from `AgrumeOverlayView: UIView`. As this is nothing more than a regular `UIView` you can do anything you want with it like add a custom toolbar or buttons to it. The example app shows a detailed example of how this can be achieved.
240 |
241 | ### Live Text Support
242 |
243 | Agrume supports Live Text introduced since iOS 16. This allows user to interact with texts and QR codes in the image. It is available for iOS 16 or newer, on devices with A12 Bionic Chip (iPhone XS) or newer.
244 |
245 | ```swift
246 | let agrume = Agrume(image: UIImage(named: "…")!, enableLiveText: true)
247 | ```
248 |
249 | ### Lifecycle
250 |
251 | `Agrume` offers the following lifecycle closures that you can optionally set:
252 |
253 | - `willDismiss`
254 | - `didDismiss`
255 | - `didScroll`
256 |
257 | ### Running the Sample Code
258 |
259 | The project ships with an example app that shows the different functions documented above. Since there is a dependency on [SwiftyGif](https://github.com/kirualex/SwiftyGif) you will also need to fetch that to run the project. It's included as git submodule. After fetching the repository, from the project's root directory run:
260 |
261 | ```bash
262 | git submodule update --init
263 | ```
264 |
265 | ## Licence
266 |
267 | Agrume is released under the MIT license. See LICENSE for details
268 |
--------------------------------------------------------------------------------
/Agrume/AgrumeCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2016 Schnaub. All rights reserved.
3 | //
4 |
5 | import SwiftyGif
6 | import UIKit
7 | import VisionKit
8 |
9 | protocol AgrumeCellDelegate: AnyObject {
10 |
11 | var isSingleImageMode: Bool { get }
12 | var presentingController: UIViewController { get }
13 |
14 | func dismissAfterFlick()
15 | func dismissAfterTap()
16 | func toggleOverlayVisibility()
17 | }
18 |
19 | final class AgrumeCell: UICollectionViewCell {
20 |
21 | var tapBehavior: Agrume.TapBehavior = .dismissIfZoomedOut
22 | /// Specifies dismissal physics behavior; if `nil` then no physics is used for dismissal.
23 | var panPhysics: Dismissal.Physics? = .standard
24 |
25 | private lazy var scrollView = with(UIScrollView()) { scrollView in
26 | scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
27 | scrollView.delegate = self
28 | scrollView.zoomScale = 1
29 | scrollView.maximumZoomScale = 8
30 | scrollView.isScrollEnabled = false
31 | scrollView.showsHorizontalScrollIndicator = false
32 | scrollView.showsVerticalScrollIndicator = false
33 | }
34 | private lazy var imageView = with(UIImageView()) { imageView in
35 | imageView.contentMode = .scaleAspectFit
36 | imageView.clipsToBounds = true
37 | imageView.layer.allowsEdgeAntialiasing = true
38 | }
39 | private lazy var singleTapGesture = with(
40 | UITapGestureRecognizer(
41 | target: self,
42 | action: #selector(singleTap)
43 | )
44 | ) { gesture in
45 | gesture.require(toFail: doubleTapGesture)
46 | gesture.delegate = self
47 | }
48 | private lazy var doubleTapGesture = with(
49 | UITapGestureRecognizer(target: self, action: #selector(doubleTap))
50 | ) { gesture in
51 | gesture.numberOfTapsRequired = 2
52 | }
53 | private lazy var panGesture = with(
54 | UIPanGestureRecognizer(target: self, action: #selector(dismissPan))
55 | ) { gesture in
56 | gesture.maximumNumberOfTouches = 1
57 | gesture.delegate = self
58 | }
59 |
60 | private var animator: UIDynamicAnimator?
61 | private var flickedToDismiss = false
62 | private var isDraggingImage = false
63 | private var imageDragStartingPoint: CGPoint!
64 | private var imageDragOffsetFromActualTranslation: UIOffset!
65 | private var imageDragOffsetFromImageCenter: UIOffset!
66 | private var attachmentBehavior: UIAttachmentBehavior?
67 |
68 | // index of the cell in the collection view
69 | var index: Int?
70 |
71 | // if set to true, it means we are updating image on the same cell, so we want to reserve the zoom level & position
72 | var updatingImageOnSameCell = false
73 |
74 | // enables Live Text analysis & interaction
75 | var enableLiveText = false
76 |
77 | var image: UIImage? {
78 | didSet {
79 | if image?.imageData != nil, let image = image {
80 | imageView.setGifImage(image)
81 | } else {
82 | imageView.image = image
83 | if #available(iOS 16, macCatalyst 17.0, *), enableLiveText, let image = image {
84 | analyzeImage(image)
85 | }
86 | }
87 | if !updatingImageOnSameCell {
88 | updateScrollViewAndImageViewForCurrentMetrics()
89 | }
90 | updatingImageOnSameCell = false
91 | }
92 | }
93 | weak var delegate: AgrumeCellDelegate?
94 |
95 | private(set) lazy var swipeGesture = with(UISwipeGestureRecognizer(target: self, action: nil)) { gesture in
96 | gesture.direction = [.left, .right]
97 | gesture.delegate = self
98 | }
99 |
100 | override init(frame: CGRect) {
101 | super.init(frame: frame)
102 |
103 | backgroundColor = .clear
104 | contentView.addSubview(scrollView)
105 | scrollView.addSubview(imageView)
106 | setupGestureRecognizers()
107 | if panPhysics != nil {
108 | animator = UIDynamicAnimator(referenceView: scrollView)
109 | }
110 | }
111 |
112 | required init?(coder aDecoder: NSCoder) {
113 | super.init(coder: aDecoder)
114 | }
115 |
116 | override func prepareForReuse() {
117 | super.prepareForReuse()
118 |
119 | if !updatingImageOnSameCell {
120 | imageView.image = nil
121 | scrollView.zoomScale = 1
122 | updateScrollViewAndImageViewForCurrentMetrics()
123 | }
124 | }
125 |
126 | private func setupGestureRecognizers() {
127 | contentView.addGestureRecognizer(singleTapGesture)
128 | contentView.addGestureRecognizer(doubleTapGesture)
129 | scrollView.addGestureRecognizer(panGesture)
130 | contentView.addGestureRecognizer(swipeGesture)
131 | }
132 |
133 | func cleanup() {
134 | animator = nil
135 | }
136 |
137 | }
138 |
139 | extension AgrumeCell: UIGestureRecognizerDelegate {
140 |
141 | private var notZoomed: Bool {
142 | scrollView.zoomScale == 1
143 | }
144 |
145 | private var isImageViewOffscreen: Bool {
146 | let visibleRect = scrollView.convert(contentView.bounds, from: contentView)
147 | return animator?.items(in: visibleRect).isEmpty == true
148 | }
149 |
150 | override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
151 | if notZoomed, let pan = gestureRecognizer as? UIPanGestureRecognizer {
152 | let velocity = pan.velocity(in: scrollView)
153 | if delegate?.isSingleImageMode == true {
154 | return true
155 | }
156 | return abs(velocity.y) > abs(velocity.x)
157 | } else if notZoomed, gestureRecognizer as? UISwipeGestureRecognizer != nil {
158 | return false
159 | }
160 | return true
161 | }
162 |
163 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
164 | if gestureRecognizer as? UIPanGestureRecognizer != nil {
165 | return notZoomed
166 | }
167 | return true
168 | }
169 |
170 | @objc
171 | private func doubleTap(_ sender: UITapGestureRecognizer) {
172 | let point = scrollView.convert(sender.location(in: sender.view), from: sender.view)
173 |
174 | if notZoomed {
175 | zoom(to: point, scale: .targetZoomForDoubleTap)
176 | } else {
177 | zoom(to: .zero, scale: 1)
178 | }
179 | }
180 |
181 | private func zoom(to point: CGPoint, scale: CGFloat) {
182 | let factor = 1 / scrollView.zoomScale
183 | let translatedZoom = CGPoint(
184 | x: (point.x + scrollView.contentOffset.x) * factor,
185 | y: (point.y + scrollView.contentOffset.y) * factor
186 | )
187 |
188 | let width = scrollView.frame.width / scale
189 | let height = scrollView.frame.height / scale
190 | let destination = CGRect(
191 | x: translatedZoom.x - width / 2,
192 | y: translatedZoom.y - height / 2,
193 | width: width,
194 | height: height
195 | )
196 |
197 | contentView.isUserInteractionEnabled = false
198 |
199 | CATransaction.begin()
200 | CATransaction.setCompletionBlock { [weak self] in
201 | // captures self weakly to avoid extending the lifetime of the cell
202 | guard let self else { return }
203 | self.contentView.isUserInteractionEnabled = true
204 | }
205 | scrollView.zoom(to: destination, animated: true)
206 | CATransaction.commit()
207 | }
208 |
209 | private func contentInsetForScrollView(atScale: CGFloat) -> UIEdgeInsets {
210 | let boundsWidth = scrollView.bounds.width
211 | let boundsHeight = scrollView.bounds.height
212 | let contentWidth = max(image?.size.width ?? 0, boundsWidth)
213 | let contentHeight = max(image?.size.height ?? 0, boundsHeight)
214 |
215 | var minContentWidth: CGFloat
216 | var minContentHeight: CGFloat
217 |
218 | if contentHeight > contentWidth {
219 | if boundsHeight / boundsWidth < contentHeight / contentWidth {
220 | minContentHeight = boundsHeight
221 | minContentWidth = contentWidth * (minContentHeight / contentHeight)
222 | } else {
223 | minContentWidth = boundsWidth
224 | minContentHeight = contentHeight * (minContentWidth / contentWidth)
225 | }
226 | } else {
227 | if boundsWidth / boundsHeight < contentWidth / contentHeight {
228 | minContentWidth = boundsWidth
229 | minContentHeight = contentHeight * (minContentWidth / contentWidth)
230 | } else {
231 | minContentHeight = boundsHeight
232 | minContentWidth = contentWidth * (minContentHeight / contentHeight)
233 | }
234 | }
235 | minContentWidth *= atScale
236 | minContentHeight *= atScale
237 |
238 | if minContentWidth > contentView.bounds.width && minContentHeight > contentView.bounds.height {
239 | return .zero
240 | } else {
241 | let verticalDiff = max(boundsHeight - minContentHeight, 0) / 2
242 | let horizontalDiff = max(boundsWidth - minContentWidth, 0) / 2
243 | return UIEdgeInsets(top: verticalDiff, left: horizontalDiff, bottom: verticalDiff, right: horizontalDiff)
244 | }
245 | }
246 |
247 | @objc
248 | private func singleTap() {
249 | switch tapBehavior {
250 | case .dismissIfZoomedOut:
251 | if notZoomed {
252 | dismiss()
253 | }
254 | case .dismissAlways:
255 | dismiss()
256 | case .zoomOut where notZoomed:
257 | dismiss()
258 | case .zoomOut:
259 | zoom(to: .zero, scale: 1)
260 | case .toggleOverlayVisibility:
261 | delegate?.toggleOverlayVisibility()
262 | }
263 | }
264 |
265 | private func dismiss() {
266 | if flickedToDismiss {
267 | delegate?.dismissAfterFlick()
268 | } else {
269 | delegate?.dismissAfterTap()
270 | }
271 | }
272 |
273 | @objc
274 | private func dismissPan(_ gesture: UIPanGestureRecognizer) {
275 | guard let panPhysics else { return }
276 |
277 | let translation = gesture.translation(in: gesture.view)
278 | let locationInView = gesture.location(in: gesture.view)
279 | let velocity = gesture.velocity(in: gesture.view)
280 | let vectorDistance: CGFloat
281 | switch panPhysics.permittedDirections {
282 | case .horizontalAndVertical:
283 | vectorDistance = sqrt(pow(velocity.x, 2) + pow(velocity.y, 2))
284 | case .verticalOnly:
285 | vectorDistance = velocity.y
286 | }
287 |
288 | if case .began = gesture.state {
289 | isDraggingImage = imageView.frame.contains(locationInView)
290 | if isDraggingImage {
291 | startImageDragging(locationInView, translationOffset: .zero)
292 | }
293 | } else if case .changed = gesture.state {
294 | if isDraggingImage {
295 | var newAnchor = imageDragStartingPoint
296 | if panPhysics.permittedDirections == .horizontalAndVertical {
297 | // Only include x component if panning along both axes is allowed
298 | newAnchor?.x += translation.x + imageDragOffsetFromActualTranslation.horizontal
299 | }
300 | newAnchor?.y += translation.y + imageDragOffsetFromActualTranslation.vertical
301 | attachmentBehavior?.anchorPoint = newAnchor!
302 | } else {
303 | isDraggingImage = imageView.frame.contains(locationInView)
304 | if isDraggingImage {
305 | let translationOffset: UIOffset
306 | switch panPhysics.permittedDirections {
307 | case .horizontalAndVertical:
308 | translationOffset = UIOffset(horizontal: -1 * translation.x, vertical: -1 * translation.y)
309 | case .verticalOnly:
310 | translationOffset = UIOffset(horizontal: 0, vertical: -1 * translation.y)
311 | }
312 | startImageDragging(locationInView, translationOffset: translationOffset)
313 | }
314 | }
315 | } else {
316 | if vectorDistance > .minFlickDismissalVelocity {
317 | if isDraggingImage {
318 | dismissWithFlick(velocity)
319 | } else {
320 | dismiss()
321 | }
322 | } else {
323 | cancelCurrentImageDrag(true)
324 | }
325 | }
326 | }
327 |
328 | private func dismissWithFlick(_ velocity: CGPoint) {
329 | guard let panPhysics else { return }
330 |
331 | flickedToDismiss = true
332 |
333 | let push = UIPushBehavior(items: [imageView], mode: .instantaneous)
334 | switch panPhysics.permittedDirections {
335 | case .horizontalAndVertical:
336 | push.pushDirection = CGVector(dx: velocity.x * 0.1, dy: velocity.y * 0.1)
337 | case .verticalOnly:
338 | push.pushDirection = CGVector(dx: 0, dy: velocity.y * 0.1)
339 | }
340 | if let pushMagnitude = panPhysics.pushMagnitude {
341 | push.magnitude = pushMagnitude
342 | }
343 | push.setTargetOffsetFromCenter(imageDragOffsetFromImageCenter, for: imageView)
344 | push.action = pushAction
345 | if let attachmentBehavior = attachmentBehavior {
346 | animator?.removeBehavior(attachmentBehavior)
347 | }
348 | animator?.addBehavior(push)
349 | }
350 |
351 | private func pushAction() {
352 | if isImageViewOffscreen {
353 | animator?.removeAllBehaviors()
354 | attachmentBehavior = nil
355 | imageView.removeFromSuperview()
356 | dismiss()
357 | }
358 | }
359 |
360 | private func cancelCurrentImageDrag(_ animated: Bool, duration: TimeInterval = 0.7) {
361 | animator?.removeAllBehaviors()
362 | attachmentBehavior = nil
363 | isDraggingImage = false
364 |
365 | if !animated {
366 | imageView.transform = .identity
367 | recenterImage(size: scrollView.contentSize)
368 | } else {
369 | UIView.animate(
370 | withDuration: duration,
371 | delay: 0,
372 | usingSpringWithDamping: 0.7,
373 | initialSpringVelocity: 0,
374 | options: [.allowUserInteraction, .beginFromCurrentState],
375 | animations: {
376 | if self.isDraggingImage {
377 | return
378 | }
379 |
380 | self.imageView.transform = .identity
381 | if !self.scrollView.isDragging && !self.scrollView.isDecelerating {
382 | self.recenterImage(size: self.scrollView.contentSize)
383 | self.updateScrollViewAndImageViewForCurrentMetrics()
384 | }
385 | }
386 | )
387 | }
388 | }
389 |
390 | func recenterDuringRotation(size: CGSize) {
391 | self.recenterImage(size: size)
392 | self.updateScrollViewAndImageViewForCurrentMetrics()
393 | }
394 |
395 | func recenterImage(size: CGSize) {
396 | imageView.center = CGPoint(x: size.width / 2, y: size.height / 2)
397 | }
398 |
399 | private func updateScrollViewAndImageViewForCurrentMetrics() {
400 | scrollView.frame = contentView.frame
401 | if let image = imageView.image ?? imageView.currentImage {
402 | imageView.frame = resizedFrame(forSize: image.size)
403 | }
404 | scrollView.contentSize = imageView.bounds.size
405 | scrollView.contentInset = contentInsetForScrollView(atScale: scrollView.zoomScale)
406 | }
407 |
408 | private func resizedFrame(forSize size: CGSize) -> CGRect {
409 | var frame = contentView.bounds
410 | let screenWidth = frame.width * scrollView.zoomScale
411 | let screenHeight = frame.height * scrollView.zoomScale
412 | var targetWidth = screenWidth
413 | var targetHeight = screenHeight
414 | let nativeWidth = max(size.width, screenWidth)
415 | let nativeHeight = max(size.height, screenHeight)
416 |
417 | if nativeHeight > nativeWidth {
418 | if screenHeight / screenWidth < nativeHeight / nativeWidth {
419 | targetWidth = screenHeight / (nativeHeight / nativeWidth)
420 | } else {
421 | targetHeight = screenWidth / (nativeWidth / nativeHeight)
422 | }
423 | } else {
424 | if screenWidth / screenHeight < nativeWidth / nativeHeight {
425 | targetHeight = screenWidth / (nativeWidth / nativeHeight)
426 | } else {
427 | targetWidth = screenHeight / (nativeHeight / nativeWidth)
428 | }
429 | }
430 |
431 | frame.size = CGSize(width: targetWidth, height: targetHeight)
432 | return frame.integral
433 | }
434 |
435 | private func startImageDragging(_ locationInView: CGPoint, translationOffset: UIOffset) {
436 | imageDragStartingPoint = locationInView
437 | imageDragOffsetFromActualTranslation = translationOffset
438 |
439 | let anchor = imageDragStartingPoint
440 | let imageCenter = imageView.center
441 | let offset = UIOffset(horizontal: locationInView.x - imageCenter.x, vertical: locationInView.y - imageCenter.y)
442 | imageDragOffsetFromImageCenter = offset
443 |
444 | if let panPhysics = panPhysics {
445 | attachmentBehavior = UIAttachmentBehavior(item: imageView, offsetFromCenter: offset, attachedToAnchor: anchor!)
446 | animator!.addBehavior(attachmentBehavior!)
447 |
448 | let modifier = UIDynamicItemBehavior(items: [imageView])
449 | modifier.angularResistance = angularResistance(in: imageView)
450 | modifier.density = density(in: imageView)
451 | modifier.allowsRotation = panPhysics.allowsRotation
452 | animator!.addBehavior(modifier)
453 | }
454 | }
455 |
456 | private func angularResistance(in view: UIView) -> CGFloat {
457 | let defaultResistance: CGFloat = 4
458 | return appropriateValue(defaultValue: defaultResistance) * factor(forView: view)
459 | }
460 |
461 | private func density(in view: UIView) -> CGFloat {
462 | let defaultDensity: CGFloat = 0.5
463 | return appropriateValue(defaultValue: defaultDensity) * factor(forView: view)
464 | }
465 |
466 | private func appropriateValue(defaultValue: CGFloat) -> CGFloat {
467 | let screenWidth = UIApplication.shared.windows.first?.bounds.width ?? UIScreen.main.bounds.width
468 | let screenHeight = UIApplication.shared.windows.first?.bounds.height ?? UIScreen.main.bounds.height
469 | // Default value that works well for the screenSize adjusted for the actual size of the device
470 | return defaultValue * ((320 * 480) / (screenWidth * screenHeight))
471 | }
472 |
473 | private func factor(forView view: UIView) -> CGFloat {
474 | let actualArea = view.bounds.width * view.bounds.height
475 | let referenceArea = contentView.bounds.height * contentView.bounds.width
476 | return referenceArea / actualArea
477 | }
478 |
479 | }
480 |
481 | extension AgrumeCell: UIScrollViewDelegate {
482 |
483 | func viewForZooming(in scrollView: UIScrollView) -> UIView? {
484 | imageView
485 | }
486 |
487 | func scrollViewDidZoom(_ scrollView: UIScrollView) {
488 | scrollView.contentInset = contentInsetForScrollView(atScale: scrollView.zoomScale)
489 |
490 | if !scrollView.isScrollEnabled {
491 | scrollView.isScrollEnabled = true
492 | }
493 | }
494 |
495 | func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
496 | scrollView.isScrollEnabled = scale > 1
497 | scrollView.contentInset = contentInsetForScrollView(atScale: scale)
498 | }
499 |
500 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
501 | let highVelocity: CGFloat = .highScrollVelocity
502 | let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView.panGestureRecognizer.view)
503 | if notZoomed && (abs(velocity.x) > highVelocity || abs(velocity.y) > highVelocity) {
504 | dismiss()
505 | }
506 | }
507 |
508 | @available(iOS 16, macCatalyst 17.0, *)
509 | private func analyzeImage(_ image: UIImage) {
510 | guard ImageAnalyzer.isSupported else {
511 | return
512 | }
513 | let interaction = ImageAnalysisInteraction()
514 | interaction.delegate = self
515 | imageView.addInteraction(interaction)
516 |
517 | let analyzer = ImageAnalyzer()
518 | let configuration = ImageAnalyzer.Configuration([.text, .machineReadableCode])
519 |
520 | Task { @MainActor in
521 | do {
522 | let analysis = try await analyzer.analyze(image, configuration: configuration)
523 | interaction.analysis = analysis
524 | interaction.preferredInteractionTypes = .automatic
525 | } catch {
526 | print(error.localizedDescription)
527 | }
528 | }
529 | }
530 | }
531 |
532 | @available(iOS 16.0, macCatalyst 17.0, *)
533 | extension AgrumeCell: ImageAnalysisInteractionDelegate {
534 | func presentingViewController(for interaction: ImageAnalysisInteraction) -> UIViewController? {
535 | delegate?.presentingController
536 | }
537 | }
538 |
--------------------------------------------------------------------------------
/Agrume/Agrume.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2016 Schnaub. All rights reserved.
3 | //
4 |
5 | import UIKit
6 |
7 | public final class Agrume: UIViewController {
8 |
9 | /// Tap behaviour, i.e. what happens when you tap outside of the image area
10 | public enum TapBehavior {
11 | case dismissIfZoomedOut
12 | case dismissAlways
13 | case zoomOut
14 | case toggleOverlayVisibility
15 | }
16 |
17 | private var images: [AgrumeImage]!
18 | private let startIndex: Int
19 | private let dismissal: Dismissal
20 | private let enableLiveText: Bool
21 |
22 | private var overlayView: AgrumeOverlayView?
23 | private weak var dataSource: AgrumeDataSource?
24 |
25 | /// The background property. Set through the initialiser for most use cases.
26 | public var background: Background
27 |
28 | /// The "page" index for the current image
29 | public private(set) var currentIndex: Int
30 |
31 | public typealias DownloadCompletion = (_ image: UIImage?) -> Void
32 |
33 | /// Optional closure to call when user long pressed on an image
34 | public var onLongPress: ((UIImage?, UIViewController) -> Void)?
35 | /// Optional closure to call whenever Agrume is about to dismiss.
36 | public var willDismiss: (() -> Void)?
37 | /// Optional closure to call whenever Agrume is dismissed.
38 | public var didDismiss: (() -> Void)?
39 | /// Optional closure to call whenever Agrume scrolls to the next image in a collection. Passes the "page" index
40 | public var didScroll: ((_ index: Int) -> Void)?
41 | /// An optional download handler. Passed the URL that is supposed to be loaded. Call the completion with the image
42 | /// when the download is done.
43 | public var download: ((_ url: URL, _ completion: @escaping DownloadCompletion) -> Void)?
44 | /// Status bar style when presenting
45 | public var statusBarStyle: UIStatusBarStyle? {
46 | didSet {
47 | setNeedsStatusBarAppearanceUpdate()
48 | }
49 | }
50 | /// Hide status bar when presenting. Defaults to `false`
51 | public var hideStatusBar = false
52 |
53 | /// Default tap behaviour is to dismiss the view if zoomed out
54 | public var tapBehavior: TapBehavior = .dismissIfZoomedOut
55 |
56 | override public var preferredStatusBarStyle: UIStatusBarStyle {
57 | statusBarStyle ?? super.preferredStatusBarStyle
58 | }
59 |
60 | /// Initialize with a single image
61 | ///
62 | /// - Parameters:
63 | /// - image: The image to present
64 | /// - background: The background configuration
65 | /// - dismissal: The dismiss configuration
66 | /// - overlayView: View to overlay the image (does not display with 'button' dismissals)
67 | /// - enableLiveText: Enables Live Text interaction, iOS 16 only
68 | public convenience init(
69 | image: UIImage,
70 | background: Background = .colored(.black),
71 | dismissal: Dismissal = .withPan(.standard),
72 | overlayView: AgrumeOverlayView? = nil,
73 | enableLiveText: Bool = false
74 | ) {
75 | self.init(
76 | images: [image],
77 | background: background,
78 | dismissal: dismissal,
79 | overlayView: overlayView,
80 | enableLiveText: enableLiveText
81 | )
82 | }
83 |
84 | /// Initialize with a single image url
85 | ///
86 | /// - Parameters:
87 | /// - url: The image url to present
88 | /// - background: The background configuration
89 | /// - dismissal: The dismiss configuration
90 | /// - overlayView: View to overlay the image (does not display with 'button' dismissals)
91 | /// - enableLiveText: Enables Live Text interaction, iOS 16 only
92 | public convenience init(
93 | url: URL,
94 | background: Background = .colored(.black),
95 | dismissal: Dismissal = .withPan(.standard),
96 | overlayView: AgrumeOverlayView? = nil,
97 | enableLiveText: Bool = false
98 | ) {
99 | self.init(
100 | urls: [url],
101 | background: background,
102 | dismissal: dismissal,
103 | overlayView: overlayView,
104 | enableLiveText: enableLiveText
105 | )
106 | }
107 |
108 | /// Initialize with a data source
109 | ///
110 | /// - Parameters:
111 | /// - dataSource: The `AgrumeDataSource` to use
112 | /// - startIndex: The optional start index when showing multiple images
113 | /// - background: The background configuration
114 | /// - dismissal: The dismiss configuration
115 | /// - overlayView: View to overlay the image (does not display with 'button' dismissals)
116 | /// - enableLiveText: Enables Live Text interaction, iOS 16 only
117 | public convenience init(
118 | dataSource: AgrumeDataSource,
119 | startIndex: Int = 0,
120 | background: Background = .colored(.black),
121 | dismissal: Dismissal = .withPan(.standard),
122 | overlayView: AgrumeOverlayView? = nil,
123 | enableLiveText: Bool = false
124 | ) {
125 | self.init(
126 | images: nil,
127 | dataSource: dataSource,
128 | startIndex: startIndex,
129 | background: background,
130 | dismissal: dismissal,
131 | overlayView: overlayView,
132 | enableLiveText: enableLiveText
133 | )
134 | }
135 |
136 | /// Initialize with an array of images
137 | ///
138 | /// - Parameters:
139 | /// - images: The images to present
140 | /// - startIndex: The optional start index when showing multiple images
141 | /// - background: The background configuration
142 | /// - dismissal: The dismiss configuration
143 | /// - overlayView: View to overlay the image (does not display with 'button' dismissals)
144 | /// - enableLiveText: Enables Live Text interaction, iOS 16 only
145 | public convenience init(
146 | images: [UIImage],
147 | startIndex: Int = 0,
148 | background: Background = .colored(.black),
149 | dismissal: Dismissal = .withPan(.standard),
150 | overlayView: AgrumeOverlayView? = nil,
151 | enableLiveText: Bool = false
152 | ) {
153 | self.init(
154 | images: images,
155 | urls: nil,
156 | startIndex: startIndex,
157 | background: background,
158 | dismissal: dismissal,
159 | overlayView: overlayView,
160 | enableLiveText: enableLiveText
161 | )
162 | }
163 |
164 | /// Initialize with an array of image urls
165 | ///
166 | /// - Parameters:
167 | /// - urls: The image urls to present
168 | /// - startIndex: The optional start index when showing multiple images
169 | /// - background: The background configuration
170 | /// - dismissal: The dismiss configuration
171 | /// - overlayView: View to overlay the image (does not display with 'button' dismissals)
172 | /// - enableLiveText: Enables Live Text interaction, iOS 16 only
173 | public convenience init(
174 | urls: [URL],
175 | startIndex: Int = 0,
176 | background: Background = .colored(.black),
177 | dismissal: Dismissal = .withPan(.standard),
178 | overlayView: AgrumeOverlayView? = nil,
179 | enableLiveText: Bool = false
180 | ) {
181 | self.init(
182 | images: nil,
183 | urls: urls,
184 | startIndex: startIndex,
185 | background: background,
186 | dismissal: dismissal,
187 | overlayView: overlayView,
188 | enableLiveText: enableLiveText
189 | )
190 | }
191 |
192 | private init(
193 | images: [UIImage]? = nil,
194 | urls: [URL]? = nil,
195 | dataSource: AgrumeDataSource? = nil,
196 | startIndex: Int,
197 | background: Background,
198 | dismissal: Dismissal,
199 | overlayView: AgrumeOverlayView? = nil,
200 | enableLiveText: Bool = false
201 | ) {
202 | switch (images, urls) {
203 | case (let images?, nil):
204 | self.images = images.map { AgrumeImage(image: $0) }
205 | case (_, let urls?):
206 | self.images = urls.map { AgrumeImage(url: $0) }
207 | default:
208 | assert(dataSource != nil, "No images or URLs passed. You must provide an AgrumeDataSource in that case.")
209 | }
210 |
211 | self.startIndex = startIndex
212 | self.currentIndex = startIndex
213 | self.background = background
214 | self.dismissal = dismissal
215 | self.enableLiveText = enableLiveText
216 | super.init(nibName: nil, bundle: nil)
217 |
218 | self.overlayView = overlayView
219 | self.dataSource = dataSource ?? self
220 |
221 | modalPresentationStyle = .custom
222 | modalPresentationCapturesStatusBarAppearance = true
223 | }
224 |
225 | deinit {
226 | downloadTask?.cancel()
227 | }
228 |
229 | @available(*, unavailable)
230 | required public init?(coder aDecoder: NSCoder) {
231 | fatalError("Not implemented")
232 | }
233 |
234 | private var _blurContainerView: UIView?
235 | private var blurContainerView: UIView {
236 | if _blurContainerView == nil {
237 | let blurContainerView = UIView()
238 | blurContainerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
239 | if case .colored(let color) = background {
240 | blurContainerView.backgroundColor = color
241 | } else {
242 | blurContainerView.backgroundColor = .clear
243 | }
244 | blurContainerView.frame = CGRect(origin: view.frame.origin, size: view.frame.size * 2)
245 | _blurContainerView = blurContainerView
246 | }
247 | return _blurContainerView!
248 | }
249 | private var _blurView: UIVisualEffectView?
250 | private var blurView: UIVisualEffectView {
251 | guard case .blurred(let style) = background, _blurView == nil else {
252 | return _blurView!
253 | }
254 | let blurView = UIVisualEffectView(effect: UIBlurEffect(style: style))
255 | blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
256 | blurView.frame = blurContainerView.bounds
257 | _blurView = blurView
258 | return _blurView!
259 | }
260 | private var _collectionView: UICollectionView?
261 | private var collectionView: UICollectionView {
262 | if _collectionView == nil {
263 | let layout = UICollectionViewFlowLayout()
264 | layout.minimumInteritemSpacing = 0
265 | layout.minimumLineSpacing = 0
266 | layout.scrollDirection = .horizontal
267 |
268 | let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
269 | collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
270 | collectionView.register(AgrumeCell.self)
271 | collectionView.dataSource = self
272 | collectionView.delegate = self
273 | collectionView.isPagingEnabled = true
274 | collectionView.backgroundColor = .clear
275 | collectionView.delaysContentTouches = false
276 | collectionView.showsHorizontalScrollIndicator = false
277 | if #available(iOS 11.0, *) {
278 | collectionView.contentInsetAdjustmentBehavior = .never
279 | }
280 | _collectionView = collectionView
281 | }
282 | return _collectionView!
283 | }
284 | private var _spinner: UIActivityIndicatorView?
285 | private var spinner: UIActivityIndicatorView {
286 | if _spinner == nil {
287 | let indicatorStyle: UIActivityIndicatorView.Style
288 | switch background {
289 | case let .blurred(style):
290 | indicatorStyle = style == .dark ? .large : .medium
291 | case let .colored(color):
292 | indicatorStyle = color.isLight ? .medium : .large
293 | }
294 | let spinner = UIActivityIndicatorView(style: indicatorStyle)
295 | spinner.center = view.center
296 | spinner.startAnimating()
297 | spinner.alpha = 0
298 | _spinner = spinner
299 | }
300 | return _spinner!
301 | }
302 | // Container for the collection view. Fixes an RTL display bug
303 | private lazy var containerView = with(UIView(frame: view.bounds)) { containerView in
304 | containerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
305 | }
306 |
307 | private var downloadTask: URLSessionDataTask?
308 |
309 | /// Present Agrume
310 | ///
311 | /// - Parameters:
312 | /// - viewController: The UIViewController to present from
313 | public func show(from viewController: UIViewController) {
314 | view.isUserInteractionEnabled = false
315 | addSubviews()
316 | present(from: viewController)
317 | }
318 |
319 | /// Update image at index
320 | /// - Parameters:
321 | /// - index: The target index
322 | /// - image: The replacement UIImage
323 | /// - newTitle: The new title, if nil then no change
324 | public func updateImage(at index: Int, with image: UIImage, newTitle: NSAttributedString? = nil) {
325 | assert(images.count > index)
326 | let replacement = with(images[index]) {
327 | $0.url = nil
328 | $0.image = image
329 | if let newTitle {
330 | $0.title = newTitle
331 | }
332 | }
333 |
334 | markAsUpdatingSameCell(at: index)
335 | images[index] = replacement
336 | reload()
337 | }
338 |
339 | /// Update image at a specific index
340 | /// - Parameters:
341 | /// - index: The target index
342 | /// - url: The replacement URL
343 | /// - newTitle: The new title, if nil then no change
344 | public func updateImage(at index: Int, with url: URL, newTitle: NSAttributedString? = nil) {
345 | assert(images.count > index)
346 | let replacement = with(images[index]) {
347 | $0.image = nil
348 | $0.url = url
349 | if let newTitle {
350 | $0.title = newTitle
351 | }
352 | }
353 |
354 | markAsUpdatingSameCell(at: index)
355 | images[index] = replacement
356 | reload()
357 | }
358 |
359 | private func markAsUpdatingSameCell(at index: Int) {
360 | collectionView.visibleCells.forEach { cell in
361 | if let cell = cell as? AgrumeCell, cell.index == index {
362 | cell.updatingImageOnSameCell = true
363 | }
364 | }
365 | }
366 |
367 | override public func viewDidLoad() {
368 | super.viewDidLoad()
369 | addSubviews()
370 |
371 | if onLongPress != nil {
372 | let longPress = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress(_:)))
373 | view.addGestureRecognizer(longPress)
374 | }
375 | }
376 |
377 | @objc
378 | func didLongPress(_ gesture: UIGestureRecognizer) {
379 | guard case .began = gesture.state else {
380 | return
381 | }
382 | fetchImage(forIndex: currentIndex) { [weak self] image in
383 | guard let self else {
384 | return
385 | }
386 | self.onLongPress?(image, self)
387 | }
388 | }
389 |
390 | public func addSubviews() {
391 | view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
392 |
393 | if case .blurred = background {
394 | blurContainerView.addSubview(blurView)
395 | }
396 | view.addSubview(blurContainerView)
397 | view.addSubview(containerView)
398 | containerView.addSubview(collectionView)
399 | view.addSubview(spinner)
400 | }
401 |
402 | private func present(from viewController: UIViewController) {
403 | DispatchQueue.main.async {
404 | self.blurContainerView.alpha = 1
405 | self.containerView.alpha = 0
406 | let scale: CGFloat = .initialScaleToExpandFrom
407 |
408 | viewController.present(self, animated: false) {
409 | // Transform the container view, not the collection view to prevent an RTL display bug
410 | self.containerView.transform = CGAffineTransform(scaleX: scale, y: scale)
411 |
412 | UIView.animate(
413 | withDuration: .transitionAnimationDuration,
414 | delay: 0,
415 | options: .beginFromCurrentState,
416 | animations: {
417 | self.containerView.alpha = 1
418 | self.containerView.transform = .identity
419 | self.addOverlayView()
420 | },
421 | completion: { _ in
422 | self.view.isUserInteractionEnabled = true
423 | }
424 | )
425 | }
426 | }
427 | }
428 |
429 | public func addOverlayView() {
430 | switch (dismissal, overlayView) {
431 | case let (.withButton(button), _), let (.withPanAndButton(_, button), _):
432 | let overlayView = AgrumeCloseButtonOverlayView(closeButton: button)
433 | overlayView.delegate = self
434 | overlayView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
435 | overlayView.frame = view.bounds
436 | view.addSubview(overlayView)
437 | self.overlayView = overlayView
438 | case (.withPan, let overlayView?):
439 | overlayView.alpha = 1
440 | overlayView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
441 | overlayView.frame = view.bounds
442 | view.addSubview(overlayView)
443 | default:
444 | break
445 | }
446 | }
447 |
448 | private func viewControllerForSnapshot(fromViewController viewController: UIViewController) -> UIViewController? {
449 | var presentingVC = viewController.view.window?.rootViewController
450 | while presentingVC?.presentedViewController != nil {
451 | presentingVC = presentingVC?.presentedViewController
452 | }
453 | return presentingVC
454 | }
455 |
456 | public override var keyCommands: [UIKeyCommand]? {
457 | return [
458 | UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(escPressed))
459 | ]
460 | }
461 |
462 | @objc
463 | func escPressed() {
464 | dismiss()
465 | }
466 |
467 | public func dismiss() {
468 | dismissAfterFlick()
469 | }
470 |
471 | public func showImage(atIndex index: Int, animated: Bool = true) {
472 | scrollToImage(atIndex: index, animated: animated)
473 | }
474 |
475 | public func reload() {
476 | DispatchQueue.main.async {
477 | self.collectionView.reloadData()
478 | }
479 | }
480 |
481 | override public var prefersStatusBarHidden: Bool {
482 | hideStatusBar
483 | }
484 |
485 | private func scrollToImage(atIndex index: Int, animated: Bool = false) {
486 | collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: animated)
487 | }
488 |
489 | private func currentlyVisibleCellIndex() -> Int {
490 | let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
491 | let visiblePoint = CGPoint(x: visibleRect.minX, y: visibleRect.minY)
492 | return collectionView.indexPathForItem(at: visiblePoint)?.item ?? startIndex
493 | }
494 |
495 | override public func viewWillLayoutSubviews() {
496 | super.viewWillLayoutSubviews()
497 | let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
498 | layout.itemSize = view.bounds.size
499 | layout.invalidateLayout()
500 |
501 | spinner.center = view.center
502 |
503 | if currentIndex != currentlyVisibleCellIndex() {
504 | scrollToImage(atIndex: currentIndex)
505 | }
506 | }
507 |
508 | override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
509 | let indexToRotate = currentIndex
510 | let rotationHandler: ((UIViewControllerTransitionCoordinatorContext) -> Void) = { _ in
511 | self.scrollToImage(atIndex: indexToRotate)
512 | self.collectionView.visibleCells.forEach { cell in
513 | (cell as! AgrumeCell).recenterDuringRotation(size: size)
514 | }
515 | }
516 | coordinator.animate(alongsideTransition: rotationHandler, completion: rotationHandler)
517 | super.viewWillTransition(to: size, with: coordinator)
518 | }
519 | }
520 |
521 | extension Agrume: AgrumeDataSource {
522 |
523 | public var numberOfImages: Int {
524 | images.count
525 | }
526 |
527 | public func image(forIndex index: Int, completion: @escaping (UIImage?) -> Void) {
528 | let downloadHandler = download ?? AgrumeServiceLocator.shared.downloadHandler
529 | if let handler = downloadHandler, let url = images[index].url {
530 | handler(url, completion)
531 | } else if let url = images[index].url {
532 | downloadTask = ImageDownloader.downloadImage(url, completion: completion)
533 | } else {
534 | completion(images[index].image)
535 | }
536 | }
537 |
538 | }
539 |
540 | extension Agrume: UICollectionViewDataSource {
541 |
542 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
543 | dataSource?.numberOfImages ?? 0
544 | }
545 |
546 | public func collectionView(
547 | _ collectionView: UICollectionView,
548 | cellForItemAt indexPath: IndexPath
549 | ) -> UICollectionViewCell {
550 | let cell: AgrumeCell = collectionView.dequeue(indexPath: indexPath)
551 |
552 | cell.enableLiveText = enableLiveText
553 | cell.tapBehavior = tapBehavior
554 | switch dismissal {
555 | case .withPan(let physics), .withPanAndButton(let physics, _):
556 | cell.panPhysics = physics
557 | case .withButton:
558 | cell.panPhysics = nil
559 | // Backward compatibility
560 | case .withPhysics, .withPhysicsAndButton:
561 | cell.panPhysics = .standard
562 | }
563 |
564 | spinner.alpha = 1
565 | fetchImage(forIndex: indexPath.item) { [weak self] image in
566 | cell.index = indexPath.item
567 | cell.image = image
568 | self?.spinner.alpha = 0
569 | }
570 | // Only allow panning if horizontal swiping fails. Horizontal swiping is only active for zoomed in images
571 | collectionView.panGestureRecognizer.require(toFail: cell.swipeGesture)
572 | cell.delegate = self
573 | return cell
574 | }
575 |
576 | private func fetchImage(forIndex index: Int, handler: @escaping (UIImage?) -> Void) {
577 | dataSource?.image(forIndex: index) { image in
578 | DispatchQueue.main.async {
579 | handler(image)
580 | }
581 | }
582 | }
583 |
584 | }
585 |
586 | extension Agrume: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
587 | public func collectionView(_ collectionView: UICollectionView,
588 | layout collectionViewLayout: UICollectionViewLayout,
589 | insetForSectionAt section: Int) -> UIEdgeInsets {
590 | // Center cells horizontally
591 | let cellWidth = view.bounds.width
592 | let totalWidth = cellWidth * CGFloat(dataSource?.numberOfImages ?? 0)
593 | let leftRightEdgeInset = max(0, (collectionView.bounds.width - totalWidth) / 2)
594 | return UIEdgeInsets(top: 0, left: leftRightEdgeInset, bottom: 0, right: leftRightEdgeInset)
595 | }
596 |
597 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
598 | didScroll?(currentlyVisibleCellIndex())
599 | }
600 |
601 | public func scrollViewDidScroll(_ scrollView: UIScrollView) {
602 | let center = CGPoint(x: scrollView.contentOffset.x + (scrollView.frame.width / 2), y: (scrollView.frame.height / 2))
603 | if let indexPath = collectionView.indexPathForItem(at: center) {
604 | currentIndex = indexPath.row
605 | }
606 | }
607 |
608 | }
609 |
610 | extension Agrume: AgrumeCellDelegate {
611 |
612 | var isSingleImageMode: Bool {
613 | dataSource?.numberOfImages == 1
614 | }
615 |
616 | var presentingController: UIViewController {
617 | self
618 | }
619 |
620 | private func dismissCompletion(_ finished: Bool) {
621 | presentingViewController?.dismiss(animated: false) { [unowned self] in
622 | self.cleanup()
623 | self.didDismiss?()
624 | }
625 | }
626 |
627 | private func cleanup() {
628 | _blurContainerView?.removeFromSuperview()
629 | _blurContainerView = nil
630 | _blurView = nil
631 | _collectionView?.visibleCells.forEach { cell in
632 | (cell as? AgrumeCell)?.cleanup()
633 | }
634 | _collectionView?.removeFromSuperview()
635 | _collectionView = nil
636 | _spinner?.removeFromSuperview()
637 | _spinner = nil
638 | }
639 |
640 | func dismissAfterFlick() {
641 | self.willDismiss?()
642 | UIView.animate(
643 | withDuration: .transitionAnimationDuration,
644 | delay: 0,
645 | options: .beginFromCurrentState,
646 | animations: {
647 | self.collectionView.alpha = 0
648 | self.blurContainerView.alpha = 0
649 | self.overlayView?.alpha = 0
650 | },
651 | completion: dismissCompletion
652 | )
653 | }
654 |
655 | func dismissAfterTap() {
656 | view.isUserInteractionEnabled = false
657 |
658 | self.willDismiss?()
659 | UIView.animate(
660 | withDuration: .transitionAnimationDuration,
661 | delay: 0,
662 | options: .beginFromCurrentState,
663 | animations: {
664 | self.collectionView.alpha = 0
665 | self.blurContainerView.alpha = 0
666 | self.overlayView?.alpha = 0
667 | let scale: CGFloat = .maxScaleForExpandingOffscreen
668 | self.collectionView.transform = CGAffineTransform(scaleX: scale, y: scale)
669 | },
670 | completion: dismissCompletion
671 | )
672 | }
673 |
674 | func toggleOverlayVisibility() {
675 | UIView.animate(
676 | withDuration: .transitionAnimationDuration,
677 | delay: 0,
678 | options: .beginFromCurrentState,
679 | animations: {
680 | if let overlayView = self.overlayView {
681 | overlayView.alpha = overlayView.alpha < 0.5 ? 1 : 0
682 | }
683 | },
684 | completion: nil
685 | )
686 | }
687 | }
688 |
689 | extension Agrume: AgrumeCloseButtonOverlayViewDelegate {
690 |
691 | func agrumeOverlayViewWantsToClose(_ view: AgrumeCloseButtonOverlayView) {
692 | dismiss()
693 | }
694 |
695 | }
696 |
--------------------------------------------------------------------------------
/Agrume.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 54;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 833C23CA23CC800800F689E2 /* AgrumePhotoLibraryHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C23C923CC800800F689E2 /* AgrumePhotoLibraryHelper.swift */; };
11 | 94318E541D32612D0096215A /* AgrumeServiceLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94318E531D32612D0096215A /* AgrumeServiceLocator.swift */; };
12 | F224A7392783301200A8F5ED /* AgrumeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F224A7362783301200A8F5ED /* AgrumeView.swift */; };
13 | F2539BA120F22E7700062C80 /* AgrumeOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2539BA020F22E7700062C80 /* AgrumeOverlayView.swift */; };
14 | F2609E23209F047200E0E93D /* AgrumeDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2609E22209F047200E0E93D /* AgrumeDataSource.swift */; };
15 | F2609E26209F06F800E0E93D /* AgrumeImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2609E25209F06F800E0E93D /* AgrumeImage.swift */; };
16 | F2609E28209F2BC600E0E93D /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2609E27209F2BC600E0E93D /* Configuration.swift */; };
17 | F2609E2A209F2E0200E0E93D /* UIKit+Agrume.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2609E29209F2E0200E0E93D /* UIKit+Agrume.swift */; };
18 | F2A51FF41B10E00700924912 /* Agrume.h in Headers */ = {isa = PBXBuildFile; fileRef = F2A51FF31B10E00700924912 /* Agrume.h */; settings = {ATTRIBUTES = (Public, ); }; };
19 | F2A51FFA1B10E00700924912 /* Agrume.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F2A51FEE1B10E00700924912 /* Agrume.framework */; };
20 | F2A5200B1B10E29B00924912 /* Agrume.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2A5200A1B10E29B00924912 /* Agrume.swift */; };
21 | F2D9598C1B1A108800073772 /* AgrumeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D9598B1B1A108800073772 /* AgrumeCell.swift */; };
22 | F2DC79D61B170C4B00818A8C /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2DC79D51B170C4B00818A8C /* ImageDownloader.swift */; };
23 | F2EE29AE209F31B800F281A2 /* Foundation+Agrume.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2EE29AD209F31B800F281A2 /* Foundation+Agrume.swift */; };
24 | F2F24F9423BB3BBE005AA731 /* With.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2F24F9323BB3BBE005AA731 /* With.swift */; };
25 | F2FA5E7F2288602F009C0DA0 /* SwiftyGif.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F2FA5E7E22885FFB009C0DA0 /* SwiftyGif.framework */; };
26 | /* End PBXBuildFile section */
27 |
28 | /* Begin PBXContainerItemProxy section */
29 | F2A51FFB1B10E00700924912 /* PBXContainerItemProxy */ = {
30 | isa = PBXContainerItemProxy;
31 | containerPortal = F2A51FE51B10E00700924912 /* Project object */;
32 | proxyType = 1;
33 | remoteGlobalIDString = F2A51FED1B10E00700924912;
34 | remoteInfo = Agrume;
35 | };
36 | F2FA5E7B22885FFB009C0DA0 /* PBXContainerItemProxy */ = {
37 | isa = PBXContainerItemProxy;
38 | containerPortal = F2FA5E7622885FFB009C0DA0 /* SwiftyGif.xcodeproj */;
39 | proxyType = 2;
40 | remoteGlobalIDString = FA29E9321CA9340E00E579D5;
41 | remoteInfo = SwiftyGifExample;
42 | };
43 | F2FA5E7D22885FFB009C0DA0 /* PBXContainerItemProxy */ = {
44 | isa = PBXContainerItemProxy;
45 | containerPortal = F2FA5E7622885FFB009C0DA0 /* SwiftyGif.xcodeproj */;
46 | proxyType = 2;
47 | remoteGlobalIDString = 3B18BAF41E289899009C125A;
48 | remoteInfo = SwiftyGif;
49 | };
50 | /* End PBXContainerItemProxy section */
51 |
52 | /* Begin PBXFileReference section */
53 | 833C23C923CC800800F689E2 /* AgrumePhotoLibraryHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgrumePhotoLibraryHelper.swift; sourceTree = ""; };
54 | 94318E531D32612D0096215A /* AgrumeServiceLocator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgrumeServiceLocator.swift; sourceTree = ""; };
55 | F224A7362783301200A8F5ED /* AgrumeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgrumeView.swift; sourceTree = ""; };
56 | F2539BA020F22E7700062C80 /* AgrumeOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgrumeOverlayView.swift; sourceTree = ""; };
57 | F2609E22209F047200E0E93D /* AgrumeDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgrumeDataSource.swift; sourceTree = ""; };
58 | F2609E25209F06F800E0E93D /* AgrumeImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgrumeImage.swift; sourceTree = ""; };
59 | F2609E27209F2BC600E0E93D /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; };
60 | F2609E29209F2E0200E0E93D /* UIKit+Agrume.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIKit+Agrume.swift"; sourceTree = ""; };
61 | F2A51FEE1B10E00700924912 /* Agrume.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Agrume.framework; sourceTree = BUILT_PRODUCTS_DIR; };
62 | F2A51FF21B10E00700924912 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
63 | F2A51FF31B10E00700924912 /* Agrume.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Agrume.h; sourceTree = ""; };
64 | F2A51FF91B10E00700924912 /* AgrumeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AgrumeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
65 | F2A51FFF1B10E00700924912 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
66 | F2A5200A1B10E29B00924912 /* Agrume.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Agrume.swift; sourceTree = ""; };
67 | F2D9598B1B1A108800073772 /* AgrumeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgrumeCell.swift; sourceTree = ""; };
68 | F2DC79D51B170C4B00818A8C /* ImageDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = ""; };
69 | F2EE29AD209F31B800F281A2 /* Foundation+Agrume.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Foundation+Agrume.swift"; sourceTree = ""; };
70 | F2F24F9323BB3BBE005AA731 /* With.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = With.swift; sourceTree = ""; };
71 | F2FA5E7622885FFB009C0DA0 /* SwiftyGif.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SwiftyGif.xcodeproj; path = Frameworks/SwiftyGif/SwiftyGif.xcodeproj; sourceTree = ""; };
72 | /* End PBXFileReference section */
73 |
74 | /* Begin PBXFrameworksBuildPhase section */
75 | F2A51FEA1B10E00700924912 /* Frameworks */ = {
76 | isa = PBXFrameworksBuildPhase;
77 | buildActionMask = 2147483647;
78 | files = (
79 | F2FA5E7F2288602F009C0DA0 /* SwiftyGif.framework in Frameworks */,
80 | );
81 | runOnlyForDeploymentPostprocessing = 0;
82 | };
83 | F2A51FF61B10E00700924912 /* Frameworks */ = {
84 | isa = PBXFrameworksBuildPhase;
85 | buildActionMask = 2147483647;
86 | files = (
87 | F2A51FFA1B10E00700924912 /* Agrume.framework in Frameworks */,
88 | );
89 | runOnlyForDeploymentPostprocessing = 0;
90 | };
91 | /* End PBXFrameworksBuildPhase section */
92 |
93 | /* Begin PBXGroup section */
94 | F2539B9A20F22D6F00062C80 /* Frameworks */ = {
95 | isa = PBXGroup;
96 | children = (
97 | );
98 | name = Frameworks;
99 | sourceTree = "";
100 | };
101 | F2A51FE41B10E00700924912 = {
102 | isa = PBXGroup;
103 | children = (
104 | F2FA5E7622885FFB009C0DA0 /* SwiftyGif.xcodeproj */,
105 | F2A51FF01B10E00700924912 /* Agrume */,
106 | F2A51FFD1B10E00700924912 /* AgrumeTests */,
107 | F2A51FEF1B10E00700924912 /* Products */,
108 | F2539B9A20F22D6F00062C80 /* Frameworks */,
109 | );
110 | indentWidth = 2;
111 | sourceTree = "";
112 | tabWidth = 2;
113 | };
114 | F2A51FEF1B10E00700924912 /* Products */ = {
115 | isa = PBXGroup;
116 | children = (
117 | F2A51FEE1B10E00700924912 /* Agrume.framework */,
118 | F2A51FF91B10E00700924912 /* AgrumeTests.xctest */,
119 | );
120 | name = Products;
121 | sourceTree = "";
122 | };
123 | F2A51FF01B10E00700924912 /* Agrume */ = {
124 | isa = PBXGroup;
125 | children = (
126 | F2A51FF31B10E00700924912 /* Agrume.h */,
127 | F2A5200A1B10E29B00924912 /* Agrume.swift */,
128 | F2D9598B1B1A108800073772 /* AgrumeCell.swift */,
129 | F2609E22209F047200E0E93D /* AgrumeDataSource.swift */,
130 | F2609E25209F06F800E0E93D /* AgrumeImage.swift */,
131 | F2539BA020F22E7700062C80 /* AgrumeOverlayView.swift */,
132 | 94318E531D32612D0096215A /* AgrumeServiceLocator.swift */,
133 | F224A7362783301200A8F5ED /* AgrumeView.swift */,
134 | F2609E27209F2BC600E0E93D /* Configuration.swift */,
135 | F2EE29AD209F31B800F281A2 /* Foundation+Agrume.swift */,
136 | 833C23C923CC800800F689E2 /* AgrumePhotoLibraryHelper.swift */,
137 | F2DC79D51B170C4B00818A8C /* ImageDownloader.swift */,
138 | F2A51FF11B10E00700924912 /* Supporting Files */,
139 | F2609E29209F2E0200E0E93D /* UIKit+Agrume.swift */,
140 | F2F24F9323BB3BBE005AA731 /* With.swift */,
141 | );
142 | path = Agrume;
143 | sourceTree = "";
144 | };
145 | F2A51FF11B10E00700924912 /* Supporting Files */ = {
146 | isa = PBXGroup;
147 | children = (
148 | F2A51FF21B10E00700924912 /* Info.plist */,
149 | );
150 | name = "Supporting Files";
151 | sourceTree = "";
152 | };
153 | F2A51FFD1B10E00700924912 /* AgrumeTests */ = {
154 | isa = PBXGroup;
155 | children = (
156 | F2A51FFE1B10E00700924912 /* Supporting Files */,
157 | );
158 | path = AgrumeTests;
159 | sourceTree = "";
160 | };
161 | F2A51FFE1B10E00700924912 /* Supporting Files */ = {
162 | isa = PBXGroup;
163 | children = (
164 | F2A51FFF1B10E00700924912 /* Info.plist */,
165 | );
166 | name = "Supporting Files";
167 | sourceTree = "";
168 | };
169 | F2FA5E7722885FFB009C0DA0 /* Products */ = {
170 | isa = PBXGroup;
171 | children = (
172 | F2FA5E7C22885FFB009C0DA0 /* SwiftyGifExample.app */,
173 | F2FA5E7E22885FFB009C0DA0 /* SwiftyGif.framework */,
174 | );
175 | name = Products;
176 | sourceTree = "";
177 | };
178 | /* End PBXGroup section */
179 |
180 | /* Begin PBXHeadersBuildPhase section */
181 | F2A51FEB1B10E00700924912 /* Headers */ = {
182 | isa = PBXHeadersBuildPhase;
183 | buildActionMask = 2147483647;
184 | files = (
185 | F2A51FF41B10E00700924912 /* Agrume.h in Headers */,
186 | );
187 | runOnlyForDeploymentPostprocessing = 0;
188 | };
189 | /* End PBXHeadersBuildPhase section */
190 |
191 | /* Begin PBXNativeTarget section */
192 | F2A51FED1B10E00700924912 /* Agrume */ = {
193 | isa = PBXNativeTarget;
194 | buildConfigurationList = F2A520041B10E00700924912 /* Build configuration list for PBXNativeTarget "Agrume" */;
195 | buildPhases = (
196 | F2A51FE91B10E00700924912 /* Sources */,
197 | F2A51FEA1B10E00700924912 /* Frameworks */,
198 | F2A51FEB1B10E00700924912 /* Headers */,
199 | F2A51FEC1B10E00700924912 /* Resources */,
200 | 77CAF6741FADFD45000C0929 /* Run SwiftLint */,
201 | );
202 | buildRules = (
203 | );
204 | dependencies = (
205 | );
206 | name = Agrume;
207 | productName = Agrume;
208 | productReference = F2A51FEE1B10E00700924912 /* Agrume.framework */;
209 | productType = "com.apple.product-type.framework";
210 | };
211 | F2A51FF81B10E00700924912 /* AgrumeTests */ = {
212 | isa = PBXNativeTarget;
213 | buildConfigurationList = F2A520071B10E00700924912 /* Build configuration list for PBXNativeTarget "AgrumeTests" */;
214 | buildPhases = (
215 | F2A51FF51B10E00700924912 /* Sources */,
216 | F2A51FF61B10E00700924912 /* Frameworks */,
217 | F2A51FF71B10E00700924912 /* Resources */,
218 | );
219 | buildRules = (
220 | );
221 | dependencies = (
222 | F2A51FFC1B10E00700924912 /* PBXTargetDependency */,
223 | );
224 | name = AgrumeTests;
225 | productName = AgrumeTests;
226 | productReference = F2A51FF91B10E00700924912 /* AgrumeTests.xctest */;
227 | productType = "com.apple.product-type.bundle.unit-test";
228 | };
229 | /* End PBXNativeTarget section */
230 |
231 | /* Begin PBXProject section */
232 | F2A51FE51B10E00700924912 /* Project object */ = {
233 | isa = PBXProject;
234 | attributes = {
235 | LastSwiftMigration = 0700;
236 | LastSwiftUpdateCheck = 0730;
237 | LastUpgradeCheck = 1300;
238 | ORGANIZATIONNAME = Schnaub;
239 | TargetAttributes = {
240 | F2A51FED1B10E00700924912 = {
241 | CreatedOnToolsVersion = 6.3.2;
242 | LastSwiftMigration = 1020;
243 | };
244 | F2A51FF81B10E00700924912 = {
245 | CreatedOnToolsVersion = 6.3.2;
246 | LastSwiftMigration = 0900;
247 | };
248 | };
249 | };
250 | buildConfigurationList = F2A51FE81B10E00700924912 /* Build configuration list for PBXProject "Agrume" */;
251 | compatibilityVersion = "Xcode 3.2";
252 | developmentRegion = en;
253 | hasScannedForEncodings = 0;
254 | knownRegions = (
255 | en,
256 | Base,
257 | );
258 | mainGroup = F2A51FE41B10E00700924912;
259 | productRefGroup = F2A51FEF1B10E00700924912 /* Products */;
260 | projectDirPath = "";
261 | projectReferences = (
262 | {
263 | ProductGroup = F2FA5E7722885FFB009C0DA0 /* Products */;
264 | ProjectRef = F2FA5E7622885FFB009C0DA0 /* SwiftyGif.xcodeproj */;
265 | },
266 | );
267 | projectRoot = "";
268 | targets = (
269 | F2A51FED1B10E00700924912 /* Agrume */,
270 | F2A51FF81B10E00700924912 /* AgrumeTests */,
271 | );
272 | };
273 | /* End PBXProject section */
274 |
275 | /* Begin PBXReferenceProxy section */
276 | F2FA5E7C22885FFB009C0DA0 /* SwiftyGifExample.app */ = {
277 | isa = PBXReferenceProxy;
278 | fileType = wrapper.application;
279 | path = SwiftyGifExample.app;
280 | remoteRef = F2FA5E7B22885FFB009C0DA0 /* PBXContainerItemProxy */;
281 | sourceTree = BUILT_PRODUCTS_DIR;
282 | };
283 | F2FA5E7E22885FFB009C0DA0 /* SwiftyGif.framework */ = {
284 | isa = PBXReferenceProxy;
285 | fileType = wrapper.framework;
286 | path = SwiftyGif.framework;
287 | remoteRef = F2FA5E7D22885FFB009C0DA0 /* PBXContainerItemProxy */;
288 | sourceTree = BUILT_PRODUCTS_DIR;
289 | };
290 | /* End PBXReferenceProxy section */
291 |
292 | /* Begin PBXResourcesBuildPhase section */
293 | F2A51FEC1B10E00700924912 /* Resources */ = {
294 | isa = PBXResourcesBuildPhase;
295 | buildActionMask = 2147483647;
296 | files = (
297 | );
298 | runOnlyForDeploymentPostprocessing = 0;
299 | };
300 | F2A51FF71B10E00700924912 /* Resources */ = {
301 | isa = PBXResourcesBuildPhase;
302 | buildActionMask = 2147483647;
303 | files = (
304 | );
305 | runOnlyForDeploymentPostprocessing = 0;
306 | };
307 | /* End PBXResourcesBuildPhase section */
308 |
309 | /* Begin PBXShellScriptBuildPhase section */
310 | 77CAF6741FADFD45000C0929 /* Run SwiftLint */ = {
311 | isa = PBXShellScriptBuildPhase;
312 | alwaysOutOfDate = 1;
313 | buildActionMask = 2147483647;
314 | files = (
315 | );
316 | inputPaths = (
317 | );
318 | name = "Run SwiftLint";
319 | outputPaths = (
320 | );
321 | runOnlyForDeploymentPostprocessing = 0;
322 | shellPath = /bin/sh;
323 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nfi\n";
324 | };
325 | /* End PBXShellScriptBuildPhase section */
326 |
327 | /* Begin PBXSourcesBuildPhase section */
328 | F2A51FE91B10E00700924912 /* Sources */ = {
329 | isa = PBXSourcesBuildPhase;
330 | buildActionMask = 2147483647;
331 | files = (
332 | F2D9598C1B1A108800073772 /* AgrumeCell.swift in Sources */,
333 | F2609E26209F06F800E0E93D /* AgrumeImage.swift in Sources */,
334 | F2609E2A209F2E0200E0E93D /* UIKit+Agrume.swift in Sources */,
335 | F2609E28209F2BC600E0E93D /* Configuration.swift in Sources */,
336 | F2EE29AE209F31B800F281A2 /* Foundation+Agrume.swift in Sources */,
337 | 833C23CA23CC800800F689E2 /* AgrumePhotoLibraryHelper.swift in Sources */,
338 | F2F24F9423BB3BBE005AA731 /* With.swift in Sources */,
339 | F2DC79D61B170C4B00818A8C /* ImageDownloader.swift in Sources */,
340 | F2539BA120F22E7700062C80 /* AgrumeOverlayView.swift in Sources */,
341 | F2609E23209F047200E0E93D /* AgrumeDataSource.swift in Sources */,
342 | F224A7392783301200A8F5ED /* AgrumeView.swift in Sources */,
343 | F2A5200B1B10E29B00924912 /* Agrume.swift in Sources */,
344 | 94318E541D32612D0096215A /* AgrumeServiceLocator.swift in Sources */,
345 | );
346 | runOnlyForDeploymentPostprocessing = 0;
347 | };
348 | F2A51FF51B10E00700924912 /* Sources */ = {
349 | isa = PBXSourcesBuildPhase;
350 | buildActionMask = 2147483647;
351 | files = (
352 | );
353 | runOnlyForDeploymentPostprocessing = 0;
354 | };
355 | /* End PBXSourcesBuildPhase section */
356 |
357 | /* Begin PBXTargetDependency section */
358 | F2A51FFC1B10E00700924912 /* PBXTargetDependency */ = {
359 | isa = PBXTargetDependency;
360 | target = F2A51FED1B10E00700924912 /* Agrume */;
361 | targetProxy = F2A51FFB1B10E00700924912 /* PBXContainerItemProxy */;
362 | };
363 | /* End PBXTargetDependency section */
364 |
365 | /* Begin XCBuildConfiguration section */
366 | F2A520021B10E00700924912 /* Debug */ = {
367 | isa = XCBuildConfiguration;
368 | buildSettings = {
369 | ALWAYS_SEARCH_USER_PATHS = NO;
370 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
371 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
372 | CLANG_CXX_LIBRARY = "libc++";
373 | CLANG_ENABLE_MODULES = YES;
374 | CLANG_ENABLE_OBJC_ARC = YES;
375 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
376 | CLANG_WARN_BOOL_CONVERSION = YES;
377 | CLANG_WARN_COMMA = YES;
378 | CLANG_WARN_CONSTANT_CONVERSION = YES;
379 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
380 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
381 | CLANG_WARN_EMPTY_BODY = YES;
382 | CLANG_WARN_ENUM_CONVERSION = YES;
383 | CLANG_WARN_INFINITE_RECURSION = YES;
384 | CLANG_WARN_INT_CONVERSION = YES;
385 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
386 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
387 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
388 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
389 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
390 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
391 | CLANG_WARN_STRICT_PROTOTYPES = YES;
392 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
393 | CLANG_WARN_UNREACHABLE_CODE = YES;
394 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
395 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
396 | COPY_PHASE_STRIP = NO;
397 | CURRENT_PROJECT_VERSION = 1;
398 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
399 | ENABLE_STRICT_OBJC_MSGSEND = YES;
400 | ENABLE_TESTABILITY = YES;
401 | GCC_C_LANGUAGE_STANDARD = gnu99;
402 | GCC_DYNAMIC_NO_PIC = NO;
403 | GCC_NO_COMMON_BLOCKS = YES;
404 | GCC_OPTIMIZATION_LEVEL = 0;
405 | GCC_PREPROCESSOR_DEFINITIONS = (
406 | "DEBUG=1",
407 | "$(inherited)",
408 | );
409 | GCC_SYMBOLS_PRIVATE_EXTERN = NO;
410 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
411 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
412 | GCC_WARN_UNDECLARED_SELECTOR = YES;
413 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
414 | GCC_WARN_UNUSED_FUNCTION = YES;
415 | GCC_WARN_UNUSED_VARIABLE = YES;
416 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
417 | MTL_ENABLE_DEBUG_INFO = YES;
418 | ONLY_ACTIVE_ARCH = YES;
419 | SDKROOT = iphoneos;
420 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
421 | TARGETED_DEVICE_FAMILY = "1,2";
422 | VERSIONING_SYSTEM = "apple-generic";
423 | VERSION_INFO_PREFIX = "";
424 | };
425 | name = Debug;
426 | };
427 | F2A520031B10E00700924912 /* Release */ = {
428 | isa = XCBuildConfiguration;
429 | buildSettings = {
430 | ALWAYS_SEARCH_USER_PATHS = NO;
431 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
432 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
433 | CLANG_CXX_LIBRARY = "libc++";
434 | CLANG_ENABLE_MODULES = YES;
435 | CLANG_ENABLE_OBJC_ARC = YES;
436 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
437 | CLANG_WARN_BOOL_CONVERSION = YES;
438 | CLANG_WARN_COMMA = YES;
439 | CLANG_WARN_CONSTANT_CONVERSION = YES;
440 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
441 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
442 | CLANG_WARN_EMPTY_BODY = YES;
443 | CLANG_WARN_ENUM_CONVERSION = YES;
444 | CLANG_WARN_INFINITE_RECURSION = YES;
445 | CLANG_WARN_INT_CONVERSION = YES;
446 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
447 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
448 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
449 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
450 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
451 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
452 | CLANG_WARN_STRICT_PROTOTYPES = YES;
453 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
454 | CLANG_WARN_UNREACHABLE_CODE = YES;
455 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
456 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
457 | COPY_PHASE_STRIP = NO;
458 | CURRENT_PROJECT_VERSION = 1;
459 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
460 | ENABLE_NS_ASSERTIONS = NO;
461 | ENABLE_STRICT_OBJC_MSGSEND = YES;
462 | GCC_C_LANGUAGE_STANDARD = gnu99;
463 | GCC_NO_COMMON_BLOCKS = YES;
464 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
465 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
466 | GCC_WARN_UNDECLARED_SELECTOR = YES;
467 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
468 | GCC_WARN_UNUSED_FUNCTION = YES;
469 | GCC_WARN_UNUSED_VARIABLE = YES;
470 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
471 | MTL_ENABLE_DEBUG_INFO = NO;
472 | SDKROOT = iphoneos;
473 | SWIFT_COMPILATION_MODE = wholemodule;
474 | SWIFT_OPTIMIZATION_LEVEL = "-O";
475 | TARGETED_DEVICE_FAMILY = "1,2";
476 | VALIDATE_PRODUCT = YES;
477 | VERSIONING_SYSTEM = "apple-generic";
478 | VERSION_INFO_PREFIX = "";
479 | };
480 | name = Release;
481 | };
482 | F2A520051B10E00700924912 /* Debug */ = {
483 | isa = XCBuildConfiguration;
484 | buildSettings = {
485 | CLANG_ENABLE_MODULES = YES;
486 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
487 | DEFINES_MODULE = YES;
488 | DYLIB_COMPATIBILITY_VERSION = 1;
489 | DYLIB_CURRENT_VERSION = 1;
490 | DYLIB_INSTALL_NAME_BASE = "@rpath";
491 | ENABLE_TESTABILITY = YES;
492 | INFOPLIST_FILE = Agrume/Info.plist;
493 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
494 | LD_RUNPATH_SEARCH_PATHS = (
495 | "$(inherited)",
496 | "@executable_path/Frameworks",
497 | "@loader_path/Frameworks",
498 | );
499 | PRODUCT_BUNDLE_IDENTIFIER = "com.schnaub.$(PRODUCT_NAME:rfc1034identifier)";
500 | PRODUCT_NAME = "$(TARGET_NAME)";
501 | SKIP_INSTALL = YES;
502 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
503 | SWIFT_VERSION = 5.0;
504 | };
505 | name = Debug;
506 | };
507 | F2A520061B10E00700924912 /* Release */ = {
508 | isa = XCBuildConfiguration;
509 | buildSettings = {
510 | CLANG_ENABLE_MODULES = YES;
511 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
512 | DEFINES_MODULE = YES;
513 | DYLIB_COMPATIBILITY_VERSION = 1;
514 | DYLIB_CURRENT_VERSION = 1;
515 | DYLIB_INSTALL_NAME_BASE = "@rpath";
516 | ENABLE_TESTABILITY = YES;
517 | INFOPLIST_FILE = Agrume/Info.plist;
518 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
519 | LD_RUNPATH_SEARCH_PATHS = (
520 | "$(inherited)",
521 | "@executable_path/Frameworks",
522 | "@loader_path/Frameworks",
523 | );
524 | PRODUCT_BUNDLE_IDENTIFIER = "com.schnaub.$(PRODUCT_NAME:rfc1034identifier)";
525 | PRODUCT_NAME = "$(TARGET_NAME)";
526 | SKIP_INSTALL = YES;
527 | SWIFT_VERSION = 5.0;
528 | };
529 | name = Release;
530 | };
531 | F2A520081B10E00700924912 /* Debug */ = {
532 | isa = XCBuildConfiguration;
533 | buildSettings = {
534 | CLANG_ENABLE_MODULES = YES;
535 | GCC_PREPROCESSOR_DEFINITIONS = (
536 | "DEBUG=1",
537 | "$(inherited)",
538 | );
539 | INFOPLIST_FILE = AgrumeTests/Info.plist;
540 | LD_RUNPATH_SEARCH_PATHS = (
541 | "$(inherited)",
542 | "@executable_path/Frameworks",
543 | "@loader_path/Frameworks",
544 | );
545 | PRODUCT_BUNDLE_IDENTIFIER = "com.schnaub.$(PRODUCT_NAME:rfc1034identifier)";
546 | PRODUCT_NAME = "$(TARGET_NAME)";
547 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
548 | SWIFT_SWIFT3_OBJC_INFERENCE = Off;
549 | SWIFT_VERSION = 4.0;
550 | };
551 | name = Debug;
552 | };
553 | F2A520091B10E00700924912 /* Release */ = {
554 | isa = XCBuildConfiguration;
555 | buildSettings = {
556 | CLANG_ENABLE_MODULES = YES;
557 | INFOPLIST_FILE = AgrumeTests/Info.plist;
558 | LD_RUNPATH_SEARCH_PATHS = (
559 | "$(inherited)",
560 | "@executable_path/Frameworks",
561 | "@loader_path/Frameworks",
562 | );
563 | PRODUCT_BUNDLE_IDENTIFIER = "com.schnaub.$(PRODUCT_NAME:rfc1034identifier)";
564 | PRODUCT_NAME = "$(TARGET_NAME)";
565 | SWIFT_SWIFT3_OBJC_INFERENCE = Off;
566 | SWIFT_VERSION = 4.0;
567 | };
568 | name = Release;
569 | };
570 | /* End XCBuildConfiguration section */
571 |
572 | /* Begin XCConfigurationList section */
573 | F2A51FE81B10E00700924912 /* Build configuration list for PBXProject "Agrume" */ = {
574 | isa = XCConfigurationList;
575 | buildConfigurations = (
576 | F2A520021B10E00700924912 /* Debug */,
577 | F2A520031B10E00700924912 /* Release */,
578 | );
579 | defaultConfigurationIsVisible = 0;
580 | defaultConfigurationName = Release;
581 | };
582 | F2A520041B10E00700924912 /* Build configuration list for PBXNativeTarget "Agrume" */ = {
583 | isa = XCConfigurationList;
584 | buildConfigurations = (
585 | F2A520051B10E00700924912 /* Debug */,
586 | F2A520061B10E00700924912 /* Release */,
587 | );
588 | defaultConfigurationIsVisible = 0;
589 | defaultConfigurationName = Release;
590 | };
591 | F2A520071B10E00700924912 /* Build configuration list for PBXNativeTarget "AgrumeTests" */ = {
592 | isa = XCConfigurationList;
593 | buildConfigurations = (
594 | F2A520081B10E00700924912 /* Debug */,
595 | F2A520091B10E00700924912 /* Release */,
596 | );
597 | defaultConfigurationIsVisible = 0;
598 | defaultConfigurationName = Release;
599 | };
600 | /* End XCConfigurationList section */
601 | };
602 | rootObject = F2A51FE51B10E00700924912 /* Project object */;
603 | }
604 |
--------------------------------------------------------------------------------
/Example/Agrume Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 39B9D7C228DE0B500016BE7F /* LiveTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39B9D7C128DE0B500016BE7F /* LiveTextViewController.swift */; };
11 | 39CA658926EFFC5700A5A910 /* URLUpdatedToImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CA658826EFFC5700A5A910 /* URLUpdatedToImageViewController.swift */; };
12 | 771DA7342179EF1800541206 /* SwiftyGif.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 771DA7332179EF1800541206 /* SwiftyGif.framework */; };
13 | 9464AFE923C692C7006ADEBD /* OverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9464AFE823C692C7006ADEBD /* OverlayView.swift */; };
14 | 948117D723C7A83600AE200D /* MultipleImagesCustomOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 948117D623C7A83600AE200D /* MultipleImagesCustomOverlayView.swift */; };
15 | 94D6B2121E1411B100927735 /* SingeImageBackgroundColorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D6B2111E1411B100927735 /* SingeImageBackgroundColorViewController.swift */; };
16 | E77809E31D17821400CC60F1 /* SingleImageModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E77809E21D17821400CC60F1 /* SingleImageModalViewController.swift */; };
17 | F20F5BD41B134CAF00F9F499 /* Agrume.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F2A520401B130CC000924912 /* Agrume.framework */; };
18 | F224A73227832DD900A8F5ED /* SwiftUIExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F224A73127832DD900A8F5ED /* SwiftUIExampleViewController.swift */; };
19 | F2539BCB20F23ABB00062C80 /* CloseButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2539BCA20F23ABB00062C80 /* CloseButtonViewController.swift */; };
20 | F2539BD020F23F2F00062C80 /* Agrume.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F2A520401B130CC000924912 /* Agrume.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
21 | F2539BD420F2418900062C80 /* CustomCloseButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2539BD320F2418900062C80 /* CustomCloseButtonViewController.swift */; };
22 | F29C53E62221AF7500903FBD /* SwiftyGif.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F2539B9D20F22D9000062C80 /* SwiftyGif.framework */; };
23 | F29C53E72221AF7500903FBD /* SwiftyGif.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F2539B9D20F22D9000062C80 /* SwiftyGif.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
24 | F2A5201B1B130C7E00924912 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2A5201A1B130C7E00924912 /* AppDelegate.swift */; };
25 | F2A520201B130C7E00924912 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F2A5201E1B130C7E00924912 /* Main.storyboard */; };
26 | F2A520221B130C7E00924912 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F2A520211B130C7E00924912 /* Images.xcassets */; };
27 | F2A520251B130C7E00924912 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = F2A520231B130C7E00924912 /* LaunchScreen.xib */; };
28 | F2D7BA1F20A47FB500D5EE66 /* AnimatedGifViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D7BA1E20A47FB500D5EE66 /* AnimatedGifViewController.swift */; };
29 | F2D7BA2220A4812C00D5EE66 /* animated.gif in Resources */ = {isa = PBXBuildFile; fileRef = F2D7BA2120A4812C00D5EE66 /* animated.gif */; };
30 | F2D9598E1B1A133800073772 /* SingleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D9598D1B1A133800073772 /* SingleImageViewController.swift */; };
31 | F2D959911B1A140200073772 /* SingleURLViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D959901B1A140200073772 /* SingleURLViewController.swift */; };
32 | F2D959931B1A153F00073772 /* MultipleImagesCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D959921B1A153F00073772 /* MultipleImagesCollectionViewController.swift */; };
33 | F2D959951B1A15ED00073772 /* DemoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D959941B1A15ED00073772 /* DemoCell.swift */; };
34 | F2D959971B1A199F00073772 /* MultipleURLsCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D959961B1A199F00073772 /* MultipleURLsCollectionViewController.swift */; };
35 | /* End PBXBuildFile section */
36 |
37 | /* Begin PBXContainerItemProxy section */
38 | F2A5202B1B130C7E00924912 /* PBXContainerItemProxy */ = {
39 | isa = PBXContainerItemProxy;
40 | containerPortal = F2A5200D1B130C7E00924912 /* Project object */;
41 | proxyType = 1;
42 | remoteGlobalIDString = F2A520141B130C7E00924912;
43 | remoteInfo = "Agrume Example";
44 | };
45 | F2A5203F1B130CC000924912 /* PBXContainerItemProxy */ = {
46 | isa = PBXContainerItemProxy;
47 | containerPortal = F2A5203A1B130CC000924912 /* Agrume.xcodeproj */;
48 | proxyType = 2;
49 | remoteGlobalIDString = F2A51FEE1B10E00700924912;
50 | remoteInfo = Agrume;
51 | };
52 | F2A520411B130CC000924912 /* PBXContainerItemProxy */ = {
53 | isa = PBXContainerItemProxy;
54 | containerPortal = F2A5203A1B130CC000924912 /* Agrume.xcodeproj */;
55 | proxyType = 2;
56 | remoteGlobalIDString = F2A51FF91B10E00700924912;
57 | remoteInfo = AgrumeTests;
58 | };
59 | F2A520431B130CC800924912 /* PBXContainerItemProxy */ = {
60 | isa = PBXContainerItemProxy;
61 | containerPortal = F2A5203A1B130CC000924912 /* Agrume.xcodeproj */;
62 | proxyType = 1;
63 | remoteGlobalIDString = F2A51FED1B10E00700924912;
64 | remoteInfo = Agrume;
65 | };
66 | /* End PBXContainerItemProxy section */
67 |
68 | /* Begin PBXCopyFilesBuildPhase section */
69 | F2539BCE20F23F1800062C80 /* Embed Frameworks */ = {
70 | isa = PBXCopyFilesBuildPhase;
71 | buildActionMask = 2147483647;
72 | dstPath = "";
73 | dstSubfolderSpec = 10;
74 | files = (
75 | F2539BD020F23F2F00062C80 /* Agrume.framework in Embed Frameworks */,
76 | F29C53E72221AF7500903FBD /* SwiftyGif.framework in Embed Frameworks */,
77 | );
78 | name = "Embed Frameworks";
79 | runOnlyForDeploymentPostprocessing = 0;
80 | };
81 | /* End PBXCopyFilesBuildPhase section */
82 |
83 | /* Begin PBXFileReference section */
84 | 39B9D7C128DE0B500016BE7F /* LiveTextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTextViewController.swift; sourceTree = ""; };
85 | 39CA658826EFFC5700A5A910 /* URLUpdatedToImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLUpdatedToImageViewController.swift; sourceTree = ""; };
86 | 771DA7332179EF1800541206 /* SwiftyGif.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftyGif.framework; sourceTree = BUILT_PRODUCTS_DIR; };
87 | 9464AFE823C692C7006ADEBD /* OverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayView.swift; sourceTree = ""; };
88 | 948117D623C7A83600AE200D /* MultipleImagesCustomOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleImagesCustomOverlayView.swift; sourceTree = ""; };
89 | 94D6B2111E1411B100927735 /* SingeImageBackgroundColorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingeImageBackgroundColorViewController.swift; sourceTree = ""; };
90 | E77809E21D17821400CC60F1 /* SingleImageModalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleImageModalViewController.swift; sourceTree = ""; };
91 | F224A73127832DD900A8F5ED /* SwiftUIExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExampleViewController.swift; sourceTree = ""; };
92 | F2539B9D20F22D9000062C80 /* SwiftyGif.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftyGif.framework; sourceTree = BUILT_PRODUCTS_DIR; };
93 | F2539BCA20F23ABB00062C80 /* CloseButtonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseButtonViewController.swift; sourceTree = ""; };
94 | F2539BD320F2418900062C80 /* CustomCloseButtonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCloseButtonViewController.swift; sourceTree = ""; };
95 | F2A520151B130C7E00924912 /* Agrume Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Agrume Example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
96 | F2A520191B130C7E00924912 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
97 | F2A5201A1B130C7E00924912 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
98 | F2A5201F1B130C7E00924912 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
99 | F2A520211B130C7E00924912 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; };
100 | F2A520241B130C7E00924912 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; };
101 | F2A5202A1B130C7E00924912 /* Agrume ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Agrume ExampleTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
102 | F2A5202F1B130C7E00924912 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
103 | F2A5203A1B130CC000924912 /* Agrume.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Agrume.xcodeproj; path = ../Agrume.xcodeproj; sourceTree = ""; };
104 | F2D7BA1E20A47FB500D5EE66 /* AnimatedGifViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedGifViewController.swift; sourceTree = ""; };
105 | F2D7BA2120A4812C00D5EE66 /* animated.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = animated.gif; sourceTree = ""; };
106 | F2D9598D1B1A133800073772 /* SingleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleImageViewController.swift; sourceTree = ""; };
107 | F2D959901B1A140200073772 /* SingleURLViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleURLViewController.swift; sourceTree = ""; };
108 | F2D959921B1A153F00073772 /* MultipleImagesCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipleImagesCollectionViewController.swift; sourceTree = ""; };
109 | F2D959941B1A15ED00073772 /* DemoCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoCell.swift; sourceTree = ""; };
110 | F2D959961B1A199F00073772 /* MultipleURLsCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipleURLsCollectionViewController.swift; sourceTree = ""; };
111 | /* End PBXFileReference section */
112 |
113 | /* Begin PBXFrameworksBuildPhase section */
114 | F2A520121B130C7E00924912 /* Frameworks */ = {
115 | isa = PBXFrameworksBuildPhase;
116 | buildActionMask = 2147483647;
117 | files = (
118 | F29C53E62221AF7500903FBD /* SwiftyGif.framework in Frameworks */,
119 | 771DA7342179EF1800541206 /* SwiftyGif.framework in Frameworks */,
120 | F20F5BD41B134CAF00F9F499 /* Agrume.framework in Frameworks */,
121 | );
122 | runOnlyForDeploymentPostprocessing = 0;
123 | };
124 | F2A520271B130C7E00924912 /* Frameworks */ = {
125 | isa = PBXFrameworksBuildPhase;
126 | buildActionMask = 2147483647;
127 | files = (
128 | );
129 | runOnlyForDeploymentPostprocessing = 0;
130 | };
131 | /* End PBXFrameworksBuildPhase section */
132 |
133 | /* Begin PBXGroup section */
134 | F2539B9C20F22D9000062C80 /* Frameworks */ = {
135 | isa = PBXGroup;
136 | children = (
137 | 771DA7332179EF1800541206 /* SwiftyGif.framework */,
138 | F2539B9D20F22D9000062C80 /* SwiftyGif.framework */,
139 | );
140 | name = Frameworks;
141 | sourceTree = "";
142 | };
143 | F2A5200C1B130C7E00924912 = {
144 | isa = PBXGroup;
145 | children = (
146 | F2A5203A1B130CC000924912 /* Agrume.xcodeproj */,
147 | F2A520171B130C7E00924912 /* Agrume Example */,
148 | F2A5202D1B130C7E00924912 /* Agrume ExampleTests */,
149 | F2A520161B130C7E00924912 /* Products */,
150 | F2539B9C20F22D9000062C80 /* Frameworks */,
151 | );
152 | indentWidth = 2;
153 | sourceTree = "";
154 | tabWidth = 2;
155 | };
156 | F2A520161B130C7E00924912 /* Products */ = {
157 | isa = PBXGroup;
158 | children = (
159 | F2A520151B130C7E00924912 /* Agrume Example.app */,
160 | F2A5202A1B130C7E00924912 /* Agrume ExampleTests.xctest */,
161 | );
162 | name = Products;
163 | sourceTree = "";
164 | };
165 | F2A520171B130C7E00924912 /* Agrume Example */ = {
166 | isa = PBXGroup;
167 | children = (
168 | F2D7BA2120A4812C00D5EE66 /* animated.gif */,
169 | F2D7BA1E20A47FB500D5EE66 /* AnimatedGifViewController.swift */,
170 | F2A5201A1B130C7E00924912 /* AppDelegate.swift */,
171 | F2539BCA20F23ABB00062C80 /* CloseButtonViewController.swift */,
172 | F2539BD320F2418900062C80 /* CustomCloseButtonViewController.swift */,
173 | F2D959941B1A15ED00073772 /* DemoCell.swift */,
174 | 9464AFE823C692C7006ADEBD /* OverlayView.swift */,
175 | F2A520211B130C7E00924912 /* Images.xcassets */,
176 | F2A520231B130C7E00924912 /* LaunchScreen.xib */,
177 | F2A5201E1B130C7E00924912 /* Main.storyboard */,
178 | F224A73127832DD900A8F5ED /* SwiftUIExampleViewController.swift */,
179 | F2D959921B1A153F00073772 /* MultipleImagesCollectionViewController.swift */,
180 | F2D959961B1A199F00073772 /* MultipleURLsCollectionViewController.swift */,
181 | 948117D623C7A83600AE200D /* MultipleImagesCustomOverlayView.swift */,
182 | 94D6B2111E1411B100927735 /* SingeImageBackgroundColorViewController.swift */,
183 | E77809E21D17821400CC60F1 /* SingleImageModalViewController.swift */,
184 | F2D9598D1B1A133800073772 /* SingleImageViewController.swift */,
185 | F2D959901B1A140200073772 /* SingleURLViewController.swift */,
186 | 39B9D7C128DE0B500016BE7F /* LiveTextViewController.swift */,
187 | 39CA658826EFFC5700A5A910 /* URLUpdatedToImageViewController.swift */,
188 | F2A520181B130C7E00924912 /* Supporting Files */,
189 | );
190 | path = "Agrume Example";
191 | sourceTree = "";
192 | };
193 | F2A520181B130C7E00924912 /* Supporting Files */ = {
194 | isa = PBXGroup;
195 | children = (
196 | F2A520191B130C7E00924912 /* Info.plist */,
197 | );
198 | name = "Supporting Files";
199 | sourceTree = "";
200 | };
201 | F2A5202D1B130C7E00924912 /* Agrume ExampleTests */ = {
202 | isa = PBXGroup;
203 | children = (
204 | F2A5202E1B130C7E00924912 /* Supporting Files */,
205 | );
206 | path = "Agrume ExampleTests";
207 | sourceTree = "";
208 | };
209 | F2A5202E1B130C7E00924912 /* Supporting Files */ = {
210 | isa = PBXGroup;
211 | children = (
212 | F2A5202F1B130C7E00924912 /* Info.plist */,
213 | );
214 | name = "Supporting Files";
215 | sourceTree = "";
216 | };
217 | F2A5203B1B130CC000924912 /* Products */ = {
218 | isa = PBXGroup;
219 | children = (
220 | F2A520401B130CC000924912 /* Agrume.framework */,
221 | F2A520421B130CC000924912 /* AgrumeTests.xctest */,
222 | );
223 | name = Products;
224 | sourceTree = "";
225 | };
226 | /* End PBXGroup section */
227 |
228 | /* Begin PBXNativeTarget section */
229 | F2A520141B130C7E00924912 /* Agrume Example */ = {
230 | isa = PBXNativeTarget;
231 | buildConfigurationList = F2A520341B130C7E00924912 /* Build configuration list for PBXNativeTarget "Agrume Example" */;
232 | buildPhases = (
233 | F2A520111B130C7E00924912 /* Sources */,
234 | F2A520121B130C7E00924912 /* Frameworks */,
235 | F2A520131B130C7E00924912 /* Resources */,
236 | F2539BCE20F23F1800062C80 /* Embed Frameworks */,
237 | );
238 | buildRules = (
239 | );
240 | dependencies = (
241 | F2A520441B130CC800924912 /* PBXTargetDependency */,
242 | );
243 | name = "Agrume Example";
244 | productName = "Agrume Example";
245 | productReference = F2A520151B130C7E00924912 /* Agrume Example.app */;
246 | productType = "com.apple.product-type.application";
247 | };
248 | F2A520291B130C7E00924912 /* Agrume ExampleTests */ = {
249 | isa = PBXNativeTarget;
250 | buildConfigurationList = F2A520371B130C7E00924912 /* Build configuration list for PBXNativeTarget "Agrume ExampleTests" */;
251 | buildPhases = (
252 | F2A520261B130C7E00924912 /* Sources */,
253 | F2A520271B130C7E00924912 /* Frameworks */,
254 | F2A520281B130C7E00924912 /* Resources */,
255 | );
256 | buildRules = (
257 | );
258 | dependencies = (
259 | F2A5202C1B130C7E00924912 /* PBXTargetDependency */,
260 | );
261 | name = "Agrume ExampleTests";
262 | productName = "Agrume ExampleTests";
263 | productReference = F2A5202A1B130C7E00924912 /* Agrume ExampleTests.xctest */;
264 | productType = "com.apple.product-type.bundle.unit-test";
265 | };
266 | /* End PBXNativeTarget section */
267 |
268 | /* Begin PBXProject section */
269 | F2A5200D1B130C7E00924912 /* Project object */ = {
270 | isa = PBXProject;
271 | attributes = {
272 | LastSwiftMigration = 0700;
273 | LastSwiftUpdateCheck = 0700;
274 | LastUpgradeCheck = 1200;
275 | ORGANIZATIONNAME = Schnaub;
276 | TargetAttributes = {
277 | F2A520141B130C7E00924912 = {
278 | CreatedOnToolsVersion = 6.3.2;
279 | DevelopmentTeam = CPRVB9W254;
280 | LastSwiftMigration = 1020;
281 | };
282 | F2A520291B130C7E00924912 = {
283 | CreatedOnToolsVersion = 6.3.2;
284 | LastSwiftMigration = 0800;
285 | TestTargetID = F2A520141B130C7E00924912;
286 | };
287 | };
288 | };
289 | buildConfigurationList = F2A520101B130C7E00924912 /* Build configuration list for PBXProject "Agrume Example" */;
290 | compatibilityVersion = "Xcode 3.2";
291 | developmentRegion = en;
292 | hasScannedForEncodings = 0;
293 | knownRegions = (
294 | en,
295 | Base,
296 | );
297 | mainGroup = F2A5200C1B130C7E00924912;
298 | productRefGroup = F2A520161B130C7E00924912 /* Products */;
299 | projectDirPath = "";
300 | projectReferences = (
301 | {
302 | ProductGroup = F2A5203B1B130CC000924912 /* Products */;
303 | ProjectRef = F2A5203A1B130CC000924912 /* Agrume.xcodeproj */;
304 | },
305 | );
306 | projectRoot = "";
307 | targets = (
308 | F2A520141B130C7E00924912 /* Agrume Example */,
309 | F2A520291B130C7E00924912 /* Agrume ExampleTests */,
310 | );
311 | };
312 | /* End PBXProject section */
313 |
314 | /* Begin PBXReferenceProxy section */
315 | F2A520401B130CC000924912 /* Agrume.framework */ = {
316 | isa = PBXReferenceProxy;
317 | fileType = wrapper.framework;
318 | path = Agrume.framework;
319 | remoteRef = F2A5203F1B130CC000924912 /* PBXContainerItemProxy */;
320 | sourceTree = BUILT_PRODUCTS_DIR;
321 | };
322 | F2A520421B130CC000924912 /* AgrumeTests.xctest */ = {
323 | isa = PBXReferenceProxy;
324 | fileType = wrapper.cfbundle;
325 | path = AgrumeTests.xctest;
326 | remoteRef = F2A520411B130CC000924912 /* PBXContainerItemProxy */;
327 | sourceTree = BUILT_PRODUCTS_DIR;
328 | };
329 | /* End PBXReferenceProxy section */
330 |
331 | /* Begin PBXResourcesBuildPhase section */
332 | F2A520131B130C7E00924912 /* Resources */ = {
333 | isa = PBXResourcesBuildPhase;
334 | buildActionMask = 2147483647;
335 | files = (
336 | F2A520201B130C7E00924912 /* Main.storyboard in Resources */,
337 | F2A520251B130C7E00924912 /* LaunchScreen.xib in Resources */,
338 | F2D7BA2220A4812C00D5EE66 /* animated.gif in Resources */,
339 | F2A520221B130C7E00924912 /* Images.xcassets in Resources */,
340 | );
341 | runOnlyForDeploymentPostprocessing = 0;
342 | };
343 | F2A520281B130C7E00924912 /* Resources */ = {
344 | isa = PBXResourcesBuildPhase;
345 | buildActionMask = 2147483647;
346 | files = (
347 | );
348 | runOnlyForDeploymentPostprocessing = 0;
349 | };
350 | /* End PBXResourcesBuildPhase section */
351 |
352 | /* Begin PBXSourcesBuildPhase section */
353 | F2A520111B130C7E00924912 /* Sources */ = {
354 | isa = PBXSourcesBuildPhase;
355 | buildActionMask = 2147483647;
356 | files = (
357 | E77809E31D17821400CC60F1 /* SingleImageModalViewController.swift in Sources */,
358 | 39CA658926EFFC5700A5A910 /* URLUpdatedToImageViewController.swift in Sources */,
359 | F2D959911B1A140200073772 /* SingleURLViewController.swift in Sources */,
360 | F2D7BA1F20A47FB500D5EE66 /* AnimatedGifViewController.swift in Sources */,
361 | 948117D723C7A83600AE200D /* MultipleImagesCustomOverlayView.swift in Sources */,
362 | 94D6B2121E1411B100927735 /* SingeImageBackgroundColorViewController.swift in Sources */,
363 | F2539BCB20F23ABB00062C80 /* CloseButtonViewController.swift in Sources */,
364 | F2D959951B1A15ED00073772 /* DemoCell.swift in Sources */,
365 | F2D9598E1B1A133800073772 /* SingleImageViewController.swift in Sources */,
366 | F2539BD420F2418900062C80 /* CustomCloseButtonViewController.swift in Sources */,
367 | F2A5201B1B130C7E00924912 /* AppDelegate.swift in Sources */,
368 | 39B9D7C228DE0B500016BE7F /* LiveTextViewController.swift in Sources */,
369 | 9464AFE923C692C7006ADEBD /* OverlayView.swift in Sources */,
370 | F2D959971B1A199F00073772 /* MultipleURLsCollectionViewController.swift in Sources */,
371 | F224A73227832DD900A8F5ED /* SwiftUIExampleViewController.swift in Sources */,
372 | F2D959931B1A153F00073772 /* MultipleImagesCollectionViewController.swift in Sources */,
373 | );
374 | runOnlyForDeploymentPostprocessing = 0;
375 | };
376 | F2A520261B130C7E00924912 /* Sources */ = {
377 | isa = PBXSourcesBuildPhase;
378 | buildActionMask = 2147483647;
379 | files = (
380 | );
381 | runOnlyForDeploymentPostprocessing = 0;
382 | };
383 | /* End PBXSourcesBuildPhase section */
384 |
385 | /* Begin PBXTargetDependency section */
386 | F2A5202C1B130C7E00924912 /* PBXTargetDependency */ = {
387 | isa = PBXTargetDependency;
388 | target = F2A520141B130C7E00924912 /* Agrume Example */;
389 | targetProxy = F2A5202B1B130C7E00924912 /* PBXContainerItemProxy */;
390 | };
391 | F2A520441B130CC800924912 /* PBXTargetDependency */ = {
392 | isa = PBXTargetDependency;
393 | name = Agrume;
394 | targetProxy = F2A520431B130CC800924912 /* PBXContainerItemProxy */;
395 | };
396 | /* End PBXTargetDependency section */
397 |
398 | /* Begin PBXVariantGroup section */
399 | F2A5201E1B130C7E00924912 /* Main.storyboard */ = {
400 | isa = PBXVariantGroup;
401 | children = (
402 | F2A5201F1B130C7E00924912 /* Base */,
403 | );
404 | name = Main.storyboard;
405 | sourceTree = "";
406 | };
407 | F2A520231B130C7E00924912 /* LaunchScreen.xib */ = {
408 | isa = PBXVariantGroup;
409 | children = (
410 | F2A520241B130C7E00924912 /* Base */,
411 | );
412 | name = LaunchScreen.xib;
413 | sourceTree = "";
414 | };
415 | /* End PBXVariantGroup section */
416 |
417 | /* Begin XCBuildConfiguration section */
418 | F2A520321B130C7E00924912 /* Debug */ = {
419 | isa = XCBuildConfiguration;
420 | buildSettings = {
421 | ALWAYS_SEARCH_USER_PATHS = NO;
422 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
423 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
424 | CLANG_CXX_LIBRARY = "libc++";
425 | CLANG_ENABLE_MODULES = YES;
426 | CLANG_ENABLE_OBJC_ARC = YES;
427 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
428 | CLANG_WARN_BOOL_CONVERSION = YES;
429 | CLANG_WARN_COMMA = YES;
430 | CLANG_WARN_CONSTANT_CONVERSION = YES;
431 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
432 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
433 | CLANG_WARN_EMPTY_BODY = YES;
434 | CLANG_WARN_ENUM_CONVERSION = YES;
435 | CLANG_WARN_INFINITE_RECURSION = YES;
436 | CLANG_WARN_INT_CONVERSION = YES;
437 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
438 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
439 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
440 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
441 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
442 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
443 | CLANG_WARN_STRICT_PROTOTYPES = YES;
444 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
445 | CLANG_WARN_UNREACHABLE_CODE = YES;
446 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
447 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
448 | COPY_PHASE_STRIP = NO;
449 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
450 | ENABLE_STRICT_OBJC_MSGSEND = YES;
451 | ENABLE_TESTABILITY = YES;
452 | GCC_C_LANGUAGE_STANDARD = gnu99;
453 | GCC_DYNAMIC_NO_PIC = NO;
454 | GCC_NO_COMMON_BLOCKS = YES;
455 | GCC_OPTIMIZATION_LEVEL = 0;
456 | GCC_PREPROCESSOR_DEFINITIONS = (
457 | "DEBUG=1",
458 | "$(inherited)",
459 | );
460 | GCC_SYMBOLS_PRIVATE_EXTERN = NO;
461 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
462 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
463 | GCC_WARN_UNDECLARED_SELECTOR = YES;
464 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
465 | GCC_WARN_UNUSED_FUNCTION = YES;
466 | GCC_WARN_UNUSED_VARIABLE = YES;
467 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
468 | MTL_ENABLE_DEBUG_INFO = YES;
469 | ONLY_ACTIVE_ARCH = YES;
470 | SDKROOT = iphoneos;
471 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
472 | };
473 | name = Debug;
474 | };
475 | F2A520331B130C7E00924912 /* Release */ = {
476 | isa = XCBuildConfiguration;
477 | buildSettings = {
478 | ALWAYS_SEARCH_USER_PATHS = NO;
479 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
480 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
481 | CLANG_CXX_LIBRARY = "libc++";
482 | CLANG_ENABLE_MODULES = YES;
483 | CLANG_ENABLE_OBJC_ARC = YES;
484 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
485 | CLANG_WARN_BOOL_CONVERSION = YES;
486 | CLANG_WARN_COMMA = YES;
487 | CLANG_WARN_CONSTANT_CONVERSION = YES;
488 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
489 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
490 | CLANG_WARN_EMPTY_BODY = YES;
491 | CLANG_WARN_ENUM_CONVERSION = YES;
492 | CLANG_WARN_INFINITE_RECURSION = YES;
493 | CLANG_WARN_INT_CONVERSION = YES;
494 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
495 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
496 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
497 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
498 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
499 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
500 | CLANG_WARN_STRICT_PROTOTYPES = YES;
501 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
502 | CLANG_WARN_UNREACHABLE_CODE = YES;
503 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
504 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
505 | COPY_PHASE_STRIP = NO;
506 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
507 | ENABLE_NS_ASSERTIONS = NO;
508 | ENABLE_STRICT_OBJC_MSGSEND = YES;
509 | GCC_C_LANGUAGE_STANDARD = gnu99;
510 | GCC_NO_COMMON_BLOCKS = YES;
511 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
512 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
513 | GCC_WARN_UNDECLARED_SELECTOR = YES;
514 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
515 | GCC_WARN_UNUSED_FUNCTION = YES;
516 | GCC_WARN_UNUSED_VARIABLE = YES;
517 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
518 | MTL_ENABLE_DEBUG_INFO = NO;
519 | SDKROOT = iphoneos;
520 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
521 | VALIDATE_PRODUCT = YES;
522 | };
523 | name = Release;
524 | };
525 | F2A520351B130C7E00924912 /* Debug */ = {
526 | isa = XCBuildConfiguration;
527 | buildSettings = {
528 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
529 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
530 | DEVELOPMENT_TEAM = CPRVB9W254;
531 | INFOPLIST_FILE = "Agrume Example/Info.plist";
532 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
533 | PRODUCT_BUNDLE_IDENTIFIER = "com.schnaub.$(PRODUCT_NAME:rfc1034identifier)";
534 | PRODUCT_NAME = "$(TARGET_NAME)";
535 | SWIFT_VERSION = 5.0;
536 | };
537 | name = Debug;
538 | };
539 | F2A520361B130C7E00924912 /* Release */ = {
540 | isa = XCBuildConfiguration;
541 | buildSettings = {
542 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
543 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
544 | DEVELOPMENT_TEAM = CPRVB9W254;
545 | INFOPLIST_FILE = "Agrume Example/Info.plist";
546 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
547 | PRODUCT_BUNDLE_IDENTIFIER = "com.schnaub.$(PRODUCT_NAME:rfc1034identifier)";
548 | PRODUCT_NAME = "$(TARGET_NAME)";
549 | SWIFT_VERSION = 5.0;
550 | };
551 | name = Release;
552 | };
553 | F2A520381B130C7E00924912 /* Debug */ = {
554 | isa = XCBuildConfiguration;
555 | buildSettings = {
556 | BUNDLE_LOADER = "$(TEST_HOST)";
557 | FRAMEWORK_SEARCH_PATHS = (
558 | "$(SDKROOT)/Developer/Library/Frameworks",
559 | "$(inherited)",
560 | );
561 | GCC_PREPROCESSOR_DEFINITIONS = (
562 | "DEBUG=1",
563 | "$(inherited)",
564 | );
565 | INFOPLIST_FILE = "Agrume ExampleTests/Info.plist";
566 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
567 | PRODUCT_BUNDLE_IDENTIFIER = "com.schnaub.$(PRODUCT_NAME:rfc1034identifier)";
568 | PRODUCT_NAME = "$(TARGET_NAME)";
569 | SWIFT_VERSION = 4.0;
570 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Agrume Example.app/Agrume Example";
571 | };
572 | name = Debug;
573 | };
574 | F2A520391B130C7E00924912 /* Release */ = {
575 | isa = XCBuildConfiguration;
576 | buildSettings = {
577 | BUNDLE_LOADER = "$(TEST_HOST)";
578 | FRAMEWORK_SEARCH_PATHS = (
579 | "$(SDKROOT)/Developer/Library/Frameworks",
580 | "$(inherited)",
581 | );
582 | INFOPLIST_FILE = "Agrume ExampleTests/Info.plist";
583 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
584 | PRODUCT_BUNDLE_IDENTIFIER = "com.schnaub.$(PRODUCT_NAME:rfc1034identifier)";
585 | PRODUCT_NAME = "$(TARGET_NAME)";
586 | SWIFT_VERSION = 4.0;
587 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Agrume Example.app/Agrume Example";
588 | };
589 | name = Release;
590 | };
591 | /* End XCBuildConfiguration section */
592 |
593 | /* Begin XCConfigurationList section */
594 | F2A520101B130C7E00924912 /* Build configuration list for PBXProject "Agrume Example" */ = {
595 | isa = XCConfigurationList;
596 | buildConfigurations = (
597 | F2A520321B130C7E00924912 /* Debug */,
598 | F2A520331B130C7E00924912 /* Release */,
599 | );
600 | defaultConfigurationIsVisible = 0;
601 | defaultConfigurationName = Release;
602 | };
603 | F2A520341B130C7E00924912 /* Build configuration list for PBXNativeTarget "Agrume Example" */ = {
604 | isa = XCConfigurationList;
605 | buildConfigurations = (
606 | F2A520351B130C7E00924912 /* Debug */,
607 | F2A520361B130C7E00924912 /* Release */,
608 | );
609 | defaultConfigurationIsVisible = 0;
610 | defaultConfigurationName = Release;
611 | };
612 | F2A520371B130C7E00924912 /* Build configuration list for PBXNativeTarget "Agrume ExampleTests" */ = {
613 | isa = XCConfigurationList;
614 | buildConfigurations = (
615 | F2A520381B130C7E00924912 /* Debug */,
616 | F2A520391B130C7E00924912 /* Release */,
617 | );
618 | defaultConfigurationIsVisible = 0;
619 | defaultConfigurationName = Release;
620 | };
621 | /* End XCConfigurationList section */
622 | };
623 | rootObject = F2A5200D1B130C7E00924912 /* Project object */;
624 | }
625 |
--------------------------------------------------------------------------------