├── .gitignore ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── img0.imageset │ │ ├── 300x400.png │ │ └── Contents.json │ ├── img1.imageset │ │ ├── 400x300.png │ │ └── Contents.json │ └── img2.imageset │ │ ├── 400x400.png │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── ViewController.swift ├── FocusableImageView.podspec ├── FocusableImageView.xcodeproj ├── FocusableImageViewTests_Info.plist ├── FocusableImageView_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ └── FocusableImageView-Package.xcscheme ├── FocusableImageView.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── FocusableImageView │ ├── FocusableImageView.swift │ ├── FocusableImageViewConfiguration.swift │ ├── FocusableImageViewManager.swift │ └── ImagesViewController.swift ├── Tests ├── FocusableImageViewTests │ ├── FocusableImageViewTests.swift │ └── XCTestManifests.swift └── LinuxMain.swift └── readme └── Screenshot.gif /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | Carthage 6 | *.framework.zip -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | ED7DBF752424FB0400643701 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED7DBF742424FB0400643701 /* AppDelegate.swift */; }; 11 | ED7DBF792424FB0400643701 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED7DBF782424FB0400643701 /* ViewController.swift */; }; 12 | ED7DBF7C2424FB0400643701 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ED7DBF7A2424FB0400643701 /* Main.storyboard */; }; 13 | ED7DBF7E2424FB0500643701 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = ED7DBF7D2424FB0500643701 /* Assets.xcassets */; }; 14 | ED7DBF812424FB0500643701 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ED7DBF7F2424FB0500643701 /* LaunchScreen.storyboard */; }; 15 | ED7DBF8A2425010900643701 /* FocusableImageView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED7DBF892425010900643701 /* FocusableImageView.framework */; }; 16 | ED7DBF8B2425010900643701 /* FocusableImageView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = ED7DBF892425010900643701 /* FocusableImageView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | ED7DBF8C2425010900643701 /* Embed Frameworks */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = ""; 24 | dstSubfolderSpec = 10; 25 | files = ( 26 | ED7DBF8B2425010900643701 /* FocusableImageView.framework in Embed Frameworks */, 27 | ); 28 | name = "Embed Frameworks"; 29 | runOnlyForDeploymentPostprocessing = 0; 30 | }; 31 | /* End PBXCopyFilesBuildPhase section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | ED7DBF712424FB0400643701 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | ED7DBF742424FB0400643701 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 36 | ED7DBF782424FB0400643701 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 37 | ED7DBF7B2424FB0400643701 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 38 | ED7DBF7D2424FB0500643701 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 39 | ED7DBF802424FB0500643701 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 40 | ED7DBF822424FB0500643701 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | ED7DBF892425010900643701 /* FocusableImageView.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FocusableImageView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | /* End PBXFileReference section */ 43 | 44 | /* Begin PBXFrameworksBuildPhase section */ 45 | ED7DBF6E2424FB0400643701 /* Frameworks */ = { 46 | isa = PBXFrameworksBuildPhase; 47 | buildActionMask = 2147483647; 48 | files = ( 49 | ED7DBF8A2425010900643701 /* FocusableImageView.framework in Frameworks */, 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXFrameworksBuildPhase section */ 54 | 55 | /* Begin PBXGroup section */ 56 | ED7DBF682424FB0400643701 = { 57 | isa = PBXGroup; 58 | children = ( 59 | ED7DBF732424FB0400643701 /* Example */, 60 | ED7DBF722424FB0400643701 /* Products */, 61 | ED7DBF882425010900643701 /* Frameworks */, 62 | ); 63 | sourceTree = ""; 64 | }; 65 | ED7DBF722424FB0400643701 /* Products */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | ED7DBF712424FB0400643701 /* Example.app */, 69 | ); 70 | name = Products; 71 | sourceTree = ""; 72 | }; 73 | ED7DBF732424FB0400643701 /* Example */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | ED7DBF742424FB0400643701 /* AppDelegate.swift */, 77 | ED7DBF782424FB0400643701 /* ViewController.swift */, 78 | ED7DBF7A2424FB0400643701 /* Main.storyboard */, 79 | ED7DBF7D2424FB0500643701 /* Assets.xcassets */, 80 | ED7DBF7F2424FB0500643701 /* LaunchScreen.storyboard */, 81 | ED7DBF822424FB0500643701 /* Info.plist */, 82 | ); 83 | path = Example; 84 | sourceTree = ""; 85 | }; 86 | ED7DBF882425010900643701 /* Frameworks */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | ED7DBF892425010900643701 /* FocusableImageView.framework */, 90 | ); 91 | name = Frameworks; 92 | sourceTree = ""; 93 | }; 94 | /* End PBXGroup section */ 95 | 96 | /* Begin PBXNativeTarget section */ 97 | ED7DBF702424FB0400643701 /* Example */ = { 98 | isa = PBXNativeTarget; 99 | buildConfigurationList = ED7DBF852424FB0500643701 /* Build configuration list for PBXNativeTarget "Example" */; 100 | buildPhases = ( 101 | ED7DBF6D2424FB0400643701 /* Sources */, 102 | ED7DBF6E2424FB0400643701 /* Frameworks */, 103 | ED7DBF6F2424FB0400643701 /* Resources */, 104 | ED7DBF8C2425010900643701 /* Embed Frameworks */, 105 | ); 106 | buildRules = ( 107 | ); 108 | dependencies = ( 109 | ); 110 | name = Example; 111 | productName = Example; 112 | productReference = ED7DBF712424FB0400643701 /* Example.app */; 113 | productType = "com.apple.product-type.application"; 114 | }; 115 | /* End PBXNativeTarget section */ 116 | 117 | /* Begin PBXProject section */ 118 | ED7DBF692424FB0400643701 /* Project object */ = { 119 | isa = PBXProject; 120 | attributes = { 121 | LastSwiftUpdateCheck = 1130; 122 | LastUpgradeCheck = 1130; 123 | ORGANIZATIONNAME = "Koji Murata"; 124 | TargetAttributes = { 125 | ED7DBF702424FB0400643701 = { 126 | CreatedOnToolsVersion = 11.3; 127 | }; 128 | }; 129 | }; 130 | buildConfigurationList = ED7DBF6C2424FB0400643701 /* Build configuration list for PBXProject "Example" */; 131 | compatibilityVersion = "Xcode 9.3"; 132 | developmentRegion = en; 133 | hasScannedForEncodings = 0; 134 | knownRegions = ( 135 | en, 136 | Base, 137 | ); 138 | mainGroup = ED7DBF682424FB0400643701; 139 | productRefGroup = ED7DBF722424FB0400643701 /* Products */; 140 | projectDirPath = ""; 141 | projectRoot = ""; 142 | targets = ( 143 | ED7DBF702424FB0400643701 /* Example */, 144 | ); 145 | }; 146 | /* End PBXProject section */ 147 | 148 | /* Begin PBXResourcesBuildPhase section */ 149 | ED7DBF6F2424FB0400643701 /* Resources */ = { 150 | isa = PBXResourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | ED7DBF812424FB0500643701 /* LaunchScreen.storyboard in Resources */, 154 | ED7DBF7E2424FB0500643701 /* Assets.xcassets in Resources */, 155 | ED7DBF7C2424FB0400643701 /* Main.storyboard in Resources */, 156 | ); 157 | runOnlyForDeploymentPostprocessing = 0; 158 | }; 159 | /* End PBXResourcesBuildPhase section */ 160 | 161 | /* Begin PBXSourcesBuildPhase section */ 162 | ED7DBF6D2424FB0400643701 /* Sources */ = { 163 | isa = PBXSourcesBuildPhase; 164 | buildActionMask = 2147483647; 165 | files = ( 166 | ED7DBF792424FB0400643701 /* ViewController.swift in Sources */, 167 | ED7DBF752424FB0400643701 /* AppDelegate.swift in Sources */, 168 | ); 169 | runOnlyForDeploymentPostprocessing = 0; 170 | }; 171 | /* End PBXSourcesBuildPhase section */ 172 | 173 | /* Begin PBXVariantGroup section */ 174 | ED7DBF7A2424FB0400643701 /* Main.storyboard */ = { 175 | isa = PBXVariantGroup; 176 | children = ( 177 | ED7DBF7B2424FB0400643701 /* Base */, 178 | ); 179 | name = Main.storyboard; 180 | sourceTree = ""; 181 | }; 182 | ED7DBF7F2424FB0500643701 /* LaunchScreen.storyboard */ = { 183 | isa = PBXVariantGroup; 184 | children = ( 185 | ED7DBF802424FB0500643701 /* Base */, 186 | ); 187 | name = LaunchScreen.storyboard; 188 | sourceTree = ""; 189 | }; 190 | /* End PBXVariantGroup section */ 191 | 192 | /* Begin XCBuildConfiguration section */ 193 | ED7DBF832424FB0500643701 /* Debug */ = { 194 | isa = XCBuildConfiguration; 195 | buildSettings = { 196 | ALWAYS_SEARCH_USER_PATHS = NO; 197 | CLANG_ANALYZER_NONNULL = YES; 198 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 199 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 200 | CLANG_CXX_LIBRARY = "libc++"; 201 | CLANG_ENABLE_MODULES = YES; 202 | CLANG_ENABLE_OBJC_ARC = YES; 203 | CLANG_ENABLE_OBJC_WEAK = YES; 204 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 205 | CLANG_WARN_BOOL_CONVERSION = YES; 206 | CLANG_WARN_COMMA = YES; 207 | CLANG_WARN_CONSTANT_CONVERSION = YES; 208 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 209 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 210 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 211 | CLANG_WARN_EMPTY_BODY = YES; 212 | CLANG_WARN_ENUM_CONVERSION = YES; 213 | CLANG_WARN_INFINITE_RECURSION = YES; 214 | CLANG_WARN_INT_CONVERSION = YES; 215 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 217 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 218 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 219 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 220 | CLANG_WARN_STRICT_PROTOTYPES = YES; 221 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 222 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 223 | CLANG_WARN_UNREACHABLE_CODE = YES; 224 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 225 | COPY_PHASE_STRIP = NO; 226 | DEBUG_INFORMATION_FORMAT = dwarf; 227 | ENABLE_STRICT_OBJC_MSGSEND = YES; 228 | ENABLE_TESTABILITY = YES; 229 | GCC_C_LANGUAGE_STANDARD = gnu11; 230 | GCC_DYNAMIC_NO_PIC = NO; 231 | GCC_NO_COMMON_BLOCKS = YES; 232 | GCC_OPTIMIZATION_LEVEL = 0; 233 | GCC_PREPROCESSOR_DEFINITIONS = ( 234 | "DEBUG=1", 235 | "$(inherited)", 236 | ); 237 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 238 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 239 | GCC_WARN_UNDECLARED_SELECTOR = YES; 240 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 241 | GCC_WARN_UNUSED_FUNCTION = YES; 242 | GCC_WARN_UNUSED_VARIABLE = YES; 243 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 244 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 245 | MTL_FAST_MATH = YES; 246 | ONLY_ACTIVE_ARCH = YES; 247 | SDKROOT = iphoneos; 248 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 249 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 250 | }; 251 | name = Debug; 252 | }; 253 | ED7DBF842424FB0500643701 /* Release */ = { 254 | isa = XCBuildConfiguration; 255 | buildSettings = { 256 | ALWAYS_SEARCH_USER_PATHS = NO; 257 | CLANG_ANALYZER_NONNULL = YES; 258 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 259 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 260 | CLANG_CXX_LIBRARY = "libc++"; 261 | CLANG_ENABLE_MODULES = YES; 262 | CLANG_ENABLE_OBJC_ARC = YES; 263 | CLANG_ENABLE_OBJC_WEAK = YES; 264 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 265 | CLANG_WARN_BOOL_CONVERSION = YES; 266 | CLANG_WARN_COMMA = YES; 267 | CLANG_WARN_CONSTANT_CONVERSION = YES; 268 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 269 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 270 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 271 | CLANG_WARN_EMPTY_BODY = YES; 272 | CLANG_WARN_ENUM_CONVERSION = YES; 273 | CLANG_WARN_INFINITE_RECURSION = YES; 274 | CLANG_WARN_INT_CONVERSION = YES; 275 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 276 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 277 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 278 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 279 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 280 | CLANG_WARN_STRICT_PROTOTYPES = YES; 281 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 282 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 283 | CLANG_WARN_UNREACHABLE_CODE = YES; 284 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 285 | COPY_PHASE_STRIP = NO; 286 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 287 | ENABLE_NS_ASSERTIONS = NO; 288 | ENABLE_STRICT_OBJC_MSGSEND = YES; 289 | GCC_C_LANGUAGE_STANDARD = gnu11; 290 | GCC_NO_COMMON_BLOCKS = YES; 291 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 292 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 293 | GCC_WARN_UNDECLARED_SELECTOR = YES; 294 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 295 | GCC_WARN_UNUSED_FUNCTION = YES; 296 | GCC_WARN_UNUSED_VARIABLE = YES; 297 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 298 | MTL_ENABLE_DEBUG_INFO = NO; 299 | MTL_FAST_MATH = YES; 300 | SDKROOT = iphoneos; 301 | SWIFT_COMPILATION_MODE = wholemodule; 302 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 303 | VALIDATE_PRODUCT = YES; 304 | }; 305 | name = Release; 306 | }; 307 | ED7DBF862424FB0500643701 /* Debug */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 311 | CODE_SIGN_IDENTITY = "Apple Development"; 312 | CODE_SIGN_STYLE = Automatic; 313 | DEVELOPMENT_TEAM = 8AKT7PFJ2M; 314 | INFOPLIST_FILE = Example/Info.plist; 315 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 316 | LD_RUNPATH_SEARCH_PATHS = ( 317 | "$(inherited)", 318 | "@executable_path/Frameworks", 319 | ); 320 | PRODUCT_BUNDLE_IDENTIFIER = "com.malt03.example-app"; 321 | PRODUCT_NAME = "$(TARGET_NAME)"; 322 | PROVISIONING_PROFILE_SPECIFIER = ""; 323 | SWIFT_VERSION = 5.0; 324 | TARGETED_DEVICE_FAMILY = "1,2"; 325 | }; 326 | name = Debug; 327 | }; 328 | ED7DBF872424FB0500643701 /* Release */ = { 329 | isa = XCBuildConfiguration; 330 | buildSettings = { 331 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 332 | CODE_SIGN_IDENTITY = "Apple Development"; 333 | CODE_SIGN_STYLE = Automatic; 334 | DEVELOPMENT_TEAM = 8AKT7PFJ2M; 335 | INFOPLIST_FILE = Example/Info.plist; 336 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 337 | LD_RUNPATH_SEARCH_PATHS = ( 338 | "$(inherited)", 339 | "@executable_path/Frameworks", 340 | ); 341 | PRODUCT_BUNDLE_IDENTIFIER = "com.malt03.example-app"; 342 | PRODUCT_NAME = "$(TARGET_NAME)"; 343 | PROVISIONING_PROFILE_SPECIFIER = ""; 344 | SWIFT_VERSION = 5.0; 345 | TARGETED_DEVICE_FAMILY = "1,2"; 346 | }; 347 | name = Release; 348 | }; 349 | /* End XCBuildConfiguration section */ 350 | 351 | /* Begin XCConfigurationList section */ 352 | ED7DBF6C2424FB0400643701 /* Build configuration list for PBXProject "Example" */ = { 353 | isa = XCConfigurationList; 354 | buildConfigurations = ( 355 | ED7DBF832424FB0500643701 /* Debug */, 356 | ED7DBF842424FB0500643701 /* Release */, 357 | ); 358 | defaultConfigurationIsVisible = 0; 359 | defaultConfigurationName = Release; 360 | }; 361 | ED7DBF852424FB0500643701 /* Build configuration list for PBXNativeTarget "Example" */ = { 362 | isa = XCConfigurationList; 363 | buildConfigurations = ( 364 | ED7DBF862424FB0500643701 /* Debug */, 365 | ED7DBF872424FB0500643701 /* Release */, 366 | ); 367 | defaultConfigurationIsVisible = 0; 368 | defaultConfigurationName = Release; 369 | }; 370 | /* End XCConfigurationList section */ 371 | }; 372 | rootObject = ED7DBF692424FB0400643701 /* Project object */; 373 | } 374 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Koji Murata on 2020/03/20. 6 | // Copyright © 2020 Koji Murata. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FocusableImageView 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | FocusableImageViewConfiguration.default = .init( 18 | backgroundColor: .init(white: 0, alpha: 0.5), 19 | animationDuration: 0.5, 20 | pageControlConfiguration: .init(hidesForSinglePage: false, pageIndicatorTintColor: nil, currentPageIndicatorTintColor: nil), 21 | maximumZoomScale: 2, 22 | createDismissButton: { (parentView) -> UIButton in 23 | let button = UIButton() 24 | button.translatesAutoresizingMaskIntoConstraints = false 25 | button.setTitle("Close", for: .normal) 26 | button.setTitleColor(.white, for: .normal) 27 | button.titleLabel?.font = UIFont.systemFont(ofSize: 16) 28 | parentView.addSubview(button) 29 | NSLayoutConstraint.activate([ 30 | button.leadingAnchor.constraint(equalTo: parentView.leadingAnchor, constant: 16), 31 | button.topAnchor.constraint(equalTo: parentView.safeAreaLayoutGuide.topAnchor, constant: 16), 32 | ]) 33 | return button 34 | } 35 | ) 36 | return true 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/img0.imageset/300x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malt03/FocusableImageView/e18aee11a998ee0d11cd8d08f87350a78d9b1e2b/Example/Example/Assets.xcassets/img0.imageset/300x400.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/img0.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "300x400.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/img1.imageset/400x300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malt03/FocusableImageView/e18aee11a998ee0d11cd8d08f87350a78d9b1e2b/Example/Example/Assets.xcassets/img1.imageset/400x300.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/img1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "400x300.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/img2.imageset/400x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malt03/FocusableImageView/e18aee11a998ee0d11cd8d08f87350a78d9b1e2b/Example/Example/Assets.xcassets/img2.imageset/400x400.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/img2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "400x400.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/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 | -------------------------------------------------------------------------------- /Example/Example/Base.lproj/Main.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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /Example/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by Koji Murata on 2020/03/20. 6 | // Copyright © 2020 Koji Murata. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FocusableImageView 11 | 12 | class ViewController: UIViewController { 13 | @IBOutlet private weak var stackView: UIStackView! 14 | private lazy var manager = FocusableImageViewManager() 15 | private var imageViews: [FocusableImageView] { stackView.arrangedSubviews as! [FocusableImageView] } 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | manager.delegate = self 21 | imageViews.forEach { 22 | $0.inner.layer.cornerRadius = 8 23 | $0.inner.clipsToBounds = true 24 | } 25 | manager.register(parentViewController: self, imageViews: imageViews) 26 | } 27 | } 28 | 29 | extension ViewController: FocusableImageViewDelegate { 30 | func focusableImageViewPresentAnimation(views: [FocusableImageView]) { 31 | views.forEach { $0.inner.layer.cornerRadius = 0 } 32 | } 33 | 34 | func focusableImageViewDismissAnimation(views: [FocusableImageView]) { 35 | views.forEach { $0.inner.layer.cornerRadius = 8 } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /FocusableImageView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'FocusableImageView' 3 | s.version = '0.1.0' 4 | s.summary = 'FocusableImageView is a library for creating focusable imageview.' 5 | 6 | s.description = <<-DESC 7 | FocusableImageView is a library for creating focusable imageview. 8 | Users can focus images by tapping views. 9 | DESC 10 | 11 | s.homepage = 'https://github.com/malt03/FocusableImageView' 12 | s.license = { :type => 'MIT', :file => 'LICENSE' } 13 | s.author = { 'Koji Murata' => 'malt.koji@gmail.com' } 14 | s.source = { :git => 'https://github.com/malt03/FocusableImageView.git', :tag => s.version.to_s } 15 | 16 | s.source_files = "Sources/**/*.swift" 17 | 18 | s.swift_version = "5.1" 19 | s.ios.deployment_target = "11.0" 20 | end 21 | -------------------------------------------------------------------------------- /FocusableImageView.xcodeproj/FocusableImageViewTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /FocusableImageView.xcodeproj/FocusableImageView_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /FocusableImageView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | OBJ_24 /* ImagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* ImagesViewController.swift */; }; 11 | OBJ_25 /* FocusableImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* FocusableImageView.swift */; }; 12 | OBJ_26 /* FocusableImageViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* FocusableImageViewConfiguration.swift */; }; 13 | OBJ_27 /* FocusableImageViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* FocusableImageViewManager.swift */; }; 14 | OBJ_34 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | "FocusableImageView::FocusableImageView::Product" /* FocusableImageView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FocusableImageView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | OBJ_10 /* FocusableImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableImageView.swift; sourceTree = ""; }; 20 | OBJ_11 /* FocusableImageViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableImageViewConfiguration.swift; sourceTree = ""; }; 21 | OBJ_12 /* FocusableImageViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableImageViewManager.swift; sourceTree = ""; }; 22 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 23 | OBJ_9 /* ImagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesViewController.swift; sourceTree = ""; }; 24 | /* End PBXFileReference section */ 25 | 26 | /* Begin PBXFrameworksBuildPhase section */ 27 | OBJ_28 /* Frameworks */ = { 28 | isa = PBXFrameworksBuildPhase; 29 | buildActionMask = 0; 30 | files = ( 31 | ); 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXFrameworksBuildPhase section */ 35 | 36 | /* Begin PBXGroup section */ 37 | OBJ_14 /* Products */ = { 38 | isa = PBXGroup; 39 | children = ( 40 | "FocusableImageView::FocusableImageView::Product" /* FocusableImageView.framework */, 41 | ); 42 | name = Products; 43 | sourceTree = BUILT_PRODUCTS_DIR; 44 | }; 45 | OBJ_5 = { 46 | isa = PBXGroup; 47 | children = ( 48 | OBJ_6 /* Package.swift */, 49 | OBJ_7 /* Sources */, 50 | OBJ_14 /* Products */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | OBJ_7 /* Sources */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | OBJ_8 /* FocusableImageView */, 58 | ); 59 | name = Sources; 60 | sourceTree = SOURCE_ROOT; 61 | }; 62 | OBJ_8 /* FocusableImageView */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | OBJ_9 /* ImagesViewController.swift */, 66 | OBJ_10 /* FocusableImageView.swift */, 67 | OBJ_11 /* FocusableImageViewConfiguration.swift */, 68 | OBJ_12 /* FocusableImageViewManager.swift */, 69 | ); 70 | name = FocusableImageView; 71 | path = Sources/FocusableImageView; 72 | sourceTree = SOURCE_ROOT; 73 | }; 74 | /* End PBXGroup section */ 75 | 76 | /* Begin PBXNativeTarget section */ 77 | "FocusableImageView::FocusableImageView" /* FocusableImageView */ = { 78 | isa = PBXNativeTarget; 79 | buildConfigurationList = OBJ_20 /* Build configuration list for PBXNativeTarget "FocusableImageView" */; 80 | buildPhases = ( 81 | OBJ_23 /* Sources */, 82 | OBJ_28 /* Frameworks */, 83 | ); 84 | buildRules = ( 85 | ); 86 | dependencies = ( 87 | ); 88 | name = FocusableImageView; 89 | productName = FocusableImageView; 90 | productReference = "FocusableImageView::FocusableImageView::Product" /* FocusableImageView.framework */; 91 | productType = "com.apple.product-type.framework"; 92 | }; 93 | "FocusableImageView::SwiftPMPackageDescription" /* FocusableImageViewPackageDescription */ = { 94 | isa = PBXNativeTarget; 95 | buildConfigurationList = OBJ_30 /* Build configuration list for PBXNativeTarget "FocusableImageViewPackageDescription" */; 96 | buildPhases = ( 97 | OBJ_33 /* Sources */, 98 | ); 99 | buildRules = ( 100 | ); 101 | dependencies = ( 102 | ); 103 | name = FocusableImageViewPackageDescription; 104 | productName = FocusableImageViewPackageDescription; 105 | productType = "com.apple.product-type.framework"; 106 | }; 107 | /* End PBXNativeTarget section */ 108 | 109 | /* Begin PBXProject section */ 110 | OBJ_1 /* Project object */ = { 111 | isa = PBXProject; 112 | attributes = { 113 | LastSwiftMigration = 9999; 114 | LastUpgradeCheck = 9999; 115 | }; 116 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "FocusableImageView" */; 117 | compatibilityVersion = "Xcode 3.2"; 118 | developmentRegion = en; 119 | hasScannedForEncodings = 0; 120 | knownRegions = ( 121 | en, 122 | ); 123 | mainGroup = OBJ_5; 124 | productRefGroup = OBJ_14 /* Products */; 125 | projectDirPath = ""; 126 | projectRoot = ""; 127 | targets = ( 128 | "FocusableImageView::FocusableImageView" /* FocusableImageView */, 129 | "FocusableImageView::SwiftPMPackageDescription" /* FocusableImageViewPackageDescription */, 130 | ); 131 | }; 132 | /* End PBXProject section */ 133 | 134 | /* Begin PBXSourcesBuildPhase section */ 135 | OBJ_23 /* Sources */ = { 136 | isa = PBXSourcesBuildPhase; 137 | buildActionMask = 0; 138 | files = ( 139 | OBJ_24 /* ImagesViewController.swift in Sources */, 140 | OBJ_25 /* FocusableImageView.swift in Sources */, 141 | OBJ_26 /* FocusableImageViewConfiguration.swift in Sources */, 142 | OBJ_27 /* FocusableImageViewManager.swift in Sources */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | OBJ_33 /* Sources */ = { 147 | isa = PBXSourcesBuildPhase; 148 | buildActionMask = 0; 149 | files = ( 150 | OBJ_34 /* Package.swift in Sources */, 151 | ); 152 | runOnlyForDeploymentPostprocessing = 0; 153 | }; 154 | /* End PBXSourcesBuildPhase section */ 155 | 156 | /* Begin XCBuildConfiguration section */ 157 | OBJ_21 /* Debug */ = { 158 | isa = XCBuildConfiguration; 159 | buildSettings = { 160 | ENABLE_TESTABILITY = YES; 161 | FRAMEWORK_SEARCH_PATHS = ( 162 | "$(inherited)", 163 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 164 | ); 165 | HEADER_SEARCH_PATHS = "$(inherited)"; 166 | INFOPLIST_FILE = FocusableImageView.xcodeproj/FocusableImageView_Info.plist; 167 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 168 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 169 | MACOSX_DEPLOYMENT_TARGET = 10.10; 170 | OTHER_CFLAGS = "$(inherited)"; 171 | OTHER_LDFLAGS = "$(inherited)"; 172 | OTHER_SWIFT_FLAGS = "$(inherited)"; 173 | PRODUCT_BUNDLE_IDENTIFIER = FocusableImageView; 174 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 175 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 176 | SDKROOT = iphoneos; 177 | SKIP_INSTALL = YES; 178 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; 179 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 180 | SWIFT_VERSION = 5.0; 181 | TARGETED_DEVICE_FAMILY = "1,2"; 182 | TARGET_NAME = FocusableImageView; 183 | TVOS_DEPLOYMENT_TARGET = 9.0; 184 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 185 | }; 186 | name = Debug; 187 | }; 188 | OBJ_22 /* Release */ = { 189 | isa = XCBuildConfiguration; 190 | buildSettings = { 191 | ENABLE_TESTABILITY = YES; 192 | FRAMEWORK_SEARCH_PATHS = ( 193 | "$(inherited)", 194 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 195 | ); 196 | HEADER_SEARCH_PATHS = "$(inherited)"; 197 | INFOPLIST_FILE = FocusableImageView.xcodeproj/FocusableImageView_Info.plist; 198 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 199 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 200 | MACOSX_DEPLOYMENT_TARGET = 10.10; 201 | OTHER_CFLAGS = "$(inherited)"; 202 | OTHER_LDFLAGS = "$(inherited)"; 203 | OTHER_SWIFT_FLAGS = "$(inherited)"; 204 | PRODUCT_BUNDLE_IDENTIFIER = FocusableImageView; 205 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 206 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 207 | SDKROOT = iphoneos; 208 | SKIP_INSTALL = YES; 209 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; 210 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 211 | SWIFT_VERSION = 5.0; 212 | TARGETED_DEVICE_FAMILY = "1,2"; 213 | TARGET_NAME = FocusableImageView; 214 | TVOS_DEPLOYMENT_TARGET = 9.0; 215 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 216 | }; 217 | name = Release; 218 | }; 219 | OBJ_3 /* Debug */ = { 220 | isa = XCBuildConfiguration; 221 | buildSettings = { 222 | CLANG_ENABLE_OBJC_ARC = YES; 223 | COMBINE_HIDPI_IMAGES = YES; 224 | COPY_PHASE_STRIP = NO; 225 | DEBUG_INFORMATION_FORMAT = dwarf; 226 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 227 | ENABLE_NS_ASSERTIONS = YES; 228 | GCC_OPTIMIZATION_LEVEL = 0; 229 | GCC_PREPROCESSOR_DEFINITIONS = ( 230 | "$(inherited)", 231 | "SWIFT_PACKAGE=1", 232 | "DEBUG=1", 233 | ); 234 | MACOSX_DEPLOYMENT_TARGET = 10.10; 235 | ONLY_ACTIVE_ARCH = YES; 236 | OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; 237 | PRODUCT_NAME = "$(TARGET_NAME)"; 238 | SDKROOT = macosx; 239 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 240 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG"; 241 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 242 | USE_HEADERMAP = NO; 243 | }; 244 | name = Debug; 245 | }; 246 | OBJ_31 /* Debug */ = { 247 | isa = XCBuildConfiguration; 248 | buildSettings = { 249 | LD = /usr/bin/true; 250 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.1"; 251 | SWIFT_VERSION = 5.0; 252 | }; 253 | name = Debug; 254 | }; 255 | OBJ_32 /* Release */ = { 256 | isa = XCBuildConfiguration; 257 | buildSettings = { 258 | LD = /usr/bin/true; 259 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.1"; 260 | SWIFT_VERSION = 5.0; 261 | }; 262 | name = Release; 263 | }; 264 | OBJ_4 /* Release */ = { 265 | isa = XCBuildConfiguration; 266 | buildSettings = { 267 | CLANG_ENABLE_OBJC_ARC = YES; 268 | COMBINE_HIDPI_IMAGES = YES; 269 | COPY_PHASE_STRIP = YES; 270 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 271 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 272 | GCC_OPTIMIZATION_LEVEL = s; 273 | GCC_PREPROCESSOR_DEFINITIONS = ( 274 | "$(inherited)", 275 | "SWIFT_PACKAGE=1", 276 | ); 277 | MACOSX_DEPLOYMENT_TARGET = 10.10; 278 | OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; 279 | PRODUCT_NAME = "$(TARGET_NAME)"; 280 | SDKROOT = macosx; 281 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 282 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE"; 283 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 284 | USE_HEADERMAP = NO; 285 | }; 286 | name = Release; 287 | }; 288 | /* End XCBuildConfiguration section */ 289 | 290 | /* Begin XCConfigurationList section */ 291 | OBJ_2 /* Build configuration list for PBXProject "FocusableImageView" */ = { 292 | isa = XCConfigurationList; 293 | buildConfigurations = ( 294 | OBJ_3 /* Debug */, 295 | OBJ_4 /* Release */, 296 | ); 297 | defaultConfigurationIsVisible = 0; 298 | defaultConfigurationName = Release; 299 | }; 300 | OBJ_20 /* Build configuration list for PBXNativeTarget "FocusableImageView" */ = { 301 | isa = XCConfigurationList; 302 | buildConfigurations = ( 303 | OBJ_21 /* Debug */, 304 | OBJ_22 /* Release */, 305 | ); 306 | defaultConfigurationIsVisible = 0; 307 | defaultConfigurationName = Release; 308 | }; 309 | OBJ_30 /* Build configuration list for PBXNativeTarget "FocusableImageViewPackageDescription" */ = { 310 | isa = XCConfigurationList; 311 | buildConfigurations = ( 312 | OBJ_31 /* Debug */, 313 | OBJ_32 /* Release */, 314 | ); 315 | defaultConfigurationIsVisible = 0; 316 | defaultConfigurationName = Release; 317 | }; 318 | /* End XCConfigurationList section */ 319 | }; 320 | rootObject = OBJ_1 /* Project object */; 321 | } 322 | -------------------------------------------------------------------------------- /FocusableImageView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FocusableImageView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FocusableImageView.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FocusableImageView.xcodeproj/xcshareddata/xcschemes/FocusableImageView-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /FocusableImageView.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /FocusableImageView.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FocusableImageView.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 malt03 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.1 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: "FocusableImageView", 8 | platforms: [ 9 | .iOS(.v11), 10 | ], 11 | products: [ 12 | .library(name: "FocusableImageView", targets: ["FocusableImageView"]), 13 | ], 14 | dependencies: [], 15 | targets: [ 16 | .target(name: "FocusableImageView", dependencies: []), 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FocusableImageView [![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-4BC51D.svg)](https://github.com/apple/swift-package-manager) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg)](https://github.com/Carthage/Carthage) [![CocoaPods](https://img.shields.io/cocoapods/v/FocusableImageView.svg?style=flat)](http://cocoapods.org/pods/FocusableImageView) ![License](https://img.shields.io/github/license/malt03/FocusableImageView.svg) 2 | 3 | ![Screenshot](https://raw.githubusercontent.com/malt03/FocusableImageView/master/readme/Screenshot.gif) 4 | 5 | ## Minimum Example 6 | 7 | ```swift 8 | import UIKit 9 | import FocusableImageView 10 | 11 | class ViewController: UIViewController { 12 | @IBOutlet private weak var stackView: UIStackView! 13 | private let manager = FocusableImageViewManager() 14 | private var imageViews: [FocusableImageView] { stackView.arrangedSubviews as! [FocusableImageView] } 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | manager.register(parentViewController: self, imageViews: imageViews) 19 | } 20 | } 21 | ``` 22 | 23 | ## Installation 24 | ### [SwiftPM](https://github.com/apple/swift-package-manager) (Recommended) 25 | 26 | - On Xcode, click `File` > `Swift Packages` > `Add Package Dependency...` 27 | - Input `https://github.com/malt03/FocusableImageView.git` 28 | 29 | ### [Carthage](https://github.com/Carthage/Carthage) 30 | 31 | - Insert `github "malt03/FocusableImageView"` to your Cartfile. 32 | - Run `carthage update`. 33 | - Link your app with `FocusableImageView.framework` in `Carthage/Build`. 34 | 35 | ### [CocoaPods](https://github.com/cocoapods/cocoapods) 36 | 37 | - Insert `pod 'FocusableImageView'` to your Podfile. 38 | - Run `pod install`. 39 | 40 | ## Advanced Example 41 | ### Access to inner UIImageView 42 | ```swift 43 | imageView.inner.kf.setImage(url) // Set Image URL with Kingfisher 44 | ``` 45 | 46 | ### Additional Animation for ImageView 47 | ```swift 48 | manager.delegate = self 49 | 50 | extension ViewController: FocusableImageViewDelegate { 51 | func selectableImageViewPresentAnimation(views: [FocusableImageView]) { 52 | views.forEach { $0.inner.layer.cornerRadius = 0 } 53 | } 54 | 55 | func selectableImageViewDismissAnimation(views: [FocusableImageView]) { 56 | views.forEach { $0.inner.layer.cornerRadius = 8 } 57 | } 58 | } 59 | ``` 60 | 61 | ### Set Configuration 62 | ```swift 63 | manager.configuration = .init( 64 | backgroundColor: .init(white: 0, alpha: 0.5), 65 | animationDuration: 0.5, 66 | pageControlConfiguration: .init(hidesForSinglePage: false, pageIndicatorTintColor: nil, currentPageIndicatorTintColor: nil), 67 | maximumZoomScale: 2, 68 | createDismissButton: { (parentView) -> UIButton in 69 | let button = UIButton() 70 | button.translatesAutoresizingMaskIntoConstraints = false 71 | button.setTitle("Close", for: .normal) 72 | button.setTitleColor(.white, for: .normal) 73 | button.titleLabel?.font = UIFont.systemFont(ofSize: 16) 74 | parentView.addSubview(button) 75 | NSLayoutConstraint.activate([ 76 | button.leadingAnchor.constraint(equalTo: parentView.leadingAnchor, constant: 16), 77 | button.topAnchor.constraint(equalTo: parentView.safeAreaLayoutGuide.topAnchor, constant: 16), 78 | ]) 79 | return button 80 | } 81 | ) 82 | ``` 83 | 84 | ### default configuration 85 | ```swift 86 | FocusableImageViewConfiguration.default = configuration 87 | ``` 88 | -------------------------------------------------------------------------------- /Sources/FocusableImageView/FocusableImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusableImageView.swift 3 | // FocusableImageView 4 | // 5 | // Created by Koji Murata on 2020/03/20. 6 | // 7 | 8 | import UIKit 9 | 10 | public class FocusableImageView: UIView { 11 | @IBInspectable public var image: UIImage? { 12 | get { inner.image } 13 | set { inner.image = newValue } 14 | } 15 | public override var contentMode: UIView.ContentMode { 16 | get { inner.contentMode } 17 | set { inner.contentMode = newValue } 18 | } 19 | 20 | public let inner = UIImageView() 21 | private var innerImageViewConstraints: [NSLayoutConstraint]? 22 | var tappedHandler: ((FocusableImageView) -> Void)? 23 | 24 | override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | initialize() 27 | } 28 | 29 | required init?(coder: NSCoder) { 30 | super.init(coder: coder) 31 | initialize() 32 | } 33 | 34 | private func initialize() { 35 | inner.translatesAutoresizingMaskIntoConstraints = false 36 | addImageView() 37 | 38 | let tapGestureRecognizer = UITapGestureRecognizer() 39 | tapGestureRecognizer.addTarget(self, action: #selector(tapped)) 40 | addGestureRecognizer(tapGestureRecognizer) 41 | } 42 | 43 | func addImageView() { 44 | addSubview(inner) 45 | let constraints = [ 46 | topAnchor.constraint(equalTo: inner.topAnchor), 47 | bottomAnchor.constraint(equalTo: inner.bottomAnchor), 48 | leadingAnchor.constraint(equalTo: inner.leadingAnchor), 49 | trailingAnchor.constraint(equalTo: inner.trailingAnchor), 50 | ] 51 | NSLayoutConstraint.activate(constraints) 52 | innerImageViewConstraints = constraints 53 | } 54 | 55 | func removeImageView() { 56 | if let innerImageViewConstraints = innerImageViewConstraints { 57 | NSLayoutConstraint.deactivate(innerImageViewConstraints) 58 | } 59 | innerImageViewConstraints = nil 60 | inner.removeFromSuperview() 61 | } 62 | 63 | @objc private func tapped() { 64 | tappedHandler?(self) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/FocusableImageView/FocusableImageViewConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusableImageViewConfiguration.swift 3 | // FocusableImageView 4 | // 5 | // Created by Koji Murata on 2020/03/21. 6 | // 7 | 8 | import UIKit 9 | 10 | public struct FocusableImageViewConfiguration { 11 | let backgroundColor: UIColor 12 | let animationDuration: TimeInterval 13 | let pageControlConfiguration: PageControlConfiguration 14 | let maximumZoomScale: CGFloat 15 | let createDismissButton: ((_ parentView: UIView) -> UIButton)? 16 | 17 | public static var `default` = FocusableImageViewConfiguration() 18 | 19 | public init( 20 | backgroundColor: UIColor = .init(white: 0, alpha: 0.5), 21 | animationDuration: TimeInterval = 0.3, 22 | pageControlConfiguration: PageControlConfiguration = .init(), 23 | maximumZoomScale: CGFloat = 1, 24 | createDismissButton: ((_ parentView: UIView) -> UIButton)? = nil 25 | ) { 26 | self.backgroundColor = backgroundColor 27 | self.animationDuration = animationDuration 28 | self.pageControlConfiguration = pageControlConfiguration 29 | self.maximumZoomScale = maximumZoomScale 30 | self.createDismissButton = createDismissButton 31 | } 32 | 33 | public struct PageControlConfiguration { 34 | let hidesForSinglePage: Bool 35 | let pageIndicatorTintColor: UIColor? 36 | let currentPageIndicatorTintColor: UIColor? 37 | 38 | public init( 39 | hidesForSinglePage: Bool = true, 40 | pageIndicatorTintColor: UIColor? = nil, 41 | currentPageIndicatorTintColor: UIColor? = nil 42 | ) { 43 | self.hidesForSinglePage = hidesForSinglePage 44 | self.pageIndicatorTintColor = pageIndicatorTintColor 45 | self.currentPageIndicatorTintColor = currentPageIndicatorTintColor 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/FocusableImageView/FocusableImageViewManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusableImageViewManager.swift 3 | // FocusableImageView 4 | // 5 | // Created by Koji Murata on 2020/03/20. 6 | // 7 | 8 | import UIKit 9 | 10 | public protocol FocusableImageViewDelegate: class { 11 | func focusableImageViewPresentAnimation(views: [FocusableImageView]) 12 | func focusableImageViewDismissAnimation(views: [FocusableImageView]) 13 | } 14 | 15 | public final class FocusableImageViewManager { 16 | public init(configuration: FocusableImageViewConfiguration = .default) { 17 | self.configuration = configuration 18 | } 19 | 20 | public var configuration: FocusableImageViewConfiguration 21 | public weak var delegate: FocusableImageViewDelegate? 22 | 23 | public func register(parentViewController: UIViewController, imageViews: S) where S.Element == FocusableImageView { 24 | for imageView in imageViews { 25 | imageView.tappedHandler = { [weak self] (imageView) in 26 | self?.present(imageView: imageView) 27 | } 28 | } 29 | viewController = parentViewController 30 | self.imageViews = imageViews.map { .init($0) } 31 | } 32 | 33 | weak var viewController: UIViewController? 34 | private var imageViews: [ImageViewHolder]? 35 | 36 | private final class ImageViewHolder { 37 | weak var value: FocusableImageView? 38 | fileprivate init(_ value: FocusableImageView) { self.value = value } 39 | } 40 | 41 | func present(imageView: FocusableImageView) { 42 | guard let viewController = viewController, let imageViews = imageViews else { return } 43 | let vc = ImagesViewController() 44 | let views = imageViews.compactMap { $0.value } 45 | vc.prepare( 46 | delegate: delegate, 47 | configuration: configuration, 48 | selectableImageViews: views, 49 | selectedImageIndex: views.firstIndex(of: imageView) ?? 0 50 | ) 51 | viewController.present(vc, animated: true, completion: nil) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/FocusableImageView/ImagesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagesViewController.swift 3 | // FocusableImageView 4 | // 5 | // Created by Koji Murata on 2020/03/20. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ImagesViewController: UIViewController { 11 | private let backgroundView = UIView() 12 | private let scrollView = UIScrollView() 13 | private let scrollContainerView = UIView() 14 | private let pageControl = UIPageControl() 15 | 16 | private var presenting = true 17 | private var constraints: [NSLayoutConstraint]? 18 | private var imageScrollViews = [UIScrollView]() 19 | private var lastPanInfo: (velocityY: CGFloat, targetImageView: UIImageView)? 20 | 21 | private var pannableConstraints = [UIView: NSLayoutConstraint]() 22 | 23 | private weak var delegate: FocusableImageViewDelegate? 24 | private var configuration: FocusableImageViewConfiguration! 25 | private var selectableImageViews: [FocusableImageView]! 26 | private var selectedImageIndex: Int! 27 | private var dismissButton: UIButton? 28 | 29 | override func viewWillAppear(_ animated: Bool) { 30 | presenting = true 31 | super.viewWillAppear(animated) 32 | } 33 | 34 | override func viewWillDisappear(_ animated: Bool) { 35 | presenting = false 36 | super.viewWillDisappear(animated) 37 | } 38 | 39 | func prepare( 40 | delegate: FocusableImageViewDelegate?, 41 | configuration: FocusableImageViewConfiguration, 42 | selectableImageViews: [FocusableImageView], 43 | selectedImageIndex: Int 44 | ) { 45 | self.delegate = delegate 46 | self.configuration = configuration 47 | self.selectableImageViews = selectableImageViews 48 | self.selectedImageIndex = selectedImageIndex 49 | modalPresentationStyle = .overFullScreen 50 | transitioningDelegate = self 51 | } 52 | 53 | @objc private func tapped() { 54 | dismiss(animated: true, completion: nil) 55 | } 56 | override func viewDidLoad() { 57 | super.viewDidLoad() 58 | 59 | let pan = UIPanGestureRecognizer(target: self, action: #selector(panned(_:))) 60 | view.addGestureRecognizer(pan) 61 | 62 | view.backgroundColor = .clear 63 | backgroundView.backgroundColor = configuration.backgroundColor 64 | scrollView.translatesAutoresizingMaskIntoConstraints = false 65 | backgroundView.translatesAutoresizingMaskIntoConstraints = false 66 | scrollContainerView.translatesAutoresizingMaskIntoConstraints = false 67 | pageControl.translatesAutoresizingMaskIntoConstraints = false 68 | 69 | scrollView.isPagingEnabled = true 70 | scrollView.showsHorizontalScrollIndicator = false 71 | scrollView.delegate = self 72 | 73 | view.addSubview(backgroundView) 74 | view.addSubview(scrollView) 75 | backgroundView.addSubview(pageControl) 76 | scrollView.addSubview(scrollContainerView) 77 | 78 | pageControl.hidesForSinglePage = configuration.pageControlConfiguration.hidesForSinglePage 79 | pageControl.pageIndicatorTintColor = configuration.pageControlConfiguration.pageIndicatorTintColor 80 | pageControl.currentPageIndicatorTintColor = configuration.pageControlConfiguration.currentPageIndicatorTintColor 81 | pageControl.numberOfPages = selectableImageViews.count 82 | pageControl.currentPage = selectedImageIndex 83 | 84 | NSLayoutConstraint.activate([ 85 | view.topAnchor.constraint(equalTo: backgroundView.topAnchor), 86 | view.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor), 87 | view.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor), 88 | view.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor), 89 | view.topAnchor.constraint(equalTo: scrollView.topAnchor), 90 | view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), 91 | view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), 92 | view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), 93 | backgroundView.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: pageControl.bottomAnchor, constant: 8), 94 | backgroundView.centerXAnchor.constraint(equalTo: pageControl.centerXAnchor), 95 | scrollView.topAnchor.constraint(equalTo: scrollContainerView.topAnchor), 96 | scrollView.bottomAnchor.constraint(equalTo: scrollContainerView.bottomAnchor), 97 | scrollView.leadingAnchor.constraint(equalTo: scrollContainerView.leadingAnchor), 98 | scrollView.trailingAnchor.constraint(equalTo: scrollContainerView.trailingAnchor), 99 | scrollView.heightAnchor.constraint(equalTo: scrollContainerView.heightAnchor), 100 | ]) 101 | 102 | let button = configuration.createDismissButton?(view) 103 | button?.addTarget(self, action: #selector(close), for: .touchUpInside) 104 | dismissButton = button 105 | } 106 | 107 | @objc private func panned(_ sender: UIPanGestureRecognizer) { 108 | guard let target = pannableConstraints.keys.first(where: { (0...view.bounds.width).contains(scrollView.convert($0.center, to: view).x) }) else { return } 109 | switch sender.state { 110 | case .changed: 111 | let translation = sender.translation(in: view) 112 | pannableConstraints[target]?.constant = translation.y 113 | case .ended, .cancelled: 114 | lastPanInfo = (sender.velocity(in: view).y, target.subviews.first! as! UIImageView) 115 | close() 116 | default: 117 | break 118 | } 119 | } 120 | 121 | @objc private func close() { 122 | dismiss(animated: true, completion: nil) 123 | } 124 | 125 | @objc private func doubleTapped(_ sender: UITapGestureRecognizer) { 126 | let imageScrollView = (sender.view as! UIScrollView) 127 | if imageScrollView.zoomScale == 1 { 128 | let point = sender.location(in: imageScrollView) 129 | imageScrollView.zoom(to: CGRect(origin: point, size: .zero), animated: true) 130 | } else { 131 | imageScrollView.setZoomScale(1, animated: true) 132 | } 133 | } 134 | } 135 | 136 | extension ImagesViewController: UIScrollViewDelegate { 137 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 138 | if scrollView != self.scrollView { return } 139 | let page = Int((scrollView.contentOffset.x / scrollView.bounds.width) + 0.5) 140 | pageControl.currentPage = min(max(0, page), pageControl.numberOfPages - 1) 141 | } 142 | 143 | func viewForZooming(in scrollView: UIScrollView) -> UIView? { 144 | if scrollView == self.scrollView { return nil } 145 | return scrollView.subviews.first 146 | } 147 | 148 | func scrollViewDidZoom(_ scrollView: UIScrollView) { 149 | if scrollView == self.scrollView { return } 150 | let v = scrollView.subviews.first! 151 | scrollView.contentInset = UIEdgeInsets( 152 | top: max((scrollView.frame.height - v.frame.height)/2, 0), 153 | left: 0, 154 | bottom: max((scrollView.frame.width - v.frame.width)/2, 0), 155 | right: 0 156 | ) 157 | } 158 | } 159 | 160 | extension ImagesViewController: UIViewControllerTransitioningDelegate { 161 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 162 | return self 163 | } 164 | 165 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 166 | return self 167 | } 168 | } 169 | 170 | extension ImagesViewController: UIViewControllerAnimatedTransitioning { 171 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 172 | configuration.animationDuration 173 | } 174 | 175 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 176 | if presenting { 177 | presentAnimateTransition(using: transitionContext) 178 | } else { 179 | dismissAnimateTransition(using: transitionContext) 180 | } 181 | } 182 | 183 | private func presentAnimateTransition(using transitionContext: UIViewControllerContextTransitioning) { 184 | var constraints = [NSLayoutConstraint]() 185 | 186 | transitionContext.containerView.addSubview(view) 187 | view.frame = transitionContext.containerView.bounds 188 | scrollView.frame = view.bounds 189 | let widthMultiplier = CGFloat(selectableImageViews.count) 190 | scrollContainerView.frame = CGRect( 191 | x: 0, y: 0, 192 | width: widthMultiplier * scrollView.bounds.width, 193 | height: scrollView.bounds.height 194 | ) 195 | scrollContainerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: widthMultiplier).isActive = true 196 | 197 | scrollView.contentOffset.x = scrollView.bounds.width * CGFloat(selectedImageIndex) 198 | 199 | view.layoutIfNeeded() 200 | 201 | var lastAnchor = scrollContainerView.leadingAnchor 202 | imageScrollViews = [] 203 | for (i, selectableImageView) in selectableImageViews.enumerated() { 204 | selectableImageView.removeImageView() 205 | let imageView = selectableImageView.inner 206 | let imageScrollView = UIScrollView() 207 | imageScrollViews.append(imageScrollView) 208 | imageScrollView.clipsToBounds = false 209 | imageScrollView.translatesAutoresizingMaskIntoConstraints = false 210 | imageScrollView.delegate = self 211 | imageScrollView.maximumZoomScale = configuration.maximumZoomScale 212 | imageScrollView.bouncesZoom = false 213 | imageScrollView.bounces = false 214 | imageScrollView.showsVerticalScrollIndicator = false 215 | imageScrollView.showsHorizontalScrollIndicator = false 216 | let tapGestureRecognizer = UITapGestureRecognizer() 217 | tapGestureRecognizer.numberOfTapsRequired = 2 218 | tapGestureRecognizer.addTarget(self, action: #selector(doubleTapped(_:))) 219 | imageScrollView.addGestureRecognizer(tapGestureRecognizer) 220 | 221 | scrollContainerView.addSubview(imageScrollView) 222 | imageScrollView.addSubview(imageView) 223 | 224 | imageScrollView.frame = CGRect(origin: CGPoint(x: CGFloat(i) * scrollView.bounds.width, y: 0), size: scrollView.bounds.size) 225 | 226 | let imageRatio = imageView.image.map { $0.size.width / $0.size.height } ?? 1 227 | let inset = max((imageScrollView.bounds.height - imageScrollView.bounds.width / imageRatio) / 2, 0) 228 | imageScrollView.contentInset = UIEdgeInsets(top: inset, left: 0, bottom: 0, right: 0) 229 | 230 | imageView.frame = selectableImageView.convert(selectableImageView.bounds, to: imageScrollView) 231 | 232 | let centerYConstraint = imageScrollView.centerYAnchor.constraint(equalTo: scrollContainerView.centerYAnchor) 233 | constraints.append(contentsOf: [ 234 | lastAnchor.constraint(equalTo: imageScrollView.leadingAnchor), 235 | scrollView.widthAnchor.constraint(equalTo: imageScrollView.widthAnchor), 236 | scrollView.heightAnchor.constraint(equalTo: imageScrollView.heightAnchor), 237 | imageScrollView.leadingAnchor.constraint(equalTo: imageView.leadingAnchor), 238 | imageScrollView.trailingAnchor.constraint(equalTo: imageView.trailingAnchor), 239 | imageScrollView.topAnchor.constraint(equalTo: imageView.topAnchor), 240 | imageScrollView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor), 241 | imageScrollView.widthAnchor.constraint(equalTo: imageView.widthAnchor), 242 | imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: imageRatio), 243 | centerYConstraint, 244 | ]) 245 | lastAnchor = imageScrollView.trailingAnchor 246 | pannableConstraints[imageScrollView] = centerYConstraint 247 | } 248 | if let last = imageScrollViews.last { 249 | constraints.append(last.trailingAnchor.constraint(equalTo: scrollContainerView.trailingAnchor)) 250 | } 251 | 252 | NSLayoutConstraint.activate(constraints) 253 | self.constraints = constraints 254 | 255 | backgroundView.alpha = 0 256 | dismissButton?.alpha = 0 257 | backgroundView.frame = view.bounds 258 | 259 | UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: { 260 | self.delegate?.focusableImageViewPresentAnimation(views: self.selectableImageViews) 261 | self.backgroundView.alpha = 1 262 | self.dismissButton?.alpha = 1 263 | self.view.layoutIfNeeded() 264 | }, completion: { _ in 265 | transitionContext.completeTransition(true) 266 | }) 267 | } 268 | 269 | private func dismissAnimateTransition(using transitionContext: UIViewControllerContextTransitioning) { 270 | imageScrollViews.forEach { $0.setZoomScale(1, animated: true) } 271 | if let constraints = constraints { 272 | NSLayoutConstraint.deactivate(constraints) 273 | } 274 | constraints = nil 275 | 276 | var tmpConstraints = [NSLayoutConstraint]() 277 | var imageViewTargetRects = [UIImageView: CGRect]() 278 | for selectableImageView in self.selectableImageViews { 279 | let imageView = selectableImageView.inner 280 | let newRect = selectableImageView.convert(selectableImageView.bounds, to: scrollContainerView) 281 | imageViewTargetRects[imageView] = newRect 282 | tmpConstraints.append(contentsOf: [ 283 | imageView.leadingAnchor.constraint(equalTo: scrollContainerView.leadingAnchor, constant: newRect.minX), 284 | imageView.topAnchor.constraint(equalTo: scrollContainerView.topAnchor, constant: newRect.minY), 285 | imageView.widthAnchor.constraint(equalToConstant: newRect.width), 286 | imageView.heightAnchor.constraint(equalToConstant: newRect.height), 287 | ]) 288 | } 289 | 290 | NSLayoutConstraint.activate(tmpConstraints) 291 | 292 | let velocity: CGFloat 293 | if let lastPanInfo = lastPanInfo, let targetRect = imageViewTargetRects[lastPanInfo.targetImageView] { 294 | let fromCenterY = lastPanInfo.targetImageView.center.y 295 | let targetCenterY = targetRect.origin.y + targetRect.height / 2 296 | velocity = lastPanInfo.velocityY / (targetCenterY - fromCenterY) 297 | } else { 298 | velocity = 0 299 | } 300 | 301 | UIView.animate( 302 | withDuration: transitionDuration(using: transitionContext), 303 | delay: 0, 304 | usingSpringWithDamping: 1, 305 | initialSpringVelocity: velocity, 306 | animations: { 307 | self.delegate?.focusableImageViewDismissAnimation(views: self.selectableImageViews) 308 | self.backgroundView.alpha = 0 309 | self.dismissButton?.alpha = 0 310 | self.view.layoutIfNeeded() 311 | }, 312 | completion: { _ in 313 | NSLayoutConstraint.deactivate(tmpConstraints) 314 | self.selectableImageViews.forEach { $0.inner.removeFromSuperview() } 315 | self.selectableImageViews.forEach { $0.addImageView() } 316 | transitionContext.completeTransition(true) 317 | } 318 | ) 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /Tests/FocusableImageViewTests/FocusableImageViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FocusableImageView 3 | 4 | final class FocusableImageViewTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(FocusableImageView().text, "Hello, World!") 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/FocusableImageViewTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(FocusableImageViewTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import FocusableImageViewTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += FocusableImageViewTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /readme/Screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malt03/FocusableImageView/e18aee11a998ee0d11cd8d08f87350a78d9b1e2b/readme/Screenshot.gif --------------------------------------------------------------------------------