├── .gitignore ├── Fusion.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── Fusion ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── AppIcon-20.0x20.0@2x.png │ │ ├── AppIcon-20.0x20.0@3x.png │ │ ├── AppIcon-29.0x29.0@2x.png │ │ ├── AppIcon-29.0x29.0@3x.png │ │ ├── AppIcon-40.0x40.0@2x.png │ │ ├── AppIcon-40.0x40.0@3x.png │ │ ├── AppIcon-60.0x60.0@2x.png │ │ ├── AppIcon-60.0x60.0@3x.png │ │ └── Contents.json │ ├── Contents.json │ ├── img_post.imageset │ │ ├── Contents.json │ │ └── img_post@3x.png │ └── img_prev.imageset │ │ ├── Contents.json │ │ └── img_prev@3x.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Extension │ ├── SwiftHEXColors.swift │ ├── UIColor+Fusion.swift │ ├── UIImage+Pixel.h │ └── UIImage+Pixel.m ├── Fusion-Bridging-Header.h ├── Info.plist ├── Util │ ├── DVSFusion.h │ └── DVSFusion.m ├── ViewController │ ├── BaseNavigationController.swift │ ├── BaseViewController.swift │ ├── HomeViewController.swift │ ├── PreviewViewController.swift │ └── PreviewViewController.xib └── Widget │ └── PhotosPicker │ ├── TLAlbumPopView.swift │ ├── TLAssetsCollection.swift │ ├── TLBundle.swift │ ├── TLCollectionTableViewCell.swift │ ├── TLCollectionTableViewCell.xib │ ├── TLPhotoCollectionViewCell.swift │ ├── TLPhotoCollectionViewCell.xib │ ├── TLPhotoLibrary.swift │ ├── TLPhotoPickerController.bundle │ ├── arrow.png │ ├── camera@3x.png │ ├── insertPhotoMaterial@3x.png │ ├── pop_arrow.png │ └── video.png │ ├── TLPhotosPickerViewController.swift │ └── TLPhotosPickerViewController.xib ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | .idea/* 69 | # OS generated files # 70 | ###################### 71 | .DS_Store 72 | -------------------------------------------------------------------------------- /Fusion.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2B72EA671ED80C9C00079173 /* BaseNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B72EA661ED80C9C00079173 /* BaseNavigationController.swift */; }; 11 | 2B72EA691ED80ECB00079173 /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B72EA681ED80ECB00079173 /* BaseViewController.swift */; }; 12 | 2B72EA6B1ED80EE500079173 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B72EA6A1ED80EE500079173 /* HomeViewController.swift */; }; 13 | 2B72EA6F1ED8155900079173 /* SwiftHEXColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B72EA6D1ED8155900079173 /* SwiftHEXColors.swift */; }; 14 | 2B72EA701ED8155900079173 /* UIColor+Fusion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B72EA6E1ED8155900079173 /* UIColor+Fusion.swift */; }; 15 | 2BB738B01ED7ED2F0068EAF0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB738AF1ED7ED2F0068EAF0 /* AppDelegate.swift */; }; 16 | 2BB738B51ED7ED2F0068EAF0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2BB738B31ED7ED2F0068EAF0 /* Main.storyboard */; }; 17 | 2BB738B71ED7ED2F0068EAF0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BB738B61ED7ED2F0068EAF0 /* Assets.xcassets */; }; 18 | 2BB738BA1ED7ED2F0068EAF0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2BB738B81ED7ED2F0068EAF0 /* LaunchScreen.storyboard */; }; 19 | 2BB738D71ED7F32E0068EAF0 /* TLAlbumPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB738CD1ED7F32E0068EAF0 /* TLAlbumPopView.swift */; }; 20 | 2BB738D81ED7F32E0068EAF0 /* TLAssetsCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB738CE1ED7F32E0068EAF0 /* TLAssetsCollection.swift */; }; 21 | 2BB738D91ED7F32E0068EAF0 /* TLBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB738CF1ED7F32E0068EAF0 /* TLBundle.swift */; }; 22 | 2BB738DA1ED7F32E0068EAF0 /* TLCollectionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB738D01ED7F32E0068EAF0 /* TLCollectionTableViewCell.swift */; }; 23 | 2BB738DB1ED7F32E0068EAF0 /* TLCollectionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2BB738D11ED7F32E0068EAF0 /* TLCollectionTableViewCell.xib */; }; 24 | 2BB738DC1ED7F32E0068EAF0 /* TLPhotoCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB738D21ED7F32E0068EAF0 /* TLPhotoCollectionViewCell.swift */; }; 25 | 2BB738DD1ED7F32E0068EAF0 /* TLPhotoCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2BB738D31ED7F32E0068EAF0 /* TLPhotoCollectionViewCell.xib */; }; 26 | 2BB738DE1ED7F32E0068EAF0 /* TLPhotoLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB738D41ED7F32E0068EAF0 /* TLPhotoLibrary.swift */; }; 27 | 2BB738DF1ED7F32E0068EAF0 /* TLPhotosPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB738D51ED7F32E0068EAF0 /* TLPhotosPickerViewController.swift */; }; 28 | 2BB738E01ED7F32E0068EAF0 /* TLPhotosPickerViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2BB738D61ED7F32E0068EAF0 /* TLPhotosPickerViewController.xib */; }; 29 | 2BB738E21ED7FB110068EAF0 /* TLPhotoPickerController.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 2BB738E11ED7FB110068EAF0 /* TLPhotoPickerController.bundle */; }; 30 | 2BB738E61ED8084F0068EAF0 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB738E41ED8084F0068EAF0 /* PreviewViewController.swift */; }; 31 | 2BB738E71ED8084F0068EAF0 /* PreviewViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2BB738E51ED8084F0068EAF0 /* PreviewViewController.xib */; }; 32 | 2BED7B641EDEAB3100645499 /* UIImage+Pixel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2BED7B631EDEAB3100645499 /* UIImage+Pixel.m */; }; 33 | 2BED7B681EDEAB6C00645499 /* DVSFusion.m in Sources */ = {isa = PBXBuildFile; fileRef = 2BED7B671EDEAB6C00645499 /* DVSFusion.m */; }; 34 | 2BED7B6E1EDEABC200645499 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2BED7B6D1EDEABC200645499 /* Info.plist */; }; 35 | /* End PBXBuildFile section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | 2B72EA661ED80C9C00079173 /* BaseNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseNavigationController.swift; sourceTree = ""; }; 39 | 2B72EA681ED80ECB00079173 /* BaseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; }; 40 | 2B72EA6A1ED80EE500079173 /* HomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; 41 | 2B72EA6D1ED8155900079173 /* SwiftHEXColors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftHEXColors.swift; sourceTree = ""; }; 42 | 2B72EA6E1ED8155900079173 /* UIColor+Fusion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Fusion.swift"; sourceTree = ""; }; 43 | 2BB738AC1ED7ED2F0068EAF0 /* Fusion.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Fusion.app; sourceTree = BUILT_PRODUCTS_DIR; }; 44 | 2BB738AF1ED7ED2F0068EAF0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 45 | 2BB738B41ED7ED2F0068EAF0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 46 | 2BB738B61ED7ED2F0068EAF0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 47 | 2BB738B91ED7ED2F0068EAF0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 48 | 2BB738CD1ED7F32E0068EAF0 /* TLAlbumPopView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLAlbumPopView.swift; sourceTree = ""; }; 49 | 2BB738CE1ED7F32E0068EAF0 /* TLAssetsCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLAssetsCollection.swift; sourceTree = ""; }; 50 | 2BB738CF1ED7F32E0068EAF0 /* TLBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLBundle.swift; sourceTree = ""; }; 51 | 2BB738D01ED7F32E0068EAF0 /* TLCollectionTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLCollectionTableViewCell.swift; sourceTree = ""; }; 52 | 2BB738D11ED7F32E0068EAF0 /* TLCollectionTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TLCollectionTableViewCell.xib; sourceTree = ""; }; 53 | 2BB738D21ED7F32E0068EAF0 /* TLPhotoCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLPhotoCollectionViewCell.swift; sourceTree = ""; }; 54 | 2BB738D31ED7F32E0068EAF0 /* TLPhotoCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TLPhotoCollectionViewCell.xib; sourceTree = ""; }; 55 | 2BB738D41ED7F32E0068EAF0 /* TLPhotoLibrary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLPhotoLibrary.swift; sourceTree = ""; }; 56 | 2BB738D51ED7F32E0068EAF0 /* TLPhotosPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLPhotosPickerViewController.swift; sourceTree = ""; }; 57 | 2BB738D61ED7F32E0068EAF0 /* TLPhotosPickerViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TLPhotosPickerViewController.xib; sourceTree = ""; }; 58 | 2BB738E11ED7FB110068EAF0 /* TLPhotoPickerController.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = TLPhotoPickerController.bundle; sourceTree = ""; }; 59 | 2BB738E41ED8084F0068EAF0 /* PreviewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; 60 | 2BB738E51ED8084F0068EAF0 /* PreviewViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PreviewViewController.xib; sourceTree = ""; }; 61 | 2BED7B621EDEAB3100645499 /* UIImage+Pixel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+Pixel.h"; sourceTree = ""; }; 62 | 2BED7B631EDEAB3100645499 /* UIImage+Pixel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+Pixel.m"; sourceTree = ""; }; 63 | 2BED7B661EDEAB6C00645499 /* DVSFusion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DVSFusion.h; sourceTree = ""; }; 64 | 2BED7B671EDEAB6C00645499 /* DVSFusion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DVSFusion.m; sourceTree = ""; }; 65 | 2BED7B6D1EDEABC200645499 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 66 | 2BED7B6F1EDEAC3300645499 /* Fusion-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Fusion-Bridging-Header.h"; sourceTree = ""; }; 67 | /* End PBXFileReference section */ 68 | 69 | /* Begin PBXFrameworksBuildPhase section */ 70 | 2BB738A91ED7ED2F0068EAF0 /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXFrameworksBuildPhase section */ 78 | 79 | /* Begin PBXGroup section */ 80 | 2B72EA6C1ED8155900079173 /* Extension */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 2BED7B621EDEAB3100645499 /* UIImage+Pixel.h */, 84 | 2BED7B631EDEAB3100645499 /* UIImage+Pixel.m */, 85 | 2B72EA6D1ED8155900079173 /* SwiftHEXColors.swift */, 86 | 2B72EA6E1ED8155900079173 /* UIColor+Fusion.swift */, 87 | ); 88 | path = Extension; 89 | sourceTree = ""; 90 | }; 91 | 2BB738A31ED7ED2F0068EAF0 = { 92 | isa = PBXGroup; 93 | children = ( 94 | 2BB738AE1ED7ED2F0068EAF0 /* Fusion */, 95 | 2BB738AD1ED7ED2F0068EAF0 /* Products */, 96 | ); 97 | sourceTree = ""; 98 | }; 99 | 2BB738AD1ED7ED2F0068EAF0 /* Products */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 2BB738AC1ED7ED2F0068EAF0 /* Fusion.app */, 103 | ); 104 | name = Products; 105 | sourceTree = ""; 106 | }; 107 | 2BB738AE1ED7ED2F0068EAF0 /* Fusion */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 2BED7B651EDEAB5800645499 /* Util */, 111 | 2B72EA6C1ED8155900079173 /* Extension */, 112 | 2BB738CB1ED7F2770068EAF0 /* Widget */, 113 | 2BB738E31ED808300068EAF0 /* ViewController */, 114 | 2BB738AF1ED7ED2F0068EAF0 /* AppDelegate.swift */, 115 | 2BB738B31ED7ED2F0068EAF0 /* Main.storyboard */, 116 | 2BED7B6D1EDEABC200645499 /* Info.plist */, 117 | 2BED7B6F1EDEAC3300645499 /* Fusion-Bridging-Header.h */, 118 | 2BB738B61ED7ED2F0068EAF0 /* Assets.xcassets */, 119 | 2BB738B81ED7ED2F0068EAF0 /* LaunchScreen.storyboard */, 120 | ); 121 | path = Fusion; 122 | sourceTree = ""; 123 | }; 124 | 2BB738CB1ED7F2770068EAF0 /* Widget */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 2BB738CC1ED7F2770068EAF0 /* PhotosPicker */, 128 | ); 129 | path = Widget; 130 | sourceTree = ""; 131 | }; 132 | 2BB738CC1ED7F2770068EAF0 /* PhotosPicker */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 2BB738E11ED7FB110068EAF0 /* TLPhotoPickerController.bundle */, 136 | 2BB738CD1ED7F32E0068EAF0 /* TLAlbumPopView.swift */, 137 | 2BB738CE1ED7F32E0068EAF0 /* TLAssetsCollection.swift */, 138 | 2BB738CF1ED7F32E0068EAF0 /* TLBundle.swift */, 139 | 2BB738D01ED7F32E0068EAF0 /* TLCollectionTableViewCell.swift */, 140 | 2BB738D11ED7F32E0068EAF0 /* TLCollectionTableViewCell.xib */, 141 | 2BB738D21ED7F32E0068EAF0 /* TLPhotoCollectionViewCell.swift */, 142 | 2BB738D31ED7F32E0068EAF0 /* TLPhotoCollectionViewCell.xib */, 143 | 2BB738D41ED7F32E0068EAF0 /* TLPhotoLibrary.swift */, 144 | 2BB738D51ED7F32E0068EAF0 /* TLPhotosPickerViewController.swift */, 145 | 2BB738D61ED7F32E0068EAF0 /* TLPhotosPickerViewController.xib */, 146 | ); 147 | path = PhotosPicker; 148 | sourceTree = ""; 149 | }; 150 | 2BB738E31ED808300068EAF0 /* ViewController */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 2B72EA661ED80C9C00079173 /* BaseNavigationController.swift */, 154 | 2B72EA681ED80ECB00079173 /* BaseViewController.swift */, 155 | 2B72EA6A1ED80EE500079173 /* HomeViewController.swift */, 156 | 2BB738E41ED8084F0068EAF0 /* PreviewViewController.swift */, 157 | 2BB738E51ED8084F0068EAF0 /* PreviewViewController.xib */, 158 | ); 159 | path = ViewController; 160 | sourceTree = ""; 161 | }; 162 | 2BED7B651EDEAB5800645499 /* Util */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 2BED7B661EDEAB6C00645499 /* DVSFusion.h */, 166 | 2BED7B671EDEAB6C00645499 /* DVSFusion.m */, 167 | ); 168 | path = Util; 169 | sourceTree = ""; 170 | }; 171 | /* End PBXGroup section */ 172 | 173 | /* Begin PBXNativeTarget section */ 174 | 2BB738AB1ED7ED2F0068EAF0 /* Fusion */ = { 175 | isa = PBXNativeTarget; 176 | buildConfigurationList = 2BB738BE1ED7ED2F0068EAF0 /* Build configuration list for PBXNativeTarget "Fusion" */; 177 | buildPhases = ( 178 | 2BB738A81ED7ED2F0068EAF0 /* Sources */, 179 | 2BB738A91ED7ED2F0068EAF0 /* Frameworks */, 180 | 2BB738AA1ED7ED2F0068EAF0 /* Resources */, 181 | ); 182 | buildRules = ( 183 | ); 184 | dependencies = ( 185 | ); 186 | name = Fusion; 187 | productName = Fusion; 188 | productReference = 2BB738AC1ED7ED2F0068EAF0 /* Fusion.app */; 189 | productType = "com.apple.product-type.application"; 190 | }; 191 | /* End PBXNativeTarget section */ 192 | 193 | /* Begin PBXProject section */ 194 | 2BB738A41ED7ED2F0068EAF0 /* Project object */ = { 195 | isa = PBXProject; 196 | attributes = { 197 | LastSwiftUpdateCheck = 0830; 198 | LastUpgradeCheck = 0830; 199 | ORGANIZATIONNAME = DevinShine; 200 | TargetAttributes = { 201 | 2BB738AB1ED7ED2F0068EAF0 = { 202 | CreatedOnToolsVersion = 8.3.1; 203 | DevelopmentTeam = 8GU4AAQG3C; 204 | LastSwiftMigration = 0830; 205 | ProvisioningStyle = Automatic; 206 | }; 207 | }; 208 | }; 209 | buildConfigurationList = 2BB738A71ED7ED2F0068EAF0 /* Build configuration list for PBXProject "Fusion" */; 210 | compatibilityVersion = "Xcode 3.2"; 211 | developmentRegion = English; 212 | hasScannedForEncodings = 0; 213 | knownRegions = ( 214 | en, 215 | Base, 216 | ); 217 | mainGroup = 2BB738A31ED7ED2F0068EAF0; 218 | productRefGroup = 2BB738AD1ED7ED2F0068EAF0 /* Products */; 219 | projectDirPath = ""; 220 | projectRoot = ""; 221 | targets = ( 222 | 2BB738AB1ED7ED2F0068EAF0 /* Fusion */, 223 | ); 224 | }; 225 | /* End PBXProject section */ 226 | 227 | /* Begin PBXResourcesBuildPhase section */ 228 | 2BB738AA1ED7ED2F0068EAF0 /* Resources */ = { 229 | isa = PBXResourcesBuildPhase; 230 | buildActionMask = 2147483647; 231 | files = ( 232 | 2BB738BA1ED7ED2F0068EAF0 /* LaunchScreen.storyboard in Resources */, 233 | 2BB738DB1ED7F32E0068EAF0 /* TLCollectionTableViewCell.xib in Resources */, 234 | 2BB738DD1ED7F32E0068EAF0 /* TLPhotoCollectionViewCell.xib in Resources */, 235 | 2BB738E71ED8084F0068EAF0 /* PreviewViewController.xib in Resources */, 236 | 2BB738B71ED7ED2F0068EAF0 /* Assets.xcassets in Resources */, 237 | 2BB738E01ED7F32E0068EAF0 /* TLPhotosPickerViewController.xib in Resources */, 238 | 2BB738B51ED7ED2F0068EAF0 /* Main.storyboard in Resources */, 239 | 2BED7B6E1EDEABC200645499 /* Info.plist in Resources */, 240 | 2BB738E21ED7FB110068EAF0 /* TLPhotoPickerController.bundle in Resources */, 241 | ); 242 | runOnlyForDeploymentPostprocessing = 0; 243 | }; 244 | /* End PBXResourcesBuildPhase section */ 245 | 246 | /* Begin PBXSourcesBuildPhase section */ 247 | 2BB738A81ED7ED2F0068EAF0 /* Sources */ = { 248 | isa = PBXSourcesBuildPhase; 249 | buildActionMask = 2147483647; 250 | files = ( 251 | 2B72EA671ED80C9C00079173 /* BaseNavigationController.swift in Sources */, 252 | 2BED7B641EDEAB3100645499 /* UIImage+Pixel.m in Sources */, 253 | 2BB738DF1ED7F32E0068EAF0 /* TLPhotosPickerViewController.swift in Sources */, 254 | 2B72EA701ED8155900079173 /* UIColor+Fusion.swift in Sources */, 255 | 2BB738DE1ED7F32E0068EAF0 /* TLPhotoLibrary.swift in Sources */, 256 | 2B72EA6F1ED8155900079173 /* SwiftHEXColors.swift in Sources */, 257 | 2BB738DC1ED7F32E0068EAF0 /* TLPhotoCollectionViewCell.swift in Sources */, 258 | 2BED7B681EDEAB6C00645499 /* DVSFusion.m in Sources */, 259 | 2B72EA6B1ED80EE500079173 /* HomeViewController.swift in Sources */, 260 | 2B72EA691ED80ECB00079173 /* BaseViewController.swift in Sources */, 261 | 2BB738DA1ED7F32E0068EAF0 /* TLCollectionTableViewCell.swift in Sources */, 262 | 2BB738D71ED7F32E0068EAF0 /* TLAlbumPopView.swift in Sources */, 263 | 2BB738D81ED7F32E0068EAF0 /* TLAssetsCollection.swift in Sources */, 264 | 2BB738D91ED7F32E0068EAF0 /* TLBundle.swift in Sources */, 265 | 2BB738B01ED7ED2F0068EAF0 /* AppDelegate.swift in Sources */, 266 | 2BB738E61ED8084F0068EAF0 /* PreviewViewController.swift in Sources */, 267 | ); 268 | runOnlyForDeploymentPostprocessing = 0; 269 | }; 270 | /* End PBXSourcesBuildPhase section */ 271 | 272 | /* Begin PBXVariantGroup section */ 273 | 2BB738B31ED7ED2F0068EAF0 /* Main.storyboard */ = { 274 | isa = PBXVariantGroup; 275 | children = ( 276 | 2BB738B41ED7ED2F0068EAF0 /* Base */, 277 | ); 278 | name = Main.storyboard; 279 | sourceTree = ""; 280 | }; 281 | 2BB738B81ED7ED2F0068EAF0 /* LaunchScreen.storyboard */ = { 282 | isa = PBXVariantGroup; 283 | children = ( 284 | 2BB738B91ED7ED2F0068EAF0 /* Base */, 285 | ); 286 | name = LaunchScreen.storyboard; 287 | sourceTree = ""; 288 | }; 289 | /* End PBXVariantGroup section */ 290 | 291 | /* Begin XCBuildConfiguration section */ 292 | 2BB738BC1ED7ED2F0068EAF0 /* Debug */ = { 293 | isa = XCBuildConfiguration; 294 | buildSettings = { 295 | ALWAYS_SEARCH_USER_PATHS = NO; 296 | CLANG_ANALYZER_NONNULL = YES; 297 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 298 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 299 | CLANG_CXX_LIBRARY = "libc++"; 300 | CLANG_ENABLE_MODULES = YES; 301 | CLANG_ENABLE_OBJC_ARC = YES; 302 | CLANG_WARN_BOOL_CONVERSION = YES; 303 | CLANG_WARN_CONSTANT_CONVERSION = YES; 304 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 305 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 306 | CLANG_WARN_EMPTY_BODY = YES; 307 | CLANG_WARN_ENUM_CONVERSION = YES; 308 | CLANG_WARN_INFINITE_RECURSION = YES; 309 | CLANG_WARN_INT_CONVERSION = YES; 310 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 311 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 312 | CLANG_WARN_UNREACHABLE_CODE = YES; 313 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 314 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 315 | COPY_PHASE_STRIP = NO; 316 | DEBUG_INFORMATION_FORMAT = dwarf; 317 | ENABLE_STRICT_OBJC_MSGSEND = YES; 318 | ENABLE_TESTABILITY = YES; 319 | GCC_C_LANGUAGE_STANDARD = gnu99; 320 | GCC_DYNAMIC_NO_PIC = NO; 321 | GCC_NO_COMMON_BLOCKS = YES; 322 | GCC_OPTIMIZATION_LEVEL = 0; 323 | GCC_PREPROCESSOR_DEFINITIONS = ( 324 | "DEBUG=1", 325 | "$(inherited)", 326 | ); 327 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 328 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 329 | GCC_WARN_UNDECLARED_SELECTOR = YES; 330 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 331 | GCC_WARN_UNUSED_FUNCTION = YES; 332 | GCC_WARN_UNUSED_VARIABLE = YES; 333 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 334 | MTL_ENABLE_DEBUG_INFO = YES; 335 | ONLY_ACTIVE_ARCH = YES; 336 | SDKROOT = iphoneos; 337 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 338 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 339 | }; 340 | name = Debug; 341 | }; 342 | 2BB738BD1ED7ED2F0068EAF0 /* Release */ = { 343 | isa = XCBuildConfiguration; 344 | buildSettings = { 345 | ALWAYS_SEARCH_USER_PATHS = NO; 346 | CLANG_ANALYZER_NONNULL = YES; 347 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 348 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 349 | CLANG_CXX_LIBRARY = "libc++"; 350 | CLANG_ENABLE_MODULES = YES; 351 | CLANG_ENABLE_OBJC_ARC = YES; 352 | CLANG_WARN_BOOL_CONVERSION = YES; 353 | CLANG_WARN_CONSTANT_CONVERSION = 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_OBJC_ROOT_CLASS = YES_ERROR; 361 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 362 | CLANG_WARN_UNREACHABLE_CODE = YES; 363 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 364 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 365 | COPY_PHASE_STRIP = NO; 366 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 367 | ENABLE_NS_ASSERTIONS = NO; 368 | ENABLE_STRICT_OBJC_MSGSEND = YES; 369 | GCC_C_LANGUAGE_STANDARD = gnu99; 370 | GCC_NO_COMMON_BLOCKS = YES; 371 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 372 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 373 | GCC_WARN_UNDECLARED_SELECTOR = YES; 374 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 375 | GCC_WARN_UNUSED_FUNCTION = YES; 376 | GCC_WARN_UNUSED_VARIABLE = YES; 377 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 378 | MTL_ENABLE_DEBUG_INFO = NO; 379 | SDKROOT = iphoneos; 380 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 381 | VALIDATE_PRODUCT = YES; 382 | }; 383 | name = Release; 384 | }; 385 | 2BB738BF1ED7ED2F0068EAF0 /* Debug */ = { 386 | isa = XCBuildConfiguration; 387 | buildSettings = { 388 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 389 | CLANG_ENABLE_MODULES = YES; 390 | DEVELOPMENT_TEAM = 8GU4AAQG3C; 391 | INFOPLIST_FILE = "$(SRCROOT)/Fusion/Info.plist"; 392 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 393 | PRODUCT_BUNDLE_IDENTIFIER = com.shadev.Fusion; 394 | PRODUCT_NAME = "$(TARGET_NAME)"; 395 | SWIFT_OBJC_BRIDGING_HEADER = "Fusion/Fusion-Bridging-Header.h"; 396 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 397 | SWIFT_VERSION = 3.0; 398 | }; 399 | name = Debug; 400 | }; 401 | 2BB738C01ED7ED2F0068EAF0 /* Release */ = { 402 | isa = XCBuildConfiguration; 403 | buildSettings = { 404 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 405 | CLANG_ENABLE_MODULES = YES; 406 | DEVELOPMENT_TEAM = 8GU4AAQG3C; 407 | INFOPLIST_FILE = "$(SRCROOT)/Fusion/Info.plist"; 408 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 409 | PRODUCT_BUNDLE_IDENTIFIER = com.shadev.Fusion; 410 | PRODUCT_NAME = "$(TARGET_NAME)"; 411 | SWIFT_OBJC_BRIDGING_HEADER = "Fusion/Fusion-Bridging-Header.h"; 412 | SWIFT_VERSION = 3.0; 413 | }; 414 | name = Release; 415 | }; 416 | /* End XCBuildConfiguration section */ 417 | 418 | /* Begin XCConfigurationList section */ 419 | 2BB738A71ED7ED2F0068EAF0 /* Build configuration list for PBXProject "Fusion" */ = { 420 | isa = XCConfigurationList; 421 | buildConfigurations = ( 422 | 2BB738BC1ED7ED2F0068EAF0 /* Debug */, 423 | 2BB738BD1ED7ED2F0068EAF0 /* Release */, 424 | ); 425 | defaultConfigurationIsVisible = 0; 426 | defaultConfigurationName = Release; 427 | }; 428 | 2BB738BE1ED7ED2F0068EAF0 /* Build configuration list for PBXNativeTarget "Fusion" */ = { 429 | isa = XCConfigurationList; 430 | buildConfigurations = ( 431 | 2BB738BF1ED7ED2F0068EAF0 /* Debug */, 432 | 2BB738C01ED7ED2F0068EAF0 /* Release */, 433 | ); 434 | defaultConfigurationIsVisible = 0; 435 | defaultConfigurationName = Release; 436 | }; 437 | /* End XCConfigurationList section */ 438 | }; 439 | rootObject = 2BB738A41ED7ED2F0068EAF0 /* Project object */; 440 | } 441 | -------------------------------------------------------------------------------- /Fusion.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Fusion/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Fusion 4 | // 5 | // Created by DevinShine on 2017/5/26. 6 | // Copyright © 2017年 DevinShine. 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 | UIApplication.shared.statusBarStyle = .lightContent 19 | // Override point for customization after application launch. 20 | return true 21 | } 22 | 23 | func applicationWillResignActive(_ application: UIApplication) { 24 | // 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. 25 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 26 | } 27 | 28 | func applicationDidEnterBackground(_ application: UIApplication) { 29 | // 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. 30 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 31 | } 32 | 33 | func applicationWillEnterForeground(_ application: UIApplication) { 34 | // 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. 35 | } 36 | 37 | func applicationDidBecomeActive(_ application: UIApplication) { 38 | // 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. 39 | } 40 | 41 | func applicationWillTerminate(_ application: UIApplication) { 42 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 43 | } 44 | 45 | 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-20.0x20.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-20.0x20.0@2x.png -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-20.0x20.0@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-20.0x20.0@3x.png -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-29.0x29.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-29.0x29.0@2x.png -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-29.0x29.0@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-29.0x29.0@3x.png -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-40.0x40.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-40.0x40.0@2x.png -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-40.0x40.0@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-40.0x40.0@3x.png -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-60.0x60.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-60.0x60.0@2x.png -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-60.0x60.0@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Assets.xcassets/AppIcon.appiconset/AppIcon-60.0x60.0@3x.png -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "appicon", 4 | "version" : 1 5 | }, 6 | "images" : [ 7 | { 8 | "size" : "20x20", 9 | "filename" : "AppIcon-20.0x20.0@2x.png", 10 | "idiom" : "iphone", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "size" : "20x20", 15 | "filename" : "AppIcon-20.0x20.0@3x.png", 16 | "idiom" : "iphone", 17 | "scale" : "3x" 18 | }, 19 | { 20 | "size" : "29x29", 21 | "filename" : "AppIcon-29.0x29.0@2x.png", 22 | "idiom" : "iphone", 23 | "scale" : "2x" 24 | }, 25 | { 26 | "size" : "29x29", 27 | "filename" : "AppIcon-29.0x29.0@3x.png", 28 | "idiom" : "iphone", 29 | "scale" : "3x" 30 | }, 31 | { 32 | "size" : "40x40", 33 | "filename" : "AppIcon-40.0x40.0@2x.png", 34 | "idiom" : "iphone", 35 | "scale" : "2x" 36 | }, 37 | { 38 | "size" : "40x40", 39 | "filename" : "AppIcon-40.0x40.0@3x.png", 40 | "idiom" : "iphone", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "size" : "60x60", 45 | "filename" : "AppIcon-60.0x60.0@2x.png", 46 | "idiom" : "iphone", 47 | "scale" : "2x" 48 | }, 49 | { 50 | "size" : "60x60", 51 | "filename" : "AppIcon-60.0x60.0@3x.png", 52 | "idiom" : "iphone", 53 | "scale" : "3x" 54 | } 55 | ] 56 | } -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/img_post.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "img_post@3x.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/img_post.imageset/img_post@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Assets.xcassets/img_post.imageset/img_post@3x.png -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/img_prev.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "img_prev@3x.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Fusion/Assets.xcassets/img_prev.imageset/img_prev@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Assets.xcassets/img_prev.imageset/img_prev@3x.png -------------------------------------------------------------------------------- /Fusion/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 | 27 | 28 | -------------------------------------------------------------------------------- /Fusion/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 | 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 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /Fusion/Extension/SwiftHEXColors.swift: -------------------------------------------------------------------------------- 1 | // SwiftHEXColors.swift 2 | // 3 | // Copyright (c) 2014 Doan Truong Thi 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | #if os(iOS) || os(tvOS) 24 | import UIKit 25 | typealias SWColor = UIColor 26 | #else 27 | import Cocoa 28 | typealias SWColor = NSColor 29 | #endif 30 | 31 | private extension Int { 32 | func duplicate4bits() -> Int { 33 | return (self << 4) + self 34 | } 35 | } 36 | 37 | /// An extension of UIColor (on iOS) or NSColor (on OSX) providing HEX color handling. 38 | public extension SWColor { 39 | /** 40 | Create non-autoreleased color with in the given hex string. Alpha will be set as 1 by default. 41 | - parameter hexString: The hex string, with or without the hash character. 42 | - returns: A color with the given hex string. 43 | */ 44 | public convenience init?(hexString: String) { 45 | self.init(hexString: hexString, alpha: 1.0) 46 | } 47 | 48 | fileprivate convenience init?(hex3: Int, alpha: Float) { 49 | self.init(red: CGFloat( ((hex3 & 0xF00) >> 8).duplicate4bits() ) / 255.0, 50 | green: CGFloat( ((hex3 & 0x0F0) >> 4).duplicate4bits() ) / 255.0, 51 | blue: CGFloat( ((hex3 & 0x00F) >> 0).duplicate4bits() ) / 255.0, alpha: CGFloat(alpha)) 52 | } 53 | 54 | fileprivate convenience init?(hex6: Int, alpha: Float) { 55 | self.init(red: CGFloat( (hex6 & 0xFF0000) >> 16 ) / 255.0, 56 | green: CGFloat( (hex6 & 0x00FF00) >> 8 ) / 255.0, 57 | blue: CGFloat( (hex6 & 0x0000FF) >> 0 ) / 255.0, alpha: CGFloat(alpha)) 58 | } 59 | 60 | /** 61 | Create non-autoreleased color with in the given hex string and alpha. 62 | - parameter hexString: The hex string, with or without the hash character. 63 | - parameter alpha: The alpha value, a floating value between 0 and 1. 64 | - returns: A color with the given hex string and alpha. 65 | */ 66 | public convenience init?(hexString: String, alpha: Float) { 67 | var hex = hexString 68 | 69 | // Check for hash and remove the hash 70 | if hex.hasPrefix("#") { 71 | hex = hex.substring(from: hex.index(hex.startIndex, offsetBy: 1)) 72 | } 73 | 74 | guard let hexVal = Int(hex, radix: 16) else { 75 | self.init() 76 | return nil 77 | } 78 | 79 | switch hex.characters.count { 80 | case 3: 81 | self.init(hex3: hexVal, alpha: alpha) 82 | case 6: 83 | self.init(hex6: hexVal, alpha: alpha) 84 | default: 85 | // Note: 86 | // The swift 1.1 compiler is currently unable to destroy partially initialized classes in all cases, 87 | // so it disallows formation of a situation where it would have to. We consider this a bug to be fixed 88 | // in future releases, not a feature. -- Apple Forum 89 | self.init() 90 | return nil 91 | } 92 | } 93 | 94 | /** 95 | Create non-autoreleased color with in the given hex value. Alpha will be set as 1 by default. 96 | - parameter hex: The hex value. For example: 0xff8942 (no quotation). 97 | - returns: A color with the given hex value 98 | */ 99 | public convenience init?(hex: Int) { 100 | self.init(hex: hex, alpha: 1.0) 101 | } 102 | 103 | /** 104 | Create non-autoreleased color with in the given hex value and alpha 105 | - parameter hex: The hex value. For example: 0xff8942 (no quotation). 106 | - parameter alpha: The alpha value, a floating value between 0 and 1. 107 | - returns: color with the given hex value and alpha 108 | */ 109 | public convenience init?(hex: Int, alpha: Float) { 110 | if (0x000000 ... 0xFFFFFF) ~= hex { 111 | self.init(hex6: hex , alpha: alpha) 112 | } else { 113 | self.init() 114 | return nil 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Fusion/Extension/UIColor+Fusion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Neat.swift 3 | // Neat 4 | // 5 | // Created by DevinShine on 16/10/24. 6 | // Copyright © 2016年 DevinShine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | extension UIColor { 11 | open class var mainBackgroundColor: UIColor { 12 | return UIColor(hex: 0x211E3B)! 13 | } 14 | 15 | open class var mainColor: UIColor { 16 | return UIColor(hex: 0xFD4A6C)! 17 | } 18 | 19 | open class var textLevel1Color: UIColor { 20 | return UIColor(hex: 0x3C3C3C)! 21 | } 22 | 23 | open class var textLevel2Color: UIColor { 24 | return UIColor(hex: 0x555555)! 25 | } 26 | 27 | open class var textLevel3Color: UIColor { 28 | return UIColor(hex: 0xA7A7A7)! 29 | } 30 | 31 | func imageFromColor() -> UIImage { 32 | let rect = CGRect(x: 0, y: 0, width: 1, height: 1) 33 | UIGraphicsBeginImageContext(rect.size) 34 | let context = UIGraphicsGetCurrentContext() 35 | context!.setFillColor(self.cgColor) 36 | context!.fill(rect) 37 | let image = UIGraphicsGetImageFromCurrentImageContext() 38 | UIGraphicsEndImageContext() 39 | return image! 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Fusion/Extension/UIImage+Pixel.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Pixel.h 3 | // Fusion 4 | // 5 | // Created by DevinShine on 2017/5/26. 6 | // Copyright © 2017年 DevinShine. All rights reserved. 7 | // 8 | 9 | #import 10 | struct PixelData 11 | { 12 | unsigned char *rawData; 13 | int count; 14 | int width; 15 | int height; 16 | int endIndex; 17 | }; 18 | @interface UIImage (Pixel) 19 | - (struct PixelData)pixelData; 20 | @end 21 | -------------------------------------------------------------------------------- /Fusion/Extension/UIImage+Pixel.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Pixel.m 3 | // Fusion 4 | // 5 | // Created by DevinShine on 2017/5/26. 6 | // Copyright © 2017年 DevinShine. All rights reserved. 7 | // 8 | 9 | #import "UIImage+Pixel.h" 10 | 11 | 12 | @implementation UIImage (Pixel) 13 | 14 | - (struct PixelData)pixelData { 15 | CGImageRef imageRef = [self CGImage]; 16 | CGSize size = self.size; 17 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 18 | unsigned char *rawData = (unsigned char*) calloc(size.height * size.width * 4, sizeof(unsigned char)); 19 | NSUInteger bytesPerPixel = 4; 20 | NSUInteger bytesPerRow = bytesPerPixel * size.width; 21 | NSUInteger bitsPerComponent = 8; 22 | CGContextRef context = CGBitmapContextCreate(rawData, size.width, size.height, 23 | bitsPerComponent, bytesPerRow, colorSpace, 24 | kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); 25 | CGColorSpaceRelease(colorSpace); 26 | CGContextDrawImage(context, CGRectMake(0, 0, size.width, size.height), imageRef); 27 | CGContextRelease(context); 28 | 29 | struct PixelData pd; 30 | pd.rawData = rawData; 31 | pd.count = size.height * size.width * 4; 32 | pd.height = size.height; 33 | pd.width = size.width; 34 | pd.endIndex = pd.count; 35 | return pd; 36 | } 37 | @end 38 | -------------------------------------------------------------------------------- /Fusion/Fusion-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | #import "DVSFusion.h" 5 | -------------------------------------------------------------------------------- /Fusion/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSCameraUsageDescription 24 | You need Usage Camera Description 25 | NSPhotoLibraryUsageDescription 26 | You need Usage Photo Description 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UIViewControllerBasedStatusBarAppearance 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Fusion/Util/DVSFusion.h: -------------------------------------------------------------------------------- 1 | // 2 | // DVSFusion.h 3 | // Fusion 4 | // 5 | // Created by DevinShine on 2017/5/26. 6 | // Copyright © 2017年 DevinShine. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DVSFusion : NSObject 12 | 13 | + (UIImage *)mergeImages:(NSArray *) images; 14 | @end 15 | -------------------------------------------------------------------------------- /Fusion/Util/DVSFusion.m: -------------------------------------------------------------------------------- 1 | // 2 | // DVSFusion.m 3 | // Fusion 4 | // 长图拼接功能实现 5 | // 应用场景,手Q聊天截图的长图合并 6 | // 7 | // 8 | // Data Divide 9 | // 10 | // ┌─────────────────────┐ 11 | // │ Top(Navigation) │ 12 | // │ │ 13 | // ├─────────────────────┤ 14 | // │ │ 15 | // │ │ 16 | // │ │ 17 | // │ │ 18 | // │ │ 19 | // │ Data │ 20 | // │ │ 21 | // │ │ 22 | // │ │ 23 | // │ │ 24 | // │ │ 25 | // ├─────────────────────┤ 26 | // │ Bottom │ 27 | // │ │ 28 | // └─────────────────────┘ 29 | // 30 | // 31 | // 32 | // 33 | // Example 34 | // 35 | // 36 | // Prev Post 37 | // 38 | // ┌─────────────────────┐ ┌─────────────────────┐ 39 | // │ Top(Navigation) │ │ Top(Navigation) │ 40 | // │ │ │ │ 41 | // ├─────────────────────┤ ├─────────────────────┤ 42 | // │ ┌───────┐ │ │ ┌───────┐ │ 43 | // │ │ Hi │ │ │ │ 456 │ │ 44 | // │ └───────┘ │ │ └───────┘ │ 45 | // │ ┌───────┐ │ │ ┌───────┐ │ 46 | // │ │ 123 │ │ │ │ 789 │ │ 47 | // │ └───────┘ │ │ └───────┘ │ 48 | // │ ┌───────┐ │ │ ┌───────┐ │ 49 | // │ │ 321 │ │ │ │ 101 │ │ 50 | // │ └───────┘ │ │ └───────┘ │ 51 | // │ ┌───────┐ │ │ ┌───────┐ │ 52 | // │ │ 456 │ │ │ │ 189 │ │ 53 | // ├─┴───────┴───────────┤ ├───────────┴───────┴─┤ 54 | // │ Bottom │ │ Bottom │ 55 | // │ │ │ │ 56 | // └─────────────────────┘ └─────────────────────┘ 57 | // │ │ 58 | // │ Merge │ 59 | // └────────────┬────────────┘ 60 | // │ 61 | // │ 62 | // ▼ 63 | // 64 | // Result 65 | // 66 | // ┌─────────────────────┐ 67 | // │ Top(Navigation) │ 68 | // │ │ 69 | // ├─────────────────────┤ 70 | // │ ┌───────┐ │ 71 | // │ │ Hi │ │ 72 | // │ └───────┘ │ 73 | // │ ┌───────┐ │ 74 | // │ │ 123 │ │ 75 | // │ └───────┘ │ 76 | // │ ┌───────┐ │ 77 | // │ │ 321 │ │ 78 | // │ └───────┘ │ 79 | // │ ┌───────┐ │ 80 | // │ │ 456 │ │ 81 | // │ └───────┘ │ 82 | // │ ┌───────┐ │ 83 | // │ │ 789 │ │ 84 | // │ └───────┘ │ 85 | // │ ┌───────┐ │ 86 | // │ │ 101 │ │ 87 | // │ └───────┘ │ 88 | // │ ┌───────┐ │ 89 | // │ │ 189 │ │ 90 | // ├───────────┴───────┴─┤ 91 | // │ Bottom │ 92 | // │ │ 93 | // └─────────────────────┘ 94 | // Created by DevinShine on 2017/5/26. 95 | // Copyright © 2017年 DevinShine. All rights reserved. 96 | // 97 | 98 | #import "DVSFusion.h" 99 | #import "UIImage+Pixel.h" 100 | 101 | @implementation DVSFusion 102 | 103 | // min bottom height 104 | int minBottomHeight = 49; 105 | int minDuplicateRows = 300; 106 | 107 | + (UIImage *)mergeImages:(NSArray *)images { 108 | if (images == nil) { 109 | return nil; 110 | } else if (images.count == 1) { 111 | return images[0]; 112 | } 113 | 114 | unsigned char *prevPixelOrigin = [images[0] pixelData].rawData; 115 | int width = images[0].size.width; 116 | int height = images[0].size.height; 117 | int topHeight = 0, bottomHeight = 0, contentHeight = 0; 118 | 119 | unsigned char *topPixelData = NULL; 120 | unsigned char *bottomPixelData = NULL; 121 | struct PixelData dataArray[images.count]; 122 | memset(dataArray, 0, sizeof(dataArray)); 123 | for (int i = 1; i < images.count; i++) { 124 | struct PixelData postPixelData = [images[i] pixelData]; 125 | struct PixelData *prevPixelContentData = NULL, *postPixelContentData = NULL; 126 | unsigned char *postPixelOrigin = postPixelData.rawData; 127 | if (i == 1) { 128 | NSArray *heights = [self getTopBottomHeight:postPixelOrigin prevPixel:prevPixelOrigin width:width height:height]; 129 | topHeight = [heights[0] intValue]; 130 | bottomHeight = [heights[1] intValue]; 131 | contentHeight = height - topHeight - bottomHeight; 132 | 133 | // prev content 134 | int prevPixelContentCount = postPixelData.count - bottomHeight * width * 4 - topHeight * width * 4; 135 | int prevStartIndex = topHeight * width * 4; 136 | unsigned char *prevPixelContent = (unsigned char *)calloc(prevPixelContentCount, sizeof(unsigned char)); 137 | for (int i = 0; i < prevPixelContentCount; i++) { 138 | prevPixelContent[i] = prevPixelOrigin[prevStartIndex++]; 139 | } 140 | 141 | // post content 142 | int postPixelContentCount = prevPixelContentCount; 143 | int postStartIndex = topHeight * width * 4; 144 | unsigned char *postPixelContent = (unsigned char *)calloc(postPixelContentCount, sizeof(unsigned char)); 145 | for (int i = 0; i < postPixelContentCount; i++) { 146 | postPixelContent[i] = postPixelOrigin[postStartIndex++]; 147 | } 148 | 149 | // top content 150 | int topStartIndex = 0; 151 | topPixelData = (unsigned char *)calloc(topHeight * width * 4, sizeof(unsigned char)); 152 | for (int i = 0; i < topHeight * width * 4; i++) { 153 | topPixelData[i] = prevPixelOrigin[topStartIndex++]; 154 | } 155 | 156 | // bottom content 157 | if (bottomHeight > 0) { 158 | int bottomPixelCount = bottomHeight * width * 4; 159 | int bottomStartIndex = postPixelData.count - bottomPixelCount; 160 | bottomPixelData = (unsigned char *)calloc(bottomHeight * width * 4, sizeof(unsigned char)); 161 | for (int i = 0; i < bottomPixelCount; i++) { 162 | bottomPixelData[i] = prevPixelOrigin[bottomStartIndex++]; 163 | } 164 | } 165 | 166 | struct PixelData prev = { 167 | prevPixelContent, 168 | prevPixelContentCount, 169 | width, 170 | prevPixelContentCount / width / 4, 171 | prevPixelContentCount}; 172 | prevPixelContentData = &prev; 173 | 174 | struct PixelData post = { 175 | postPixelContent, 176 | postPixelContentCount, 177 | width, 178 | postPixelContentCount / width / 4, 179 | postPixelContentCount}; 180 | postPixelContentData = &post; 181 | 182 | [self mergeInPrev:prevPixelContentData postPixel:postPixelContentData]; 183 | 184 | dataArray[0] = *prevPixelContentData; 185 | dataArray[1] = *postPixelContentData; 186 | } else { 187 | // post content 188 | int postPixelContentCount = postPixelData.count - bottomHeight * width * 4 - topHeight * width * 4; 189 | int postStartIndex = topHeight * width * 4; 190 | unsigned char *postPixelContent = (unsigned char *)calloc(postPixelContentCount, sizeof(unsigned char)); 191 | for (int i = 0; i < postPixelContentCount; i++) { 192 | postPixelContent[i] = postPixelOrigin[postStartIndex++]; 193 | } 194 | struct PixelData post = { 195 | postPixelContent, 196 | postPixelContentCount, 197 | width, 198 | postPixelContentCount / width / 4, 199 | postPixelContentCount}; 200 | postPixelContentData = &post; 201 | 202 | [self mergeInPrev:&dataArray[i - 1] postPixel:postPixelContentData]; 203 | dataArray[i] = *postPixelContentData; 204 | } 205 | 206 | if (i == images.count - 1) { 207 | dataArray[i] = *postPixelContentData; 208 | } 209 | free(postPixelOrigin); 210 | } 211 | int dataCount = 0; 212 | for (int i = 0; i < images.count; i++) { 213 | dataCount += dataArray[i].endIndex; 214 | } 215 | int resultCount = (topHeight + bottomHeight) * width * 4 + dataCount; 216 | unsigned char *result = (unsigned char *)calloc(resultCount, sizeof(unsigned char)); 217 | int i = 0; 218 | for (int j = 0; j < topHeight * width * 4; j++) { 219 | result[i] = topPixelData[j]; 220 | i++; 221 | } 222 | for (int j = 0; j < images.count; j++) { 223 | for (int k = 0; k < dataArray[j].endIndex; k++) { 224 | result[i] = dataArray[j].rawData[k]; 225 | i++; 226 | } 227 | } 228 | for (int j = 0; j < bottomHeight * width * 4; j++) { 229 | result[i] = bottomPixelData[j]; 230 | i++; 231 | } 232 | UIImage *image = [self pixel2Image:result width:width height:topHeight + bottomHeight + dataCount / width / 4]; 233 | 234 | if (topPixelData != NULL) { 235 | free(topPixelData); 236 | } 237 | if (bottomPixelData != NULL) { 238 | free(bottomPixelData); 239 | } 240 | 241 | for (int i = 0; i < images.count; i++) { 242 | free(dataArray[i].rawData); 243 | } 244 | 245 | free(prevPixelOrigin); 246 | 247 | return image; 248 | } 249 | 250 | + (UIImage *)pixel2Image:(unsigned char *)pixels 251 | width:(int)width 252 | height:(int)height { 253 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 254 | CGContextRef bitmapContext = CGBitmapContextCreate(pixels, width, height, 8, 4 * width, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrderDefault); 255 | CFRelease(colorSpace); 256 | CGImageRef cgImage = CGBitmapContextCreateImage(bitmapContext); 257 | CGContextRelease(bitmapContext); 258 | UIImage *newimage = [UIImage imageWithCGImage:cgImage]; 259 | CGImageRelease(cgImage); 260 | free(pixels); 261 | return newimage; 262 | } 263 | 264 | + (NSArray *)getTopBottomHeight:(unsigned char *)postPixel 265 | prevPixel:(unsigned char *)prevPixel 266 | width:(int)width 267 | height:(int)height { 268 | int topHeight = 0; 269 | int bottomHeight = 0; 270 | int count = width * height * 4; 271 | for (int i = 0; i < count; i++) { 272 | if (prevPixel[i] != postPixel[i]) { 273 | topHeight = i / width / 4; 274 | break; 275 | } 276 | } 277 | for (int i = count - 1; i >= 0; i--) { 278 | if (prevPixel[i] != postPixel[i]) { 279 | bottomHeight = height - i / width / 4 - 1; 280 | break; 281 | } 282 | } 283 | 284 | if (bottomHeight < minBottomHeight) { // clear 285 | bottomHeight = 0; 286 | } 287 | return @[ @(topHeight), @(bottomHeight) ]; 288 | } 289 | 290 | + (void)mergeInPrev:(struct PixelData *)prevPixelData 291 | postPixel:(struct PixelData *)postPixelData { 292 | int duplicateRow = 60; 293 | int duplicateEnd = postPixelData->width * 4 * duplicateRow; 294 | unsigned char *duplicatePixelData = (unsigned char *)calloc(duplicateEnd, sizeof(unsigned char)); 295 | for (int i = 0; i < duplicateEnd; i++) { 296 | duplicatePixelData[i] = postPixelData->rawData[i]; 297 | } 298 | int targetEndIndex = prevPixelData->endIndex - 1; 299 | bool isLoop = true; 300 | int loopCount = 1; 301 | int minErrorCount = duplicateEnd * 0.02; 302 | 303 | while (isLoop) { 304 | int equalCount = 0; 305 | for (int i = duplicateEnd - 1; i >= 0; i--) { 306 | if (duplicatePixelData[i] != prevPixelData->rawData[targetEndIndex]) { 307 | if (duplicateEnd - equalCount >= minErrorCount) { 308 | break; 309 | } 310 | } else { 311 | targetEndIndex--; 312 | equalCount++; 313 | } 314 | } 315 | float p = equalCount * 1.0 / duplicateEnd; 316 | if (p >= 1.0) { 317 | prevPixelData->endIndex = targetEndIndex + 1; 318 | break; 319 | } else { 320 | if (targetEndIndex <= postPixelData->width * 4 * minDuplicateRows) { 321 | // 一般走到这就认为没必要继续跑了,也就是两图没重复 322 | break; 323 | } 324 | targetEndIndex = prevPixelData->count - loopCount * prevPixelData->width * 4 - 1; 325 | loopCount++; 326 | } 327 | } 328 | free(duplicatePixelData); 329 | } 330 | @end 331 | -------------------------------------------------------------------------------- /Fusion/ViewController/BaseNavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseNavigationController.swift 3 | // Fusion 4 | // 5 | // Created by DevinShine on 2017/5/26. 6 | // Copyright © 2017年 DevinShine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BaseNavigationController: UINavigationController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | self.navigationBar.isTranslucent = false;//不启用透明效果 16 | self.navigationBar.barTintColor = UIColor.mainBackgroundColor 17 | self.navigationBar.tintColor = UIColor.white 18 | self.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName:UIColor.white] 19 | } 20 | 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Fusion/ViewController/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.swift 3 | // Fusion 4 | // 5 | // Created by DevinShine on 2017/5/26. 6 | // Copyright © 2017年 DevinShine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BaseViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view. 16 | } 17 | 18 | override func didReceiveMemoryWarning() { 19 | super.didReceiveMemoryWarning() 20 | // Dispose of any resources that can be recreated. 21 | } 22 | 23 | // override var preferredStatusBarStyle: UIStatusBarStyle { 24 | // return .lightContent 25 | // } 26 | 27 | /* 28 | // MARK: - Navigation 29 | 30 | // In a storyboard-based application, you will often want to do a little preparation before navigation 31 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 32 | // Get the new view controller using segue.destinationViewController. 33 | // Pass the selected object to the new view controller. 34 | } 35 | */ 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Fusion/ViewController/HomeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewController.swift 3 | // Fusion 4 | // 5 | // Created by DevinShine on 2017/5/26. 6 | // Copyright © 2017年 DevinShine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class HomeViewController: BaseViewController,TLPhotosPickerViewControllerDelegate { 12 | 13 | @IBOutlet var startButton:UIButton! 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | self.view.backgroundColor = UIColor.mainBackgroundColor 17 | self.startButton.backgroundColor = UIColor.mainColor 18 | self.startButton.setTitleColor(UIColor.white, for: .normal) 19 | self.startButton.layer.cornerRadius = 10 20 | self.startButton.layer.masksToBounds = true 21 | } 22 | 23 | override func viewWillAppear(_ animated: Bool) { 24 | self.navigationController?.setNavigationBarHidden(true, animated: true) 25 | } 26 | 27 | override func didReceiveMemoryWarning() { 28 | super.didReceiveMemoryWarning() 29 | // Dispose of any resources that can be recreated. 30 | } 31 | 32 | @IBAction func startAction(_ sender: Any) { 33 | let viewController = TLPhotosPickerViewController() 34 | viewController.delegate = self 35 | viewController.didExceedMaximumNumberOfSelection = { [weak self] (picker) in 36 | self?.showAlert(vc: picker) 37 | } 38 | var configure = TLPhotosPickerConfigure() 39 | configure.numberOfColumn = 3 40 | configure.maxSelectedAssets = 10 41 | viewController.configure = configure 42 | 43 | self.navigationController?.pushViewController(viewController, animated: true) 44 | } 45 | func dismissPhotoPicker(withTLPHAssets: [TLPHAsset]) { 46 | guard withTLPHAssets.count > 1 else { 47 | let alert = UIAlertController(title: "提示", message: "请至少选择两张图片", preferredStyle: .alert) 48 | alert.addAction(UIAlertAction(title: "确定", style: .default, handler: nil)) 49 | self.navigationController?.present(alert, animated: true, completion: nil) 50 | return 51 | } 52 | 53 | let images = withTLPHAssets.flatMap { (asset) -> UIImage? in 54 | return asset.fullResolutionImage 55 | } 56 | 57 | let controller = PreviewViewController(images) 58 | self.navigationController?.pushViewController(controller, animated: true) 59 | } 60 | 61 | func showAlert(vc: UIViewController) { 62 | let alert = UIAlertController(title: "提示", message: "超出了可选的最大数量", preferredStyle: .alert) 63 | alert.addAction(UIAlertAction(title: "确定", style: .default, handler: nil)) 64 | vc.present(alert, animated: true, completion: nil) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Fusion/ViewController/PreviewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewViewController.swift 3 | // Fusion 4 | // 5 | // Created by DevinShine on 2017/5/26. 6 | // Copyright © 2017年 DevinShine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PreviewViewController: UIViewController { 12 | 13 | var images:[UIImage?]? 14 | @IBOutlet var previewHeightConstraint:NSLayoutConstraint! 15 | @IBOutlet var previewWidthConstraint:NSLayoutConstraint! 16 | @IBOutlet var previewImageView:UIImageView! 17 | @IBOutlet var generateButton:UIButton! 18 | deinit { 19 | } 20 | 21 | required public init?(coder aDecoder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | 25 | public init() { 26 | super.init(nibName: "PreviewViewController", bundle: Bundle(for: PreviewViewController.self)) 27 | } 28 | 29 | public convenience init(_ images: [UIImage?]) { 30 | self.init() 31 | self.images = images 32 | } 33 | 34 | override func viewWillAppear(_ animated: Bool) { 35 | UIApplication.shared.statusBarStyle = .lightContent 36 | if (self.navigationController?.isNavigationBarHidden)! { 37 | self.navigationController?.setNavigationBarHidden(false, animated: true) 38 | } 39 | } 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | self.navigationItem.title = "预览" 44 | self.generateButton.backgroundColor = UIColor.mainBackgroundColor 45 | self.generateButton.setTitle("保存到本地", for: .normal) 46 | 47 | let image = DVSFusion.merge(self.images! as! [UIImage])! 48 | previewImageView.image = image 49 | previewHeightConstraint.constant = UIScreen.main.bounds.size.width * (image.size.height / image.size.width) 50 | previewWidthConstraint.constant = UIScreen.main.bounds.size.width 51 | } 52 | 53 | override func didReceiveMemoryWarning() { 54 | super.didReceiveMemoryWarning() 55 | } 56 | 57 | @IBAction func saveAction(_ sender: Any) { 58 | UIImageWriteToSavedPhotosAlbum(previewImageView.image!, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil) 59 | } 60 | 61 | func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) { 62 | if let error = error { 63 | let ac = UIAlertController(title: "保存失败", message: error.localizedDescription, preferredStyle: .alert) 64 | ac.addAction(UIAlertAction(title: "确定", style: .default){ action -> Void in 65 | self.navigationController?.popViewController(animated: true) 66 | }) 67 | present(ac, animated: true) 68 | } else { 69 | let ac = UIAlertController(title: "保存成功", message: "已经成功保存到本地", preferredStyle: .alert) 70 | ac.addAction(UIAlertAction(title: "确定", style: .default){ action -> Void in 71 | self.navigationController?.popViewController(animated: true) 72 | }) 73 | present(ac, animated: true) 74 | } 75 | } 76 | /* 77 | // MARK: - Navigation 78 | 79 | // In a storyboard-based application, you will often want to do a little preparation before navigation 80 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 81 | // Get the new view controller using segue.destinationViewController. 82 | // Pass the selected object to the new view controller. 83 | } 84 | */ 85 | 86 | } 87 | -------------------------------------------------------------------------------- /Fusion/ViewController/PreviewViewController.xib: -------------------------------------------------------------------------------- 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 | 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 | -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLAlbumPopView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TLAlbumPopView.swift 3 | // TLPhotosPicker 4 | // 5 | // Created by wade.hawk on 2017. 4. 19.. 6 | // Copyright © 2017년 wade.hawk. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol PopupViewProtocol: class { 12 | var bgView: UIView! { get set } 13 | var popupView: UIView! { get set } 14 | var originalFrame: CGRect { get set } 15 | var show: Bool { get set } 16 | func setupPopupFrame() 17 | } 18 | 19 | extension PopupViewProtocol where Self: UIView { 20 | fileprivate func getFrame(scale: CGFloat) -> CGRect { 21 | var frame = self.originalFrame 22 | frame.size.width = frame.size.width * scale 23 | frame.size.height = frame.size.height * scale 24 | frame.origin.x = self.frame.width/2 - frame.width/2 25 | return frame 26 | } 27 | func setupPopupFrame() { 28 | if self.originalFrame != self.popupView.frame { 29 | self.originalFrame = self.popupView.frame 30 | } 31 | } 32 | func show(_ show: Bool, duration: TimeInterval = 0.1) { 33 | guard self.show != show else { return } 34 | self.layer.removeAllAnimations() 35 | self.isHidden = false 36 | self.popupView.frame = show ? getFrame(scale: 0.1) : self.popupView.frame 37 | self.bgView.alpha = show ? 0 : 1 38 | UIView.animate(withDuration: duration, animations: { 39 | self.bgView.alpha = show ? 1 : 0 40 | self.popupView.transform = show ? CGAffineTransform(scaleX: 1.05, y: 1.05) : CGAffineTransform(scaleX: 0.1, y: 0.1) 41 | self.popupView.frame = show ? self.getFrame(scale: 1.05) : self.getFrame(scale: 0.1) 42 | }) { _ in 43 | self.isHidden = show ? false : true 44 | UIView.animate(withDuration: duration) { 45 | if show { 46 | self.popupView.transform = CGAffineTransform(scaleX: 1, y: 1) 47 | self.popupView.frame = self.originalFrame 48 | } 49 | self.show = show 50 | } 51 | } 52 | } 53 | } 54 | 55 | class TLAlbumPopView: UIView,PopupViewProtocol { 56 | @IBOutlet var bgView: UIView! 57 | @IBOutlet var popupView: UIView! 58 | @IBOutlet var popupViewHeight: NSLayoutConstraint! 59 | @IBOutlet var tableView: UITableView! 60 | var originalFrame = CGRect.zero 61 | var show = false 62 | 63 | deinit { 64 | // print("deinit TLAlbumPopView") 65 | } 66 | 67 | override func awakeFromNib() { 68 | super.awakeFromNib() 69 | self.popupView.layer.cornerRadius = 5.0 70 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapBgView)) 71 | self.bgView.addGestureRecognizer(tapGesture) 72 | self.tableView.register(UINib(nibName: "TLCollectionTableViewCell", bundle: Bundle(for: TLCollectionTableViewCell.self)), forCellReuseIdentifier: "TLCollectionTableViewCell") 73 | } 74 | 75 | func tapBgView() { 76 | self.show(false) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLAssetsCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TLAssetsCollection.swift 3 | // TLPhotosPicker 4 | // 5 | // Created by wade.hawk on 2017. 4. 18.. 6 | // Copyright © 2017년 wade.hawk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Photos 11 | import PhotosUI 12 | 13 | public struct TLPHAsset { 14 | enum CloudDownloadState { 15 | case ready,progress,complete,failed 16 | } 17 | var camera: Bool = false 18 | var state = CloudDownloadState.ready 19 | 20 | public enum AssetType { 21 | case photo,video,livePhoto 22 | } 23 | public var phAsset: PHAsset? = nil 24 | public var selectedOrder: Int = 0 25 | public var type: AssetType { 26 | get { 27 | guard let phAsset = self.phAsset else { return .photo } 28 | if phAsset.mediaSubtypes.contains(.photoLive) { 29 | return .livePhoto 30 | }else if phAsset.mediaType == .video { 31 | return .video 32 | }else { 33 | return .photo 34 | } 35 | } 36 | } 37 | public var fullResolutionImage: UIImage? { 38 | get { 39 | guard let phAsset = self.phAsset else { return nil } 40 | return TLPhotoLibrary.fullResolutionImageData(asset: phAsset) 41 | } 42 | } 43 | public var originalFileName: String? { 44 | get { 45 | guard let phAsset = self.phAsset,let resource = PHAssetResource.assetResources(for: phAsset).first else { return nil } 46 | return resource.originalFilename 47 | } 48 | } 49 | 50 | init(asset: PHAsset?) { 51 | self.phAsset = asset 52 | } 53 | } 54 | 55 | struct TLAssetsCollection { 56 | var collection: PHAssetCollection 57 | var assets = [TLPHAsset]() 58 | var thumbnail: UIImage? = nil 59 | //var loadComplete: Bool = false 60 | var recentPosition: CGPoint = CGPoint.zero 61 | var title: String { 62 | get { 63 | return self.collection.localizedTitle ?? "" 64 | } 65 | } 66 | var count: Int { 67 | get { 68 | return self.assets.count 69 | } 70 | } 71 | 72 | init(collection: PHAssetCollection) { 73 | self.collection = collection 74 | } 75 | 76 | static func ==(lhs: TLAssetsCollection, rhs: TLAssetsCollection) -> Bool { 77 | return lhs.collection.localIdentifier == rhs.collection.localIdentifier 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLBundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TLBundle.swift 3 | // Pods 4 | // 5 | // Created by wade.hawk on 2017. 5. 9.. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | class TLBundle { 12 | class func podBundleImage(named: String) -> UIImage? { 13 | let podBundle = Bundle(for: TLBundle.self) 14 | if let url = podBundle.url(forResource: "TLPhotoPickerController", withExtension: "bundle") { 15 | let bundle = Bundle(url: url) 16 | return UIImage(named: named, in: bundle, compatibleWith: nil)! 17 | } 18 | return nil 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLCollectionTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TLCollectionTableViewCell.swift 3 | // TLPhotosPicker 4 | // 5 | // Created by wade.hawk on 2017. 5. 3.. 6 | // Copyright © 2017년 wade.hawk. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TLCollectionTableViewCell: UITableViewCell { 12 | @IBOutlet var thumbImageView: UIImageView! 13 | @IBOutlet var titleLabel: UILabel! 14 | @IBOutlet var subTitleLabel: UILabel! 15 | } 16 | -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLCollectionTableViewCell.xib: -------------------------------------------------------------------------------- 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 | 35 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLPhotoCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TLPhotoCollectionViewCell.swift 3 | // TLPhotosPicker 4 | // 5 | // Created by wade.hawk on 2017. 5. 3.. 6 | // Copyright © 2017년 wade.hawk. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PhotosUI 11 | 12 | open class TLPlayerView: UIView { 13 | open var player: AVPlayer? { 14 | get { 15 | return playerLayer.player 16 | } 17 | set { 18 | playerLayer.player = newValue 19 | } 20 | } 21 | 22 | open var playerLayer: AVPlayerLayer { 23 | return layer as! AVPlayerLayer 24 | } 25 | 26 | // Override UIView property 27 | override open static var layerClass: AnyClass { 28 | return AVPlayerLayer.self 29 | } 30 | } 31 | 32 | open class TLPhotoCollectionViewCell: UICollectionViewCell { 33 | @IBOutlet open var imageView: UIImageView? 34 | @IBOutlet open var playerView: TLPlayerView? 35 | @IBOutlet open var livePhotoView: PHLivePhotoView? 36 | @IBOutlet open var liveBadgeImageView: UIImageView? 37 | @IBOutlet open var durationView: UIView? 38 | @IBOutlet open var videoIconImageView: UIImageView? 39 | @IBOutlet open var durationLabel: UILabel? 40 | @IBOutlet open var indicator: UIActivityIndicatorView? 41 | @IBOutlet open var selectedView: UIView? 42 | @IBOutlet open var selectedHeight: NSLayoutConstraint? 43 | @IBOutlet open var orderLabel: UILabel? 44 | @IBOutlet open var orderBgView: UIView? 45 | 46 | var configure = TLPhotosPickerConfigure() { 47 | didSet { 48 | self.selectedView?.layer.borderColor = self.configure.selectedColor.cgColor 49 | self.orderBgView?.backgroundColor = self.configure.selectedColor 50 | self.videoIconImageView?.image = self.configure.videoIcon 51 | } 52 | } 53 | 54 | open var isCameraCell = false 55 | 56 | open var duration: TimeInterval? { 57 | didSet { 58 | guard let duration = self.duration else { return } 59 | self.selectedHeight?.constant = -10 60 | self.durationLabel?.text = timeFormatted(timeInterval: duration) 61 | } 62 | } 63 | 64 | open var player: AVPlayer? = nil { 65 | didSet { 66 | if self.player == nil { 67 | self.playerView?.playerLayer.player = nil 68 | NotificationCenter.default.removeObserver(self) 69 | }else { 70 | self.playerView?.playerLayer.player = self.player 71 | NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.player?.currentItem, queue: nil, using: { [weak self] (_) in 72 | DispatchQueue.main.async { 73 | guard let `self` = self else { return } 74 | self.player?.seek(to: kCMTimeZero) 75 | self.player?.play() 76 | } 77 | }) 78 | } 79 | } 80 | } 81 | 82 | open var selectedAsset: Bool = false { 83 | willSet(newValue) { 84 | self.selectedView?.isHidden = !newValue 85 | self.durationView?.backgroundColor = newValue ? self.configure.selectedColor : UIColor(red: 0, green: 0, blue: 0, alpha: 0.6) 86 | if !newValue { 87 | self.orderLabel?.text = "" 88 | } 89 | } 90 | } 91 | 92 | open func timeFormatted(timeInterval: TimeInterval) -> String { 93 | let seconds: Int = lround(timeInterval) 94 | var hour: Int = 0 95 | var minute: Int = Int(seconds/60) 96 | let second: Int = seconds % 60 97 | if minute > 59 { 98 | hour = minute / 60 99 | minute = minute % 60 100 | return String(format: "%d:%d:%02d", hour, minute, second) 101 | } else { 102 | return String(format: "%d:%02d", minute, second) 103 | } 104 | } 105 | 106 | open func popScaleAnim() { 107 | UIView.animate(withDuration: 0.1, animations: { 108 | self.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) 109 | }) { _ in 110 | UIView.animate(withDuration: 0.1, animations: { 111 | self.transform = CGAffineTransform(scaleX: 1, y: 1) 112 | }) 113 | } 114 | } 115 | 116 | func stopPlay() { 117 | if let player = self.player { 118 | player.pause() 119 | self.player = nil 120 | } 121 | self.livePhotoView?.isHidden = true 122 | self.livePhotoView?.stopPlayback() 123 | } 124 | 125 | deinit { 126 | // print("deinit TLPhotoCollectionViewCell") 127 | } 128 | 129 | override open func awakeFromNib() { 130 | super.awakeFromNib() 131 | self.playerView?.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill 132 | self.livePhotoView?.isHidden = true 133 | self.durationView?.isHidden = true 134 | self.selectedView?.isHidden = true 135 | self.selectedView?.layer.borderWidth = 10 136 | self.selectedView?.layer.cornerRadius = 15 137 | self.orderBgView?.layer.cornerRadius = 2 138 | self.videoIconImageView?.image = self.configure.videoIcon 139 | } 140 | 141 | override open func prepareForReuse() { 142 | super.prepareForReuse() 143 | self.livePhotoView?.isHidden = true 144 | self.durationView?.isHidden = true 145 | self.durationView?.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6) 146 | self.selectedHeight?.constant = 10 147 | self.selectedAsset = false 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLPhotoCollectionViewCell.xib: -------------------------------------------------------------------------------- 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 | 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 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLPhotoLibrary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TLPhotoLibrary.swift 3 | // TLPhotosPicker 4 | // 5 | // Created by wade.hawk on 2017. 5. 3.. 6 | // Copyright © 2017년 wade.hawk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Photos 11 | 12 | protocol TLPhotoLibraryDelegate: class { 13 | func loadCameraRollCollection(collection: TLAssetsCollection) 14 | func loadCompleteAllCollection(collections: [TLAssetsCollection]) 15 | func focusCollection(collection: TLAssetsCollection) 16 | } 17 | 18 | class TLPhotoLibrary { 19 | 20 | weak var delegate: TLPhotoLibraryDelegate? = nil 21 | 22 | lazy var imageManager: PHCachingImageManager = { 23 | return PHCachingImageManager() 24 | }() 25 | 26 | deinit { 27 | //print("deinit TLPhotoLibrary") 28 | } 29 | 30 | @discardableResult 31 | func livePhotoAsset(asset: PHAsset, size: CGSize = CGSize(width: 720, height: 1280), progressBlock: Photos.PHAssetImageProgressHandler? = nil, completionBlock:@escaping (PHLivePhoto)-> Void ) -> PHImageRequestID { 32 | let options = PHLivePhotoRequestOptions() 33 | options.deliveryMode = .highQualityFormat 34 | options.isNetworkAccessAllowed = true 35 | options.progressHandler = progressBlock 36 | let requestId = self.imageManager.requestLivePhoto(for: asset, targetSize: size, contentMode: .aspectFill, options: options) { (livePhoto, info) in 37 | if let livePhoto = livePhoto { 38 | completionBlock(livePhoto) 39 | } 40 | } 41 | return requestId 42 | } 43 | 44 | @discardableResult 45 | func videoAsset(asset: PHAsset, size: CGSize = CGSize(width: 720, height: 1280), progressBlock: Photos.PHAssetImageProgressHandler? = nil, completionBlock:@escaping (AVPlayerItem?, [AnyHashable : Any]?) -> Void ) -> PHImageRequestID { 46 | let options = PHVideoRequestOptions() 47 | options.isNetworkAccessAllowed = true 48 | options.deliveryMode = .automatic 49 | options.progressHandler = progressBlock 50 | let requestId = self.imageManager.requestPlayerItem(forVideo: asset, options: options, resultHandler: { playerItem, info in 51 | completionBlock(playerItem,info) 52 | }) 53 | return requestId 54 | } 55 | 56 | @discardableResult 57 | func imageAsset(asset: PHAsset, size: CGSize = CGSize(width: 720, height: 1280), options: PHImageRequestOptions? = nil, completionBlock:@escaping (UIImage)-> Void ) -> PHImageRequestID { 58 | var options = options 59 | if options == nil { 60 | options = PHImageRequestOptions() 61 | options?.deliveryMode = .highQualityFormat 62 | options?.isNetworkAccessAllowed = false 63 | } 64 | let requestId = self.imageManager.requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: options) { image, info in 65 | if let image = image { 66 | completionBlock(image) 67 | } 68 | } 69 | return requestId 70 | } 71 | 72 | func cancelPHImageRequest(requestId: PHImageRequestID) { 73 | self.imageManager.cancelImageRequest(requestId) 74 | } 75 | 76 | @discardableResult 77 | func cloudImageDownload(asset: PHAsset, size: CGSize = PHImageManagerMaximumSize, progressBlock: @escaping (Double) -> Void, completionBlock:@escaping (UIImage?)-> Void ) -> PHImageRequestID { 78 | let options = PHImageRequestOptions() 79 | options.isSynchronous = false 80 | options.isNetworkAccessAllowed = true 81 | options.deliveryMode = .opportunistic 82 | options.version = .current 83 | options.progressHandler = { (progress,error,stop,info) in 84 | progressBlock(progress) 85 | } 86 | let requestId = self.imageManager.requestImageData(for: asset, options: options) { (imageData, dataUTI, orientation, info) in 87 | if let data = imageData,let _ = info { 88 | completionBlock(UIImage(data: data)) 89 | } 90 | } 91 | return requestId 92 | } 93 | 94 | @discardableResult 95 | class func fullResolutionImageData(asset: PHAsset) -> UIImage? { 96 | let options = PHImageRequestOptions() 97 | options.isSynchronous = true 98 | options.resizeMode = .none 99 | options.isNetworkAccessAllowed = false 100 | options.version = .current 101 | var image: UIImage? = nil 102 | _ = PHCachingImageManager().requestImageData(for: asset, options: options) { (imageData, dataUTI, orientation, info) in 103 | if let data = imageData { 104 | image = UIImage(data: data) 105 | } 106 | } 107 | return image 108 | } 109 | } 110 | 111 | //MARK: - Load Collection 112 | extension TLPhotoLibrary { 113 | func fetchCollection(allowedVideo: Bool = true, addCameraAsset: Bool = true, mediaType: PHAssetMediaType? = nil) { 114 | func loadAssets(collection: PHAssetCollection, options: PHFetchOptions?) -> [PHAsset] { 115 | let assetFetchResult = PHAsset.fetchAssets(in: collection, options: options) 116 | var assets = [PHAsset]() 117 | if assetFetchResult.count > 0 { 118 | assetFetchResult.enumerateObjects({ object, index, stop in 119 | assets.insert(object, at: 0) 120 | }) 121 | } 122 | return assets 123 | } 124 | 125 | func getUseableCollection(_ fetchCollection: PHFetchResult) -> PHAssetCollection? { 126 | let options = PHFetchOptions() 127 | var result: PHAssetCollection? = nil 128 | fetchCollection.enumerateObjects({ (collection, index, stop) -> Void in 129 | if let fetchAssets = PHAsset.fetchKeyAssets(in: collection, options: options), fetchAssets.count > 0 { 130 | result = collection 131 | } 132 | }) 133 | return result 134 | } 135 | 136 | @discardableResult 137 | func getSmartAlbum(subType: PHAssetCollectionSubtype, result: inout [TLAssetsCollection]) -> TLAssetsCollection? { 138 | let fetchCollection = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: subType, options: nil) 139 | if let collection = getUseableCollection(fetchCollection), !result.contains(where: { $0.collection == collection }) { 140 | let assetsCollection = TLAssetsCollection(collection: collection) 141 | result.append(assetsCollection) 142 | return assetsCollection 143 | } 144 | return nil 145 | } 146 | 147 | var assetCollections = [TLAssetsCollection]() 148 | //Camera Roll 149 | let camerarollCollection = getSmartAlbum(subType: .smartAlbumUserLibrary, result: &assetCollections) 150 | let options = PHFetchOptions() 151 | if let mediaType = mediaType { 152 | options.predicate = NSPredicate(format: "mediaType = %i", mediaType.rawValue) 153 | }else if !allowedVideo { 154 | options.predicate = NSPredicate(format: "mediaType = %i", PHAssetMediaType.image.rawValue) 155 | } 156 | if var cameraRoll = camerarollCollection { 157 | DispatchQueue.main.async { 158 | self.delegate?.focusCollection(collection: cameraRoll) 159 | } 160 | cameraRoll.assets = loadAssets(collection: cameraRoll.collection, options: options).map{ TLPHAsset(asset: $0) } 161 | if addCameraAsset { 162 | var cameraAsset = TLPHAsset(asset: nil) 163 | cameraAsset.camera = true 164 | cameraRoll.assets.insert(cameraAsset, at: 0) 165 | } 166 | assetCollections[0] = cameraRoll 167 | DispatchQueue.main.async { 168 | self.delegate?.loadCameraRollCollection(collection: cameraRoll) 169 | } 170 | } 171 | DispatchQueue.global(qos: .userInteractive).async { [weak self] _ in 172 | //Selfies 173 | getSmartAlbum(subType: .smartAlbumSelfPortraits, result: &assetCollections) 174 | //Panoramas 175 | getSmartAlbum(subType: .smartAlbumPanoramas, result: &assetCollections) 176 | //Favorites 177 | getSmartAlbum(subType: .smartAlbumFavorites, result: &assetCollections) 178 | if allowedVideo { 179 | //Videos 180 | getSmartAlbum(subType: .smartAlbumVideos, result: &assetCollections) 181 | } 182 | //Album 183 | let albumsResult = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: nil) 184 | albumsResult.enumerateObjects({ (collection, index, stop) -> Void in 185 | if let result = PHAsset.fetchKeyAssets(in: collection, options: options), result.count > 0, !assetCollections.contains(where: { $0.collection == collection }) { 186 | assetCollections.append(TLAssetsCollection(collection: collection)) 187 | } 188 | }) 189 | 190 | let collections = assetCollections.flatMap{ collection -> TLAssetsCollection in 191 | if let cameraRoll = camerarollCollection, collection == cameraRoll { return collection } 192 | var collection = collection 193 | collection.assets = loadAssets(collection: collection.collection, options: options).map{ TLPHAsset(asset: $0) } 194 | return collection 195 | } 196 | DispatchQueue.main.async { 197 | self?.delegate?.loadCompleteAllCollection(collections: collections) 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLPhotoPickerController.bundle/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Widget/PhotosPicker/TLPhotoPickerController.bundle/arrow.png -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLPhotoPickerController.bundle/camera@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Widget/PhotosPicker/TLPhotoPickerController.bundle/camera@3x.png -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLPhotoPickerController.bundle/insertPhotoMaterial@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Widget/PhotosPicker/TLPhotoPickerController.bundle/insertPhotoMaterial@3x.png -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLPhotoPickerController.bundle/pop_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Widget/PhotosPicker/TLPhotoPickerController.bundle/pop_arrow.png -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLPhotoPickerController.bundle/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevinShine/Fusion/bc2c4b227f8d13e03052c2c2e55f98e9c159e332/Fusion/Widget/PhotosPicker/TLPhotoPickerController.bundle/video.png -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLPhotosPickerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TLPhotosPickerViewController.swift 3 | // TLPhotosPicker 4 | // 5 | // Created by wade.hawk on 2017. 4. 14.. 6 | // Copyright © 2017년 wade.hawk. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | import PhotosUI 12 | 13 | public protocol TLPhotosPickerViewControllerDelegate: class { 14 | func dismissPhotoPicker(withPHAssets: [PHAsset]) 15 | func dismissPhotoPicker(withTLPHAssets: [TLPHAsset]) 16 | func dismissComplete() 17 | func photoPickerDidCancel() 18 | func didExceedMaximumNumberOfSelection(picker: TLPhotosPickerViewController) 19 | } 20 | extension TLPhotosPickerViewControllerDelegate { 21 | public func dismissPhotoPicker(withPHAssets: [PHAsset]) { } 22 | public func dismissPhotoPicker(withTLPHAssets: [TLPHAsset]) { } 23 | public func dismissComplete() { } 24 | public func photoPickerDidCancel() { } 25 | public func didExceedMaximumNumberOfSelection(picker: TLPhotosPickerViewController) { } 26 | } 27 | 28 | public struct TLPhotosPickerConfigure { 29 | public var defaultCameraRollTitle = "相机胶卷" 30 | public var tapHereToChange = "Tap here to change" 31 | public var cancelTitle = "取消" 32 | public var doneTitle = "确定" 33 | public var usedCameraButton = false 34 | public var usedPrefetch = false 35 | public var allowedLivePhotos = false 36 | public var allowedVideo = false 37 | public var mediaType: PHAssetMediaType? = nil 38 | public var numberOfColumn = 3 39 | public var maxSelectedAssets: Int? = nil 40 | public var selectedColor = UIColor(red: 88/255, green: 144/255, blue: 255/255, alpha: 1.0) 41 | public var cameraBgColor = UIColor(red: 221/255, green: 223/255, blue: 226/255, alpha: 1) 42 | public var cameraIcon = TLBundle.podBundleImage(named: "camera") 43 | public var videoIcon = TLBundle.podBundleImage(named: "video") 44 | public var placeholderIcon = TLBundle.podBundleImage(named: "insertPhotoMaterial") 45 | public var nibSet: (nibName: String, bundle:Bundle)? = nil 46 | public init() { 47 | 48 | } 49 | } 50 | 51 | 52 | public struct Platform { 53 | 54 | static var isSimulator: Bool { 55 | return TARGET_OS_SIMULATOR != 0 // Use this line in Xcode 7 or newer 56 | } 57 | 58 | } 59 | 60 | 61 | open class TLPhotosPickerViewController: UIViewController { 62 | // @IBOutlet var titleView: UIView! 63 | // @IBOutlet var titleLabel: UILabel! 64 | // @IBOutlet var subTitleLabel: UILabel! 65 | // @IBOutlet var subTitleArrowImageView: UIImageView! 66 | @IBOutlet var albumPopView: TLAlbumPopView! 67 | @IBOutlet var collectionView: UICollectionView! 68 | @IBOutlet var indicator: UIActivityIndicatorView! 69 | @IBOutlet var popArrowImageView: UIImageView! 70 | // @IBOutlet var doneButton: UIBarButtonItem! 71 | // @IBOutlet var cancelButton: UIBarButtonItem! 72 | 73 | public weak var delegate: TLPhotosPickerViewControllerDelegate? = nil 74 | public var selectedAssets = [TLPHAsset]() 75 | public var configure = TLPhotosPickerConfigure() 76 | 77 | fileprivate var usedCameraButton: Bool { 78 | get { 79 | return self.configure.usedCameraButton 80 | } 81 | } 82 | fileprivate var allowedVideo: Bool { 83 | get { 84 | return self.configure.allowedVideo 85 | } 86 | } 87 | fileprivate var usedPrefetch: Bool { 88 | get { 89 | return self.configure.usedPrefetch 90 | } 91 | set { 92 | self.configure.usedPrefetch = newValue 93 | } 94 | } 95 | fileprivate var allowedLivePhotos: Bool { 96 | get { 97 | return self.configure.allowedLivePhotos 98 | } 99 | set { 100 | self.configure.allowedLivePhotos = newValue 101 | } 102 | } 103 | open var didExceedMaximumNumberOfSelection: ((TLPhotosPickerViewController) -> Void)? = nil 104 | open var dismissCompletion: (() -> Void)? = nil 105 | fileprivate var completionWithPHAssets: (([PHAsset]) -> Void)? = nil 106 | fileprivate var completionWithTLPHAssets: (([TLPHAsset]) -> Void)? = nil 107 | fileprivate var didCancel: ((Void) -> Void)? = nil 108 | 109 | fileprivate var collections = [TLAssetsCollection]() 110 | fileprivate var focusedCollection: TLAssetsCollection? = nil 111 | fileprivate var requestIds = [IndexPath:PHImageRequestID]() 112 | fileprivate var cloudRequestIds = [IndexPath:PHImageRequestID]() 113 | fileprivate var playRequestId: (indexPath: IndexPath, requestId: PHImageRequestID)? = nil 114 | fileprivate var photoLibrary = TLPhotoLibrary() 115 | fileprivate var queue = DispatchQueue(label: "tilltue.photos.pikcker.queue") 116 | fileprivate var thumbnailSize = CGSize.zero 117 | fileprivate var placeholderThumbnail: UIImage? = nil 118 | fileprivate var cameraImage: UIImage? = nil 119 | 120 | deinit { 121 | //print("deinit TLPhotosPickerViewController") 122 | } 123 | 124 | required public init?(coder aDecoder: NSCoder) { 125 | fatalError("init(coder:) has not been implemented") 126 | } 127 | 128 | public init() { 129 | super.init(nibName: "TLPhotosPickerViewController", bundle: Bundle(for: TLPhotosPickerViewController.self)) 130 | if PHPhotoLibrary.authorizationStatus() != .authorized { 131 | PHPhotoLibrary.requestAuthorization { [weak self] status in 132 | self?.initPhotoLibrary() 133 | } 134 | } 135 | } 136 | 137 | convenience public init(withPHAssets: (([PHAsset]) -> Void)? = nil, didCancel: ((Void) -> Void)? = nil) { 138 | self.init() 139 | self.completionWithPHAssets = withPHAssets 140 | self.didCancel = didCancel 141 | } 142 | 143 | convenience public init(withTLPHAssets: (([TLPHAsset]) -> Void)? = nil, didCancel: ((Void) -> Void)? = nil) { 144 | self.init() 145 | self.completionWithTLPHAssets = withTLPHAssets 146 | self.didCancel = didCancel 147 | } 148 | 149 | override open var supportedInterfaceOrientations: UIInterfaceOrientationMask { 150 | return UIInterfaceOrientationMask.portrait 151 | } 152 | 153 | override open func didReceiveMemoryWarning() { 154 | super.didReceiveMemoryWarning() 155 | stopPlay() 156 | } 157 | 158 | override open func viewDidLoad() { 159 | super.viewDidLoad() 160 | makeUI() 161 | } 162 | 163 | override open func viewDidLayoutSubviews() { 164 | super.viewDidLayoutSubviews() 165 | initItemSize() 166 | // if isMovingToParentViewController { 167 | // initItemSize() 168 | // } 169 | } 170 | 171 | override open func viewWillAppear(_ animated: Bool) { 172 | super.viewWillAppear(animated) 173 | 174 | if isMovingToParentViewController { 175 | initPhotoLibrary() 176 | } 177 | UIApplication.shared.statusBarStyle = .lightContent 178 | if (self.navigationController?.isNavigationBarHidden)! { 179 | self.navigationController?.setNavigationBarHidden(false, animated: true) 180 | } 181 | } 182 | } 183 | 184 | // MARK: - UI & UI Action 185 | extension TLPhotosPickerViewController { 186 | 187 | public func registerNib(nibName: String, bundle: Bundle) { 188 | self.collectionView.register(UINib(nibName: nibName, bundle: bundle), forCellWithReuseIdentifier: nibName) 189 | } 190 | 191 | fileprivate func centerAtRect(image: UIImage?, rect: CGRect, bgColor: UIColor = UIColor.white) -> UIImage? { 192 | guard let image = image else { return nil } 193 | UIGraphicsBeginImageContextWithOptions(rect.size, false, image.scale) 194 | bgColor.setFill() 195 | UIRectFill(CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height)) 196 | image.draw(in: CGRect(x:rect.size.width/2 - image.size.width/2, y:rect.size.height/2 - image.size.height/2, width:image.size.width, height:image.size.height)) 197 | let result = UIGraphicsGetImageFromCurrentImageContext() 198 | UIGraphicsEndImageContext() 199 | return result 200 | } 201 | 202 | fileprivate func initItemSize() { 203 | guard let layout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return } 204 | let count = CGFloat(self.configure.numberOfColumn) 205 | let width = (self.view.frame.size.width-(5*(count-1)))/count 206 | self.thumbnailSize = CGSize(width: width, height: width) 207 | layout.itemSize = self.thumbnailSize 208 | self.collectionView.collectionViewLayout = layout 209 | self.placeholderThumbnail = centerAtRect(image: self.configure.placeholderIcon, rect: CGRect(x: 0, y: 0, width: width, height: width)) 210 | self.cameraImage = centerAtRect(image: self.configure.cameraIcon, rect: CGRect(x: 0, y: 0, width: width, height: width), bgColor: self.configure.cameraBgColor) 211 | } 212 | 213 | fileprivate func makeUI() { 214 | registerNib(nibName: "TLPhotoCollectionViewCell", bundle: Bundle(for: TLPhotoCollectionViewCell.self)) 215 | if let nibSet = self.configure.nibSet { 216 | registerNib(nibName: nibSet.nibName, bundle: nibSet.bundle) 217 | } 218 | self.indicator.startAnimating() 219 | // let tapGesture = UITapGestureRecognizer(target: self, action: #selector(titleTap)) 220 | // self.titleView.addGestureRecognizer(tapGesture) 221 | // self.titleLabel.text = self.configure.defaultCameraRollTitle 222 | // self.subTitleLabel.text = self.configure.tapHereToChange 223 | // self.cancelButton.title = self.configure.cancelTitle 224 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.configure.doneTitle, style: .plain, target: self, action: #selector(doneButtonTap)) 225 | self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.configure.cancelTitle, style: .plain, target: self, action: #selector(cancelButtonTap)) 226 | // self.doneButton.title = self.configure.doneTitle 227 | // self.doneButton.setTitleTextAttributes([NSFontAttributeName: UIFont.boldSystemFont(ofSize: UIFont.labelFontSize)], for: .normal) 228 | self.albumPopView.tableView.delegate = self 229 | self.albumPopView.tableView.dataSource = self 230 | self.popArrowImageView.image = TLBundle.podBundleImage(named: "pop_arrow") 231 | // self.subTitleArrowImageView.image = TLBundle.podBundleImage(named: "arrow") 232 | if #available(iOS 10.0, *), self.usedPrefetch { 233 | self.collectionView.isPrefetchingEnabled = true 234 | self.collectionView.prefetchDataSource = self 235 | } else { 236 | self.usedPrefetch = false 237 | } 238 | if #available(iOS 9.0, *), self.allowedLivePhotos { 239 | }else { 240 | self.allowedLivePhotos = false 241 | } 242 | // add 243 | self.navigationItem.title = self.configure.defaultCameraRollTitle 244 | self.navigationController?.navigationBar.barTintColor = UIColor.mainBackgroundColor 245 | self.navigationController?.navigationBar.tintColor = UIColor.white 246 | self.navigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName:UIColor.white] 247 | } 248 | 249 | fileprivate func updateTitle() { 250 | guard self.focusedCollection != nil else { return } 251 | // self.titleLabel.text = self.focusedCollection?.collection.localizedTitle 252 | } 253 | 254 | fileprivate func reloadCollectionView() { 255 | guard self.focusedCollection != nil else { return } 256 | self.collectionView.reloadData() 257 | } 258 | 259 | fileprivate func reloadTableView() { 260 | let count = min(5, self.collections.count) 261 | var frame = self.albumPopView.popupView.frame 262 | frame.size.height = CGFloat(count * 75) 263 | self.albumPopView.popupViewHeight.constant = CGFloat(count * 75) 264 | UIView.animate(withDuration: self.albumPopView.show ? 0.1:0) { 265 | self.albumPopView.popupView.frame = frame 266 | self.albumPopView.setNeedsLayout() 267 | } 268 | self.albumPopView.tableView.reloadData() 269 | self.albumPopView.setupPopupFrame() 270 | } 271 | 272 | fileprivate func initPhotoLibrary() { 273 | if PHPhotoLibrary.authorizationStatus() == .authorized { 274 | self.photoLibrary.delegate = self 275 | self.photoLibrary.fetchCollection(allowedVideo: self.allowedVideo, addCameraAsset: self.usedCameraButton, mediaType: self.configure.mediaType) 276 | }else{ 277 | //self.dismiss(animated: true, completion: nil) 278 | } 279 | } 280 | 281 | fileprivate func getfocusedIndex() -> Int { 282 | guard let focused = self.focusedCollection, let result = self.collections.index(where: { $0 == focused }) else { return 0 } 283 | return result 284 | } 285 | 286 | fileprivate func focused(collection: TLAssetsCollection) { 287 | func resetRequest() { 288 | cancelAllCloudRequest() 289 | cancelAllImageAssets() 290 | } 291 | resetRequest() 292 | self.collections[getfocusedIndex()].recentPosition = self.collectionView.contentOffset 293 | var reloadIndexPaths = [IndexPath(row: getfocusedIndex(), section: 0)] 294 | self.focusedCollection = collection 295 | reloadIndexPaths.append(IndexPath(row: getfocusedIndex(), section: 0)) 296 | self.albumPopView.tableView.reloadRows(at: reloadIndexPaths, with: .none) 297 | self.albumPopView.show(false, duration: 0.2) 298 | self.updateTitle() 299 | self.reloadCollectionView() 300 | self.collectionView.contentOffset = collection.recentPosition 301 | } 302 | 303 | // Asset Request 304 | fileprivate func requestCloudDownload(asset: TLPHAsset, indexPath: IndexPath) { 305 | if asset.state != .complete { 306 | var asset = asset 307 | asset.state = .ready 308 | guard let phAsset = asset.phAsset else { return } 309 | let requestId = self.photoLibrary.cloudImageDownload(asset: phAsset, progressBlock: { [weak self] (progress) in 310 | guard let `self` = self else { return } 311 | if asset.state == .ready { 312 | asset.state = .progress 313 | if let index = self.selectedAssets.index(where: { $0.phAsset == phAsset }) { 314 | self.selectedAssets[index] = asset 315 | } 316 | guard self.collectionView.indexPathsForVisibleItems.contains(indexPath) else { return } 317 | guard let cell = self.collectionView.cellForItem(at: indexPath) as? TLPhotoCollectionViewCell else { return } 318 | cell.indicator?.startAnimating() 319 | } 320 | }, completionBlock: { [weak self] image in 321 | guard let `self` = self else { return } 322 | asset.state = .complete 323 | if let index = self.selectedAssets.index(where: { $0.phAsset == phAsset }) { 324 | self.selectedAssets[index] = asset 325 | } 326 | self.cloudRequestIds.removeValue(forKey: indexPath) 327 | guard self.collectionView.indexPathsForVisibleItems.contains(indexPath) else { return } 328 | guard let cell = self.collectionView.cellForItem(at: indexPath) as? TLPhotoCollectionViewCell else { return } 329 | cell.imageView?.image = image 330 | cell.indicator?.stopAnimating() 331 | }) 332 | if requestId > 0 { 333 | self.cloudRequestIds[indexPath] = requestId 334 | } 335 | } 336 | } 337 | 338 | fileprivate func cancelCloudRequest(indexPath: IndexPath) { 339 | guard let requestId = self.cloudRequestIds[indexPath] else { return } 340 | self.cloudRequestIds.removeValue(forKey: indexPath) 341 | self.photoLibrary.cancelPHImageRequest(requestId: requestId) 342 | } 343 | 344 | fileprivate func cancelAllCloudRequest() { 345 | for (_,requestId) in self.cloudRequestIds { 346 | self.photoLibrary.cancelPHImageRequest(requestId: requestId) 347 | } 348 | self.cloudRequestIds.removeAll() 349 | } 350 | 351 | fileprivate func cancelAllImageAssets() { 352 | for (_,requestId) in self.requestIds { 353 | self.photoLibrary.cancelPHImageRequest(requestId: requestId) 354 | } 355 | self.requestIds.removeAll() 356 | } 357 | 358 | // User Action 359 | func titleTap() { 360 | guard collections.count > 0 else { return } 361 | self.albumPopView.show(self.albumPopView.isHidden) 362 | } 363 | 364 | func cancelButtonTap() { 365 | self.dismiss(done: false) 366 | } 367 | 368 | func doneButtonTap() { 369 | self.dismiss(done: true) 370 | } 371 | 372 | fileprivate func dismiss(done: Bool) { 373 | if done { 374 | self.delegate?.dismissPhotoPicker(withPHAssets: self.selectedAssets.flatMap{ $0.phAsset }) 375 | self.delegate?.dismissPhotoPicker(withTLPHAssets: self.selectedAssets) 376 | self.completionWithTLPHAssets?(self.selectedAssets) 377 | self.completionWithPHAssets?(self.selectedAssets.flatMap{ $0.phAsset }) 378 | }else { 379 | self.delegate?.photoPickerDidCancel() 380 | self.didCancel?() 381 | self.navigationController?.popViewController(animated: true) 382 | } 383 | self.delegate?.dismissComplete() 384 | // self.dismissCompletion!() 385 | // self.dismiss(animated: true) { [weak self] _ in 386 | // self?.delegate?.dismissComplete() 387 | // self?.dismissCompletion?() 388 | // } 389 | } 390 | fileprivate func maxCheck() -> Bool { 391 | if let max = self.configure.maxSelectedAssets, max <= self.selectedAssets.count { 392 | self.delegate?.didExceedMaximumNumberOfSelection(picker: self) 393 | self.didExceedMaximumNumberOfSelection?(self) 394 | return true 395 | } 396 | return false 397 | } 398 | } 399 | 400 | // MARK: - TLPhotoLibraryDelegate 401 | extension TLPhotosPickerViewController: TLPhotoLibraryDelegate { 402 | func loadCameraRollCollection(collection: TLAssetsCollection) { 403 | if let focused = self.focusedCollection, focused == collection { 404 | focusCollection(collection: collection) 405 | } 406 | self.collections = [collection] 407 | self.indicator.stopAnimating() 408 | self.reloadCollectionView() 409 | self.reloadTableView() 410 | } 411 | 412 | func loadCompleteAllCollection(collections: [TLAssetsCollection]) { 413 | self.collections = collections 414 | self.reloadTableView() 415 | } 416 | 417 | func focusCollection(collection: TLAssetsCollection) { 418 | self.focusedCollection = collection 419 | self.updateTitle() 420 | } 421 | } 422 | 423 | // MARK: - Camera Picker 424 | extension TLPhotosPickerViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { 425 | fileprivate func showCamera() { 426 | guard !maxCheck() else { return } 427 | let picker = UIImagePickerController() 428 | picker.sourceType = .camera 429 | picker.allowsEditing = false 430 | picker.delegate = self 431 | self.present(picker, animated: true, completion: nil) 432 | } 433 | 434 | public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { 435 | picker.dismiss(animated: true, completion: nil) 436 | } 437 | 438 | public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { 439 | if let image = (info[UIImagePickerControllerOriginalImage] as? UIImage) { 440 | var placeholderAsset: PHObjectPlaceholder? = nil 441 | PHPhotoLibrary.shared().performChanges({ 442 | let newAssetRequest = PHAssetChangeRequest.creationRequestForAsset(from: image) 443 | placeholderAsset = newAssetRequest.placeholderForCreatedAsset 444 | }, completionHandler: { [weak self] (sucess, error) in 445 | if sucess, let `self` = self, let identifier = placeholderAsset?.localIdentifier { 446 | guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject else { return } 447 | var result = TLPHAsset(asset: asset) 448 | result.selectedOrder = self.selectedAssets.count + 1 449 | self.selectedAssets.append(result) 450 | self.dismiss(done: true) 451 | } 452 | }) 453 | } 454 | picker.dismiss(animated: true, completion: nil) 455 | } 456 | } 457 | 458 | // MARK: - UICollectionView Scroll Delegate 459 | extension TLPhotosPickerViewController { 460 | public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 461 | if !decelerate { 462 | videoCheck() 463 | } 464 | } 465 | 466 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 467 | videoCheck() 468 | } 469 | 470 | fileprivate func videoCheck() { 471 | func play(asset: (IndexPath,TLPHAsset)) { 472 | if self.playRequestId?.indexPath != asset.0 { 473 | playVideo(asset: asset.1, indexPath: asset.0) 474 | } 475 | } 476 | guard self.allowedVideo || self.allowedLivePhotos else { return } 477 | guard self.playRequestId == nil else { return } 478 | guard let boundAssets = (getVisibleBoundAsset()?.filter{ $0.1.type != .photo }) else { return } 479 | if let firstSelectedVideoAsset = (boundAssets.filter{ getSelectedAssets($0.1) != nil }.first) { 480 | play(asset: firstSelectedVideoAsset) 481 | }else if let firstVideoAsset = boundAssets.first { 482 | play(asset: firstVideoAsset) 483 | } 484 | 485 | } 486 | } 487 | // MARK: - Video & LivePhotos Control PHLivePhotoViewDelegate 488 | extension TLPhotosPickerViewController: PHLivePhotoViewDelegate { 489 | fileprivate func stopPlay() { 490 | guard let playRequest = self.playRequestId else { return } 491 | self.playRequestId = nil 492 | guard let cell = self.collectionView.cellForItem(at: playRequest.indexPath) as? TLPhotoCollectionViewCell else { return } 493 | cell.stopPlay() 494 | } 495 | 496 | fileprivate func playVideo(asset: TLPHAsset, indexPath: IndexPath) { 497 | stopPlay() 498 | guard self.allowedVideo || self.allowedLivePhotos else { return } 499 | guard let phAsset = asset.phAsset else { return } 500 | if asset.type == .video { 501 | guard let cell = self.collectionView.cellForItem(at: indexPath) as? TLPhotoCollectionViewCell else { return } 502 | let requestId = self.photoLibrary.videoAsset(asset: phAsset, completionBlock: { (playerItem, info) in 503 | DispatchQueue.main.sync { [weak cell] _ in 504 | guard let cell = cell, cell.player == nil else { return } 505 | let player = AVPlayer(playerItem: playerItem) 506 | cell.player = player 507 | player.play() 508 | } 509 | }) 510 | if requestId > 0 { 511 | self.playRequestId = (indexPath,requestId) 512 | } 513 | }else if asset.type == .livePhoto, self.allowedLivePhotos { 514 | guard let cell = self.collectionView.cellForItem(at: indexPath) as? TLPhotoCollectionViewCell else { return } 515 | let requestId = self.photoLibrary.livePhotoAsset(asset: phAsset, size: self.thumbnailSize, completionBlock: { (livePhoto) in 516 | cell.livePhotoView?.isHidden = false 517 | cell.livePhotoView?.livePhoto = livePhoto 518 | cell.livePhotoView?.startPlayback(with: .hint) 519 | }) 520 | if requestId > 0 { 521 | self.playRequestId = (indexPath,requestId) 522 | } 523 | } 524 | } 525 | 526 | public func livePhotoView(_ livePhotoView: PHLivePhotoView, didEndPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) { 527 | livePhotoView.startPlayback(with: .hint) 528 | } 529 | 530 | public func livePhotoView(_ livePhotoView: PHLivePhotoView, willBeginPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) { 531 | } 532 | } 533 | 534 | 535 | // UICollectionView delegate & datasource 536 | extension TLPhotosPickerViewController: UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDataSourcePrefetching { 537 | fileprivate func getVisibleBoundAsset() -> [(IndexPath,TLPHAsset)]? { 538 | let visibleIndexPaths = self.collectionView.indexPathsForVisibleItems.sorted(by: { $0.0.row < $0.1.row }) 539 | guard let first = visibleIndexPaths.first?.row, let last = visibleIndexPaths.last?.row else { return nil } 540 | guard let boundAssets = self.focusedCollection?.assets[first...last] else { return nil } 541 | return boundAssets.enumerated().flatMap{ (index,asset) -> (IndexPath,TLPHAsset) in 542 | return (IndexPath(row: index+first, section: 0), asset) 543 | } 544 | } 545 | 546 | fileprivate func getSelectedAssets(_ asset: TLPHAsset) -> TLPHAsset? { 547 | if let index = self.selectedAssets.index(where: { $0.phAsset == asset.phAsset }) { 548 | return self.selectedAssets[index] 549 | } 550 | return nil 551 | } 552 | 553 | fileprivate func orderUpdateCells() { 554 | guard let visibleAssets = getVisibleBoundAsset() else { return } 555 | for (indexPath,asset) in visibleAssets { 556 | guard let cell = self.collectionView.cellForItem(at: indexPath) as? TLPhotoCollectionViewCell else { return } 557 | if let selectedAsset = getSelectedAssets(asset) { 558 | cell.selectedAsset = true 559 | cell.orderLabel?.text = "\(selectedAsset.selectedOrder)" 560 | }else { 561 | cell.selectedAsset = false 562 | } 563 | } 564 | } 565 | 566 | //Delegate 567 | public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 568 | guard var asset = self.focusedCollection?.assets[indexPath.row] else { return } 569 | if asset.camera { 570 | if Platform.isSimulator { 571 | print("not supported by the simulator.") 572 | return 573 | }else { 574 | showCamera() 575 | return 576 | } 577 | } 578 | guard let cell = self.collectionView.cellForItem(at: indexPath) as? TLPhotoCollectionViewCell else { return } 579 | cell.popScaleAnim() 580 | if let index = self.selectedAssets.index(where: { $0.phAsset == asset.phAsset }) { 581 | //deselect 582 | self.selectedAssets.remove(at: index) 583 | self.selectedAssets = self.selectedAssets.enumerated().flatMap({ (offset,asset) -> TLPHAsset? in 584 | var asset = asset 585 | asset.selectedOrder = offset + 1 586 | return asset 587 | }) 588 | self.orderUpdateCells() 589 | cancelCloudRequest(indexPath: indexPath) 590 | if self.playRequestId?.indexPath == indexPath { 591 | stopPlay() 592 | } 593 | }else { 594 | //select 595 | guard !maxCheck() else { return } 596 | asset.selectedOrder = self.selectedAssets.count + 1 597 | self.selectedAssets.append(asset) 598 | requestCloudDownload(asset: asset, indexPath: indexPath) 599 | cell.selectedAsset = true 600 | cell.orderLabel?.text = "\(asset.selectedOrder)" 601 | if asset.type != .photo { 602 | playVideo(asset: asset, indexPath: indexPath) 603 | } 604 | } 605 | } 606 | 607 | public func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 608 | if indexPath == self.playRequestId?.indexPath, let cell = cell as? TLPhotoCollectionViewCell { 609 | self.playRequestId = nil 610 | cell.stopPlay() 611 | } 612 | guard let requestId = self.requestIds[indexPath] else { return } 613 | self.requestIds.removeValue(forKey: indexPath) 614 | self.photoLibrary.cancelPHImageRequest(requestId: requestId) 615 | } 616 | 617 | //Datasource 618 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 619 | let nibName = self.configure.nibSet?.nibName ?? "TLPhotoCollectionViewCell" 620 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: nibName, for: indexPath) as! TLPhotoCollectionViewCell 621 | cell.configure = self.configure 622 | cell.imageView?.image = self.placeholderThumbnail 623 | guard let focusAssets = self.focusedCollection?.assets else { return cell } 624 | let asset = focusAssets[indexPath.row] 625 | cell.isCameraCell = asset.camera && self.usedCameraButton 626 | if cell.isCameraCell { 627 | cell.imageView?.image = self.cameraImage 628 | return cell 629 | } 630 | if let selectedAsset = getSelectedAssets(asset) { 631 | cell.selectedAsset = true 632 | cell.orderLabel?.text = "\(selectedAsset.selectedOrder)" 633 | }else{ 634 | cell.selectedAsset = false 635 | } 636 | if asset.state == .progress { 637 | cell.indicator?.startAnimating() 638 | }else { 639 | cell.indicator?.stopAnimating() 640 | } 641 | if let phAsset = asset.phAsset { 642 | if self.usedPrefetch { 643 | let options = PHImageRequestOptions() 644 | options.deliveryMode = .opportunistic 645 | options.isNetworkAccessAllowed = true 646 | self.photoLibrary.imageAsset(asset: phAsset, size: self.thumbnailSize, options: options) { [weak cell] image in 647 | cell?.imageView?.image = image 648 | } 649 | }else { 650 | queue.async { [weak self, weak cell] _ in 651 | guard let `self` = self else { return } 652 | let requestId = self.photoLibrary.imageAsset(asset: phAsset, size: self.thumbnailSize, completionBlock: { image in 653 | cell?.imageView?.image = image 654 | if self.allowedVideo { 655 | cell?.durationView?.isHidden = asset.type != .video 656 | cell?.duration = asset.type == .video ? phAsset.duration : nil 657 | } 658 | self.requestIds.removeValue(forKey: indexPath) 659 | }) 660 | if requestId > 0 { 661 | self.requestIds[indexPath] = requestId 662 | } 663 | } 664 | } 665 | if self.allowedLivePhotos { 666 | cell.liveBadgeImageView?.image = asset.type == .livePhoto ? PHLivePhotoView.livePhotoBadgeImage(options: .overContent) : nil 667 | cell.livePhotoView?.delegate = asset.type == .livePhoto ? self : nil 668 | } 669 | } 670 | cell.alpha = 0 671 | UIView.transition(with: cell, duration: 0.1, options: .curveEaseIn, animations: { 672 | cell.alpha = 1 673 | }, completion: nil) 674 | return cell 675 | } 676 | 677 | public func numberOfSections(in collectionView: UICollectionView) -> Int { 678 | return 1 679 | } 680 | 681 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 682 | return self.focusedCollection?.assets.count ?? 0 683 | } 684 | 685 | //Prefetch 686 | public func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { 687 | if self.usedPrefetch { 688 | queue.async { [weak self] _ in 689 | guard let `self` = self, let focusAssets = self.focusedCollection?.assets else { return } 690 | if indexPaths.count <= focusAssets.count { 691 | self.photoLibrary.imageManager.startCachingImages(for: indexPaths.flatMap{ focusAssets[$0.item].phAsset }, targetSize: self.thumbnailSize, contentMode: .aspectFill, options: nil) 692 | } 693 | } 694 | } 695 | } 696 | 697 | public func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) { 698 | if self.usedPrefetch { 699 | for indexPath in indexPaths { 700 | guard let requestId = self.requestIds[indexPath] else { continue } 701 | self.photoLibrary.cancelPHImageRequest(requestId: requestId) 702 | self.requestIds.removeValue(forKey: indexPath) 703 | } 704 | queue.async { [weak self] _ in 705 | guard let `self` = self, let focusAssets = self.focusedCollection?.assets else { return } 706 | if indexPaths.count <= focusAssets.count { 707 | self.photoLibrary.imageManager.stopCachingImages(for: indexPaths.flatMap{ focusAssets[$0.item].phAsset }, targetSize: self.thumbnailSize, contentMode: .aspectFill, options: nil) 708 | } 709 | } 710 | } 711 | } 712 | } 713 | 714 | // MARK: - UITableView datasource & delegate 715 | extension TLPhotosPickerViewController: UITableViewDelegate,UITableViewDataSource { 716 | //delegate 717 | public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 718 | self.focused(collection: self.collections[indexPath.row]) 719 | } 720 | 721 | //datasource 722 | public func numberOfSections(in tableView: UITableView) -> Int { 723 | return 1 724 | } 725 | 726 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 727 | return self.collections.count 728 | } 729 | 730 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 731 | let cell = tableView.dequeueReusableCell(withIdentifier: "TLCollectionTableViewCell", for: indexPath) as! TLCollectionTableViewCell 732 | let collection = self.collections[indexPath.row] 733 | cell.thumbImageView.image = collection.thumbnail 734 | cell.titleLabel.text = collection.title 735 | cell.subTitleLabel.text = "\(collection.assets.count)" 736 | if let phAsset = (collection.assets.filter{ !$0.camera }.first?.phAsset), collection.thumbnail == nil { 737 | let scale = UIScreen.main.scale 738 | let size = CGSize(width: 80*scale, height: 80*scale) 739 | self.photoLibrary.imageAsset(asset: phAsset, size: size, completionBlock: { [weak cell] image in 740 | cell?.thumbImageView.image = image 741 | }) 742 | } 743 | cell.accessoryType = getfocusedIndex() == indexPath.row ? .checkmark : .none 744 | cell.selectionStyle = .none 745 | return cell 746 | } 747 | } 748 | -------------------------------------------------------------------------------- /Fusion/Widget/PhotosPicker/TLPhotosPickerViewController.xib: -------------------------------------------------------------------------------- 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 | 43 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 DevinShine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fusion 2 | 3 | 长图拼接 beta 版本,核心算法在 DVSFusion 文件当中,还有些条件没考虑,有时间再弄~ :) 4 | 5 | ## Effect 6 | ![](http://upload-images.jianshu.io/upload_images/119657-e4aff434379cd606.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 7 | 8 | ![](http://upload-images.jianshu.io/upload_images/119657-28c692f9c0caaaa2.gif?imageMogr2/auto-orient/strip) 9 | 10 | ## License 11 | Fusion is provided under the MIT license. See LICENSE file for details. 12 | --------------------------------------------------------------------------------