├── .gitignore ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── icn_comment.imageset │ │ │ ├── Contents.json │ │ │ ├── comment.png │ │ │ ├── comment@2x.png │ │ │ └── comment@3x.png │ │ └── icn_like.imageset │ │ │ ├── Contents.json │ │ │ ├── like.png │ │ │ ├── like@2x.png │ │ │ └── like@3x.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── FMImageViewBottomView.swift │ ├── ImageCell.swift │ ├── Info.plist │ ├── PhotoGrid.swift │ ├── TableViewController.swift │ └── funmee.png └── ExampleTests │ ├── ExampleTests.swift │ └── Info.plist ├── FMImageView ├── FMImageView.framework │ ├── FMImageView │ ├── Headers │ │ ├── FMImageView-Swift.h │ │ └── FMImageView.h │ ├── Info.plist │ ├── Modules │ │ ├── FMImageView.swiftmodule │ │ │ ├── arm.swiftdoc │ │ │ ├── arm.swiftmodule │ │ │ ├── arm64.swiftdoc │ │ │ ├── arm64.swiftmodule │ │ │ ├── x86_64.swiftdoc │ │ │ └── x86_64.swiftmodule │ │ └── module.modulemap │ └── PresentFMImageViewController.nib ├── FMImageView.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── FMImageView.xcscheme ├── FMImageView │ ├── Alert │ │ └── FMAlert.swift │ ├── Animator │ │ ├── FMPhotoInteractionAnimator.swift │ │ ├── FMZoomInAnimationController.swift │ │ └── FMZoomOutAnimationController.swift │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── icn_close.imageset │ │ │ ├── Contents.json │ │ │ ├── close.png │ │ │ ├── close@2x.png │ │ │ └── close@3x.png │ │ └── icn_refresh.imageset │ │ │ ├── Contents.json │ │ │ ├── refresh.png │ │ │ ├── refresh@2x.png │ │ │ └── refresh@3x.png │ ├── Extensions │ │ ├── CGPoint.swift │ │ ├── CGSize.swift │ │ ├── UIImage.swift │ │ ├── UIPanGestureRecognizer.swift │ │ └── UIView.swift │ ├── FMImageView.h │ ├── Help │ │ ├── CGImage+Help.swift │ │ ├── Constants.swift │ │ ├── Enums.swift │ │ └── Image+URL+Size.swift │ ├── Indicator │ │ └── FMLoadingView.swift │ ├── Info.plist │ ├── Network │ │ └── ImageLoader.swift │ ├── Protocols │ │ └── FMDelegateProtocol.swift │ └── Views │ │ ├── Config │ │ ├── Config.swift │ │ └── ConfigureZoomView.swift │ │ ├── Datasource │ │ └── FMImageDataSource.swift │ │ ├── FMImagePreviewViewController.swift │ │ ├── FMImageSlideViewController.swift │ │ ├── ImageZoomView.swift │ │ └── Subviews │ │ └── HorizontalStackView.swift └── FMImageViewTests │ ├── FMImageViewTests.swift │ └── Info.plist ├── LICENSE ├── README.md └── resources ├── FMImageView.gif └── funmee.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.xcbkptlist 2 | .DS_Store 3 | 4 | ## Various settings 5 | *.pbxuser 6 | xcuserdata/ 7 | 8 | ## Other 9 | *.moved-aside 10 | *.xccheckout 11 | *.xcscmblueprint 12 | 13 | ## Obj-C/Swift specific 14 | *.hmap 15 | *.ipa 16 | *.dSYM.zip 17 | *.dSYM 18 | FMImageView/build 19 | FMImageView/FMImageView/.DS_Store 20 | FMImageView/FMImageView.xcodeproj/xcuserdata/ 21 | Example/Example.xcodeproj/project.xcworkspace/xcuserdata/ 22 | Example/Example/.DS_Store 23 | Example/Example/Assets.xcassets/.DS_Store 24 | -------------------------------------------------------------------------------- /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 | 096953A920B520C50057BFEE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 096953A820B520C50057BFEE /* AppDelegate.swift */; }; 11 | 096953AE20B520C50057BFEE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 096953AC20B520C50057BFEE /* Main.storyboard */; }; 12 | 096953B020B520C80057BFEE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 096953AF20B520C80057BFEE /* Assets.xcassets */; }; 13 | 096953B320B520C80057BFEE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 096953B120B520C80057BFEE /* LaunchScreen.storyboard */; }; 14 | 096953BE20B520C80057BFEE /* ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 096953BD20B520C80057BFEE /* ExampleTests.swift */; }; 15 | 096953D720B5216D0057BFEE /* FMImageView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 096953CF20B520E10057BFEE /* FMImageView.framework */; }; 16 | 096953D820B5216D0057BFEE /* FMImageView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 096953CF20B520E10057BFEE /* FMImageView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 17 | 09F663C220DC970000AB2B8A /* funmee.png in Resources */ = {isa = PBXBuildFile; fileRef = 09F663C120DC970000AB2B8A /* funmee.png */; }; 18 | AB5E53032112A14700F24D8A /* PhotoGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5E53002112A14700F24D8A /* PhotoGrid.swift */; }; 19 | ABAE8A8620EDBAE500383EEC /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABAE8A8520EDBAE500383EEC /* TableViewController.swift */; }; 20 | ABAE8A8A20EDBCF600383EEC /* ImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABAE8A8920EDBCF600383EEC /* ImageCell.swift */; }; 21 | ABB852BA21182EB200CC3CA6 /* FMImageViewBottomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB852B921182EB200CC3CA6 /* FMImageViewBottomView.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXContainerItemProxy section */ 25 | 096953BA20B520C80057BFEE /* PBXContainerItemProxy */ = { 26 | isa = PBXContainerItemProxy; 27 | containerPortal = 0969539D20B520C50057BFEE /* Project object */; 28 | proxyType = 1; 29 | remoteGlobalIDString = 096953A420B520C50057BFEE; 30 | remoteInfo = Example; 31 | }; 32 | 096953CE20B520E10057BFEE /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = 096953C820B520E10057BFEE /* FMImageView.xcodeproj */; 35 | proxyType = 2; 36 | remoteGlobalIDString = 0969537B20B51ECE0057BFEE; 37 | remoteInfo = FMImageView; 38 | }; 39 | 096953D020B520E10057BFEE /* PBXContainerItemProxy */ = { 40 | isa = PBXContainerItemProxy; 41 | containerPortal = 096953C820B520E10057BFEE /* FMImageView.xcodeproj */; 42 | proxyType = 2; 43 | remoteGlobalIDString = 0969538420B51ECE0057BFEE; 44 | remoteInfo = FMImageViewTests; 45 | }; 46 | 096953D920B5216D0057BFEE /* PBXContainerItemProxy */ = { 47 | isa = PBXContainerItemProxy; 48 | containerPortal = 096953C820B520E10057BFEE /* FMImageView.xcodeproj */; 49 | proxyType = 1; 50 | remoteGlobalIDString = 0969537A20B51ECE0057BFEE; 51 | remoteInfo = FMImageView; 52 | }; 53 | /* End PBXContainerItemProxy section */ 54 | 55 | /* Begin PBXCopyFilesBuildPhase section */ 56 | 096953DB20B5216D0057BFEE /* Embed Frameworks */ = { 57 | isa = PBXCopyFilesBuildPhase; 58 | buildActionMask = 2147483647; 59 | dstPath = ""; 60 | dstSubfolderSpec = 10; 61 | files = ( 62 | 096953D820B5216D0057BFEE /* FMImageView.framework in Embed Frameworks */, 63 | ); 64 | name = "Embed Frameworks"; 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | /* End PBXCopyFilesBuildPhase section */ 68 | 69 | /* Begin PBXFileReference section */ 70 | 096953A520B520C50057BFEE /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 71 | 096953A820B520C50057BFEE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 72 | 096953AD20B520C50057BFEE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 73 | 096953AF20B520C80057BFEE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 74 | 096953B220B520C80057BFEE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 75 | 096953B420B520C80057BFEE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 76 | 096953B920B520C80057BFEE /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 77 | 096953BD20B520C80057BFEE /* ExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTests.swift; sourceTree = ""; }; 78 | 096953BF20B520C80057BFEE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 79 | 096953C820B520E10057BFEE /* FMImageView.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = FMImageView.xcodeproj; path = ../FMImageView/FMImageView.xcodeproj; sourceTree = ""; }; 80 | 09F663C120DC970000AB2B8A /* funmee.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = funmee.png; sourceTree = ""; }; 81 | AB5E53002112A14700F24D8A /* PhotoGrid.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoGrid.swift; sourceTree = ""; }; 82 | ABAE8A8520EDBAE500383EEC /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 83 | ABAE8A8920EDBCF600383EEC /* ImageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCell.swift; sourceTree = ""; }; 84 | ABB852B921182EB200CC3CA6 /* FMImageViewBottomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FMImageViewBottomView.swift; sourceTree = ""; }; 85 | /* End PBXFileReference section */ 86 | 87 | /* Begin PBXFrameworksBuildPhase section */ 88 | 096953A220B520C50057BFEE /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | 096953D720B5216D0057BFEE /* FMImageView.framework in Frameworks */, 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | 096953B620B520C80057BFEE /* Frameworks */ = { 97 | isa = PBXFrameworksBuildPhase; 98 | buildActionMask = 2147483647; 99 | files = ( 100 | ); 101 | runOnlyForDeploymentPostprocessing = 0; 102 | }; 103 | /* End PBXFrameworksBuildPhase section */ 104 | 105 | /* Begin PBXGroup section */ 106 | 0969539C20B520C50057BFEE = { 107 | isa = PBXGroup; 108 | children = ( 109 | 096953C820B520E10057BFEE /* FMImageView.xcodeproj */, 110 | 096953A720B520C50057BFEE /* Example */, 111 | 096953BC20B520C80057BFEE /* ExampleTests */, 112 | 096953A620B520C50057BFEE /* Products */, 113 | ); 114 | sourceTree = ""; 115 | }; 116 | 096953A620B520C50057BFEE /* Products */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 096953A520B520C50057BFEE /* Example.app */, 120 | 096953B920B520C80057BFEE /* ExampleTests.xctest */, 121 | ); 122 | name = Products; 123 | sourceTree = ""; 124 | }; 125 | 096953A720B520C50057BFEE /* Example */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | AB5E53002112A14700F24D8A /* PhotoGrid.swift */, 129 | 096953A820B520C50057BFEE /* AppDelegate.swift */, 130 | ABAE8A8520EDBAE500383EEC /* TableViewController.swift */, 131 | ABAE8A8920EDBCF600383EEC /* ImageCell.swift */, 132 | ABB852B921182EB200CC3CA6 /* FMImageViewBottomView.swift */, 133 | 096953AC20B520C50057BFEE /* Main.storyboard */, 134 | 096953AF20B520C80057BFEE /* Assets.xcassets */, 135 | 096953B120B520C80057BFEE /* LaunchScreen.storyboard */, 136 | 096953B420B520C80057BFEE /* Info.plist */, 137 | 09F663C120DC970000AB2B8A /* funmee.png */, 138 | ); 139 | path = Example; 140 | sourceTree = ""; 141 | }; 142 | 096953BC20B520C80057BFEE /* ExampleTests */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 096953BD20B520C80057BFEE /* ExampleTests.swift */, 146 | 096953BF20B520C80057BFEE /* Info.plist */, 147 | ); 148 | path = ExampleTests; 149 | sourceTree = ""; 150 | }; 151 | 096953C920B520E10057BFEE /* Products */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | 096953CF20B520E10057BFEE /* FMImageView.framework */, 155 | 096953D120B520E10057BFEE /* FMImageViewTests.xctest */, 156 | ); 157 | name = Products; 158 | sourceTree = ""; 159 | }; 160 | /* End PBXGroup section */ 161 | 162 | /* Begin PBXNativeTarget section */ 163 | 096953A420B520C50057BFEE /* Example */ = { 164 | isa = PBXNativeTarget; 165 | buildConfigurationList = 096953C220B520C80057BFEE /* Build configuration list for PBXNativeTarget "Example" */; 166 | buildPhases = ( 167 | 096953A120B520C50057BFEE /* Sources */, 168 | 096953A220B520C50057BFEE /* Frameworks */, 169 | 096953A320B520C50057BFEE /* Resources */, 170 | 096953DB20B5216D0057BFEE /* Embed Frameworks */, 171 | ); 172 | buildRules = ( 173 | ); 174 | dependencies = ( 175 | 096953DA20B5216D0057BFEE /* PBXTargetDependency */, 176 | ); 177 | name = Example; 178 | productName = Example; 179 | productReference = 096953A520B520C50057BFEE /* Example.app */; 180 | productType = "com.apple.product-type.application"; 181 | }; 182 | 096953B820B520C80057BFEE /* ExampleTests */ = { 183 | isa = PBXNativeTarget; 184 | buildConfigurationList = 096953C520B520C80057BFEE /* Build configuration list for PBXNativeTarget "ExampleTests" */; 185 | buildPhases = ( 186 | 096953B520B520C80057BFEE /* Sources */, 187 | 096953B620B520C80057BFEE /* Frameworks */, 188 | 096953B720B520C80057BFEE /* Resources */, 189 | ); 190 | buildRules = ( 191 | ); 192 | dependencies = ( 193 | 096953BB20B520C80057BFEE /* PBXTargetDependency */, 194 | ); 195 | name = ExampleTests; 196 | productName = ExampleTests; 197 | productReference = 096953B920B520C80057BFEE /* ExampleTests.xctest */; 198 | productType = "com.apple.product-type.bundle.unit-test"; 199 | }; 200 | /* End PBXNativeTarget section */ 201 | 202 | /* Begin PBXProject section */ 203 | 0969539D20B520C50057BFEE /* Project object */ = { 204 | isa = PBXProject; 205 | attributes = { 206 | LastSwiftUpdateCheck = 0930; 207 | LastUpgradeCheck = 0930; 208 | ORGANIZATIONNAME = "Hoang Trong Anh"; 209 | TargetAttributes = { 210 | 096953A420B520C50057BFEE = { 211 | CreatedOnToolsVersion = 9.3.1; 212 | }; 213 | 096953B820B520C80057BFEE = { 214 | CreatedOnToolsVersion = 9.3.1; 215 | TestTargetID = 096953A420B520C50057BFEE; 216 | }; 217 | }; 218 | }; 219 | buildConfigurationList = 096953A020B520C50057BFEE /* Build configuration list for PBXProject "Example" */; 220 | compatibilityVersion = "Xcode 9.3"; 221 | developmentRegion = en; 222 | hasScannedForEncodings = 0; 223 | knownRegions = ( 224 | en, 225 | Base, 226 | ); 227 | mainGroup = 0969539C20B520C50057BFEE; 228 | productRefGroup = 096953A620B520C50057BFEE /* Products */; 229 | projectDirPath = ""; 230 | projectReferences = ( 231 | { 232 | ProductGroup = 096953C920B520E10057BFEE /* Products */; 233 | ProjectRef = 096953C820B520E10057BFEE /* FMImageView.xcodeproj */; 234 | }, 235 | ); 236 | projectRoot = ""; 237 | targets = ( 238 | 096953A420B520C50057BFEE /* Example */, 239 | 096953B820B520C80057BFEE /* ExampleTests */, 240 | ); 241 | }; 242 | /* End PBXProject section */ 243 | 244 | /* Begin PBXReferenceProxy section */ 245 | 096953CF20B520E10057BFEE /* FMImageView.framework */ = { 246 | isa = PBXReferenceProxy; 247 | fileType = wrapper.framework; 248 | path = FMImageView.framework; 249 | remoteRef = 096953CE20B520E10057BFEE /* PBXContainerItemProxy */; 250 | sourceTree = BUILT_PRODUCTS_DIR; 251 | }; 252 | 096953D120B520E10057BFEE /* FMImageViewTests.xctest */ = { 253 | isa = PBXReferenceProxy; 254 | fileType = wrapper.cfbundle; 255 | path = FMImageViewTests.xctest; 256 | remoteRef = 096953D020B520E10057BFEE /* PBXContainerItemProxy */; 257 | sourceTree = BUILT_PRODUCTS_DIR; 258 | }; 259 | /* End PBXReferenceProxy section */ 260 | 261 | /* Begin PBXResourcesBuildPhase section */ 262 | 096953A320B520C50057BFEE /* Resources */ = { 263 | isa = PBXResourcesBuildPhase; 264 | buildActionMask = 2147483647; 265 | files = ( 266 | 096953B320B520C80057BFEE /* LaunchScreen.storyboard in Resources */, 267 | 09F663C220DC970000AB2B8A /* funmee.png in Resources */, 268 | 096953B020B520C80057BFEE /* Assets.xcassets in Resources */, 269 | 096953AE20B520C50057BFEE /* Main.storyboard in Resources */, 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | 096953B720B520C80057BFEE /* Resources */ = { 274 | isa = PBXResourcesBuildPhase; 275 | buildActionMask = 2147483647; 276 | files = ( 277 | ); 278 | runOnlyForDeploymentPostprocessing = 0; 279 | }; 280 | /* End PBXResourcesBuildPhase section */ 281 | 282 | /* Begin PBXSourcesBuildPhase section */ 283 | 096953A120B520C50057BFEE /* Sources */ = { 284 | isa = PBXSourcesBuildPhase; 285 | buildActionMask = 2147483647; 286 | files = ( 287 | ABB852BA21182EB200CC3CA6 /* FMImageViewBottomView.swift in Sources */, 288 | ABAE8A8A20EDBCF600383EEC /* ImageCell.swift in Sources */, 289 | ABAE8A8620EDBAE500383EEC /* TableViewController.swift in Sources */, 290 | AB5E53032112A14700F24D8A /* PhotoGrid.swift in Sources */, 291 | 096953A920B520C50057BFEE /* AppDelegate.swift in Sources */, 292 | ); 293 | runOnlyForDeploymentPostprocessing = 0; 294 | }; 295 | 096953B520B520C80057BFEE /* Sources */ = { 296 | isa = PBXSourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | 096953BE20B520C80057BFEE /* ExampleTests.swift in Sources */, 300 | ); 301 | runOnlyForDeploymentPostprocessing = 0; 302 | }; 303 | /* End PBXSourcesBuildPhase section */ 304 | 305 | /* Begin PBXTargetDependency section */ 306 | 096953BB20B520C80057BFEE /* PBXTargetDependency */ = { 307 | isa = PBXTargetDependency; 308 | target = 096953A420B520C50057BFEE /* Example */; 309 | targetProxy = 096953BA20B520C80057BFEE /* PBXContainerItemProxy */; 310 | }; 311 | 096953DA20B5216D0057BFEE /* PBXTargetDependency */ = { 312 | isa = PBXTargetDependency; 313 | name = FMImageView; 314 | targetProxy = 096953D920B5216D0057BFEE /* PBXContainerItemProxy */; 315 | }; 316 | /* End PBXTargetDependency section */ 317 | 318 | /* Begin PBXVariantGroup section */ 319 | 096953AC20B520C50057BFEE /* Main.storyboard */ = { 320 | isa = PBXVariantGroup; 321 | children = ( 322 | 096953AD20B520C50057BFEE /* Base */, 323 | ); 324 | name = Main.storyboard; 325 | sourceTree = ""; 326 | }; 327 | 096953B120B520C80057BFEE /* LaunchScreen.storyboard */ = { 328 | isa = PBXVariantGroup; 329 | children = ( 330 | 096953B220B520C80057BFEE /* Base */, 331 | ); 332 | name = LaunchScreen.storyboard; 333 | sourceTree = ""; 334 | }; 335 | /* End PBXVariantGroup section */ 336 | 337 | /* Begin XCBuildConfiguration section */ 338 | 096953C020B520C80057BFEE /* Debug */ = { 339 | isa = XCBuildConfiguration; 340 | buildSettings = { 341 | ALWAYS_SEARCH_USER_PATHS = NO; 342 | CLANG_ANALYZER_NONNULL = YES; 343 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 344 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 345 | CLANG_CXX_LIBRARY = "libc++"; 346 | CLANG_ENABLE_MODULES = YES; 347 | CLANG_ENABLE_OBJC_ARC = YES; 348 | CLANG_ENABLE_OBJC_WEAK = YES; 349 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 350 | CLANG_WARN_BOOL_CONVERSION = YES; 351 | CLANG_WARN_COMMA = YES; 352 | CLANG_WARN_CONSTANT_CONVERSION = YES; 353 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 354 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 355 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 356 | CLANG_WARN_EMPTY_BODY = YES; 357 | CLANG_WARN_ENUM_CONVERSION = YES; 358 | CLANG_WARN_INFINITE_RECURSION = YES; 359 | CLANG_WARN_INT_CONVERSION = YES; 360 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 361 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 362 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 363 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 364 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 365 | CLANG_WARN_STRICT_PROTOTYPES = YES; 366 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 367 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 368 | CLANG_WARN_UNREACHABLE_CODE = YES; 369 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 370 | CODE_SIGN_IDENTITY = "iPhone Developer"; 371 | COPY_PHASE_STRIP = NO; 372 | DEBUG_INFORMATION_FORMAT = dwarf; 373 | ENABLE_STRICT_OBJC_MSGSEND = YES; 374 | ENABLE_TESTABILITY = YES; 375 | GCC_C_LANGUAGE_STANDARD = gnu11; 376 | GCC_DYNAMIC_NO_PIC = NO; 377 | GCC_NO_COMMON_BLOCKS = YES; 378 | GCC_OPTIMIZATION_LEVEL = 0; 379 | GCC_PREPROCESSOR_DEFINITIONS = ( 380 | "DEBUG=1", 381 | "$(inherited)", 382 | ); 383 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 384 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 385 | GCC_WARN_UNDECLARED_SELECTOR = YES; 386 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 387 | GCC_WARN_UNUSED_FUNCTION = YES; 388 | GCC_WARN_UNUSED_VARIABLE = YES; 389 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 390 | MTL_ENABLE_DEBUG_INFO = YES; 391 | ONLY_ACTIVE_ARCH = YES; 392 | SDKROOT = iphoneos; 393 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 394 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 395 | }; 396 | name = Debug; 397 | }; 398 | 096953C120B520C80057BFEE /* Release */ = { 399 | isa = XCBuildConfiguration; 400 | buildSettings = { 401 | ALWAYS_SEARCH_USER_PATHS = NO; 402 | CLANG_ANALYZER_NONNULL = YES; 403 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 404 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 405 | CLANG_CXX_LIBRARY = "libc++"; 406 | CLANG_ENABLE_MODULES = YES; 407 | CLANG_ENABLE_OBJC_ARC = YES; 408 | CLANG_ENABLE_OBJC_WEAK = YES; 409 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 410 | CLANG_WARN_BOOL_CONVERSION = YES; 411 | CLANG_WARN_COMMA = YES; 412 | CLANG_WARN_CONSTANT_CONVERSION = YES; 413 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 414 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 415 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 416 | CLANG_WARN_EMPTY_BODY = YES; 417 | CLANG_WARN_ENUM_CONVERSION = YES; 418 | CLANG_WARN_INFINITE_RECURSION = YES; 419 | CLANG_WARN_INT_CONVERSION = YES; 420 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 421 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 422 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 423 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 424 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 425 | CLANG_WARN_STRICT_PROTOTYPES = YES; 426 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 427 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 428 | CLANG_WARN_UNREACHABLE_CODE = YES; 429 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 430 | CODE_SIGN_IDENTITY = "iPhone Developer"; 431 | COPY_PHASE_STRIP = NO; 432 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 433 | ENABLE_NS_ASSERTIONS = NO; 434 | ENABLE_STRICT_OBJC_MSGSEND = YES; 435 | GCC_C_LANGUAGE_STANDARD = gnu11; 436 | GCC_NO_COMMON_BLOCKS = YES; 437 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 438 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 439 | GCC_WARN_UNDECLARED_SELECTOR = YES; 440 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 441 | GCC_WARN_UNUSED_FUNCTION = YES; 442 | GCC_WARN_UNUSED_VARIABLE = YES; 443 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 444 | MTL_ENABLE_DEBUG_INFO = NO; 445 | SDKROOT = iphoneos; 446 | SWIFT_COMPILATION_MODE = wholemodule; 447 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 448 | VALIDATE_PRODUCT = YES; 449 | }; 450 | name = Release; 451 | }; 452 | 096953C320B520C80057BFEE /* Debug */ = { 453 | isa = XCBuildConfiguration; 454 | buildSettings = { 455 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 456 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 457 | CODE_SIGN_STYLE = Automatic; 458 | DEVELOPMENT_TEAM = 254Q23AKWK; 459 | INFOPLIST_FILE = Example/Info.plist; 460 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 461 | LD_RUNPATH_SEARCH_PATHS = ( 462 | "$(inherited)", 463 | "@executable_path/Frameworks", 464 | ); 465 | PRODUCT_BUNDLE_IDENTIFIER = anhht.Example; 466 | PRODUCT_NAME = "$(TARGET_NAME)"; 467 | SWIFT_VERSION = 4.0; 468 | TARGETED_DEVICE_FAMILY = 1; 469 | }; 470 | name = Debug; 471 | }; 472 | 096953C420B520C80057BFEE /* Release */ = { 473 | isa = XCBuildConfiguration; 474 | buildSettings = { 475 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 476 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 477 | CODE_SIGN_STYLE = Automatic; 478 | DEVELOPMENT_TEAM = 254Q23AKWK; 479 | INFOPLIST_FILE = Example/Info.plist; 480 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 481 | LD_RUNPATH_SEARCH_PATHS = ( 482 | "$(inherited)", 483 | "@executable_path/Frameworks", 484 | ); 485 | PRODUCT_BUNDLE_IDENTIFIER = anhht.Example; 486 | PRODUCT_NAME = "$(TARGET_NAME)"; 487 | SWIFT_VERSION = 4.0; 488 | TARGETED_DEVICE_FAMILY = 1; 489 | }; 490 | name = Release; 491 | }; 492 | 096953C620B520C80057BFEE /* Debug */ = { 493 | isa = XCBuildConfiguration; 494 | buildSettings = { 495 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 496 | BUNDLE_LOADER = "$(TEST_HOST)"; 497 | CODE_SIGN_STYLE = Automatic; 498 | DEVELOPMENT_TEAM = 254Q23AKWK; 499 | INFOPLIST_FILE = ExampleTests/Info.plist; 500 | LD_RUNPATH_SEARCH_PATHS = ( 501 | "$(inherited)", 502 | "@executable_path/Frameworks", 503 | "@loader_path/Frameworks", 504 | ); 505 | PRODUCT_BUNDLE_IDENTIFIER = anhht.ExampleTests; 506 | PRODUCT_NAME = "$(TARGET_NAME)"; 507 | SWIFT_VERSION = 4.0; 508 | TARGETED_DEVICE_FAMILY = "1,2"; 509 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; 510 | }; 511 | name = Debug; 512 | }; 513 | 096953C720B520C80057BFEE /* Release */ = { 514 | isa = XCBuildConfiguration; 515 | buildSettings = { 516 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 517 | BUNDLE_LOADER = "$(TEST_HOST)"; 518 | CODE_SIGN_STYLE = Automatic; 519 | DEVELOPMENT_TEAM = 254Q23AKWK; 520 | INFOPLIST_FILE = ExampleTests/Info.plist; 521 | LD_RUNPATH_SEARCH_PATHS = ( 522 | "$(inherited)", 523 | "@executable_path/Frameworks", 524 | "@loader_path/Frameworks", 525 | ); 526 | PRODUCT_BUNDLE_IDENTIFIER = anhht.ExampleTests; 527 | PRODUCT_NAME = "$(TARGET_NAME)"; 528 | SWIFT_VERSION = 4.0; 529 | TARGETED_DEVICE_FAMILY = "1,2"; 530 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; 531 | }; 532 | name = Release; 533 | }; 534 | /* End XCBuildConfiguration section */ 535 | 536 | /* Begin XCConfigurationList section */ 537 | 096953A020B520C50057BFEE /* Build configuration list for PBXProject "Example" */ = { 538 | isa = XCConfigurationList; 539 | buildConfigurations = ( 540 | 096953C020B520C80057BFEE /* Debug */, 541 | 096953C120B520C80057BFEE /* Release */, 542 | ); 543 | defaultConfigurationIsVisible = 0; 544 | defaultConfigurationName = Release; 545 | }; 546 | 096953C220B520C80057BFEE /* Build configuration list for PBXNativeTarget "Example" */ = { 547 | isa = XCConfigurationList; 548 | buildConfigurations = ( 549 | 096953C320B520C80057BFEE /* Debug */, 550 | 096953C420B520C80057BFEE /* Release */, 551 | ); 552 | defaultConfigurationIsVisible = 0; 553 | defaultConfigurationName = Release; 554 | }; 555 | 096953C520B520C80057BFEE /* Build configuration list for PBXNativeTarget "ExampleTests" */ = { 556 | isa = XCConfigurationList; 557 | buildConfigurations = ( 558 | 096953C620B520C80057BFEE /* Debug */, 559 | 096953C720B520C80057BFEE /* Release */, 560 | ); 561 | defaultConfigurationIsVisible = 0; 562 | defaultConfigurationName = Release; 563 | }; 564 | /* End XCConfigurationList section */ 565 | }; 566 | rootObject = 0969539D20B520C50057BFEE /* Project object */; 567 | } 568 | -------------------------------------------------------------------------------- /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 Hoang Trong Anh on 5/23/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/icn_comment.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "comment.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "comment@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "comment@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/icn_comment.imageset/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/Example/Example/Assets.xcassets/icn_comment.imageset/comment.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/icn_comment.imageset/comment@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/Example/Example/Assets.xcassets/icn_comment.imageset/comment@2x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/icn_comment.imageset/comment@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/Example/Example/Assets.xcassets/icn_comment.imageset/comment@3x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/icn_like.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "like.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "like@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "like@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/icn_like.imageset/like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/Example/Example/Assets.xcassets/icn_like.imageset/like.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/icn_like.imageset/like@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/Example/Example/Assets.xcassets/icn_like.imageset/like@2x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/icn_like.imageset/like@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/Example/Example/Assets.xcassets/icn_like.imageset/like@3x.png -------------------------------------------------------------------------------- /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 | 81 | -------------------------------------------------------------------------------- /Example/Example/FMImageViewBottomView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FMImageViewBottomView.swift 3 | // Example 4 | // 5 | // Created by Trần Quang Minh on 8/6/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FMImageViewBottomView: UIView { 12 | var stackView: UIStackView? 13 | 14 | var subAreaBottomView: [(button: UIButton, label: UILabel)] = [] 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | commonInit() 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | super.init(coder: aDecoder) 23 | } 24 | 25 | 26 | func commonInit() { 27 | let likeButton = UIButton() 28 | likeButton.setImage(#imageLiteral(resourceName: "icn_like"), for: .normal) 29 | likeButton.isUserInteractionEnabled = true 30 | likeButton.addTarget(self, action: #selector(target1(_:)), for: .touchUpInside) 31 | let label = UILabel() 32 | label.text = "10" 33 | 34 | let button1 = UIButton() 35 | button1.setImage(#imageLiteral(resourceName: "icn_comment"), for: .normal) 36 | button1.isUserInteractionEnabled = true 37 | button1.addTarget(self, action: #selector(target2(_:)), for: .touchUpInside) 38 | let label1 = UILabel() 39 | label1.text = "20" 40 | 41 | self.subAreaBottomView.append((button: likeButton, label: label)) 42 | self.subAreaBottomView.append((button: button1, label: label1)) 43 | 44 | self.stackView = UIStackView() 45 | 46 | 47 | self.stackView?.translatesAutoresizingMaskIntoConstraints = false 48 | self.stackView?.heightAnchor.constraint(equalToConstant: 40).isActive = true 49 | 50 | self.stackView?.backgroundColor = .red 51 | self.stackView?.alignment = .center 52 | self.stackView?.distribution = .equalSpacing 53 | self.stackView?.axis = .horizontal 54 | self.stackView?.spacing = 0 55 | 56 | self.stackView?.autoresizingMask = [.flexibleWidth, .flexibleHeight, .flexibleBottomMargin] 57 | 58 | self.addSubview(self.stackView!) 59 | 60 | self.stackView?.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true 61 | self.stackView?.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0).isActive = true 62 | self.stackView?.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0).isActive = true 63 | 64 | for value in subAreaBottomView { 65 | let stack = UIView() 66 | 67 | stack.heightAnchor.constraint(equalToConstant: 40).isActive = true 68 | stack.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width / CGFloat(self.subAreaBottomView.count)).isActive = true 69 | 70 | let subStack = UIView() 71 | subStack.translatesAutoresizingMaskIntoConstraints = false 72 | subStack.heightAnchor.constraint(equalToConstant: 40 / 2).isActive = true 73 | 74 | // items of sub view 75 | // ** Buttons ** 76 | value.button.translatesAutoresizingMaskIntoConstraints = false 77 | value.button.heightAnchor.constraint(equalToConstant: 50).isActive = true 78 | value.button.widthAnchor.constraint(equalToConstant: 50).isActive = true 79 | 80 | subStack.addSubview(value.button) 81 | 82 | // ** Labels ** 83 | value.button.leftAnchor.constraint(equalTo: subStack.leftAnchor, constant: 0).isActive = true 84 | 85 | value.label.translatesAutoresizingMaskIntoConstraints = false 86 | value.label.heightAnchor.constraint(equalToConstant: 40 / 2).isActive = true 87 | value.label.numberOfLines = 1 88 | value.label.font = UIFont.systemFont(ofSize: 20.0, weight: .bold) 89 | value.label.textColor = .white 90 | 91 | subStack.addSubview(value.label) 92 | 93 | // ** Buttons ** 94 | value.button.leadingAnchor.constraint(equalTo: subStack.leadingAnchor, constant: 0).isActive = true 95 | value.button.centerYAnchor.constraint(equalTo: subStack.centerYAnchor, constant: 0).isActive = true 96 | 97 | // ** Labels ** 98 | value.label.trailingAnchor.constraint(equalTo: subStack.trailingAnchor, constant: 0).isActive = true 99 | value.label.leftAnchor.constraint(equalTo: value.button.rightAnchor, constant: 10).isActive = true 100 | value.label.centerYAnchor.constraint(equalTo: subStack.centerYAnchor, constant: 0).isActive = true 101 | 102 | stack.addSubview(subStack) 103 | 104 | // ** subview inside stack ** 105 | subStack.centerYAnchor.constraint(equalTo: stack.centerYAnchor, constant: 0).isActive = true 106 | subStack.centerXAnchor.constraint(equalTo: stack.centerXAnchor, constant: 0).isActive = true 107 | 108 | self.stackView?.addArrangedSubview(stack) 109 | } 110 | } 111 | 112 | @objc func target1(_ sender: UIButton) { 113 | print(#function) 114 | } 115 | 116 | @objc func target2(_ sender: UIButton) { 117 | print(#function) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Example/Example/ImageCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCell.swift 3 | // Example 4 | // 5 | // Created by Trần Quang Minh on 7/5/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ImageCell: UITableViewCell { 12 | 13 | @IBOutlet weak var photoImageView: UIImageView! 14 | @IBOutlet weak var photoGridView: UIView! 15 | @IBOutlet weak var photoGridHeightConstraint: NSLayoutConstraint! 16 | 17 | var presentFMImageView: ((_ index: Int, _ imageView: UIImageView, _ photoURLs: [URL]) -> Void)? 18 | 19 | var photoGrid = PhotoGridView(frame: CGRect.zero) 20 | private var photoUrl: [URL] = [URL(string: "https://cdn.stocksnap.io/img-thumbs/960w/LHOLK9MS1O.jpg")!, 21 | URL(string: "https://cdn.stocksnap.io/img-thumbs/960w/FAOI1VDJBI.jpg")!, 22 | URL(string: "https://cdn.stocksnap.io/img-thumbs/960w/L4B2YC09F2.jpg")!, 23 | URL(string: "https://cdn.stocksnap.io/img-thumbs/960w/GHR0BY9GIJ.jpg")!] 24 | 25 | override func awakeFromNib() { 26 | super.awakeFromNib() 27 | // Initialization code 28 | self.photoImageView.contentMode = .scaleAspectFill 29 | self.photoImageView.clipsToBounds = true 30 | 31 | photoGrid.delegate = self 32 | } 33 | 34 | override func setSelected(_ selected: Bool, animated: Bool) { 35 | super.setSelected(selected, animated: animated) 36 | 37 | // Configure the view for the selected state 38 | } 39 | 40 | func updateCell() { 41 | let photoGridWidth = UIScreen.main.bounds.width - 16 * 2 42 | photoGrid.frame = CGRect(x: 0, y: 0, width: photoGridWidth, height: photoGridWidth) 43 | photoGrid.photoUrl = photoUrl 44 | photoGridHeightConstraint.constant = photoGrid.frame.height 45 | for view in photoGridView.subviews { 46 | view.removeFromSuperview() 47 | } 48 | photoGridView.addSubview(photoGrid) 49 | } 50 | 51 | } 52 | 53 | extension ImageCell: PhotoGridDelegate { 54 | func didSelect(index: Int, imageView: UIImageView) { 55 | self.presentFMImageView?(index, imageView, self.photoUrl) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | APPL 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/PhotoGrid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoGridView.swift 3 | // funmee-app 4 | // 5 | // Created by Duc Nguyen Viet on 7/5/18. 6 | // Copyright © 2018 Tribal Media Houes.inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FMImageView 11 | 12 | // ratio of height and width in case >= 5 photos 13 | private let kRectPhotoGridViewHeightRatio: CGFloat = 99/119 14 | 15 | // ratio of big photo and small photo in case has 4 photos 16 | private let kImageSizeRatio: CGFloat = 173/304 17 | 18 | // padding between photos 19 | private let kPhotoPadding: CGFloat = 3 20 | 21 | class PhotoGridView: UIView { 22 | 23 | let imageView0 = UIImageView() 24 | let imageView1 = UIImageView() 25 | let imageView2 = UIImageView() 26 | let imageView3 = UIImageView() 27 | let imageView4 = UIImageView() 28 | 29 | // more photo view 30 | var remainingPhotoLabel = UILabel() 31 | var morePhotoView = UIView() 32 | 33 | var photoUrl: [URL] = [] { 34 | didSet { 35 | clearConstraints() 36 | loadPhoto() 37 | setFrameHeight(frame: &self.frame) 38 | setMorePhotoViewAppearance() 39 | setupConstraint() 40 | } 41 | } 42 | 43 | weak var delegate: PhotoGridDelegate? 44 | 45 | private var allConstraints: [NSLayoutConstraint] = [] 46 | private var horizontalConstraints: [NSLayoutConstraint] = [] 47 | private var verticalConstraints: [NSLayoutConstraint] = [] 48 | 49 | override init(frame: CGRect) { 50 | super.init(frame: frame) 51 | setupImageViewOption(imageView0) 52 | setupImageViewOption(imageView1) 53 | setupImageViewOption(imageView2) 54 | setupImageViewOption(imageView3) 55 | setupImageViewOption(imageView4) 56 | 57 | addImageView(imageView0) 58 | addImageView(imageView1) 59 | addImageView(imageView2) 60 | addImageView(imageView3) 61 | addImageView(imageView4) 62 | 63 | addImageViewTapGesture() 64 | 65 | self.addSubview(morePhotoView) 66 | setupMorePhotoView() 67 | addMorePhotoViewTapGesture() 68 | } 69 | 70 | required init?(coder aDecoder: NSCoder) { 71 | super.init(coder: aDecoder) 72 | } 73 | 74 | // MARK: - Public functions 75 | 76 | public func getImageView(index: Int) -> UIImageView { 77 | switch index { 78 | case 0: 79 | return self.imageView0 80 | 81 | case 1: 82 | return self.imageView1 83 | 84 | case 2: 85 | return self.imageView2 86 | 87 | case 3: 88 | return self.imageView3 89 | 90 | default: 91 | return self.imageView4 92 | } 93 | } 94 | 95 | private func addImageView(_ imgView: UIImageView) { 96 | self.addSubview(imgView) 97 | } 98 | 99 | // MARK: Setup Tap Gesture 100 | private func addImageViewTapGesture() { 101 | addImageView0TapGesture() 102 | addImageView1TapGesture() 103 | addImageView2TapGesture() 104 | addImageView3TapGesture() 105 | addImageView4TapGesture() 106 | } 107 | 108 | // image0 109 | private func addImageView0TapGesture() { 110 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(image0TapGesture)) 111 | imageView0.addGestureRecognizer(tapGesture) 112 | } 113 | 114 | @objc private func image0TapGesture(_ gesture: UITapGestureRecognizer) { 115 | delegate?.didSelect(index: 0, imageView: imageView0) 116 | } 117 | 118 | // image1 119 | private func addImageView1TapGesture() { 120 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(image1TapGesture)) 121 | imageView1.addGestureRecognizer(tapGesture) 122 | } 123 | 124 | @objc private func image1TapGesture(_ gesture: UITapGestureRecognizer) { 125 | delegate?.didSelect(index: 1, imageView: imageView1) 126 | } 127 | 128 | // image2 129 | private func addImageView2TapGesture() { 130 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(image2TapGesture)) 131 | imageView2.addGestureRecognizer(tapGesture) 132 | } 133 | 134 | @objc private func image2TapGesture(_ gesture: UITapGestureRecognizer) { 135 | delegate?.didSelect(index: 2, imageView: imageView2) 136 | } 137 | 138 | // image3 139 | private func addImageView3TapGesture() { 140 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(image3TapGesture)) 141 | imageView3.addGestureRecognizer(tapGesture) 142 | } 143 | 144 | @objc private func image3TapGesture(_ gesture: UITapGestureRecognizer) { 145 | delegate?.didSelect(index: 3, imageView: imageView3) 146 | } 147 | 148 | // image4 149 | private func addImageView4TapGesture() { 150 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(image4TapGesture)) 151 | imageView4.addGestureRecognizer(tapGesture) 152 | } 153 | 154 | @objc private func image4TapGesture(_ gesture: UITapGestureRecognizer) { 155 | delegate?.didSelect(index: 4, imageView: imageView4) 156 | } 157 | 158 | // more photo's view 159 | private func addMorePhotoViewTapGesture() { 160 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(image4TapGesture)) 161 | morePhotoView.addGestureRecognizer(tapGesture) 162 | } 163 | 164 | private func clearConstraints() { 165 | self.removeConstraints(allConstraints) 166 | } 167 | 168 | private func collectConstraints(_ constraint: [NSLayoutConstraint]) { 169 | self.addConstraints(constraint) 170 | allConstraints.append(contentsOf: constraint) 171 | } 172 | 173 | private func setupMorePhotoView() { 174 | remainingPhotoLabel.translatesAutoresizingMaskIntoConstraints = false 175 | remainingPhotoLabel.font = UIFont.systemFont(ofSize: 25) 176 | remainingPhotoLabel.textColor = UIColor.white 177 | 178 | morePhotoView.translatesAutoresizingMaskIntoConstraints = false 179 | morePhotoView.backgroundColor = UIColor.black.withAlphaComponent(0.4) 180 | morePhotoView.addSubview(remainingPhotoLabel) 181 | 182 | let horizontalConstraint = NSLayoutConstraint(item: remainingPhotoLabel, attribute: .centerX, relatedBy: .equal, toItem: morePhotoView, attribute: .centerX, multiplier: 1, constant: 0) 183 | let verticalConstraint = NSLayoutConstraint(item: remainingPhotoLabel, attribute: .centerY, relatedBy: .equal, toItem: morePhotoView, attribute: .centerY, multiplier: 1, constant: 0) 184 | morePhotoView.addConstraint(horizontalConstraint) 185 | morePhotoView.addConstraint(verticalConstraint) 186 | 187 | morePhotoView.isUserInteractionEnabled = true 188 | } 189 | 190 | // MARK: Load photos by url into PhotoGridView 191 | private func loadPhoto() { 192 | for (index, photoUrl) in photoUrl.enumerated() { 193 | if index > 4 { 194 | return 195 | } 196 | 197 | // update new photos 198 | // (self.subviews[index] as? UIImageView)?.sd_setImage(with: photoUrl, completed: nil) 199 | ImageLoader.sharedLoader.imageForUrl(url: photoUrl) { (image, urlString, networkErr) in 200 | (self.subviews[index] as? UIImageView)?.image = image 201 | } 202 | } 203 | // clear unnecessary photos 204 | if self.photoUrl.count < 5 { 205 | for i in stride(from: 5, to: self.photoUrl.count, by: -1) { 206 | (self.subviews[i-1] as? UIImageView)?.image = nil 207 | } 208 | } 209 | } 210 | 211 | private func setMorePhotoViewAppearance() { 212 | if photoUrl.count > 5 { 213 | morePhotoView.isHidden = false 214 | } else { 215 | morePhotoView.isHidden = true 216 | } 217 | } 218 | 219 | private func setFrameHeight(frame: inout CGRect) { 220 | var photoViewHeight: CGFloat = self.frame.width 221 | switch photoUrl.count { 222 | case 1, 3, 4: 223 | break 224 | case 2: 225 | photoViewHeight = (self.frame.width - kPhotoPadding) / 2 226 | default: 227 | let firstRowPhotoHeight = (self.frame.width - kPhotoPadding) / 2 228 | let secondRowPhotoHeight = (self.frame.width - kPhotoPadding * 2) / 3 229 | photoViewHeight = firstRowPhotoHeight + secondRowPhotoHeight + kPhotoPadding 230 | 231 | } 232 | frame = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: self.frame.width, height: photoViewHeight) 233 | } 234 | 235 | // MARK: Start setup constraint 236 | private func setupConstraint() { 237 | 238 | for index in 0.. 4 { 240 | break 241 | } 242 | // photo size for each photo 243 | let currentPhotoSize = photoSize(photoPosition: index) 244 | let metric = ["w": currentPhotoSize.width, "h": currentPhotoSize.height, "padding": kPhotoPadding] 245 | 246 | // setup constraint for each photo 247 | switch self.photoUrl.count { 248 | case 1: 249 | constraintForViewHasOnePhoto(metric) 250 | case 2: 251 | constraintForViewHasTwoPhoto(index, metric) 252 | case 3: 253 | constraintForViewHasThreePhoto(index, metric) 254 | case 4: 255 | constraintForViewHasFourPhoto(index, metric) 256 | default: 257 | constraintForViewHasFivePhoto(index, metric) 258 | } 259 | } 260 | } 261 | 262 | // MARK: Constraint photos 263 | private func constraintForViewHasOnePhoto(_ metric: [String: CGFloat]) { 264 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|[img(w)]", options: [], metrics: metric, views: ["img": imageView0]) 265 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[img(h)]", options: [], metrics: metric, views: ["img": imageView0]) 266 | collectConstraints(horizontalConstraints) 267 | collectConstraints(verticalConstraints) 268 | } 269 | 270 | private func constraintForViewHasTwoPhoto(_ index: Int, _ metric: [String: CGFloat]) { 271 | switch index { 272 | case 0: 273 | // left photo 274 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|[imgLeft(w)]", options: [], metrics: metric, views: ["imgLeft": imageView0]) 275 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[imgLeft(h)]", options: [], metrics: metric, views: ["imgLeft": imageView0]) 276 | case 1: 277 | // right photo 278 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:[img0]-padding-[imgRight(w)]", options: [], metrics: metric, views: ["img0": imageView0, "imgRight": imageView1]) 279 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[imgRight(h)]", options: [], metrics: metric, views: ["imgRight": imageView1]) 280 | default: 281 | break 282 | } 283 | collectConstraints(horizontalConstraints) 284 | collectConstraints(verticalConstraints) 285 | } 286 | 287 | private func constraintForViewHasThreePhoto(_ index: Int, _ metric: [String: CGFloat]) { 288 | switch index { 289 | case 0: 290 | // top photo 291 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|[imgTop(w)]", options: [], metrics: metric, views: ["imgTop": imageView0]) 292 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[imgTop(h)]", options: [], metrics: metric, views: ["imgTop": imageView0]) 293 | case 1: 294 | // bottom left photo 295 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|[imgBotLeft(w)]", options: [], metrics: metric, views: ["imgBotLeft": imageView1]) 296 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:[imgTop]-padding-[imgBotLeft(h)]", options: [], metrics: metric, views: ["imgTop": imageView0, "imgBotLeft": imageView1]) 297 | case 2: 298 | // bottom right photo 299 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:[imgBotLeft]-padding-[imgBotRight(w)]", options: [], metrics: metric, views: ["imgBotLeft": imageView1, "imgBotRight": imageView2]) 300 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:[imgTop]-padding-[imgBotRight(h)]", options: [], metrics: metric, views: ["imgTop": imageView0, "imgBotRight": imageView2]) 301 | default: 302 | break 303 | } 304 | collectConstraints(horizontalConstraints) 305 | collectConstraints(verticalConstraints) 306 | } 307 | 308 | private func constraintForViewHasFourPhoto(_ index: Int, _ metric: [String: CGFloat]) { 309 | switch index { 310 | case 0: 311 | // top left photo 312 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|[imgTopLeft(w)]", options: [], metrics: metric, views: ["imgTopLeft": imageView0]) 313 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[imgTopLeft(h)]", options: [], metrics: metric, views: ["imgTopLeft": imageView0]) 314 | case 1: 315 | // top right photo 316 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:[imgTopLeft]-padding-[imgTopRight(w)]", options: [], metrics: metric, views: ["imgTopLeft": imageView0, "imgTopRight": imageView1]) 317 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[imgTopRight(h)]", options: [], metrics: metric, views: ["imgTopRight": imageView1]) 318 | case 2: 319 | // bottom left photo 320 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|[imgBotLeft(w)]", options: [], metrics: metric, views: ["imgBotLeft": imageView2]) 321 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:[imgTopLeft]-padding-[imgBotLeft(h)]", options: [], metrics: metric, views: ["imgTopLeft": imageView0, "imgBotLeft": imageView2]) 322 | case 3: 323 | // bottom right photo 324 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:[imgBotLeft]-padding-[imgBotRight(w)]", options: [], metrics: metric, views: ["imgBotLeft": imageView2, "imgBotRight": imageView3]) 325 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:[imgTopRight]-padding-[imgBotRight(h)]", options: [], metrics: metric, views: ["imgTopRight": self.subviews[1], "imgBotRight": imageView3]) 326 | default: 327 | break 328 | } 329 | collectConstraints(horizontalConstraints) 330 | collectConstraints(verticalConstraints) 331 | } 332 | 333 | private func constraintForViewHasFivePhoto(_ index: Int, _ metric: [String: CGFloat]) { 334 | switch index { 335 | case 0: 336 | // top left photo 337 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|[imgTopLeft(w)]", options: [], metrics: metric, views: ["imgTopLeft": imageView0]) 338 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[imgTopLeft(h)]", options: [], metrics: metric, views: ["imgTopLeft": imageView0]) 339 | case 1: 340 | // top right photo 341 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:[imgTopLeft]-padding-[imgTopRight(w)]", options: [], metrics: metric, views: ["imgTopLeft": imageView0, "imgTopRight": imageView1]) 342 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[imgTopRight(h)]", options: [], metrics: metric, views: ["imgTopRight": imageView1]) 343 | case 2: 344 | // bottom left photo 345 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|[imgBotLeft(w)]", options: [], metrics: metric, views: ["imgBotLeft": imageView2]) 346 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:[imgTopLeft]-padding-[imgBotLeft(h)]", options: [], metrics: metric, views: ["imgTopLeft": imageView0, "imgBotLeft": imageView2]) 347 | case 3: 348 | // bottom middle photo 349 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:[imgBotLeft]-padding-[imgBotMid(w)]", options: [], metrics: metric, views: ["imgBotLeft": imageView2, "imgBotMid": imageView3]) 350 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:[imgTopLeft]-padding-[imgBotMid(h)]", options: [], metrics: metric, views: ["imgTopLeft": imageView0, "imgBotMid": imageView3]) 351 | case 4: 352 | // bottom right photo 353 | horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:[imgBotMid]-padding-[imgBotRight(w)]", options: [], metrics: metric, views: ["imgBotMid": imageView3, "imgBotRight": imageView4]) 354 | verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:[imgTopRight]-padding-[imgBotRight(h)]", options: [], metrics: metric, views: ["imgTopRight": imageView1, "imgBotRight": imageView4]) 355 | 356 | if self.photoUrl.count > 5 { 357 | // add "morePhotoRemaining" layer into last photo's view 358 | constraintForMorePhotoView(metric) 359 | } 360 | default: 361 | break 362 | } 363 | collectConstraints(horizontalConstraints) 364 | collectConstraints(verticalConstraints) 365 | } 366 | 367 | private func constraintForMorePhotoView(_ metric: [String: CGFloat]) { 368 | let morePhotoView = self.morePhotoView(width: metric["w"]!, height: metric["h"]!, numberOfRemainingPhoto: self.photoUrl.count - 5) 369 | 370 | // set constraint for this view 371 | let horizontalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "H:[imgBotMid]-padding-[remainPhotoView(w)]", options: [], metrics: metric, views: ["imgBotMid": imageView3, "remainPhotoView": morePhotoView]) 372 | let verticalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "V:[imgTopRight]-padding-[remainPhotoView(h)]", options: [], metrics: metric, views: ["imgTopRight": imageView1, "remainPhotoView": morePhotoView]) 373 | collectConstraints(horizontalConstraint) 374 | collectConstraints(verticalConstraint) 375 | } 376 | 377 | // MARK: ImageView Options 378 | private func setupImageViewOption(_ imgView: UIImageView) { 379 | imgView.isUserInteractionEnabled = true 380 | imgView.translatesAutoresizingMaskIntoConstraints = false 381 | imgView.contentMode = .scaleAspectFill 382 | imgView.clipsToBounds = true 383 | } 384 | 385 | // MARK: Calculate photo size 386 | private func photoSize(photoPosition: Int) -> CGSize { 387 | let contentWidth = self.frame.width 388 | switch self.photoUrl.count { 389 | // 1 photo 390 | case 1: 391 | return CGSize(width: contentWidth, height: contentWidth) 392 | // 2 photos 393 | case 2: 394 | return CGSize(width: (contentWidth - kPhotoPadding) / 2, height: (contentWidth - kPhotoPadding) / 2) 395 | // 3 photos 396 | case 3: 397 | return getPhotoSizeByPositionForThreePhotos(contentWidth, photoPosition) 398 | // 4 photos 399 | case 4: 400 | return getPhotoSizeByPositionForFourPhotos(contentWidth, photoPosition) 401 | // more than or equal 5 photos 402 | default: 403 | return getPhotoSizeByPositionForFivePhotos(contentWidth, photoPosition) 404 | } 405 | } 406 | 407 | private func getPhotoSizeByPositionForThreePhotos(_ width: CGFloat, _ position: Int) -> CGSize { 408 | switch position { 409 | case 0: 410 | return CGSize(width: width, height: (width - kPhotoPadding) / 2) 411 | default: 412 | return CGSize(width: (width - kPhotoPadding) / 2, height: (width - kPhotoPadding) / 2) 413 | } 414 | } 415 | 416 | private func getPhotoSizeByPositionForFourPhotos(_ width: CGFloat, _ position: Int) -> CGSize { 417 | switch position { 418 | case 0, 3: 419 | let topLeftPhotoWidth = (width - kPhotoPadding) * kImageSizeRatio 420 | return CGSize(width: topLeftPhotoWidth, height: (width - kPhotoPadding) / 2) 421 | default: 422 | let topLeftPhotoWidth = (width - kPhotoPadding) * kImageSizeRatio 423 | return CGSize(width: width - topLeftPhotoWidth - kPhotoPadding, height: (width - kPhotoPadding) / 2) 424 | } 425 | } 426 | 427 | private func getPhotoSizeByPositionForFivePhotos(_ width: CGFloat, _ position: Int) -> CGSize { 428 | switch position { 429 | case 0, 1: 430 | return CGSize(width: (width - kPhotoPadding) / 2, height: (width - kPhotoPadding) / 2) 431 | default: 432 | let contentHeight = width * kRectPhotoGridViewHeightRatio 433 | let bottomPhotoHeight = contentHeight - (width - kPhotoPadding) / 2 - kPhotoPadding 434 | return CGSize(width: bottomPhotoHeight, height: bottomPhotoHeight) 435 | } 436 | } 437 | 438 | // MARK: MorePhotoView 439 | private func morePhotoView(width: CGFloat, height: CGFloat, numberOfRemainingPhoto: Int) -> UIView { 440 | // number of remaining photo 441 | remainingPhotoLabel.text = "+\(String.init(describing: numberOfRemainingPhoto))" 442 | 443 | // uiview that contain number of remaining photo's label 444 | let frame = CGRect(x: 0, y: 0, width: width, height: height) 445 | morePhotoView.frame = frame 446 | 447 | return morePhotoView 448 | } 449 | } 450 | 451 | protocol PhotoGridDelegate: class { 452 | func didSelect(index: Int, imageView: UIImageView) 453 | } 454 | 455 | -------------------------------------------------------------------------------- /Example/Example/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController.swift 3 | // Example 4 | // 5 | // Created by Trần Quang Minh on 7/5/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FMImageView 11 | 12 | class TableViewController: UITableViewController { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | self.tableView.rowHeight = UITableViewAutomaticDimension 18 | self.tableView.estimatedRowHeight = 200 19 | } 20 | 21 | override func didReceiveMemoryWarning() { 22 | super.didReceiveMemoryWarning() 23 | } 24 | 25 | // MARK: - Table view data source 26 | 27 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 28 | // #warning Incomplete implementation, return the number of rows 29 | return 5 30 | } 31 | 32 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 33 | let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as! ImageCell 34 | 35 | cell.presentFMImageView = { [unowned self] (_ index: Int, _ imageView: UIImageView, _ photoURLs: [URL]) -> Void in 36 | 37 | let fmImageVC = self.setupPresenter(fromImage: imageView, initIndex: index, photoURLs: photoURLs) 38 | 39 | // update new destination frame when swipe page 40 | fmImageVC.didMoveToViewControllerHandler = { index in 41 | let imageViewAtIndex = cell.photoGrid.getImageView(index: index) 42 | fmImageVC.setNewDestinatonFrame(imageView: imageViewAtIndex) 43 | } 44 | 45 | self.present(fmImageVC, animated: true, completion: nil) 46 | } 47 | cell.updateCell() 48 | 49 | return cell 50 | } 51 | 52 | // set up presenter 53 | private func setupPresenter(fromImage: UIImageView, initIndex: Int, photoURLs: [URL]) -> FMImageSlideViewController { 54 | var config = Config(initImageView: fromImage, initIndex: initIndex) 55 | config.bottomView = HorizontalStackView(view: FMImageViewBottomView()) 56 | 57 | let vc = FMImageSlideViewController(datasource: FMImageDataSource(imageURLs: photoURLs), config: config) 58 | 59 | vc.view.frame = self.view.frame 60 | 61 | return vc 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Example/Example/funmee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/Example/Example/funmee.png -------------------------------------------------------------------------------- /Example/ExampleTests/ExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleTests.swift 3 | // ExampleTests 4 | // 5 | // Created by Hoang Trong Anh on 5/23/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Example 11 | 12 | class ExampleTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Example/ExampleTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /FMImageView/FMImageView.framework/FMImageView: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView.framework/FMImageView -------------------------------------------------------------------------------- /FMImageView/FMImageView.framework/Headers/FMImageView-Swift.h: -------------------------------------------------------------------------------- 1 | // Generated by Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1) 2 | #pragma clang diagnostic push 3 | #pragma clang diagnostic ignored "-Wgcc-compat" 4 | 5 | #if !defined(__has_include) 6 | # define __has_include(x) 0 7 | #endif 8 | #if !defined(__has_attribute) 9 | # define __has_attribute(x) 0 10 | #endif 11 | #if !defined(__has_feature) 12 | # define __has_feature(x) 0 13 | #endif 14 | #if !defined(__has_warning) 15 | # define __has_warning(x) 0 16 | #endif 17 | 18 | #if __has_include() 19 | # include 20 | #endif 21 | 22 | #pragma clang diagnostic ignored "-Wauto-import" 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #if !defined(SWIFT_TYPEDEFS) 29 | # define SWIFT_TYPEDEFS 1 30 | # if __has_include() 31 | # include 32 | # elif !defined(__cplusplus) 33 | typedef uint_least16_t char16_t; 34 | typedef uint_least32_t char32_t; 35 | # endif 36 | typedef float swift_float2 __attribute__((__ext_vector_type__(2))); 37 | typedef float swift_float3 __attribute__((__ext_vector_type__(3))); 38 | typedef float swift_float4 __attribute__((__ext_vector_type__(4))); 39 | typedef double swift_double2 __attribute__((__ext_vector_type__(2))); 40 | typedef double swift_double3 __attribute__((__ext_vector_type__(3))); 41 | typedef double swift_double4 __attribute__((__ext_vector_type__(4))); 42 | typedef int swift_int2 __attribute__((__ext_vector_type__(2))); 43 | typedef int swift_int3 __attribute__((__ext_vector_type__(3))); 44 | typedef int swift_int4 __attribute__((__ext_vector_type__(4))); 45 | typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); 46 | typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); 47 | typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); 48 | #endif 49 | 50 | #if !defined(SWIFT_PASTE) 51 | # define SWIFT_PASTE_HELPER(x, y) x##y 52 | # define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) 53 | #endif 54 | #if !defined(SWIFT_METATYPE) 55 | # define SWIFT_METATYPE(X) Class 56 | #endif 57 | #if !defined(SWIFT_CLASS_PROPERTY) 58 | # if __has_feature(objc_class_property) 59 | # define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ 60 | # else 61 | # define SWIFT_CLASS_PROPERTY(...) 62 | # endif 63 | #endif 64 | 65 | #if __has_attribute(objc_runtime_name) 66 | # define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) 67 | #else 68 | # define SWIFT_RUNTIME_NAME(X) 69 | #endif 70 | #if __has_attribute(swift_name) 71 | # define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) 72 | #else 73 | # define SWIFT_COMPILE_NAME(X) 74 | #endif 75 | #if __has_attribute(objc_method_family) 76 | # define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) 77 | #else 78 | # define SWIFT_METHOD_FAMILY(X) 79 | #endif 80 | #if __has_attribute(noescape) 81 | # define SWIFT_NOESCAPE __attribute__((noescape)) 82 | #else 83 | # define SWIFT_NOESCAPE 84 | #endif 85 | #if __has_attribute(warn_unused_result) 86 | # define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) 87 | #else 88 | # define SWIFT_WARN_UNUSED_RESULT 89 | #endif 90 | #if __has_attribute(noreturn) 91 | # define SWIFT_NORETURN __attribute__((noreturn)) 92 | #else 93 | # define SWIFT_NORETURN 94 | #endif 95 | #if !defined(SWIFT_CLASS_EXTRA) 96 | # define SWIFT_CLASS_EXTRA 97 | #endif 98 | #if !defined(SWIFT_PROTOCOL_EXTRA) 99 | # define SWIFT_PROTOCOL_EXTRA 100 | #endif 101 | #if !defined(SWIFT_ENUM_EXTRA) 102 | # define SWIFT_ENUM_EXTRA 103 | #endif 104 | #if !defined(SWIFT_CLASS) 105 | # if __has_attribute(objc_subclassing_restricted) 106 | # define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA 107 | # define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA 108 | # else 109 | # define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA 110 | # define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA 111 | # endif 112 | #endif 113 | 114 | #if !defined(SWIFT_PROTOCOL) 115 | # define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA 116 | # define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA 117 | #endif 118 | 119 | #if !defined(SWIFT_EXTENSION) 120 | # define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) 121 | #endif 122 | 123 | #if !defined(OBJC_DESIGNATED_INITIALIZER) 124 | # if __has_attribute(objc_designated_initializer) 125 | # define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) 126 | # else 127 | # define OBJC_DESIGNATED_INITIALIZER 128 | # endif 129 | #endif 130 | #if !defined(SWIFT_ENUM_ATTR) 131 | # if defined(__has_attribute) && __has_attribute(enum_extensibility) 132 | # define SWIFT_ENUM_ATTR __attribute__((enum_extensibility(open))) 133 | # else 134 | # define SWIFT_ENUM_ATTR 135 | # endif 136 | #endif 137 | #if !defined(SWIFT_ENUM) 138 | # define SWIFT_ENUM(_type, _name) enum _name : _type _name; enum SWIFT_ENUM_ATTR SWIFT_ENUM_EXTRA _name : _type 139 | # if __has_feature(generalized_swift_name) 140 | # define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR SWIFT_ENUM_EXTRA _name : _type 141 | # else 142 | # define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME) SWIFT_ENUM(_type, _name) 143 | # endif 144 | #endif 145 | #if !defined(SWIFT_UNAVAILABLE) 146 | # define SWIFT_UNAVAILABLE __attribute__((unavailable)) 147 | #endif 148 | #if !defined(SWIFT_UNAVAILABLE_MSG) 149 | # define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) 150 | #endif 151 | #if !defined(SWIFT_AVAILABILITY) 152 | # define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) 153 | #endif 154 | #if !defined(SWIFT_DEPRECATED) 155 | # define SWIFT_DEPRECATED __attribute__((deprecated)) 156 | #endif 157 | #if !defined(SWIFT_DEPRECATED_MSG) 158 | # define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) 159 | #endif 160 | #if __has_feature(attribute_diagnose_if_objc) 161 | # define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) 162 | #else 163 | # define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) 164 | #endif 165 | #if __has_feature(modules) 166 | @import UIKit; 167 | #endif 168 | 169 | #pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" 170 | #pragma clang diagnostic ignored "-Wduplicate-method-arg" 171 | #if __has_warning("-Wpragma-clang-attribute") 172 | # pragma clang diagnostic ignored "-Wpragma-clang-attribute" 173 | #endif 174 | #pragma clang diagnostic ignored "-Wunknown-pragmas" 175 | #pragma clang diagnostic ignored "-Wnullability" 176 | 177 | #if __has_attribute(external_source_symbol) 178 | # pragma push_macro("any") 179 | # undef any 180 | # pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="FMImageView",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) 181 | # pragma pop_macro("any") 182 | #endif 183 | 184 | @class NSBundle; 185 | @class NSCoder; 186 | 187 | SWIFT_CLASS("_TtC11FMImageView28PresentFMImageViewController") 188 | @interface PresentFMImageViewController : UIViewController 189 | - (void)viewDidLoad; 190 | - (void)didReceiveMemoryWarning; 191 | - (nonnull instancetype)initWithNibName:(NSString * _Nullable)nibNameOrNil bundle:(NSBundle * _Nullable)nibBundleOrNil OBJC_DESIGNATED_INITIALIZER; 192 | - (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)aDecoder OBJC_DESIGNATED_INITIALIZER; 193 | @end 194 | 195 | #if __has_attribute(external_source_symbol) 196 | # pragma clang attribute pop 197 | #endif 198 | #pragma clang diagnostic pop 199 | -------------------------------------------------------------------------------- /FMImageView/FMImageView.framework/Headers/FMImageView.h: -------------------------------------------------------------------------------- 1 | // 2 | // FMImageView.h 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 5/23/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for FMImageView. 12 | FOUNDATION_EXPORT double FMImageViewVersionNumber; 13 | 14 | //! Project version string for FMImageView. 15 | FOUNDATION_EXPORT const unsigned char FMImageViewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /FMImageView/FMImageView.framework/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView.framework/Info.plist -------------------------------------------------------------------------------- /FMImageView/FMImageView.framework/Modules/FMImageView.swiftmodule/arm.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView.framework/Modules/FMImageView.swiftmodule/arm.swiftdoc -------------------------------------------------------------------------------- /FMImageView/FMImageView.framework/Modules/FMImageView.swiftmodule/arm.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView.framework/Modules/FMImageView.swiftmodule/arm.swiftmodule -------------------------------------------------------------------------------- /FMImageView/FMImageView.framework/Modules/FMImageView.swiftmodule/arm64.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView.framework/Modules/FMImageView.swiftmodule/arm64.swiftdoc -------------------------------------------------------------------------------- /FMImageView/FMImageView.framework/Modules/FMImageView.swiftmodule/arm64.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView.framework/Modules/FMImageView.swiftmodule/arm64.swiftmodule -------------------------------------------------------------------------------- /FMImageView/FMImageView.framework/Modules/FMImageView.swiftmodule/x86_64.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView.framework/Modules/FMImageView.swiftmodule/x86_64.swiftdoc -------------------------------------------------------------------------------- /FMImageView/FMImageView.framework/Modules/FMImageView.swiftmodule/x86_64.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView.framework/Modules/FMImageView.swiftmodule/x86_64.swiftmodule -------------------------------------------------------------------------------- /FMImageView/FMImageView.framework/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module FMImageView { 2 | umbrella header "FMImageView.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | 8 | module FMImageView.Swift { 9 | header "FMImageView-Swift.h" 10 | requires objc 11 | } 12 | -------------------------------------------------------------------------------- /FMImageView/FMImageView.framework/PresentFMImageViewController.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView.framework/PresentFMImageViewController.nib -------------------------------------------------------------------------------- /FMImageView/FMImageView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | 0969539720B51F5B0057BFEE /* Example */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = 0969539820B51F5B0057BFEE /* Build configuration list for PBXAggregateTarget "Example" */; 13 | buildPhases = ( 14 | 0969539B20B51F730057BFEE /* ShellScript */, 15 | ); 16 | dependencies = ( 17 | ); 18 | name = Example; 19 | productName = Example; 20 | }; 21 | /* End PBXAggregateTarget section */ 22 | 23 | /* Begin PBXBuildFile section */ 24 | 090BFED320E36DBF00819FBF /* ImageZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090BFED220E36DBF00819FBF /* ImageZoomView.swift */; }; 25 | 090BFED520E36E2C00819FBF /* FMImagePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090BFED420E36E2C00819FBF /* FMImagePreviewViewController.swift */; }; 26 | 090BFED720E36ED700819FBF /* FMImageSlideViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090BFED620E36ED700819FBF /* FMImageSlideViewController.swift */; }; 27 | 090BFEF420E3A30E00819FBF /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090BFEF320E3A30E00819FBF /* UIImage.swift */; }; 28 | 091922F120EA0E82009EC24B /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091922F020EA0E82009EC24B /* ImageLoader.swift */; }; 29 | 091922F320EA1217009EC24B /* FMLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091922F220EA1217009EC24B /* FMLoadingView.swift */; }; 30 | 091922F720EA340A009EC24B /* FMAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091922F620EA340A009EC24B /* FMAlert.swift */; }; 31 | 0957AD5020EB03A900616929 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0957AD4F20EB03A900616929 /* Assets.xcassets */; }; 32 | 0957AD5320EB148100616929 /* HorizontalStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0957AD5220EB148100616929 /* HorizontalStackView.swift */; }; 33 | 0957AD5520EB399C00616929 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0957AD5420EB399C00616929 /* Constants.swift */; }; 34 | 0969538520B51ECE0057BFEE /* FMImageView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0969537B20B51ECE0057BFEE /* FMImageView.framework */; }; 35 | 0969538A20B51ECE0057BFEE /* FMImageViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0969538920B51ECE0057BFEE /* FMImageViewTests.swift */; }; 36 | 0969538C20B51ECE0057BFEE /* FMImageView.h in Headers */ = {isa = PBXBuildFile; fileRef = 0969537E20B51ECE0057BFEE /* FMImageView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 37 | 096953E720B52C290057BFEE /* Image+URL+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = 096953E620B52C290057BFEE /* Image+URL+Size.swift */; }; 38 | 096953E920B5531C0057BFEE /* CGImage+Help.swift in Sources */ = {isa = PBXBuildFile; fileRef = 096953E820B5531C0057BFEE /* CGImage+Help.swift */; }; 39 | 09906A1B20E60BFC00C06D69 /* UIPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09906A1A20E60BFC00C06D69 /* UIPanGestureRecognizer.swift */; }; 40 | 09906A1D20E60C3400C06D69 /* Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09906A1C20E60C3400C06D69 /* Enums.swift */; }; 41 | 09919DBE20EE088500FF4874 /* FMDelegateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09919DBD20EE088500FF4874 /* FMDelegateProtocol.swift */; }; 42 | 09B0DCBC20E20F0100CB7DD3 /* CGSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B0DCBB20E20F0100CB7DD3 /* CGSize.swift */; }; 43 | 09B0DCBE20E20F1500CB7DD3 /* CGPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B0DCBD20E20F1500CB7DD3 /* CGPoint.swift */; }; 44 | 09BA00E221096D010095186C /* FMImageDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09BA00E121096D010095186C /* FMImageDataSource.swift */; }; 45 | 09BA00E521096D270095186C /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09BA00E421096D270095186C /* Config.swift */; }; 46 | 09BA00E721096D480095186C /* ConfigureZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09BA00E621096D480095186C /* ConfigureZoomView.swift */; }; 47 | 09FD300D20F5DA3E00DC132E /* FMPhotoInteractionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09FD300C20F5DA3E00DC132E /* FMPhotoInteractionAnimator.swift */; }; 48 | 09FD301020F5DA6600DC132E /* FMZoomInAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09FD300F20F5DA6600DC132E /* FMZoomInAnimationController.swift */; }; 49 | 09FD301220F5DA7A00DC132E /* FMZoomOutAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09FD301120F5DA7A00DC132E /* FMZoomOutAnimationController.swift */; }; 50 | AB813DC1210EE59000031206 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB813DC0210EE59000031206 /* UIView.swift */; }; 51 | /* End PBXBuildFile section */ 52 | 53 | /* Begin PBXContainerItemProxy section */ 54 | 0969538620B51ECE0057BFEE /* PBXContainerItemProxy */ = { 55 | isa = PBXContainerItemProxy; 56 | containerPortal = 0969537220B51ECE0057BFEE /* Project object */; 57 | proxyType = 1; 58 | remoteGlobalIDString = 0969537A20B51ECE0057BFEE; 59 | remoteInfo = FMImageView; 60 | }; 61 | /* End PBXContainerItemProxy section */ 62 | 63 | /* Begin PBXFileReference section */ 64 | 090BFED220E36DBF00819FBF /* ImageZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageZoomView.swift; sourceTree = ""; }; 65 | 090BFED420E36E2C00819FBF /* FMImagePreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FMImagePreviewViewController.swift; sourceTree = ""; }; 66 | 090BFED620E36ED700819FBF /* FMImageSlideViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FMImageSlideViewController.swift; sourceTree = ""; }; 67 | 090BFEF320E3A30E00819FBF /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; 68 | 091922F020EA0E82009EC24B /* ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = ""; }; 69 | 091922F220EA1217009EC24B /* FMLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FMLoadingView.swift; sourceTree = ""; }; 70 | 091922F620EA340A009EC24B /* FMAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FMAlert.swift; sourceTree = ""; }; 71 | 0957AD4F20EB03A900616929 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 72 | 0957AD5220EB148100616929 /* HorizontalStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalStackView.swift; sourceTree = ""; }; 73 | 0957AD5420EB399C00616929 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 74 | 0969537B20B51ECE0057BFEE /* FMImageView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FMImageView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 75 | 0969537E20B51ECE0057BFEE /* FMImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FMImageView.h; sourceTree = ""; }; 76 | 0969537F20B51ECE0057BFEE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 77 | 0969538420B51ECE0057BFEE /* FMImageViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FMImageViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 78 | 0969538920B51ECE0057BFEE /* FMImageViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FMImageViewTests.swift; sourceTree = ""; }; 79 | 0969538B20B51ECE0057BFEE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 80 | 096953E620B52C290057BFEE /* Image+URL+Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Image+URL+Size.swift"; sourceTree = ""; }; 81 | 096953E820B5531C0057BFEE /* CGImage+Help.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGImage+Help.swift"; sourceTree = ""; }; 82 | 09906A1A20E60BFC00C06D69 /* UIPanGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIPanGestureRecognizer.swift; sourceTree = ""; }; 83 | 09906A1C20E60C3400C06D69 /* Enums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Enums.swift; sourceTree = ""; }; 84 | 09919DBD20EE088500FF4874 /* FMDelegateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FMDelegateProtocol.swift; sourceTree = ""; }; 85 | 09B0DCBB20E20F0100CB7DD3 /* CGSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSize.swift; sourceTree = ""; }; 86 | 09B0DCBD20E20F1500CB7DD3 /* CGPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGPoint.swift; sourceTree = ""; }; 87 | 09BA00E121096D010095186C /* FMImageDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FMImageDataSource.swift; sourceTree = ""; }; 88 | 09BA00E421096D270095186C /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; 89 | 09BA00E621096D480095186C /* ConfigureZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigureZoomView.swift; sourceTree = ""; }; 90 | 09FD300C20F5DA3E00DC132E /* FMPhotoInteractionAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FMPhotoInteractionAnimator.swift; sourceTree = ""; }; 91 | 09FD300F20F5DA6600DC132E /* FMZoomInAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FMZoomInAnimationController.swift; sourceTree = ""; }; 92 | 09FD301120F5DA7A00DC132E /* FMZoomOutAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FMZoomOutAnimationController.swift; sourceTree = ""; }; 93 | AB813DC0210EE59000031206 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 94 | /* End PBXFileReference section */ 95 | 96 | /* Begin PBXFrameworksBuildPhase section */ 97 | 0969537720B51ECE0057BFEE /* Frameworks */ = { 98 | isa = PBXFrameworksBuildPhase; 99 | buildActionMask = 2147483647; 100 | files = ( 101 | ); 102 | runOnlyForDeploymentPostprocessing = 0; 103 | }; 104 | 0969538120B51ECE0057BFEE /* Frameworks */ = { 105 | isa = PBXFrameworksBuildPhase; 106 | buildActionMask = 2147483647; 107 | files = ( 108 | 0969538520B51ECE0057BFEE /* FMImageView.framework in Frameworks */, 109 | ); 110 | runOnlyForDeploymentPostprocessing = 0; 111 | }; 112 | /* End PBXFrameworksBuildPhase section */ 113 | 114 | /* Begin PBXGroup section */ 115 | 090BFED120E36D7D00819FBF /* Views */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 09BA00E321096D170095186C /* Config */, 119 | 09BA00E021096CE20095186C /* Datasource */, 120 | 0957AD5120EB144400616929 /* Subviews */, 121 | 090BFED220E36DBF00819FBF /* ImageZoomView.swift */, 122 | 090BFED420E36E2C00819FBF /* FMImagePreviewViewController.swift */, 123 | 090BFED620E36ED700819FBF /* FMImageSlideViewController.swift */, 124 | ); 125 | path = Views; 126 | sourceTree = ""; 127 | }; 128 | 091922F520EA33EB009EC24B /* Alert */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 091922F620EA340A009EC24B /* FMAlert.swift */, 132 | ); 133 | path = Alert; 134 | sourceTree = ""; 135 | }; 136 | 0957AD5120EB144400616929 /* Subviews */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 0957AD5220EB148100616929 /* HorizontalStackView.swift */, 140 | ); 141 | path = Subviews; 142 | sourceTree = ""; 143 | }; 144 | 0969537120B51ECE0057BFEE = { 145 | isa = PBXGroup; 146 | children = ( 147 | 0969537D20B51ECE0057BFEE /* FMImageView */, 148 | 0969538820B51ECE0057BFEE /* FMImageViewTests */, 149 | 0969537C20B51ECE0057BFEE /* Products */, 150 | ); 151 | sourceTree = ""; 152 | }; 153 | 0969537C20B51ECE0057BFEE /* Products */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | 0969537B20B51ECE0057BFEE /* FMImageView.framework */, 157 | 0969538420B51ECE0057BFEE /* FMImageViewTests.xctest */, 158 | ); 159 | name = Products; 160 | sourceTree = ""; 161 | }; 162 | 0969537D20B51ECE0057BFEE /* FMImageView */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 09919DBC20EE084100FF4874 /* Protocols */, 166 | AB7E5E9520ECB15800FF09EB /* Animator */, 167 | 090BFED120E36D7D00819FBF /* Views */, 168 | 09F663C520DCAB7400AB2B8A /* Extensions */, 169 | 096953E520B52BE00057BFEE /* Help */, 170 | 096953E420B5287D0057BFEE /* Indicator */, 171 | 091922F520EA33EB009EC24B /* Alert */, 172 | 096953E320B528760057BFEE /* Network */, 173 | 0969537E20B51ECE0057BFEE /* FMImageView.h */, 174 | 0969537F20B51ECE0057BFEE /* Info.plist */, 175 | 0957AD4F20EB03A900616929 /* Assets.xcassets */, 176 | ); 177 | path = FMImageView; 178 | sourceTree = ""; 179 | }; 180 | 0969538820B51ECE0057BFEE /* FMImageViewTests */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | 0969538920B51ECE0057BFEE /* FMImageViewTests.swift */, 184 | 0969538B20B51ECE0057BFEE /* Info.plist */, 185 | ); 186 | path = FMImageViewTests; 187 | sourceTree = ""; 188 | }; 189 | 096953E320B528760057BFEE /* Network */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | 091922F020EA0E82009EC24B /* ImageLoader.swift */, 193 | ); 194 | path = Network; 195 | sourceTree = ""; 196 | }; 197 | 096953E420B5287D0057BFEE /* Indicator */ = { 198 | isa = PBXGroup; 199 | children = ( 200 | 091922F220EA1217009EC24B /* FMLoadingView.swift */, 201 | ); 202 | path = Indicator; 203 | sourceTree = ""; 204 | }; 205 | 096953E520B52BE00057BFEE /* Help */ = { 206 | isa = PBXGroup; 207 | children = ( 208 | 096953E620B52C290057BFEE /* Image+URL+Size.swift */, 209 | 096953E820B5531C0057BFEE /* CGImage+Help.swift */, 210 | 09906A1C20E60C3400C06D69 /* Enums.swift */, 211 | 0957AD5420EB399C00616929 /* Constants.swift */, 212 | ); 213 | path = Help; 214 | sourceTree = ""; 215 | }; 216 | 09919DBC20EE084100FF4874 /* Protocols */ = { 217 | isa = PBXGroup; 218 | children = ( 219 | 09919DBD20EE088500FF4874 /* FMDelegateProtocol.swift */, 220 | ); 221 | path = Protocols; 222 | sourceTree = ""; 223 | }; 224 | 09BA00E021096CE20095186C /* Datasource */ = { 225 | isa = PBXGroup; 226 | children = ( 227 | 09BA00E121096D010095186C /* FMImageDataSource.swift */, 228 | ); 229 | path = Datasource; 230 | sourceTree = ""; 231 | }; 232 | 09BA00E321096D170095186C /* Config */ = { 233 | isa = PBXGroup; 234 | children = ( 235 | 09BA00E421096D270095186C /* Config.swift */, 236 | 09BA00E621096D480095186C /* ConfigureZoomView.swift */, 237 | ); 238 | path = Config; 239 | sourceTree = ""; 240 | }; 241 | 09F663C520DCAB7400AB2B8A /* Extensions */ = { 242 | isa = PBXGroup; 243 | children = ( 244 | 09B0DCBB20E20F0100CB7DD3 /* CGSize.swift */, 245 | 09B0DCBD20E20F1500CB7DD3 /* CGPoint.swift */, 246 | 090BFEF320E3A30E00819FBF /* UIImage.swift */, 247 | 09906A1A20E60BFC00C06D69 /* UIPanGestureRecognizer.swift */, 248 | AB813DC0210EE59000031206 /* UIView.swift */, 249 | ); 250 | path = Extensions; 251 | sourceTree = ""; 252 | }; 253 | AB7E5E9520ECB15800FF09EB /* Animator */ = { 254 | isa = PBXGroup; 255 | children = ( 256 | 09FD300C20F5DA3E00DC132E /* FMPhotoInteractionAnimator.swift */, 257 | 09FD300F20F5DA6600DC132E /* FMZoomInAnimationController.swift */, 258 | 09FD301120F5DA7A00DC132E /* FMZoomOutAnimationController.swift */, 259 | ); 260 | path = Animator; 261 | sourceTree = ""; 262 | }; 263 | /* End PBXGroup section */ 264 | 265 | /* Begin PBXHeadersBuildPhase section */ 266 | 0969537820B51ECE0057BFEE /* Headers */ = { 267 | isa = PBXHeadersBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | 0969538C20B51ECE0057BFEE /* FMImageView.h in Headers */, 271 | ); 272 | runOnlyForDeploymentPostprocessing = 0; 273 | }; 274 | /* End PBXHeadersBuildPhase section */ 275 | 276 | /* Begin PBXNativeTarget section */ 277 | 0969537A20B51ECE0057BFEE /* FMImageView */ = { 278 | isa = PBXNativeTarget; 279 | buildConfigurationList = 0969538F20B51ECE0057BFEE /* Build configuration list for PBXNativeTarget "FMImageView" */; 280 | buildPhases = ( 281 | 0969537620B51ECE0057BFEE /* Sources */, 282 | 0969537720B51ECE0057BFEE /* Frameworks */, 283 | 0969537820B51ECE0057BFEE /* Headers */, 284 | 0969537920B51ECE0057BFEE /* Resources */, 285 | ); 286 | buildRules = ( 287 | ); 288 | dependencies = ( 289 | ); 290 | name = FMImageView; 291 | productName = FMImageView; 292 | productReference = 0969537B20B51ECE0057BFEE /* FMImageView.framework */; 293 | productType = "com.apple.product-type.framework"; 294 | }; 295 | 0969538320B51ECE0057BFEE /* FMImageViewTests */ = { 296 | isa = PBXNativeTarget; 297 | buildConfigurationList = 0969539220B51ECE0057BFEE /* Build configuration list for PBXNativeTarget "FMImageViewTests" */; 298 | buildPhases = ( 299 | 0969538020B51ECE0057BFEE /* Sources */, 300 | 0969538120B51ECE0057BFEE /* Frameworks */, 301 | 0969538220B51ECE0057BFEE /* Resources */, 302 | ); 303 | buildRules = ( 304 | ); 305 | dependencies = ( 306 | 0969538720B51ECE0057BFEE /* PBXTargetDependency */, 307 | ); 308 | name = FMImageViewTests; 309 | productName = FMImageViewTests; 310 | productReference = 0969538420B51ECE0057BFEE /* FMImageViewTests.xctest */; 311 | productType = "com.apple.product-type.bundle.unit-test"; 312 | }; 313 | /* End PBXNativeTarget section */ 314 | 315 | /* Begin PBXProject section */ 316 | 0969537220B51ECE0057BFEE /* Project object */ = { 317 | isa = PBXProject; 318 | attributes = { 319 | LastSwiftUpdateCheck = 0930; 320 | LastUpgradeCheck = 0930; 321 | ORGANIZATIONNAME = "Hoang Trong Anh"; 322 | TargetAttributes = { 323 | 0969537A20B51ECE0057BFEE = { 324 | CreatedOnToolsVersion = 9.3.1; 325 | LastSwiftMigration = 0930; 326 | }; 327 | 0969538320B51ECE0057BFEE = { 328 | CreatedOnToolsVersion = 9.3.1; 329 | }; 330 | 0969539720B51F5B0057BFEE = { 331 | CreatedOnToolsVersion = 9.3.1; 332 | }; 333 | }; 334 | }; 335 | buildConfigurationList = 0969537520B51ECE0057BFEE /* Build configuration list for PBXProject "FMImageView" */; 336 | compatibilityVersion = "Xcode 9.3"; 337 | developmentRegion = en; 338 | hasScannedForEncodings = 0; 339 | knownRegions = ( 340 | en, 341 | ); 342 | mainGroup = 0969537120B51ECE0057BFEE; 343 | productRefGroup = 0969537C20B51ECE0057BFEE /* Products */; 344 | projectDirPath = ""; 345 | projectRoot = ""; 346 | targets = ( 347 | 0969537A20B51ECE0057BFEE /* FMImageView */, 348 | 0969538320B51ECE0057BFEE /* FMImageViewTests */, 349 | 0969539720B51F5B0057BFEE /* Example */, 350 | ); 351 | }; 352 | /* End PBXProject section */ 353 | 354 | /* Begin PBXResourcesBuildPhase section */ 355 | 0969537920B51ECE0057BFEE /* Resources */ = { 356 | isa = PBXResourcesBuildPhase; 357 | buildActionMask = 2147483647; 358 | files = ( 359 | 0957AD5020EB03A900616929 /* Assets.xcassets in Resources */, 360 | ); 361 | runOnlyForDeploymentPostprocessing = 0; 362 | }; 363 | 0969538220B51ECE0057BFEE /* Resources */ = { 364 | isa = PBXResourcesBuildPhase; 365 | buildActionMask = 2147483647; 366 | files = ( 367 | ); 368 | runOnlyForDeploymentPostprocessing = 0; 369 | }; 370 | /* End PBXResourcesBuildPhase section */ 371 | 372 | /* Begin PBXShellScriptBuildPhase section */ 373 | 0969539B20B51F730057BFEE /* ShellScript */ = { 374 | isa = PBXShellScriptBuildPhase; 375 | buildActionMask = 2147483647; 376 | files = ( 377 | ); 378 | inputPaths = ( 379 | ); 380 | outputPaths = ( 381 | ); 382 | runOnlyForDeploymentPostprocessing = 0; 383 | shellPath = /bin/sh; 384 | shellScript = "#!/bin/sh\n\nUNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-example\n\n# make sure the output directory exists\nmkdir -p \"${UNIVERSAL_OUTPUTFOLDER}\"\n\n# Step 1. Build Device and Simulator versions\nxcodebuild -target \"FMImageView\" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos BUILD_DIR=\"${BUILD_DIR}\" BUILD_ROOT=\"${BUILD_ROOT}\" clean build\nxcodebuild -target \"AlamoWater\" -configuration ${CONFIGURATION} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO BUILD_DIR=\"${BUILD_DIR}\" BUILD_ROOT=\"${BUILD_ROOT}\" clean build\n\n# Step 2. Copy the framework structure (from iphoneos build) to the universal folder\ncp -R \"${BUILD_DIR}/${CONFIGURATION}-iphoneos/FMImageView.framework\" \"${UNIVERSAL_OUTPUTFOLDER}/\"\n\n# Step 3. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory\nSIMULATOR_SWIFT_MODULES_DIR=\"${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/FMImageView.framework/Modules/FMImageView.swiftmodule/.\"\nif [ -d \"${SIMULATOR_SWIFT_MODULES_DIR}\" ]; then\ncp -R \"${SIMULATOR_SWIFT_MODULES_DIR}\" \"${UNIVERSAL_OUTPUTFOLDER}/FMImageView.framework/Modules/FMImageView.swiftmodule\"\nfi\n\n# Step 4. Create universal binary file using lipo and place the combined executable in the copied framework directory\nlipo -create -output \"${UNIVERSAL_OUTPUTFOLDER}/FMImageView.framework/FMImageView\" \"${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/FMImageView.framework/FMImageView\" \"${BUILD_DIR}/${CONFIGURATION}-iphoneos/FMImageView.framework/FMImageView\"\n\n# Step 5. Convenience step to copy the framework to the project's directory\ncp -R \"${UNIVERSAL_OUTPUTFOLDER}/FMImageView.framework\" \"${PROJECT_DIR}\"\n\n# Step 6. Convenience step to open the project's directory in Finder\nopen \"${PROJECT_DIR}\""; 385 | }; 386 | /* End PBXShellScriptBuildPhase section */ 387 | 388 | /* Begin PBXSourcesBuildPhase section */ 389 | 0969537620B51ECE0057BFEE /* Sources */ = { 390 | isa = PBXSourcesBuildPhase; 391 | buildActionMask = 2147483647; 392 | files = ( 393 | 09FD300D20F5DA3E00DC132E /* FMPhotoInteractionAnimator.swift in Sources */, 394 | 09BA00E221096D010095186C /* FMImageDataSource.swift in Sources */, 395 | 0957AD5520EB399C00616929 /* Constants.swift in Sources */, 396 | 091922F320EA1217009EC24B /* FMLoadingView.swift in Sources */, 397 | 096953E720B52C290057BFEE /* Image+URL+Size.swift in Sources */, 398 | 09906A1B20E60BFC00C06D69 /* UIPanGestureRecognizer.swift in Sources */, 399 | 09B0DCBC20E20F0100CB7DD3 /* CGSize.swift in Sources */, 400 | AB813DC1210EE59000031206 /* UIView.swift in Sources */, 401 | 090BFED520E36E2C00819FBF /* FMImagePreviewViewController.swift in Sources */, 402 | 09FD301020F5DA6600DC132E /* FMZoomInAnimationController.swift in Sources */, 403 | 09B0DCBE20E20F1500CB7DD3 /* CGPoint.swift in Sources */, 404 | 090BFED320E36DBF00819FBF /* ImageZoomView.swift in Sources */, 405 | 09FD301220F5DA7A00DC132E /* FMZoomOutAnimationController.swift in Sources */, 406 | 096953E920B5531C0057BFEE /* CGImage+Help.swift in Sources */, 407 | 090BFED720E36ED700819FBF /* FMImageSlideViewController.swift in Sources */, 408 | 09BA00E521096D270095186C /* Config.swift in Sources */, 409 | 090BFEF420E3A30E00819FBF /* UIImage.swift in Sources */, 410 | 09906A1D20E60C3400C06D69 /* Enums.swift in Sources */, 411 | 091922F120EA0E82009EC24B /* ImageLoader.swift in Sources */, 412 | 09BA00E721096D480095186C /* ConfigureZoomView.swift in Sources */, 413 | 091922F720EA340A009EC24B /* FMAlert.swift in Sources */, 414 | 0957AD5320EB148100616929 /* HorizontalStackView.swift in Sources */, 415 | 09919DBE20EE088500FF4874 /* FMDelegateProtocol.swift in Sources */, 416 | ); 417 | runOnlyForDeploymentPostprocessing = 0; 418 | }; 419 | 0969538020B51ECE0057BFEE /* Sources */ = { 420 | isa = PBXSourcesBuildPhase; 421 | buildActionMask = 2147483647; 422 | files = ( 423 | 0969538A20B51ECE0057BFEE /* FMImageViewTests.swift in Sources */, 424 | ); 425 | runOnlyForDeploymentPostprocessing = 0; 426 | }; 427 | /* End PBXSourcesBuildPhase section */ 428 | 429 | /* Begin PBXTargetDependency section */ 430 | 0969538720B51ECE0057BFEE /* PBXTargetDependency */ = { 431 | isa = PBXTargetDependency; 432 | target = 0969537A20B51ECE0057BFEE /* FMImageView */; 433 | targetProxy = 0969538620B51ECE0057BFEE /* PBXContainerItemProxy */; 434 | }; 435 | /* End PBXTargetDependency section */ 436 | 437 | /* Begin XCBuildConfiguration section */ 438 | 0969538D20B51ECE0057BFEE /* Debug */ = { 439 | isa = XCBuildConfiguration; 440 | buildSettings = { 441 | ALWAYS_SEARCH_USER_PATHS = NO; 442 | CLANG_ANALYZER_NONNULL = YES; 443 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 444 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 445 | CLANG_CXX_LIBRARY = "libc++"; 446 | CLANG_ENABLE_MODULES = YES; 447 | CLANG_ENABLE_OBJC_ARC = YES; 448 | CLANG_ENABLE_OBJC_WEAK = YES; 449 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 450 | CLANG_WARN_BOOL_CONVERSION = YES; 451 | CLANG_WARN_COMMA = YES; 452 | CLANG_WARN_CONSTANT_CONVERSION = YES; 453 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 454 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 455 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 456 | CLANG_WARN_EMPTY_BODY = YES; 457 | CLANG_WARN_ENUM_CONVERSION = YES; 458 | CLANG_WARN_INFINITE_RECURSION = YES; 459 | CLANG_WARN_INT_CONVERSION = YES; 460 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 461 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 462 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 463 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 464 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 465 | CLANG_WARN_STRICT_PROTOTYPES = YES; 466 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 467 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 468 | CLANG_WARN_UNREACHABLE_CODE = YES; 469 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 470 | CODE_SIGN_IDENTITY = "iPhone Developer"; 471 | COPY_PHASE_STRIP = NO; 472 | CURRENT_PROJECT_VERSION = 1; 473 | DEBUG_INFORMATION_FORMAT = dwarf; 474 | ENABLE_STRICT_OBJC_MSGSEND = YES; 475 | ENABLE_TESTABILITY = YES; 476 | GCC_C_LANGUAGE_STANDARD = gnu11; 477 | GCC_DYNAMIC_NO_PIC = NO; 478 | GCC_NO_COMMON_BLOCKS = YES; 479 | GCC_OPTIMIZATION_LEVEL = 0; 480 | GCC_PREPROCESSOR_DEFINITIONS = ( 481 | "DEBUG=1", 482 | "$(inherited)", 483 | ); 484 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 485 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 486 | GCC_WARN_UNDECLARED_SELECTOR = YES; 487 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 488 | GCC_WARN_UNUSED_FUNCTION = YES; 489 | GCC_WARN_UNUSED_VARIABLE = YES; 490 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 491 | MTL_ENABLE_DEBUG_INFO = YES; 492 | ONLY_ACTIVE_ARCH = YES; 493 | SDKROOT = iphoneos; 494 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 495 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 496 | VERSIONING_SYSTEM = "apple-generic"; 497 | VERSION_INFO_PREFIX = ""; 498 | }; 499 | name = Debug; 500 | }; 501 | 0969538E20B51ECE0057BFEE /* Release */ = { 502 | isa = XCBuildConfiguration; 503 | buildSettings = { 504 | ALWAYS_SEARCH_USER_PATHS = NO; 505 | CLANG_ANALYZER_NONNULL = YES; 506 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 507 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 508 | CLANG_CXX_LIBRARY = "libc++"; 509 | CLANG_ENABLE_MODULES = YES; 510 | CLANG_ENABLE_OBJC_ARC = YES; 511 | CLANG_ENABLE_OBJC_WEAK = YES; 512 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 513 | CLANG_WARN_BOOL_CONVERSION = YES; 514 | CLANG_WARN_COMMA = YES; 515 | CLANG_WARN_CONSTANT_CONVERSION = YES; 516 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 517 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 518 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 519 | CLANG_WARN_EMPTY_BODY = YES; 520 | CLANG_WARN_ENUM_CONVERSION = YES; 521 | CLANG_WARN_INFINITE_RECURSION = YES; 522 | CLANG_WARN_INT_CONVERSION = YES; 523 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 524 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 525 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 526 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 527 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 528 | CLANG_WARN_STRICT_PROTOTYPES = YES; 529 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 530 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 531 | CLANG_WARN_UNREACHABLE_CODE = YES; 532 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 533 | CODE_SIGN_IDENTITY = "iPhone Developer"; 534 | COPY_PHASE_STRIP = NO; 535 | CURRENT_PROJECT_VERSION = 1; 536 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 537 | ENABLE_NS_ASSERTIONS = NO; 538 | ENABLE_STRICT_OBJC_MSGSEND = YES; 539 | GCC_C_LANGUAGE_STANDARD = gnu11; 540 | GCC_NO_COMMON_BLOCKS = YES; 541 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 542 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 543 | GCC_WARN_UNDECLARED_SELECTOR = YES; 544 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 545 | GCC_WARN_UNUSED_FUNCTION = YES; 546 | GCC_WARN_UNUSED_VARIABLE = YES; 547 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 548 | MTL_ENABLE_DEBUG_INFO = NO; 549 | SDKROOT = iphoneos; 550 | SWIFT_COMPILATION_MODE = wholemodule; 551 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 552 | VALIDATE_PRODUCT = YES; 553 | VERSIONING_SYSTEM = "apple-generic"; 554 | VERSION_INFO_PREFIX = ""; 555 | }; 556 | name = Release; 557 | }; 558 | 0969539020B51ECE0057BFEE /* Debug */ = { 559 | isa = XCBuildConfiguration; 560 | buildSettings = { 561 | CLANG_ENABLE_MODULES = YES; 562 | CODE_SIGN_IDENTITY = ""; 563 | CODE_SIGN_STYLE = Automatic; 564 | DEFINES_MODULE = YES; 565 | DEVELOPMENT_TEAM = 254Q23AKWK; 566 | DYLIB_COMPATIBILITY_VERSION = 1; 567 | DYLIB_CURRENT_VERSION = 1; 568 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 569 | INFOPLIST_FILE = FMImageView/Info.plist; 570 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 571 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 572 | LD_RUNPATH_SEARCH_PATHS = ( 573 | "$(inherited)", 574 | "@executable_path/Frameworks", 575 | "@loader_path/Frameworks", 576 | ); 577 | PRODUCT_BUNDLE_IDENTIFIER = anhht.FMImageView; 578 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 579 | SKIP_INSTALL = YES; 580 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 581 | SWIFT_VERSION = 4.0; 582 | TARGETED_DEVICE_FAMILY = "1,2"; 583 | }; 584 | name = Debug; 585 | }; 586 | 0969539120B51ECE0057BFEE /* Release */ = { 587 | isa = XCBuildConfiguration; 588 | buildSettings = { 589 | CLANG_ENABLE_MODULES = YES; 590 | CODE_SIGN_IDENTITY = ""; 591 | CODE_SIGN_STYLE = Automatic; 592 | DEFINES_MODULE = YES; 593 | DEVELOPMENT_TEAM = 254Q23AKWK; 594 | DYLIB_COMPATIBILITY_VERSION = 1; 595 | DYLIB_CURRENT_VERSION = 1; 596 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 597 | INFOPLIST_FILE = FMImageView/Info.plist; 598 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 599 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 600 | LD_RUNPATH_SEARCH_PATHS = ( 601 | "$(inherited)", 602 | "@executable_path/Frameworks", 603 | "@loader_path/Frameworks", 604 | ); 605 | PRODUCT_BUNDLE_IDENTIFIER = anhht.FMImageView; 606 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 607 | SKIP_INSTALL = YES; 608 | SWIFT_VERSION = 4.0; 609 | TARGETED_DEVICE_FAMILY = "1,2"; 610 | }; 611 | name = Release; 612 | }; 613 | 0969539320B51ECE0057BFEE /* Debug */ = { 614 | isa = XCBuildConfiguration; 615 | buildSettings = { 616 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 617 | CODE_SIGN_STYLE = Automatic; 618 | DEVELOPMENT_TEAM = 254Q23AKWK; 619 | INFOPLIST_FILE = FMImageViewTests/Info.plist; 620 | LD_RUNPATH_SEARCH_PATHS = ( 621 | "$(inherited)", 622 | "@executable_path/Frameworks", 623 | "@loader_path/Frameworks", 624 | ); 625 | PRODUCT_BUNDLE_IDENTIFIER = anhht.FMImageViewTests; 626 | PRODUCT_NAME = "$(TARGET_NAME)"; 627 | SWIFT_VERSION = 4.0; 628 | TARGETED_DEVICE_FAMILY = "1,2"; 629 | }; 630 | name = Debug; 631 | }; 632 | 0969539420B51ECE0057BFEE /* Release */ = { 633 | isa = XCBuildConfiguration; 634 | buildSettings = { 635 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 636 | CODE_SIGN_STYLE = Automatic; 637 | DEVELOPMENT_TEAM = 254Q23AKWK; 638 | INFOPLIST_FILE = FMImageViewTests/Info.plist; 639 | LD_RUNPATH_SEARCH_PATHS = ( 640 | "$(inherited)", 641 | "@executable_path/Frameworks", 642 | "@loader_path/Frameworks", 643 | ); 644 | PRODUCT_BUNDLE_IDENTIFIER = anhht.FMImageViewTests; 645 | PRODUCT_NAME = "$(TARGET_NAME)"; 646 | SWIFT_VERSION = 4.0; 647 | TARGETED_DEVICE_FAMILY = "1,2"; 648 | }; 649 | name = Release; 650 | }; 651 | 0969539920B51F5B0057BFEE /* Debug */ = { 652 | isa = XCBuildConfiguration; 653 | buildSettings = { 654 | CODE_SIGN_STYLE = Automatic; 655 | DEVELOPMENT_TEAM = 254Q23AKWK; 656 | PRODUCT_NAME = "$(TARGET_NAME)"; 657 | }; 658 | name = Debug; 659 | }; 660 | 0969539A20B51F5B0057BFEE /* Release */ = { 661 | isa = XCBuildConfiguration; 662 | buildSettings = { 663 | CODE_SIGN_STYLE = Automatic; 664 | DEVELOPMENT_TEAM = 254Q23AKWK; 665 | PRODUCT_NAME = "$(TARGET_NAME)"; 666 | }; 667 | name = Release; 668 | }; 669 | /* End XCBuildConfiguration section */ 670 | 671 | /* Begin XCConfigurationList section */ 672 | 0969537520B51ECE0057BFEE /* Build configuration list for PBXProject "FMImageView" */ = { 673 | isa = XCConfigurationList; 674 | buildConfigurations = ( 675 | 0969538D20B51ECE0057BFEE /* Debug */, 676 | 0969538E20B51ECE0057BFEE /* Release */, 677 | ); 678 | defaultConfigurationIsVisible = 0; 679 | defaultConfigurationName = Release; 680 | }; 681 | 0969538F20B51ECE0057BFEE /* Build configuration list for PBXNativeTarget "FMImageView" */ = { 682 | isa = XCConfigurationList; 683 | buildConfigurations = ( 684 | 0969539020B51ECE0057BFEE /* Debug */, 685 | 0969539120B51ECE0057BFEE /* Release */, 686 | ); 687 | defaultConfigurationIsVisible = 0; 688 | defaultConfigurationName = Release; 689 | }; 690 | 0969539220B51ECE0057BFEE /* Build configuration list for PBXNativeTarget "FMImageViewTests" */ = { 691 | isa = XCConfigurationList; 692 | buildConfigurations = ( 693 | 0969539320B51ECE0057BFEE /* Debug */, 694 | 0969539420B51ECE0057BFEE /* Release */, 695 | ); 696 | defaultConfigurationIsVisible = 0; 697 | defaultConfigurationName = Release; 698 | }; 699 | 0969539820B51F5B0057BFEE /* Build configuration list for PBXAggregateTarget "Example" */ = { 700 | isa = XCConfigurationList; 701 | buildConfigurations = ( 702 | 0969539920B51F5B0057BFEE /* Debug */, 703 | 0969539A20B51F5B0057BFEE /* Release */, 704 | ); 705 | defaultConfigurationIsVisible = 0; 706 | defaultConfigurationName = Release; 707 | }; 708 | /* End XCConfigurationList section */ 709 | }; 710 | rootObject = 0969537220B51ECE0057BFEE /* Project object */; 711 | } 712 | -------------------------------------------------------------------------------- /FMImageView/FMImageView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FMImageView/FMImageView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FMImageView/FMImageView.xcodeproj/xcshareddata/xcschemes/FMImageView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Alert/FMAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FMAlert.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 7/2/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | protocol RefreshProtocol: class { 13 | func refreshHandling() 14 | } 15 | 16 | class FMAlert { 17 | private let transparentView: UIView 18 | private let refreshButton: UIButton 19 | private let messageLabel: UILabel 20 | 21 | static let shared = FMAlert() 22 | 23 | weak var __delegate: RefreshProtocol? 24 | 25 | private init() { 26 | let heightTranparentView = UIScreen.main.bounds.width - Constants.Layout.cHeightTV - Constants.Layout.cHeightBV 27 | 28 | self.transparentView = UIView(frame: CGRect(x: 0, y: Constants.Layout.cHeightTV, width: UIScreen.main.bounds.width, height: heightTranparentView)) 29 | 30 | self.transparentView.backgroundColor = UIColor(white: 0, alpha: 0.4) 31 | 32 | self.refreshButton = UIButton(frame: CGRect(x: 0, y: 0, width: 35, height: 35)) 33 | self.refreshButton.center = self.transparentView.center 34 | self.refreshButton.setImage(UIImage(named: "icn_refresh", in: Bundle(for: type(of: self)), compatibleWith: nil), for: .normal) 35 | 36 | 37 | self.messageLabel = UILabel(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 35)) 38 | self.messageLabel.center = CGPoint(x: self.transparentView.center.x, y: self.transparentView.center.y + 35.0) 39 | self.messageLabel.textAlignment = .center 40 | self.messageLabel.textColor = .white 41 | self.messageLabel.numberOfLines = 2 42 | self.messageLabel.adjustsFontSizeToFitWidth = true 43 | 44 | 45 | self.transparentView.addSubview(self.refreshButton) 46 | self.transparentView.addSubview(self.messageLabel) 47 | 48 | 49 | 50 | self.refreshButton.addTarget(self, action: #selector(refresh(_:)), for: .touchUpInside) 51 | } 52 | 53 | @objc func refresh(_ sender: UIButton) { 54 | __delegate?.refreshHandling() 55 | } 56 | 57 | func show(inView: UIView?, message: String?) { 58 | self.messageLabel.text = message 59 | 60 | self.transparentView.removeFromSuperview() 61 | inView?.addSubview(self.transparentView) 62 | self.transparentView.alpha = 0 63 | UIView.animate(withDuration: 0.3, animations: { 64 | self.transparentView.alpha = 1 65 | }) 66 | } 67 | 68 | func hide() { 69 | UIView.animate(withDuration: 0.3, 70 | animations: { 71 | DispatchQueue.global(qos: .default).async(execute: { 72 | DispatchQueue.main.sync(execute: { 73 | self.transparentView.alpha = 0 74 | }) 75 | }) 76 | }, 77 | completion: { completed in 78 | self.transparentView.removeFromSuperview() 79 | }) 80 | } 81 | 82 | required init?(coder aDecoder: NSCoder) { 83 | fatalError("init(coder:) has not been implemented") 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Animator/FMPhotoInteractionAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FMPhotoInteractionAnimator.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 7/11/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class FMPhotoInteractionAnimator: NSObject, UIViewControllerInteractiveTransitioning { 12 | 13 | // MARK: - Public 14 | 15 | public var interactionInProgress = false 16 | public var animator: UIViewControllerAnimatedTransitioning? 17 | 18 | // MARK: - Private 19 | 20 | private var destFrame: CGRect? 21 | private var transitionContext: UIViewControllerContextTransitioning? 22 | private var shouldCompleteTransition = false 23 | private weak var viewController: FMImageSlideViewController! 24 | fileprivate var originFrameForTransition: CGRect? 25 | lazy private var panGestureRecognizer: UIPanGestureRecognizer = { 26 | let pan = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:))) 27 | 28 | // So important to prevent unexpected behavior when mixing GestrureRecognizer and touchesBegan 29 | // especially when a touch event occur near edge of the screen where the system menu recognizer is also handing. 30 | pan.cancelsTouchesInView = false 31 | return pan 32 | }() 33 | 34 | // MARK: - Constructor 35 | 36 | init(viewController: FMImageSlideViewController, fromImageView: UIImageView) { 37 | super.init() 38 | 39 | self.viewController = viewController 40 | 41 | self.originFrameForTransition = fromImageView.convert(fromImageView.bounds, to: viewController.view) 42 | 43 | self.viewController.view.addGestureRecognizer(self.panGestureRecognizer) 44 | } 45 | 46 | // MARK: - Public functions 47 | 48 | public func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) { 49 | self.transitionContext = transitionContext 50 | } 51 | 52 | public func enable() { 53 | self.panGestureRecognizer.isEnabled = true 54 | } 55 | 56 | public func disable() { 57 | self.panGestureRecognizer.isEnabled = false 58 | } 59 | 60 | // MARK: - Private functions 61 | 62 | @objc private func handleGesture(_ gestureRecognizer: UIPanGestureRecognizer) { 63 | 64 | self.viewController.runDelegate(gestureRecognizer) 65 | 66 | if gestureRecognizer.state == .began { 67 | interactionInProgress = true 68 | viewController.dismiss(animated: true, completion: nil) 69 | } else { 70 | self.handlePanWithPanGestureRecognizer(gestureRecognizer, 71 | viewToPan: self.viewController.pageViewController!.view, 72 | anchorPoint: CGPoint(x: self.viewController.view.bounds.midX, 73 | y: self.viewController.view.bounds.midY)) 74 | } 75 | } 76 | 77 | private func handlePanWithPanGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer, viewToPan: UIView, anchorPoint: CGPoint) { 78 | guard let fromView = transitionContext?.view(forKey: UITransitionContextViewKey.from) else { 79 | return 80 | } 81 | let translatedPanGesturePoint = gestureRecognizer.translation(in: fromView) 82 | let newCenterPoint = CGPoint(x: anchorPoint.x, y: anchorPoint.y + translatedPanGesturePoint.y) 83 | 84 | viewToPan.center = newCenterPoint 85 | 86 | let verticalDelta = newCenterPoint.y - anchorPoint.y 87 | let backgroundAlpha = backgroundAlphaForPanningWithVerticalDelta(verticalDelta) 88 | fromView.backgroundColor = fromView.backgroundColor?.withAlphaComponent(backgroundAlpha) 89 | 90 | if gestureRecognizer.state == .ended { 91 | interactionInProgress = false 92 | finishPanWithPanGestureRecognizer(gestureRecognizer, verticalDelta: verticalDelta,viewToPan: viewToPan, anchorPoint: anchorPoint) 93 | } 94 | } 95 | 96 | private func finishPanWithPanGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer, verticalDelta: CGFloat, viewToPan: UIView, anchorPoint: CGPoint) { 97 | guard let fromView = transitionContext?.view(forKey: UITransitionContextViewKey.from) else { 98 | return 99 | } 100 | let returnToCenterVelocityAnimationRatio = 0.00007 101 | let panDismissDistanceRatio = 50.0 / 667.0 // distance over iPhone 6 height 102 | let panDismissMaximumDuration = 0.45 103 | 104 | let velocityY = gestureRecognizer.velocity(in: gestureRecognizer.view).y 105 | 106 | var animationDuration = (Double(abs(velocityY)) * returnToCenterVelocityAnimationRatio) + 0.2 107 | var animationCurve: UIViewAnimationOptions = .curveEaseOut 108 | var finalPageViewCenterPoint = anchorPoint 109 | var finalBackgroundAlpha = 1.0 110 | 111 | let dismissDistance = panDismissDistanceRatio * Double(fromView.bounds.height) 112 | let isDismissing = Double(abs(verticalDelta)) > dismissDistance 113 | 114 | var didAnimateUsingAnimator = false 115 | 116 | if isDismissing { 117 | if let animator = self.animator, let transitionContext = transitionContext { 118 | animator.animateTransition(using: transitionContext) 119 | didAnimateUsingAnimator = true 120 | } else { 121 | let isPositiveDelta = verticalDelta >= 0 122 | let modifier: CGFloat = isPositiveDelta ? 1 : -1 123 | let finalCenterY = fromView.bounds.midY + modifier * fromView.bounds.height 124 | finalPageViewCenterPoint = CGPoint(x: fromView.center.x, y: finalCenterY) 125 | 126 | animationDuration = Double(abs(finalPageViewCenterPoint.y - viewToPan.center.y) / abs(velocityY)) 127 | animationDuration = min(animationDuration, panDismissMaximumDuration) 128 | animationCurve = .curveEaseOut 129 | finalBackgroundAlpha = 0.0 130 | } 131 | } 132 | 133 | if didAnimateUsingAnimator { 134 | self.transitionContext = nil 135 | } else { 136 | UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in 137 | viewToPan.center = finalPageViewCenterPoint 138 | fromView.backgroundColor = fromView.backgroundColor?.withAlphaComponent(CGFloat(finalBackgroundAlpha)) 139 | }, completion: { finished in 140 | if isDismissing { 141 | self.transitionContext?.finishInteractiveTransition() 142 | } else { 143 | self.transitionContext?.cancelInteractiveTransition() 144 | } 145 | 146 | self.transitionContext?.completeTransition(isDismissing && !(self.transitionContext?.transitionWasCancelled ?? false)) 147 | self.transitionContext = nil 148 | }) 149 | } 150 | } 151 | 152 | private func backgroundAlphaForPanningWithVerticalDelta(_ delta: CGFloat) -> CGFloat { 153 | return 1 - min(abs(delta) / 400, 1.0) 154 | } 155 | } 156 | 157 | // MARK: - FMPhotoInteractionAnimator extension 158 | 159 | extension FMPhotoInteractionAnimator: UIViewControllerTransitioningDelegate { 160 | public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 161 | let animationController = FMZoomInAnimationController() 162 | animationController.getOriginFrame = self.getOriginFrameForTransition 163 | return animationController 164 | } 165 | 166 | public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 167 | guard let photoPresenterViewController = dismissed as? FMImageSlideViewController else { return nil } 168 | let animationController = FMZoomOutAnimationController(interactionController: photoPresenterViewController.swipeInteractionController) 169 | animationController.getDestFrame = self.getOriginFrameForTransition 170 | return animationController 171 | } 172 | 173 | public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 174 | guard let animator = animator as? FMZoomOutAnimationController, 175 | let interactionController = animator.interactionController, 176 | interactionController.interactionInProgress 177 | else { 178 | return nil 179 | } 180 | 181 | interactionController.animator = animator 182 | return interactionController 183 | } 184 | 185 | private func getOriginFrameForTransition() -> CGRect { 186 | if let destFrame = self.viewController.getNewDestinatonFrame() { 187 | return destFrame 188 | } 189 | return self.originFrameForTransition ?? CGRect.zero 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Animator/FMZoomInAnimationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FMZoomInAnimationController.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 7/11/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class FMZoomInAnimationController: NSObject, UIViewControllerAnimatedTransitioning { 12 | public var getOriginFrame: (() -> CGRect)! 13 | 14 | public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 15 | return Constants.AnimationDuration.defaultDuration 16 | } 17 | 18 | public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 19 | guard let toVC = transitionContext.viewController(forKey: .to) as? FMImageSlideViewController, 20 | let photoVC = toVC.pageViewController?.viewControllers?.first as? FMImagePreviewViewController 21 | else { return } 22 | 23 | let containerView = transitionContext.containerView 24 | 25 | let snapshot = photoVC.viewToSnapshot().snapshot() 26 | 27 | let bgView = UIView(frame: containerView.frame) 28 | containerView.addSubview(bgView) 29 | 30 | let originalSnapshotCornerRadius = snapshot.layer.cornerRadius 31 | let originalSnapshotSize = snapshot.frame.size 32 | 33 | let startFrame = self.getOriginFrame() 34 | 35 | snapshot.layer.cornerRadius = 0 36 | snapshot.frame = startFrame 37 | snapshot.contentMode = .scaleAspectFill 38 | snapshot.clipsToBounds = true 39 | 40 | containerView.addSubview(toVC.view) 41 | containerView.addSubview(snapshot) 42 | 43 | toVC.view.isHidden = true 44 | 45 | snapshot.alpha = 0 46 | bgView.backgroundColor = toVC.view.backgroundColor 47 | bgView.alpha = 0 48 | 49 | let duration = transitionDuration(using: transitionContext) 50 | 51 | UIView.animateKeyframes(withDuration: duration, 52 | delay: 0, 53 | options: .calculationModeCubic, 54 | animations: { 55 | UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.1) { 56 | snapshot.alpha = 1 57 | } 58 | 59 | UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.9) { 60 | snapshot.frame = CGRect(origin: .zero, size: originalSnapshotSize) 61 | snapshot.center = containerView.center 62 | snapshot.layer.cornerRadius = originalSnapshotCornerRadius 63 | } 64 | 65 | UIView.addKeyframe(withRelativeStartTime: 0.2, relativeDuration: 0.9) { 66 | bgView.alpha = 1 67 | } 68 | }, 69 | completion: { _ in 70 | toVC.view.isHidden = false 71 | snapshot.removeFromSuperview() 72 | bgView.removeFromSuperview() 73 | 74 | if transitionContext.isInteractive { 75 | if transitionContext.transitionWasCancelled { 76 | transitionContext.cancelInteractiveTransition() 77 | } else { 78 | transitionContext.finishInteractiveTransition() 79 | } 80 | } 81 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 82 | }) 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Animator/FMZoomOutAnimationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FMZoomOutAnimationController.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 7/11/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class FMZoomOutAnimationController: NSObject, UIViewControllerAnimatedTransitioning { 12 | public var interactionInProgress = false 13 | 14 | public var getDestFrame: (() -> CGRect)! 15 | 16 | public var interactionController: FMPhotoInteractionAnimator? 17 | 18 | public init(interactionController: FMPhotoInteractionAnimator?) { 19 | self.interactionController = interactionController 20 | } 21 | 22 | public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 23 | return Constants.AnimationDuration.defaultDuration 24 | } 25 | 26 | public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 27 | guard let fromVC = transitionContext.viewController(forKey: .from) as? FMImageSlideViewController, 28 | let toVC = transitionContext.viewController(forKey: .to), 29 | let photoVC = fromVC.pageViewController!.viewControllers?.first as? FMImagePreviewViewController 30 | else { 31 | return 32 | } 33 | 34 | let containerView = transitionContext.containerView 35 | 36 | let pannedVector = fromVC.pageViewController!.view.frame.origin 37 | 38 | let snapshot = photoVC.viewToSnapshot().snapshot() 39 | 40 | let bgView = UIView(frame: containerView.frame) 41 | bgView.backgroundColor = fromVC.view.backgroundColor 42 | 43 | containerView.addSubview(bgView) 44 | containerView.addSubview(snapshot) 45 | 46 | let originSnapshotSize = snapshot.frame.size 47 | 48 | snapshot.frame = CGRect(origin: .zero, size: originSnapshotSize) 49 | snapshot.clipsToBounds = true 50 | snapshot.contentMode = .scaleAspectFill 51 | snapshot.center = containerView.center 52 | 53 | snapshot.frame = CGRect(origin: CGPoint(x: snapshot.frame.origin.x + pannedVector.x, 54 | y: snapshot.frame.origin.y + pannedVector.y), 55 | size: originSnapshotSize) 56 | 57 | fromVC.view.isHidden = true 58 | 59 | let duration = transitionDuration(using: transitionContext) 60 | UIView.animate(withDuration: duration, animations: { 61 | let destFrame: CGRect = self.getDestFrame() 62 | snapshot.frame = destFrame 63 | snapshot.layer.cornerRadius = 0 64 | bgView.alpha = 0 65 | }) { _ in 66 | fromVC.view.isHidden = false 67 | snapshot.removeFromSuperview() 68 | bgView.removeFromSuperview() 69 | 70 | if transitionContext.transitionWasCancelled { 71 | toVC.view.removeFromSuperview() 72 | } 73 | 74 | if transitionContext.isInteractive { 75 | if transitionContext.transitionWasCancelled { 76 | transitionContext.cancelInteractiveTransition() 77 | } else { 78 | transitionContext.finishInteractiveTransition() 79 | } 80 | } 81 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /FMImageView/FMImageView/Assets.xcassets/icn_close.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "close.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "close@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "close@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FMImageView/FMImageView/Assets.xcassets/icn_close.imageset/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView/Assets.xcassets/icn_close.imageset/close.png -------------------------------------------------------------------------------- /FMImageView/FMImageView/Assets.xcassets/icn_close.imageset/close@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView/Assets.xcassets/icn_close.imageset/close@2x.png -------------------------------------------------------------------------------- /FMImageView/FMImageView/Assets.xcassets/icn_close.imageset/close@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView/Assets.xcassets/icn_close.imageset/close@3x.png -------------------------------------------------------------------------------- /FMImageView/FMImageView/Assets.xcassets/icn_refresh.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "refresh.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "refresh@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "refresh@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FMImageView/FMImageView/Assets.xcassets/icn_refresh.imageset/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView/Assets.xcassets/icn_refresh.imageset/refresh.png -------------------------------------------------------------------------------- /FMImageView/FMImageView/Assets.xcassets/icn_refresh.imageset/refresh@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView/Assets.xcassets/icn_refresh.imageset/refresh@2x.png -------------------------------------------------------------------------------- /FMImageView/FMImageView/Assets.xcassets/icn_refresh.imageset/refresh@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/FMImageView/FMImageView/Assets.xcassets/icn_refresh.imageset/refresh@3x.png -------------------------------------------------------------------------------- /FMImageView/FMImageView/Extensions/CGPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGPoint.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 6/26/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension CGPoint { 12 | static func +(lhs: CGPoint, rhs: CGSize) -> CGPoint { 13 | return CGPoint(x: lhs.x + rhs.width, y: lhs.y + rhs.height) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Extensions/CGSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSize.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 6/26/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension CGSize { 12 | static func *(lhs: CGSize, rhs: CGFloat) -> CGSize { 13 | return CGSize(width: lhs.width * rhs, height: lhs.height * rhs) 14 | } 15 | static func /(lhs: CGSize, rhs: CGFloat) -> CGSize { 16 | return CGSize(width: lhs.width / rhs, height: lhs.height / rhs) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Extensions/UIImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 6/27/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension UIImage { 12 | 13 | /** 14 | Returns an UIImage with a specified background color. 15 | - parameter color: The color of the background 16 | */ 17 | convenience init(withBackground color: UIColor) { 18 | 19 | let rect: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1) 20 | UIGraphicsBeginImageContext(rect.size); 21 | let context:CGContext = UIGraphicsGetCurrentContext()!; 22 | context.setFillColor(color.cgColor); 23 | context.fill(rect) 24 | 25 | let image:UIImage = UIGraphicsGetImageFromCurrentImageContext()! 26 | UIGraphicsEndImageContext() 27 | 28 | self.init(ciImage: CIImage(image: image)!) 29 | 30 | } 31 | 32 | func extractColor() -> String? { 33 | let pixel = UnsafeMutablePointer.allocate(capacity: 4) 34 | let colorSpace:CGColorSpace = CGColorSpaceCreateDeviceRGB() 35 | let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) 36 | let context = CGContext(data: pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) 37 | context?.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: 1, height: 1)) 38 | 39 | var color: UIColor? 40 | 41 | if pixel[3] > 0 { 42 | let alpha:CGFloat = CGFloat(pixel[3]) / 255.0 43 | let multiplier:CGFloat = alpha / 255.0 44 | 45 | color = UIColor(red: CGFloat(pixel[0]) * multiplier, green: CGFloat(pixel[1]) * multiplier, blue: CGFloat(pixel[2]) * multiplier, alpha: alpha) 46 | }else{ 47 | 48 | color = UIColor(red: CGFloat(pixel[0]) / 255.0, green: CGFloat(pixel[1]) / 255.0, blue: CGFloat(pixel[2]) / 255.0, alpha: CGFloat(pixel[3]) / 255.0) 49 | } 50 | 51 | #if swift(>=4.1) 52 | pixel.deinitialize(count: 4) 53 | pixel.deallocate() 54 | #else 55 | pixel.deinitialize() 56 | pixel.deallocate(capacity: 4) 57 | #endif 58 | 59 | if color != nil { 60 | return self.toHexString(color: color!) 61 | } 62 | return nil 63 | } 64 | 65 | func toHexString(color: UIColor) -> String { 66 | var r:CGFloat = 0 67 | var g:CGFloat = 0 68 | var b:CGFloat = 0 69 | var a:CGFloat = 0 70 | 71 | color.getRed(&r, green: &g, blue: &b, alpha: &a) 72 | 73 | let rgb:Int = (Int)(r*255)<<16 | (Int)(g*255)<<8 | (Int)(b*255)<<0 74 | 75 | return String(format:"#%06x", rgb) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Extensions/UIPanGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIPanGestureRecognizer.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 6/29/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension UIPanGestureRecognizer { 12 | public var direction: PanDirection? { 13 | let velocity = self.velocity(in: view) 14 | let isVertical = fabs(velocity.y) > fabs(velocity.x) 15 | switch (isVertical, velocity.x, velocity.y) { 16 | case (true, _, let y) where y < 0: return .up 17 | case (true, _, let y) where y > 0: return .down 18 | case (false, let x, _) where x > 0: return .right 19 | case (false, let x, _) where x < 0: return .left 20 | default: return nil 21 | } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Extensions/UIView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView.swift 3 | // FMImageView 4 | // 5 | // Created by Trần Quang Minh on 7/30/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | extension UIView { 10 | func snapshot() -> UIView { 11 | if let contents = layer.contents { 12 | var snapshotedView: UIView! 13 | 14 | if let view = self as? UIImageView { 15 | snapshotedView = type(of: view).init(image: view.image) 16 | snapshotedView.bounds = view.bounds 17 | } else { 18 | snapshotedView = UIView(frame: frame) 19 | snapshotedView.layer.contents = contents 20 | snapshotedView.layer.bounds = layer.bounds 21 | } 22 | snapshotedView.layer.cornerRadius = layer.cornerRadius 23 | snapshotedView.layer.masksToBounds = layer.masksToBounds 24 | snapshotedView.contentMode = contentMode 25 | snapshotedView.transform = transform 26 | 27 | return snapshotedView 28 | } else { 29 | return snapshotView(afterScreenUpdates: true)! 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/FMImageView.h: -------------------------------------------------------------------------------- 1 | // 2 | // FMImageView.h 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 5/23/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for FMImageView. 12 | FOUNDATION_EXPORT double FMImageViewVersionNumber; 13 | 14 | //! Project version string for FMImageView. 15 | FOUNDATION_EXPORT const unsigned char FMImageViewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Help/CGImage+Help.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGImage+Help.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 5/23/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ImageIO 11 | 12 | open class RenderCGImage { 13 | public static let shared = RenderCGImage() 14 | 15 | public func createCGImage(atURL: URL) -> CGImage? { 16 | let options: CFDictionary = [kCGImageSourceShouldCache : kCFBooleanTrue, 17 | kCGImageSourceShouldAllowFloat: kCFBooleanTrue] as CFDictionary 18 | 19 | if let source = CGImageSourceCreateWithURL(atURL as CFURL, options) { 20 | return CGImageSourceCreateImageAtIndex(source, 0, nil) 21 | } 22 | 23 | return nil 24 | } 25 | 26 | public func createCGImage(atData: Data) -> CGImage? { 27 | if let source = CGImageSourceCreateWithData(atData as CFData, nil) { 28 | let options: CFDictionary = [kCGImageSourceCreateThumbnailWithTransform: kCFBooleanTrue, 29 | kCGImageSourceCreateThumbnailFromImageIfAbsent: kCFBooleanTrue] as CFDictionary 30 | return CGImageSourceCreateThumbnailAtIndex(source, 0, options) 31 | } 32 | 33 | return nil 34 | } 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Help/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 7/3/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Constants { 12 | struct Layout { 13 | // TV : TopView , BV: BottomView 14 | static let cTopTV: CGFloat = 0.0 15 | static let cBottomTV: CGFloat = 0.0 16 | static let cLeadingTV: CGFloat = 0.0 17 | static let cTrainingTV: CGFloat = 0.0 18 | static let cHeightTV: CGFloat = 80.0 19 | static let cBottomBV: CGFloat = 0.0 20 | static let cHeightBV: CGFloat = 40.0 21 | 22 | // Items in TopView 23 | static let leadingDismissButton: CGFloat = 20.0 24 | } 25 | 26 | struct Color { 27 | static let cBackgroundColor: UIColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1) 28 | } 29 | 30 | // ScrollView 31 | struct Scale { 32 | static let cMax: CGFloat = 3.0 33 | static let cMin: CGFloat = 1.0 34 | } 35 | 36 | struct AnimationDuration { 37 | static let defaultDuration: Double = 0.375 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Help/Enums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Enums.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 6/29/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum PanDirection: Int { 12 | case up, down, left, right 13 | public var isVertical: Bool { return [.up, .down].contains(self) } 14 | public var isHorizontal: Bool { return !isVertical } 15 | } 16 | 17 | public enum ContentMode: Int { 18 | case aspectFill 19 | case aspectFit 20 | case widthFill 21 | case heightFill 22 | } 23 | 24 | public enum Offset: Int { 25 | case begining 26 | case center 27 | } 28 | 29 | public enum SlideStatus { 30 | case pendding 31 | case completed 32 | } 33 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Help/Image+URL+Size.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image+URL+Size.swift 3 | // FMImageView 4 | // refer : https://gist.github.com/shpakovski/1744633 5 | // Created by Hoang Trong Anh on 5/23/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import ImageIO 10 | import Foundation 11 | 12 | open class ImageIOSizeImage: NSObject { 13 | public static let shared = ImageIOSizeImage() 14 | 15 | open func cgSizeOfImage(atURL: URL) -> CGSize { 16 | if let source = CGImageSourceCreateWithURL(atURL as CFURL, nil), 17 | let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, [kCGImageSourceShouldCache as String : 0] as CFDictionary), 18 | let width = (properties as? [AnyHashable: Any])?[kCGImagePropertyPixelWidth as String] as? NSNumber, 19 | let height = (properties as? [AnyHashable: Any])?[kCGImagePropertyPixelHeight as String] as? NSNumber { 20 | return CGSize(width: CGFloat(Float(truncating: width)), height: CGFloat(Float(truncating: height))) 21 | } 22 | 23 | return CGSize.zero 24 | } 25 | 26 | open func cgSizeOfImage(atImage: UIImage) -> CGSize { 27 | return atImage.size 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Indicator/FMLoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Indicator.swift 3 | // FMImageView 4 | // refer : https://github.com/tribalmedia/FMPhotoPicker/blob/master/FMPhotoPicker/FMPhotoPicker/source/Utilities/FMLoadingView.swift 5 | // Created by Hoang Trong Anh on 7/2/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class FMLoadingView { 13 | private let transparentView: UIView 14 | private let indicator: UIActivityIndicatorView 15 | 16 | static let shared = FMLoadingView() 17 | 18 | private init() { 19 | let rootVC = (UIApplication.shared.delegate?.window??.rootViewController)! 20 | 21 | self.transparentView = UIView(frame: rootVC.view.frame) 22 | self.transparentView.backgroundColor = UIColor(white: 0, alpha: 0.4) 23 | 24 | self.indicator = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 35, height: 35)) 25 | self.indicator.center = self.transparentView.center 26 | self.indicator.color = .white 27 | self.indicator.hidesWhenStopped = true 28 | 29 | self.transparentView.addSubview(self.indicator) 30 | } 31 | 32 | func show(inView: UIView?) { 33 | inView?.addSubview(self.transparentView) 34 | 35 | self.transparentView.alpha = 0 36 | self.indicator.startAnimating() 37 | UIView.animate(withDuration: 0.3, animations: { 38 | self.transparentView.alpha = 1 39 | }) 40 | } 41 | 42 | func hide() { 43 | UIView.animate(withDuration: 0.3, 44 | animations: { 45 | DispatchQueue.global(qos: .default).async(execute: { 46 | DispatchQueue.main.sync(execute: { 47 | self.transparentView.alpha = 0 48 | }) 49 | }) 50 | }, 51 | completion: { completed in 52 | self.transparentView.removeFromSuperview() 53 | self.indicator.stopAnimating() 54 | }) 55 | } 56 | 57 | required init?(coder aDecoder: NSCoder) { 58 | fatalError("init(coder:) has not been implemented") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | UIViewControllerBasedStatusBarAppearance 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Network/ImageLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageLoader.swift 3 | // FMImageView 4 | // refer : https://github.com/MengTo/Spring/blob/master/Spring/ImageLoader.swift 5 | // 6 | // Created by Hoang Trong Anh on 7/2/18. 7 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 8 | // 9 | import UIKit 10 | import Foundation 11 | 12 | 13 | public class ImageLoader { 14 | 15 | var cache = NSCache() 16 | 17 | public class var sharedLoader : ImageLoader { 18 | struct Static { 19 | static let instance : ImageLoader = ImageLoader() 20 | } 21 | return Static.instance 22 | } 23 | 24 | public func imageForUrl(url: URL, completionHandler: @escaping(_ image: UIImage?, _ url: String, _ error: NetworkingErrors?) -> ()) { 25 | DispatchQueue.global(qos: DispatchQoS.QoSClass.background).async { 26 | var data: NSData? 27 | 28 | if let dataCache = self.cache.object(forKey: url.absoluteString as NSString) { 29 | data = (dataCache) as NSData 30 | 31 | } else { 32 | guard let data = NSData(contentsOf: url) else { 33 | completionHandler(nil, url.absoluteString, .dataReturnedNil) 34 | return 35 | } 36 | 37 | self.cache.setObject(data, forKey: url.absoluteString as NSString) 38 | } 39 | 40 | if let goodData = data { 41 | let image = UIImage(data: goodData as Data) 42 | 43 | DispatchQueue.main.async(execute: {() in 44 | completionHandler(image, url.absoluteString, nil) 45 | }) 46 | return 47 | } 48 | 49 | let downloadTask: URLSessionDataTask = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) -> Void in 50 | 51 | // Check for errors in responses. 52 | if let error = error { 53 | let nsError = error as NSError 54 | if nsError.domain == NSURLErrorDomain && (nsError.code == NSURLErrorNotConnectedToInternet || nsError.code == NSURLErrorTimedOut) { 55 | completionHandler(nil, url.absoluteString, .noInternetConnection) 56 | } else { 57 | completionHandler(nil, url.absoluteString, .returnedError(error)) 58 | } 59 | return 60 | } 61 | 62 | if let response = response as? HTTPURLResponse, response.statusCode <= 200 && response.statusCode >= 299 { 63 | completionHandler(nil, url.absoluteString, .invalidStatusCode("Request returned status code other than 2xx \(response)")) 64 | return 65 | } 66 | 67 | if let _data = data { 68 | let image = UIImage(data: _data) 69 | 70 | self.cache.setObject(_data as NSData, forKey: url.absoluteString as NSString) 71 | 72 | DispatchQueue.main.async(execute: {() in 73 | completionHandler(image, url.absoluteString, nil) 74 | }) 75 | return 76 | } else { 77 | completionHandler(nil, url.absoluteString, .dataReturnedNil) 78 | } 79 | }) 80 | downloadTask.resume() 81 | 82 | } 83 | 84 | } 85 | } 86 | 87 | public enum NetworkingErrors: Error { 88 | case errorParsingJSON 89 | case noInternetConnection 90 | case dataReturnedNil 91 | case returnedError(Error) 92 | case invalidStatusCode(String) 93 | case customError(String) 94 | } 95 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Protocols/FMDelegateProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FMDelegateProtocol.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 7/5/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct TypeName { 12 | enum Scroll { 13 | case sc_disable 14 | case sc_enable 15 | } 16 | 17 | enum Modal { 18 | case md_dismiss 19 | } 20 | 21 | enum Swipe { 22 | case enable 23 | case disable 24 | } 25 | 26 | enum Elasticity { 27 | case elasticity_in 28 | case elasticity_out 29 | } 30 | } 31 | 32 | protocol ImagePreviewFMDelegate: class { 33 | func notificationHandlingModal(type: TypeName.Modal) 34 | func notificationHandlingSwipe(type: TypeName.Swipe) 35 | func notificationHandlingElasticityOfTopViewAndBottomView(type: TypeName.Elasticity) 36 | 37 | } 38 | 39 | extension ImagePreviewFMDelegate { 40 | // set optional func of delegate 41 | } 42 | 43 | protocol ImageSlideFMDelegate: class { 44 | func handlingModal(type: TypeName.Modal) 45 | func handlingSwipe(type: TypeName.Swipe) 46 | func handlingElasticityOfTopViewAndBottomView(type: TypeName.Elasticity) 47 | } 48 | 49 | extension ImageSlideFMDelegate { 50 | // set optional func of delegate 51 | } 52 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Views/Config/Config.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Config.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 7/26/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Config { 12 | public var initImageView: UIImageView 13 | public var initIndex: Int 14 | public var bottomView: HorizontalStackView? 15 | 16 | public init(initImageView: UIImageView, initIndex: Int) { 17 | self.initImageView = initImageView 18 | self.initIndex = initIndex 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Views/Config/ConfigureZoomView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigureZoomView.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 7/26/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct ConfigureZoomView { 12 | public var _imageContentMode: ContentMode 13 | public var _initialOffset: Offset 14 | public var _maxScaleFromMinScale: CGFloat 15 | 16 | public init(imageContentMode: ContentMode?, initialOffset: Offset?, maxScaleFromMinScale: CGFloat?) { 17 | self._imageContentMode = imageContentMode ?? .aspectFit 18 | self._initialOffset = initialOffset ?? .center 19 | self._maxScaleFromMinScale = maxScaleFromMinScale ?? Constants.Scale.cMax 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Views/Datasource/FMImageDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FMImageDataSource.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 7/24/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class FMImageDataSource { 12 | var useURLs: Bool = false 13 | 14 | var images: [UIImage]? 15 | 16 | var imageURLs: [URL]? 17 | 18 | public init(useURLs: Bool) { 19 | self.useURLs = useURLs 20 | } 21 | 22 | convenience public init(imageURLs: [URL]) { 23 | self.init(useURLs: true) 24 | 25 | self.imageURLs = imageURLs 26 | } 27 | 28 | convenience public init(images: [UIImage]) { 29 | self.init(useURLs: false) 30 | 31 | self.images = images 32 | } 33 | 34 | func selectImage(index: Int) -> UIImage? { 35 | 36 | guard let images = self.images else { 37 | return nil 38 | } 39 | 40 | return images[index] 41 | } 42 | 43 | func selectImageURL(index: Int) -> URL? { 44 | guard let imageURLs = self.imageURLs else { 45 | return nil 46 | } 47 | 48 | return imageURLs[index] 49 | } 50 | 51 | func total() -> Int { 52 | return self.useURLs ? self.imageURLs?.count ?? 0 : self.images?.count ?? 0 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Views/FMImagePreviewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePreviewViewController.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 6/27/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol Move: class { 12 | func moving(_ sender: UIPanGestureRecognizer) 13 | } 14 | 15 | extension FMImagePreviewViewController: Move { 16 | func moving(_ sender: UIPanGestureRecognizer) { 17 | self.scrollView.moveScrollViewFrame(sender) 18 | } 19 | } 20 | 21 | class FMImagePreviewViewController: UIViewController { 22 | var itemIndex: Int = -1 23 | 24 | var image: UIImage? 25 | 26 | var imageURL: URL? 27 | 28 | var scrollView: ImageZoomView! 29 | 30 | var slideStatus: SlideStatus? { 31 | didSet { 32 | self.scrollView.slideStatus = self.slideStatus 33 | } 34 | } 35 | 36 | 37 | weak var _delegate: ImageSlideFMDelegate? 38 | 39 | var parentVC: FMImageSlideViewController? 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | 44 | // Do any additional setup after loading the view. 45 | scrollView = ImageZoomView(config: ConfigureZoomView(imageContentMode: .aspectFit, initialOffset: .center, maxScaleFromMinScale: Constants.Scale.cMax)) 46 | scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 47 | scrollView.backgroundColor = UIColor.clear 48 | scrollView._delegate = self 49 | 50 | FMAlert.shared.__delegate = self 51 | 52 | self.view = scrollView 53 | 54 | if let fromImage = self.image { 55 | DispatchQueue.main.async { 56 | self.scrollView.displayImage(fromImage) 57 | } 58 | } else { 59 | let image = UIImage(withBackground: UIColor.black) 60 | scrollView.displayImage(image) 61 | self.updateImage() 62 | } 63 | } 64 | 65 | public func viewToSnapshot() -> UIView { 66 | guard let scrollView = scrollView, let imgV = scrollView._imageView else { 67 | return self.view 68 | } 69 | 70 | return imgV 71 | } 72 | 73 | override func viewWillAppear(_ animated: Bool) { 74 | parentVC?.mDelegate = self 75 | } 76 | 77 | private func updateImage() { 78 | guard let imageURL = self.imageURL else { 79 | return 80 | } 81 | 82 | DispatchQueue.main.async { 83 | self.parentVC?.swipeInteractionController?.disable() 84 | 85 | FMLoadingView.shared.show(inView: self.parentVC?.view) 86 | } 87 | 88 | ImageLoader.sharedLoader.imageForUrl(url: imageURL) { (image, urlString, error) in 89 | if let _ = error { 90 | DispatchQueue.main.async { 91 | FMAlert.shared.show(inView: self.parentVC?.view, message: "Whoops! Something went wrong.\nPlease try again!") 92 | } 93 | 94 | } 95 | 96 | if let image = image { 97 | self.scrollView.displayImage(image) 98 | } 99 | 100 | DispatchQueue.main.async { 101 | self.parentVC?.swipeInteractionController?.enable() 102 | 103 | FMLoadingView.shared.hide() 104 | } 105 | } 106 | } 107 | 108 | override func viewWillDisappear(_ animated: Bool) { 109 | super.viewWillDisappear(animated) 110 | DispatchQueue.main.async { 111 | FMAlert.shared.hide() 112 | } 113 | } 114 | 115 | override func didReceiveMemoryWarning() { 116 | super.didReceiveMemoryWarning() 117 | // Dispose of any resources that can be recreated. 118 | } 119 | 120 | } 121 | 122 | extension FMImagePreviewViewController: RefreshProtocol { 123 | func refreshHandling() { 124 | DispatchQueue.main.async { 125 | FMAlert.shared.hide() 126 | } 127 | updateImage() 128 | } 129 | } 130 | 131 | 132 | extension FMImagePreviewViewController: ImagePreviewFMDelegate { 133 | func notificationHandlingModal(type: TypeName.Modal) { 134 | self._delegate?.handlingModal(type: type) 135 | } 136 | 137 | func notificationHandlingSwipe(type: TypeName.Swipe) { 138 | self._delegate?.handlingSwipe(type: type) 139 | } 140 | 141 | func notificationHandlingElasticityOfTopViewAndBottomView(type: TypeName.Elasticity) { 142 | self._delegate?.handlingElasticityOfTopViewAndBottomView(type: type) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Views/FMImageSlideViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageSlideViewController.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 6/27/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class FMImageSlideViewController: UIViewController { 12 | 13 | // *********************************************** 14 | // MARK: Custom variables 15 | // *********************************************** 16 | 17 | // public 18 | public var didMoveToViewControllerHandler: ((Int) -> Void)? 19 | public var swipeInteractionController: FMPhotoInteractionAnimator? 20 | 21 | // internal 22 | var topView: UIView? 23 | 24 | var bottomView: HorizontalStackView? 25 | 26 | var pageViewController: UIPageViewController? 27 | 28 | var scrollView: ImageZoomView! 29 | 30 | var config: Config! 31 | 32 | var datasource: FMImageDataSource! 33 | 34 | weak var mDelegate: Move? 35 | 36 | // outlets 37 | private var destinationFrame: CGRect? 38 | private var dismissButton: UIButton! 39 | private var numberImageLabel: UILabel! 40 | 41 | private var topConstrainTopView: NSLayoutConstraint? 42 | private var bottomConstraintStackView: NSLayoutConstraint? 43 | 44 | 45 | // default init 46 | public init(config: Config) { 47 | self.config = config 48 | super.init(nibName: nil, bundle: nil) 49 | } 50 | 51 | // custom init 52 | public convenience init(datasource: FMImageDataSource, config: Config) { 53 | self.init(config: config) 54 | self.datasource = datasource 55 | } 56 | 57 | required public init?(coder: NSCoder) { 58 | fatalError("init(coder:) has not been implemented") 59 | } 60 | 61 | override public func viewDidLoad() { 62 | super.viewDidLoad() 63 | 64 | self.view.backgroundColor = Constants.Color.cBackgroundColor 65 | 66 | // step 1 67 | self.configurePageViewController() 68 | // step 2 69 | self.createFirstScreen() 70 | // step 3 71 | self.configureSwipeInteractionController() 72 | } 73 | 74 | public override func viewWillAppear(_ animated: Bool) { 75 | super.viewWillAppear(animated) 76 | 77 | // hide status bar 78 | if let window = UIApplication.shared.delegate?.window as? UIWindow { 79 | window.windowLevel = UIWindowLevelStatusBar + 1 80 | } 81 | 82 | view.frame = UIScreen.main.bounds 83 | 84 | self.displayTabBar() 85 | } 86 | 87 | public override func viewWillDisappear(_ animated: Bool) { 88 | super.viewWillDisappear(animated) 89 | 90 | // show status bar 91 | if let window = UIApplication.shared.delegate?.window as? UIWindow { 92 | window.windowLevel = UIWindowLevelNormal 93 | } 94 | 95 | self.displayTabBar() 96 | } 97 | 98 | private func displayTabBar() { 99 | guard let _ = self.tabBarController?.tabBar else { 100 | return 101 | } 102 | 103 | if self.tabBarController!.tabBar.isHidden { 104 | self.tabBarController?.tabBar.isHidden = false 105 | } else { 106 | self.tabBarController?.tabBar.isHidden = true 107 | } 108 | } 109 | 110 | override public func viewDidLayoutSubviews() { 111 | super.viewDidLayoutSubviews() 112 | 113 | // setup top view when hide status bar 114 | if #available(iOS 11, *) { 115 | // safe area constraints already set 116 | additionalSafeAreaInsets = UIEdgeInsets(top: -UIApplication.shared.statusBarFrame.height, left: 0, bottom: 0, right: 0) 117 | } else { 118 | self.view.topAnchor.constraint(equalTo: topLayoutGuide.topAnchor, constant: 0).isActive = true 119 | } 120 | } 121 | 122 | // MARK: Public functions 123 | 124 | public func setNewDestinatonFrame(imageView: UIImageView) { 125 | let destFrame = imageView.convert(imageView.bounds, to: self.view) 126 | self.destinationFrame = destFrame 127 | } 128 | 129 | public func getNewDestinatonFrame() -> CGRect? { 130 | return self.destinationFrame 131 | } 132 | 133 | private func configurePageViewController() { 134 | self.pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [UIPageViewControllerOptionInterPageSpacingKey: 16.0]) 135 | self.pageViewController!.dataSource = self 136 | self.pageViewController!.delegate = self 137 | 138 | self.pageViewController?.view.backgroundColor = .clear 139 | 140 | self.addChildViewController(self.pageViewController!) 141 | self.view.addSubview(pageViewController!.view) 142 | self.view.sendSubview(toBack: pageViewController!.view) 143 | 144 | self.pageViewController!.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 145 | self.pageViewController!.didMove(toParentViewController: self) 146 | } 147 | 148 | private func createFirstScreen() { 149 | // Create the first screen 150 | if let config = self.config, let startingViewController = self.getItemController(config.initIndex) { 151 | self.pageViewController?.setViewControllers([startingViewController], direction: .forward, animated: true) { (completed) in 152 | self.prepareNumbersImageLabel() 153 | self.prepareDismissButton() 154 | 155 | self.configSubviewViewController() 156 | } 157 | } 158 | } 159 | 160 | private func configureSwipeInteractionController() { 161 | guard let _ = self.config else { return } 162 | // init animation transition 163 | self.swipeInteractionController = FMPhotoInteractionAnimator(viewController: self, fromImageView: self.config!.initImageView) 164 | 165 | self.transitioningDelegate = self.swipeInteractionController 166 | self.modalPresentationStyle = .custom 167 | self.modalPresentationCapturesStatusBarAppearance = true 168 | } 169 | 170 | private func prepareNumbersImageLabel() { 171 | self.numberImageLabel = UILabel() 172 | self.numberImageLabel.textColor = .white 173 | self.numberImageLabel.font = UIFont.systemFont(ofSize: 20.0, weight: .bold) 174 | } 175 | 176 | private func prepareDismissButton() { 177 | self.dismissButton = UIButton(type: .custom) 178 | self.dismissButton.setImage(UIImage(named: "icn_close", in: Bundle(for: self.classForCoder), compatibleWith: nil), for: .normal) 179 | self.dismissButton.clipsToBounds = true 180 | self.dismissButton.addTarget(self, action: #selector(self.dismissTargetButton(_:)), for: .touchUpInside) 181 | } 182 | 183 | @objc func dismissTargetButton(_ sender: UIButton) { 184 | self.dismiss(animated: true, completion: nil) 185 | } 186 | 187 | private func configSubviewViewController() { 188 | setupTopSubView() 189 | 190 | setupBottomSubView() 191 | } 192 | 193 | private func setupTopSubView() { 194 | self.updateUINumberImageLabel(numerator: self.config.initIndex) 195 | 196 | topView = UIView() 197 | topView?.translatesAutoresizingMaskIntoConstraints = false 198 | 199 | self.view.addSubview(topView!) 200 | 201 | self.topView?.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: Constants.Layout.cLeadingTV).isActive = true 202 | self.topView?.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: Constants.Layout.cTrainingTV).isActive = true 203 | self.topConstrainTopView = self.topView?.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Layout.cTopTV) 204 | self.topConstrainTopView?.isActive = true 205 | self.topView?.heightAnchor.constraint(equalToConstant: Constants.Layout.cHeightTV).isActive = true 206 | 207 | let subviews: [UIView] = [ numberImageLabel, dismissButton ] 208 | 209 | for v in subviews { 210 | v.translatesAutoresizingMaskIntoConstraints = false 211 | topView!.addSubview(v) 212 | } 213 | 214 | NSLayoutConstraint.activate( 215 | [NSLayoutConstraint(item: dismissButton, attribute: .left, relatedBy: .equal, toItem: topView, attribute: .left, multiplier: 1, constant: Constants.Layout.leadingDismissButton)] + 216 | [NSLayoutConstraint(item: dismissButton,attribute: .top, relatedBy: .equal, toItem: topView,attribute: .top, multiplier: 1, constant: Constants.Layout.cHeightTV / 2)] + 217 | [NSLayoutConstraint(item: numberImageLabel,attribute: .centerX, relatedBy: .equal, toItem: topView, attribute: .centerX, multiplier: 1, constant: 0)] + 218 | [NSLayoutConstraint( item: numberImageLabel, attribute: .top, relatedBy: .equal, toItem: topView, attribute: .top, multiplier: 1, constant: Constants.Layout.cHeightTV / 2)] 219 | ) 220 | 221 | } 222 | 223 | func runDelegate(_ sender: UIPanGestureRecognizer) { 224 | switch sender.state { 225 | case .began: 226 | self.handlingElasticityOfTopViewAndBottomView(type: .elasticity_out) 227 | case .ended, .cancelled, .failed: 228 | self.handlingElasticityOfTopViewAndBottomView(type: .elasticity_in) 229 | default: 230 | break 231 | } 232 | 233 | } 234 | 235 | private func setupBottomSubView() { 236 | guard let bottomView = self.config.bottomView else { 237 | return 238 | } 239 | 240 | self.bottomView = bottomView 241 | self.view.addSubview(self.bottomView!) 242 | 243 | // setup layout 244 | self.bottomView!.heightAnchor.constraint(equalToConstant: Constants.Layout.cHeightBV).isActive = true 245 | self.bottomView!.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0).isActive = true 246 | self.bottomView!.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0).isActive = true 247 | self.bottomConstraintStackView = self.bottomView!.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0) 248 | self.bottomConstraintStackView?.isActive = true 249 | } 250 | 251 | private func updateUINumberImageLabel(numerator: Int?) { 252 | guard let numerator = numerator else { 253 | self.numberImageLabel.text = "1/\(self.datasource.total())" 254 | return 255 | } 256 | 257 | numberImageLabel.text = "\(numerator + 1)/\(self.datasource.total())" 258 | } 259 | 260 | // *********************************************** 261 | // MARK: UIPageViewController 262 | // *********************************************** 263 | 264 | fileprivate func getItemController(_ itemIndex: Int) -> UIViewController? { 265 | 266 | if itemIndex < self.datasource.total() { 267 | 268 | let result = FMImagePreviewViewController() 269 | 270 | result.itemIndex = itemIndex 271 | 272 | if let fromImage = self.config?.initImageView, itemIndex == self.config?.initIndex { 273 | result.image = fromImage.image 274 | } else { 275 | if self.datasource.useURLs { 276 | result.imageURL = self.datasource.selectImageURL(index: itemIndex) 277 | } else { 278 | result.image = self.datasource.selectImage(index: itemIndex) 279 | } 280 | } 281 | 282 | if result.scrollView == nil { 283 | result.scrollView = self.scrollView 284 | } 285 | 286 | result._delegate = self 287 | 288 | result.parentVC = self 289 | 290 | return result 291 | } 292 | 293 | return nil 294 | } 295 | 296 | private func fadeOut(with duration: TimeInterval = Constants.AnimationDuration.defaultDuration) { 297 | guard let bottomView = self.bottomView, let topView = self.topView else { 298 | return 299 | } 300 | 301 | self.topConstrainTopView?.constant = -(self.topView!.frame.height / 2) 302 | self.bottomConstraintStackView?.constant = bottomView.heightStackView / 2 303 | 304 | UIView.animate(withDuration: duration) { 305 | topView.alpha = 0 306 | bottomView.alpha = 0 307 | self.view.layoutIfNeeded() 308 | } 309 | } 310 | 311 | private func fadeIn(with duration: TimeInterval = Constants.AnimationDuration.defaultDuration) { 312 | guard let bottomView = self.bottomView, let topView = self.topView else { 313 | return 314 | } 315 | 316 | self.topConstrainTopView?.constant = Constants.Layout.cTopTV 317 | self.bottomConstraintStackView?.constant = Constants.Layout.cBottomBV 318 | 319 | UIView.animate(withDuration: duration) { 320 | topView.alpha = 1 321 | bottomView.alpha = 1 322 | self.view.layoutIfNeeded() 323 | } 324 | } 325 | } 326 | 327 | // *********************************************** 328 | // MARK: UIPageViewControllerDataSource 329 | // *********************************************** 330 | 331 | extension FMImageSlideViewController: UIPageViewControllerDataSource { 332 | public func pageViewController(_ pageViewController: UIPageViewController, 333 | viewControllerBefore viewController: UIViewController) -> UIViewController? { 334 | 335 | let itemController = viewController as! FMImagePreviewViewController 336 | 337 | if itemController.itemIndex > 0 { 338 | return getItemController(itemController.itemIndex - 1) 339 | } 340 | 341 | return nil 342 | } 343 | 344 | public func pageViewController(_ pageViewController: UIPageViewController, 345 | viewControllerAfter viewController: UIViewController) -> UIViewController? { 346 | 347 | let itemController = viewController as! FMImagePreviewViewController 348 | 349 | if itemController.itemIndex + 1 < self.datasource.total() { 350 | return getItemController(itemController.itemIndex + 1) 351 | } 352 | 353 | return nil 354 | 355 | } 356 | } 357 | 358 | // *********************************************** 359 | // MARK: UIPageViewControllerDelegate 360 | // *********************************************** 361 | 362 | extension FMImageSlideViewController: UIPageViewControllerDelegate { 363 | public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { 364 | if let vc = pageViewController.viewControllers?.first as? FMImagePreviewViewController { 365 | self.updateUINumberImageLabel(numerator: vc.itemIndex) 366 | vc.slideStatus = .completed 367 | self.didMoveToViewControllerHandler?(vc.itemIndex) 368 | } 369 | } 370 | 371 | public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { 372 | if let vc = pageViewController.viewControllers?.first as? FMImagePreviewViewController { 373 | vc.slideStatus = .pendding 374 | } 375 | } 376 | } 377 | 378 | extension FMImageSlideViewController: ImageSlideFMDelegate { 379 | func handlingModal(type: TypeName.Modal) { 380 | if type == .md_dismiss { 381 | self.dismiss(animated: true, completion: nil) 382 | } 383 | } 384 | 385 | func handlingSwipe(type: TypeName.Swipe) { 386 | guard let scrollView = self.pageViewController!.view.subviews.filter({$0.isKind(of: UIScrollView.self)}).first as? UIScrollView else { return } 387 | 388 | if type == .enable { 389 | swipeInteractionController?.enable() 390 | scrollView.isScrollEnabled = true 391 | } else { 392 | swipeInteractionController?.disable() 393 | scrollView.isScrollEnabled = false 394 | } 395 | } 396 | 397 | func handlingElasticityOfTopViewAndBottomView(type: TypeName.Elasticity) { 398 | if type == .elasticity_out { 399 | self.fadeOut() 400 | } else { 401 | self.fadeIn() 402 | } 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Views/ImageZoomView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageZoomView.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 6/27/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | 12 | class ImageZoomView: UIScrollView { 13 | 14 | var _imageView: UIImageView? 15 | 16 | fileprivate var _imageSize: CGSize = CGSize.zero 17 | 18 | fileprivate var _pointToCenterAfterResize: CGPoint! 19 | 20 | fileprivate var _scaleToRestoreAfterResize: CGFloat! 21 | 22 | private var initialTouchPoint: CGPoint = CGPoint.zero 23 | 24 | private var _pinchGestureRecognizer: UIPinchGestureRecognizer? 25 | 26 | private var _isPinching: Bool = false 27 | 28 | var config: ConfigureZoomView = ConfigureZoomView(imageContentMode: nil, 29 | initialOffset: nil, 30 | maxScaleFromMinScale: nil) 31 | 32 | var slideStatus: SlideStatus? = .completed 33 | 34 | weak var _delegate: ImagePreviewFMDelegate? 35 | 36 | public init(config: ConfigureZoomView) { 37 | self.config = config 38 | super.init(frame: CGRect.zero) 39 | self.customInit() 40 | } 41 | 42 | override init(frame: CGRect) { 43 | super.init(frame: frame) 44 | self.customInit() 45 | } 46 | 47 | required init?(coder aDecoder: NSCoder) { 48 | super.init(coder: aDecoder) 49 | self.customInit() 50 | } 51 | 52 | private func customInit() { 53 | self.showsVerticalScrollIndicator = false 54 | self.showsHorizontalScrollIndicator = false 55 | self.bouncesZoom = true 56 | self.decelerationRate = UIScrollViewDecelerationRateFast 57 | self.delegate = self 58 | } 59 | 60 | func moveScrollViewFrame(_ sender: UIPanGestureRecognizer) { 61 | guard let window = window else {return} 62 | 63 | let touchPoint = sender.location(in: window) 64 | 65 | switch sender.state { 66 | case .began: 67 | 68 | initialTouchPoint = touchPoint 69 | case .changed: 70 | 71 | frame.origin.y = touchPoint.y - initialTouchPoint.y 72 | case .ended, .cancelled, .failed: 73 | UIView.animate(withDuration: Constants.AnimationDuration.defaultDuration, animations: { 74 | self.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height) 75 | }) 76 | default: 77 | break 78 | } 79 | } 80 | 81 | override func layoutSubviews() { 82 | super.layoutSubviews() 83 | 84 | guard let imageView = self._imageView else { 85 | return 86 | } 87 | 88 | // center the zoom view as it becomes smaller than the size of the screen 89 | let boundsSize: CGSize = self.bounds.size 90 | var frameToCenter: CGRect = imageView.frame 91 | 92 | // center horizontally 93 | if frameToCenter.size.width < boundsSize.width { 94 | frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2 95 | } 96 | else { 97 | frameToCenter.origin.x = 0 98 | } 99 | 100 | // center vertically 101 | if (frameToCenter.size.height < boundsSize.height) { 102 | frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2 103 | } 104 | else { 105 | frameToCenter.origin.y = 0 106 | } 107 | 108 | self._imageView!.frame = frameToCenter 109 | } 110 | 111 | override var frame: CGRect { 112 | willSet{ 113 | // check to see if there is a resize coming. prepare if there is one 114 | let sizeChanging: Bool = !frame.size.equalTo(newValue.size) 115 | if sizeChanging { self.prepareToResize()} 116 | } 117 | didSet { 118 | // check to see if there was a resize. recover if there was one 119 | let sizeChanged: Bool = !frame.size.equalTo(oldValue.size) 120 | if sizeChanged { self.recoverFromResizing() } 121 | } 122 | } 123 | 124 | 125 | 126 | // *********************************************** 127 | // MARK: Configure scrollview to display image 128 | // *********************************************** 129 | 130 | func displayImage(_ image: UIImage) { 131 | 132 | if let _ = self._imageView { 133 | self._imageView?.removeFromSuperview() 134 | self._imageView = nil 135 | } 136 | 137 | self.zoomScale = Constants.Scale.cMin 138 | 139 | self._imageView = UIImageView(image: image) 140 | 141 | _pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(pinchGestureTarget(_:))) 142 | self.addGestureRecognizer(_pinchGestureRecognizer!) 143 | 144 | self.addSubview(self._imageView!) 145 | 146 | 147 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(ImageZoomView.doubleTapGestureRecognizer(_:))) 148 | tapGesture.numberOfTapsRequired = 2 149 | _imageView!.isUserInteractionEnabled = true 150 | _imageView!.addGestureRecognizer(tapGesture) 151 | 152 | self.configureForImageSize(image.size) 153 | } 154 | 155 | @objc func pinchGestureTarget(_ sender: UIPinchGestureRecognizer) { 156 | if sender.state == UIGestureRecognizerState.began { 157 | if _isPinching == false { 158 | _isPinching = true 159 | self._delegate?.notificationHandlingElasticityOfTopViewAndBottomView(type: .elasticity_out) 160 | } 161 | } else if sender.state == UIGestureRecognizerState.changed { 162 | 163 | } else if sender.state == UIGestureRecognizerState.ended || sender.state == UIGestureRecognizerState.cancelled { 164 | if zoomScale > minimumZoomScale { 165 | self._delegate?.notificationHandlingSwipe(type: .disable) 166 | } else { 167 | self._delegate?.notificationHandlingSwipe(type: .enable) 168 | self._delegate?.notificationHandlingElasticityOfTopViewAndBottomView(type: .elasticity_in) 169 | _isPinching = false 170 | } 171 | } 172 | } 173 | 174 | 175 | private func configureForImageSize(_ imageSize: CGSize) { 176 | 177 | _imageSize = imageSize 178 | self.contentSize = imageSize 179 | self.setMaxMinZoomScalesForCurrentBounds() 180 | self.zoomScale = self.minimumZoomScale 181 | 182 | switch self.config._initialOffset { 183 | case .begining: 184 | contentOffset = CGPoint.zero 185 | case .center: 186 | let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width) / 2 187 | let yOffset = contentSize.height < bounds.height ? 0 : (contentSize.height - bounds.height) / 2 188 | 189 | switch self.config._imageContentMode { 190 | case .aspectFit: 191 | contentOffset = CGPoint.zero 192 | case .aspectFill: 193 | contentOffset = CGPoint(x: xOffset, y: yOffset) 194 | case .heightFill: 195 | contentOffset = CGPoint(x: xOffset, y: 0) 196 | case .widthFill: 197 | contentOffset = CGPoint(x: 0, y: yOffset) 198 | } 199 | } 200 | } 201 | 202 | fileprivate func setMaxMinZoomScalesForCurrentBounds() { 203 | // calculate min/max zoomscale 204 | let xScale = bounds.width / _imageSize.width // the scale needed to perfectly fit the image width-wise 205 | let yScale = bounds.height / _imageSize.height // the scale needed to perfectly fit the image height-wise 206 | 207 | var minScale: CGFloat = 1 208 | 209 | switch self.config._imageContentMode { 210 | case .aspectFill: 211 | minScale = max(xScale, yScale) 212 | case .aspectFit: 213 | minScale = min(xScale, yScale) 214 | case .widthFill: 215 | minScale = xScale 216 | case .heightFill: 217 | minScale = yScale 218 | } 219 | 220 | let maxScale = self.config._maxScaleFromMinScale * minScale 221 | 222 | // don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.) 223 | if minScale > maxScale { 224 | minScale = maxScale 225 | } 226 | 227 | maximumZoomScale = maxScale 228 | minimumZoomScale = minScale * 0.999 // the multiply factor to prevent user cannot scroll page while they use this control in UIPageViewController 229 | } 230 | 231 | 232 | 233 | // *********************************************** 234 | // MARK: Configure scrollview to display image 235 | // *********************************************** 236 | 237 | 238 | private func prepareToResize() { 239 | 240 | let boundsCenter: CGPoint = CGPoint(x: self.bounds.midX, y: self.bounds.midY) 241 | _pointToCenterAfterResize = self.convert(boundsCenter, to: self._imageView) 242 | 243 | _scaleToRestoreAfterResize = self.zoomScale 244 | 245 | // If we're at the minimum zoom scale, preserve that by returning 0, which will be converted to the minimum 246 | // allowable scale when the scale is restored. 247 | 248 | if Float(_scaleToRestoreAfterResize) <= Float(self.minimumZoomScale) + .ulpOfOne { 249 | _scaleToRestoreAfterResize = 0 250 | } 251 | 252 | } 253 | 254 | private func recoverFromResizing() { 255 | 256 | self.setMaxMinZoomScalesForCurrentBounds() 257 | 258 | // Step 1: restore zoom scale, first making sure it is within the allowable range. 259 | let maxZoomScale: CGFloat = max(self.minimumZoomScale, _scaleToRestoreAfterResize) 260 | self.zoomScale = min(self.maximumZoomScale, maxZoomScale) 261 | 262 | // Step 2: restore center point, first making sure it is within the allowable range. 263 | 264 | // 2a: convert our desired center point back to our own coordinate space 265 | let boundsCenter: CGPoint = self.convert(_pointToCenterAfterResize, from: self._imageView) 266 | 267 | // 2b: calculate the content offset that would yield that center point 268 | var offset: CGPoint = CGPoint(x: boundsCenter.x - self.bounds.size.width / 2.0, y: boundsCenter.y - self.bounds.size.height / 2.0) 269 | 270 | // 2c: restore offset, adjusted to be within the allowable range 271 | let maxOffset: CGPoint = self.maximumContentOffset() 272 | let minOffset: CGPoint = self.minimumContentOffset() 273 | 274 | var realMaxOffset: CGFloat = min(maxOffset.x, offset.x) 275 | offset.x = max(minOffset.x, realMaxOffset) 276 | 277 | realMaxOffset = min(maxOffset.y, offset.y) 278 | offset.y = max(minOffset.y, realMaxOffset) 279 | 280 | self.contentOffset = offset 281 | 282 | } 283 | 284 | private func maximumContentOffset() -> CGPoint { 285 | let contentSize: CGSize = self.contentSize 286 | let boundsSize: CGSize = self.bounds.size 287 | return CGPoint(x: contentSize.width - boundsSize.width, y: contentSize.height - boundsSize.height) 288 | } 289 | 290 | private func minimumContentOffset() -> CGPoint { 291 | return CGPoint.zero 292 | } 293 | 294 | @objc func doubleTapGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { 295 | // zoom out if it bigger than middle scale point. Else, zoom in 296 | if zoomScale > minimumZoomScale { 297 | setZoomScale(minimumZoomScale, animated: true) 298 | 299 | self._delegate?.notificationHandlingElasticityOfTopViewAndBottomView(type: .elasticity_in) 300 | self._delegate?.notificationHandlingSwipe(type: .enable) 301 | } 302 | else { 303 | let center = gestureRecognizer.location(in: gestureRecognizer.view) 304 | let zoomRect = zoomRectForScale(self.config._maxScaleFromMinScale, center: center) 305 | zoom(to: zoomRect, animated: true) 306 | 307 | self._delegate?.notificationHandlingElasticityOfTopViewAndBottomView(type: .elasticity_out) 308 | self._delegate?.notificationHandlingSwipe(type: .disable) 309 | } 310 | } 311 | 312 | fileprivate func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect { 313 | var zoomRect = CGRect.zero 314 | 315 | // the zoom rect is in the content view's coordinates. 316 | // at a zoom scale of 1.0, it would be the size of the imageScrollView's bounds. 317 | // as the zoom scale decreases, so more content is visible, the size of the rect grows. 318 | zoomRect.size.height = frame.size.height / scale 319 | zoomRect.size.width = frame.size.width / scale 320 | 321 | // choose an origin so as to get the right center. 322 | zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0) 323 | zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0) 324 | 325 | return zoomRect 326 | } 327 | 328 | /// Adjust the position of the image after zooming 329 | func updateImageCenter() { 330 | guard let imageView = _imageView, let image = imageView.image else { return } 331 | 332 | // Find the image size of UIImageView at UIViewContentMode.ScaleAspectFit 333 | let frame = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds) 334 | 335 | var imageSize = CGSize(width: frame.size.width, height: frame.size.height) 336 | imageSize.width *= zoomScale 337 | imageSize.height *= zoomScale 338 | 339 | var point: CGPoint = CGPoint.zero 340 | point.x = imageSize.width / 2 341 | if imageSize.width < bounds.width { 342 | point.x += (bounds.width - imageSize.width) / 2 343 | } 344 | point.y = imageSize.height / 2 345 | if imageSize.height < bounds.height { 346 | point.y += (bounds.height - imageSize.height) / 2 347 | } 348 | _imageView?.center = point 349 | } 350 | 351 | } 352 | 353 | // *********************************************** 354 | // MARK: UIScrollViewDelegate 355 | // *********************************************** 356 | 357 | extension ImageZoomView: UIScrollViewDelegate { 358 | func scrollViewDidZoom(_ scrollView: UIScrollView) { 359 | updateImageCenter() 360 | } 361 | 362 | func viewForZooming(in scrollView: UIScrollView) -> UIView? { 363 | return self._imageView 364 | } 365 | 366 | // MARK: - Scroll delegate 367 | open func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { 368 | scrollView.panGestureRecognizer.isEnabled = true 369 | } 370 | 371 | open func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { 372 | // There is a bug, especially prevalent on iPhone 6 Plus, that causes zooming to render all other gesture recognizers ineffective. 373 | // This bug is fixed by disabling the pan gesture recognizer of the scroll view when it is not needed. 374 | if (scrollView.zoomScale == scrollView.minimumZoomScale) { 375 | scrollView.panGestureRecognizer.isEnabled = false; 376 | } 377 | } 378 | } 379 | 380 | // *********************************************** 381 | // MARK: UIGestureRecognizerDelegate 382 | // *********************************************** 383 | 384 | extension ImageZoomView: UIGestureRecognizerDelegate { 385 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 386 | return true 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /FMImageView/FMImageView/Views/Subviews/HorizontalStackView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalStackView.swift 3 | // FMImageView 4 | // 5 | // Created by Hoang Trong Anh on 7/3/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias FMTuple = (button: UIButton, label: UILabel) 12 | 13 | public class HorizontalStackView: UIStackView { 14 | var items: [FMTuple] = [] 15 | 16 | var _view: UIView? 17 | 18 | var heightStackView: CGFloat = Constants.Layout.cHeightBV 19 | 20 | init() { 21 | super.init(frame: CGRect(x: 0, y: UIScreen.main.bounds.height - heightStackView, width: UIScreen.main.bounds.width, height: heightStackView)) 22 | } 23 | 24 | public convenience init(items: [FMTuple]) { 25 | self.init() 26 | self.items = items 27 | config() 28 | 29 | addItemsToStackView() 30 | } 31 | 32 | public convenience init(view: UIView) { 33 | self.init() 34 | self._view = view 35 | config() 36 | 37 | addViewToStackView() 38 | } 39 | 40 | 41 | required public init(coder: NSCoder) { 42 | super.init(coder: coder) 43 | config() 44 | } 45 | 46 | private func config() { 47 | backgroundColor = .white 48 | alignment = .center 49 | distribution = .equalSpacing 50 | axis = .horizontal 51 | spacing = 0 52 | translatesAutoresizingMaskIntoConstraints = false 53 | } 54 | 55 | private func addViewToStackView() { 56 | guard let view = self._view else { 57 | return 58 | } 59 | 60 | view.translatesAutoresizingMaskIntoConstraints = false 61 | view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 62 | 63 | self.addArrangedSubview(view) 64 | 65 | view.topAnchor.constraint(equalTo: self.topAnchor).isActive = true 66 | } 67 | 68 | private func addItemsToStackView() { 69 | 70 | let kPadding: CGFloat = 10.0 71 | 72 | for value in items { 73 | let stack = UIView() 74 | 75 | stack.heightAnchor.constraint(equalToConstant: heightStackView).isActive = true 76 | stack.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width / CGFloat(self.items.count)).isActive = true 77 | 78 | let subStack = UIView() 79 | subStack.translatesAutoresizingMaskIntoConstraints = false 80 | subStack.heightAnchor.constraint(equalToConstant: heightStackView / 2).isActive = true 81 | 82 | // items of sub view 83 | // ** Buttons ** 84 | value.button.translatesAutoresizingMaskIntoConstraints = false 85 | value.button.heightAnchor.constraint(equalToConstant: heightStackView / 2).isActive = true 86 | value.button.widthAnchor.constraint(equalToConstant: heightStackView / 2).isActive = true 87 | 88 | subStack.addSubview(value.button) 89 | 90 | // ** Labels ** 91 | value.button.leftAnchor.constraint(equalTo: subStack.leftAnchor, constant: 0).isActive = true 92 | 93 | value.label.translatesAutoresizingMaskIntoConstraints = false 94 | value.label.heightAnchor.constraint(equalToConstant: heightStackView / 2).isActive = true 95 | value.label.numberOfLines = 1 96 | value.label.font = UIFont.systemFont(ofSize: 20.0, weight: .bold) 97 | value.label.textColor = .white 98 | 99 | subStack.addSubview(value.label) 100 | 101 | // ** Buttons ** 102 | value.button.leadingAnchor.constraint(equalTo: subStack.leadingAnchor, constant: 0).isActive = true 103 | value.button.centerYAnchor.constraint(equalTo: subStack.centerYAnchor, constant: 0).isActive = true 104 | 105 | // ** Labels ** 106 | value.label.trailingAnchor.constraint(equalTo: subStack.trailingAnchor, constant: 0).isActive = true 107 | value.label.leftAnchor.constraint(equalTo: value.button.rightAnchor, constant: kPadding).isActive = true 108 | value.label.centerYAnchor.constraint(equalTo: subStack.centerYAnchor, constant: 0).isActive = true 109 | 110 | stack.addSubview(subStack) 111 | 112 | // ** subview inside stack ** 113 | subStack.centerYAnchor.constraint(equalTo: stack.centerYAnchor, constant: 0).isActive = true 114 | subStack.centerXAnchor.constraint(equalTo: stack.centerXAnchor, constant: 0).isActive = true 115 | 116 | self.addArrangedSubview(stack) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /FMImageView/FMImageViewTests/FMImageViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FMImageViewTests.swift 3 | // FMImageViewTests 4 | // 5 | // Created by Hoang Trong Anh on 5/23/18. 6 | // Copyright © 2018 Hoang Trong Anh. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import FMImageView 11 | 12 | class FMImageViewTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /FMImageView/FMImageViewTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present, Tribal Media House. 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # FMImageView 4 | 5 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE) 6 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 7 | 8 | **FMImageView** is a slideshow and image viewer with zoom and interactive dismissal transitions. 9 | 10 |

