├── .gitignore ├── CODE_OF_CONDUCT.md ├── InteractiveImageView.podspec ├── InteractiveImageView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── InteractiveImageView.xcscheme ├── InteractiveImageView.xcscheme ├── InteractiveImageViewExample ├── InteractiveImageViewExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── egzonpllana.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist └── InteractiveImageViewExample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 180.png │ │ ├── 29.png │ │ ├── 40.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 80.png │ │ ├── 87.png │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── Resources │ └── image.png │ ├── SceneDelegate.swift │ └── ViewControllers │ └── ViewController.swift ├── LICENSE ├── LICENSE.md ├── Package.swift ├── README.md ├── Sources └── InteractiveImageView │ ├── Enums │ ├── IIVContentMode.swift │ ├── IIVFailType.swift │ └── IIVFocusOffset.swift │ ├── Helpers │ ├── IIVCropHandler.swift │ ├── IIVImageRect.swift │ └── UIImage+extensions.swift │ ├── InteractiveImageView.swift │ ├── PrivacyInfo.xcprivacy │ └── Protocols │ └── InteractiveImageViewProtocol.swift ├── example-preview.gif └── logo.png /.gitignore: -------------------------------------------------------------------------------- 1 | ## Build generated 2 | build/ 3 | DerivedData/ 4 | */xcuserdata/* 5 | 6 | # Swift Package Manager 7 | # 8 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 9 | # Packages/ 10 | .build/ 11 | .swiftpm/ 12 | 13 | # CocoaPods 14 | # 15 | # We recommend against adding the Pods directory to your .gitignore. However 16 | # you should judge for yourself, the pros and cons are mentioned at: 17 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 18 | # 19 | # Pods/ 20 | 21 | # Carthage 22 | # 23 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 24 | # Carthage/Checkouts 25 | 26 | Carthage/Build -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | docpllana@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /InteractiveImageView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'InteractiveImageView' 3 | s.version = '1.0.28' 4 | s.summary = 'Simple UIView to interact with UIImageView like scroll, zoom, rotate, pinch and crop.' 5 | s.homepage = 'https://github.com/egzonpllana/InteractiveImageView' 6 | s.license = { :type => 'MIT', :file => 'LICENSE.md' } 7 | s.author = { 'Egzon Pllana' => 'docpllana@gmail.com' } 8 | s.source = { :git => 'https://github.com/egzonpllana/InteractiveImageView.git', :tag => s.version.to_s } 9 | s.ios.deployment_target = '11.0' 10 | s.swift_version = '5.0' 11 | s.source_files = 'Sources/InteractiveImageView/**/*' 12 | end 13 | -------------------------------------------------------------------------------- /InteractiveImageView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 52EECA2028B80F5F005C92BA /* LICENSE.md in Resources */ = {isa = PBXBuildFile; fileRef = 52EECA1728B80F5F005C92BA /* LICENSE.md */; }; 11 | 52EECA2128B80F5F005C92BA /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 52EECA1828B80F5F005C92BA /* logo.png */; }; 12 | 52EECA2228B80F5F005C92BA /* Sources in Resources */ = {isa = PBXBuildFile; fileRef = 52EECA1928B80F5F005C92BA /* Sources */; }; 13 | 52EECA2328B80F5F005C92BA /* CODE_OF_CONDUCT.md in Resources */ = {isa = PBXBuildFile; fileRef = 52EECA1A28B80F5F005C92BA /* CODE_OF_CONDUCT.md */; }; 14 | 52EECA2528B80F5F005C92BA /* InteractiveImageView.xcscheme in Resources */ = {isa = PBXBuildFile; fileRef = 52EECA1C28B80F5F005C92BA /* InteractiveImageView.xcscheme */; }; 15 | 52EECA2628B80F5F005C92BA /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 52EECA1D28B80F5F005C92BA /* README.md */; }; 16 | 52EECA2728B80F5F005C92BA /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 52EECA1E28B80F5F005C92BA /* LICENSE */; }; 17 | 52EECA2828B80F5F005C92BA /* InteractiveImageView.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 52EECA1F28B80F5F005C92BA /* InteractiveImageView.podspec */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 52EECA0D28B80F26005C92BA /* InteractiveImageView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = InteractiveImageView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 52EECA1728B80F5F005C92BA /* LICENSE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 23 | 52EECA1828B80F5F005C92BA /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo.png; sourceTree = ""; }; 24 | 52EECA1928B80F5F005C92BA /* Sources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Sources; sourceTree = ""; }; 25 | 52EECA1A28B80F5F005C92BA /* CODE_OF_CONDUCT.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CODE_OF_CONDUCT.md; sourceTree = ""; }; 26 | 52EECA1C28B80F5F005C92BA /* InteractiveImageView.xcscheme */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = InteractiveImageView.xcscheme; sourceTree = ""; }; 27 | 52EECA1D28B80F5F005C92BA /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 28 | 52EECA1E28B80F5F005C92BA /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 29 | 52EECA1F28B80F5F005C92BA /* InteractiveImageView.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = InteractiveImageView.podspec; sourceTree = ""; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFrameworksBuildPhase section */ 33 | 52EECA0A28B80F26005C92BA /* Frameworks */ = { 34 | isa = PBXFrameworksBuildPhase; 35 | buildActionMask = 2147483647; 36 | files = ( 37 | ); 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXFrameworksBuildPhase section */ 41 | 42 | /* Begin PBXGroup section */ 43 | 52EECA0328B80F26005C92BA = { 44 | isa = PBXGroup; 45 | children = ( 46 | 52EECA1A28B80F5F005C92BA /* CODE_OF_CONDUCT.md */, 47 | 52EECA1F28B80F5F005C92BA /* InteractiveImageView.podspec */, 48 | 52EECA1C28B80F5F005C92BA /* InteractiveImageView.xcscheme */, 49 | 52EECA1E28B80F5F005C92BA /* LICENSE */, 50 | 52EECA1728B80F5F005C92BA /* LICENSE.md */, 51 | 52EECA1828B80F5F005C92BA /* logo.png */, 52 | 52EECA1D28B80F5F005C92BA /* README.md */, 53 | 52EECA1928B80F5F005C92BA /* Sources */, 54 | 52EECA0E28B80F26005C92BA /* Products */, 55 | ); 56 | sourceTree = ""; 57 | }; 58 | 52EECA0E28B80F26005C92BA /* Products */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 52EECA0D28B80F26005C92BA /* InteractiveImageView.framework */, 62 | ); 63 | name = Products; 64 | sourceTree = ""; 65 | }; 66 | /* End PBXGroup section */ 67 | 68 | /* Begin PBXHeadersBuildPhase section */ 69 | 52EECA0828B80F26005C92BA /* Headers */ = { 70 | isa = PBXHeadersBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | /* End PBXHeadersBuildPhase section */ 77 | 78 | /* Begin PBXNativeTarget section */ 79 | 52EECA0C28B80F26005C92BA /* InteractiveImageView */ = { 80 | isa = PBXNativeTarget; 81 | buildConfigurationList = 52EECA1428B80F26005C92BA /* Build configuration list for PBXNativeTarget "InteractiveImageView" */; 82 | buildPhases = ( 83 | 52EECA0828B80F26005C92BA /* Headers */, 84 | 52EECA0928B80F26005C92BA /* Sources */, 85 | 52EECA0A28B80F26005C92BA /* Frameworks */, 86 | 52EECA0B28B80F26005C92BA /* Resources */, 87 | ); 88 | buildRules = ( 89 | ); 90 | dependencies = ( 91 | ); 92 | name = InteractiveImageView; 93 | productName = InteractiveImageView; 94 | productReference = 52EECA0D28B80F26005C92BA /* InteractiveImageView.framework */; 95 | productType = "com.apple.product-type.framework"; 96 | }; 97 | /* End PBXNativeTarget section */ 98 | 99 | /* Begin PBXProject section */ 100 | 52EECA0428B80F26005C92BA /* Project object */ = { 101 | isa = PBXProject; 102 | attributes = { 103 | BuildIndependentTargetsInParallel = 1; 104 | LastUpgradeCheck = 1340; 105 | TargetAttributes = { 106 | 52EECA0C28B80F26005C92BA = { 107 | CreatedOnToolsVersion = 13.4; 108 | LastSwiftMigration = 1340; 109 | }; 110 | }; 111 | }; 112 | buildConfigurationList = 52EECA0728B80F26005C92BA /* Build configuration list for PBXProject "InteractiveImageView" */; 113 | compatibilityVersion = "Xcode 13.0"; 114 | developmentRegion = en; 115 | hasScannedForEncodings = 0; 116 | knownRegions = ( 117 | en, 118 | Base, 119 | ); 120 | mainGroup = 52EECA0328B80F26005C92BA; 121 | productRefGroup = 52EECA0E28B80F26005C92BA /* Products */; 122 | projectDirPath = ""; 123 | projectRoot = ""; 124 | targets = ( 125 | 52EECA0C28B80F26005C92BA /* InteractiveImageView */, 126 | ); 127 | }; 128 | /* End PBXProject section */ 129 | 130 | /* Begin PBXResourcesBuildPhase section */ 131 | 52EECA0B28B80F26005C92BA /* Resources */ = { 132 | isa = PBXResourcesBuildPhase; 133 | buildActionMask = 2147483647; 134 | files = ( 135 | 52EECA2628B80F5F005C92BA /* README.md in Resources */, 136 | 52EECA2228B80F5F005C92BA /* Sources in Resources */, 137 | 52EECA2128B80F5F005C92BA /* logo.png in Resources */, 138 | 52EECA2728B80F5F005C92BA /* LICENSE in Resources */, 139 | 52EECA2828B80F5F005C92BA /* InteractiveImageView.podspec in Resources */, 140 | 52EECA2528B80F5F005C92BA /* InteractiveImageView.xcscheme in Resources */, 141 | 52EECA2028B80F5F005C92BA /* LICENSE.md in Resources */, 142 | 52EECA2328B80F5F005C92BA /* CODE_OF_CONDUCT.md in Resources */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | /* End PBXResourcesBuildPhase section */ 147 | 148 | /* Begin PBXSourcesBuildPhase section */ 149 | 52EECA0928B80F26005C92BA /* Sources */ = { 150 | isa = PBXSourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | }; 156 | /* End PBXSourcesBuildPhase section */ 157 | 158 | /* Begin XCBuildConfiguration section */ 159 | 52EECA1228B80F26005C92BA /* Debug */ = { 160 | isa = XCBuildConfiguration; 161 | buildSettings = { 162 | ALWAYS_SEARCH_USER_PATHS = NO; 163 | CLANG_ANALYZER_NONNULL = YES; 164 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 165 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 166 | CLANG_ENABLE_MODULES = YES; 167 | CLANG_ENABLE_OBJC_ARC = YES; 168 | CLANG_ENABLE_OBJC_WEAK = YES; 169 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 170 | CLANG_WARN_BOOL_CONVERSION = YES; 171 | CLANG_WARN_COMMA = YES; 172 | CLANG_WARN_CONSTANT_CONVERSION = YES; 173 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 174 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 175 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 176 | CLANG_WARN_EMPTY_BODY = YES; 177 | CLANG_WARN_ENUM_CONVERSION = YES; 178 | CLANG_WARN_INFINITE_RECURSION = YES; 179 | CLANG_WARN_INT_CONVERSION = YES; 180 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 181 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 182 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 183 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 184 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 185 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 186 | CLANG_WARN_STRICT_PROTOTYPES = YES; 187 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 188 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 189 | CLANG_WARN_UNREACHABLE_CODE = YES; 190 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 191 | COPY_PHASE_STRIP = NO; 192 | CURRENT_PROJECT_VERSION = 1; 193 | DEBUG_INFORMATION_FORMAT = dwarf; 194 | ENABLE_STRICT_OBJC_MSGSEND = YES; 195 | ENABLE_TESTABILITY = YES; 196 | GCC_C_LANGUAGE_STANDARD = gnu11; 197 | GCC_DYNAMIC_NO_PIC = NO; 198 | GCC_NO_COMMON_BLOCKS = YES; 199 | GCC_OPTIMIZATION_LEVEL = 0; 200 | GCC_PREPROCESSOR_DEFINITIONS = ( 201 | "DEBUG=1", 202 | "$(inherited)", 203 | ); 204 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 205 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 206 | GCC_WARN_UNDECLARED_SELECTOR = YES; 207 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 208 | GCC_WARN_UNUSED_FUNCTION = YES; 209 | GCC_WARN_UNUSED_VARIABLE = YES; 210 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 211 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 212 | MTL_FAST_MATH = YES; 213 | ONLY_ACTIVE_ARCH = YES; 214 | SDKROOT = iphoneos; 215 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 216 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 217 | VERSIONING_SYSTEM = "apple-generic"; 218 | VERSION_INFO_PREFIX = ""; 219 | }; 220 | name = Debug; 221 | }; 222 | 52EECA1328B80F26005C92BA /* Release */ = { 223 | isa = XCBuildConfiguration; 224 | buildSettings = { 225 | ALWAYS_SEARCH_USER_PATHS = NO; 226 | CLANG_ANALYZER_NONNULL = YES; 227 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 228 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 229 | CLANG_ENABLE_MODULES = YES; 230 | CLANG_ENABLE_OBJC_ARC = YES; 231 | CLANG_ENABLE_OBJC_WEAK = YES; 232 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 233 | CLANG_WARN_BOOL_CONVERSION = YES; 234 | CLANG_WARN_COMMA = YES; 235 | CLANG_WARN_CONSTANT_CONVERSION = YES; 236 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 237 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 238 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 239 | CLANG_WARN_EMPTY_BODY = YES; 240 | CLANG_WARN_ENUM_CONVERSION = YES; 241 | CLANG_WARN_INFINITE_RECURSION = YES; 242 | CLANG_WARN_INT_CONVERSION = YES; 243 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 244 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 245 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 246 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 247 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 248 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 249 | CLANG_WARN_STRICT_PROTOTYPES = YES; 250 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 251 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 252 | CLANG_WARN_UNREACHABLE_CODE = YES; 253 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 254 | COPY_PHASE_STRIP = NO; 255 | CURRENT_PROJECT_VERSION = 1; 256 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 257 | ENABLE_NS_ASSERTIONS = NO; 258 | ENABLE_STRICT_OBJC_MSGSEND = YES; 259 | GCC_C_LANGUAGE_STANDARD = gnu11; 260 | GCC_NO_COMMON_BLOCKS = YES; 261 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 262 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 263 | GCC_WARN_UNDECLARED_SELECTOR = YES; 264 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 265 | GCC_WARN_UNUSED_FUNCTION = YES; 266 | GCC_WARN_UNUSED_VARIABLE = YES; 267 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 268 | MTL_ENABLE_DEBUG_INFO = NO; 269 | MTL_FAST_MATH = YES; 270 | SDKROOT = iphoneos; 271 | SWIFT_COMPILATION_MODE = wholemodule; 272 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 273 | VALIDATE_PRODUCT = YES; 274 | VERSIONING_SYSTEM = "apple-generic"; 275 | VERSION_INFO_PREFIX = ""; 276 | }; 277 | name = Release; 278 | }; 279 | 52EECA1528B80F26005C92BA /* Debug */ = { 280 | isa = XCBuildConfiguration; 281 | buildSettings = { 282 | CLANG_ENABLE_MODULES = YES; 283 | CODE_SIGN_STYLE = Automatic; 284 | CURRENT_PROJECT_VERSION = 1; 285 | DEFINES_MODULE = YES; 286 | DEVELOPMENT_TEAM = YUQDS8QJFD; 287 | DYLIB_COMPATIBILITY_VERSION = 1; 288 | DYLIB_CURRENT_VERSION = 1; 289 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 290 | GENERATE_INFOPLIST_FILE = YES; 291 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 292 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 293 | LD_RUNPATH_SEARCH_PATHS = ( 294 | "$(inherited)", 295 | "@executable_path/Frameworks", 296 | "@loader_path/Frameworks", 297 | ); 298 | MARKETING_VERSION = 1.0; 299 | PRODUCT_BUNDLE_IDENTIFIER = com.nativecoders.InteractiveImageView; 300 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 301 | SKIP_INSTALL = YES; 302 | SWIFT_EMIT_LOC_STRINGS = YES; 303 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 304 | SWIFT_VERSION = 5.0; 305 | TARGETED_DEVICE_FAMILY = "1,2"; 306 | }; 307 | name = Debug; 308 | }; 309 | 52EECA1628B80F26005C92BA /* Release */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | CLANG_ENABLE_MODULES = YES; 313 | CODE_SIGN_STYLE = Automatic; 314 | CURRENT_PROJECT_VERSION = 1; 315 | DEFINES_MODULE = YES; 316 | DEVELOPMENT_TEAM = YUQDS8QJFD; 317 | DYLIB_COMPATIBILITY_VERSION = 1; 318 | DYLIB_CURRENT_VERSION = 1; 319 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 320 | GENERATE_INFOPLIST_FILE = YES; 321 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 322 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 323 | LD_RUNPATH_SEARCH_PATHS = ( 324 | "$(inherited)", 325 | "@executable_path/Frameworks", 326 | "@loader_path/Frameworks", 327 | ); 328 | MARKETING_VERSION = 1.0; 329 | PRODUCT_BUNDLE_IDENTIFIER = com.nativecoders.InteractiveImageView; 330 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 331 | SKIP_INSTALL = YES; 332 | SWIFT_EMIT_LOC_STRINGS = YES; 333 | SWIFT_VERSION = 5.0; 334 | TARGETED_DEVICE_FAMILY = "1,2"; 335 | }; 336 | name = Release; 337 | }; 338 | /* End XCBuildConfiguration section */ 339 | 340 | /* Begin XCConfigurationList section */ 341 | 52EECA0728B80F26005C92BA /* Build configuration list for PBXProject "InteractiveImageView" */ = { 342 | isa = XCConfigurationList; 343 | buildConfigurations = ( 344 | 52EECA1228B80F26005C92BA /* Debug */, 345 | 52EECA1328B80F26005C92BA /* Release */, 346 | ); 347 | defaultConfigurationIsVisible = 0; 348 | defaultConfigurationName = Release; 349 | }; 350 | 52EECA1428B80F26005C92BA /* Build configuration list for PBXNativeTarget "InteractiveImageView" */ = { 351 | isa = XCConfigurationList; 352 | buildConfigurations = ( 353 | 52EECA1528B80F26005C92BA /* Debug */, 354 | 52EECA1628B80F26005C92BA /* Release */, 355 | ); 356 | defaultConfigurationIsVisible = 0; 357 | defaultConfigurationName = Release; 358 | }; 359 | /* End XCConfigurationList section */ 360 | }; 361 | rootObject = 52EECA0428B80F26005C92BA /* Project object */; 362 | } 363 | -------------------------------------------------------------------------------- /InteractiveImageView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /InteractiveImageView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /InteractiveImageView.xcodeproj/xcshareddata/xcschemes/InteractiveImageView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /InteractiveImageView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5242FC9328B81E6300CFD51A /* image.png in Resources */ = {isa = PBXBuildFile; fileRef = 5242FC9228B81E6300CFD51A /* image.png */; }; 11 | 52EEC9EA28B80733005C92BA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EEC9E928B80733005C92BA /* AppDelegate.swift */; }; 12 | 52EEC9EC28B80733005C92BA /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EEC9EB28B80733005C92BA /* SceneDelegate.swift */; }; 13 | 52EEC9EE28B80733005C92BA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EEC9ED28B80733005C92BA /* ViewController.swift */; }; 14 | 52EEC9F128B80733005C92BA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 52EEC9EF28B80733005C92BA /* Main.storyboard */; }; 15 | 52EEC9F328B80734005C92BA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 52EEC9F228B80734005C92BA /* Assets.xcassets */; }; 16 | 52EEC9F628B80734005C92BA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 52EEC9F428B80734005C92BA /* LaunchScreen.storyboard */; }; 17 | 52F88FC928CE0EB700B334A8 /* InteractiveImageView in Frameworks */ = {isa = PBXBuildFile; productRef = 52F88FC828CE0EB700B334A8 /* InteractiveImageView */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 5242FC9228B81E6300CFD51A /* image.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = image.png; sourceTree = ""; }; 22 | 52EEC9E628B80733005C92BA /* InteractiveImageViewExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InteractiveImageViewExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 52EEC9E928B80733005C92BA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | 52EEC9EB28B80733005C92BA /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 25 | 52EEC9ED28B80733005C92BA /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 26 | 52EEC9F028B80733005C92BA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 27 | 52EEC9F228B80734005C92BA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | 52EEC9F528B80734005C92BA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 29 | 52EEC9F728B80734005C92BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFrameworksBuildPhase section */ 33 | 52EEC9E328B80733005C92BA /* Frameworks */ = { 34 | isa = PBXFrameworksBuildPhase; 35 | buildActionMask = 2147483647; 36 | files = ( 37 | 52F88FC928CE0EB700B334A8 /* InteractiveImageView in Frameworks */, 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 5242FC8E28B81AC000CFD51A /* ViewControllers */ = { 45 | isa = PBXGroup; 46 | children = ( 47 | 52EEC9ED28B80733005C92BA /* ViewController.swift */, 48 | ); 49 | path = ViewControllers; 50 | sourceTree = ""; 51 | }; 52 | 5242FC8F28B81AC800CFD51A /* Resources */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 5242FC9228B81E6300CFD51A /* image.png */, 56 | ); 57 | path = Resources; 58 | sourceTree = ""; 59 | }; 60 | 52EEC9DD28B80733005C92BA = { 61 | isa = PBXGroup; 62 | children = ( 63 | 52EEC9E828B80733005C92BA /* InteractiveImageViewExample */, 64 | 52EEC9E728B80733005C92BA /* Products */, 65 | ); 66 | sourceTree = ""; 67 | }; 68 | 52EEC9E728B80733005C92BA /* Products */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 52EEC9E628B80733005C92BA /* InteractiveImageViewExample.app */, 72 | ); 73 | name = Products; 74 | sourceTree = ""; 75 | }; 76 | 52EEC9E828B80733005C92BA /* InteractiveImageViewExample */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 52EEC9E928B80733005C92BA /* AppDelegate.swift */, 80 | 52EEC9EB28B80733005C92BA /* SceneDelegate.swift */, 81 | 5242FC8E28B81AC000CFD51A /* ViewControllers */, 82 | 5242FC8F28B81AC800CFD51A /* Resources */, 83 | 52EEC9EF28B80733005C92BA /* Main.storyboard */, 84 | 52EEC9F228B80734005C92BA /* Assets.xcassets */, 85 | 52EEC9F428B80734005C92BA /* LaunchScreen.storyboard */, 86 | 52EEC9F728B80734005C92BA /* Info.plist */, 87 | ); 88 | path = InteractiveImageViewExample; 89 | sourceTree = ""; 90 | }; 91 | /* End PBXGroup section */ 92 | 93 | /* Begin PBXNativeTarget section */ 94 | 52EEC9E528B80733005C92BA /* InteractiveImageViewExample */ = { 95 | isa = PBXNativeTarget; 96 | buildConfigurationList = 52EEC9FA28B80734005C92BA /* Build configuration list for PBXNativeTarget "InteractiveImageViewExample" */; 97 | buildPhases = ( 98 | 52EEC9E228B80733005C92BA /* Sources */, 99 | 52EEC9E328B80733005C92BA /* Frameworks */, 100 | 52EEC9E428B80733005C92BA /* Resources */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = InteractiveImageViewExample; 107 | packageProductDependencies = ( 108 | 52F88FC828CE0EB700B334A8 /* InteractiveImageView */, 109 | ); 110 | productName = InteractiveImageViewExample; 111 | productReference = 52EEC9E628B80733005C92BA /* InteractiveImageViewExample.app */; 112 | productType = "com.apple.product-type.application"; 113 | }; 114 | /* End PBXNativeTarget section */ 115 | 116 | /* Begin PBXProject section */ 117 | 52EEC9DE28B80733005C92BA /* Project object */ = { 118 | isa = PBXProject; 119 | attributes = { 120 | BuildIndependentTargetsInParallel = 1; 121 | LastSwiftUpdateCheck = 1340; 122 | LastUpgradeCheck = 1340; 123 | TargetAttributes = { 124 | 52EEC9E528B80733005C92BA = { 125 | CreatedOnToolsVersion = 13.4; 126 | }; 127 | }; 128 | }; 129 | buildConfigurationList = 52EEC9E128B80733005C92BA /* Build configuration list for PBXProject "InteractiveImageViewExample" */; 130 | compatibilityVersion = "Xcode 13.0"; 131 | developmentRegion = en; 132 | hasScannedForEncodings = 0; 133 | knownRegions = ( 134 | en, 135 | Base, 136 | ); 137 | mainGroup = 52EEC9DD28B80733005C92BA; 138 | packageReferences = ( 139 | 52F88FC728CE0EB700B334A8 /* XCRemoteSwiftPackageReference "InteractiveImageView" */, 140 | ); 141 | productRefGroup = 52EEC9E728B80733005C92BA /* Products */; 142 | projectDirPath = ""; 143 | projectRoot = ""; 144 | targets = ( 145 | 52EEC9E528B80733005C92BA /* InteractiveImageViewExample */, 146 | ); 147 | }; 148 | /* End PBXProject section */ 149 | 150 | /* Begin PBXResourcesBuildPhase section */ 151 | 52EEC9E428B80733005C92BA /* Resources */ = { 152 | isa = PBXResourcesBuildPhase; 153 | buildActionMask = 2147483647; 154 | files = ( 155 | 52EEC9F628B80734005C92BA /* LaunchScreen.storyboard in Resources */, 156 | 52EEC9F328B80734005C92BA /* Assets.xcassets in Resources */, 157 | 52EEC9F128B80733005C92BA /* Main.storyboard in Resources */, 158 | 5242FC9328B81E6300CFD51A /* image.png in Resources */, 159 | ); 160 | runOnlyForDeploymentPostprocessing = 0; 161 | }; 162 | /* End PBXResourcesBuildPhase section */ 163 | 164 | /* Begin PBXSourcesBuildPhase section */ 165 | 52EEC9E228B80733005C92BA /* Sources */ = { 166 | isa = PBXSourcesBuildPhase; 167 | buildActionMask = 2147483647; 168 | files = ( 169 | 52EEC9EE28B80733005C92BA /* ViewController.swift in Sources */, 170 | 52EEC9EA28B80733005C92BA /* AppDelegate.swift in Sources */, 171 | 52EEC9EC28B80733005C92BA /* SceneDelegate.swift in Sources */, 172 | ); 173 | runOnlyForDeploymentPostprocessing = 0; 174 | }; 175 | /* End PBXSourcesBuildPhase section */ 176 | 177 | /* Begin PBXVariantGroup section */ 178 | 52EEC9EF28B80733005C92BA /* Main.storyboard */ = { 179 | isa = PBXVariantGroup; 180 | children = ( 181 | 52EEC9F028B80733005C92BA /* Base */, 182 | ); 183 | name = Main.storyboard; 184 | sourceTree = ""; 185 | }; 186 | 52EEC9F428B80734005C92BA /* LaunchScreen.storyboard */ = { 187 | isa = PBXVariantGroup; 188 | children = ( 189 | 52EEC9F528B80734005C92BA /* Base */, 190 | ); 191 | name = LaunchScreen.storyboard; 192 | sourceTree = ""; 193 | }; 194 | /* End PBXVariantGroup section */ 195 | 196 | /* Begin XCBuildConfiguration section */ 197 | 52EEC9F828B80734005C92BA /* Debug */ = { 198 | isa = XCBuildConfiguration; 199 | buildSettings = { 200 | ALWAYS_SEARCH_USER_PATHS = NO; 201 | CLANG_ANALYZER_NONNULL = YES; 202 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 203 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 204 | CLANG_ENABLE_MODULES = YES; 205 | CLANG_ENABLE_OBJC_ARC = YES; 206 | CLANG_ENABLE_OBJC_WEAK = YES; 207 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 208 | CLANG_WARN_BOOL_CONVERSION = YES; 209 | CLANG_WARN_COMMA = YES; 210 | CLANG_WARN_CONSTANT_CONVERSION = YES; 211 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 212 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 213 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 214 | CLANG_WARN_EMPTY_BODY = YES; 215 | CLANG_WARN_ENUM_CONVERSION = YES; 216 | CLANG_WARN_INFINITE_RECURSION = YES; 217 | CLANG_WARN_INT_CONVERSION = YES; 218 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 219 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 220 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 221 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 222 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 223 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 224 | CLANG_WARN_STRICT_PROTOTYPES = YES; 225 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 226 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 227 | CLANG_WARN_UNREACHABLE_CODE = YES; 228 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 229 | COPY_PHASE_STRIP = NO; 230 | DEBUG_INFORMATION_FORMAT = dwarf; 231 | ENABLE_STRICT_OBJC_MSGSEND = YES; 232 | ENABLE_TESTABILITY = YES; 233 | GCC_C_LANGUAGE_STANDARD = gnu11; 234 | GCC_DYNAMIC_NO_PIC = NO; 235 | GCC_NO_COMMON_BLOCKS = YES; 236 | GCC_OPTIMIZATION_LEVEL = 0; 237 | GCC_PREPROCESSOR_DEFINITIONS = ( 238 | "DEBUG=1", 239 | "$(inherited)", 240 | ); 241 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 242 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 243 | GCC_WARN_UNDECLARED_SELECTOR = YES; 244 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 245 | GCC_WARN_UNUSED_FUNCTION = YES; 246 | GCC_WARN_UNUSED_VARIABLE = YES; 247 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 248 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 249 | MTL_FAST_MATH = YES; 250 | ONLY_ACTIVE_ARCH = YES; 251 | SDKROOT = iphoneos; 252 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 253 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 254 | }; 255 | name = Debug; 256 | }; 257 | 52EEC9F928B80734005C92BA /* Release */ = { 258 | isa = XCBuildConfiguration; 259 | buildSettings = { 260 | ALWAYS_SEARCH_USER_PATHS = NO; 261 | CLANG_ANALYZER_NONNULL = YES; 262 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 263 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 264 | CLANG_ENABLE_MODULES = YES; 265 | CLANG_ENABLE_OBJC_ARC = YES; 266 | CLANG_ENABLE_OBJC_WEAK = YES; 267 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 268 | CLANG_WARN_BOOL_CONVERSION = YES; 269 | CLANG_WARN_COMMA = YES; 270 | CLANG_WARN_CONSTANT_CONVERSION = YES; 271 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 272 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 273 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 274 | CLANG_WARN_EMPTY_BODY = YES; 275 | CLANG_WARN_ENUM_CONVERSION = YES; 276 | CLANG_WARN_INFINITE_RECURSION = YES; 277 | CLANG_WARN_INT_CONVERSION = YES; 278 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 279 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 280 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 281 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 282 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 283 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 284 | CLANG_WARN_STRICT_PROTOTYPES = YES; 285 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 286 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 287 | CLANG_WARN_UNREACHABLE_CODE = YES; 288 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 289 | COPY_PHASE_STRIP = NO; 290 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 291 | ENABLE_NS_ASSERTIONS = NO; 292 | ENABLE_STRICT_OBJC_MSGSEND = YES; 293 | GCC_C_LANGUAGE_STANDARD = gnu11; 294 | GCC_NO_COMMON_BLOCKS = YES; 295 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 296 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 297 | GCC_WARN_UNDECLARED_SELECTOR = YES; 298 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 299 | GCC_WARN_UNUSED_FUNCTION = YES; 300 | GCC_WARN_UNUSED_VARIABLE = YES; 301 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 302 | MTL_ENABLE_DEBUG_INFO = NO; 303 | MTL_FAST_MATH = YES; 304 | SDKROOT = iphoneos; 305 | SWIFT_COMPILATION_MODE = wholemodule; 306 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 307 | VALIDATE_PRODUCT = YES; 308 | }; 309 | name = Release; 310 | }; 311 | 52EEC9FB28B80734005C92BA /* Debug */ = { 312 | isa = XCBuildConfiguration; 313 | buildSettings = { 314 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 315 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 316 | CODE_SIGN_STYLE = Automatic; 317 | CURRENT_PROJECT_VERSION = 1; 318 | DEVELOPMENT_TEAM = ""; 319 | GENERATE_INFOPLIST_FILE = YES; 320 | INFOPLIST_FILE = InteractiveImageViewExample/Info.plist; 321 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 322 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 323 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 324 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 325 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 326 | LD_RUNPATH_SEARCH_PATHS = ( 327 | "$(inherited)", 328 | "@executable_path/Frameworks", 329 | ); 330 | MARKETING_VERSION = 1.0; 331 | PRODUCT_BUNDLE_IDENTIFIER = com.egzonpllana.InteractiveImageViewExample; 332 | PRODUCT_NAME = "$(TARGET_NAME)"; 333 | SWIFT_EMIT_LOC_STRINGS = YES; 334 | SWIFT_VERSION = 5.0; 335 | TARGETED_DEVICE_FAMILY = 1; 336 | }; 337 | name = Debug; 338 | }; 339 | 52EEC9FC28B80734005C92BA /* Release */ = { 340 | isa = XCBuildConfiguration; 341 | buildSettings = { 342 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 343 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 344 | CODE_SIGN_STYLE = Automatic; 345 | CURRENT_PROJECT_VERSION = 1; 346 | DEVELOPMENT_TEAM = ""; 347 | GENERATE_INFOPLIST_FILE = YES; 348 | INFOPLIST_FILE = InteractiveImageViewExample/Info.plist; 349 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 350 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 351 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 352 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 353 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 354 | LD_RUNPATH_SEARCH_PATHS = ( 355 | "$(inherited)", 356 | "@executable_path/Frameworks", 357 | ); 358 | MARKETING_VERSION = 1.0; 359 | PRODUCT_BUNDLE_IDENTIFIER = com.egzonpllana.InteractiveImageViewExample; 360 | PRODUCT_NAME = "$(TARGET_NAME)"; 361 | SWIFT_EMIT_LOC_STRINGS = YES; 362 | SWIFT_VERSION = 5.0; 363 | TARGETED_DEVICE_FAMILY = 1; 364 | }; 365 | name = Release; 366 | }; 367 | /* End XCBuildConfiguration section */ 368 | 369 | /* Begin XCConfigurationList section */ 370 | 52EEC9E128B80733005C92BA /* Build configuration list for PBXProject "InteractiveImageViewExample" */ = { 371 | isa = XCConfigurationList; 372 | buildConfigurations = ( 373 | 52EEC9F828B80734005C92BA /* Debug */, 374 | 52EEC9F928B80734005C92BA /* Release */, 375 | ); 376 | defaultConfigurationIsVisible = 0; 377 | defaultConfigurationName = Release; 378 | }; 379 | 52EEC9FA28B80734005C92BA /* Build configuration list for PBXNativeTarget "InteractiveImageViewExample" */ = { 380 | isa = XCConfigurationList; 381 | buildConfigurations = ( 382 | 52EEC9FB28B80734005C92BA /* Debug */, 383 | 52EEC9FC28B80734005C92BA /* Release */, 384 | ); 385 | defaultConfigurationIsVisible = 0; 386 | defaultConfigurationName = Release; 387 | }; 388 | /* End XCConfigurationList section */ 389 | 390 | /* Begin XCRemoteSwiftPackageReference section */ 391 | 52F88FC728CE0EB700B334A8 /* XCRemoteSwiftPackageReference "InteractiveImageView" */ = { 392 | isa = XCRemoteSwiftPackageReference; 393 | repositoryURL = "https://github.com/egzonpllana/InteractiveImageView.git"; 394 | requirement = { 395 | kind = upToNextMajorVersion; 396 | minimumVersion = 1.0.0; 397 | }; 398 | }; 399 | /* End XCRemoteSwiftPackageReference section */ 400 | 401 | /* Begin XCSwiftPackageProductDependency section */ 402 | 52F88FC828CE0EB700B334A8 /* InteractiveImageView */ = { 403 | isa = XCSwiftPackageProductDependency; 404 | package = 52F88FC728CE0EB700B334A8 /* XCRemoteSwiftPackageReference "InteractiveImageView" */; 405 | productName = InteractiveImageView; 406 | }; 407 | /* End XCSwiftPackageProductDependency section */ 408 | }; 409 | rootObject = 52EEC9DE28B80733005C92BA /* Project object */; 410 | } 411 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "interactiveimageview", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/egzonpllana/InteractiveImageView.git", 7 | "state" : { 8 | "revision" : "47a28205ab8f2b5a1f6a9adc1b7a446a55f938fc", 9 | "version" : "1.0.27" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample.xcodeproj/xcuserdata/egzonpllana.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample.xcodeproj/xcuserdata/egzonpllana.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | InteractiveImageViewExample.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // InteractiveImageViewExample 4 | // 5 | // Created by Egzon Pllana on 25.8.22. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "58.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "87.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "80.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "57.png", 47 | "idiom" : "iphone", 48 | "scale" : "1x", 49 | "size" : "57x57" 50 | }, 51 | { 52 | "filename" : "114.png", 53 | "idiom" : "iphone", 54 | "scale" : "2x", 55 | "size" : "57x57" 56 | }, 57 | { 58 | "filename" : "120.png", 59 | "idiom" : "iphone", 60 | "scale" : "2x", 61 | "size" : "60x60" 62 | }, 63 | { 64 | "filename" : "180.png", 65 | "idiom" : "iphone", 66 | "scale" : "3x", 67 | "size" : "60x60" 68 | }, 69 | { 70 | "filename" : "1024.png", 71 | "idiom" : "ios-marketing", 72 | "scale" : "1x", 73 | "size" : "1024x1024" 74 | } 75 | ], 76 | "info" : { 77 | "author" : "xcode", 78 | "version" : 1 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 58 | 59 | 60 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/Resources/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/InteractiveImageViewExample/InteractiveImageViewExample/Resources/image.png -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // InteractiveImageViewExample 4 | // 5 | // Created by Egzon Pllana on 25.8.22. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /InteractiveImageViewExample/InteractiveImageViewExample/ViewControllers/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // InteractiveImageViewExample 4 | // 5 | // Created by Egzon Pllana on 25.8.22. 6 | // 7 | 8 | import UIKit 9 | import InteractiveImageView 10 | 11 | class ViewController: UIViewController { 12 | 13 | // MARK: - Outlets 14 | 15 | @IBOutlet private weak var imageView: InteractiveImageView! 16 | @IBOutlet private weak var croppedImageView: UIImageView! 17 | 18 | // MARK: - View life cycle 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | // Do any additional setup after loading the view. 23 | 24 | // Configure InteractiveImageView 25 | configureImageView() 26 | } 27 | 28 | // MARK: - Methods 29 | 30 | private func configureImageView() { 31 | // Setup imageView 32 | let imageExample = #imageLiteral(resourceName: "image.png") 33 | imageView.configure(withNextContentMode: .customOffset(offset: Double(2)/Double(3)), withFocusOffset: .center, withImage: imageExample, withIdentifier: 0) 34 | imageView.delegate = self 35 | 36 | // Controll imageView user actions 37 | imageView.isScrollEnabled = true 38 | imageView.isDoubleTapToZoomAllowed = true 39 | imageView.isPinchAllowed = false 40 | imageView.doubleTapZoomFactor = 3.0 41 | } 42 | 43 | // MARK: - Actions 44 | 45 | 46 | @IBAction func changeContentModePressed(_ sender: Any) { 47 | imageView.toggleImageContentMode() 48 | } 49 | 50 | @IBAction func cropImagePressed(_ sender: Any) { 51 | // Crop: Method 1 52 | imageView.performCropImage() 53 | 54 | // Crop: Method 2 55 | // Use direct crop image method. 56 | // croppedImageView.image = imageView.cropAndGetImage() 57 | } 58 | } 59 | 60 | extension ViewController: InteractiveImageViewDelegate { 61 | func didCropImage(image: UIImage, fromView: InteractiveImageView) { 62 | croppedImageView.image = image 63 | } 64 | 65 | func didScrollAt(offset: CGPoint, scale: CGFloat, fromView: InteractiveImageView) { 66 | // 67 | } 68 | 69 | func didZoomAt(offset: CGPoint, scale: CGFloat, fromView: InteractiveImageView) { 70 | // 71 | } 72 | 73 | func didFail(_ fail: IIVFailType) { 74 | // 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Egzon Pllana 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 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Egzon Pllana 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.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "InteractiveImageView", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "InteractiveImageView", 12 | targets: ["InteractiveImageView"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "InteractiveImageView", 23 | dependencies: []), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | “InteractiveImageView” 3 |

