├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── stale.yml ├── .gitignore ├── .gitmodules ├── .swiftlint.yml ├── .travis.yml ├── Agrume.podspec ├── Agrume.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── Agrume.xcscheme ├── Agrume.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Agrume ├── Agrume.h ├── Agrume.swift ├── AgrumeCell.swift ├── AgrumeDataSource.swift ├── AgrumeImage.swift ├── AgrumeOverlayView.swift ├── AgrumePhotoLibraryHelper.swift ├── AgrumeServiceLocator.swift ├── AgrumeView.swift ├── Configuration.swift ├── Foundation+Agrume.swift ├── ImageDownloader.swift ├── Info.plist ├── UIKit+Agrume.swift └── With.swift ├── AgrumeTests └── Info.plist ├── Cartfile ├── Cartfile.resolved ├── Dangerfile ├── Example ├── Agrume Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── Agrume Example │ ├── AnimatedGifViewController.swift │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── CloseButtonViewController.swift │ ├── CustomCloseButtonViewController.swift │ ├── DemoCell.swift │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── EvilBacon.imageset │ │ │ ├── Contents.json │ │ │ └── EvilBacon.png │ │ ├── MapleBacon.imageset │ │ │ ├── Contents.json │ │ │ └── MapleBacon.png │ │ └── TextAndQR.imageset │ │ │ ├── Contents.json │ │ │ └── textAndQR.png │ ├── Info.plist │ ├── LiveTextViewController.swift │ ├── MultipleImagesCollectionViewController.swift │ ├── MultipleImagesCustomOverlayView.swift │ ├── MultipleURLsCollectionViewController.swift │ ├── OverlayView.swift │ ├── SingeImageBackgroundColorViewController.swift │ ├── SingleImageModalViewController.swift │ ├── SingleImageViewController.swift │ ├── SingleURLViewController.swift │ ├── SwiftUIExampleViewController.swift │ ├── URLUpdatedToImageViewController.swift │ └── animated.gif └── Agrume ExampleTests │ └── Info.plist ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.resolved ├── Package.swift └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [JanGorman] 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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.' -------------------------------------------------------------------------------- /.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/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Frameworks/SwiftyGif"] 2 | path = Frameworks/SwiftyGif 3 | url = https://github.com/kirualex/SwiftyGif 4 | shallow = true 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode11 3 | script: 4 | - git submodule init 5 | - bundle exec danger 6 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /Agrume.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Agrume.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Agrume.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Agrume.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "kirualex/SwiftyGif" -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "kirualex/SwiftyGif" "5.4.3" 2 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /Example/Agrume Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /Example/Agrume Example/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /Example/Agrume Example/Images.xcassets/EvilBacon.imageset/EvilBacon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanGorman/Agrume/13973b656425d60a99e95940c4e269460b2ec906/Example/Agrume Example/Images.xcassets/EvilBacon.imageset/EvilBacon.png -------------------------------------------------------------------------------- /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/MapleBacon.imageset/MapleBacon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanGorman/Agrume/13973b656425d60a99e95940c4e269460b2ec906/Example/Agrume Example/Images.xcassets/MapleBacon.imageset/MapleBacon.png -------------------------------------------------------------------------------- /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/Images.xcassets/TextAndQR.imageset/textAndQR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanGorman/Agrume/13973b656425d60a99e95940c4e269460b2ec906/Example/Agrume Example/Images.xcassets/TextAndQR.imageset/textAndQR.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Example/Agrume Example/animated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanGorman/Agrume/13973b656425d60a99e95940c4e269460b2ec906/Example/Agrume Example/animated.gif -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | gem "danger" 5 | gem "danger-swiftlint" -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (8.0.2) 9 | base64 10 | benchmark (>= 0.3) 11 | bigdecimal 12 | concurrent-ruby (~> 1.0, >= 1.3.1) 13 | connection_pool (>= 2.2.5) 14 | drb 15 | i18n (>= 1.6, < 2) 16 | logger (>= 1.4.2) 17 | minitest (>= 5.1) 18 | securerandom (>= 0.3) 19 | tzinfo (~> 2.0, >= 2.0.5) 20 | uri (>= 0.13.1) 21 | addressable (2.8.7) 22 | public_suffix (>= 2.0.2, < 7.0) 23 | artifactory (3.0.17) 24 | atomos (0.1.3) 25 | aws-eventstream (1.4.0) 26 | aws-partitions (1.1125.0) 27 | aws-sdk-core (3.226.2) 28 | aws-eventstream (~> 1, >= 1.3.0) 29 | aws-partitions (~> 1, >= 1.992.0) 30 | aws-sigv4 (~> 1.9) 31 | base64 32 | jmespath (~> 1, >= 1.6.1) 33 | logger 34 | aws-sdk-kms (1.106.0) 35 | aws-sdk-core (~> 3, >= 3.225.0) 36 | aws-sigv4 (~> 1.5) 37 | aws-sdk-s3 (1.192.0) 38 | aws-sdk-core (~> 3, >= 3.225.0) 39 | aws-sdk-kms (~> 1) 40 | aws-sigv4 (~> 1.5) 41 | aws-sigv4 (1.12.1) 42 | aws-eventstream (~> 1, >= 1.0.2) 43 | babosa (1.0.4) 44 | base64 (0.3.0) 45 | benchmark (0.4.1) 46 | bigdecimal (3.2.2) 47 | claide (1.1.0) 48 | claide-plugins (0.9.2) 49 | cork 50 | nap 51 | open4 (~> 1.3) 52 | colored (1.2) 53 | colored2 (3.1.2) 54 | commander (4.6.0) 55 | highline (~> 2.0.0) 56 | concurrent-ruby (1.3.5) 57 | connection_pool (2.5.3) 58 | cork (0.3.0) 59 | colored2 (~> 3.1) 60 | danger (9.5.3) 61 | base64 (~> 0.2) 62 | claide (~> 1.0) 63 | claide-plugins (>= 0.9.2) 64 | colored2 (>= 3.1, < 5) 65 | cork (~> 0.1) 66 | faraday (>= 0.9.0, < 3.0) 67 | faraday-http-cache (~> 2.0) 68 | git (>= 1.13, < 3.0) 69 | kramdown (>= 2.5.1, < 3.0) 70 | kramdown-parser-gfm (~> 1.0) 71 | octokit (>= 4.0) 72 | pstore (~> 0.1) 73 | terminal-table (>= 1, < 5) 74 | danger-swiftlint (0.37.2) 75 | danger 76 | rake (> 10) 77 | thor (~> 1.4) 78 | declarative (0.0.20) 79 | digest-crc (0.7.0) 80 | rake (>= 12.0.0, < 14.0.0) 81 | domain_name (0.6.20240107) 82 | dotenv (2.8.1) 83 | drb (2.2.3) 84 | emoji_regex (3.2.3) 85 | excon (0.112.0) 86 | faraday (1.10.4) 87 | faraday-em_http (~> 1.0) 88 | faraday-em_synchrony (~> 1.0) 89 | faraday-excon (~> 1.1) 90 | faraday-httpclient (~> 1.0) 91 | faraday-multipart (~> 1.0) 92 | faraday-net_http (~> 1.0) 93 | faraday-net_http_persistent (~> 1.0) 94 | faraday-patron (~> 1.0) 95 | faraday-rack (~> 1.0) 96 | faraday-retry (~> 1.0) 97 | ruby2_keywords (>= 0.0.4) 98 | faraday-cookie_jar (0.0.7) 99 | faraday (>= 0.8.0) 100 | http-cookie (~> 1.0.0) 101 | faraday-em_http (1.0.0) 102 | faraday-em_synchrony (1.0.1) 103 | faraday-excon (1.1.0) 104 | faraday-http-cache (2.5.1) 105 | faraday (>= 0.8) 106 | faraday-httpclient (1.0.1) 107 | faraday-multipart (1.1.1) 108 | multipart-post (~> 2.0) 109 | faraday-net_http (1.0.2) 110 | faraday-net_http_persistent (1.2.0) 111 | faraday-patron (1.0.0) 112 | faraday-rack (1.0.0) 113 | faraday-retry (1.0.3) 114 | faraday_middleware (1.2.1) 115 | faraday (~> 1.0) 116 | fastimage (2.4.0) 117 | fastlane (2.228.0) 118 | CFPropertyList (>= 2.3, < 4.0.0) 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 | bundler (>= 1.12.0, < 3.0.0) 124 | colored (~> 1.2) 125 | commander (~> 4.6) 126 | dotenv (>= 2.1.1, < 3.0.0) 127 | emoji_regex (>= 0.1, < 4.0) 128 | excon (>= 0.71.0, < 1.0.0) 129 | faraday (~> 1.0) 130 | faraday-cookie_jar (~> 0.0.6) 131 | faraday_middleware (~> 1.0) 132 | fastimage (>= 2.1.0, < 3.0.0) 133 | fastlane-sirp (>= 1.0.0) 134 | gh_inspector (>= 1.1.2, < 2.0.0) 135 | google-apis-androidpublisher_v3 (~> 0.3) 136 | google-apis-playcustomapp_v1 (~> 0.1) 137 | google-cloud-env (>= 1.6.0, < 2.0.0) 138 | google-cloud-storage (~> 1.31) 139 | highline (~> 2.0) 140 | http-cookie (~> 1.0.5) 141 | json (< 3.0.0) 142 | jwt (>= 2.1.0, < 3) 143 | mini_magick (>= 4.9.4, < 5.0.0) 144 | multipart-post (>= 2.0.0, < 3.0.0) 145 | naturally (~> 2.2) 146 | optparse (>= 0.1.1, < 1.0.0) 147 | plist (>= 3.1.0, < 4.0.0) 148 | rubyzip (>= 2.0.0, < 3.0.0) 149 | security (= 0.1.5) 150 | simctl (~> 1.6.3) 151 | terminal-notifier (>= 2.0.0, < 3.0.0) 152 | terminal-table (~> 3) 153 | tty-screen (>= 0.6.3, < 1.0.0) 154 | tty-spinner (>= 0.8.0, < 1.0.0) 155 | word_wrap (~> 1.0.0) 156 | xcodeproj (>= 1.13.0, < 2.0.0) 157 | xcpretty (~> 0.4.1) 158 | xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) 159 | fastlane-sirp (1.0.0) 160 | sysrandom (~> 1.0) 161 | gh_inspector (1.1.3) 162 | git (2.3.3) 163 | activesupport (>= 5.0) 164 | addressable (~> 2.8) 165 | process_executer (~> 1.1) 166 | rchardet (~> 1.8) 167 | google-apis-androidpublisher_v3 (0.54.0) 168 | google-apis-core (>= 0.11.0, < 2.a) 169 | google-apis-core (0.11.3) 170 | addressable (~> 2.5, >= 2.5.1) 171 | googleauth (>= 0.16.2, < 2.a) 172 | httpclient (>= 2.8.1, < 3.a) 173 | mini_mime (~> 1.0) 174 | representable (~> 3.0) 175 | retriable (>= 2.0, < 4.a) 176 | rexml 177 | google-apis-iamcredentials_v1 (0.17.0) 178 | google-apis-core (>= 0.11.0, < 2.a) 179 | google-apis-playcustomapp_v1 (0.13.0) 180 | google-apis-core (>= 0.11.0, < 2.a) 181 | google-apis-storage_v1 (0.31.0) 182 | google-apis-core (>= 0.11.0, < 2.a) 183 | google-cloud-core (1.8.0) 184 | google-cloud-env (>= 1.0, < 3.a) 185 | google-cloud-errors (~> 1.0) 186 | google-cloud-env (1.6.0) 187 | faraday (>= 0.17.3, < 3.0) 188 | google-cloud-errors (1.5.0) 189 | google-cloud-storage (1.47.0) 190 | addressable (~> 2.8) 191 | digest-crc (~> 0.4) 192 | google-apis-iamcredentials_v1 (~> 0.1) 193 | google-apis-storage_v1 (~> 0.31.0) 194 | google-cloud-core (~> 1.6) 195 | googleauth (>= 0.16.2, < 2.a) 196 | mini_mime (~> 1.0) 197 | googleauth (1.8.1) 198 | faraday (>= 0.17.3, < 3.a) 199 | jwt (>= 1.4, < 3.0) 200 | multi_json (~> 1.11) 201 | os (>= 0.9, < 2.0) 202 | signet (>= 0.16, < 2.a) 203 | highline (2.0.3) 204 | http-cookie (1.0.8) 205 | domain_name (~> 0.5) 206 | httpclient (2.9.0) 207 | mutex_m 208 | i18n (1.14.7) 209 | concurrent-ruby (~> 1.0) 210 | jmespath (1.6.2) 211 | json (2.12.2) 212 | jwt (2.10.2) 213 | base64 214 | kramdown (2.5.1) 215 | rexml (>= 3.3.9) 216 | kramdown-parser-gfm (1.1.0) 217 | kramdown (~> 2.0) 218 | logger (1.7.0) 219 | mini_magick (4.13.2) 220 | mini_mime (1.1.5) 221 | minitest (5.25.5) 222 | multi_json (1.15.0) 223 | multipart-post (2.4.1) 224 | mutex_m (0.3.0) 225 | nanaimo (0.4.0) 226 | nap (1.1.0) 227 | naturally (2.3.0) 228 | nkf (0.2.0) 229 | octokit (10.0.0) 230 | faraday (>= 1, < 3) 231 | sawyer (~> 0.9) 232 | open4 (1.3.4) 233 | optparse (0.6.0) 234 | os (1.1.4) 235 | plist (3.7.2) 236 | process_executer (1.3.0) 237 | pstore (0.2.0) 238 | public_suffix (6.0.2) 239 | rake (13.3.0) 240 | rchardet (1.9.0) 241 | representable (3.2.0) 242 | declarative (< 0.1.0) 243 | trailblazer-option (>= 0.1.1, < 0.2.0) 244 | uber (< 0.2.0) 245 | retriable (3.1.2) 246 | rexml (3.4.2) 247 | rouge (3.28.0) 248 | ruby2_keywords (0.0.5) 249 | rubyzip (2.4.1) 250 | sawyer (0.9.2) 251 | addressable (>= 2.3.5) 252 | faraday (>= 0.17.3, < 3) 253 | securerandom (0.4.1) 254 | security (0.1.5) 255 | signet (0.20.0) 256 | addressable (~> 2.8) 257 | faraday (>= 0.17.5, < 3.a) 258 | jwt (>= 1.5, < 3.0) 259 | multi_json (~> 1.10) 260 | simctl (1.6.10) 261 | CFPropertyList 262 | naturally 263 | sysrandom (1.0.5) 264 | terminal-notifier (2.0.0) 265 | terminal-table (3.0.2) 266 | unicode-display_width (>= 1.1.1, < 3) 267 | thor (1.4.0) 268 | trailblazer-option (0.1.2) 269 | tty-cursor (0.7.1) 270 | tty-screen (0.8.2) 271 | tty-spinner (0.9.3) 272 | tty-cursor (~> 0.7) 273 | tzinfo (2.0.6) 274 | concurrent-ruby (~> 1.0) 275 | uber (0.1.0) 276 | unicode-display_width (2.6.0) 277 | uri (1.0.3) 278 | word_wrap (1.0.0) 279 | xcodeproj (1.27.0) 280 | CFPropertyList (>= 2.3.3, < 4.0) 281 | atomos (~> 0.1.3) 282 | claide (>= 1.0.2, < 2.0) 283 | colored2 (~> 3.1) 284 | nanaimo (~> 0.4.0) 285 | rexml (>= 3.3.6, < 4.0) 286 | xcpretty (0.4.1) 287 | rouge (~> 3.28.0) 288 | xcpretty-travis-formatter (1.0.1) 289 | xcpretty (~> 0.2, >= 0.0.7) 290 | 291 | PLATFORMS 292 | ruby 293 | 294 | DEPENDENCIES 295 | danger 296 | danger-swiftlint 297 | fastlane 298 | 299 | BUNDLED WITH 300 | 2.1.4 301 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Agrume 2 | 3 | [![Build Status](https://travis-ci.org/JanGorman/Agrume.svg?branch=master)](https://travis-ci.org/JanGorman/Agrume) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 4 | [![Version](https://img.shields.io/cocoapods/v/Agrume.svg?style=flat)](http://cocoapods.org/pods/Agrume) 5 | [![License](https://img.shields.io/cocoapods/l/Agrume.svg?style=flat)](http://cocoapods.org/pods/Agrume) 6 | [![Platform](https://img.shields.io/cocoapods/p/Agrume.svg?style=flat)](http://cocoapods.org/pods/Agrume) 7 | [![SPM](https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager) 8 | 9 | An iOS image viewer written in Swift with support for multiple images. 10 | 11 | ![Agrume](https://www.dropbox.com/s/bdt6sphcyloa38u/Agrume.gif?raw=1) 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 | --------------------------------------------------------------------------------