11 | 12 |

13 | 14 | ## Features 15 | - [x] Support image slideshow with pagination 16 | - [x] Support double tapping to zoom in/out 17 | - [x] Support remote image loader with caching 18 | - [x] Support the interactive transition animations 19 | - [x] Support custom bottom view 20 | - [x] Support custom configuration 21 | 22 | ## Requirements 23 | - iOS 9.0+ 24 | 25 | ## Installation 26 | 27 | Insert the following line in your Cartfile: 28 | ``` 29 | git "git@github.com:tribalmedia/FMImageView.git" 30 | ``` 31 | and run `carthage update FMImageView --platform ios --no-use-binaries` 32 | 33 | 34 | ## Usage 35 | #### Create a configuration object 36 | ```swift 37 | var config = Config(initImageView: UIImageView, initIndex: Int) 38 | ``` 39 | For details, see [Configuration](#configuration) 40 | 41 | #### Create a datasource object 42 | ```swift 43 | var datasource = FMImageDataSource(imageURLs: [URL]) 44 | ``` 45 | or 46 | 47 | ```swift 48 | var datasource = FMImageDataSource(images: [UIImage]) 49 | ``` 50 | 51 | ### Controller 52 | ```swift 53 | let fmImageVC = FMImageSlideViewController(datasource: datasource, config: config) 54 | 55 | fmImageVC.view.frame = UIScreen.main.bounds 56 | 57 | self.present(fmImageVC, animated: true) 58 | ``` 59 | 60 | ### Callback 61 | - Implement callback to handle location of images 62 | ```swift 63 | let fmImageVC = FMImageSlideViewController(datasource: datasource, config: config) 64 | fmImageVC.didMoveToViewControllerHandler = { index in 65 | // Mark code get imageView by index 66 | 67 | fmImageVC.setNewDestinatonFrame(imageView: UIImageView) 68 | } 69 | ``` 70 | 71 | ## Configuration 72 | #### The configuration supports the following parameter: 73 | - [`bottomView`](#ref-bottom-view) 74 | 75 | #### Reference 76 | - `bottomView` 77 | It will always show the bottom 78 | Type: `HorizontalStackView` 79 | Default: `nil` 80 | Default height: `40.0` 81 | 82 | ## Apps using FMImageView 83 | 84 | 85 | ## Author 86 | Made by Tribal Media House with ❤️ 87 | 88 | ## License 89 | FMImageView is released under the MIT license. See LICENSE for details. 90 | -------------------------------------------------------------------------------- /resources/FMImageView.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/resources/FMImageView.gif -------------------------------------------------------------------------------- /resources/funmee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tribalmedia/FMImageView/5d68a0e78f14bcff49cca255c930a10a50a49639/resources/funmee.png --------------------------------------------------------------------------------