├── .gitignore ├── LICENSE ├── PhotoPicker.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── PhotoPicker ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── arrow_back.imageset │ │ ├── Contents.json │ │ └── navi_back@2x.png │ ├── image_add.imageset │ │ ├── Contents.json │ │ └── image_add@3x.png │ ├── image_select.imageset │ │ ├── Contents.json │ │ ├── image_select@2x.png │ │ └── image_select@3x.png │ ├── picture_select.imageset │ │ ├── Contents.json │ │ └── picture_select@2x.png │ └── picture_unselect.imageset │ │ ├── Contents.json │ │ └── picture_unselect@2x.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── PhotoImageModel.swift ├── SinglePhotoPreviewViewController.swift ├── ViewController.swift ├── core │ ├── AlbumToolbarView.swift │ ├── ImageModel.swift │ ├── PhotoAlbumTableViewCell.swift │ ├── PhotoAlbumTableViewCell.xib │ ├── PhotoAlbumsTableViewController.swift │ ├── PhotoCollectionViewController.swift │ ├── PhotoFetchOptions.swift │ ├── PhotoImage.swift │ ├── PhotoImageManager.swift │ ├── PhotoPickerConfig.swift │ ├── PhotoPickerController.swift │ ├── PhotoPreviewBottomBarView.swift │ ├── PhotoPreviewCell.swift │ ├── PhotoPreviewToolbarView.swift │ ├── PhotoPreviewViewController.swift │ ├── UICollectionView+Extension.swift │ ├── UIImage+Extension.swift │ ├── photoCollectionViewCell.swift │ └── photoCollectionViewCell.xib └── en.lproj │ └── Main.strings ├── README.md └── swift-wechar-photo-picker.gif /.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 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/screenshots 64 | 65 | .DS_Store 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PhotoPicker.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CE019C571C8BF4A900DBAB3B /* PhotoAlbumTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE019C561C8BF4A900DBAB3B /* PhotoAlbumTableViewCell.xib */; }; 11 | CE019C5C1C8BFA7300DBAB3B /* PhotoAlbumTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE019C5B1C8BFA7300DBAB3B /* PhotoAlbumTableViewCell.swift */; }; 12 | CE019C5E1C8C05DB00DBAB3B /* PhotoPickerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE019C5D1C8C05DB00DBAB3B /* PhotoPickerConfig.swift */; }; 13 | CE019C601C8C127C00DBAB3B /* PhotoCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE019C5F1C8C127C00DBAB3B /* PhotoCollectionViewController.swift */; }; 14 | CE019C641C8C504B00DBAB3B /* photoCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE019C621C8C504B00DBAB3B /* photoCollectionViewCell.swift */; }; 15 | CE019C651C8C504B00DBAB3B /* photoCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE019C631C8C504B00DBAB3B /* photoCollectionViewCell.xib */; }; 16 | CE019C671C8C7A1D00DBAB3B /* PhotoPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE019C661C8C7A1D00DBAB3B /* PhotoPickerController.swift */; }; 17 | CE9106861C8FDE190055DE61 /* PhotoPreviewToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9106851C8FDE190055DE61 /* PhotoPreviewToolbarView.swift */; }; 18 | CE9106881C900E1A0055DE61 /* PhotoPreviewBottomBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9106871C900E1A0055DE61 /* PhotoPreviewBottomBarView.swift */; }; 19 | CE9F28441C8D54F0004F3BA2 /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9F28431C8D54F0004F3BA2 /* UIImage+Extension.swift */; }; 20 | CE9FDF111C943A160051848A /* SinglePhotoPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9FDF101C943A160051848A /* SinglePhotoPreviewViewController.swift */; }; 21 | CEA139DC1C8ACCD1003AEBC4 /* PhotoAlbumsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA139DB1C8ACCD1003AEBC4 /* PhotoAlbumsTableViewController.swift */; }; 22 | CEAAA63C1C8E83E80044336B /* AlbumToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEAAA63B1C8E83E80044336B /* AlbumToolbarView.swift */; }; 23 | CEAAA63E1C8EAB950044336B /* PhotoPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEAAA63D1C8EAB950044336B /* PhotoPreviewViewController.swift */; }; 24 | CEAAA6401C8EBFD50044336B /* PhotoPreviewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEAAA63F1C8EBFD50044336B /* PhotoPreviewCell.swift */; }; 25 | CEAAA6431C8ECE0F0044336B /* PhotoImageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEAAA6421C8ECE0F0044336B /* PhotoImageManager.swift */; }; 26 | CEBCC8B01C893D5A000BD551 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBCC8AF1C893D5A000BD551 /* AppDelegate.swift */; }; 27 | CEBCC8B21C893D5A000BD551 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBCC8B11C893D5A000BD551 /* ViewController.swift */; }; 28 | CEBCC8B51C893D5A000BD551 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CEBCC8B31C893D5A000BD551 /* Main.storyboard */; }; 29 | CEBCC8B71C893D5A000BD551 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CEBCC8B61C893D5A000BD551 /* Assets.xcassets */; }; 30 | CEBCC8BA1C893D5A000BD551 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CEBCC8B81C893D5A000BD551 /* LaunchScreen.storyboard */; }; 31 | CEC69ED81C927CD1008A4C4A /* ImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC69ED71C927CD1008A4C4A /* ImageModel.swift */; }; 32 | CEC69EDA1C92A886008A4C4A /* PhotoFetchOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC69ED91C92A886008A4C4A /* PhotoFetchOptions.swift */; }; 33 | CEC69EDC1C92C027008A4C4A /* PhotoImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC69EDB1C92C027008A4C4A /* PhotoImageModel.swift */; }; 34 | CEE098891C91BEE40048FEF4 /* UICollectionView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE098881C91BEE40048FEF4 /* UICollectionView+Extension.swift */; }; 35 | CEF3B1051C906AFC00406590 /* PhotoImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF3B1041C906AFC00406590 /* PhotoImage.swift */; }; 36 | /* End PBXBuildFile section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | CE019C561C8BF4A900DBAB3B /* PhotoAlbumTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PhotoAlbumTableViewCell.xib; sourceTree = ""; }; 40 | CE019C5B1C8BFA7300DBAB3B /* PhotoAlbumTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoAlbumTableViewCell.swift; sourceTree = ""; }; 41 | CE019C5D1C8C05DB00DBAB3B /* PhotoPickerConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoPickerConfig.swift; sourceTree = ""; }; 42 | CE019C5F1C8C127C00DBAB3B /* PhotoCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionViewController.swift; sourceTree = ""; }; 43 | CE019C621C8C504B00DBAB3B /* photoCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = photoCollectionViewCell.swift; sourceTree = ""; }; 44 | CE019C631C8C504B00DBAB3B /* photoCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = photoCollectionViewCell.xib; sourceTree = ""; }; 45 | CE019C661C8C7A1D00DBAB3B /* PhotoPickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoPickerController.swift; sourceTree = ""; }; 46 | CE9106851C8FDE190055DE61 /* PhotoPreviewToolbarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoPreviewToolbarView.swift; sourceTree = ""; }; 47 | CE9106871C900E1A0055DE61 /* PhotoPreviewBottomBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoPreviewBottomBarView.swift; sourceTree = ""; }; 48 | CE9F28431C8D54F0004F3BA2 /* UIImage+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = ""; }; 49 | CE9FDF101C943A160051848A /* SinglePhotoPreviewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SinglePhotoPreviewViewController.swift; sourceTree = ""; }; 50 | CEA139DB1C8ACCD1003AEBC4 /* PhotoAlbumsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoAlbumsTableViewController.swift; sourceTree = ""; }; 51 | CEAAA63B1C8E83E80044336B /* AlbumToolbarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumToolbarView.swift; sourceTree = ""; }; 52 | CEAAA63D1C8EAB950044336B /* PhotoPreviewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoPreviewViewController.swift; sourceTree = ""; }; 53 | CEAAA63F1C8EBFD50044336B /* PhotoPreviewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoPreviewCell.swift; sourceTree = ""; }; 54 | CEAAA6421C8ECE0F0044336B /* PhotoImageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoImageManager.swift; sourceTree = ""; }; 55 | CEBCC8AC1C893D5A000BD551 /* PhotoPicker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PhotoPicker.app; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | CEBCC8AF1C893D5A000BD551 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 57 | CEBCC8B11C893D5A000BD551 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 58 | CEBCC8B41C893D5A000BD551 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 59 | CEBCC8B61C893D5A000BD551 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 60 | CEBCC8B91C893D5A000BD551 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 61 | CEBCC8BB1C893D5A000BD551 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 62 | CEC69ED71C927CD1008A4C4A /* ImageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageModel.swift; sourceTree = ""; }; 63 | CEC69ED91C92A886008A4C4A /* PhotoFetchOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoFetchOptions.swift; sourceTree = ""; }; 64 | CEC69EDB1C92C027008A4C4A /* PhotoImageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoImageModel.swift; sourceTree = ""; }; 65 | CEE098881C91BEE40048FEF4 /* UICollectionView+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extension.swift"; sourceTree = ""; }; 66 | CEF3B1041C906AFC00406590 /* PhotoImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoImage.swift; sourceTree = ""; }; 67 | /* End PBXFileReference section */ 68 | 69 | /* Begin PBXFrameworksBuildPhase section */ 70 | CEBCC8A91C893D5A000BD551 /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXFrameworksBuildPhase section */ 78 | 79 | /* Begin PBXGroup section */ 80 | CE019C581C8BF95000DBAB3B /* albumTableViewCell */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | CE019C561C8BF4A900DBAB3B /* PhotoAlbumTableViewCell.xib */, 84 | CE019C5B1C8BFA7300DBAB3B /* PhotoAlbumTableViewCell.swift */, 85 | ); 86 | name = albumTableViewCell; 87 | sourceTree = ""; 88 | }; 89 | CE019C611C8C4FD200DBAB3B /* photoCollectionViewCell */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | CE019C621C8C504B00DBAB3B /* photoCollectionViewCell.swift */, 93 | CE019C631C8C504B00DBAB3B /* photoCollectionViewCell.xib */, 94 | ); 95 | name = photoCollectionViewCell; 96 | sourceTree = ""; 97 | }; 98 | CE9F28421C8D54BB004F3BA2 /* extension */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | CE9F28431C8D54F0004F3BA2 /* UIImage+Extension.swift */, 102 | CEE098881C91BEE40048FEF4 /* UICollectionView+Extension.swift */, 103 | ); 104 | name = extension; 105 | sourceTree = ""; 106 | }; 107 | CEAAA6381C8E80F90044336B /* view */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | CEAAA63B1C8E83E80044336B /* AlbumToolbarView.swift */, 111 | CE9106851C8FDE190055DE61 /* PhotoPreviewToolbarView.swift */, 112 | CE9106871C900E1A0055DE61 /* PhotoPreviewBottomBarView.swift */, 113 | ); 114 | name = view; 115 | sourceTree = ""; 116 | }; 117 | CEAAA6411C8EBFFB0044336B /* PhotoPreviewCell */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | CEAAA63F1C8EBFD50044336B /* PhotoPreviewCell.swift */, 121 | ); 122 | name = PhotoPreviewCell; 123 | sourceTree = ""; 124 | }; 125 | CEBCC8A31C893D5A000BD551 = { 126 | isa = PBXGroup; 127 | children = ( 128 | CEBCC8AE1C893D5A000BD551 /* PhotoPicker */, 129 | CEBCC8AD1C893D5A000BD551 /* Products */, 130 | ); 131 | sourceTree = ""; 132 | }; 133 | CEBCC8AD1C893D5A000BD551 /* Products */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | CEBCC8AC1C893D5A000BD551 /* PhotoPicker.app */, 137 | ); 138 | name = Products; 139 | sourceTree = ""; 140 | }; 141 | CEBCC8AE1C893D5A000BD551 /* PhotoPicker */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | CEBCC8C11C8946B8000BD551 /* core */, 145 | CEBCC8AF1C893D5A000BD551 /* AppDelegate.swift */, 146 | CEBCC8B11C893D5A000BD551 /* ViewController.swift */, 147 | CEBCC8B31C893D5A000BD551 /* Main.storyboard */, 148 | CEBCC8B61C893D5A000BD551 /* Assets.xcassets */, 149 | CEBCC8B81C893D5A000BD551 /* LaunchScreen.storyboard */, 150 | CEBCC8BB1C893D5A000BD551 /* Info.plist */, 151 | CEC69EDB1C92C027008A4C4A /* PhotoImageModel.swift */, 152 | CE9FDF101C943A160051848A /* SinglePhotoPreviewViewController.swift */, 153 | ); 154 | path = PhotoPicker; 155 | sourceTree = ""; 156 | }; 157 | CEBCC8C11C8946B8000BD551 /* core */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | CEC69ED61C927C45008A4C4A /* model */, 161 | CEAAA6411C8EBFFB0044336B /* PhotoPreviewCell */, 162 | CEAAA6381C8E80F90044336B /* view */, 163 | CE9F28421C8D54BB004F3BA2 /* extension */, 164 | CE019C611C8C4FD200DBAB3B /* photoCollectionViewCell */, 165 | CE019C581C8BF95000DBAB3B /* albumTableViewCell */, 166 | CEA139DB1C8ACCD1003AEBC4 /* PhotoAlbumsTableViewController.swift */, 167 | CE019C5D1C8C05DB00DBAB3B /* PhotoPickerConfig.swift */, 168 | CE019C5F1C8C127C00DBAB3B /* PhotoCollectionViewController.swift */, 169 | CE019C661C8C7A1D00DBAB3B /* PhotoPickerController.swift */, 170 | CEAAA63D1C8EAB950044336B /* PhotoPreviewViewController.swift */, 171 | CEAAA6421C8ECE0F0044336B /* PhotoImageManager.swift */, 172 | CEF3B1041C906AFC00406590 /* PhotoImage.swift */, 173 | CEC69ED91C92A886008A4C4A /* PhotoFetchOptions.swift */, 174 | ); 175 | path = core; 176 | sourceTree = ""; 177 | }; 178 | CEC69ED61C927C45008A4C4A /* model */ = { 179 | isa = PBXGroup; 180 | children = ( 181 | CEC69ED71C927CD1008A4C4A /* ImageModel.swift */, 182 | ); 183 | name = model; 184 | sourceTree = ""; 185 | }; 186 | /* End PBXGroup section */ 187 | 188 | /* Begin PBXNativeTarget section */ 189 | CEBCC8AB1C893D5A000BD551 /* PhotoPicker */ = { 190 | isa = PBXNativeTarget; 191 | buildConfigurationList = CEBCC8BE1C893D5A000BD551 /* Build configuration list for PBXNativeTarget "PhotoPicker" */; 192 | buildPhases = ( 193 | CEBCC8A81C893D5A000BD551 /* Sources */, 194 | CEBCC8A91C893D5A000BD551 /* Frameworks */, 195 | CEBCC8AA1C893D5A000BD551 /* Resources */, 196 | ); 197 | buildRules = ( 198 | ); 199 | dependencies = ( 200 | ); 201 | name = PhotoPicker; 202 | productName = PhotoPicker; 203 | productReference = CEBCC8AC1C893D5A000BD551 /* PhotoPicker.app */; 204 | productType = "com.apple.product-type.application"; 205 | }; 206 | /* End PBXNativeTarget section */ 207 | 208 | /* Begin PBXProject section */ 209 | CEBCC8A41C893D5A000BD551 /* Project object */ = { 210 | isa = PBXProject; 211 | attributes = { 212 | LastSwiftUpdateCheck = 0720; 213 | LastUpgradeCheck = 0800; 214 | ORGANIZATIONNAME = dailyios; 215 | TargetAttributes = { 216 | CEBCC8AB1C893D5A000BD551 = { 217 | CreatedOnToolsVersion = 7.2; 218 | DevelopmentTeam = Z865YP4AC3; 219 | LastSwiftMigration = 0800; 220 | }; 221 | }; 222 | }; 223 | buildConfigurationList = CEBCC8A71C893D5A000BD551 /* Build configuration list for PBXProject "PhotoPicker" */; 224 | compatibilityVersion = "Xcode 3.2"; 225 | developmentRegion = English; 226 | hasScannedForEncodings = 0; 227 | knownRegions = ( 228 | en, 229 | Base, 230 | ); 231 | mainGroup = CEBCC8A31C893D5A000BD551; 232 | productRefGroup = CEBCC8AD1C893D5A000BD551 /* Products */; 233 | projectDirPath = ""; 234 | projectRoot = ""; 235 | targets = ( 236 | CEBCC8AB1C893D5A000BD551 /* PhotoPicker */, 237 | ); 238 | }; 239 | /* End PBXProject section */ 240 | 241 | /* Begin PBXResourcesBuildPhase section */ 242 | CEBCC8AA1C893D5A000BD551 /* Resources */ = { 243 | isa = PBXResourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | CEBCC8BA1C893D5A000BD551 /* LaunchScreen.storyboard in Resources */, 247 | CEBCC8B71C893D5A000BD551 /* Assets.xcassets in Resources */, 248 | CE019C571C8BF4A900DBAB3B /* PhotoAlbumTableViewCell.xib in Resources */, 249 | CE019C651C8C504B00DBAB3B /* photoCollectionViewCell.xib in Resources */, 250 | CEBCC8B51C893D5A000BD551 /* Main.storyboard in Resources */, 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | }; 254 | /* End PBXResourcesBuildPhase section */ 255 | 256 | /* Begin PBXSourcesBuildPhase section */ 257 | CEBCC8A81C893D5A000BD551 /* Sources */ = { 258 | isa = PBXSourcesBuildPhase; 259 | buildActionMask = 2147483647; 260 | files = ( 261 | CEAAA63C1C8E83E80044336B /* AlbumToolbarView.swift in Sources */, 262 | CE019C5E1C8C05DB00DBAB3B /* PhotoPickerConfig.swift in Sources */, 263 | CE9106861C8FDE190055DE61 /* PhotoPreviewToolbarView.swift in Sources */, 264 | CEBCC8B21C893D5A000BD551 /* ViewController.swift in Sources */, 265 | CE019C671C8C7A1D00DBAB3B /* PhotoPickerController.swift in Sources */, 266 | CE9F28441C8D54F0004F3BA2 /* UIImage+Extension.swift in Sources */, 267 | CEAAA6401C8EBFD50044336B /* PhotoPreviewCell.swift in Sources */, 268 | CEBCC8B01C893D5A000BD551 /* AppDelegate.swift in Sources */, 269 | CE9FDF111C943A160051848A /* SinglePhotoPreviewViewController.swift in Sources */, 270 | CE019C601C8C127C00DBAB3B /* PhotoCollectionViewController.swift in Sources */, 271 | CEAAA6431C8ECE0F0044336B /* PhotoImageManager.swift in Sources */, 272 | CE019C5C1C8BFA7300DBAB3B /* PhotoAlbumTableViewCell.swift in Sources */, 273 | CE9106881C900E1A0055DE61 /* PhotoPreviewBottomBarView.swift in Sources */, 274 | CEC69EDC1C92C027008A4C4A /* PhotoImageModel.swift in Sources */, 275 | CE019C641C8C504B00DBAB3B /* photoCollectionViewCell.swift in Sources */, 276 | CEF3B1051C906AFC00406590 /* PhotoImage.swift in Sources */, 277 | CEE098891C91BEE40048FEF4 /* UICollectionView+Extension.swift in Sources */, 278 | CEAAA63E1C8EAB950044336B /* PhotoPreviewViewController.swift in Sources */, 279 | CEA139DC1C8ACCD1003AEBC4 /* PhotoAlbumsTableViewController.swift in Sources */, 280 | CEC69EDA1C92A886008A4C4A /* PhotoFetchOptions.swift in Sources */, 281 | CEC69ED81C927CD1008A4C4A /* ImageModel.swift in Sources */, 282 | ); 283 | runOnlyForDeploymentPostprocessing = 0; 284 | }; 285 | /* End PBXSourcesBuildPhase section */ 286 | 287 | /* Begin PBXVariantGroup section */ 288 | CEBCC8B31C893D5A000BD551 /* Main.storyboard */ = { 289 | isa = PBXVariantGroup; 290 | children = ( 291 | CEBCC8B41C893D5A000BD551 /* Base */, 292 | ); 293 | name = Main.storyboard; 294 | sourceTree = ""; 295 | }; 296 | CEBCC8B81C893D5A000BD551 /* LaunchScreen.storyboard */ = { 297 | isa = PBXVariantGroup; 298 | children = ( 299 | CEBCC8B91C893D5A000BD551 /* Base */, 300 | ); 301 | name = LaunchScreen.storyboard; 302 | sourceTree = ""; 303 | }; 304 | /* End PBXVariantGroup section */ 305 | 306 | /* Begin XCBuildConfiguration section */ 307 | CEBCC8BC1C893D5A000BD551 /* Debug */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ALWAYS_SEARCH_USER_PATHS = NO; 311 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 312 | CLANG_CXX_LIBRARY = "libc++"; 313 | CLANG_ENABLE_MODULES = YES; 314 | CLANG_ENABLE_OBJC_ARC = YES; 315 | CLANG_WARN_BOOL_CONVERSION = YES; 316 | CLANG_WARN_CONSTANT_CONVERSION = YES; 317 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 318 | CLANG_WARN_EMPTY_BODY = YES; 319 | CLANG_WARN_ENUM_CONVERSION = YES; 320 | CLANG_WARN_INFINITE_RECURSION = YES; 321 | CLANG_WARN_INT_CONVERSION = YES; 322 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 323 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 324 | CLANG_WARN_UNREACHABLE_CODE = YES; 325 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 326 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 327 | COPY_PHASE_STRIP = NO; 328 | DEBUG_INFORMATION_FORMAT = dwarf; 329 | ENABLE_STRICT_OBJC_MSGSEND = YES; 330 | ENABLE_TESTABILITY = YES; 331 | GCC_C_LANGUAGE_STANDARD = gnu99; 332 | GCC_DYNAMIC_NO_PIC = NO; 333 | GCC_NO_COMMON_BLOCKS = YES; 334 | GCC_OPTIMIZATION_LEVEL = 0; 335 | GCC_PREPROCESSOR_DEFINITIONS = ( 336 | "DEBUG=1", 337 | "$(inherited)", 338 | ); 339 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 340 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 341 | GCC_WARN_UNDECLARED_SELECTOR = YES; 342 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 343 | GCC_WARN_UNUSED_FUNCTION = YES; 344 | GCC_WARN_UNUSED_VARIABLE = YES; 345 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 346 | MTL_ENABLE_DEBUG_INFO = YES; 347 | ONLY_ACTIVE_ARCH = YES; 348 | SDKROOT = iphoneos; 349 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 350 | }; 351 | name = Debug; 352 | }; 353 | CEBCC8BD1C893D5A000BD551 /* Release */ = { 354 | isa = XCBuildConfiguration; 355 | buildSettings = { 356 | ALWAYS_SEARCH_USER_PATHS = NO; 357 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 358 | CLANG_CXX_LIBRARY = "libc++"; 359 | CLANG_ENABLE_MODULES = YES; 360 | CLANG_ENABLE_OBJC_ARC = YES; 361 | CLANG_WARN_BOOL_CONVERSION = YES; 362 | CLANG_WARN_CONSTANT_CONVERSION = YES; 363 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 364 | CLANG_WARN_EMPTY_BODY = YES; 365 | CLANG_WARN_ENUM_CONVERSION = YES; 366 | CLANG_WARN_INFINITE_RECURSION = YES; 367 | CLANG_WARN_INT_CONVERSION = YES; 368 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 369 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 370 | CLANG_WARN_UNREACHABLE_CODE = YES; 371 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 372 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 373 | COPY_PHASE_STRIP = NO; 374 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 375 | ENABLE_NS_ASSERTIONS = NO; 376 | ENABLE_STRICT_OBJC_MSGSEND = YES; 377 | GCC_C_LANGUAGE_STANDARD = gnu99; 378 | GCC_NO_COMMON_BLOCKS = YES; 379 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 380 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 381 | GCC_WARN_UNDECLARED_SELECTOR = YES; 382 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 383 | GCC_WARN_UNUSED_FUNCTION = YES; 384 | GCC_WARN_UNUSED_VARIABLE = YES; 385 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 386 | MTL_ENABLE_DEBUG_INFO = NO; 387 | SDKROOT = iphoneos; 388 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 389 | VALIDATE_PRODUCT = YES; 390 | }; 391 | name = Release; 392 | }; 393 | CEBCC8BF1C893D5A000BD551 /* Debug */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 397 | DEVELOPMENT_TEAM = Z865YP4AC3; 398 | INFOPLIST_FILE = PhotoPicker/Info.plist; 399 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 400 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 401 | PRODUCT_BUNDLE_IDENTIFIER = com.dailyios.PhotoPicker; 402 | PRODUCT_NAME = "$(TARGET_NAME)"; 403 | SWIFT_VERSION = 3.0; 404 | }; 405 | name = Debug; 406 | }; 407 | CEBCC8C01C893D5A000BD551 /* Release */ = { 408 | isa = XCBuildConfiguration; 409 | buildSettings = { 410 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 411 | DEVELOPMENT_TEAM = Z865YP4AC3; 412 | INFOPLIST_FILE = PhotoPicker/Info.plist; 413 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 414 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 415 | PRODUCT_BUNDLE_IDENTIFIER = com.dailyios.PhotoPicker; 416 | PRODUCT_NAME = "$(TARGET_NAME)"; 417 | SWIFT_VERSION = 3.0; 418 | }; 419 | name = Release; 420 | }; 421 | /* End XCBuildConfiguration section */ 422 | 423 | /* Begin XCConfigurationList section */ 424 | CEBCC8A71C893D5A000BD551 /* Build configuration list for PBXProject "PhotoPicker" */ = { 425 | isa = XCConfigurationList; 426 | buildConfigurations = ( 427 | CEBCC8BC1C893D5A000BD551 /* Debug */, 428 | CEBCC8BD1C893D5A000BD551 /* Release */, 429 | ); 430 | defaultConfigurationIsVisible = 0; 431 | defaultConfigurationName = Release; 432 | }; 433 | CEBCC8BE1C893D5A000BD551 /* Build configuration list for PBXNativeTarget "PhotoPicker" */ = { 434 | isa = XCConfigurationList; 435 | buildConfigurations = ( 436 | CEBCC8BF1C893D5A000BD551 /* Debug */, 437 | CEBCC8C01C893D5A000BD551 /* Release */, 438 | ); 439 | defaultConfigurationIsVisible = 0; 440 | defaultConfigurationName = Release; 441 | }; 442 | /* End XCConfigurationList section */ 443 | }; 444 | rootObject = CEBCC8A41C893D5A000BD551 /* Project object */; 445 | } 446 | -------------------------------------------------------------------------------- /PhotoPicker.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PhotoPicker/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/4. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { 17 | return true 18 | } 19 | 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /PhotoPicker/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | } 43 | ], 44 | "info" : { 45 | "version" : 1, 46 | "author" : "xcode" 47 | } 48 | } -------------------------------------------------------------------------------- /PhotoPicker/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /PhotoPicker/Assets.xcassets/arrow_back.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "navi_back@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /PhotoPicker/Assets.xcassets/arrow_back.imageset/navi_back@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptut/PhotoPicker/c2115c8a740e4a45b713a6932d82f1bbcb0f68bd/PhotoPicker/Assets.xcassets/arrow_back.imageset/navi_back@2x.png -------------------------------------------------------------------------------- /PhotoPicker/Assets.xcassets/image_add.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" : "image_add@3x.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /PhotoPicker/Assets.xcassets/image_add.imageset/image_add@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptut/PhotoPicker/c2115c8a740e4a45b713a6932d82f1bbcb0f68bd/PhotoPicker/Assets.xcassets/image_add.imageset/image_add@3x.png -------------------------------------------------------------------------------- /PhotoPicker/Assets.xcassets/image_select.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "image_select@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "image_select@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /PhotoPicker/Assets.xcassets/image_select.imageset/image_select@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptut/PhotoPicker/c2115c8a740e4a45b713a6932d82f1bbcb0f68bd/PhotoPicker/Assets.xcassets/image_select.imageset/image_select@2x.png -------------------------------------------------------------------------------- /PhotoPicker/Assets.xcassets/image_select.imageset/image_select@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptut/PhotoPicker/c2115c8a740e4a45b713a6932d82f1bbcb0f68bd/PhotoPicker/Assets.xcassets/image_select.imageset/image_select@3x.png -------------------------------------------------------------------------------- /PhotoPicker/Assets.xcassets/picture_select.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "picture_select@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /PhotoPicker/Assets.xcassets/picture_select.imageset/picture_select@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptut/PhotoPicker/c2115c8a740e4a45b713a6932d82f1bbcb0f68bd/PhotoPicker/Assets.xcassets/picture_select.imageset/picture_select@2x.png -------------------------------------------------------------------------------- /PhotoPicker/Assets.xcassets/picture_unselect.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "picture_unselect@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /PhotoPicker/Assets.xcassets/picture_unselect.imageset/picture_unselect@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptut/PhotoPicker/c2115c8a740e4a45b713a6932d82f1bbcb0f68bd/PhotoPicker/Assets.xcassets/picture_unselect.imageset/picture_unselect@2x.png -------------------------------------------------------------------------------- /PhotoPicker/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 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /PhotoPicker/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /PhotoPicker/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | zh_CN 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | UIViewControllerBasedStatusBarAppearance 38 | 39 | NSPhotoLibraryUsageDescription 40 | 41 | NSCameraUsageDescription 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /PhotoPicker/PhotoImageModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageModel.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/11. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Photos 11 | 12 | enum ModelType{ 13 | case Button 14 | case Image 15 | } 16 | 17 | struct PhotoImageModel: Equatable { 18 | var type: ModelType? 19 | var data: PHAsset? 20 | 21 | init(type: ModelType?,data:PHAsset?){ 22 | self.type = type 23 | self.data = data 24 | } 25 | 26 | static func ==(lhs: PhotoImageModel, rhs: PhotoImageModel) -> Bool { 27 | return lhs.type == rhs.type && lhs.data == rhs.data 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /PhotoPicker/SinglePhotoPreviewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SinglePhotoPreviewViewController.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/12. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | class SinglePhotoPreviewViewController: UIViewController,UICollectionViewDataSource,UICollectionViewDelegate,PhotoPreviewCellDelegate { 13 | 14 | var selectImages:[PhotoImageModel]? 15 | 16 | private var collectionView: UICollectionView? 17 | private let cellIdentifier = "cellIdentifier" 18 | var currentPage: Int = 0 19 | 20 | weak var sourceDelegate: ViewController? 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | self.navigationController?.navigationItem.backBarButtonItem = UIBarButtonItem.init(title: "back", style: .plain, target: self, action: nil) 25 | 26 | self.configNavigationBar() 27 | self.configCollectionView() 28 | } 29 | 30 | private func configNavigationBar(){ 31 | UIApplication.shared.statusBarStyle = .lightContent 32 | self.navigationController?.navigationBar.barStyle = .black 33 | self.navigationController?.navigationBar.tintColor = UIColor.white 34 | 35 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(SinglePhotoPreviewViewController.eventRemoveImage)) 36 | } 37 | 38 | /** 39 | * 删除预览图片操作 40 | */ 41 | func eventRemoveImage(){ 42 | let element = self.selectImages?.remove(at: self.currentPage) 43 | self.updatePageTitle() 44 | self.sourceDelegate?.removeElement(element: element) 45 | 46 | if (self.selectImages?.count)! > 0{ 47 | self.collectionView?.reloadData() 48 | } else { 49 | _ = self.navigationController?.popViewController(animated: true) 50 | } 51 | } 52 | 53 | override func viewWillAppear(_ animated: Bool) { 54 | super.viewWillAppear(animated) 55 | self.collectionView?.setContentOffset(CGPoint(x: CGFloat(self.currentPage) * self.view.bounds.width, y: 0), animated: false) 56 | self.updatePageTitle() 57 | } 58 | 59 | private func updatePageTitle(){ 60 | self.title = String(self.currentPage+1) + "/" + String(self.selectImages!.count) 61 | } 62 | 63 | func configCollectionView(){ 64 | self.automaticallyAdjustsScrollViewInsets = false 65 | let layout = UICollectionViewFlowLayout() 66 | layout.scrollDirection = .horizontal 67 | layout.itemSize = CGSize(width:self.view.frame.width,height: self.view.frame.height) 68 | layout.minimumInteritemSpacing = 0 69 | layout.minimumLineSpacing = 0 70 | 71 | self.collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout) 72 | self.collectionView!.dataSource = self 73 | self.collectionView!.delegate = self 74 | self.collectionView!.isPagingEnabled = true 75 | self.collectionView!.scrollsToTop = false 76 | self.collectionView!.showsHorizontalScrollIndicator = false 77 | self.collectionView!.contentOffset = CGPoint(x:0, y: 0) 78 | self.collectionView!.contentSize = CGSize(width: self.view.bounds.width * CGFloat(self.selectImages!.count), height: self.view.bounds.height) 79 | 80 | self.view.addSubview(self.collectionView!) 81 | self.collectionView!.register(PhotoPreviewCell.self, forCellWithReuseIdentifier: self.cellIdentifier) 82 | } 83 | 84 | // MARK: - collectionView dataSource delagate 85 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 86 | return self.selectImages!.count 87 | } 88 | 89 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 90 | 91 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cellIdentifier, for: indexPath) as! PhotoPreviewCell 92 | cell.delegate = self 93 | if let asset = self.selectImages?[indexPath.row] { 94 | cell.renderModel(asset: asset.data!) 95 | } 96 | 97 | return cell 98 | } 99 | 100 | // MARK: - Photo Preview Cell Delegate 101 | func onImageSingleTap() { 102 | let status = !UIApplication.shared.isStatusBarHidden 103 | UIApplication.shared.setStatusBarHidden(status, with: .slide) 104 | self.navigationController?.setNavigationBarHidden(status, animated: true) 105 | } 106 | 107 | // MARK: - scroll page 108 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 109 | let offset = scrollView.contentOffset 110 | self.currentPage = Int(offset.x / self.view.bounds.width) 111 | self.updatePageTitle() 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /PhotoPicker/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/4. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | import MobileCoreServices 12 | 13 | class ViewController: UIViewController,PhotoPickerControllerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate { 14 | 15 | var selectModel = [PhotoImageModel]() 16 | var containerView = UIView() 17 | 18 | var triggerRefresh = false 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | self.view.addSubview(self.containerView) 23 | self.checkNeedAddButton() 24 | self.renderView() 25 | } 26 | 27 | private func checkNeedAddButton(){ 28 | if self.selectModel.count < PhotoPickerController.imageMaxSelectedNum && !hasButton() { 29 | selectModel.append(PhotoImageModel(type: ModelType.Button, data: nil)) 30 | } 31 | } 32 | 33 | private func hasButton() -> Bool{ 34 | for item in self.selectModel { 35 | if item.type == ModelType.Button { 36 | return true 37 | } 38 | } 39 | return false 40 | } 41 | 42 | /** 43 | * 删除已选择图片数据 Model 44 | */ 45 | func removeElement(element: PhotoImageModel?){ 46 | if let current = element { 47 | self.selectModel = self.selectModel.filter({$0 != current}) 48 | self.triggerRefresh = true // 删除数据事出发重绘界面逻辑 49 | } 50 | } 51 | 52 | 53 | override func viewWillAppear(_ animated: Bool) { 54 | super.viewWillAppear(animated) 55 | UIApplication.shared.statusBarStyle = .default 56 | self.navigationController?.navigationBar.barStyle = .default 57 | if self.triggerRefresh { // 检测是否需要重绘界面 58 | self.triggerRefresh = false 59 | self.updateView() 60 | } 61 | } 62 | 63 | private func updateView(){ 64 | self.clearAll() 65 | self.checkNeedAddButton() 66 | self.renderView() 67 | } 68 | 69 | private func renderView(){ 70 | 71 | if selectModel.count <= 0 {return;} 72 | 73 | let totalWidth = UIScreen.main.bounds.width 74 | let space:CGFloat = 10 75 | let lineImageTotal = 4 76 | 77 | let line = self.selectModel.count / lineImageTotal 78 | let lastItems = self.selectModel.count % lineImageTotal 79 | 80 | let lessItemWidth = (totalWidth - (CGFloat(lineImageTotal) + 1) * space) 81 | let itemWidth = lessItemWidth / CGFloat(lineImageTotal) 82 | 83 | for i in 0 ..< line { 84 | let itemY = CGFloat(i+1) * space + CGFloat(i) * itemWidth 85 | for j in 0 ..< lineImageTotal { 86 | let itemX = CGFloat(j+1) * space + CGFloat(j) * itemWidth 87 | let index = i * lineImageTotal + j 88 | self.renderItemView(itemX: itemX, itemY: itemY, itemWidth: itemWidth, index: index) 89 | } 90 | } 91 | 92 | // last line 93 | for i in 0.. Void in 123 | if image != nil { 124 | button.setImage(image, for: UIControlState.normal) 125 | button.contentMode = .scaleAspectFill 126 | button.clipsToBounds = true 127 | } 128 | }) 129 | } 130 | } 131 | self.containerView.addSubview(button) 132 | } 133 | 134 | private func clearAll(){ 135 | for subview in self.containerView.subviews { 136 | if let view = subview as? UIButton { 137 | view.removeFromSuperview() 138 | } 139 | } 140 | } 141 | 142 | // MARK: - 按钮事件 143 | func eventPreview(button:UIButton){ 144 | let preview = SinglePhotoPreviewViewController() 145 | let data = self.getModelExceptButton() 146 | preview.selectImages = data 147 | preview.sourceDelegate = self 148 | preview.currentPage = button.tag 149 | self.show(preview, sender: nil) 150 | } 151 | 152 | 153 | // 页面底部 stylesheet 154 | func eventAddImage() { 155 | let alert = UIAlertController.init(title: nil, message: nil, preferredStyle: .actionSheet) 156 | 157 | // change the style sheet text color 158 | alert.view.tintColor = UIColor.black 159 | 160 | let actionCancel = UIAlertAction.init(title: "取消", style: .cancel, handler: nil) 161 | let actionCamera = UIAlertAction.init(title: "拍照", style: .default) { (UIAlertAction) -> Void in 162 | self.selectByCamera() 163 | } 164 | 165 | let actionPhoto = UIAlertAction.init(title: "从手机照片中选择", style: .default) { (UIAlertAction) -> Void in 166 | self.selectFromPhoto() 167 | } 168 | 169 | alert.addAction(actionCancel) 170 | alert.addAction(actionCamera) 171 | alert.addAction(actionPhoto) 172 | 173 | self.present(alert, animated: true, completion: nil) 174 | } 175 | 176 | // 拍照获取 177 | private func selectByCamera(){ 178 | let imagePicker = UIImagePickerController() 179 | imagePicker.sourceType = .camera // 调用摄像头 180 | imagePicker.cameraDevice = .rear // 后置摄像头拍照 181 | imagePicker.cameraCaptureMode = .photo // 拍照 182 | imagePicker.allowsEditing = true 183 | imagePicker.delegate = self 184 | imagePicker.mediaTypes = [kUTTypeImage as String] 185 | 186 | imagePicker.modalPresentationStyle = .popover 187 | self.show(imagePicker, sender: nil) 188 | } 189 | 190 | // MARK: - 拍照 delegate相关方法 191 | // 退出拍照 192 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { 193 | picker.dismiss(animated: true, completion: nil) 194 | } 195 | 196 | 197 | // 完成拍照 198 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { 199 | picker.dismiss(animated: true, completion: nil) 200 | let mediaType = info[UIImagePickerControllerMediaType] as! String; 201 | if mediaType == kUTTypeImage as String { // 图片类型 202 | var image: UIImage? = nil 203 | var localId: String? = "" 204 | 205 | if picker.isEditing { // 拍照图片运行编辑,则优先尝试从编辑后的类型中获取图片 206 | image = info[UIImagePickerControllerEditedImage] as? UIImage 207 | }else{ 208 | image = info[UIImagePickerControllerOriginalImage] as? UIImage 209 | } 210 | // 存入相册 211 | if image != nil { 212 | PHPhotoLibrary.shared().performChanges({ 213 | let result = PHAssetChangeRequest.creationRequestForAsset(from: image!) 214 | let assetPlaceholder = result.placeholderForCreatedAsset 215 | localId = assetPlaceholder?.localIdentifier 216 | }, completionHandler: { (success, error) in 217 | if success && localId != nil { 218 | let assetResult = PHAsset.fetchAssets(withLocalIdentifiers: [localId!], options: nil) 219 | let asset = assetResult[0] 220 | DispatchQueue.main.async { 221 | self.renderSelectImages(images: [asset]) 222 | } 223 | } 224 | }) 225 | } 226 | } 227 | } 228 | 229 | 230 | /** 231 | * 从相册中选择图片 232 | */ 233 | private func selectFromPhoto(){ 234 | PHPhotoLibrary.requestAuthorization {[unowned self] (status) -> Void in 235 | DispatchQueue.main.async { 236 | switch status { 237 | case .authorized: 238 | self.showLocalPhotoGallery() 239 | break 240 | default: 241 | self.showNoPermissionDailog() 242 | break 243 | } 244 | } 245 | } 246 | } 247 | 248 | /** 249 | * 用户相册未授权,Dialog提示 250 | */ 251 | private func showNoPermissionDailog(){ 252 | let alert = UIAlertController.init(title: nil, message: "没有打开相册的权限", preferredStyle: .alert) 253 | alert.addAction(UIAlertAction.init(title: "确定", style: .default, handler: nil)) 254 | self.present(alert, animated: true, completion: nil) 255 | } 256 | 257 | /** 258 | * 打开本地相册列表 259 | */ 260 | private func showLocalPhotoGallery(){ 261 | let picker = PhotoPickerController(type: PageType.RecentAlbum) 262 | picker.imageSelectDelegate = self 263 | picker.modalPresentationStyle = .popover 264 | PhotoPickerController.imageMaxSelectedNum = 4 // 允许选择的最大图片张数 265 | let realModel = self.getModelExceptButton() // 获取已经选择过的图片 266 | PhotoPickerController.alreadySelectedImageNum = realModel.count 267 | debugPrint(realModel.count) 268 | self.show(picker, sender: nil) 269 | } 270 | 271 | func onImageSelectFinished(images: [PHAsset]) { 272 | self.renderSelectImages(images: images) 273 | } 274 | 275 | private func renderSelectImages(images: [PHAsset]){ 276 | for item in images { 277 | self.selectModel.insert(PhotoImageModel(type: ModelType.Image, data: item), at: 0) 278 | } 279 | 280 | let total = self.selectModel.count; 281 | if total > PhotoPickerController.imageMaxSelectedNum { 282 | for i in 0 ..< total { 283 | let item = self.selectModel[i] 284 | if item.type == .Button { 285 | self.selectModel.remove(at: i) 286 | } 287 | } 288 | } 289 | self.renderView() 290 | } 291 | 292 | private func getModelExceptButton()->[PhotoImageModel]{ 293 | var newModels = [PhotoImageModel]() 294 | for i in 0.. 0 { 94 | self.buttonDone?.isEnabled = true 95 | self.doneNumberContainer?.isHidden = false 96 | self.doneNumberAnimationLayer!.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) 97 | UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 10, options: UIViewAnimationOptions.curveEaseIn, animations: { () -> Void in 98 | self.doneNumberAnimationLayer!.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) 99 | }, completion: nil) 100 | } else { 101 | self.buttonDone?.isEnabled = false 102 | self.doneNumberContainer?.isHidden = true 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /PhotoPicker/core/ImageModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageModel.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/11. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Photos 11 | 12 | struct ImageModel { 13 | var fetchResult: PHFetchResult 14 | var label: String? 15 | var assetType: PHAssetCollectionSubtype 16 | 17 | 18 | init(result: PHFetchResult,label: String?, assetType: PHAssetCollectionSubtype){ 19 | self.fetchResult = result 20 | self.label = label 21 | self.assetType = assetType 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PhotoPicker/core/PhotoAlbumTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoAlbumTableViewCell.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/6. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | class PhotoAlbumTableViewCell: UITableViewCell { 13 | 14 | @IBOutlet weak var albumCover: UIImageView! 15 | @IBOutlet weak var albumTitle: UILabel! 16 | @IBOutlet weak var albumNumber: UILabel! 17 | 18 | let imageSize: CGFloat = 80 19 | 20 | override func awakeFromNib() { 21 | super.awakeFromNib() 22 | self.layoutMargins = UIEdgeInsets.zero 23 | let bgView = UIView() 24 | bgView.backgroundColor = UIColor.init(red: 0, green: 0, blue: 0, alpha: 0.1) 25 | self.selectedBackgroundView = bgView 26 | } 27 | 28 | override func setSelected(_ selected: Bool, animated: Bool) { 29 | super.setSelected(selected, animated: animated) 30 | } 31 | 32 | 33 | func renderData(result:PHFetchResult, label: String?){ 34 | self.albumTitle.text = label 35 | self.albumNumber.text = String(result.count) 36 | 37 | if result.count > 0 { 38 | if let firstImageAsset = result[0] as? PHAsset { 39 | let retinaMultiplier = UIScreen.main.scale 40 | let realSize = self.imageSize * retinaMultiplier 41 | let size = CGSize(width:realSize, height: realSize) 42 | 43 | let imageOptions = PHImageRequestOptions() 44 | imageOptions.resizeMode = .exact 45 | 46 | PHImageManager.default().requestImage(for: firstImageAsset, targetSize: size, contentMode: .aspectFill, options: imageOptions, resultHandler: { (image, info) -> Void in 47 | DispatchQueue.main.async { 48 | self.albumCover.image = image 49 | } 50 | }) 51 | } 52 | } 53 | } 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /PhotoPicker/core/PhotoAlbumTableViewCell.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 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /PhotoPicker/core/PhotoAlbumsTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoAlbumsTableViewController.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/5. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | class PhotoAlbumsTableViewController: UITableViewController,PHPhotoLibraryChangeObserver{ 13 | 14 | // 自定义需要加载的相册 15 | var customSmartCollections = [ 16 | PHAssetCollectionSubtype.smartAlbumUserLibrary, // All Photos 17 | PHAssetCollectionSubtype.smartAlbumRecentlyAdded // Rencent Added 18 | ] 19 | 20 | // tableCellIndetifier 21 | let albumTableViewCellItentifier = "PhotoAlbumTableViewCell" 22 | var albums = [ImageModel]() 23 | let imageManager = PHImageManager.default() 24 | 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | // 9.0以上添加截屏图片 29 | if #available(iOS 9.0, *) { 30 | customSmartCollections.append(.smartAlbumScreenshots) 31 | } 32 | 33 | PHPhotoLibrary.shared().register(self) 34 | 35 | self.setupTableView() 36 | self.configNavigationBar() 37 | self.loadAlbums(replace: false) 38 | 39 | } 40 | 41 | 42 | deinit{ 43 | PHPhotoLibrary.shared().unregisterChangeObserver(self) 44 | } 45 | 46 | func photoLibraryDidChange(_ changeInstance: PHChange) { 47 | self.loadAlbums(replace: true) 48 | } 49 | 50 | private func setupTableView(){ 51 | self.tableView.register(UINib.init(nibName: self.albumTableViewCellItentifier, bundle: nil), forCellReuseIdentifier: self.albumTableViewCellItentifier) 52 | 53 | // 自定义 separatorLine样式 54 | self.tableView.rowHeight = PhotoPickerConfig.AlbumTableViewCellHeight 55 | self.tableView.separatorColor = UIColor.init(red: 0, green: 0, blue: 0, alpha: 0.15) 56 | self.tableView.separatorInset = UIEdgeInsets.zero 57 | 58 | // 去除tableView多余空格线 59 | self.tableView.tableFooterView = UIView.init(frame: CGRect.zero) 60 | } 61 | 62 | private func loadAlbums(replace: Bool){ 63 | 64 | if replace { 65 | self.albums.removeAll() 66 | } 67 | 68 | // 加载Smart Albumns All Photos 69 | let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: nil) 70 | 71 | for i in 0 ..< smartAlbums.count { 72 | if customSmartCollections.contains(smartAlbums[i].assetCollectionSubtype){ 73 | self.filterFetchResult(collection: smartAlbums[i]) 74 | } 75 | } 76 | 77 | // 用户相册 78 | let topUserLibarayList = PHCollectionList.fetchTopLevelUserCollections(with: nil) 79 | for i in 0 ..< topUserLibarayList.count { 80 | if let topUserAlbumItem = topUserLibarayList[i] as? PHAssetCollection { 81 | self.filterFetchResult(collection: topUserAlbumItem) 82 | } 83 | } 84 | 85 | self.tableView.reloadData() 86 | } 87 | 88 | private func filterFetchResult(collection: PHAssetCollection){ 89 | let fetchResult = PHAsset.fetchAssets(in: collection, options: PhotoFetchOptions.shareInstance) 90 | if fetchResult.count > 0 { 91 | let model = ImageModel(result: fetchResult as! PHFetchResult as! PHFetchResult, label: collection.localizedTitle, assetType: collection.assetCollectionSubtype) 92 | self.albums.append(model) 93 | } 94 | } 95 | 96 | private func configNavigationBar(){ 97 | let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(PhotoAlbumsTableViewController.eventViewControllerDismiss)) 98 | self.navigationItem.rightBarButtonItem = cancelButton 99 | } 100 | 101 | func eventViewControllerDismiss(){ 102 | PhotoImage.instance.selectedImage.removeAll() 103 | self.navigationController?.dismiss(animated: true, completion: nil) 104 | } 105 | 106 | // MARK: - Table view data source 107 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 108 | return self.albums.count 109 | } 110 | 111 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 112 | 113 | let cell = tableView.dequeueReusableCell(withIdentifier: self.albumTableViewCellItentifier, for: indexPath) as! PhotoAlbumTableViewCell 114 | 115 | let model = self.albums[indexPath.row] 116 | 117 | cell.renderData(result: model.fetchResult as! PHFetchResult, label: model.label) 118 | cell.accessoryType = .disclosureIndicator 119 | 120 | return cell 121 | } 122 | 123 | 124 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 125 | self.showDetailPageModel(model: self.albums[indexPath.row]) 126 | } 127 | 128 | 129 | private func showDetailPageModel(model: ImageModel){ 130 | let layout = PhotoCollectionViewController.configCustomCollectionLayout() 131 | let controller = PhotoCollectionViewController(collectionViewLayout: layout) 132 | 133 | controller.fetchResult = model.fetchResult 134 | self.navigationController?.show(controller, sender: nil) 135 | } 136 | 137 | 138 | } 139 | -------------------------------------------------------------------------------- /PhotoPicker/core/PhotoCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoCollectionViewController.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/6. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | private let reuseIdentifier = "photoCollectionViewCell" 13 | 14 | protocol PhotoCollectionViewControllerDelegate:class { 15 | func onPreviewPageBack() 16 | } 17 | 18 | class PhotoCollectionViewController: UICollectionViewController, PHPhotoLibraryChangeObserver,PhotoCollectionViewCellDelegate,PhotoCollectionViewControllerDelegate,AlbumToolbarViewDelegate { 19 | 20 | let imageManager = PHCachingImageManager() 21 | private let toolbarHeight: CGFloat = 44.0 22 | 23 | var assetGridThumbnailSize: CGSize? 24 | var fetchResult: PHFetchResult? 25 | var previousPreheatRect: CGRect? 26 | var toolbar: AlbumToolbarView? 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | 31 | let originFrame = self.collectionView!.frame 32 | self.collectionView!.frame = CGRect(x:originFrame.origin.x, y:originFrame.origin.y, width:originFrame.size.width, height: originFrame.height - self.toolbarHeight) 33 | 34 | self.resetCacheAssets() 35 | 36 | PHPhotoLibrary.shared().register(self) 37 | 38 | self.collectionView?.contentInset = UIEdgeInsetsMake( 39 | PhotoPickerConfig.MinimumInteritemSpacing, 40 | PhotoPickerConfig.MinimumInteritemSpacing, 41 | PhotoPickerConfig.MinimumInteritemSpacing, 42 | PhotoPickerConfig.MinimumInteritemSpacing 43 | ) 44 | 45 | self.collectionView!.register(UINib.init(nibName: reuseIdentifier, bundle: nil), forCellWithReuseIdentifier: reuseIdentifier) 46 | 47 | self.configBackground() 48 | self.configBottomToolBar() 49 | self.configNavigationBar() 50 | } 51 | 52 | private func configBottomToolBar() { 53 | if self.toolbar != nil {return} 54 | let width = UIScreen.main.bounds.width 55 | let positionX = UIScreen.main.bounds.height - self.toolbarHeight 56 | self.toolbar = AlbumToolbarView(frame: CGRect(x:0,y: positionX,width: width,height: self.toolbarHeight)) 57 | self.toolbar?.delegate = self 58 | self.view.addSubview(self.toolbar!) 59 | 60 | if PhotoImage.instance.selectedImage.count > 0 { 61 | self.toolbar?.changeNumber(number: PhotoImage.instance.selectedImage.count) 62 | } 63 | } 64 | 65 | override func viewWillAppear(_ animated: Bool) { 66 | super.viewWillAppear(animated) 67 | self.navigationController?.isNavigationBarHidden = false 68 | UIApplication.shared.setStatusBarHidden(false, with: .none) 69 | 70 | if assetGridThumbnailSize == nil { 71 | let scale = UIScreen.main.scale 72 | let cellSize = (self.collectionViewLayout as! UICollectionViewFlowLayout).itemSize 73 | let size = cellSize.width * scale 74 | assetGridThumbnailSize = CGSize(width: size, height: size) 75 | } 76 | } 77 | 78 | // MARK: - AlbumToolbarViewDelegate 79 | func onFinishedButtonClicked() { 80 | if let nav = self.navigationController as? PhotoPickerController { 81 | nav.imageSelectFinish() 82 | } 83 | } 84 | 85 | private func configNavigationBar(){ 86 | let cancelButton = UIBarButtonItem.init(barButtonSystemItem: .cancel, target: self, action: #selector(PhotoCollectionViewController.eventCancel)) 87 | self.navigationItem.rightBarButtonItem = cancelButton 88 | } 89 | 90 | // MARK: - cancel 91 | func eventCancel(){ 92 | PhotoImage.instance.selectedImage.removeAll() 93 | self.navigationController?.dismiss(animated: true, completion: nil) 94 | } 95 | 96 | override func viewDidAppear(_ animated: Bool) { 97 | super.viewDidAppear(animated) 98 | self.updateCacheAssets() 99 | } 100 | 101 | // MARK: - image caches 102 | private func resetCacheAssets() { 103 | self.imageManager.stopCachingImagesForAllAssets() 104 | self.previousPreheatRect = CGRect.zero 105 | 106 | } 107 | 108 | func updateCacheAssets() { 109 | let isViewVisible = self.isViewLoaded && self.view.window != nil; 110 | if !isViewVisible { return; } 111 | 112 | // The preheat window is twice the height of the visible rect. 113 | var preheatRect = self.collectionView?.bounds 114 | if preheatRect != nil { 115 | preheatRect = preheatRect!.insetBy(dx: 0, dy: -0.5 * preheatRect!.height) ; 116 | 117 | let delta = abs(preheatRect!.midY - self.previousPreheatRect!.midY) 118 | 119 | if (delta > self.collectionView!.bounds.height / 3.0) { 120 | 121 | var addedIndexPaths = [IndexPath]() 122 | var removedIndexPaths = [IndexPath]() 123 | self.computeDifferenceBetweenRect(oldRect: self.previousPreheatRect!, newRect: preheatRect!, removedHandler: { (removedRect) -> Void in 124 | // somde code 125 | let indexPaths = self.collectionView!.aapl_indexPathsForElementsInRect(rect: removedRect) 126 | if indexPaths != nil { 127 | removedIndexPaths.append(contentsOf: indexPaths!) 128 | } 129 | }, addedHandler: { (addedRect) -> Void in 130 | 131 | let indexPaths = self.collectionView!.aapl_indexPathsForElementsInRect(rect: addedRect) 132 | if indexPaths != nil { 133 | addedIndexPaths.append(contentsOf: indexPaths!) 134 | } 135 | }) 136 | 137 | let assetsToStartCaching = self.assetsAtIndexPaths(indexPaths: addedIndexPaths as [NSIndexPath]) 138 | let assetsToStopCaching = self.assetsAtIndexPaths(indexPaths: removedIndexPaths as [NSIndexPath]) 139 | 140 | if assetsToStartCaching != nil { 141 | self.imageManager.startCachingImages(for: assetsToStartCaching!, targetSize: self.assetGridThumbnailSize!, contentMode: .aspectFill, options: nil) 142 | } 143 | 144 | if assetsToStopCaching != nil { 145 | self.imageManager.stopCachingImages(for: assetsToStopCaching!, targetSize: self.assetGridThumbnailSize!, contentMode: .aspectFill, options: nil) 146 | } 147 | 148 | self.previousPreheatRect = preheatRect; 149 | } 150 | } 151 | } 152 | 153 | // MARK: - PhotoCollectionViewCellDelegate 154 | func eventSelectNumberChange(number: Int) { 155 | if let toolbar = self.toolbar { 156 | toolbar.changeNumber(number: number) 157 | } 158 | } 159 | 160 | override func scrollViewDidScroll(_ scrollView: UIScrollView) { 161 | self.updateCacheAssets() 162 | } 163 | 164 | func assetsAtIndexPaths(indexPaths: [NSIndexPath]) -> [PHAsset]? { 165 | if indexPaths.count == 0 { return nil; } 166 | var assets = [PHAsset]() 167 | for indexPath in indexPaths { 168 | if let asset = self.fetchResult![indexPath.item] as? PHAsset { 169 | assets.append(asset) 170 | } 171 | } 172 | return assets; 173 | } 174 | 175 | func computeDifferenceBetweenRect(oldRect: CGRect, newRect: CGRect, removedHandler: (CGRect) -> Void, addedHandler: (CGRect) -> Void) { 176 | 177 | if newRect.intersects(oldRect) { 178 | let oldMaxY = oldRect.maxY 179 | let oldMinY = oldRect.maxX 180 | let newMaxY = newRect.maxY ; 181 | let newMinY = newRect.minY ; 182 | 183 | if newMaxY > oldMaxY { 184 | let rectToAdd = CGRect(x:newRect.origin.x, y:oldMaxY, width: newRect.size.width, height: (newMaxY - oldMaxY)) 185 | addedHandler(rectToAdd) 186 | } 187 | 188 | if oldMinY > newMinY { 189 | let rectToAdd = CGRect(x: newRect.origin.x, y: newMinY, width: newRect.size.width, height: (oldMinY - newMinY)) 190 | addedHandler(rectToAdd) 191 | } 192 | 193 | if newMaxY < oldMaxY { 194 | let rectToRemove = CGRect(x: newRect.origin.x, y: newMaxY, width: newRect.size.width, height: (oldMaxY - newMaxY)) 195 | removedHandler(rectToRemove) 196 | } 197 | 198 | if oldMinY < newMinY { 199 | let rectToRemove = CGRect(x: newRect.origin.x, y: oldMinY, width: newRect.size.width, height: (newMinY - oldMinY)) 200 | removedHandler(rectToRemove) 201 | } 202 | } else { 203 | addedHandler(newRect) ; 204 | removedHandler(oldRect) ; 205 | } 206 | } 207 | 208 | deinit { 209 | PHPhotoLibrary.shared().unregisterChangeObserver(self) 210 | } 211 | 212 | func photoLibraryDidChange(_ changeInstance: PHChange) { 213 | 214 | 215 | if let collectionChanges = changeInstance.changeDetails(for: fetchResult!) { 216 | 217 | DispatchQueue.main.async { 218 | self.fetchResult = collectionChanges.fetchResultAfterChanges 219 | let collectionView = self.collectionView!; 220 | 221 | if !(collectionChanges.hasIncrementalChanges || collectionChanges.hasMoves) { 222 | collectionView.reloadData() 223 | } else { 224 | collectionView.performBatchUpdates({ () -> Void in 225 | if let removed = collectionChanges.removedIndexes , removed.count > 0 { 226 | collectionView.deleteItems(at: removed.map { IndexPath(item: $0, section:0) }) 227 | } 228 | if let inserted = collectionChanges.insertedIndexes , inserted.count > 0 { 229 | collectionView.insertItems(at: inserted.map { IndexPath(item: $0, section:0) }) 230 | } 231 | if let changed = collectionChanges.changedIndexes , changed.count > 0 { 232 | collectionView.reloadItems(at: changed.map { IndexPath(item: $0, section:0) }) 233 | } 234 | collectionChanges.enumerateMoves { fromIndex, toIndex in 235 | collectionView.moveItem(at: IndexPath(item: fromIndex, section: 0), 236 | to: IndexPath(item: toIndex, section: 0)) 237 | } 238 | }, completion: nil) 239 | } 240 | 241 | self.resetCacheAssets() 242 | } 243 | } 244 | } 245 | 246 | private func configBackground() { 247 | self.collectionView?.backgroundColor = UIColor.white 248 | } 249 | 250 | override init(collectionViewLayout layout: UICollectionViewLayout) { 251 | super.init(collectionViewLayout: layout) 252 | } 253 | 254 | required init?(coder aDecoder: NSCoder) { 255 | super.init(coder: aDecoder) 256 | } 257 | 258 | class func configCustomCollectionLayout() -> UICollectionViewFlowLayout { 259 | let collectionLayout = UICollectionViewFlowLayout() 260 | 261 | let width = UIScreen.main.bounds.width - PhotoPickerConfig.MinimumInteritemSpacing * 2 262 | collectionLayout.minimumInteritemSpacing = PhotoPickerConfig.MinimumInteritemSpacing 263 | 264 | let cellToUsableWidth = width - (PhotoPickerConfig.ColNumber - 1) * PhotoPickerConfig.MinimumInteritemSpacing 265 | let size = cellToUsableWidth / PhotoPickerConfig.ColNumber 266 | collectionLayout.itemSize = CGSize(width:size, height: size) 267 | collectionLayout.minimumLineSpacing = PhotoPickerConfig.MinimumInteritemSpacing 268 | return collectionLayout 269 | } 270 | 271 | // MARK: - UICollectionView delegate 272 | func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { 273 | return 1 274 | } 275 | 276 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 277 | return self.fetchResult != nil ? self.fetchResult!.count : 0 278 | } 279 | 280 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 281 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! photoCollectionViewCell 282 | 283 | cell.delegate = self; 284 | cell.eventDelegate = self 285 | 286 | if let asset = self.fetchResult![indexPath.row] as? PHAsset { 287 | 288 | cell.model = asset 289 | cell.representedAssetIdentifier = asset.localIdentifier 290 | self.imageManager.requestImage(for: asset, targetSize: self.assetGridThumbnailSize!, contentMode: .aspectFill, options: nil) { (image, info) -> Void in 291 | DispatchQueue.main.async { 292 | if cell.representedAssetIdentifier == asset.localIdentifier { 293 | cell.thumbnail.image = image 294 | } 295 | } 296 | 297 | } 298 | cell.updateSelected(select: PhotoImage.instance.selectedImage.index(of: asset) != nil) 299 | } 300 | return cell 301 | } 302 | 303 | // MARK: UICollectionViewDelegate 304 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 305 | let previewController = PhotoPreviewViewController(nibName: nil, bundle: nil) 306 | previewController.allSelectImage = self.fetchResult as! PHFetchResult? 307 | previewController.currentPage = indexPath.row 308 | previewController.fromDelegate = self 309 | self.navigationController?.show(previewController, sender: nil) 310 | } 311 | 312 | func onPreviewPageBack() { 313 | self.collectionView?.reloadData() 314 | self.eventSelectNumberChange(number: PhotoImage.instance.selectedImage.count) 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /PhotoPicker/core/PhotoFetchOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoFetchOptions.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/11. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | class PhotoFetchOptions: PHFetchOptions { 13 | static let shareInstance: PhotoFetchOptions = PhotoFetchOptions() 14 | private override init() { 15 | super.init() 16 | self.predicate = NSPredicate(format: "mediaType == %d", PHAssetMediaType.image.rawValue) 17 | self.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PhotoPicker/core/PhotoImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoImageSelected.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/9. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Photos 11 | 12 | class PhotoImage { 13 | 14 | // singleton 15 | static let instance = PhotoImage() 16 | private init(){} 17 | 18 | var selectedImage = [PHAsset]() 19 | 20 | } 21 | -------------------------------------------------------------------------------- /PhotoPicker/core/PhotoImageManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoImageManager.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/8. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | 13 | class PhotoImageManager: PHCachingImageManager { 14 | 15 | // singleton class 16 | static let sharedManager = PhotoImageManager() 17 | private override init() {super.init()} 18 | 19 | func getPhotoByMaxSize(asset: PHObject, size: CGFloat, completion: @escaping (UIImage?, [NSObject : AnyObject]?)->Void){ 20 | 21 | let maxSize = size > PhotoPickerConfig.PreviewImageMaxFetchMaxWidth ? PhotoPickerConfig.PreviewImageMaxFetchMaxWidth : size 22 | if let asset = asset as? PHAsset { 23 | 24 | let factor = CGFloat(asset.pixelHeight)/CGFloat(asset.pixelWidth) 25 | let scale = UIScreen.main.scale 26 | let pixcelWidth = maxSize * scale 27 | let pixcelHeight = CGFloat(pixcelWidth) * factor 28 | 29 | PhotoImageManager.sharedManager.requestImage(for: asset, targetSize: CGSize(width:pixcelWidth, height: pixcelHeight), contentMode: .aspectFit, options: nil, resultHandler: { (image, info) -> Void in 30 | 31 | if let info = info as? [String:AnyObject] { 32 | let canceled = info[PHImageCancelledKey] as? Bool 33 | let error = info[PHImageErrorKey] as? NSError 34 | 35 | if canceled == nil && error == nil && image != nil { 36 | completion(image,info as [NSObject : AnyObject]?) 37 | } 38 | 39 | // download from iCloud 40 | let isCloud = info[PHImageResultIsInCloudKey] as? Bool 41 | if isCloud != nil && image == nil { 42 | let options = PHImageRequestOptions() 43 | options.isNetworkAccessAllowed = true 44 | PhotoImageManager.sharedManager.requestImageData(for: asset, options: options, resultHandler: { (data, dataUTI, oritation, info) -> Void in 45 | 46 | if let data = data { 47 | let resultImage = UIImage(data: data, scale: 0.1) 48 | completion(resultImage,info as [NSObject : AnyObject]?) 49 | } 50 | }) 51 | } 52 | } 53 | }) 54 | } 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /PhotoPicker/core/PhotoPickerConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoPickerConfig.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/6. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class PhotoPickerConfig { 13 | // tableView Cell height 14 | static let AlbumTableViewCellHeight: CGFloat = 90.0 15 | 16 | // message when select number more than the max number 17 | static let ErrorImageMaxSelect = "图片选择最多超过不能超过#张" 18 | 19 | // button confirm title 20 | static let ButtonConfirmTitle = "确定" 21 | 22 | // button secelt image done title 23 | static let ButtonDone = "完成" 24 | 25 | // preview view bar background color 26 | static let PreviewBarBackgroundColor = UIColor(red: 40/255, green: 40/255, blue: 40/255, alpha: 1) 27 | 28 | // button green tin color 29 | static let GreenTinColor = UIColor(red: 7/255, green: 179/255, blue: 20/255, alpha: 1) 30 | 31 | // image total per line 32 | static let ColNumber: CGFloat = 4 33 | 34 | // collceiont cell padding 35 | static let MinimumInteritemSpacing: CGFloat = 5 36 | 37 | 38 | // fethch single large image max width 39 | static let PreviewImageMaxFetchMaxWidth:CGFloat = 600 40 | 41 | 42 | } -------------------------------------------------------------------------------- /PhotoPicker/core/PhotoPickerController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoPickerController.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/5. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | enum PageType{ 13 | case List 14 | case RecentAlbum 15 | case AllAlbum 16 | } 17 | 18 | protocol PhotoPickerControllerDelegate: class{ 19 | func onImageSelectFinished(images: [PHAsset]) 20 | } 21 | 22 | class PhotoPickerController: UINavigationController { 23 | 24 | // the select image max number 25 | static var imageMaxSelectedNum = 4 26 | 27 | // already select total 28 | static var alreadySelectedImageNum = 0 29 | 30 | 31 | weak var imageSelectDelegate: PhotoPickerControllerDelegate? 32 | 33 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 34 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 35 | } 36 | 37 | init(type:PageType){ 38 | let rootViewController = PhotoAlbumsTableViewController(style:.plain) 39 | // clear cache 40 | PhotoImage.instance.selectedImage.removeAll() 41 | super.init(rootViewController: rootViewController) 42 | 43 | if type == .RecentAlbum || type == .AllAlbum { 44 | let currentType = type == .RecentAlbum ? PHAssetCollectionSubtype.smartAlbumRecentlyAdded : PHAssetCollectionSubtype.smartAlbumUserLibrary 45 | let results = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype:currentType, options: nil) 46 | if results.count > 0 { 47 | if let model = self.getModel(collection: results[0]) { 48 | if model.count > 0 { 49 | let layout = PhotoCollectionViewController.configCustomCollectionLayout() 50 | let controller = PhotoCollectionViewController(collectionViewLayout: layout) 51 | 52 | controller.fetchResult = model as? PHFetchResult; 53 | self.pushViewController(controller, animated: false) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | 61 | private func getModel(collection: PHAssetCollection) -> PHFetchResult?{ 62 | let fetchResult = PHAsset.fetchAssets(in: collection, options: PhotoFetchOptions.shareInstance) 63 | if fetchResult.count > 0 { 64 | return fetchResult 65 | } 66 | return nil 67 | } 68 | 69 | 70 | required init?(coder aDecoder: NSCoder) { 71 | super.init(coder: aDecoder) 72 | } 73 | 74 | func imageSelectFinish(){ 75 | if self.imageSelectDelegate != nil { 76 | self.dismiss(animated: true, completion: nil) 77 | self.imageSelectDelegate?.onImageSelectFinished(images: PhotoImage.instance.selectedImage) 78 | } 79 | } 80 | 81 | 82 | 83 | 84 | } 85 | -------------------------------------------------------------------------------- /PhotoPicker/core/PhotoPreviewBottomBarView.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // PhotoPreviewBottomBarView.swift 4 | // PhotoPicker 5 | // 6 | // Created by liangqi on 16/3/9. 7 | // Copyright © 2016年 dailyios. All rights reserved. 8 | // 9 | 10 | import UIKit 11 | 12 | protocol PhotoPreviewBottomBarViewDelegate:class{ 13 | func onDoneButtonClicked() 14 | } 15 | 16 | class PhotoPreviewBottomBarView: UIView { 17 | 18 | var doneNumberAnimationLayer: UIView? 19 | var labelTextView: UILabel? 20 | var buttonDone: UIButton? 21 | var doneNumberContainer: UIView? 22 | 23 | weak var delegate: PhotoPreviewBottomBarViewDelegate? 24 | 25 | override init(frame: CGRect) { 26 | super.init(frame: frame) 27 | self.configView() 28 | } 29 | 30 | required init?(coder aDecoder: NSCoder) { 31 | super.init(coder: aDecoder) 32 | self.configView() 33 | } 34 | 35 | private func configView(){ 36 | self.backgroundColor = PhotoPickerConfig.PreviewBarBackgroundColor 37 | 38 | // button 39 | self.buttonDone = UIButton(type: .custom) 40 | 41 | let toolbarHeight = bounds.height 42 | let buttonWidth: CGFloat = 40 43 | let buttonHeight: CGFloat = 40 44 | let padding:CGFloat = 5 45 | let width = self.bounds.width 46 | 47 | buttonDone!.frame = CGRect(x: width - buttonWidth - padding, y: (toolbarHeight - buttonHeight) / 2, width: buttonWidth, height: buttonHeight) 48 | buttonDone!.setTitle(PhotoPickerConfig.ButtonDone, for: .normal) 49 | buttonDone!.titleLabel?.font = UIFont.systemFont(ofSize: 16.0) 50 | buttonDone!.setTitleColor(PhotoPickerConfig.GreenTinColor, for: .normal) 51 | buttonDone!.addTarget(self, action: #selector(PhotoPreviewBottomBarView.eventDoneClicked), for: .touchUpInside) 52 | buttonDone!.isEnabled = true 53 | buttonDone!.setTitleColor(UIColor.black, for: .disabled) 54 | self.addSubview(self.buttonDone!) 55 | 56 | // done number 57 | let labelWidth:CGFloat = 20 58 | let labelX = buttonDone!.frame.minX - labelWidth 59 | let labelY = (toolbarHeight - labelWidth) / 2 60 | 61 | self.doneNumberContainer = UIView(frame: CGRect(x:labelX, y:labelY, width: labelWidth, height:labelWidth)) 62 | let labelRect = CGRect(x:0, y:0, width: labelWidth, height: labelWidth) 63 | self.doneNumberAnimationLayer = UIView.init(frame: labelRect) 64 | self.doneNumberAnimationLayer!.backgroundColor = PhotoPickerConfig.GreenTinColor 65 | self.doneNumberAnimationLayer!.layer.cornerRadius = labelWidth / 2 66 | doneNumberContainer!.addSubview(self.doneNumberAnimationLayer!) 67 | 68 | self.labelTextView = UILabel(frame: labelRect) 69 | self.labelTextView!.textAlignment = .center 70 | self.labelTextView!.backgroundColor = UIColor.clear 71 | self.labelTextView!.textColor = UIColor.white 72 | doneNumberContainer!.addSubview(self.labelTextView!) 73 | 74 | self.addSubview(self.doneNumberContainer!) 75 | } 76 | 77 | // MARK: - Event delegate 78 | func eventDoneClicked(){ 79 | if let delegate = self.delegate{ 80 | delegate.onDoneButtonClicked() 81 | } 82 | } 83 | 84 | func changeNumber(number:Int,animation:Bool){ 85 | self.labelTextView?.text = String(number) 86 | if number > 0 { 87 | self.buttonDone?.isEnabled = true 88 | self.doneNumberContainer?.isHidden = false 89 | if animation { 90 | self.doneNumberAnimationLayer!.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) 91 | UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 10, options: UIViewAnimationOptions.curveEaseIn, animations: { () -> Void in 92 | self.doneNumberAnimationLayer!.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) 93 | }, completion: nil) 94 | } 95 | 96 | } else { 97 | self.buttonDone?.isEnabled = false 98 | self.doneNumberContainer?.isHidden = true 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /PhotoPicker/core/PhotoPreviewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoPreviewCell.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/8. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | protocol PhotoPreviewCellDelegate: class{ 13 | func onImageSingleTap() 14 | } 15 | 16 | class PhotoPreviewCell: UICollectionViewCell, UIScrollViewDelegate { 17 | 18 | var model: PHAsset? 19 | private var scrollView: UIScrollView? 20 | private var imageContainerView = UIView() 21 | private var imageView = UIImageView() 22 | 23 | weak var delegate: PhotoPreviewCellDelegate? 24 | 25 | override init(frame: CGRect) { 26 | super.init(frame: frame) 27 | self.configView() 28 | } 29 | 30 | required init?(coder aDecoder: NSCoder) { 31 | super.init(coder: aDecoder) 32 | self.configView() 33 | } 34 | 35 | func configView() { 36 | self.scrollView = UIScrollView(frame: self.bounds) 37 | self.scrollView!.bouncesZoom = true 38 | self.scrollView!.maximumZoomScale = 2.5 39 | self.scrollView!.isMultipleTouchEnabled = true 40 | self.scrollView!.delegate = self 41 | self.scrollView!.scrollsToTop = false 42 | self.scrollView!.showsHorizontalScrollIndicator = false 43 | self.scrollView!.showsVerticalScrollIndicator = false 44 | self.scrollView!.autoresizingMask = [.flexibleWidth, .flexibleHeight] 45 | self.scrollView!.delaysContentTouches = false 46 | self.scrollView!.canCancelContentTouches = true 47 | self.scrollView!.alwaysBounceVertical = false 48 | self.addSubview(self.scrollView!) 49 | 50 | self.imageContainerView.clipsToBounds = true 51 | self.scrollView!.addSubview(self.imageContainerView) 52 | 53 | self.imageView.backgroundColor = UIColor(white: 1.0, alpha: 0.5) 54 | self.imageView.clipsToBounds = true 55 | self.imageContainerView.addSubview(self.imageView) 56 | 57 | let singleTap = UITapGestureRecognizer.init(target: self, action: #selector(PhotoPreviewCell.singleTap(tap:))) 58 | let doubleTap = UITapGestureRecognizer.init(target: self, action: #selector(PhotoPreviewCell.doubleTap(tap:))) 59 | 60 | doubleTap.numberOfTapsRequired = 2 61 | singleTap.require(toFail: doubleTap) 62 | 63 | self.addGestureRecognizer(singleTap) 64 | self.addGestureRecognizer(doubleTap) 65 | } 66 | 67 | 68 | func renderModel(asset: PHAsset) { 69 | PhotoImageManager.sharedManager.getPhotoByMaxSize(asset: asset, size: self.bounds.width) { (image, info) -> Void in 70 | self.imageView.image = image 71 | self.resizeImageView() 72 | } 73 | } 74 | 75 | func resizeImageView() { 76 | self.imageContainerView.frame = CGRect(x:0, y:0, width: self.frame.width, height: self.imageContainerView.bounds.height) 77 | let image = self.imageView.image! 78 | 79 | 80 | if image.size.height / image.size.width > self.bounds.height / self.bounds.width { 81 | 82 | let height = floor(image.size.height / (image.size.width / self.bounds.width)) 83 | var originFrame = self.imageContainerView.frame 84 | originFrame.size.height = height 85 | self.imageContainerView.frame = originFrame 86 | } else { 87 | var height = image.size.height / image.size.width * self.frame.width 88 | if height < 1 || height.isNaN { 89 | height = self.frame.height 90 | } 91 | height = floor(height) 92 | var originFrame = self.imageContainerView.frame 93 | originFrame.size.height = height 94 | self.imageContainerView.frame = originFrame 95 | self.imageContainerView.center = CGPoint(x:self.imageContainerView.center.x, y:self.bounds.height / 2) 96 | } 97 | 98 | if self.imageContainerView.frame.height > self.frame.height && self.imageContainerView.frame.height - self.frame.height <= 1 { 99 | 100 | var originFrame = self.imageContainerView.frame 101 | originFrame.size.height = self.frame.height 102 | self.imageContainerView.frame = originFrame 103 | } 104 | 105 | self.scrollView?.contentSize = CGSize(width: self.frame.width, height: max(self.imageContainerView.frame.height, self.frame.height)) 106 | self.scrollView?.scrollRectToVisible(self.bounds, animated: false) 107 | self.scrollView?.alwaysBounceVertical = self.imageContainerView.frame.height > self.frame.height 108 | self.imageView.frame = self.imageContainerView.bounds 109 | 110 | } 111 | 112 | func singleTap(tap:UITapGestureRecognizer) { 113 | if let delegate = self.delegate { 114 | delegate.onImageSingleTap() 115 | } 116 | } 117 | 118 | func doubleTap(tap:UITapGestureRecognizer) { 119 | if (self.scrollView!.zoomScale > 1.0) { 120 | // 状态还原 121 | self.scrollView!.setZoomScale(1.0, animated: true) 122 | } else { 123 | let touchPoint = tap.location(in: self.imageView) 124 | let newZoomScale = self.scrollView!.maximumZoomScale 125 | let xsize = self.frame.size.width / newZoomScale 126 | let ysize = self.frame.size.height / newZoomScale 127 | 128 | self.scrollView!.zoom(to: CGRect(x: touchPoint.x - xsize/2, y: touchPoint.y-ysize/2, width: xsize, height: ysize), animated: true) 129 | } 130 | } 131 | 132 | func viewForZooming(in scrollView: UIScrollView) -> UIView? { 133 | return self.imageContainerView 134 | } 135 | 136 | func scrollViewDidZoom(_ scrollView: UIScrollView) { 137 | let offsetX = (scrollView.frame.width > scrollView.contentSize.width) ? (scrollView.frame.width - scrollView.contentSize.width) * 0.5 : 0.0; 138 | let offsetY = (scrollView.frame.height > scrollView.contentSize.height) ? (scrollView.frame.height - scrollView.contentSize.height) * 0.5 : 0.0; 139 | self.imageContainerView.center = CGPoint(x: scrollView.contentSize.width * 0.5 + offsetX, y: scrollView.contentSize.height * 0.5 + offsetY); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /PhotoPicker/core/PhotoPreviewToolbarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoPreviewToolbarView.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/9. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol PhotoPreviewToolbarViewDelegate: class { 12 | func onToolbarBackArrowClicked(); 13 | func onSelected(select:Bool) 14 | } 15 | 16 | class PhotoPreviewToolbarView: UIView { 17 | 18 | weak var delegate: PhotoPreviewToolbarViewDelegate? 19 | weak var sourceDelegate: PhotoPreviewViewController? 20 | 21 | private var checkboxBg: UIImageView? 22 | private var checkbox: UIButton? 23 | 24 | override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | self.configView() 27 | } 28 | 29 | required init?(coder aDecoder: NSCoder) { 30 | super.init(coder: aDecoder) 31 | self.configView() 32 | } 33 | 34 | private func configView(){ 35 | self.backgroundColor = UIColor(red: 40/255, green: 40/255, blue: 40/255, alpha: 1) 36 | 37 | // back arrow buttton 38 | let backArrow = UIButton(frame: CGRect(x: 5, y: 5, width: 40, height: 40)) 39 | let backArrowImage = UIImage(named: "arrow_back") 40 | backArrow.setImage(backArrowImage, for: UIControlState.normal) 41 | backArrow.addTarget(self, action: #selector(PhotoPreviewToolbarView.eventBackArrow), for: .touchUpInside) 42 | self.addSubview(backArrow) 43 | 44 | // right checkbox 45 | let padding: CGFloat = 10 46 | let checkboxWidth: CGFloat = 30 47 | let checkboxHeight = checkboxWidth 48 | let checkboxPositionX = self.bounds.width - checkboxWidth - padding 49 | let checkboxPositionY = (self.bounds.height - checkboxHeight) / 2 50 | 51 | self.checkbox = UIButton(type: .custom) 52 | checkbox!.frame = CGRect(x:checkboxPositionX,y: checkboxPositionY,width: checkboxWidth,height: checkboxHeight) 53 | checkbox!.addTarget(self, action: #selector(PhotoPreviewToolbarView.eventCheckbox(sender:)), for: .touchUpInside) 54 | 55 | let checkboxFront = UIImageView(image: UIImage(named: "picture_unselect")) 56 | checkboxFront.contentMode = .scaleAspectFill 57 | checkboxFront.frame = checkbox!.bounds 58 | checkbox!.addSubview(checkboxFront) 59 | 60 | self.checkboxBg = UIImageView(image: UIImage(named: "picture_select")) 61 | checkboxBg!.contentMode = .scaleAspectFill 62 | checkboxBg!.frame = checkbox!.bounds 63 | checkboxBg!.isHidden = true 64 | 65 | self.checkbox!.addSubview(checkboxBg!) 66 | 67 | self.addSubview(checkbox!) 68 | } 69 | 70 | // MARK: - Event 71 | func eventBackArrow(){ 72 | if let delegate = self.delegate { 73 | delegate.onToolbarBackArrowClicked() 74 | } 75 | } 76 | 77 | func setSelect(select:Bool){ 78 | self.checkboxBg!.isHidden = !select 79 | self.checkbox!.isSelected = select 80 | } 81 | 82 | func eventCheckbox(sender: UIButton){ 83 | if sender.isSelected { 84 | sender.isSelected = false 85 | self.checkboxBg!.isHidden = true 86 | if let delegate = self.delegate { 87 | delegate.onSelected(select: false) 88 | } 89 | } else { 90 | if let _ = self.sourceDelegate { 91 | if PhotoImage.instance.selectedImage.count >= PhotoPickerController.imageMaxSelectedNum - PhotoPickerController.alreadySelectedImageNum { 92 | return self.showSelectErrorDialog() 93 | } 94 | } 95 | sender.isSelected = true 96 | self.checkboxBg!.isHidden = false 97 | self.checkboxBg!.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) 98 | UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 8, options: [UIViewAnimationOptions.curveEaseIn], animations: { () -> Void in 99 | self.checkboxBg!.transform = CGAffineTransform(scaleX: 1, y: 1) 100 | }, completion: nil) 101 | 102 | if let delegate = self.delegate { 103 | delegate.onSelected(select: true) 104 | } 105 | } 106 | } 107 | 108 | private func showSelectErrorDialog() { 109 | if self.sourceDelegate != nil { 110 | let less = PhotoPickerController.imageMaxSelectedNum - PhotoPickerController.alreadySelectedImageNum 111 | 112 | 113 | let range = PhotoPickerConfig.ErrorImageMaxSelect.range(of:"#") 114 | var error = PhotoPickerConfig.ErrorImageMaxSelect 115 | error.replaceSubrange(range!, with: String(less)) 116 | 117 | let alert = UIAlertController.init(title: nil, message: error, preferredStyle: UIAlertControllerStyle.alert) 118 | let confirmAction = UIAlertAction(title: PhotoPickerConfig.ButtonConfirmTitle, style: .default, handler: nil) 119 | alert.addAction(confirmAction) 120 | self.sourceDelegate?.present(alert, animated: true, completion: nil) 121 | } 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /PhotoPicker/core/PhotoPreviewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoPreviewViewController.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/8. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | 13 | class PhotoPreviewViewController: UIViewController,UICollectionViewDataSource,UICollectionViewDelegate,PhotoPreviewBottomBarViewDelegate,PhotoPreviewToolbarViewDelegate,PhotoPreviewCellDelegate { 14 | 15 | var allSelectImage: PHFetchResult? 16 | var collectionView: UICollectionView? 17 | var currentPage: Int = 1 18 | 19 | let cellIdentifier = "PhotoPreviewCell" 20 | weak var fromDelegate: PhotoCollectionViewControllerDelegate? 21 | 22 | private var toolbar: PhotoPreviewToolbarView? 23 | private var bottomBar: PhotoPreviewBottomBarView? 24 | 25 | private var isAnimation = false 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | self.configCollectionView() 30 | self.configToolbar() 31 | } 32 | 33 | private func configToolbar(){ 34 | self.toolbar = PhotoPreviewToolbarView(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: 50)) 35 | self.toolbar?.delegate = self 36 | self.toolbar?.sourceDelegate = self 37 | let positionY = self.view.bounds.height - 50 38 | self.bottomBar = PhotoPreviewBottomBarView(frame: CGRect(x: 0,y: positionY,width: self.view.bounds.width,height: 50)) 39 | self.bottomBar?.delegate = self 40 | self.bottomBar?.changeNumber(number: PhotoImage.instance.selectedImage.count, animation: false) 41 | 42 | self.view.addSubview(toolbar!) 43 | self.view.addSubview(bottomBar!) 44 | } 45 | 46 | // MARK: - delegate 47 | func onDoneButtonClicked() { 48 | if let nav = self.navigationController as? PhotoPickerController { 49 | nav.imageSelectFinish() 50 | } 51 | } 52 | 53 | // MARK: - from page delegate 54 | func onToolbarBackArrowClicked() { 55 | _ = self.navigationController?.popViewController(animated: true) 56 | if let delegate = self.fromDelegate { 57 | delegate.onPreviewPageBack() 58 | } 59 | } 60 | 61 | func onSelected(select: Bool) { 62 | let currentModel = self.allSelectImage![self.currentPage] 63 | if select { 64 | PhotoImage.instance.selectedImage.append(currentModel as! PHAsset) 65 | } else { 66 | if let index = PhotoImage.instance.selectedImage.index(of: currentModel as! PHAsset){ 67 | PhotoImage.instance.selectedImage.remove(at: index) 68 | } 69 | } 70 | self.bottomBar?.changeNumber(number: PhotoImage.instance.selectedImage.count, animation: true) 71 | } 72 | 73 | override func viewWillAppear(_ animated: Bool) { 74 | super.viewWillAppear(animated) 75 | 76 | // fullscreen controller 77 | self.navigationController?.isNavigationBarHidden = true 78 | UIApplication.shared.setStatusBarHidden(true, with: .none) 79 | 80 | self.collectionView?.setContentOffset(CGPoint(x: CGFloat(self.currentPage) * self.view.bounds.width, y: 0), animated: false) 81 | 82 | self.changeCurrentToolbar() 83 | } 84 | 85 | func configCollectionView(){ 86 | self.automaticallyAdjustsScrollViewInsets = false 87 | let layout = UICollectionViewFlowLayout() 88 | layout.scrollDirection = .horizontal 89 | layout.itemSize = CGSize(width: self.view.frame.width,height: self.view.frame.height) 90 | layout.minimumInteritemSpacing = 0 91 | layout.minimumLineSpacing = 0 92 | 93 | self.collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout) 94 | self.collectionView!.backgroundColor = UIColor.black 95 | self.collectionView!.dataSource = self 96 | self.collectionView!.delegate = self 97 | self.collectionView!.isPagingEnabled = true 98 | self.collectionView!.scrollsToTop = false 99 | self.collectionView!.showsHorizontalScrollIndicator = false 100 | self.collectionView!.contentOffset = CGPoint.zero 101 | self.collectionView!.contentSize = CGSize(width: self.view.bounds.width * CGFloat(self.allSelectImage!.count), height: self.view.bounds.height) 102 | 103 | self.view.addSubview(self.collectionView!) 104 | self.collectionView!.register(PhotoPreviewCell.self, forCellWithReuseIdentifier: self.cellIdentifier) 105 | } 106 | 107 | // MARK: - collectionView dataSource delagate 108 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 109 | return self.allSelectImage!.count 110 | } 111 | 112 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 113 | 114 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cellIdentifier, for: indexPath as IndexPath) as! PhotoPreviewCell 115 | cell.delegate = self 116 | if let asset = self.allSelectImage![indexPath.row] as? PHAsset { 117 | cell.renderModel(asset: asset) 118 | } 119 | 120 | return cell 121 | } 122 | 123 | // MARK: - Photo Preview Cell Delegate 124 | func onImageSingleTap() { 125 | if self.isAnimation { 126 | return 127 | } 128 | 129 | self.isAnimation = true 130 | if self.toolbar!.frame.origin.y < 0 { 131 | UIView.animate(withDuration: 0.3, delay: 0, options: [UIViewAnimationOptions.curveEaseOut], animations: { () -> Void in 132 | self.toolbar!.frame.origin = CGPoint.zero 133 | var originPoint = self.bottomBar!.frame.origin 134 | originPoint.y = originPoint.y - self.bottomBar!.frame.height 135 | self.bottomBar!.frame.origin = originPoint 136 | }, completion: { (isFinished) -> Void in 137 | if isFinished { 138 | self.isAnimation = false 139 | } 140 | }) 141 | } else { 142 | UIView.animate(withDuration: 0.3, delay: 0, options: [UIViewAnimationOptions.curveEaseOut], animations: { () -> Void in 143 | self.toolbar!.frame.origin = CGPoint(x:0, y: -self.toolbar!.frame.height) 144 | var originPoint = self.bottomBar!.frame.origin 145 | originPoint.y = originPoint.y + self.bottomBar!.frame.height 146 | self.bottomBar!.frame.origin = originPoint 147 | 148 | }, completion: { (isFinished) -> Void in 149 | if isFinished { 150 | self.isAnimation = false 151 | } 152 | }) 153 | } 154 | 155 | } 156 | 157 | 158 | // MARK: - scroll page 159 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 160 | let offset = scrollView.contentOffset 161 | self.currentPage = Int(offset.x / self.view.bounds.width) 162 | } 163 | 164 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 165 | self.changeCurrentToolbar() 166 | } 167 | 168 | private func changeCurrentToolbar(){ 169 | let model = self.allSelectImage![self.currentPage] as! PHAsset 170 | if let _ = PhotoImage.instance.selectedImage.index(of: model){ 171 | self.toolbar!.setSelect(select: true) 172 | } else { 173 | self.toolbar!.setSelect(select: false) 174 | } 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /PhotoPicker/core/UICollectionView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+Extension.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/10. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UICollectionView { 13 | func aapl_indexPathsForElementsInRect(rect: CGRect) -> [IndexPath]? { 14 | let allLayoutAttributes = self.collectionViewLayout.layoutAttributesForElements(in: rect) 15 | if allLayoutAttributes == nil || allLayoutAttributes!.count == 0 { 16 | return nil 17 | } 18 | var indexPaths = [IndexPath]() 19 | for layoutAttributes in allLayoutAttributes! { 20 | let indexPath = layoutAttributes.indexPath 21 | indexPaths.append(indexPath) 22 | } 23 | return indexPaths; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /PhotoPicker/core/UIImage+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Extension.swift 3 | // PhotoPicker 4 | // 5 | // Created by liangqi on 16/3/7. 6 | // Copyright © 2016年 dailyios. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIImage { 13 | 14 | /** 15 | resize and crop image 16 | 17 | - parameter toSize: destnation size 18 | 19 | - returns: destination image 20 | */ 21 | func resizeAndCropImage(toSize: CGSize)-> UIImage{ 22 | 23 | let widthFactor = toSize.width / self.size.width 24 | let heightFactor = toSize.height / self.size.height 25 | 26 | var positionX:CGFloat = 0 27 | var positionY:CGFloat = 0 28 | let scaleFactor = widthFactor > heightFactor ? widthFactor : heightFactor 29 | 30 | let scaleWidth = scaleFactor * self.size.width 31 | let scaleHeight = scaleFactor * self.size.height 32 | 33 | if widthFactor > heightFactor { 34 | positionY = (toSize.height - scaleHeight) * 0.5 35 | } else { 36 | positionX = (toSize.width - scaleWidth) * 0.5 37 | } 38 | 39 | UIGraphicsBeginImageContext(toSize) 40 | self.draw(in: CGRect(x: positionX, y: positionY, width: scaleWidth, height: scaleHeight)) 41 | 42 | return UIGraphicsGetImageFromCurrentImageContext()! 43 | 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /PhotoPicker/core/photoCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // photoCollectionViewCell.swift 4 | // PhotoPicker 5 | // 6 | // Created by liangqi on 16/3/6. 7 | // Copyright © 2016年 dailyios. All rights reserved. 8 | // 9 | 10 | import UIKit 11 | import Photos 12 | protocol PhotoCollectionViewCellDelegate: class { 13 | func eventSelectNumberChange(number: Int); 14 | 15 | } 16 | class photoCollectionViewCell: UICollectionViewCell { 17 | 18 | @IBOutlet weak var thumbnail: UIImageView! 19 | @IBOutlet weak var imageSelect: UIImageView! 20 | @IBOutlet weak var selectButton: UIButton! 21 | 22 | weak var delegate: PhotoCollectionViewController? 23 | weak var eventDelegate: PhotoCollectionViewCellDelegate? 24 | 25 | var representedAssetIdentifier: String? 26 | var model : PHAsset? 27 | 28 | override func awakeFromNib() { 29 | super.awakeFromNib() 30 | self.thumbnail.contentMode = .scaleAspectFill 31 | self.thumbnail.clipsToBounds = true 32 | } 33 | 34 | func updateSelected(select:Bool){ 35 | self.selectButton.isSelected = select 36 | self.imageSelect.isHidden = !select 37 | 38 | if select { 39 | self.selectButton.setImage(nil, for: UIControlState.normal) 40 | } else { 41 | self.selectButton.setImage(UIImage(named: "picture_unselect"), for: UIControlState.normal) 42 | } 43 | } 44 | 45 | @IBAction func eventImageSelect(sender: UIButton) { 46 | if sender.isSelected { 47 | sender.isSelected = false 48 | self.imageSelect.isHidden = true 49 | sender.setImage(UIImage(named: "picture_unselect"), for: UIControlState.normal) 50 | if delegate != nil { 51 | if let index = PhotoImage.instance.selectedImage.index(of: self.model!) { 52 | PhotoImage.instance.selectedImage.remove(at: index) 53 | } 54 | 55 | if self.eventDelegate != nil { 56 | self.eventDelegate!.eventSelectNumberChange(number: PhotoImage.instance.selectedImage.count) 57 | } 58 | } 59 | } else { 60 | 61 | if delegate != nil { 62 | if PhotoImage.instance.selectedImage.count >= PhotoPickerController.imageMaxSelectedNum - PhotoPickerController.alreadySelectedImageNum { 63 | self.showSelectErrorDialog() ; 64 | return; 65 | } else { 66 | PhotoImage.instance.selectedImage.append(self.model!) 67 | 68 | if self.eventDelegate != nil { 69 | self.eventDelegate!.eventSelectNumberChange(number: PhotoImage.instance.selectedImage.count) 70 | } 71 | } 72 | } 73 | 74 | sender.isSelected = true 75 | self.imageSelect.isHidden = false 76 | sender.setImage(nil, for: UIControlState.normal) 77 | self.imageSelect.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) 78 | UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 6, options: [UIViewAnimationOptions.curveEaseIn], animations: { () -> Void in 79 | self.imageSelect.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) 80 | }, completion: nil) 81 | } 82 | } 83 | 84 | private func showSelectErrorDialog() { 85 | if self.delegate != nil { 86 | let less = PhotoPickerController.imageMaxSelectedNum - PhotoPickerController.alreadySelectedImageNum 87 | 88 | let range = PhotoPickerConfig.ErrorImageMaxSelect.range(of:"#") 89 | var error = PhotoPickerConfig.ErrorImageMaxSelect 90 | error.replaceSubrange(range!, with: String(less)) 91 | 92 | let alert = UIAlertController.init(title: nil, message: error, preferredStyle: UIAlertControllerStyle.alert) 93 | let confirmAction = UIAlertAction(title: PhotoPickerConfig.ButtonConfirmTitle, style: .default, handler: nil) 94 | alert.addAction(confirmAction) 95 | self.delegate?.present(alert, animated: true, completion: nil) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /PhotoPicker/core/photoCollectionViewCell.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 | 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 | -------------------------------------------------------------------------------- /PhotoPicker/en.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UIBarButtonItem"; title = "返回"; ObjectID = "6Ne-7l-29k"; */ 3 | "6Ne-7l-29k.title" = "返回"; 4 | 5 | /* Class = "UINavigationItem"; title = "照片选择"; ObjectID = "Tay-Us-cZs"; */ 6 | "Tay-Us-cZs.title" = "照片选择"; 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhotoPicker 2 | 3 | 高仿iOS微信图片选择器`swift`版,基于`Photokit`(photo picker like WeChat by PhotoKit) 4 | 5 | ![](./swift-wechar-photo-picker.gif) 6 | 7 | ## 支持版本 8 | ``` 9 | iOS8.0+ 10 | Swift3.0 (current master branch) 11 | 12 | ``` 13 | 14 | 15 | ## 使用说明 16 | 17 | 当前`master`分支为`Swift3.0版本`分支,如果想使用`Swift 2.0+`系列,请切换到`Swfit2`分支,在需要使用的地方,直接调用以下代码即可: 18 | 19 | ``` 20 | let picker = PhotoPickerController(type: PageType.RecentAlbum) 21 | picker.imageSelectDelegate = self 22 | picker.modalPresentationStyle = .Popover 23 | 24 | // max select number 25 | PhotoPickerController.imageMaxSelectedNum = 4 26 | 27 | self.showViewController(picker, sender: nil) 28 | ``` 29 | 图片选择器默认打开最近添加相册列表,如果需要打开其他相册,或者首先打开相册列表,请直接设置`PageType`枚举具体类型即可: 30 | 31 | ``` 32 | enum PageType{ 33 | case List // 打开相册列表 34 | case RecentAlbum // 直接打开最近添加相册 35 | case AllAlbum // 直接打开所有相册列表 36 | } 37 | ``` 38 | 更多参数配置选项,请参照`PhotoPickerConfig.swift`配置文件。 39 | 40 | ## 注意 41 | 1. 当前版本不支持视频,如有需求请反馈 42 | 2. 调用相册和拍照需要在info.plist文件中添加如下两个key, 否则程序会崩溃: 43 | 44 | ```swift 45 | Privacy - Photo Library Usage Description 46 | Privacy - Camera Usage Description 47 | ``` 48 | 49 | -------------------------------------------------------------------------------- /swift-wechar-photo-picker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptut/PhotoPicker/c2115c8a740e4a45b713a6932d82f1bbcb0f68bd/swift-wechar-photo-picker.gif --------------------------------------------------------------------------------