4 | 5 |

6 | 7 | 8 | CocoaPods 9 | 10 | 11 | Carthage 12 | 13 | 14 | Swift Package Manager 15 | 16 |

17 | 18 | Welcome to **Interactive Image View**, a simple library that provides an easier way to interact with image view, like scroll, zoom and crop. In its core it support two image content mode, the one is always square and the second one is custom. For example you can use aspect ration like instagram does 2:3 or 9:16, or any custom value. Basically, it's a thin wrapper around the `UIScrollView` and `UIImageView` APIs that `UIKit` provides. 19 | 20 | ## Features 21 | 22 | - [X] Use at any place as UIView, no need to present or configure a viewcontroller. 23 | - [X] Crop image at current position as user wants. 24 | - [X] Switch between aspect ration, 2:3 or 1:1, same as Instagram. 25 | - [X] Can be extended to support different aspect rations. 26 | - [X] Scroll image view on x and y axis. 27 | - [X] Double tap to zoom in or zoom out. 28 | - [X] Rotate image by given degrees. 29 | - [X] Pinch image view. 30 | 31 | ### Preview 32 |

33 | “InteractiveImageView” 34 |

35 | 36 | ## Setup 37 | 38 | 1. Add a view, and set the class of the view to `InteractiveImageView`. 39 | 2. In your view controller, import InteractiveImageView. 40 | 3. Connect view outlet, configure it with `interactiveImageView.configure(...)` 41 | 4. Add delegates `interactiveImageView.delegate = self` 42 | 5. Listen to delegate observers: `extension ViewController: InteractiveImageViewDelegate { ... }` 43 | 44 | ## Methods 45 | #### Configure view 46 | ```swift 47 | if let image = UIImage(named: "image.png") { 48 | interactiveImageView.configure(withNextContentMode: .heightFill, 49 | withFocusOffset: .center, 50 | withImage: image) 51 | } 52 | ``` 53 | #### Crop and get image without delegate methods 54 | ```swift 55 | let croppedImage = interactiveImageView.cropAndGetImage() 56 | ``` 57 | #### Get original image without any modification on it. 58 | ```swift 59 | let originalImage = interactiveImageView.getOriginalImage() 60 | ``` 61 | #### Set only image in the ImageView. 62 | ```swift 63 | interactiveImageView.updateImageView(withImage image: UIImage?) 64 | ``` 65 | #### Update image in the ImageView. 66 | ```swift 67 | interactiveImageView.uupdateImageOnly(_ image: UIImage?) 68 | ``` 69 | #### Toggle image content mode 70 | ```swift 71 | interactiveImageView.toggleImageContentMode() 72 | ``` 73 | #### Rotate image 74 | ```swift 75 | interactiveImageView.rotateImage(UIImage, keepChanges: Bool) 76 | ``` 77 | 78 | ## User gestures 79 | #### Double tap to zoom gesture 80 | ```swift 81 | interactiveImageView.isDoubleTapToZoomAllowed = false 82 | ``` 83 | #### Scroll view 84 | ```swift 85 | interactiveImageView.isScrollEnabled = false 86 | ``` 87 | #### Pinch to zoom gesture 88 | ```swift 89 | interactiveImageView.isPinchAllowed = false 90 | ``` 91 | ## Delegate methods 92 | ```swift 93 | protocol InteractiveImageViewDelegate: AnyObject { 94 | func didCropImage(image: UIImage, fromView: InteractiveImageView) 95 | func didScrollAt(offset: CGPoint, scale: CGFloat, fromView: InteractiveImageView) 96 | func didZoomAt(offset: CGPoint, scale: CGFloat, fromView: InteractiveImageView) 97 | func didFail(_ fail: IIVFailType) 98 | } 99 | ``` 100 | 101 | ## Example Project 102 | You can download and run example project `InteractiveImageViewExample`. 103 | 104 | ## Installation 105 | 106 | ### CocoaPods 107 | 108 | [CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate InteractiveImageView into your Xcode project using CocoaPods, specify it in your `Podfile`: 109 | 110 | ```ruby 111 | pod 'InteractiveImageView' 112 | ``` 113 | 114 | ### Carthage 115 | 116 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate InteractiveImageView into your Xcode project using Carthage, specify it in your `Cartfile`: 117 | 118 | ```ogdl 119 | github "egzonpllana/InteractiveImageView" 120 | ``` 121 | 122 | ### Swift Package Manager through Manifest File 123 | 124 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. 125 | 126 | Once you have your Swift package set up, adding InteractiveImageView as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. 127 | 128 | ```swift 129 | dependencies: [ 130 | .package(url: "https://github.com/egzonpllana/InteractiveImageView.git", .upToNextMajor(from: "1.0.0")) 131 | ] 132 | ``` 133 | 134 | ### Swift Package Manager through XCode 135 | To add InteractiveImageView as a dependency to your Xcode project, select File > Swift Packages > Add Package Dependency and enter the repository URL 136 | ```ogdl 137 | https://github.com/egzonpllana/InteractiveImageView.git 138 | ``` 139 | 140 | ## Backstory 141 | 142 | So, why was this made? While I was working on a project to provide an interactive image view based on given aspect ration, I could not find a suitable solution that offers all in one these features working in a single view without a need for a viewcontroller, so I build it. 143 | 144 | ## Questions or feedback? 145 | 146 | Feel free to [open an issue](https://github.com/egzonpllana/InteractiveImageView/issues/new), or find me [@egzonpllana on LinkedIn](https://www.linkedin.com/in/egzon-pllana/). 147 | -------------------------------------------------------------------------------- /Sources/InteractiveImageView/Enums/IIVContentMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IIVContentMode.swift 3 | // InteractiveImageView 4 | // 5 | // Created by Egzon Pllana on 28.8.22. 6 | // Copyright © 2022 Egzon Pllana. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public enum IIVContentMode: Equatable { 12 | case aspectFill 13 | case aspectFit 14 | case widthFill 15 | case heightFill 16 | case customOffset(offset: CGFloat) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/InteractiveImageView/Enums/IIVFailType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IIVFailType.swift 3 | // InteractiveImageView 4 | // 5 | // Created by Egzon Pllana on 11.09.22. 6 | // Copyright © 2022 Egzon Pllana. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum IIVFailType { 12 | case cropImageFailed 13 | case toggleContentModeFailed 14 | case adjustFramesWhenZoomingFailed 15 | case getImageViewFailed 16 | case getImageFailed 17 | } 18 | -------------------------------------------------------------------------------- /Sources/InteractiveImageView/Enums/IIVFocusOffset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IIVFocusOffset.swift 3 | // InteractiveImageView 4 | // 5 | // Created by Egzon Pllana on 28.8.22. 6 | // Copyright © 2022 Egzon Pllana. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum IIVFocusOffset: Int { 12 | case begining 13 | case center 14 | } 15 | -------------------------------------------------------------------------------- /Sources/InteractiveImageView/Helpers/IIVCropHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IIVCropHandler.swift 3 | // InteractiveImageView 4 | // 5 | // Created by Egzon Pllana on 28.8.22. 6 | // Copyright © 2022 Egzon Pllana. All rights reserved. 7 | // 8 | 9 | // Inspired by: 10 | // https://developer.apple.com/documentation/coregraphics/cgimage/1454683-cropping 11 | 12 | import UIKit 13 | 14 | public struct IIVCropHandler { 15 | static func cropImage(_ inputImage: UIImage, toRect cropRect: CGRect, viewWidth: CGFloat, viewHeight: CGFloat) -> UIImage? { 16 | let imageViewScale = max(inputImage.size.width / viewWidth, 17 | inputImage.size.height / viewHeight) 18 | 19 | // Scale cropRect to handle images larger than shown-on-screen size 20 | let cropZone = CGRect(x:cropRect.origin.x * imageViewScale, 21 | y:cropRect.origin.y * imageViewScale, 22 | width:cropRect.size.width * imageViewScale, 23 | height:cropRect.size.height * imageViewScale) 24 | 25 | // Perform cropping in Core Graphics 26 | guard let cutImageRef: CGImage = inputImage.cgImage?.cropping(to:cropZone) 27 | else { 28 | return nil 29 | } 30 | 31 | // Return image to UIImage 32 | let croppedImage: UIImage = UIImage(cgImage: cutImageRef) 33 | return croppedImage 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/InteractiveImageView/Helpers/IIVImageRect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IIVImageRect.swift 3 | // InteractiveImageView 4 | // 5 | // Created by Egzon Pllana on 28.8.22. 6 | // Copyright © 2022 Egzon Pllana. All rights reserved. 7 | // 8 | 9 | // Inspired by: 10 | // https://github.com/BasselEzzeddine/PhotoCrop 11 | 12 | import UIKit 13 | 14 | public struct IIVImageRect { 15 | static func getImageRect(fromImageView imageView: UIImageView) -> CGRect { 16 | let imageViewSize = imageView.frame.size 17 | let imgSize = imageView.image?.size 18 | 19 | guard let imageSize = imgSize else { 20 | return CGRect.zero 21 | } 22 | 23 | let scaleWidth = imageViewSize.width / imageSize.width 24 | let scaleHeight = imageViewSize.height / imageSize.height 25 | let aspect = fmin(scaleWidth, scaleHeight) 26 | 27 | var imageRect = CGRect(x: 0, y: 0, width: imageSize.width * aspect, height: imageSize.height * aspect) 28 | 29 | // Center image 30 | imageRect.origin.x = (imageViewSize.width - imageRect.size.width) / 2 31 | imageRect.origin.y = (imageViewSize.height - imageRect.size.height) / 2 32 | 33 | // Add imageView offset 34 | imageRect.origin.x += imageView.frame.origin.x 35 | imageRect.origin.y += imageView.frame.origin.y 36 | 37 | return imageRect 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/InteractiveImageView/Helpers/UIImage+extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+extensions.swift 3 | // InteractiveImageView 4 | // 5 | // Created by Egzon Pllana on 7.2.24. 6 | // Copyright © 2024 Egzon Pllana. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - Rotate image - 12 | public extension UIImage { 13 | func rotated(by degrees: CGFloat) -> UIImage? { 14 | UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) 15 | guard let context = UIGraphicsGetCurrentContext() else { return nil } 16 | 17 | context.translateBy(x: self.size.width / 2, y: self.size.height / 2) 18 | context.rotate(by: degrees * CGFloat.pi / 180.0) 19 | 20 | self.draw(at: CGPoint(x: -self.size.width / 2, y: -self.size.height / 2)) 21 | let rotatedImage = UIGraphicsGetImageFromCurrentImageContext() 22 | UIGraphicsEndImageContext() 23 | 24 | return rotatedImage 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/InteractiveImageView/InteractiveImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InteractiveImageView.swift 3 | // InteractiveImageView 4 | // 5 | // Created by Egzon Pllana on 28.8.22. 6 | // Copyright © 2022 Egzon Pllana. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A protocol to handle interactions with an `InteractiveImageView`. 12 | public protocol InteractiveImageViewDelegate: AnyObject { 13 | /// Called when an image is cropped. 14 | /// 15 | /// - Parameters: 16 | /// - image: The cropped image. 17 | /// - fromView: The `InteractiveImageView` instance where the image was cropped. 18 | func didCropImage(image: UIImage, fromView: InteractiveImageView) 19 | 20 | /// Called when the user scrolls on the interactive image view. 21 | /// 22 | /// - Parameters: 23 | /// - offset: The content offset of the scroll view. 24 | /// - scale: The current scale of the image. 25 | /// - fromView: The `InteractiveImageView` instance where the scroll occurred. 26 | func didScrollAt(offset: CGPoint, scale: CGFloat, fromView: InteractiveImageView) 27 | 28 | /// Called when the user zooms on the interactive image view. 29 | /// 30 | /// - Parameters: 31 | /// - offset: The content offset of the scroll view. 32 | /// - scale: The current scale of the image. 33 | /// - fromView: The `InteractiveImageView` instance where the zoom occurred. 34 | func didZoomAt(offset: CGPoint, scale: CGFloat, fromView: InteractiveImageView) 35 | 36 | /// Called when there's a failure during interaction with the `InteractiveImageView`. 37 | /// 38 | /// - Parameter fail: The type of failure encountered. 39 | func didFail(_ fail: IIVFailType) 40 | } 41 | 42 | /// A custom `UIView` subclass that provides interactive functionalities for displaying and manipulating images. 43 | public class InteractiveImageView: UIView { 44 | 45 | // MARK: - Public Properties - 46 | public weak var delegate: InteractiveImageViewDelegate? 47 | public var doubleTapZoomFactor: CGFloat = 2.0 48 | public var isScrollEnabled: Bool = true { 49 | didSet { 50 | scrollView.isScrollEnabled = isScrollEnabled 51 | } 52 | } 53 | public var isPinchAllowed: Bool = true { 54 | didSet { 55 | scrollView.pinchGestureRecognizer?.isEnabled = isPinchAllowed 56 | } 57 | } 58 | public var isDoubleTapToZoomAllowed: Bool = true 59 | 60 | // MARK: - Private Properties - 61 | private var scrollView: UIScrollView = UIScrollView() 62 | private var imageView: UIImageView? = .none 63 | private var originalImage: UIImage? = .none 64 | private var imageContentMode: IIVContentMode = .aspectFill 65 | private var nextContentMode: IIVContentMode = .aspectFit 66 | private var configuredImage: UIImage? = .none 67 | private var imageSize: CGSize = CGSize.zero 68 | private var initialOffset: IIVFocusOffset = .begining 69 | private var scrollViewOffsetY: CGFloat = 0.0 70 | private var scrollViewOffsetX: CGFloat = 0.0 71 | private var pointToCenterAfterResize: CGPoint = CGPoint.zero 72 | private var scaleToRestoreAfterResize: CGFloat = 1.0 73 | private var maxScaleFromMinScale: CGFloat = 999.0 // max scale factor 74 | 75 | // MARK: - Initialization - 76 | override init(frame: CGRect) { 77 | super.init(frame: frame) 78 | initialize() 79 | } 80 | 81 | required public init?(coder aDecoder: NSCoder) { 82 | super.init(coder: aDecoder) 83 | initialize() 84 | } 85 | 86 | private func initialize() { 87 | // Setup scroll view properties 88 | scrollView.showsVerticalScrollIndicator = false 89 | scrollView.showsHorizontalScrollIndicator = false 90 | scrollView.bouncesZoom = true 91 | scrollView.decelerationRate = UIScrollView.DecelerationRate.fast 92 | scrollView.delegate = self 93 | 94 | // A workaround to setup pinchGestureRecognizer 95 | DispatchQueue.main.asyncAfter(deadline: .now()) { 96 | self.scrollView.pinchGestureRecognizer?.isEnabled = self.isPinchAllowed 97 | } 98 | 99 | // Add scroll view as a subview 100 | self.addSubview(scrollView) 101 | 102 | // Add scroll view constraints 103 | scrollView.translatesAutoresizingMaskIntoConstraints = false 104 | NSLayoutConstraint.activate([ 105 | scrollView.centerXAnchor.constraint(equalTo: self.centerXAnchor), 106 | scrollView.centerYAnchor.constraint(equalTo: self.centerYAnchor), 107 | scrollView.widthAnchor.constraint(equalTo: self.widthAnchor), 108 | scrollView.heightAnchor.constraint(equalTo: self.heightAnchor), 109 | ]) 110 | 111 | // Add observer for: changeOrientationNotification 112 | NotificationCenter.default.addObserver(self, selector: #selector(InteractiveImageView.changeOrientationNotification), name: UIDevice.orientationDidChangeNotification, object: nil) 113 | } 114 | 115 | // MARK: - Deinitialization - 116 | deinit { 117 | NotificationCenter.default.removeObserver(self) 118 | } 119 | 120 | // MARK: - Overrides - 121 | public override var frame: CGRect { 122 | willSet { 123 | if frame.equalTo(newValue) == false && newValue.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false { 124 | prepareToResize() 125 | } 126 | } 127 | didSet { 128 | if frame.equalTo(oldValue) == false && frame.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false { 129 | recoverFromResizing() 130 | } 131 | } 132 | } 133 | } 134 | 135 | // MARK: - InteractiveImageViewProtocol - 136 | extension InteractiveImageView: InteractiveImageViewProtocol { 137 | public func configure(withNextContentMode nextContentMode: IIVContentMode, withFocusOffset focusOffset: IIVFocusOffset, withImage image: UIImage?) { 138 | configure(withNextContentMode: nextContentMode, withFocusOffset: focusOffset, withImage: image, withIdentifier: 0) 139 | } 140 | 141 | public func configure(withNextContentMode nextContentMode: IIVContentMode, withFocusOffset focusOffset: IIVFocusOffset, withImage image: UIImage?, withIdentifier identifier: Int) { 142 | 143 | // Setup private properties 144 | self.nextContentMode = nextContentMode 145 | self.configuredImage = image 146 | self.initialOffset = focusOffset 147 | self.setImageViewImage(image) 148 | originalImage = image 149 | 150 | // Set view identifier 151 | self.tag = identifier 152 | 153 | // get top super view 154 | var topSupperView = superview 155 | while topSupperView?.superview != nil { 156 | topSupperView = topSupperView?.superview 157 | } 158 | // make sure views have already layout with precise frame 159 | topSupperView?.layoutIfNeeded() 160 | 161 | // reload UI in main thread 162 | DispatchQueue.main.async { self.refresh() } 163 | } 164 | 165 | public func toggleImageContentMode() { 166 | guard let configuredImage = configuredImage else { 167 | delegate?.didFail(.toggleContentModeFailed) 168 | return 169 | } 170 | 171 | imageContentMode = (imageContentMode != nextContentMode) ? nextContentMode : .aspectFill 172 | setImageViewImage(configuredImage) 173 | } 174 | 175 | public func performCropImage() { 176 | let croppedImage = cropImage() 177 | if let image = croppedImage { 178 | self.delegate?.didCropImage(image: image, fromView: self) 179 | } else { 180 | delegate?.didFail(.cropImageFailed) 181 | } 182 | } 183 | 184 | public func setContentOffset(_ offset: CGPoint, animated: Bool, zoomScale: CGFloat) { 185 | scrollView.setZoomScale(zoomScale, animated: false) 186 | scrollView.setContentOffset(offset, animated: animated) 187 | } 188 | 189 | public func cropAndGetImage() -> UIImage? { 190 | return cropImage() 191 | } 192 | 193 | public func updateImageOnly(_ image: UIImage?) { 194 | setImageViewImage(image) 195 | configuredImage = image 196 | } 197 | 198 | public func updateImageView(withImage image: UIImage?) { 199 | let contentOffset = scrollView.contentOffset 200 | let zoomScale = scrollView.zoomScale 201 | updateImageOnly(image) 202 | setContentOffset(contentOffset, animated: false, zoomScale: zoomScale) 203 | } 204 | 205 | public func getOriginalImage() -> UIImage? { 206 | return imageView?.image 207 | } 208 | 209 | public func rotateImage(_ degrees: CGFloat, keepChanges: Bool) { 210 | if keepChanges, let currentImage = imageView?.image, let rotatedImage = currentImage.rotated(by: degrees) { 211 | updateImageOnly(rotatedImage) 212 | } else { 213 | guard let originalImage else { 214 | print("There was an error getting original image.") 215 | return 216 | } 217 | let rotatedImage = originalImage.rotated(by: degrees)! 218 | updateImageOnly(rotatedImage) 219 | } 220 | } 221 | } 222 | 223 | // MARK: - Private extension - 224 | private extension InteractiveImageView { 225 | 226 | // Inspired by: 227 | // https://github.com/huynguyencong/ImageScrollView 228 | 229 | func cropImage() -> UIImage? { 230 | guard let imageView = imageView else { 231 | delegate?.didFail(.getImageViewFailed) 232 | return nil 233 | } 234 | 235 | guard let image = imageView.image else { 236 | delegate?.didFail(.getImageFailed) 237 | return nil 238 | } 239 | let cropRect = CGRect(x: scrollViewOffsetX, 240 | y: scrollViewOffsetY, 241 | width: self.frame.width, 242 | height: self.frame.height) 243 | 244 | let croppedImage = IIVCropHandler.cropImage(image, 245 | toRect: cropRect, 246 | viewWidth: IIVImageRect.getImageRect(fromImageView: imageView).width, 247 | viewHeight: imageView.frame.height) 248 | return croppedImage 249 | } 250 | 251 | func setImageViewImage(_ image: UIImage?) { 252 | // Remove old view 253 | if let zoomView = imageView { 254 | zoomView.removeFromSuperview() 255 | } 256 | 257 | // Add new view 258 | imageView = UIImageView(image: image) 259 | 260 | guard let imageView = imageView else { 261 | delegate?.didFail(.getImageViewFailed) 262 | return 263 | } 264 | 265 | imageView.isUserInteractionEnabled = true 266 | scrollView.addSubview(imageView) 267 | 268 | // Add tap gesture 269 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(InteractiveImageView.doubleTapGestureRecognizer(_:))) 270 | tapGesture.numberOfTapsRequired = 2 271 | imageView.addGestureRecognizer(tapGesture) 272 | 273 | if let image = image { 274 | configureImageForSize(image.size) 275 | } 276 | } 277 | 278 | func refresh() { 279 | if let image = imageView?.image { 280 | setImageViewImage(image) 281 | } 282 | } 283 | 284 | func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect { 285 | var zoomRect = CGRect.zero 286 | 287 | // the zoom rect is in the content view's coordinates. 288 | // at a zoom scale of 1.0, it would be the size of the imageScrollView's bounds. 289 | // as the zoom scale decreases, so more content is visible, the size of the rect grows. 290 | zoomRect.size.height = frame.size.height / scale 291 | zoomRect.size.width = frame.size.width / scale 292 | 293 | // choose an origin so as to get the right center. 294 | zoomRect.origin.x = center.x - (zoomRect.size.width / doubleTapZoomFactor) 295 | zoomRect.origin.y = center.y - (zoomRect.size.height / doubleTapZoomFactor) 296 | 297 | return zoomRect 298 | } 299 | 300 | func adjustFrameToCenterWhenZoomed() { 301 | guard let unwrappedZoomView = imageView else { 302 | delegate?.didFail(.adjustFramesWhenZoomingFailed) 303 | return 304 | } 305 | var frameToCenter = unwrappedZoomView.frame 306 | 307 | // center horizontally 308 | if frameToCenter.size.width < bounds.width { 309 | frameToCenter.origin.x = (bounds.width - frameToCenter.size.width) / 2 310 | } 311 | else { 312 | frameToCenter.origin.x = 0 313 | } 314 | 315 | // center vertically 316 | if frameToCenter.size.height < bounds.height { 317 | frameToCenter.origin.y = (bounds.height - frameToCenter.size.height) / 2 318 | } 319 | else { 320 | frameToCenter.origin.y = 0 321 | } 322 | 323 | unwrappedZoomView.frame = frameToCenter 324 | } 325 | 326 | func configureImageForSize(_ size: CGSize) { 327 | imageSize = size 328 | scrollView.contentSize = imageSize 329 | setMaxMinZoomScalesForCurrentBounds() 330 | scrollView.zoomScale = scrollView.minimumZoomScale 331 | 332 | switch initialOffset { 333 | case .begining: 334 | scrollView.contentOffset = CGPoint.zero 335 | case .center: 336 | let xOffset = scrollView.contentSize.width < bounds.width ? 0 : (scrollView.contentSize.width - bounds.width)/2 337 | let yOffset = scrollView.contentSize.height < bounds.height ? 0 : (scrollView.contentSize.height - bounds.height)/2 338 | 339 | switch imageContentMode { 340 | case .aspectFit: 341 | scrollView.contentOffset = CGPoint.zero 342 | case .aspectFill: 343 | scrollView.contentOffset = CGPoint(x: xOffset, y: yOffset) 344 | case .heightFill: 345 | scrollView.contentOffset = CGPoint(x: xOffset, y: 0) 346 | case .widthFill: 347 | scrollView.contentOffset = CGPoint(x: 0, y: yOffset) 348 | case .customOffset: 349 | scrollView.contentOffset = CGPoint(x: 0, y: yOffset) 350 | } 351 | } 352 | } 353 | 354 | func setMaxMinZoomScalesForCurrentBounds() { 355 | // calculate min/max zoomscale 356 | let xScale = bounds.width / imageSize.width // the scale needed to perfectly fit the image width-wise 357 | let yScale = bounds.height / imageSize.height // the scale needed to perfectly fit the image height-wise 358 | 359 | var minScale: CGFloat = 1 360 | 361 | switch imageContentMode { 362 | case .aspectFill: 363 | minScale = max(xScale, yScale) 364 | case .aspectFit: 365 | minScale = min(xScale, yScale) 366 | case .widthFill: 367 | minScale = xScale 368 | case .heightFill: 369 | minScale = yScale 370 | case .customOffset(let offset): 371 | minScale = xScale * (offset) 372 | } 373 | 374 | let maxScale = maxScaleFromMinScale*minScale 375 | 376 | // don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.) 377 | if minScale > maxScale { 378 | minScale = maxScale 379 | } 380 | 381 | scrollView.maximumZoomScale = maxScale 382 | scrollView.minimumZoomScale = minScale * 0.999 // the multiply factor to prevent user cannot scroll page while they use this control in UIPageViewController 383 | } 384 | 385 | func prepareToResize() { 386 | let boundsCenter = CGPoint(x: bounds.midX, y: bounds.midY) 387 | pointToCenterAfterResize = convert(boundsCenter, to: imageView) 388 | 389 | scaleToRestoreAfterResize = scrollView.zoomScale 390 | 391 | // If we're at the minimum zoom scale, preserve that by returning 0, which will be converted to the minimum 392 | // allowable scale when the scale is restored. 393 | if scaleToRestoreAfterResize <= scrollView.minimumZoomScale + CGFloat(Float.ulpOfOne) { 394 | scaleToRestoreAfterResize = 0 395 | } 396 | } 397 | 398 | func recoverFromResizing() { 399 | setMaxMinZoomScalesForCurrentBounds() 400 | 401 | // restore zoom scale, first making sure it is within the allowable range. 402 | let maxZoomScale = max(scrollView.minimumZoomScale, scaleToRestoreAfterResize) 403 | scrollView.zoomScale = min(scrollView.maximumZoomScale, maxZoomScale) 404 | 405 | // restore center point, first making sure it is within the allowable range. 406 | 407 | // convert our desired center point back to our own coordinate space 408 | let boundsCenter = convert(pointToCenterAfterResize, to: imageView) 409 | 410 | // calculate the content offset that would yield that center point 411 | var offset = CGPoint(x: boundsCenter.x - bounds.size.width/doubleTapZoomFactor, y: boundsCenter.y - bounds.size.height/doubleTapZoomFactor) 412 | 413 | // restore offset, adjusted to be within the allowable range 414 | let maxOffset = maximumContentOffset() 415 | let minOffset = minimumContentOffset() 416 | 417 | var realMaxOffset = min(maxOffset.x, offset.x) 418 | offset.x = max(minOffset.x, realMaxOffset) 419 | 420 | realMaxOffset = min(maxOffset.y, offset.y) 421 | offset.y = max(minOffset.y, realMaxOffset) 422 | 423 | scrollView.contentOffset = offset 424 | } 425 | 426 | func maximumContentOffset() -> CGPoint { 427 | return CGPoint(x: scrollView.contentSize.width - bounds.width,y:scrollView.contentSize.height - bounds.height) 428 | } 429 | 430 | func minimumContentOffset() -> CGPoint { 431 | return CGPoint.zero 432 | } 433 | } 434 | 435 | // MARK: - Observers methods - 436 | private extension InteractiveImageView { 437 | @objc private func changeOrientationNotification() { 438 | // Reload UI in main thread 439 | DispatchQueue.main.async { 440 | self.configureImageForSize(self.imageSize) 441 | } 442 | } 443 | 444 | @objc private func doubleTapGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { 445 | // make sure Zoom is allowed 446 | guard isDoubleTapToZoomAllowed else { return } 447 | 448 | // zoom out if it bigger than the scale factor after double-tap scaling. Else, zoom in 449 | if scrollView.zoomScale != scrollView.minimumZoomScale { 450 | scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true) 451 | } else { 452 | let center = gestureRecognizer.location(in: gestureRecognizer.view) 453 | let zoomRect = zoomRectForScale(doubleTapZoomFactor * scrollView.minimumZoomScale, center: center) 454 | scrollView.zoom(to: zoomRect, animated: true) 455 | } 456 | } 457 | } 458 | 459 | // MARK: - UIScrollViewDelegate - 460 | extension InteractiveImageView: UIScrollViewDelegate { 461 | 462 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 463 | // Update private properties 464 | scrollViewOffsetY = scrollView.contentOffset.y 465 | scrollViewOffsetX = scrollView.contentOffset.x 466 | } 467 | 468 | public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { } 469 | 470 | public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { } 471 | 472 | public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { } 473 | 474 | public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { } 475 | 476 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 477 | delegate?.didScrollAt(offset: scrollView.contentOffset, scale: scrollView.zoomScale, fromView: self) 478 | } 479 | 480 | public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { } 481 | 482 | public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { } 483 | 484 | public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { 485 | delegate?.didZoomAt(offset: scrollView.contentOffset, scale: scrollView.zoomScale, fromView: self) 486 | } 487 | 488 | public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { 489 | return false 490 | } 491 | 492 | @available(iOS 11.0, *) 493 | public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) { } 494 | 495 | public func viewForZooming(in scrollView: UIScrollView) -> UIView? { 496 | return imageView 497 | } 498 | 499 | public func scrollViewDidZoom(_ scrollView: UIScrollView) { 500 | adjustFrameToCenterWhenZoomed() 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /Sources/InteractiveImageView/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyTrackingDomains 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/InteractiveImageView/Protocols/InteractiveImageViewProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InteractiveImageViewProtocol.swift 3 | // InteractiveImageView 4 | // 5 | // Created by Egzon Pllana on 4.11.22. 6 | // Copyright © 2022 Egzon Pllana. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A protocol defining methods to interact with an image within an `InteractiveImageView`. 12 | public protocol InteractiveImageViewProtocol { 13 | /// Double tap scale factor, default value is 2.0. 14 | var doubleTapZoomFactor: CGFloat { get set } 15 | 16 | /// Setup initial properties of the view. 17 | /// - Parameters: 18 | /// - nextContentMode: Next content mode, for example heightFill. 19 | /// - focusOffset: Initial focus mode of the image view, center or top. 20 | /// - image: Image to interact with. 21 | /// - identifier: This identifier represents UIView tag. 22 | func configure(withNextContentMode nextContentMode: IIVContentMode, withFocusOffset focusOffset: IIVFocusOffset, withImage image: UIImage?, withIdentifier identifier: Int) 23 | 24 | /// Setup initial properties of the view. 25 | /// - Parameters: 26 | /// - nextContentMode: Next content mode, for example heightFill. 27 | /// - focusOffset: Initial focus mode of the image view, center or top. 28 | /// - image: Image to interact with. 29 | func configure(withNextContentMode nextContentMode: IIVContentMode, withFocusOffset focusOffset: IIVFocusOffset, withImage image: UIImage?) 30 | 31 | /// Set image view focus properties with content offset and zoom scale. 32 | /// - Parameters: 33 | /// - offset: Offset to focus. 34 | /// - animated: Animation state when performing offset changes. 35 | /// - zoomScale: Zoom scale to apply on ImageView. 36 | func setContentOffset(_ offset: CGPoint, animated: Bool, zoomScale: CGFloat) 37 | 38 | /// Toggle between initial content mode (aspectFill) and nextContentMode. 39 | func toggleImageContentMode() 40 | 41 | /// Perform croping image process visible image and listen to delegates to get cropped image. 42 | func performCropImage() 43 | 44 | /// Crop and get presented image. 45 | /// 46 | /// - Returns: UIImage? 47 | func cropAndGetImage() -> UIImage? 48 | 49 | /// Updates the image displayed in the interactive image view. 50 | /// 51 | /// - Parameter image: The new image to be displayed. 52 | func updateImageOnly(_ image: UIImage?) 53 | 54 | /// Updates the image view with the provided image. 55 | /// 56 | /// - Parameter image: The image to be displayed. 57 | func updateImageView(withImage image: UIImage?) 58 | 59 | /// Retrieves the original image displayed in the interactive image view. 60 | /// 61 | /// - Returns: The original image displayed in the interactive image view. 62 | func getOriginalImage() -> UIImage? 63 | 64 | /// Rotates the image displayed in the interactive image view by the specified degrees. 65 | /// 66 | /// - Parameters: 67 | /// - degrees: The angle, in degrees, by which to rotate the image. 68 | /// - keepChanges: A Boolean value that determines whether the changes made to the image (e.g., cropping or scaling) should be preserved after rotation. 69 | /// If `true`, the rotated image will be based on the current state of the image view, preserving any previous changes. 70 | /// If `false`, the original image will be reset and then rotated, discarding any previous changes made to the image. 71 | func rotateImage(_ degrees: CGFloat, keepChanges: Bool) 72 | } 73 | -------------------------------------------------------------------------------- /example-preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/example-preview.gif -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egzonpllana/InteractiveImageView/598696d037e582be885d7632e2188dbfccbec01c/logo.png --------------------------------------------------------------------------------