├── .gitignore ├── .travis.yml ├── Document ├── EasyAlbum-github-description.jpg ├── EasyAlbum-github-landscape-screenshots.jpg ├── EasyAlbum-github-logo.png ├── EasyAlbum-github-permission.png └── EasyAlbum-github-portrait-screenshots.jpg ├── EasyAlbum.podspec ├── EasyAlbum.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── EasyAlbum.xcscheme │ └── EasyAlbumDemo.xcscheme ├── EasyAlbumDemo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x-1.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x-1.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x-1.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ └── ItunesArtwork@2x.png │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Cell │ ├── EasyAlbumDemoCell.swift │ └── EasyAlbumDemoCell.xib ├── Info.plist └── ViewController.swift ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── EasyAlbum │ ├── Assets.xcassets │ ├── Contents.json │ ├── album_camera.imageset │ │ ├── Contents.json │ │ ├── album_camera@2x.png │ │ └── album_camera@3x.png │ ├── album_close.imageset │ │ ├── Contents.json │ │ ├── album_close@2x.png │ │ └── album_close@3x.png │ └── album_done.imageset │ │ ├── Contents.json │ │ ├── album_done@2x.png │ │ └── album_done@3x.png │ ├── Cell │ ├── AlbumCategoryCell.swift │ └── AlbumPhotoCell.swift │ ├── EasyAlbum.bundle │ ├── album_camera@2x.png │ ├── album_camera@3x.png │ ├── album_close@2x.png │ ├── album_close@3x.png │ ├── album_done@2x.png │ └── album_done@3x.png │ ├── EasyAlbum.h │ ├── EasyAlbum.swift │ ├── EasyAlbumCore.swift │ ├── Extension │ ├── CGSize+Extension.swift │ ├── NotificationName+Extension.swift │ ├── String+Extension.swift │ ├── UICollectionView+Extension.swift │ ├── UIColor+Extension.swift │ ├── UIImage+Extension.swift │ ├── UIScreen+Extension.swift │ └── UITableView+Extension.swift │ ├── Info.plist │ ├── Manager │ └── PhotoManager.swift │ ├── Model │ ├── AlbumData.swift │ ├── AlbumFolder.swift │ └── AlbumNotification.swift │ ├── ViewController │ ├── EasyAlbumCameraVC.swift │ ├── EasyAlbumNAC.swift │ ├── EasyAlbumPageContentVC.swift │ ├── EasyAlbumPreviewPageVC.swift │ └── EasyAlbumVC.swift │ └── Widget │ ├── AlbumBorderView.swift │ ├── AlbumCategoryView.swift │ ├── AlbumDoneView.swift │ ├── AlbumSelectedButton.swift │ └── AlbumToast.swift └── Tests ├── EasyAlbumTests ├── EasyAlbumTests.swift └── XCTestManifests.swift └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/swift,xcode,cocoapods,objective-c 3 | # Edit at https://www.gitignore.io/?templates=swift,xcode,cocoapods,objective-c 4 | 5 | ### CocoaPods ### 6 | ## CocoaPods GitIgnore Template 7 | 8 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 9 | # - Also handy if you have a large number of dependant pods 10 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 11 | Pods/ 12 | 13 | ### Objective-C ### 14 | # Xcode 15 | # 16 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 17 | .swiftpm 18 | 19 | ## Build generated 20 | build/ 21 | DerivedData/ 22 | 23 | ## Various settings 24 | *.pbxuser 25 | !default.pbxuser 26 | *.mode1v3 27 | !default.mode1v3 28 | *.mode2v3 29 | !default.mode2v3 30 | *.perspectivev3 31 | !default.perspectivev3 32 | xcuserdata/ 33 | 34 | ## Other 35 | *.moved-aside 36 | *.xccheckout 37 | *.xcscmblueprint 38 | 39 | ## Obj-C/Swift specific 40 | *.hmap 41 | *.ipa 42 | *.dSYM.zip 43 | *.dSYM 44 | 45 | # CocoaPods 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # Pods/ 50 | # Add this line if you want to avoid checking in source code from the Xcode workspace 51 | # *.xcworkspace 52 | 53 | # Carthage 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | 70 | # Code Injection 71 | # After new code Injection tools there's a generated folder /iOSInjectionProject 72 | # https://github.com/johnno1962/injectionforxcode 73 | 74 | iOSInjectionProject/ 75 | 76 | ### Objective-C Patch ### 77 | 78 | ### Swift ### 79 | # Xcode 80 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 81 | 82 | 83 | 84 | 85 | 86 | ## Playgrounds 87 | timeline.xctimeline 88 | playground.xcworkspace 89 | 90 | # Swift Package Manager 91 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 92 | # Packages/ 93 | # Package.pins 94 | # Package.resolved 95 | .build/ 96 | 97 | # CocoaPods 98 | # We recommend against adding the Pods directory to your .gitignore. However 99 | # you should judge for yourself, the pros and cons are mentioned at: 100 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 101 | # Pods/ 102 | # Add this line if you want to avoid checking in source code from the Xcode workspace 103 | # *.xcworkspace 104 | 105 | # Carthage 106 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 107 | # Carthage/Checkouts 108 | 109 | 110 | # fastlane 111 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 112 | # screenshots whenever they are needed. 113 | # For more information about the recommended setup visit: 114 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 115 | 116 | 117 | # Code Injection 118 | # After new code Injection tools there's a generated folder /iOSInjectionProject 119 | # https://github.com/johnno1962/injectionforxcode 120 | 121 | 122 | ### Xcode ### 123 | # Xcode 124 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 125 | 126 | ## User settings 127 | 128 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 129 | 130 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 131 | 132 | ### Xcode Patch ### 133 | *.xcodeproj/* 134 | !*.xcodeproj/project.pbxproj 135 | !*.xcodeproj/xcshareddata/ 136 | !*.xcworkspace/contents.xcworkspacedata 137 | /*.gcno 138 | **/xcshareddata/WorkspaceSettings.xcsettings 139 | 140 | # End of https://www.gitignore.io/api/swift,xcode,cocoapods,objective-c -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: 2 | - swift 3 | 4 | osx_image: 5 | - xcode12 6 | 7 | # reference: https://docs.travis-ci.com/user/reference/osx/#xcode-12 8 | env: 9 | matrix: 10 | exclude: 11 | - osx_image: xcode12 12 | - TEST_SDK=iphoneos14.0 OS=14.0 NAME='iPhone 11 Pro Max' 13 | - TEST_SDK=iphoneos10.3 OS=10.3 NAME='iPhone 8 Plus' 14 | - TEST_SDK=iphonesimulator13.5 OS=13.5 NAME='iPhone 11 Pro' 15 | - TEST_SDK=iphonesimulator11.4 OS=11.4 NAME='iPhone 8' 16 | 17 | branches: 18 | only: 19 | - master 20 | 21 | script: 22 | - xcodebuild -project EasyAlbum.xcodeproj -scheme EasyAlbum -sdk $TEST_SDK -destination "platform=iOS Simulator,OS=$OS,name=$NAME" ONLY_ACTIVE_ARCH=YES 23 | 24 | after_success: 25 | #- bash <(curl -s https://codecov.io/bash) 26 | 27 | notifications: 28 | email: 29 | recipients: 30 | - ray00178@gmail.com 31 | - brave2risks@gmail.com 32 | on_success: never # default: change 33 | on_failure: always # default: always 34 | -------------------------------------------------------------------------------- /Document/EasyAlbum-github-description.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Document/EasyAlbum-github-description.jpg -------------------------------------------------------------------------------- /Document/EasyAlbum-github-landscape-screenshots.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Document/EasyAlbum-github-landscape-screenshots.jpg -------------------------------------------------------------------------------- /Document/EasyAlbum-github-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Document/EasyAlbum-github-logo.png -------------------------------------------------------------------------------- /Document/EasyAlbum-github-permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Document/EasyAlbum-github-permission.png -------------------------------------------------------------------------------- /Document/EasyAlbum-github-portrait-screenshots.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Document/EasyAlbum-github-portrait-screenshots.jpg -------------------------------------------------------------------------------- /EasyAlbum.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint EasyAlbum.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see https://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |spec| 10 | 11 | spec.name = "EasyAlbum" 12 | spec.version = "2.3.1" 13 | spec.summary = "📷 A lightweight, pure-Swift library for pick up photo from ur album." 14 | spec.description = <<-DESC 15 | 📷 A lightweight, pure-Swift library can help u easy to pick up photo from album. 16 | DESC 17 | 18 | spec.homepage = "https://github.com/ray00178/EasyAlbum" 19 | spec.license = { :type => "MIT", :file => "LICENSE" } 20 | spec.author = { "Ray" => "ray00178@gmail.com" } 21 | spec.social_media_url = "https://twitter.com/ray00178" 22 | spec.swift_version = '5.0' 23 | 24 | spec.platform = :ios, "10.0+" 25 | spec.ios.deployment_target = "10.0" 26 | 27 | spec.source = { :git => "https://github.com/ray00178/EasyAlbum.git", :tag => spec.version } 28 | spec.source_files = "Sources/**/*.{h,swift,xib}" 29 | spec.resource_bundles = { 'EasyAlbum' => ['Sources/EasyAlbum/EasyAlbum.bundle/*.{png,pdf}'] } 30 | spec.frameworks = 'UIKit', 'Photos', 'PhotosUI', 'ImageIO' 31 | 32 | end 33 | -------------------------------------------------------------------------------- /EasyAlbum.xcodeproj/xcshareddata/xcschemes/EasyAlbum.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /EasyAlbum.xcodeproj/xcshareddata/xcschemes/EasyAlbumDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /EasyAlbumDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // EasyAlbumDemo 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-40x40@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-60x60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "Icon-App-20x20@1x.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@2x-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-29x29@1x.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@2x-1.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-40x40@1x.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@2x-1.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-76x76@1x.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-83.5x83.5@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "ItunesArtwork@2x.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/EasyAlbumDemo/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /EasyAlbumDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /EasyAlbumDemo/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 | -------------------------------------------------------------------------------- /EasyAlbumDemo/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 | 33 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /EasyAlbumDemo/Cell/EasyAlbumDemoCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EasyAlbumDemoCell.swift 3 | // EasyAlbumDemo 4 | // 5 | // Created by Ray on 2019/4/10. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class EasyAlbumDemoCell: UITableViewCell { 12 | 13 | @IBOutlet weak var mImgView: UIImageView! 14 | @IBOutlet weak var mDescriptionLab: UILabel! 15 | 16 | var data: (image: UIImage, desc: String)! { 17 | didSet { 18 | mImgView.image = data.image 19 | mDescriptionLab.text = data.desc 20 | } 21 | } 22 | 23 | override func awakeFromNib() { 24 | super.awakeFromNib() 25 | 26 | mImgView.layer.cornerRadius = 25.0 27 | mImgView.layer.masksToBounds = true 28 | 29 | selectionStyle = .none 30 | } 31 | 32 | override func setSelected(_ selected: Bool, animated: Bool) { 33 | super.setSelected(selected, animated: animated) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /EasyAlbumDemo/Cell/EasyAlbumDemoCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /EasyAlbumDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleExecutable 6 | $(EXECUTABLE_NAME) 7 | CFBundleIdentifier 8 | $(PRODUCT_BUNDLE_IDENTIFIER) 9 | CFBundleInfoDictionaryVersion 10 | 6.0 11 | CFBundleName 12 | $(PRODUCT_NAME) 13 | CFBundlePackageType 14 | APPL 15 | CFBundleShortVersionString 16 | 1.0.0 17 | CFBundleVersion 18 | 1 19 | LSRequiresIPhoneOS 20 | 21 | NSCameraUsageDescription 22 | Please allow access to your camera for taking photos 23 | NSPhotoLibraryAddUsageDescription 24 | Please allow access to your album to save images 25 | NSPhotoLibraryUsageDescription 26 | Please allow access to your photo album to select photos 27 | PHPhotoLibraryPreventAutomaticLimitedAccessAlert 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UIRequiredDeviceCapabilities 34 | 35 | armv7 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /EasyAlbumDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // EasyAlbumDemo 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import EasyAlbum 11 | 12 | class ViewController: UIViewController { 13 | 14 | @IBOutlet weak var tableView: UITableView! 15 | @IBOutlet weak var albumOneButton: UIButton! 16 | @IBOutlet weak var albumTwoButton: UIButton! 17 | 18 | private let CELL = "EasyAlbumDemoCell" 19 | private var datas: [AlbumData] = [] 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | setup() 24 | } 25 | 26 | private func setup() { 27 | if #available(iOS 13.0, *) { 28 | overrideUserInterfaceStyle = .light 29 | } 30 | 31 | tableView.register(UINib(nibName: CELL, bundle: nil), forCellReuseIdentifier: CELL) 32 | tableView.estimatedRowHeight = 70.0 33 | tableView.rowHeight = UITableView.automaticDimension 34 | tableView.dataSource = self 35 | tableView.delegate = self 36 | 37 | albumOneButton.layer.cornerRadius = 7.5 38 | albumOneButton.addTarget(self, action: #selector(click(_:)), for: .touchUpInside) 39 | 40 | albumTwoButton.layer.cornerRadius = 7.5 41 | albumTwoButton.addTarget(self, action: #selector(click(_:)), for: .touchUpInside) 42 | } 43 | 44 | @objc private func click(_ btn: UIButton) { 45 | switch btn { 46 | case albumOneButton: 47 | EasyAlbum 48 | .of(appName: "EasyAlbum") 49 | .limit(100) 50 | // #cc0066 51 | .barTintColor(UIColor(red: 0.8, green: 0.0, blue: 0.4, alpha: 1.0)) 52 | // #00cc66 53 | .pickColor(UIColor(red: 0.0, green: 0.8, blue: 0.4, alpha: 1.0)) 54 | .sizeFactor(.auto) 55 | .orientation(.all) 56 | .start(self, delegate: self) 57 | case albumTwoButton: 58 | EasyAlbum.of(appName: "EasyAlbum") 59 | .start(self, delegate: self) 60 | default: break 61 | } 62 | } 63 | } 64 | 65 | extension ViewController: UITableViewDataSource, UITableViewDelegate { 66 | func numberOfSections(in tableView: UITableView) -> Int { 67 | return 1 68 | } 69 | 70 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 71 | return datas.count 72 | } 73 | 74 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 75 | let index = indexPath.row 76 | let photo = datas[index] 77 | let cell = tableView.dequeueReusableCell(withIdentifier: CELL, for: indexPath) as! EasyAlbumDemoCell 78 | let desc = """ 79 | FileName = \(photo.fileName ?? "") 80 | FileUTI = \(photo.fileUTI ?? "") 81 | FileSize = \(photo.fileSize / 1024)KB 82 | """ 83 | cell.data = (photo.image, desc) 84 | return cell 85 | } 86 | } 87 | 88 | extension ViewController: EasyAlbumDelegate { 89 | func easyAlbumDidSelected(_ photos: [AlbumData]) { 90 | if datas.count > 0 { datas.removeAll() } 91 | 92 | datas.append(contentsOf: photos) 93 | tableView.reloadData() 94 | 95 | photos.forEach({ print("AlbumData = \($0)") }) 96 | } 97 | 98 | func easyAlbumDidCanceled() { 99 | // do something 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jhang, Pei-Yang(Ray) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "EasyAlbum", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "EasyAlbum", targets: ["EasyAlbum"]), 11 | ], 12 | targets: [ 13 | .target( 14 | name: "EasyAlbum", 15 | exclude: [ 16 | "Info.plist" 17 | ], 18 | resources: [ 19 | .process("EasyAlbum.bundle") 20 | ] 21 | ), 22 | .testTarget( 23 | name: "EasyAlbumTests", 24 | dependencies: ["EasyAlbum"] 25 | ), 26 | ], 27 | swiftLanguageVersions: [.v5] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | EasyAlbum 3 |

4 | 5 | [![Build Status](https://travis-ci.org/ray00178/EasyAlbum.svg?branch=master)](https://travis-ci.org/ray00178/EasyAlbum) ![Cocoapods platforms](https://img.shields.io/cocoapods/p/EasyAlbum.svg) ![Cocoapods version](https://img.shields.io/cocoapods/v/EasyAlbum.svg) ![Cocoapods license](https://img.shields.io/cocoapods/l/EasyAlbum.svg) ![Language](https://img.shields.io/badge/language-swift-orange.svg) ![GitHub stars](https://img.shields.io/github/stars/ray00178/EasyAlbum.svg?style=social) 6 | 7 | ## Features 8 | - Support Single choice、Multiple choice、Preview、Folder switch and pick up photo. 9 | - In preview photo, your can zoom photo. 10 | - According to your project color, Setting your pick color、navigationBar tint color、navigationBar bar tint color. 11 | - According to your preferences / needs, Show the number of fields and select the number of restrictions. 12 | - Support language 🇹🇼Chinese Traditional、🇨🇳Chinese Simplified, otherwise use 🇺🇸English. 13 | 14 | ## Screenshots 15 | ![Screenshots Portrait](https://github.com/ray00178/EasyAlbum/blob/master/Document/EasyAlbum-github-portrait-screenshots.jpg) 16 | ![Screenshots Landscape](https://github.com/ray00178/EasyAlbum/blob/master/Document/EasyAlbum-github-landscape-screenshots.jpg) 17 | 18 | ## Requirements and Details 19 | * iOS 10.0+ 20 | * Xcode 11.0+ 21 | * Build with Swift 5.0+ 22 | 23 | ## Installation 24 | ### CocoaPods 25 | [CocoaPods](https://cocoapods.org/) is a dependency manager for Cocoa projects. You can install it with the following command: 26 | 27 | $ gem install cocoapods 28 | 29 | To integrate EasyAlbum into your XCode project using CocoaPods, specify it to a target in your `Podfile`: 30 | 31 | ```ruby 32 | source 'https://github.com/CocoaPods/Specs.git' 33 | platform :ios, '10.0' 34 | use_frameworks! 35 | 36 | target '' do 37 | # Use swift 5.0 38 | pod 'EasyAlbum', '~> 2.3.1' 39 | end 40 | ``` 41 | 42 | You should open the `{Project}.xcworkspace` instead of the `{Project}.xcodeproj` after you installed anything from CocoaPods. 43 | 44 | For more information about how to use CocoaPods, I suggest this [tutorial](https://www.raywenderlich.com/626-cocoapods-tutorial-for-swift-getting-started). 45 | 46 | ### Swift Package Manager 47 | 48 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but Alamofire does support its use on supported platforms. 49 | 50 | Once you have your Swift package set up, adding Alamofire as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. 51 | 52 | ```swift 53 | dependencies: [ 54 | .package(url: "https://github.com/ray00178/EasyAlbum", .upToNextMajor(from: "2.3.1")) 55 | ] 56 | ``` 57 | 58 | ## Usage 59 | ##### 1. Open ur Info.plist and add the following permissions. 60 | ![Photo Permission](https://github.com/ray00178/EasyAlbum/blob/master/Document/EasyAlbum-github-permission.png) 61 | ```xml 62 | NSCameraUsageDescription 63 | Please allow access to your camera then take picture. 64 | NSPhotoLibraryUsageDescription 65 | Please allow access to your album then pick up photo. 66 | NSPhotoLibraryAddUsageDescription 67 | Please allow access to your album then save photo. 68 | 69 | On iOS 14 later 70 | PHPhotoLibraryPreventAutomaticLimitedAccessAlert 71 | 72 | ``` 73 | ##### 2. Use EasyAlbum. You can building what you want. 74 | ```swift 75 | import EasyAlbum 76 | 77 | /** 78 | * @param appName : (required) (default: EasyAlbum) 79 | * @param tintColor : (choose) (default: #ffffff) 80 | * @param barTintColor : (choose) (default: #673ab7) 81 | * @param span : (choose) (default: 3) 82 | * @param limit : (choose) (default: 30) 83 | * @param orientation       : (choose)   (default: .all) 84 |  * @param message           : (choose)   (default: Photo pick up the most `30(limitCount)`!) 85 | * @param pickColor : (choose) (default: #ffc107) 86 | * @param showCamera : (choose) (default: true) 87 | * @param crop : (choose) (default: false) (Use for camera) 88 | * @param isLightStatusStyle : (choose) (default: true) 89 | * @param sizeFactor : (choose) (default: .auto) 90 | * @param orientation : (choose) (default: .all) 91 | * @param start : (required) 92 | */ 93 | 94 | // Easy way 95 | EasyAlbum.of(appName: "EasyAlbum") 96 | .start(self, delegate: self) 97 | 98 | // Use many way 99 | EasyAlbum.of(appName: "EasyAlbum") 100 | .limit(3) 101 | .sizeFactor(.fit(width: 1125.0, height: 2436.0)) 102 | .orientation(.portrait) 103 | .start(self, delegate: self) 104 | ``` 105 | 106 | ##### 3. EasyAlbum parameters 107 | ![EasyAlbum parameters](https://github.com/ray00178/EasyAlbum/blob/master/Document/EasyAlbum-github-description.jpg) 108 | 109 | ##### 4. Extension EasyAlbumDelegate 110 | ```swift 111 | extension ViewController: EasyAlbumDelegate { 112 | 113 | func easyAlbumDidSelected(_ photos: [AlbumData]) { 114 | // You can do something by selected. 115 | photos.forEach({ print("AlbumData 👉🏻 \($0)") }) 116 | } 117 | 118 | func easyAlbumDidCanceled() { 119 | // You can do something by canceled. 120 | } 121 | } 122 | ``` 123 | 124 | ##### 5. AlbumData 👉🏻 `You can get many photo information.` 125 | | Attribute | Type | Value | Note | 126 | | :--------------: | :---------: | :------------------------------------: | :-----------: | 127 | | image | UIImage | , {1125, 752} | | 128 | | mediaType | String | "image" | | 129 | | width | CGFloat | 4555.0 | Origin Width | 130 | | height | CGFloat | 3041.0 | Origin Height | 131 | | creationDate | Date? | Optional(2013-11-05 11:08:39 +0000) | | 132 | | modificationDate | Date? | Optional(2019-04-13 14:34:57 +0000) | | 133 | | isFavorite | Bool | true | | 134 | | isHidden | Bool | false | | 135 | | location | CLLocation? | Optional(<+63.53140000,-19.51120000>) | | 136 | | fileName | String? | Optional("DSC_5084.jpg") | | 137 | | fileData | Data? | Optional(8063276 bytes) | | 138 | | fileSize | Int | 8063276 bytes | Maybe zero | 139 | | fileUTI | String? | Optional("public.jpeg") | | 140 | 141 | ## Update Description 142 | #### Version:2.3.1 143 | - Supprot iOS verison from 9.0 to 10.0. 144 | - Support iOS 14 `limited` authorization status. 145 | - Improve some logic flow and code. 146 | - Support `Swift Package Manager` install 147 | 148 | #### Version:2.2.0 149 | - Optimization PhotoManager. 150 | - Fix `retain cycle`. 151 | - Preview enter `transition animation`. 152 | - Add enum SizeFactor property `original`. 153 | 154 | #### Version:2.1.0 155 | - Fix the bottom view can't adapts to `iPhone` device. 156 | - Support device rotate. 157 | - In preview page, you can to leave by swipe up or swipe down. 158 | - Add `orientation` property and remove `showGIF` & `titleColor` property. 159 | 160 | ## Communication 161 | - If you found a `bug`, open an issue. 162 | - If you have a `feature request`, open an issue. 163 | - If you want to `contribute`, submit a pull request. 164 | 165 | ## Todo List 166 | - [ ] Preview exit `transition animation` 167 | - [ ] Support `Live Photo` 168 | - [ ] Support `SPM` install 169 | 170 | ## License 171 | EasyAlbum is available under the MIT license. See the [LICENSE](LICENSE) file for more info. 172 | 173 | MIT License 174 | 175 | Copyright (c) [2019] [Jhang, Pei-Yang(Ray)] 176 | 177 | Permission is hereby granted, free of charge, to any person obtaining a copy 178 | of this software and associated documentation files (the "Software"), to deal 179 | in the Software without restriction, including without limitation the rights 180 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 181 | copies of the Software, and to permit persons to whom the Software is 182 | furnished to do so, subject to the following conditions: 183 | 184 | The above copyright notice and this permission notice shall be included in all 185 | copies or substantial portions of the Software. 186 | 187 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 188 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 189 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 190 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 191 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 192 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 193 | SOFTWARE. 194 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Assets.xcassets/album_camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "album_camera@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "album_camera@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Assets.xcassets/album_camera.imageset/album_camera@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Sources/EasyAlbum/Assets.xcassets/album_camera.imageset/album_camera@2x.png -------------------------------------------------------------------------------- /Sources/EasyAlbum/Assets.xcassets/album_camera.imageset/album_camera@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Sources/EasyAlbum/Assets.xcassets/album_camera.imageset/album_camera@3x.png -------------------------------------------------------------------------------- /Sources/EasyAlbum/Assets.xcassets/album_close.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "album_close@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "album_close@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Assets.xcassets/album_close.imageset/album_close@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Sources/EasyAlbum/Assets.xcassets/album_close.imageset/album_close@2x.png -------------------------------------------------------------------------------- /Sources/EasyAlbum/Assets.xcassets/album_close.imageset/album_close@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Sources/EasyAlbum/Assets.xcassets/album_close.imageset/album_close@3x.png -------------------------------------------------------------------------------- /Sources/EasyAlbum/Assets.xcassets/album_done.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "album_done@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "album_done@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Assets.xcassets/album_done.imageset/album_done@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Sources/EasyAlbum/Assets.xcassets/album_done.imageset/album_done@2x.png -------------------------------------------------------------------------------- /Sources/EasyAlbum/Assets.xcassets/album_done.imageset/album_done@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Sources/EasyAlbum/Assets.xcassets/album_done.imageset/album_done@3x.png -------------------------------------------------------------------------------- /Sources/EasyAlbum/Cell/AlbumCategoryCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumCategoryCell.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/10. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AlbumCategoryCell: UICollectionViewCell { 12 | 13 | private var imageView: UIImageView! 14 | private var categoryLabel: UILabel! 15 | private var selectedButton: AlbumSelectedButton! 16 | 17 | var data: AlbumFolder! { 18 | didSet { setData() } 19 | } 20 | 21 | override init(frame: CGRect) { 22 | super.init(frame: frame) 23 | setup() 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | super.init(coder: coder) 28 | setup() 29 | } 30 | 31 | private func setup() { 32 | imageView = UIImageView() 33 | imageView.contentMode = .scaleAspectFill 34 | imageView.layer.cornerRadius = 5.0 35 | imageView.layer.masksToBounds = true 36 | imageView.translatesAutoresizingMaskIntoConstraints = false 37 | contentView.addSubview(imageView) 38 | 39 | categoryLabel = UILabel() 40 | categoryLabel.textColor = UIColor(hex: "1A1A1A") 41 | categoryLabel.textAlignment = .center 42 | categoryLabel.font = .systemFont(ofSize: 12.0, weight: .medium) 43 | categoryLabel.translatesAutoresizingMaskIntoConstraints = false 44 | contentView.addSubview(categoryLabel) 45 | 46 | selectedButton = AlbumSelectedButton() 47 | selectedButton.translatesAutoresizingMaskIntoConstraints = false 48 | contentView.addSubview(selectedButton) 49 | 50 | // AutoLayout 51 | imageView.widthAnchor 52 | .constraint(equalToConstant: 50.0) 53 | .isActive = true 54 | imageView.heightAnchor 55 | .constraint(equalToConstant: 50.0) 56 | .isActive = true 57 | imageView.topAnchor 58 | .constraint(equalTo: contentView.topAnchor, constant: 10.0) 59 | .isActive = true 60 | imageView.centerXAnchor 61 | .constraint(equalTo: contentView.centerXAnchor) 62 | .isActive = true 63 | 64 | categoryLabel.heightAnchor 65 | .constraint(equalToConstant: 20.0) 66 | .isActive = true 67 | categoryLabel.leadingAnchor 68 | .constraint(equalTo: contentView.leadingAnchor, constant: 10.0) 69 | .isActive = true 70 | categoryLabel.trailingAnchor 71 | .constraint(equalTo: contentView.trailingAnchor, constant: -10.0) 72 | .isActive = true 73 | categoryLabel.topAnchor 74 | .constraint(equalTo: imageView.bottomAnchor, constant: 5.0) 75 | .isActive = true 76 | 77 | selectedButton.topAnchor 78 | .constraint(equalTo: imageView.topAnchor) 79 | .isActive = true 80 | selectedButton.leadingAnchor 81 | .constraint(equalTo: imageView.leadingAnchor) 82 | .isActive = true 83 | selectedButton.trailingAnchor 84 | .constraint(equalTo: imageView.trailingAnchor) 85 | .isActive = true 86 | selectedButton.bottomAnchor 87 | .constraint(equalTo: imageView.bottomAnchor) 88 | .isActive = true 89 | } 90 | 91 | private func setData() { 92 | let wh = 60.0 * UIScreen.density 93 | let size = CGSize(width: wh, height: wh) 94 | PhotoManager.share.fetchThumbnail(form: data.assets[0], 95 | size: size, 96 | options: .exact(isSync: true)) 97 | { [weak self] (image) in 98 | self?.imageView.image = image 99 | } 100 | 101 | categoryLabel.text = data.title 102 | selectedButton.alpha = data.isCheck ? 1.0 : 0.0 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Cell/AlbumPhotoCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumPhotoCell.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | protocol AlbumPhotoCellDelegate: class { 13 | func albumPhotoCell(didNumberClickAt item: Int) 14 | } 15 | 16 | class AlbumPhotoCell: UICollectionViewCell { 17 | 18 | private var imageView: UIImageView! 19 | private var borderView: AlbumBorderView! 20 | private var gifLabel: UILabel! 21 | private var numberButton: UIButton! 22 | 23 | var representedAssetIdentifier: String? 24 | 25 | weak var delegate: AlbumPhotoCellDelegate? 26 | 27 | override init(frame: CGRect) { 28 | super.init(frame: frame) 29 | setup() 30 | } 31 | 32 | required init?(coder: NSCoder) { 33 | super.init(coder: coder) 34 | setup() 35 | } 36 | 37 | private func setup() { 38 | imageView = UIImageView() 39 | imageView.contentMode = .scaleAspectFill 40 | imageView.clipsToBounds = true 41 | imageView.translatesAutoresizingMaskIntoConstraints = false 42 | contentView.addSubview(imageView) 43 | 44 | borderView = AlbumBorderView() 45 | borderView.isHidden = true 46 | borderView.translatesAutoresizingMaskIntoConstraints = false 47 | contentView.addSubview(borderView) 48 | 49 | gifLabel = UILabel() 50 | gifLabel.text = "GIF" 51 | gifLabel.textColor = UIColor(hex: "828282") 52 | gifLabel.textAlignment = .center 53 | gifLabel.font = .systemFont(ofSize: 13.0, weight: .medium) 54 | gifLabel.backgroundColor = .white 55 | gifLabel.alpha = 0.75 56 | gifLabel.layer.cornerRadius = 10.0 57 | gifLabel.layer.masksToBounds = true 58 | gifLabel.isHidden = true 59 | gifLabel.translatesAutoresizingMaskIntoConstraints = false 60 | contentView.addSubview(gifLabel) 61 | 62 | numberButton = UIButton(type: .system) 63 | numberButton.titleLabel?.font = .systemFont(ofSize: 13.0, weight: .bold) 64 | numberButton.setTitleColor(UIColor.white, for: .normal) 65 | numberButton.layer.cornerRadius = 14.0 66 | numberButton.layer.borderWidth = 1.5 67 | numberButton.addTarget(self, 68 | action: #selector(didNumberClicked(_:)), 69 | for: .touchUpInside) 70 | numberButton.isHidden = true 71 | numberButton.translatesAutoresizingMaskIntoConstraints = false 72 | contentView.addSubview(numberButton) 73 | 74 | // AutoLayout 75 | imageView.topAnchor 76 | .constraint(equalTo: contentView.topAnchor) 77 | .isActive = true 78 | imageView.leadingAnchor 79 | .constraint(equalTo: contentView.leadingAnchor) 80 | .isActive = true 81 | imageView.trailingAnchor 82 | .constraint(equalTo: contentView.trailingAnchor) 83 | .isActive = true 84 | imageView.bottomAnchor 85 | .constraint(equalTo: contentView.bottomAnchor) 86 | .isActive = true 87 | 88 | borderView.topAnchor 89 | .constraint(equalTo: contentView.topAnchor) 90 | .isActive = true 91 | borderView.leadingAnchor 92 | .constraint(equalTo: contentView.leadingAnchor) 93 | .isActive = true 94 | borderView.trailingAnchor 95 | .constraint(equalTo: contentView.trailingAnchor) 96 | .isActive = true 97 | borderView.bottomAnchor 98 | .constraint(equalTo: contentView.bottomAnchor) 99 | .isActive = true 100 | 101 | gifLabel.widthAnchor 102 | .constraint(equalToConstant: 30.0) 103 | .isActive = true 104 | gifLabel.heightAnchor 105 | .constraint(equalToConstant: 20.0) 106 | .isActive = true 107 | gifLabel.leadingAnchor 108 | .constraint(equalTo: contentView.leadingAnchor, constant: 10.0) 109 | .isActive = true 110 | gifLabel.bottomAnchor 111 | .constraint(equalTo: contentView.bottomAnchor, constant: -10.0) 112 | .isActive = true 113 | 114 | numberButton.widthAnchor 115 | .constraint(equalToConstant: 28.0) 116 | .isActive = true 117 | numberButton.heightAnchor 118 | .constraint(equalToConstant: 28.0) 119 | .isActive = true 120 | numberButton.topAnchor 121 | .constraint(equalTo: contentView.topAnchor, constant: 10.0) 122 | .isActive = true 123 | numberButton.trailingAnchor 124 | .constraint(equalTo: contentView.trailingAnchor, constant: -10.0) 125 | .isActive = true 126 | } 127 | 128 | override func prepareForReuse() { 129 | imageView.image = nil 130 | super.prepareForReuse() 131 | } 132 | 133 | func setData(from asset: PHAsset, image: UIImage, number: Int?, pickColor: UIColor, item: Int) { 134 | let hasNumber = number ?? 0 > 0 135 | borderView.isHidden = !hasNumber 136 | 137 | if borderView.isHidden == false { 138 | borderView.borderColor = pickColor 139 | } 140 | 141 | gifLabel.isHidden = PhotoManager.share.isAnimatedImage(from: asset) == false 142 | 143 | numberButton.layer.borderColor = borderView.isHidden ? 144 | UIColor(white: 1.0, alpha: 0.78).cgColor : 145 | pickColor.cgColor 146 | numberButton.backgroundColor = borderView.isHidden ? 147 | UIColor(hex: "000000", alpha: 0.1) : 148 | pickColor 149 | numberButton.setTitle(hasNumber ? "\(number ?? 0)" : "", for: .normal) 150 | numberButton.tag = item 151 | numberButton.isHidden = false 152 | 153 | imageView.image = image 154 | } 155 | 156 | @objc private func didNumberClicked(_ btn: UIButton) { 157 | delegate?.albumPhotoCell(didNumberClickAt: btn.tag) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/EasyAlbum.bundle/album_camera@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Sources/EasyAlbum/EasyAlbum.bundle/album_camera@2x.png -------------------------------------------------------------------------------- /Sources/EasyAlbum/EasyAlbum.bundle/album_camera@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Sources/EasyAlbum/EasyAlbum.bundle/album_camera@3x.png -------------------------------------------------------------------------------- /Sources/EasyAlbum/EasyAlbum.bundle/album_close@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Sources/EasyAlbum/EasyAlbum.bundle/album_close@2x.png -------------------------------------------------------------------------------- /Sources/EasyAlbum/EasyAlbum.bundle/album_close@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Sources/EasyAlbum/EasyAlbum.bundle/album_close@3x.png -------------------------------------------------------------------------------- /Sources/EasyAlbum/EasyAlbum.bundle/album_done@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Sources/EasyAlbum/EasyAlbum.bundle/album_done@2x.png -------------------------------------------------------------------------------- /Sources/EasyAlbum/EasyAlbum.bundle/album_done@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ray00178/EasyAlbum/c5493f3e6c439666f50d60f2bf433d38a37debe9/Sources/EasyAlbum/EasyAlbum.bundle/album_done@3x.png -------------------------------------------------------------------------------- /Sources/EasyAlbum/EasyAlbum.h: -------------------------------------------------------------------------------- 1 | // 2 | // EasyAlbum.h 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for EasyAlbum. 12 | FOUNDATION_EXPORT double EasyAlbumVersionNumber; 13 | 14 | //! Project version string for EasyAlbum. 15 | FOUNDATION_EXPORT const unsigned char EasyAlbumVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/EasyAlbum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EasyAlbum.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct EasyAlbum { 12 | 13 | private var albumNVC: EasyAlbumNAC 14 | 15 | private init(appName: String) { 16 | albumNVC = EasyAlbumNAC() 17 | albumNVC.appName = appName 18 | } 19 | 20 | public static func of(appName: String) -> EasyAlbum { 21 | return EasyAlbum(appName: appName) 22 | } 23 | 24 | /// NavigationBar tint color 25 | /// 26 | /// - Parameter color: default = #ffffff 27 | /// - Returns: EasyAlbum 28 | public func tintColor(_ color: UIColor) -> EasyAlbum { 29 | albumNVC.tintColor = color 30 | return self 31 | } 32 | 33 | /// NavigationBar bar color 34 | /// 35 | /// - Parameter color: default = #673ab7 36 | /// - Returns: EasyAlbum 37 | public func barTintColor(_ color: UIColor) -> EasyAlbum { 38 | albumNVC.barTintColor = color 39 | return self 40 | } 41 | 42 | /// Setting statusBar style 43 | /// 44 | /// - Parameter isLight: default = true 45 | /// - Returns: EasyAlbum 46 | public func lightStatusBarStyle(_ isLight: Bool) -> EasyAlbum { 47 | albumNVC.lightStatusBarStyle = isLight 48 | return self 49 | } 50 | 51 | /// Selected photo count of max 52 | /// 53 | /// - Parameter count: default = 30 54 | /// - Returns: EasyAlbum 55 | public func limit(_ count: Int) -> EasyAlbum { 56 | albumNVC.limit = count 57 | return self 58 | } 59 | 60 | /// Span count per line 61 | /// 62 | /// - Parameter count: default = 3 63 | /// - Returns: EasyAlbum 64 | public func span(_ count: Int) -> EasyAlbum { 65 | albumNVC.span = count 66 | return self 67 | } 68 | 69 | /// Selected color 70 | /// 71 | /// - Parameter color: default = #ffc107 72 | /// - Returns: EasyAlbum 73 | public func pickColor(_ color: UIColor) -> EasyAlbum { 74 | albumNVC.pickColor = color 75 | return self 76 | } 77 | 78 | /// Is need crop photo, only for camera mod 79 | /// 80 | /// - Parameter crop: default = false 81 | /// - Returns: EasyAlbum 82 | public func crop(_ crop: Bool) -> EasyAlbum { 83 | albumNVC.crop = crop 84 | return self 85 | } 86 | 87 | /// Show camera function 88 | /// 89 | /// - Parameter show: default = true 90 | /// - Returns: EasyAlbum 91 | public func showCamera(_ show: Bool) -> EasyAlbum { 92 | albumNVC.showCamera = show 93 | return self 94 | } 95 | 96 | /// UIDevice orientation (🆕 Create function after version 2.1.0) 97 | /// 98 | /// - Parameter orientation: default = .all,See more UIInterfaceOrientationMask 99 | /// - Returns: EasyAlbum 100 | public func orientation(_ orientation: UIInterfaceOrientationMask) -> EasyAlbum { 101 | albumNVC.orientation = orientation 102 | return self 103 | } 104 | 105 | /// Show message when selected count over limit 106 | /// 107 | /// - Parameter message: default = "" 108 | /// - Returns: EasyAlbum 109 | public func message(_ message: String) -> EasyAlbum { 110 | albumNVC.message = message 111 | return self 112 | } 113 | 114 | /// After selected photo scale 115 | /// ``` 116 | /// auto : scale to device's width and height. unit:px 117 | /// fit : manual setting width and height. unit:px 118 | /// scale : manual setting scale ratio. 119 | /// original : Use original size. 120 | /// ``` 121 | /// - Parameter factor: default = .auto 122 | /// - Returns: EasyAlbum 123 | public func sizeFactor(_ factor: EasyAlbumSizeFactor) -> EasyAlbum { 124 | albumNVC.sizeFactor = factor 125 | return self 126 | } 127 | 128 | /// Show photo picker 129 | /// 130 | /// - Parameters: 131 | /// - viewController: viewController 132 | /// - delegate: See more EasyAlbumDelegate 133 | public func start(_ viewController: UIViewController, delegate: EasyAlbumDelegate) { 134 | albumNVC.albumDelegate = delegate 135 | 136 | if #available(iOS 13.0, *) { 137 | albumNVC.modalPresentationStyle = .fullScreen 138 | } 139 | 140 | viewController.present(albumNVC, animated: true, completion: nil) 141 | } 142 | 143 | /// Show photo picker 144 | /// 145 | /// - Parameters: 146 | /// - viewController: navigationController 147 | /// - delegate: See more EasyAlbumDelegate 148 | public func start(_ navigationController: UINavigationController, delegate: EasyAlbumDelegate) { 149 | albumNVC.albumDelegate = delegate 150 | 151 | if #available(iOS 13.0, *) { 152 | albumNVC.modalPresentationStyle = .fullScreen 153 | } 154 | 155 | navigationController.present(albumNVC, animated: true, completion: nil) 156 | } 157 | 158 | /* 159 | ⚠️ Deprecated on verson 2.1.0 160 | 161 | /// Show gif photo 162 | /// 163 | /// - Parameter color: default = true 164 | /// - Returns: EasyAlbum 165 | public func showGIF(_ show: Bool) -> EasyAlbum { 166 | albumNVC.showGIF = show 167 | return self 168 | } 169 | 170 | /// Title color 171 | /// 172 | /// - Parameter color: default = #ffffff 173 | /// - Returns: EasyAlbum 174 | public func titleColor(_ color: UIColor) -> EasyAlbum { 175 | albumNVC.titleColor = color 176 | return self 177 | } 178 | */ 179 | } 180 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/EasyAlbumCore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EasyAlbumCore.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/4. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | struct EasyAlbumCore { 13 | 14 | /// value = com.compuserve.gif 15 | static let UTI_IMAGE_GIF: String = "com.compuserve.gif" 16 | 17 | /// value = public.jpeg 18 | static let UTI_IMAGE_JPEG: String = "public.jpeg" 19 | 20 | /// value = public.png 21 | static let UTI_IMAGE_PNG: String = "public.png" 22 | 23 | /// value = public.heic 24 | static let UTI_IMAGE_HEIC: String = "public.heic" 25 | 26 | /// value = jpeg 27 | static let IMAGE_JPEG: String = "jpeg" 28 | 29 | /// value = png 30 | static let IMAGE_PNG: String = "png" 31 | 32 | /// value = heic 33 | static let IMAGE_HEIC: String = "heic" 34 | 35 | /// value = unknow 36 | static let MEDIAT_UNKNOW: String = "unknow" 37 | 38 | /// value = image 39 | static let MEDIAT_IMAGE: String = "image" 40 | 41 | /// value = video 42 | static let MEDIAT_VIDEO: String = "video" 43 | 44 | /// value = audio 45 | static let MEDIAT_AUDIO: String = "audio" 46 | 47 | static let EASYALBUM_BUNDLE_ID: String = "com.brave2risks.EasyAlbum" 48 | 49 | /// App Name,value = EasyAlbum 50 | static let APP_NAME: String = "EasyAlbum" 51 | 52 | /// Navigation tint color,value = #ffffff 53 | static let TINT_COLOR: UIColor = .white 54 | 55 | /// NavigationBar tint color,value = #673ab7 56 | static let BAR_TINT_COLOR: UIColor = UIColor(hex: "673ab7") 57 | 58 | /// Application statusBar style,value = true 59 | static let LIGHT_STATUS_BAR_STYLE: Bool = true 60 | 61 | /// Selected photo max count,value = 30 62 | static let LIMIT: Int = 30 63 | 64 | /// Gallery span count,value = 3 65 | static let SPAN: Int = 3 66 | 67 | /// Photo selected color,value = #ffc107 68 | static let PICK_COLOR: UIColor = UIColor(hex: "ffc107") 69 | 70 | /// When use camera want to crop after take picture,value = true 71 | static let CROP: Bool = false 72 | 73 | /// Want to show camera button on navigationBar,value = true 74 | static let SHOW_CAMERA: Bool = true 75 | 76 | /// Device support orientation,value = .all 77 | static let ORIENTATION: UIInterfaceOrientationMask = .all 78 | 79 | /// Toast message,value = "" 80 | static let MESSAGE: String = "" 81 | 82 | /// After selected photo scale,value = .auto 83 | static let SIZE_FACTOR: EasyAlbumSizeFactor = .auto 84 | } 85 | 86 | // MARK: - EasyAlbumPermission 87 | enum EasyAlbumPermission: CustomStringConvertible { 88 | 89 | case camera 90 | 91 | case photo 92 | 93 | var description: String { 94 | switch self { 95 | case .camera: return LString(.camera) 96 | case .photo: return LString(.photo) 97 | } 98 | } 99 | } 100 | 101 | // MARK: - EasyAlbumText 102 | enum EasyAlbumText { 103 | 104 | case camera 105 | 106 | case photo 107 | 108 | case setting 109 | 110 | case overLimit(count: Int) 111 | 112 | case noCamera 113 | 114 | case permissionTitle(witch: String) 115 | 116 | case permissionMsg(appName: String, witch: String) 117 | 118 | case photoProcess 119 | } 120 | 121 | // MARK: - EasyAlbumSizeFactor 122 | /// Photo scale ratio 123 | /// 124 | /// - auto : Scale to device's width and height. unit:px 125 | /// - fit : Manual setting width and height. unit:px 126 | /// - scale : Manual setting scale ratio. 127 | /// - original : Use original size. 128 | public enum EasyAlbumSizeFactor { 129 | 130 | /// Scale to device's width and height. unit:px 131 | case auto 132 | 133 | /// Manual setting width and height. unit:px 134 | case fit(width: CGFloat, height: CGFloat) 135 | 136 | /// Manual setting scale ratio. 137 | case scale(width: CGFloat, height: CGFloat) 138 | 139 | /// Use original size. 140 | case original 141 | } 142 | 143 | /// Is from `EasyAlbumViewController` take photo,default = false 144 | var isFromEasyAlbumCamera: Bool = false 145 | 146 | /// Language Traditional,value = zh-Hant 147 | private let LANG_ZH_HANT: String = "zh-Hant" 148 | 149 | /// Language Simplified,value = zh-Hans 150 | private let LANG_ZH_HANS: String = "zh-Hans" 151 | 152 | /// Language English,value = en 153 | private let LANG_EN: String = "en" 154 | 155 | /// Region,value = TW 156 | private let REGION_TW: String = "TW" 157 | 158 | /// Region,value = CN 159 | private let REGION_CN: String = "CN" 160 | 161 | /// Region,value = US 162 | private let REGION_US: String = "US" 163 | 164 | /// 對應區域設定語系文字 165 | /// ``` 166 | /// Region 👉🏻 US:美國、TW:台灣、CN:中國大陸 167 | /// Language 👉🏻 en:美國、zh:台灣、zh:中國大陸 168 | /// 169 | /// Identifier 👇🏻 170 | /// 地區是台灣 171 | /// 繁體:zh_TW 172 | /// 簡體:zh-Hans_TW 173 | /// 美國:en_TW 174 | /// 175 | /// 地區是中國大陸 176 | /// 繁體:zh-Hant_CN 177 | /// 簡體:zh_CN 178 | /// 美國:en_CN 179 | /// 180 | /// 地區是美國 181 | /// 繁體:zh-Hant_US 182 | /// 簡體:zh-Hans_US 183 | /// 美國:en_US 184 | /// ``` 185 | func LString(_ text: EasyAlbumText) -> String { 186 | var region = REGION_US 187 | if let value = Locale.current.regionCode { region = value } 188 | 189 | var lang: String = "" 190 | let id: String = Locale.current.identifier 191 | 192 | switch region { 193 | case REGION_TW: 194 | lang = id.hasPrefix("zh") ? LANG_ZH_HANT : id.hasPrefix(LANG_ZH_HANS) ? LANG_ZH_HANS : LANG_EN 195 | case REGION_CN: 196 | lang = id.hasPrefix(LANG_ZH_HANT) ? LANG_ZH_HANT : id.hasPrefix("zh") ? LANG_ZH_HANS : LANG_EN 197 | default: 198 | lang = id.hasPrefix(LANG_ZH_HANT) ? LANG_ZH_HANT : id.hasPrefix(LANG_ZH_HANS) ? LANG_ZH_HANS : LANG_EN 199 | } 200 | 201 | switch text { 202 | case .camera: 203 | return lang == LANG_ZH_HANT ? "相機" : lang == LANG_ZH_HANS ? "相机" : "Camera" 204 | case .photo: 205 | return lang == LANG_ZH_HANT ? "照片" : lang == LANG_ZH_HANS ? "照片" : "Photo" 206 | case .setting: 207 | return lang == LANG_ZH_HANT ? "設定" : lang == LANG_ZH_HANS ? "设定" : "Setting" 208 | case .overLimit(let count): 209 | return lang == LANG_ZH_HANT ? "相片挑選最多\(count)張" : 210 | lang == LANG_ZH_HANS ? "相片挑选最多\(count)张" : "Photo pick up the most \(count)" 211 | case .noCamera: 212 | return lang == LANG_ZH_HANT ? "該設備未持有相機鏡頭!" : 213 | lang == LANG_ZH_HANS ? "该设备未持有摄像镜头!" : "The device hasn't camera !" 214 | case .permissionTitle(let witch): 215 | return lang == LANG_ZH_HANT ? "此功能需要\(witch)存取權" : 216 | lang == LANG_ZH_HANS ? "此功能需要\(witch)存取权" : "This feature requires \(witch) access" 217 | case .permissionMsg(let appName, let witch): 218 | return lang == LANG_ZH_HANT ? "在iPhone 設定中,點按\(appName) 然後開啟「\(witch)」" : 219 | lang == LANG_ZH_HANS ? "在iPhone 设定中,点按\(appName) 然后开启「\(witch)」" : 220 | "In iPhone settings, tap \(appName) and turn on \(witch)" 221 | case .photoProcess: 222 | return lang == LANG_ZH_HANT ? "照片處理中..." : lang == LANG_ZH_HANS ? "照片处理中..." : "Photo processing..." 223 | } 224 | } 225 | 226 | // MARK: - EasyAlbumDelegate 227 | public protocol EasyAlbumDelegate: class { 228 | func easyAlbumDidSelected(_ photos: [AlbumData]) 229 | 230 | func easyAlbumDidCanceled() 231 | } 232 | 233 | // MARK: - EasyAlbumPageContentVCDelegate 234 | protocol EasyAlbumPageContentVCDelegate: class { 235 | func singleTap(_ viewController: EasyAlbumPageContentVC) 236 | 237 | func panDidChanged(_ viewController: EasyAlbumPageContentVC, in targetView: UIView, alpha: CGFloat) 238 | 239 | func panDidEnded(_ viewController: EasyAlbumPageContentVC, in targetView: UIView) 240 | } 241 | 242 | // MARK: - typealias 243 | typealias PhotoData = (asset: PHAsset, number: Int) 244 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Extension/CGSize+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExCGSize.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/4/20. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension CGSize { 12 | 13 | /// Fit size with another size 14 | /// 15 | /// - Parameter another: Another size 16 | /// - Returns: After calc size 17 | func fit(with another: CGSize) -> CGSize { 18 | let anotherW = another.width 19 | let anotherH = another.height 20 | let nowW = self.width 21 | let nowH = self.height 22 | 23 | var scale: CGFloat = 1.0 24 | if nowW > anotherW && nowH < anotherH { 25 | scale = anotherW / nowW 26 | } else if nowW < anotherW && nowH > anotherH { 27 | scale = anotherH / nowH 28 | } else { 29 | scale = min(anotherW / nowW, anotherH / nowH) 30 | } 31 | 32 | return CGSize(width: nowW * scale, height: nowH * scale) 33 | } 34 | 35 | /// Fit frame with another frame and center in another 36 | /// 37 | /// - Parameter another: Another frame 38 | /// - Returns: After calculation frame 39 | func fit(with another: CGRect) -> CGRect { 40 | let anotherW = another.width 41 | let anotherH = another.height 42 | let nowW = self.width 43 | let nowH = self.height 44 | 45 | var scale: CGFloat = 1.0 46 | if nowW > anotherW && nowH < anotherH { 47 | scale = anotherW / nowW 48 | } else if nowW < anotherW && nowH > anotherH { 49 | scale = anotherH / nowH 50 | } else { 51 | scale = min(anotherW / nowW, anotherH / nowH) 52 | } 53 | 54 | let w = nowW * scale 55 | let h = nowH * scale 56 | let x = w == anotherW ? 0.0 : (anotherW - w) / 2 57 | let y = h == anotherH ? 0.0 : (anotherH - h) / 2 58 | 59 | return CGRect(x: x, y: y, width: w, height: h) 60 | } 61 | 62 | /// Scale to ratio 63 | /// - Parameter value: Scale ratio 64 | func scale(to value: CGFloat) -> CGSize { 65 | return CGSize(width: width * value, height: height * value) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Extension/NotificationName+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationName+Extension.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2020/9/26. 6 | // Copyright © 2020 Ray. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Notification.Name { 12 | 13 | /// 相片編號更改通知 14 | static let EasyAlbumPhotoNumberDidChangeNotification: Notification.Name = Notification.Name("EasyAlbumPhotoNumberDidChangeNotification") 15 | 16 | /// 相片預覽頁面,消失通知 17 | static let EasyAlbumPreviewPageDismissNotification: Notification.Name = Notification.Name("EasyAlbumPreviewPageDismissNotification") 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Extension/String+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExString.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/10. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension String { 12 | 13 | /// Calculator font height 14 | /// 15 | /// - Parameters: 16 | /// - width: 限制寬度的大小 17 | /// - font: font style 18 | /// - Returns: 計算後的高度 19 | func height(with width: CGFloat, font: UIFont) -> CGFloat { 20 | let attrString = NSMutableAttributedString(string: self) 21 | attrString.addAttribute(.font, value: font, range: NSRange(location: 0, length: self.utf16.count)) 22 | let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude) 23 | let boundingBox = attrString.boundingRect(with: constraintRect, 24 | options: [.usesLineFragmentOrigin, .usesFontLeading], 25 | context: nil) 26 | return ceil(boundingBox.height) 27 | } 28 | 29 | /// Calculator font width 30 | /// 31 | /// - Parameters: 32 | /// - height: 限制高度的大小 33 | /// - font: font style 34 | /// - Returns: 計算後的寬度 35 | func width(with height: CGFloat, font: UIFont) -> CGFloat { 36 | let attrString = NSMutableAttributedString(string: self) 37 | attrString.addAttribute(.font, value: font, range: NSRange(location: 0, length: self.utf16.count)) 38 | let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height) 39 | let boundingBox = attrString.boundingRect(with: constraintRect, 40 | options: [.usesLineFragmentOrigin, .usesFontLeading], 41 | context: nil) 42 | return ceil(boundingBox.width) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Extension/UICollectionView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExTableCollectionView.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UICollectionView { 12 | 13 | func registerCell(_ t: T.Type, isNib: Bool) { 14 | let identifier = String(describing: t) 15 | if isNib { 16 | self.register(UINib(nibName: identifier, bundle: Bundle(for: t)), 17 | forCellWithReuseIdentifier: identifier) 18 | } else { 19 | self.register(t, forCellWithReuseIdentifier: identifier) 20 | } 21 | } 22 | 23 | func registerHeader(_ t: T.Type, isNib: Bool) { 24 | let identifier = String(describing: t) 25 | if isNib { 26 | self.register(UINib(nibName: identifier, bundle: Bundle(for: t)), 27 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, 28 | withReuseIdentifier: identifier) 29 | } else { 30 | self.register(t, 31 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, 32 | withReuseIdentifier: identifier) 33 | } 34 | } 35 | 36 | func registerFooter(_ t: T.Type, isNib: Bool) { 37 | let identifier = String(describing: t) 38 | if isNib { 39 | self.register(UINib(nibName: identifier, bundle: Bundle(for: t)), 40 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, 41 | withReuseIdentifier: identifier) 42 | } else { 43 | self.register(t, 44 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, 45 | withReuseIdentifier: identifier) 46 | } 47 | } 48 | 49 | func dequeueCell(_ t: T.Type, indexPath: IndexPath) -> T { 50 | let identifier = String(describing: t) 51 | guard let cell = self.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as? T 52 | else { fatalError("Can not found \(t.description()) type!") } 53 | 54 | return cell 55 | } 56 | 57 | func dequeueHeader(_ t: T.Type, indexPath: IndexPath) -> T { 58 | let identifier = String(describing: t) 59 | guard let header = self.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: identifier, for: indexPath) as? T 60 | else { fatalError("Can not found \(t.description()) type!") } 61 | 62 | return header 63 | } 64 | 65 | func dequeueFooter(_ t: T.Type, indexPath: IndexPath) -> T { 66 | let identifier = String(describing: t) 67 | guard let footer = self.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: identifier, for: indexPath) as? T 68 | else { fatalError("Can not found \(t.description()) type!") } 69 | 70 | return footer 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Extension/UIColor+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExUIColor.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | /// let color = UIColor(hex: "ff0000") 14 | /// - Parameter hex: 色碼 15 | convenience init(hex: String) { 16 | self.init(hex: hex, alpha: 1.0) 17 | } 18 | 19 | /// let color = UIColor(hex: "ff0000", alpha: 1.0) 20 | /// - Parameter 21 | /// hex: 色碼 22 | /// alpha: 透明度 23 | convenience init(hex: String, alpha: CGFloat) { 24 | let scanner = Scanner(string: hex) 25 | scanner.scanLocation = 0 26 | 27 | var rgbValue: UInt64 = 0 28 | 29 | scanner.scanHexInt64(&rgbValue) 30 | 31 | let r = (rgbValue & 0xff0000) >> 16 32 | let g = (rgbValue & 0xff00) >> 8 33 | let b = rgbValue & 0xff 34 | 35 | self.init( 36 | red: CGFloat(r) / 0xff, 37 | green: CGFloat(g) / 0xff, 38 | blue: CGFloat(b) / 0xff, alpha: alpha) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Extension/UIImage+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExUIImage.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import ImageIO 12 | 13 | extension UIImageView { 14 | 15 | public func loadGif(name: String) { 16 | DispatchQueue.global().async { 17 | let image = UIImage.gif(name: name) 18 | DispatchQueue.main.async { self.image = image } 19 | } 20 | } 21 | 22 | public func loadGif(data: Data) { 23 | DispatchQueue.global().async { 24 | let image = UIImage.gif(data: data) 25 | DispatchQueue.main.async { self.image = image } 26 | } 27 | } 28 | 29 | @available(iOS 9.0, *) 30 | public func loadGif(asset: String) { 31 | DispatchQueue.global().async { 32 | let image = UIImage.gif(asset: asset) 33 | DispatchQueue.main.async { self.image = image } 34 | } 35 | } 36 | } 37 | 38 | extension UIImage { 39 | 40 | public enum Name: String { 41 | 42 | case close = "album_close" 43 | 44 | case camera = "album_camera" 45 | 46 | case done = "album_done" 47 | } 48 | 49 | public class func bundle(image name: Name) -> UIImage? { 50 | // Use from SPM 51 | #if SWIFT_PACKAGE 52 | if let image = UIImage(named: name.rawValue, in: .module, compatibleWith: nil) { 53 | return image 54 | } 55 | #endif 56 | 57 | // Use from Cocoapods 58 | let frameworkBundle = Bundle(for: EasyAlbumVC.self) 59 | let bundleURL = frameworkBundle.resourceURL?.appendingPathComponent("EasyAlbum.bundle") 60 | 61 | guard let url = bundleURL else { return nil } 62 | 63 | let resourceBundle = Bundle(url: url) 64 | 65 | guard let image = UIImage(named: name.rawValue, in: resourceBundle, compatibleWith: nil) 66 | else { return nil } 67 | 68 | return image 69 | } 70 | 71 | public class func gif(data: Data) -> UIImage? { 72 | // Create source from data 73 | guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { 74 | //print("SwiftGif: Source for the image does not exist") 75 | return nil 76 | } 77 | 78 | return UIImage.animatedImageWithSource(source) 79 | } 80 | 81 | public class func gif(url: String) -> UIImage? { 82 | // Validate URL 83 | guard let bundleURL = URL(string: url) else { 84 | //print("SwiftGif: This image named \"\(url)\" does not exist") 85 | return nil 86 | } 87 | 88 | // Validate data 89 | guard let imageData = try? Data(contentsOf: bundleURL) else { 90 | //print("SwiftGif: Cannot turn image named \"\(url)\" into NSData") 91 | return nil 92 | } 93 | 94 | return gif(data: imageData) 95 | } 96 | 97 | public class func gif(name: String) -> UIImage? { 98 | // Check for existance of gif 99 | guard let bundleURL = Bundle.main.url(forResource: name, withExtension: "gif") else { 100 | return nil 101 | } 102 | 103 | // Validate data 104 | guard let imageData = try? Data(contentsOf: bundleURL) else { 105 | return nil 106 | } 107 | 108 | return gif(data: imageData) 109 | } 110 | 111 | @available(iOS 9.0, *) 112 | public class func gif(asset: String) -> UIImage? { 113 | // Create source from assets catalog 114 | guard let dataAsset = NSDataAsset(name: asset) else { return nil } 115 | 116 | return gif(data: dataAsset.data) 117 | } 118 | 119 | internal class func delayForImageAtIndex(_ index: Int, source: CGImageSource!) -> Double { 120 | var delay = 0.1 121 | 122 | // Get dictionaries 123 | let cfProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) 124 | let gifPropertiesPointer = UnsafeMutablePointer.allocate(capacity: 0) 125 | defer { 126 | gifPropertiesPointer.deallocate() 127 | } 128 | 129 | if CFDictionaryGetValueIfPresent(cfProperties, Unmanaged.passUnretained(kCGImagePropertyGIFDictionary).toOpaque(), gifPropertiesPointer) == false { 130 | return delay 131 | } 132 | 133 | let gifProperties:CFDictionary = unsafeBitCast(gifPropertiesPointer.pointee, to: CFDictionary.self) 134 | 135 | // Get delay time 136 | var delayObject: AnyObject = unsafeBitCast( 137 | CFDictionaryGetValue(gifProperties, 138 | Unmanaged.passUnretained(kCGImagePropertyGIFUnclampedDelayTime).toOpaque()), 139 | to: AnyObject.self) 140 | if delayObject.doubleValue == 0 { 141 | delayObject = unsafeBitCast(CFDictionaryGetValue(gifProperties, 142 | Unmanaged.passUnretained(kCGImagePropertyGIFDelayTime).toOpaque()), 143 | to: AnyObject.self) 144 | } 145 | 146 | if let delayObject = delayObject as? Double, delayObject > 0 { 147 | delay = delayObject 148 | } else { 149 | delay = 0.1 // Make sure they're not too fast 150 | } 151 | 152 | return delay 153 | } 154 | 155 | internal class func gcdForPair(_ a: Int?, _ b: Int?) -> Int { 156 | var a = a 157 | var b = b 158 | // Check if one of them is nil 159 | if b == nil || a == nil { 160 | if b != nil { 161 | return b! 162 | } else if a != nil { 163 | return a! 164 | } else { 165 | return 0 166 | } 167 | } 168 | 169 | // Swap for modulo 170 | if a! < b! { 171 | let c = a 172 | a = b 173 | b = c 174 | } 175 | 176 | // Get greatest common divisor 177 | var rest: Int 178 | while true { 179 | rest = a! % b! 180 | 181 | if rest == 0 { 182 | return b! // Found it 183 | } else { 184 | a = b 185 | b = rest 186 | } 187 | } 188 | } 189 | 190 | internal class func gcdForArray(_ array: Array) -> Int { 191 | if array.isEmpty { 192 | return 1 193 | } 194 | 195 | var gcd = array[0] 196 | 197 | for val in array { 198 | gcd = UIImage.gcdForPair(val, gcd) 199 | } 200 | 201 | return gcd 202 | } 203 | 204 | internal class func animatedImageWithSource(_ source: CGImageSource) -> UIImage? { 205 | let count = CGImageSourceGetCount(source) 206 | var images = [CGImage]() 207 | var delays = [Int]() 208 | 209 | // Fill arrays 210 | for i in 0..(_ t: T.Type, isNib: Bool = true) { 16 | let identifier = String(describing: t) 17 | if isNib { 18 | self.register(UINib(nibName: identifier, bundle: Bundle(for: t)), 19 | forCellReuseIdentifier: identifier) 20 | } else { 21 | self.register(t, forCellReuseIdentifier: identifier) 22 | } 23 | } 24 | 25 | func dequeueCell(_ t: T.Type, indexPath: IndexPath) -> T { 26 | let identifier = String(describing: t) 27 | guard let cell = self.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as? T 28 | else { fatalError("Can not found \(t.description()) type!") } 29 | 30 | return cell 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 2.3.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Manager/PhotoManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoManager.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/4. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | struct PhotoManager { 13 | 14 | /// 使用者允許讀取相簿 callback 15 | typealias DidAuthorized = () -> Swift.Void 16 | 17 | /// 使用者允許讀取相簿但僅顯示部分照片 callback,firstRequest:是否第一次詢問 18 | typealias DidLimited = (_ firstRequest: Bool) -> Swift.Void 19 | 20 | /// 使用者不允許讀取相簿 callback 21 | typealias DidDenied = () -> Swift.Void 22 | 23 | /// PHImageRequestOptions Setting 24 | /// 25 | /// - fast: Photos efficiently resizes the image to a size similar to, or slightly larger than, the target size. 26 | /// - exact: Photos resizes the image to match the target size exactly. 27 | enum Options { 28 | case fast 29 | 30 | case exact(isSync: Bool) 31 | 32 | var parameters: (resize: PHImageRequestOptionsResizeMode, delivery: PHImageRequestOptionsDeliveryMode, sync: Bool) { 33 | switch self { 34 | case .fast: 35 | let resize = PHImageRequestOptionsResizeMode.fast 36 | let delivery = PHImageRequestOptionsDeliveryMode.fastFormat 37 | return (resize, delivery, false) 38 | case .exact(let isSync): 39 | let resize = PHImageRequestOptionsResizeMode.exact 40 | let delivery = PHImageRequestOptionsDeliveryMode.highQualityFormat 41 | return (resize, delivery, isSync) 42 | } 43 | } 44 | } 45 | 46 | static let share = PhotoManager() 47 | 48 | /// Photo manager object 49 | private(set) var imageManager: PHCachingImageManager? 50 | private(set) var requestOptions: PHImageRequestOptions! 51 | 52 | /// Thumbnail photo size 53 | private(set) var photoThumbnailSize: CGSize = .zero 54 | 55 | /// Save animated album of id's 56 | private(set) var animatedIDs: Set = Set() 57 | 58 | private init() { 59 | let density = UIScreen.density 60 | photoThumbnailSize = CGSize(width: 100 * density, height: 100 * density) 61 | 62 | // https://developer.apple.com/documentation/photos/phcachingimagemanager 63 | imageManager = PHCachingImageManager() 64 | imageManager?.allowsCachingHighQualityImages = false 65 | 66 | requestOptions = PHImageRequestOptions() 67 | requestOptions.isNetworkAccessAllowed = false 68 | } 69 | 70 | /// 請求相簿權限 71 | /// - Parameters: 72 | /// - didAuthorized: 使用者允許讀取相簿 callback 73 | /// - didLimited: 使用者允許讀取相簿但僅顯示部分照片 callback 74 | /// - didDenied: 使用者不允許讀取相簿 callback 75 | public func requestPermission(didAuthorized: DidAuthorized?, 76 | didLimited: DidLimited?, 77 | didDenied: DidDenied?) { 78 | if #available(iOS 14.0, *) { 79 | switch PHPhotoLibrary.authorizationStatus() { 80 | case .notDetermined: 81 | PHPhotoLibrary.requestAuthorization(for: .readWrite) { (status) in 82 | DispatchQueue.main.async { 83 | switch status { 84 | case .authorized: 85 | didAuthorized?() 86 | case .limited: 87 | didLimited?(true) 88 | case .denied, .restricted: 89 | didDenied?() 90 | case .notDetermined: 91 | // do nothing... 92 | break 93 | @unknown default: 94 | break 95 | } 96 | } 97 | } 98 | case .authorized: 99 | didAuthorized?() 100 | case .limited: 101 | didLimited?(false) 102 | case .denied, .restricted: 103 | didDenied?() 104 | default: 105 | break 106 | } 107 | } else { 108 | switch PHPhotoLibrary.authorizationStatus() { 109 | case .notDetermined: 110 | PHPhotoLibrary.requestAuthorization { (status) in 111 | DispatchQueue.main.async { 112 | switch status { 113 | case .authorized: 114 | didAuthorized?() 115 | case .denied, .restricted: 116 | didDenied?() 117 | default: 118 | break 119 | } 120 | } 121 | } 122 | case .authorized: 123 | didAuthorized?() 124 | case .denied, .restricted: 125 | didDenied?() 126 | default: 127 | break 128 | } 129 | } 130 | } 131 | 132 | /// Fetch all photos 133 | /// 134 | /// - Parameters: 135 | /// - datas: input datas 136 | /// - pickColor: pick color 137 | public mutating func fetchPhotos(in folders: inout [AlbumFolder], pickColor: UIColor) { 138 | // PHAssetCollectionType 139 | // https://developer.apple.com/documentation/photos/phassetcollectiontype 140 | // PHAssetCollectionSubtype 141 | // https://developer.apple.com/documentation/photos/phassetcollectionsubtype 142 | // http://www.jianshu.com/p/8cf7593cc44d 143 | // PHFetchOptions 144 | // https://developer.apple.com/documentation/photos/phfetchoptions 145 | 146 | let fetchOptions = PHFetchOptions() 147 | fetchOptions.includeAssetSourceTypes = .typeUserLibrary 148 | 149 | // Smart album 150 | let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, 151 | subtype: .albumRegular, 152 | options: fetchOptions) 153 | 154 | // DropBox、Instagram ... else 155 | let albums = PHAssetCollection.fetchAssetCollections(with: .album, 156 | subtype: .albumRegular, 157 | options: fetchOptions) 158 | 159 | // 取出所有相片 160 | //let allPhotos = PHAsset.fetchAssets(with: fetchOptions) 161 | // 取出所有使用者建立的相簿列表(保留) 162 | //let userCollections = PHCollectionList.fetchTopLevelUserCollections(with: nil) as! PHFetchResult 163 | 164 | let options = PHFetchOptions() 165 | options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue) 166 | options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] 167 | 168 | var temps: [(collection: PHAssetCollection, assets: PHFetchResult)] = [] 169 | 170 | for i in 0 ..< albums.count { 171 | let c = smartAlbums[i] 172 | let assets = PHAsset.fetchAssets(in: c , options: options) 173 | 174 | // if album count = 0, not show 175 | guard assets.count > 0 else { continue } 176 | 177 | temps.append((c, assets)) 178 | } 179 | 180 | for i in 0 ..< smartAlbums.count { 181 | let c = smartAlbums[i] 182 | let assets = PHAsset.fetchAssets(in: c , options: options) 183 | 184 | // if album is delete album, not show 185 | guard isDeleted(with: c.localizedTitle) == false else { continue } 186 | 187 | // if album count = 0, not show 188 | guard assets.count > 0 else { continue } 189 | 190 | // if album is animated, then save asset localIdentifier 191 | if isAnimated(with: c.localizedTitle) { 192 | for j in 0 ..< assets.count { 193 | animatedIDs.insert(assets[j].localIdentifier) 194 | } 195 | } 196 | 197 | temps.append((c, assets)) 198 | } 199 | 200 | // sort by the count from greatest to least 201 | temps.sort { $0.assets.count > $1.assets.count } 202 | 203 | temps.forEach { 204 | folders.append(AlbumFolder(title: $0.collection.localizedTitle, 205 | assets: $0.assets)) 206 | } 207 | } 208 | 209 | /// Fetch thumbnail photo 210 | public func fetchThumbnail(form asset: PHAsset, 211 | size: CGSize? = nil, 212 | options: Options, 213 | completion: @escaping (_ image: UIImage) -> Swift.Void) { 214 | requestOptions.resizeMode = options.parameters.resize 215 | requestOptions.deliveryMode = options.parameters.delivery 216 | requestOptions.isSynchronous = options.parameters.sync 217 | 218 | var thumbnailSize = photoThumbnailSize 219 | 220 | if let t = size { thumbnailSize = t } 221 | 222 | let _ = imageManager?.requestImage(for: asset, 223 | targetSize: thumbnailSize, 224 | contentMode: .aspectFill, 225 | options: requestOptions, 226 | resultHandler: 227 | { (result, info) -> Void in 228 | var thumbnail = UIImage() 229 | if let image = result { thumbnail = image } 230 | completion(thumbnail) 231 | }) 232 | } 233 | 234 | /// Fetch photo 235 | public func fetchImage(form asset: PHAsset, 236 | size: CGSize, 237 | options: Options, 238 | completion: @escaping (_ image: UIImage) -> Swift.Void) { 239 | requestOptions.resizeMode = options.parameters.resize 240 | requestOptions.deliveryMode = options.parameters.delivery 241 | requestOptions.isSynchronous = options.parameters.sync 242 | 243 | let _ = PHImageManager.default().requestImage(for: asset, 244 | targetSize: size, 245 | contentMode: .aspectFit, 246 | options: requestOptions) 247 | { (result, info) in 248 | var thumbnail = UIImage() 249 | if let image = result { thumbnail = image } 250 | completion(thumbnail) 251 | } 252 | } 253 | 254 | /// Fetch image data 255 | public func fetchImageData(from asset: PHAsset, 256 | options: Options, 257 | completion: @escaping (_ data: Data?, _ utiKey: String?) -> Swift.Void) { 258 | requestOptions.resizeMode = options.parameters.resize 259 | requestOptions.deliveryMode = options.parameters.delivery 260 | requestOptions.isSynchronous = options.parameters.sync 261 | 262 | let imageManager = PHImageManager.default() 263 | if #available(iOS 13, *) { 264 | let _ = imageManager.requestImageDataAndOrientation(for: asset, 265 | options: requestOptions) 266 | { (data, utiKey, orientation, info) in 267 | completion(data, utiKey) 268 | } 269 | } else { 270 | let _ = imageManager.requestImageData(for: asset, options: requestOptions) 271 | { (data, utiKey, orientation, info) in 272 | completion(data, utiKey) 273 | } 274 | } 275 | } 276 | 277 | public func startCacheImage(prefetchItemsAt assets: [PHAsset], options: Options) { 278 | // https://viblo.asia/p/create-a-simple-image-picker-just-like-the-camera-roll-6J3Zgk8AZmB 279 | requestOptions.resizeMode = options.parameters.resize 280 | requestOptions.deliveryMode = options.parameters.delivery 281 | requestOptions.isSynchronous = options.parameters.sync 282 | 283 | imageManager?.startCachingImages(for: assets, 284 | targetSize: photoThumbnailSize, 285 | contentMode: .aspectFill, 286 | options: requestOptions) 287 | } 288 | 289 | public func stopCacheImage(cancelPrefetchingForItemsAt assets: [PHAsset], options: Options) { 290 | requestOptions.resizeMode = options.parameters.resize 291 | requestOptions.deliveryMode = options.parameters.delivery 292 | requestOptions.isSynchronous = options.parameters.sync 293 | 294 | imageManager?.stopCachingImages(for: assets, 295 | targetSize: photoThumbnailSize, 296 | contentMode: .aspectFill, 297 | options: requestOptions) 298 | } 299 | 300 | public func stopAllCachingImages() { 301 | imageManager?.stopCachingImagesForAllAssets() 302 | } 303 | 304 | public func fetchImageName(from asset: PHAsset) -> String? { 305 | return PHAssetResource.assetResources(for: asset).first?.originalFilename 306 | } 307 | 308 | public func fetchImageUTI(from asset: PHAsset) -> String? { 309 | return PHAssetResource.assetResources(for: asset).first?.uniformTypeIdentifier 310 | } 311 | 312 | public func fetchImageURL(from asset: PHAsset, 313 | completion: @escaping (_ url : URL?) -> Swift.Void) { 314 | let options = PHContentEditingInputRequestOptions() 315 | options.isNetworkAccessAllowed = false 316 | asset.requestContentEditingInput(with: options) { (input, info) in 317 | completion(input?.fullSizeImageURL) 318 | } 319 | } 320 | 321 | /// PHAsset convert AlbumData task 322 | public func cenvertTask(from assets: [PHAsset], 323 | factor: EasyAlbumSizeFactor, 324 | completion: @escaping (_ datas: [AlbumData]) -> Swift.Void) { 325 | var datas: [AlbumData] = [] 326 | let grp = DispatchGroup() 327 | let queue = DispatchQueue(label: EasyAlbumCore.EASYALBUM_BUNDLE_ID) 328 | 329 | for asset in assets { 330 | grp.enter() 331 | queue.async { 332 | let width = CGFloat(asset.pixelWidth) 333 | let height = CGFloat(asset.pixelHeight) 334 | let size = self.calcScaleFactor(from: CGSize(width: width, height: height), factor: factor) 335 | let mediaType = asset.mediaType.rawValue 336 | let createDate = asset.creationDate 337 | let modificationDate = asset.modificationDate 338 | let isFavorite = asset.isFavorite 339 | let isHidden = asset.isHidden 340 | let location = asset.location 341 | let fileName = self.fetchImageName(from: asset) 342 | var fileData: Data? = nil 343 | var fileSize = 0 344 | var fileUTI = "" 345 | 346 | self.fetchImageData(from: asset, 347 | options: .fast, 348 | completion: 349 | { (data, uti) in 350 | if let data = data { 351 | fileData = data 352 | fileSize = data.count 353 | } 354 | 355 | if let uti = uti { 356 | fileUTI = uti 357 | } 358 | 359 | self.fetchImage(form: asset, 360 | size: size, 361 | options: .exact(isSync: false), 362 | completion: 363 | { (image) in 364 | datas.append(AlbumData(image, 365 | mediaType: mediaType, 366 | width: width, 367 | height: height, 368 | creationDate: createDate, 369 | modificationDate: modificationDate, 370 | isFavorite: isFavorite, 371 | isHidden: isHidden, 372 | location: location, 373 | fileName: fileName, 374 | fileData: fileData, 375 | fileSize: fileSize, 376 | fileUTI: fileUTI)) 377 | grp.leave() 378 | }) 379 | }) 380 | } 381 | } 382 | 383 | grp.notify(queue: .main) { completion(datas) } 384 | } 385 | 386 | /// Calculator photo scale factor 387 | public func calcScaleFactor(from size: CGSize, factor: EasyAlbumSizeFactor = .auto) -> CGSize { 388 | let oriW = size.width 389 | let oriH = size.height 390 | 391 | switch factor { 392 | case .auto: 393 | let w = UIScreen.width * UIScreen.density 394 | let h = UIScreen.height * UIScreen.density 395 | 396 | let screenW = UIScreen.isPortrait ? w : h 397 | let screenH = UIScreen.isPortrait ? h : w 398 | 399 | var factor: CGFloat = 1.0 400 | if oriW > screenW || oriH > screenH { 401 | factor = min(screenW / oriW, screenH / oriH) 402 | } 403 | 404 | return CGSize(width: oriW * factor, height: oriH * factor) 405 | case .fit(let reqW, let reqH): 406 | var factor: CGFloat = 1.0 407 | if oriW > reqW || oriH > reqH { 408 | factor = min(reqW / oriW, reqH / oriH) 409 | } 410 | 411 | return CGSize(width: oriW * factor, height: oriH * factor) 412 | case .scale(let scaleW, let scaleH): 413 | return CGSize(width: oriW * scaleW, height: oriH * scaleH) 414 | case .original: 415 | return size 416 | } 417 | } 418 | 419 | /// 檢查該相片是否為動圖 420 | /// - Parameter asset: see more PHAsset 421 | /// - Returns: If true means is animated, otherwise false. 422 | public func isAnimatedImage(from asset: PHAsset) -> Bool { 423 | if #available(iOS 11.0, *) { 424 | return asset.playbackStyle == .imageAnimated 425 | } else { 426 | return animatedIDs.contains(asset.localIdentifier) 427 | } 428 | } 429 | 430 | /// Check album is `Animated` 431 | private func isAnimated(with title: String?) -> Bool { 432 | guard let title = title else { return false } 433 | 434 | switch title { 435 | case "動圖", "动图", "Animated", "アニメーション", "움직이는 항목": return true 436 | default: return false 437 | } 438 | } 439 | 440 | /// Check album is `Recently Deleted` 441 | private func isDeleted(with title: String?) -> Bool { 442 | guard let title = title else { return false } 443 | 444 | switch title { 445 | case "最近刪除", "最近删除", "Recently Deleted", "最近削除した項目", "최근 삭제된 항목": return true 446 | default: return false 447 | } 448 | } 449 | 450 | #if DEBUG 451 | private func printLog(with asset: PHAsset, title: String, isGif: Bool) { 452 | print("title 👉🏻 \(title)") 453 | print("isGif 👉🏻 \(isGif)") 454 | print("burstIdentifier 👉🏻 \(String(describing: asset.burstIdentifier))") 455 | print("burstSelectionTypes 👉🏻 \(String(describing: asset.burstSelectionTypes))") 456 | print("creationDate 👉🏻 \(String(describing: asset.creationDate))") 457 | print("modificationDate 👉🏻 \(String(describing: asset.modificationDate))") 458 | print("duration 👉🏻 \(String(describing: asset.duration))") 459 | print("isFavorite 👉🏻 \(String(describing: asset.isFavorite))") 460 | print("isHidden 👉🏻 \(String(describing: asset.isHidden))") 461 | print("location 👉🏻 \(String(describing: asset.location))") 462 | print("mediaType 👉🏻 \(String(describing: asset.mediaType.rawValue))") 463 | print("mediaSubtypes 👉🏻 \(String(describing: asset.mediaSubtypes.rawValue))") 464 | print("pixelWidth 👉🏻 \(String(describing: asset.pixelWidth))") 465 | print("pixelHeight 👉🏻 \(String(describing: asset.pixelHeight))") 466 | print("representsBurst 👉🏻 \(String(describing: asset.representsBurst))") 467 | print("sourceType 👉🏻 \(String(describing: asset.sourceType.rawValue))") 468 | print("------------------------------------------") 469 | } 470 | #endif 471 | } 472 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Model/AlbumData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumData.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreLocation 11 | 12 | public struct AlbumData { 13 | 14 | public var image: UIImage! 15 | public var mediaType: String = "" 16 | public var width: CGFloat = 0.0 17 | public var height: CGFloat = 0.0 18 | public var creationDate: Date? 19 | public var modificationDate: Date? 20 | public var isFavorite: Bool = false 21 | public var isHidden: Bool = false 22 | public var location: CLLocation? 23 | public var fileName: String? 24 | public var fileData: Data? 25 | public var fileSize: Int = 0 26 | public var fileUTI: String? 27 | 28 | init() {} 29 | 30 | init(_ image: UIImage, 31 | mediaType: Int, 32 | width: CGFloat, 33 | height: CGFloat, 34 | creationDate: Date?, 35 | modificationDate: Date?, 36 | isFavorite: Bool, 37 | isHidden: Bool, 38 | location: CLLocation?, 39 | fileName: String?, 40 | fileData: Data?, 41 | fileSize: Int, 42 | fileUTI: String?) 43 | { 44 | self.image = image 45 | self.mediaType = mediaType == 0 ? EasyAlbumCore.MEDIAT_UNKNOW : 46 | mediaType == 1 ? EasyAlbumCore.MEDIAT_IMAGE : 47 | mediaType == 2 ? EasyAlbumCore.MEDIAT_VIDEO : EasyAlbumCore.MEDIAT_AUDIO 48 | self.width = width 49 | self.height = height 50 | self.creationDate = creationDate 51 | self.modificationDate = modificationDate 52 | self.isFavorite = isFavorite 53 | self.isHidden = isHidden 54 | self.location = location 55 | self.fileName = fileName 56 | self.fileData = fileData 57 | self.fileSize = fileSize 58 | self.fileUTI = fileUTI 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Model/AlbumFolder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumFolder.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import Photos 10 | 11 | struct AlbumFolder { 12 | 13 | private(set) var title: String 14 | 15 | var assets: PHFetchResult 16 | var isCheck: Bool = false 17 | 18 | init(title: String?, assets: PHFetchResult) { 19 | self.title = title ?? "" 20 | self.assets = assets 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Model/AlbumNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumNotification.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2020/9/28. 6 | // Copyright © 2020 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct AlbumNotification { 12 | 13 | /// Need reload items, Use for EasyAlbumPhotoNumberDidChangeNotification 14 | private(set) var reloadItems: [IndexPath] = [] 15 | 16 | /// Current selected items, Use for EasyAlbumPhotoNumberDidChangeNotification 17 | private(set) var selectedPhotos: [PhotoData] = [] 18 | 19 | /// Need send photo, Use for EasyAlbumPreviewPageDismissNotification 20 | private(set) var isSend: Bool = false 21 | 22 | init(reloadItems: [IndexPath], selectedPhotos: [PhotoData]) { 23 | self.reloadItems = reloadItems 24 | self.selectedPhotos = selectedPhotos 25 | } 26 | 27 | init(isSend: Bool) { 28 | self.isSend = isSend 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/ViewController/EasyAlbumCameraVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EasyAlbumCameraVC.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class EasyAlbumCameraVC: UIImagePickerController { 12 | 13 | var isEdit: Bool = false { 14 | didSet { 15 | allowsEditing = isEdit 16 | sourceType = .camera 17 | } 18 | } 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | setup() 23 | } 24 | 25 | private func setup() { 26 | if UIImagePickerController.isSourceTypeAvailable(.camera) == false { 27 | dismiss(animated: true, completion: nil) 28 | } 29 | 30 | delegate = self 31 | } 32 | 33 | private func didFinishTakePhoto(_ picker: UIImagePickerController, image: UIImage?) { 34 | // Use UIImageWriteToSavedPhotosAlbum, because after take photo no path so take photo to save album. 35 | guard let image = image else { return } 36 | 37 | UIImageWriteToSavedPhotosAlbum(image, 38 | self, 39 | #selector(handleSavePhoto(_:didFinishSavingWithError:contextInfo:)), 40 | nil) 41 | isFromEasyAlbumCamera = true 42 | picker.dismiss(animated: true, completion: nil) 43 | } 44 | 45 | @objc private func handleSavePhoto(_ image: UIImage, 46 | didFinishSavingWithError error: NSError?, 47 | contextInfo: UnsafeRawPointer) { 48 | // do nothing 49 | } 50 | } 51 | 52 | extension EasyAlbumCameraVC: UINavigationControllerDelegate, UIImagePickerControllerDelegate { 53 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { 54 | /* 55 | .editedImage(UIImage) = nil (需搭配allowsEditing = true) 56 | .cropRect(CGRect) = nil (需搭配allowsEditing = true) 57 | .originalImage(UIImage) = size {3024, 4032} orientation 3 scale 1.000000 58 | .referenceURL(NSURL) = nil (iOS 11.0 up use info[UIImagePickerController.InfoKey.phAsset]) 59 | .imageURL(NSURL) = nil (sourceType can't be .camera) 60 | .phAsset(PHAsset) = nil (sourceType can't be .camera) 61 | .livePhoto(PHLivePhoto) = nil (sourceType can't be .camera) 62 | .mediaMetadata(NSDictionary) = a lot of 63 | .mediaType = public.image 64 | .mediaURL = nil (sourceType can't be .camera) 65 | */ 66 | 67 | var image: UIImage? 68 | if let img = info[.editedImage] as? UIImage { 69 | image = img 70 | } else if let img = info[.originalImage] as? UIImage { 71 | image = img 72 | } 73 | 74 | didFinishTakePhoto(picker, image: image) 75 | } 76 | 77 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { 78 | picker.dismiss(animated: true, completion: nil) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/ViewController/EasyAlbumNAC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EasyAlbumNAC.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class EasyAlbumNAC: UINavigationController { 12 | 13 | var appName: String? 14 | var tintColor: UIColor? 15 | var barTintColor: UIColor? 16 | var limit: Int? 17 | var span: Int? 18 | var pickColor: UIColor? 19 | var crop: Bool? 20 | var showCamera: Bool? 21 | var message: String? 22 | var sizeFactor: EasyAlbumSizeFactor? 23 | var lightStatusBarStyle: Bool? 24 | var orientation: UIInterfaceOrientationMask? 25 | 26 | weak var albumDelegate: EasyAlbumDelegate? 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | setup() 31 | } 32 | 33 | override var preferredStatusBarStyle: UIStatusBarStyle { 34 | return lightStatusBarStyle ?? EasyAlbumCore.LIGHT_STATUS_BAR_STYLE ? .lightContent : .default 35 | } 36 | 37 | override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { 38 | return .fade 39 | } 40 | 41 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask { 42 | return orientation ?? EasyAlbumCore.ORIENTATION 43 | } 44 | 45 | deinit { 46 | #if targetEnvironment(simulator) 47 | print("EasyAlbumNAC deinit 👍🏻") 48 | #endif 49 | } 50 | 51 | private func setup() { 52 | if #available(iOS 13.0, *) { 53 | overrideUserInterfaceStyle = .light 54 | } 55 | 56 | navigationBar.tintColor = tintColor ?? EasyAlbumCore.TINT_COLOR 57 | navigationBar.barTintColor = barTintColor ?? EasyAlbumCore.BAR_TINT_COLOR 58 | navigationBar.isTranslucent = false 59 | 60 | let albumVC = EasyAlbumVC() 61 | 62 | if let value = appName { albumVC.appName = value } 63 | if let value = barTintColor { albumVC.barTintColor = value } 64 | if let value = limit { albumVC.limit = value } 65 | if let value = span { albumVC.span = value } 66 | if let value = tintColor { albumVC.titleColor = value } 67 | if let value = pickColor { albumVC.pickColor = value } 68 | if let value = crop { albumVC.crop = value } 69 | if let value = showCamera { albumVC.showCamera = value } 70 | if let value = message { albumVC.message = value } 71 | if let value = sizeFactor { albumVC.sizeFactor = value } 72 | if let value = orientation { albumVC.orientation = value } 73 | 74 | albumVC.albumDelegate = albumDelegate 75 | 76 | viewControllers = [albumVC] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/ViewController/EasyAlbumPageContentVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EasyAlbumPageContentVC.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/8/26. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | //import PhotosUI 12 | 13 | class EasyAlbumPageContentVC: UIViewController { 14 | 15 | private let scrollView = UIScrollView() 16 | private let imageView: UIImageView = UIImageView() 17 | 18 | //private var mPohtoLiveView: PHLivePhotoView? 19 | 20 | /// Device screen frame 21 | private var screenFrame: CGRect = .zero 22 | 23 | /// Initialize ImageView center 24 | private var oriImageCenter: CGPoint = .zero 25 | 26 | private var centerOfContentSize: CGPoint { 27 | let deltaWidth = screenFrame.width - scrollView.contentSize.width 28 | let offsetX = deltaWidth > 0 ? deltaWidth / 2 : 0 29 | let deltaHeight = screenFrame.height - scrollView.contentSize.height 30 | let offsetY = deltaHeight > 0 ? deltaHeight / 2 : 0 31 | 32 | return CGPoint(x: scrollView.contentSize.width / 2 + offsetX, 33 | y: scrollView.contentSize.height / 2 + offsetY) 34 | } 35 | 36 | private var imageScaleForDoubleTap: CGFloat = 3.0 37 | private var imageScale: CGFloat = 4.0 { 38 | didSet { scrollView.maximumZoomScale = imageScale } 39 | } 40 | 41 | private var photoManager: PhotoManager = PhotoManager.share 42 | 43 | var asset: PHAsset? 44 | 45 | /// The cell frame,default = .zero 46 | var cellFrame: CGRect = .zero 47 | 48 | weak var delegate: EasyAlbumPageContentVCDelegate? 49 | 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | setup() 53 | } 54 | 55 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 56 | screenFrame = CGRect(origin: .zero, size: size) 57 | doLayout() 58 | } 59 | 60 | private func setup() { 61 | if #available(iOS 13.0, *) { 62 | overrideUserInterfaceStyle = .light 63 | } 64 | 65 | scrollView.showsHorizontalScrollIndicator = false 66 | scrollView.showsVerticalScrollIndicator = false 67 | scrollView.alwaysBounceHorizontal = true 68 | scrollView.alwaysBounceVertical = true 69 | scrollView.minimumZoomScale = 1.0 70 | scrollView.maximumZoomScale = imageScale 71 | scrollView.delegate = self 72 | view.addSubview(scrollView) 73 | 74 | if #available(iOS 11.0, *) { 75 | scrollView.contentInsetAdjustmentBehavior = .never 76 | } else { 77 | automaticallyAdjustsScrollViewInsets = false 78 | } 79 | 80 | scrollView.addSubview(imageView) 81 | 82 | let singleTap = UITapGestureRecognizer(target: self, action: #selector(onSingleTap(_:))) 83 | singleTap.numberOfTapsRequired = 1 84 | view.addGestureRecognizer(singleTap) 85 | 86 | let doubleTap = UITapGestureRecognizer(target: self, action: #selector(onDoubleTap(_:))) 87 | doubleTap.numberOfTapsRequired = 2 88 | view.addGestureRecognizer(doubleTap) 89 | 90 | let pan = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:))) 91 | pan.maximumNumberOfTouches = 1 92 | pan.delegate = self 93 | imageView.isUserInteractionEnabled = true 94 | imageView.addGestureRecognizer(pan) 95 | 96 | imageView.frame = cellFrame 97 | 98 | // Single tap & Double tap 99 | singleTap.require(toFail: doubleTap) 100 | 101 | screenFrame = CGRect(origin: .zero, size: CGSize(width: UIScreen.width, height: UIScreen.height)) 102 | 103 | guard let asset = asset else { return } 104 | 105 | let width = asset.pixelWidth 106 | let height = asset.pixelHeight 107 | let size = photoManager.calcScaleFactor(from: CGSize(width: width, height: height)) 108 | 109 | if photoManager.isAnimatedImage(from: asset) { 110 | photoManager.fetchImageData(from: asset, 111 | options: .exact(isSync: false)) 112 | { (data, _) in 113 | guard let data = data else { return } 114 | 115 | self.imageView.loadGif(data: data) 116 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(320)) { self.doLayout() } 117 | } 118 | } else { 119 | photoManager.fetchImage(form: asset, 120 | size: size, 121 | options: .exact(isSync: false)) 122 | { (image) in 123 | self.imageView.image = image 124 | self.doLayout() 125 | } 126 | } 127 | } 128 | 129 | private func doLayout() { 130 | // If has zoom, we need to scale to original 131 | if scrollView.zoomScale != 1.0 { 132 | scrollView.setZoomScale(1.0, animated: false) 133 | } 134 | 135 | // Setting scrollView frame, because when rotate the frame can't be changed 136 | scrollView.frame = screenFrame 137 | 138 | // Origin image size 139 | guard let imageSize = imageView.image?.size else { return } 140 | 141 | // Calculation screen frame 142 | let fitFrame = imageSize.fit(with: screenFrame) 143 | 144 | if cellFrame != .zero { 145 | UIView.animate(withDuration: 0.32, animations: { 146 | self.imageView.frame = fitFrame 147 | self.oriImageCenter = self.imageView.center 148 | }) { (finished) in 149 | self.scrollView.setZoomScale(1.0, animated: false) 150 | } 151 | } else { 152 | imageView.frame = fitFrame 153 | oriImageCenter = imageView.center 154 | scrollView.setZoomScale(1.0, animated: false) 155 | } 156 | } 157 | 158 | @objc private func onSingleTap(_ tap: UITapGestureRecognizer) { 159 | delegate?.singleTap(self) 160 | } 161 | 162 | @objc private func onDoubleTap(_ tap: UITapGestureRecognizer) { 163 | if scrollView.zoomScale == 1.0 { 164 | let pointInView = tap.location(in: imageView) 165 | let w = scrollView.frame.size.width / imageScaleForDoubleTap 166 | let h = scrollView.frame.size.height / imageScaleForDoubleTap 167 | let x = pointInView.x - (w / 2.0) 168 | let y = pointInView.y - (h / 2.0) 169 | scrollView.zoom(to: CGRect(x: x, y: y, width: w, height: h), animated: true) 170 | } else { 171 | scrollView.setZoomScale(1.0, animated: true) 172 | } 173 | } 174 | 175 | @objc private func onPan(_ pan: UIPanGestureRecognizer) { 176 | guard scrollView.zoomScale == 1.0 else { return } 177 | 178 | let height = UIScreen.height 179 | let halfHeight = height / 2 180 | var alpha = CGFloat(1.0) 181 | 182 | switch pan.state { 183 | case .began: break 184 | case .changed: 185 | let translation = pan.translation(in: imageView.superview) 186 | 187 | // if x > y, means scroll to left or right 188 | let tx = abs(translation.x) 189 | let ty = abs(translation.y) 190 | if tx > ty { return } 191 | 192 | let y = pan.view!.center.y + translation.y 193 | 194 | // Calculator alpha 195 | alpha = y < halfHeight ? y / halfHeight : (height - y) / halfHeight 196 | 197 | // add 0.15 because don't want fast to transparent 198 | alpha += 0.15 199 | delegate?.panDidChanged(self, in: imageView, alpha: alpha) 200 | 201 | imageView.center = CGPoint(x: imageView.center.x, y: y) 202 | pan.setTranslation(.zero, in: imageView.superview) 203 | case .ended, .cancelled: 204 | let translation = pan.translation(in: imageView.superview) 205 | 206 | // if x > y, means scroll to left or right 207 | let tx = abs(translation.x) 208 | let ty = abs(translation.y) 209 | 210 | if tx > ty { 211 | // if x > y but y has move a little then image center to origin 212 | if imageView.center != oriImageCenter { 213 | UIView.animate(withDuration: 0.2, animations: { 214 | self.imageView.center = self.oriImageCenter 215 | }) { (finished) in 216 | self.delegate?.panDidChanged(self, in: self.imageView, alpha: 1.0) 217 | } 218 | } 219 | 220 | return 221 | } 222 | 223 | delegate?.panDidEnded(self, in: imageView) 224 | default: break 225 | } 226 | } 227 | } 228 | 229 | // MARK: - UIScrollViewDelegate 230 | extension EasyAlbumPageContentVC: UIScrollViewDelegate { 231 | func viewForZooming(in scrollView: UIScrollView) -> UIView? { 232 | return imageView 233 | } 234 | 235 | func scrollViewDidZoom(_ scrollView: UIScrollView) { 236 | imageView.center = centerOfContentSize 237 | } 238 | } 239 | 240 | // MARK: - UIGestureRecognizerDelegate 241 | extension EasyAlbumPageContentVC: UIGestureRecognizerDelegate { 242 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 243 | 244 | // allow scroll can to left or right 245 | if let pan = gestureRecognizer as? UIPanGestureRecognizer { 246 | let translation = pan.translation(in: imageView) 247 | return abs(translation.x) >= abs(translation.y) 248 | } 249 | 250 | // if true means both(UIPanGestureRecognizer & UICollectionView) otherwise UIPanGestureRecognizer 251 | return true 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/ViewController/EasyAlbumPreviewPageVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EasyAlbumPreviewPageVC.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/8/26. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | class EasyAlbumPreviewPageVC: UIPageViewController { 13 | 14 | private var backButton: UIButton! 15 | private var numberButton: UIButton! 16 | private var smallNumberLabel: UILabel! 17 | private var sendButton: UIButton! 18 | private var toast: AlbumToast? 19 | 20 | private let photoManager: PhotoManager = PhotoManager.share 21 | 22 | /// Be remove asset 23 | private var removeAsset: PHAsset? 24 | 25 | /// Control statusbar need hidden,default = false 26 | private var hide: Bool = false 27 | 28 | private var currentViewController: EasyAlbumPageContentVC? { 29 | return viewControllers?.first as? EasyAlbumPageContentVC 30 | } 31 | 32 | var limit: Int = EasyAlbumCore.LIMIT 33 | var pickColor: UIColor = EasyAlbumCore.PICK_COLOR 34 | var message: String = EasyAlbumCore.MESSAGE 35 | var orientation: UIInterfaceOrientationMask = EasyAlbumCore.ORIENTATION 36 | 37 | /// The cell frame,default = .zero 38 | var cellFrame: CGRect = .zero 39 | 40 | var currentItem: Int = 0 41 | 42 | /// Origin all assets 43 | var assets: PHFetchResult? 44 | 45 | /// Record selected assets and pick number 46 | var selectedPhotos: [PhotoData] = [] 47 | 48 | override func viewDidLoad() { 49 | super.viewDidLoad() 50 | setup() 51 | } 52 | 53 | override var prefersStatusBarHidden: Bool { 54 | return hide 55 | } 56 | 57 | override var preferredStatusBarStyle: UIStatusBarStyle { 58 | return .lightContent 59 | } 60 | 61 | override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { 62 | return .fade 63 | } 64 | 65 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask { 66 | return orientation 67 | } 68 | 69 | private func setup() { 70 | if #available(iOS 13.0, *) { 71 | overrideUserInterfaceStyle = .light 72 | } 73 | 74 | view.backgroundColor = .black 75 | 76 | backButton = UIButton(type: .system) 77 | backButton.setImage(UIImage.bundle(image: .close), for: .normal) 78 | backButton.tintColor = .white 79 | backButton.addTarget(self, 80 | action: #selector(back(_:)), 81 | for: .touchUpInside) 82 | backButton.translatesAutoresizingMaskIntoConstraints = false 83 | view.addSubview(backButton) 84 | 85 | numberButton = UIButton(type: .custom) 86 | numberButton.titleLabel?.font = UIFont.systemFont(ofSize: 15.0, weight: .regular) 87 | numberButton.setTitleColor(UIColor.white, for: .normal) 88 | numberButton.layer.cornerRadius = 15.0 89 | numberButton.layer.borderColor = UIColor.white.cgColor 90 | numberButton.layer.borderWidth = 3.0 91 | numberButton.addTarget(self, 92 | action: #selector(clickedNumberPhoto(_:)), 93 | for: .touchUpInside) 94 | numberButton.translatesAutoresizingMaskIntoConstraints = false 95 | view.addSubview(numberButton) 96 | 97 | let padding: CGFloat = 14.0 98 | sendButton = UIButton(type: .custom) 99 | sendButton.setImage(UIImage.bundle(image: .done), for: .normal) 100 | sendButton.imageEdgeInsets = UIEdgeInsets(top: padding, 101 | left: padding, 102 | bottom: padding, 103 | right: padding) 104 | sendButton.backgroundColor = .white 105 | sendButton.layer.cornerRadius = 25.0 106 | sendButton.addTarget(self, 107 | action: #selector(done(_:)), 108 | for: .touchUpInside) 109 | sendButton.translatesAutoresizingMaskIntoConstraints = false 110 | view.addSubview(sendButton) 111 | 112 | smallNumberLabel = UILabel(frame: .zero) 113 | smallNumberLabel.text = "\(selectedPhotos.count)" 114 | smallNumberLabel.textColor = .white 115 | smallNumberLabel.font = UIFont.systemFont(ofSize: 12.0, weight: .medium) 116 | smallNumberLabel.textAlignment = .center 117 | smallNumberLabel.backgroundColor = pickColor 118 | smallNumberLabel.layer.cornerRadius = 11.0 119 | smallNumberLabel.layer.masksToBounds = true 120 | smallNumberLabel.isHidden = selectedPhotos.count == 0 121 | smallNumberLabel.translatesAutoresizingMaskIntoConstraints = false 122 | view.addSubview(smallNumberLabel) 123 | 124 | // AutoLayout Start 125 | var btnWH: CGFloat = 23.0 126 | backButton.widthAnchor 127 | .constraint(equalToConstant: btnWH) 128 | .isActive = true 129 | backButton.heightAnchor 130 | .constraint(equalToConstant: btnWH) 131 | .isActive = true 132 | 133 | if #available(iOS 11.0, *) { 134 | backButton.topAnchor 135 | .constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10.0) 136 | .isActive = true 137 | backButton.leadingAnchor 138 | .constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16.0) 139 | .isActive = true 140 | } else { 141 | backButton.topAnchor 142 | .constraint(equalTo: view.topAnchor, constant: 30.0) 143 | .isActive = true 144 | backButton.leadingAnchor 145 | .constraint(equalTo: view.leadingAnchor, constant: 16.0) 146 | .isActive = true 147 | } 148 | 149 | btnWH = 30.0 150 | numberButton.widthAnchor 151 | .constraint(equalToConstant: btnWH) 152 | .isActive = true 153 | numberButton.heightAnchor 154 | .constraint(equalToConstant: btnWH) 155 | .isActive = true 156 | 157 | if #available(iOS 11.0, *) { 158 | numberButton.bottomAnchor 159 | .constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -24.0) 160 | .isActive = true 161 | } else { 162 | numberButton.bottomAnchor 163 | .constraint(equalTo: view.bottomAnchor, constant: -24.0) 164 | .isActive = true 165 | } 166 | 167 | numberButton.centerXAnchor 168 | .constraint(equalTo: backButton.centerXAnchor) 169 | .isActive = true 170 | 171 | btnWH = 50.0 172 | sendButton.widthAnchor 173 | .constraint(equalToConstant: btnWH) 174 | .isActive = true 175 | sendButton.heightAnchor 176 | .constraint(equalToConstant: btnWH) 177 | .isActive = true 178 | sendButton.centerYAnchor 179 | .constraint(equalTo: numberButton.centerYAnchor) 180 | .isActive = true 181 | 182 | if #available(iOS 11.0, *) { 183 | sendButton.trailingAnchor 184 | .constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -22.0) 185 | .isActive = true 186 | } else { 187 | sendButton.trailingAnchor 188 | .constraint(equalTo: view.trailingAnchor, constant: -22.0) 189 | .isActive = true 190 | } 191 | 192 | btnWH = 22.0 193 | smallNumberLabel.widthAnchor 194 | .constraint(equalToConstant: btnWH) 195 | .isActive = true 196 | smallNumberLabel.heightAnchor 197 | .constraint(equalToConstant: btnWH) 198 | .isActive = true 199 | smallNumberLabel.topAnchor 200 | .constraint(equalTo: sendButton.topAnchor, constant: -5.0) 201 | .isActive = true 202 | smallNumberLabel.trailingAnchor 203 | .constraint(equalTo: sendButton.trailingAnchor, constant: 5.0) 204 | .isActive = true 205 | // AutoLayou End 206 | 207 | dataSource = self 208 | delegate = self 209 | 210 | addContentViewController() 211 | addToastView() 212 | changeButtonNumber() 213 | } 214 | 215 | private func addContentViewController() { 216 | let vc = EasyAlbumPageContentVC() 217 | vc.cellFrame = cellFrame 218 | vc.asset = assets?[currentItem] 219 | vc.delegate = self 220 | setViewControllers([vc], direction: .forward, animated: true, completion: nil) 221 | } 222 | 223 | private func addToastView() { 224 | toast = AlbumToast(navigationVC: nil, barTintColor: nil) 225 | toast?.translatesAutoresizingMaskIntoConstraints = false 226 | view.addSubview(toast!) 227 | 228 | toast?.topAnchor 229 | .constraint(equalTo: view.topAnchor) 230 | .isActive = true 231 | toast?.leadingAnchor 232 | .constraint(equalTo: view.leadingAnchor) 233 | .isActive = true 234 | toast?.trailingAnchor 235 | .constraint(equalTo: view.trailingAnchor) 236 | .isActive = true 237 | 238 | let height = UIScreen.statusBarHeight + 44.0 239 | toast?.heightAnchor 240 | .constraint(equalToConstant: height) 241 | .isActive = true 242 | } 243 | 244 | private func changeButtonNumber() { 245 | let current = assets?[currentItem] 246 | 247 | let pickNumber = selectedPhotos.first { $0.asset == current }?.number ?? 0 248 | numberButton.layer.borderColor = pickNumber > 0 ? 249 | pickColor.cgColor : 250 | UIColor(white: 1.0, alpha: 0.78).cgColor 251 | numberButton.backgroundColor = pickNumber > 0 ? 252 | pickColor : 253 | UIColor(hex: "000000", alpha: 0.1) 254 | numberButton.setTitle(pickNumber > 0 ? "\(pickNumber)" : "", for: .normal) 255 | } 256 | 257 | private func changePhotoNumber() { 258 | var reoloadItems: [Int] = [] 259 | for (index, values) in selectedPhotos.enumerated() { 260 | selectedPhotos[index] = (values.asset, index + 1) 261 | 262 | if let i = assets?.index(of: values.asset) { 263 | reoloadItems.append(i) 264 | } 265 | } 266 | 267 | // Add remove asset of index 268 | if let remove = removeAsset, 269 | let i = assets?.index(of: remove) { 270 | reoloadItems.append(i) 271 | } 272 | 273 | // clear 274 | removeAsset = nil 275 | 276 | let notification = AlbumNotification(reloadItems: reoloadItems.map({ IndexPath(item: $0, section: 0) }), 277 | selectedPhotos: selectedPhotos) 278 | NotificationCenter.default.post(name: .EasyAlbumPhotoNumberDidChangeNotification, 279 | object: notification) 280 | 281 | smallNumberLabel.text = "\(selectedPhotos.count)" 282 | smallNumberLabel.isHidden = selectedPhotos.count == 0 283 | } 284 | 285 | @objc private func done(_ btn: UIButton) { 286 | NotificationCenter.default.post(name: .EasyAlbumPreviewPageDismissNotification, 287 | object: AlbumNotification(isSend: true)) 288 | 289 | dismiss(animated: false, completion: nil) 290 | } 291 | 292 | @objc private func back(_ btn: UIButton) { 293 | dismiss(animated: false, completion: nil) 294 | } 295 | 296 | @objc private func clickedNumberPhoto(_ btn: UIButton) { 297 | guard let asset = assets?[currentItem] else { return } 298 | 299 | let isCheck = selectedPhotos.contains { $0.asset == asset } 300 | 301 | if isCheck { 302 | removeAsset = asset 303 | selectedPhotos.removeAll { $0.asset == asset } 304 | } else { 305 | guard selectedPhotos.count <= (limit - 1) else { 306 | toast?.show(with: message) 307 | return 308 | } 309 | 310 | selectedPhotos.append((asset, selectedPhotos.count + 1)) 311 | } 312 | 313 | changeButtonNumber() 314 | changePhotoNumber() 315 | } 316 | } 317 | 318 | // MARK: - UIPageViewControllerDataSource & UIPageViewControllerDelegate 319 | extension EasyAlbumPreviewPageVC: UIPageViewControllerDataSource, UIPageViewControllerDelegate { 320 | 321 | func pageViewController(_ pageViewController: UIPageViewController, 322 | viewControllerBefore viewController: UIViewController) -> UIViewController? { 323 | 324 | if let vc = viewController as? EasyAlbumPageContentVC, 325 | let asset = vc.asset, 326 | let index = assets?.index(of: asset), 327 | index - 1 >= 0 { 328 | let vc = EasyAlbumPageContentVC() 329 | vc.asset = assets?[index - 1] 330 | vc.delegate = self 331 | return vc 332 | } 333 | 334 | return nil 335 | } 336 | 337 | func pageViewController(_ pageViewController: UIPageViewController, 338 | viewControllerAfter viewController: UIViewController) -> UIViewController? { 339 | 340 | if let vc = viewController as? EasyAlbumPageContentVC, 341 | let asset = vc.asset, 342 | let index = assets?.index(of: asset), 343 | index + 1 < assets?.count ?? 0 { 344 | let vc = EasyAlbumPageContentVC() 345 | vc.asset = assets?[index + 1] 346 | vc.delegate = self 347 | return vc 348 | } 349 | 350 | return nil 351 | } 352 | 353 | func pageViewController(_ pageViewController: UIPageViewController, 354 | didFinishAnimating finished: Bool, 355 | previousViewControllers: [UIViewController], 356 | transitionCompleted completed: Bool) { 357 | 358 | guard let currentVC = currentViewController, 359 | let asset = currentVC.asset, 360 | let index = assets?.index(of: asset), 361 | completed == true 362 | else { return } 363 | 364 | currentItem = index 365 | changeButtonNumber() 366 | } 367 | } 368 | 369 | // MARK: - EAPageContentViewControllerDelegate 370 | extension EasyAlbumPreviewPageVC: EasyAlbumPageContentVCDelegate { 371 | 372 | func singleTap(_ viewController: EasyAlbumPageContentVC) { 373 | hide.toggle() 374 | setNeedsStatusBarAppearanceUpdate() 375 | 376 | let views: [UIView] = [backButton, sendButton, smallNumberLabel] 377 | UIView.animate(withDuration: 0.32) { 378 | views.forEach({ $0.alpha = self.hide ? 0.0 : 1.0 }) 379 | } 380 | } 381 | 382 | func panDidChanged(_ viewController: EasyAlbumPageContentVC, in targetView: UIView, alpha: CGFloat) { 383 | let views: [UIView] = [backButton, sendButton, smallNumberLabel] 384 | views.forEach({ $0.alpha = alpha }) 385 | view.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: alpha) 386 | } 387 | 388 | func panDidEnded(_ viewController: EasyAlbumPageContentVC, in targetView: UIView) { 389 | dismiss(animated: false, completion: nil) 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/ViewController/EasyAlbumVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EasyAlbumVC.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | import PhotosUI 12 | 13 | class EasyAlbumVC: UIViewController { 14 | 15 | var appName: String = EasyAlbumCore.APP_NAME 16 | var barTintColor: UIColor = EasyAlbumCore.BAR_TINT_COLOR 17 | var limit: Int = EasyAlbumCore.LIMIT 18 | var span: Int = EasyAlbumCore.SPAN 19 | var titleColor: UIColor = EasyAlbumCore.TINT_COLOR 20 | var pickColor: UIColor = EasyAlbumCore.PICK_COLOR 21 | var crop: Bool = EasyAlbumCore.CROP 22 | var showCamera: Bool = EasyAlbumCore.SHOW_CAMERA 23 | var message: String = EasyAlbumCore.MESSAGE 24 | var sizeFactor: EasyAlbumSizeFactor = EasyAlbumCore.SIZE_FACTOR 25 | var orientation: UIInterfaceOrientationMask = EasyAlbumCore.ORIENTATION 26 | 27 | weak var albumDelegate: EasyAlbumDelegate? 28 | 29 | private var titleButton: UIButton! 30 | private var refreshCtrl: UIRefreshControl! 31 | private var collectionView: UICollectionView! 32 | private var doneView: AlbumDoneView? 33 | private var toast: AlbumToast? 34 | private var doneViewHeightConst: NSLayoutConstraint? 35 | 36 | private var photoManager: PhotoManager = PhotoManager.share 37 | 38 | /// Cache image object 39 | private var imageCache: NSCache? 40 | 41 | /// Put item size for portrait or landscape 42 | private var dynamicItemSizeDictionary: Dictionary = [:] 43 | 44 | /// Album folders 45 | private var albumFolders: [AlbumFolder] = [] 46 | 47 | /// Album category type index 48 | private var categoryIndex: Int = 0 49 | 50 | private var currentResultAsset: PHFetchResult? 51 | 52 | /// Selected photos,default = [] 53 | private var selectedPhotos: [PhotoData] = [] 54 | 55 | private var isPortrait: Bool = UIScreen.isPortrait 56 | 57 | /// Is processing photo,default = false 58 | private var isProcessing: Bool = false 59 | 60 | private let font = UIFont.systemFont(ofSize: 18.0, weight: .medium) 61 | private let doneViewHeight: CGFloat = 54.0 62 | private var safeAreaBottom: CGFloat = 0.0 63 | private let animateDuration: TimeInterval = 0.25 64 | 65 | override func viewDidLoad() { 66 | super.viewDidLoad() 67 | setup() 68 | addAlbumObserver() 69 | requestPhotoPermission() 70 | } 71 | 72 | override func viewDidLayoutSubviews() { 73 | safeAreaBottom = view.layoutMargins.bottom 74 | 75 | if #available(iOS 11.0, *) { 76 | safeAreaBottom = view.safeAreaInsets.bottom 77 | } 78 | 79 | doneViewHeightConst?.constant = doneViewHeight + safeAreaBottom 80 | } 81 | 82 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 83 | // When device rotate, trigger invalidateLayout 84 | collectionView.collectionViewLayout.invalidateLayout() 85 | } 86 | 87 | deinit { 88 | if PHPhotoLibrary.authorizationStatus() == .authorized { 89 | photoManager.stopAllCachingImages() 90 | } 91 | 92 | imageCache?.removeAllObjects() 93 | 94 | NotificationCenter.default.removeObserver(self) 95 | PHPhotoLibrary.shared().unregisterChangeObserver(self) 96 | 97 | #if targetEnvironment(simulator) 98 | print("EasyAlbumVC deinit 👍🏻") 99 | #endif 100 | } 101 | 102 | private func setup() { 103 | if #available(iOS 13.0, *) { 104 | overrideUserInterfaceStyle = .light 105 | } 106 | 107 | view.backgroundColor = .white 108 | 109 | if message.isEmpty { message = LString(.overLimit(count: limit)) } 110 | 111 | titleButton = UIButton(type: .custom) 112 | titleButton.setTitleColor(titleColor, for: .normal) 113 | titleButton.titleLabel?.font = font 114 | navigationItem.titleView = titleButton 115 | 116 | let barClose = UIBarButtonItem(image: UIImage.bundle(image: .close), 117 | style: .plain, 118 | target: self, 119 | action: #selector(close(_:))) 120 | let barCamera = UIBarButtonItem(image: UIImage.bundle(image: .camera), 121 | style: .plain, 122 | target: self, 123 | action: #selector(openCamera(_:))) 124 | navigationItem.leftBarButtonItem = barClose 125 | navigationItem.rightBarButtonItem = showCamera ? barCamera : nil 126 | 127 | refreshCtrl = UIRefreshControl() 128 | refreshCtrl.transform = CGAffineTransform(scaleX: 0.75, y: 0.75) 129 | refreshCtrl.tintColor = .gray 130 | 131 | let flowLayout = UICollectionViewFlowLayout() 132 | flowLayout.minimumInteritemSpacing = CGFloat(1) 133 | flowLayout.minimumLineSpacing = CGFloat(1) 134 | 135 | collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) 136 | collectionView.registerHeader(AlbumCategoryView.self, isNib: false) 137 | collectionView.registerCell(AlbumPhotoCell.self, isNib: false) 138 | collectionView.showsVerticalScrollIndicator = false 139 | collectionView.backgroundColor = .white 140 | collectionView.delegate = self 141 | collectionView.dataSource = self 142 | collectionView.prefetchDataSource = self 143 | collectionView.refreshControl = refreshCtrl 144 | collectionView.translatesAutoresizingMaskIntoConstraints = false 145 | view.addSubview(collectionView) 146 | 147 | // AutoLayout 148 | if #available(iOS 11.0, *) { 149 | collectionView.leadingAnchor 150 | .constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor) 151 | .isActive = true 152 | collectionView.trailingAnchor 153 | .constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) 154 | .isActive = true 155 | } else { 156 | collectionView.leadingAnchor 157 | .constraint(equalTo: view.leadingAnchor) 158 | .isActive = true 159 | collectionView.trailingAnchor 160 | .constraint(equalTo: view.trailingAnchor) 161 | .isActive = true 162 | } 163 | 164 | collectionView.topAnchor 165 | .constraint(equalTo: view.topAnchor) 166 | .isActive = true 167 | collectionView.bottomAnchor 168 | .constraint(equalTo: view.bottomAnchor) 169 | .isActive = true 170 | 171 | refreshCtrl.beginRefreshing() 172 | } 173 | 174 | private func addAlbumObserver() { 175 | NotificationCenter.default.addObserver(self, 176 | selector: #selector(photoNumberDidChangeNotification(_:)), 177 | name: .EasyAlbumPhotoNumberDidChangeNotification, 178 | object: nil) 179 | NotificationCenter.default.addObserver(self, 180 | selector: #selector(previewPageDismissNotification(_:)), 181 | name: .EasyAlbumPreviewPageDismissNotification, 182 | object: nil) 183 | PHPhotoLibrary.shared().register(self) 184 | } 185 | 186 | private func requestPhotoPermission() { 187 | photoManager.requestPermission { 188 | self.loadAlbums(isLimit: false) 189 | } didLimited: { (isLimit) in 190 | self.loadAlbums(isLimit: isLimit) 191 | 192 | if #available(iOS 14, *), isLimit { 193 | let library = PHPhotoLibrary.shared() 194 | library.register(self) 195 | library.presentLimitedLibraryPicker(from: self) 196 | } 197 | } didDenied: { 198 | self.showDialog(with: .photo) 199 | } 200 | } 201 | 202 | private func addToastView() { 203 | guard let navigationVC = navigationController as? EasyAlbumNAC else { return } 204 | 205 | toast = AlbumToast(navigationVC: navigationVC, barTintColor: barTintColor) 206 | toast?.translatesAutoresizingMaskIntoConstraints = false 207 | navigationVC.navigationBar.addSubview(toast!) 208 | 209 | // AutoLayout 210 | toast?.topAnchor 211 | .constraint(equalTo: navigationVC.navigationBar.topAnchor) 212 | .isActive = true 213 | toast?.leadingAnchor 214 | .constraint(equalTo: navigationVC.navigationBar.leadingAnchor) 215 | .isActive = true 216 | toast?.trailingAnchor 217 | .constraint(equalTo: navigationVC.navigationBar.trailingAnchor) 218 | .isActive = true 219 | toast?.bottomAnchor 220 | .constraint(equalTo: navigationVC.navigationBar.bottomAnchor) 221 | .isActive = true 222 | } 223 | 224 | private func addDoneView() { 225 | doneView = AlbumDoneView() 226 | doneView?.delegate = self 227 | doneView?.translatesAutoresizingMaskIntoConstraints = false 228 | view.addSubview(doneView!) 229 | 230 | // AutoLayout 231 | doneViewHeightConst = doneView?.heightAnchor.constraint(equalToConstant: 0.0) 232 | doneViewHeightConst?.isActive = true 233 | 234 | doneView?.topAnchor 235 | .constraint(equalTo: view.bottomAnchor) 236 | .isActive = true 237 | 238 | if #available(iOS 11.0, *) { 239 | doneView?.leadingAnchor 240 | .constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor) 241 | .isActive = true 242 | doneView?.trailingAnchor 243 | .constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) 244 | .isActive = true 245 | } else { 246 | doneView?.leadingAnchor 247 | .constraint(equalTo: view.leadingAnchor) 248 | .isActive = true 249 | doneView?.trailingAnchor 250 | .constraint(equalTo: view.trailingAnchor) 251 | .isActive = true 252 | } 253 | } 254 | 255 | private func loadAlbums(isLimit: Bool) { 256 | if imageCache == nil { imageCache = NSCache() } 257 | photoManager.fetchPhotos(in: &albumFolders, pickColor: pickColor) 258 | 259 | if isLimit == false { 260 | // Setup first is selected 261 | albumFolders[0].isCheck = true 262 | currentResultAsset = albumFolders[0].assets 263 | 264 | // Show first album name 265 | collectionView.reloadData() 266 | setNavigationTitle(with: albumFolders[0].title) 267 | 268 | // Stop refreshing and remove 269 | refreshCtrl.endRefreshing() 270 | collectionView.refreshControl = nil 271 | 272 | addToastView() 273 | addDoneView() 274 | } 275 | } 276 | 277 | private func showDialog(with permission: EasyAlbumPermission) { 278 | let witch = permission.description 279 | let msg = LString(.permissionMsg(appName: appName, witch: witch)) 280 | let ac = UIAlertController(title: LString(.permissionTitle(witch: witch)), 281 | message: msg, 282 | preferredStyle: .alert) 283 | 284 | let setting = UIAlertAction(title: LString(.setting), 285 | style: .default) 286 | { (action) in 287 | let url = URL(string: UIApplication.openSettingsURLString)! 288 | if UIApplication.shared.canOpenURL(url) { 289 | UIApplication.shared.open(url, options: [:], completionHandler: nil) 290 | 291 | // Back to previous 292 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(320), execute: { 293 | self.dismiss(animated: true, completion: nil) 294 | }) 295 | } 296 | } 297 | 298 | ac.addAction(setting) 299 | present(ac, animated: true, completion: nil) 300 | } 301 | 302 | private func clickedNumberPhoto(on item: Int) { 303 | guard isProcessing == false else { return } 304 | guard let asset = currentResultAsset?[item] else { return } 305 | 306 | let isCheck = selectedPhotos.contains { $0.asset == asset } 307 | 308 | if isCheck { 309 | selectedPhotos.removeAll { $0.asset == asset } 310 | } else { 311 | guard selectedPhotos.count <= (limit - 1) else { 312 | toast?.show(with: message) 313 | return 314 | } 315 | 316 | selectedPhotos.append((asset, selectedPhotos.count + 1)) 317 | } 318 | 319 | UIView.performWithoutAnimation { 320 | self.collectionView.reloadItems(at: [IndexPath(row: item, section: 0)]) 321 | } 322 | 323 | changePhotoNumber() 324 | changeDoneViewData() 325 | } 326 | 327 | /// Change selected photo pick number 328 | private func changePhotoNumber() { 329 | var needReoloadItems: [Int] = [] 330 | for (index, values) in selectedPhotos.enumerated() { 331 | selectedPhotos[index] = (values.asset, index + 1) 332 | 333 | if let i = currentResultAsset?.index(of: values.asset) { 334 | needReoloadItems.append(i) 335 | } 336 | } 337 | 338 | UIView.performWithoutAnimation { 339 | self.collectionView.reloadItems(at: needReoloadItems.map { IndexPath(item: $0, section: 0) }) 340 | } 341 | } 342 | 343 | private func changeDoneViewData() { 344 | let isGreaterZero = selectedPhotos.count > 0 345 | let h = doneViewHeight + safeAreaBottom 346 | 347 | if isGreaterZero { 348 | let density = UIScreen.density 349 | let size = CGSize(width: AlbumDoneView.width * density, height: AlbumDoneView.height * density) 350 | photoManager.fetchThumbnail(form: selectedPhotos[0].asset, 351 | size: size, 352 | options: .exact(isSync: false)) 353 | { [weak self] (image) in 354 | self?.doneView?.image = image 355 | } 356 | 357 | doneView?.number = selectedPhotos.count 358 | UIView.animate(withDuration: animateDuration) { 359 | self.doneView?.transform = CGAffineTransform(translationX: 0.0, y: -h) 360 | } 361 | } else { 362 | UIView.animate(withDuration: animateDuration) { 363 | self.doneView?.transform = .identity 364 | } 365 | } 366 | 367 | // Setting collectionView content margin 368 | collectionView.contentInset = UIEdgeInsets(top: 0.0, 369 | left: 0.0, 370 | bottom: isGreaterZero ? h : 0.0, 371 | right: 0.0) 372 | } 373 | 374 | private func setNavigationTitle(with text: String) { 375 | var width = text.height(with: 22.0, font: font) 376 | 377 | if let image = titleButton.imageView?.image { 378 | width += image.size.width 379 | } 380 | 381 | titleButton.frame.size = CGSize(width: width, height: 22.0) 382 | titleButton.setTitle(text, for: .normal) 383 | } 384 | 385 | private func convertTask() { 386 | guard isProcessing == false else { return } 387 | 388 | isProcessing = true 389 | 390 | toast?.show(with: LString(.photoProcess), autoCancel: false) 391 | photoManager.cenvertTask(from: selectedPhotos.compactMap({ $0.asset }), factor: sizeFactor) 392 | { [weak self] (datas) in 393 | self?.toast?.hide() 394 | self?.albumDelegate?.easyAlbumDidSelected(datas) 395 | self?.dismiss(animated: true, completion: nil) 396 | } 397 | } 398 | 399 | private func handlePhotoFromAppCamera(assets: [PHAsset]) { 400 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(320)) { 401 | if isFromEasyAlbumCamera { 402 | isFromEasyAlbumCamera = false 403 | 404 | self.photoManager.cenvertTask(from: assets, 405 | factor: self.sizeFactor) 406 | { (datas) in 407 | self.albumDelegate?.easyAlbumDidSelected(datas) 408 | self.dismiss(animated: true, completion: nil) 409 | } 410 | } 411 | } 412 | } 413 | 414 | @objc private func close(_ btn: UIButton) { 415 | albumDelegate?.easyAlbumDidCanceled() 416 | dismiss(animated: true, completion: nil) 417 | } 418 | 419 | @objc private func openCamera(_ btn: UIButton) { 420 | let hasCamera = UIImagePickerController.isSourceTypeAvailable(.camera) 421 | if hasCamera { 422 | let authStatus = AVCaptureDevice.authorizationStatus(for: .video) 423 | switch authStatus { 424 | case .authorized, .notDetermined: 425 | let camera = EasyAlbumCameraVC() 426 | camera.isEdit = crop 427 | present(camera, animated: true, completion: nil) 428 | case .denied, .restricted: 429 | showDialog(with: .camera) 430 | default: break 431 | } 432 | } else { 433 | toast?.show(with: LString(.noCamera)) 434 | } 435 | } 436 | 437 | // MARK: - Notification 通知 438 | 439 | @objc private func photoNumberDidChangeNotification(_ notification: Notification) { 440 | guard let albumNotification = notification.object as? AlbumNotification 441 | else { return } 442 | 443 | selectedPhotos = albumNotification.selectedPhotos 444 | collectionView.reloadItems(at: albumNotification.reloadItems) 445 | changeDoneViewData() 446 | } 447 | 448 | @objc private func previewPageDismissNotification(_ notification: Notification) { 449 | guard let albumNotification = notification.object as? AlbumNotification 450 | else { return } 451 | 452 | if albumNotification.isSend, selectedPhotos.isEmpty == false { convertTask() } 453 | } 454 | } 455 | 456 | // MARK: - UICollectionViewDataSource & UICollectionViewDelegate 457 | extension EasyAlbumVC: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDataSourcePrefetching { 458 | 459 | func numberOfSections(in collectionView: UICollectionView) -> Int { 460 | return 1 461 | } 462 | 463 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 464 | return currentResultAsset?.count ?? 0 465 | } 466 | 467 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 468 | let cell = collectionView.dequeueCell(AlbumPhotoCell.self, indexPath: indexPath) 469 | 470 | let item = indexPath.item 471 | 472 | guard let asset = currentResultAsset?[item] else { return cell } 473 | 474 | cell.representedAssetIdentifier = asset.localIdentifier 475 | cell.delegate = self 476 | 477 | if let image = imageCache?.object(forKey: asset) { 478 | if cell.representedAssetIdentifier == asset.localIdentifier { 479 | let values = selectedPhotos.first { $0.asset == asset } 480 | cell.setData(from: asset, 481 | image: image, 482 | number: values?.number, 483 | pickColor: pickColor, 484 | item: item) 485 | } 486 | } else { 487 | let isPortrait = UIScreen.height >= UIScreen.width 488 | let size = dynamicItemSizeDictionary[isPortrait]?.scale(to: 1.8) 489 | 490 | photoManager.fetchThumbnail(form: asset, size: size, options: .exact(isSync: false)) 491 | { [weak self] (image) in 492 | guard let self = self else { return } 493 | 494 | self.imageCache?.setObject(image, forKey: asset) 495 | if cell.representedAssetIdentifier == asset.localIdentifier { 496 | let values = self.selectedPhotos.first { $0.asset == asset } 497 | cell.setData(from: asset, 498 | image: image, 499 | number: values?.number, 500 | pickColor: self.pickColor, 501 | item: item) 502 | } 503 | } 504 | } 505 | 506 | return cell 507 | } 508 | 509 | func collectionView(_ collectionView: UICollectionView, 510 | viewForSupplementaryElementOfKind kind: String, 511 | at indexPath: IndexPath) -> UICollectionReusableView { 512 | if kind == UICollectionView.elementKindSectionHeader { 513 | let headerView = collectionView.dequeueHeader(AlbumCategoryView.self, indexPath: indexPath) 514 | headerView.datas = albumFolders 515 | headerView.delegate = self 516 | return headerView 517 | } 518 | 519 | return UICollectionReusableView() 520 | } 521 | 522 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 523 | guard !isProcessing else { return } 524 | 525 | // Get cell position relative `collectionView.contentOffset = .zero` 526 | var cellFrame: CGRect = .zero 527 | if let cell = collectionView.cellForItem(at: indexPath) as? AlbumPhotoCell { 528 | let originX = cell.frame.minX 529 | let relativeY = cell.center.y - collectionView.contentOffset.y 530 | cellFrame = CGRect(origin: CGPoint(x: originX, y: relativeY), size: cell.frame.size) 531 | } 532 | 533 | let item = indexPath.item 534 | 535 | let previewVC = EasyAlbumPreviewPageVC(transitionStyle: .scroll, 536 | navigationOrientation: .horizontal, 537 | options: nil) 538 | previewVC.limit = limit 539 | previewVC.pickColor = pickColor 540 | previewVC.message = message 541 | previewVC.orientation = orientation 542 | previewVC.currentItem = item 543 | previewVC.assets = currentResultAsset 544 | previewVC.selectedPhotos = selectedPhotos 545 | previewVC.cellFrame = cellFrame 546 | previewVC.modalPresentationStyle = .overCurrentContext 547 | 548 | present(previewVC, animated: false, completion: nil) 549 | } 550 | 551 | func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { 552 | let assets = indexPaths.compactMap({ currentResultAsset?[$0.item] }) 553 | 554 | DispatchQueue.main.async { 555 | self.photoManager.startCacheImage(prefetchItemsAt: assets, options: .fast) 556 | } 557 | } 558 | 559 | func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) { 560 | guard let count = currentResultAsset?.count, count < indexPaths.count 561 | else { return } 562 | 563 | var assets: [PHAsset?] = [] 564 | for i in 0 ..< indexPaths.count { 565 | assets.append(i < count ? currentResultAsset?[i] : nil) 566 | } 567 | 568 | DispatchQueue.main.async { 569 | self.photoManager.startCacheImage(prefetchItemsAt: assets.compactMap { $0 }, options: .fast) 570 | } 571 | } 572 | } 573 | 574 | // MARK: - UICollectionViewDelegateFlowLayout 575 | extension EasyAlbumVC: UICollectionViewDelegateFlowLayout { 576 | 577 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 578 | return section == 0 ? CGSize(width: UIScreen.width, height: AlbumCategoryView.height) : .zero 579 | } 580 | 581 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 582 | 583 | let isPortrait = UIScreen.height >= UIScreen.width 584 | 585 | if let size = dynamicItemSizeDictionary[isPortrait] { 586 | return size 587 | } 588 | 589 | // Get margin of left and right 590 | var left = CGFloat(0.0) 591 | var right = CGFloat(0.0) 592 | 593 | if #available(iOS 11.0, *) { 594 | left = view.safeAreaInsets.left 595 | right = view.safeAreaInsets.right 596 | } 597 | 598 | // Calc span count, if orientation = landscape, then span count + 2 599 | let spanCount = isPortrait ? span : span + 2 600 | 601 | let divider = CGFloat(spanCount - 1) + left + right 602 | let itemW = (UIScreen.width - divider) / CGFloat(spanCount) 603 | let itemH = itemW 604 | let size = CGSize(width: itemW, height: itemH) 605 | 606 | dynamicItemSizeDictionary[isPortrait] = size 607 | return size 608 | } 609 | } 610 | 611 | // MARK: - AlbumPhotoCellDelegate 612 | extension EasyAlbumVC: AlbumPhotoCellDelegate { 613 | 614 | func albumPhotoCell(didNumberClickAt item: Int) { 615 | clickedNumberPhoto(on: item) 616 | } 617 | } 618 | 619 | // MARK: - AlbumDoneViewDelegate 620 | extension EasyAlbumVC: AlbumDoneViewDelegate { 621 | 622 | func albumDoneViewDidClicked(_ albumDoneView: AlbumDoneView) { 623 | convertTask() 624 | } 625 | } 626 | 627 | // MARK: - AlbumCategoryViewDelegate 628 | extension EasyAlbumVC: AlbumCategoryViewDelegate { 629 | 630 | func albumCategoryView(_ albumCategoryView: AlbumCategoryView, didSelectedAt index: Int) { 631 | for i in 0 ..< albumFolders.count { albumFolders[i].isCheck = false } 632 | 633 | categoryIndex = index 634 | albumFolders[index].isCheck = true 635 | currentResultAsset = albumFolders[index].assets 636 | 637 | setNavigationTitle(with: albumFolders[index].title) 638 | collectionView.reloadData() 639 | } 640 | } 641 | 642 | // MARK: - PHPhotoLibraryChangeObserver 643 | extension EasyAlbumVC: PHPhotoLibraryChangeObserver { 644 | 645 | func photoLibraryDidChange(_ changeInstance: PHChange) { 646 | // Update all folder assets 647 | for (index, folder) in albumFolders.enumerated() { 648 | if let changeDetails = changeInstance.changeDetails(for: folder.assets) { 649 | albumFolders[index].assets = changeDetails.fetchResultAfterChanges 650 | } 651 | } 652 | 653 | if let assets = currentResultAsset, 654 | let changeDetails = changeInstance.changeDetails(for: assets) { 655 | currentResultAsset = changeDetails.fetchResultAfterChanges 656 | 657 | DispatchQueue.main.async { 658 | if changeDetails.hasIncrementalChanges { 659 | guard let collectionView = self.collectionView else { fatalError() } 660 | 661 | // Handle removals, insertions, and moves in a batch update. 662 | collectionView.performBatchUpdates { 663 | if let removed = changeDetails.removedIndexes, 664 | removed.isEmpty == false { 665 | collectionView.deleteItems(at: removed.map({ IndexPath(item: $0, section: 0) })) 666 | } 667 | 668 | if let inserted = changeDetails.insertedIndexes, 669 | inserted.isEmpty == false { 670 | collectionView.insertItems(at: inserted.map({ IndexPath(item: $0, section: 0) })) 671 | } 672 | } completion: { (finished) in 673 | // We are reloading items after the batch update since 674 | // `PHFetchResultChangeDetails.changedIndexes` refers to 675 | // items in the *after* state and not the *before* state as expected by 676 | // `performBatchUpdates(_:completion:)`. 677 | if let changed = changeDetails.changedIndexes, 678 | changed.isEmpty == false, 679 | finished == true { 680 | collectionView.reloadItems(at: changed.map({ IndexPath(item: $0, section: 0) })) 681 | } 682 | 683 | for asset in changeDetails.removedObjects { 684 | if let i = self.selectedPhotos.firstIndex(where: { $0.asset == asset }) { 685 | self.selectedPhotos.remove(at: i) 686 | } 687 | } 688 | 689 | self.changePhotoNumber() 690 | self.changeDoneViewData() 691 | } 692 | } else { 693 | // Reload the collection view if incremental changes are not available. 694 | self.collectionView.reloadData() 695 | } 696 | 697 | self.handlePhotoFromAppCamera(assets: changeDetails.insertedObjects) 698 | } 699 | } 700 | } 701 | } 702 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Widget/AlbumBorderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumBorderView.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/3. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | class AlbumBorderView: UIView { 13 | 14 | @IBInspectable var borderColor: UIColor = UIColor(hex: "6600ff") { 15 | didSet { setNeedsDisplay() } 16 | } 17 | 18 | @IBInspectable var strokeWidth: CGFloat = 6.5 { 19 | didSet { setNeedsDisplay() } 20 | } 21 | 22 | private var path: UIBezierPath! 23 | 24 | override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | backgroundColor = UIColor.clear 27 | } 28 | 29 | override func awakeFromNib() { 30 | super.awakeFromNib() 31 | backgroundColor = UIColor.clear 32 | } 33 | 34 | required public init?(coder aDecoder: NSCoder) { 35 | super.init(coder: aDecoder) 36 | backgroundColor = UIColor.clear 37 | } 38 | 39 | override func draw(_ rect: CGRect) { 40 | super.draw(rect) 41 | guard let ctx = UIGraphicsGetCurrentContext() else { return } 42 | 43 | // draw translucent background 44 | ctx.setFillColor(UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.35).cgColor) 45 | ctx.addRect(rect) 46 | ctx.fillPath() 47 | 48 | // draw border 49 | ctx.setLineCap(.square) 50 | ctx.setLineJoin(.miter) 51 | ctx.setLineWidth(strokeWidth) 52 | ctx.setStrokeColor(borderColor.cgColor) 53 | 54 | let oriSX = rect.minX 55 | let oriSY = rect.minY 56 | let oriEX = rect.maxX 57 | let oriEY = rect.maxY 58 | 59 | // left 60 | ctx.move(to: CGPoint(x: oriSX, y: oriSY)) 61 | ctx.addLine(to: CGPoint(x: oriSX, y: oriEY)) 62 | // bottom 63 | ctx.move(to: CGPoint(x: oriSX, y: oriEY)) 64 | ctx.addLine(to: CGPoint(x: oriEX, y: oriEY)) 65 | // right 66 | ctx.move(to: CGPoint(x: oriEX, y: oriEY)) 67 | ctx.addLine(to: CGPoint(x: oriEX, y: oriSY)) 68 | // top 69 | ctx.move(to: CGPoint(x: oriEX, y: oriSY)) 70 | ctx.addLine(to: CGPoint(x: oriSX, y: oriSY)) 71 | 72 | ctx.strokePath() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Widget/AlbumCategoryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumCategoryView.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/10. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol AlbumCategoryViewDelegate: class { 12 | func albumCategoryView(_ albumCategoryView: AlbumCategoryView, didSelectedAt index: Int) 13 | } 14 | 15 | class AlbumCategoryView: UICollectionReusableView { 16 | 17 | public static let height: CGFloat = 95.0 18 | private let width: CGFloat = 95.0 19 | 20 | private var collectionView: UICollectionView? 21 | 22 | weak var delegate: AlbumCategoryViewDelegate? 23 | 24 | var datas: [AlbumFolder] = [] { 25 | didSet { collectionView?.reloadData() } 26 | } 27 | 28 | override init(frame: CGRect) { 29 | super.init(frame: frame) 30 | setup() 31 | } 32 | 33 | convenience init() { 34 | self.init(frame: .zero) 35 | } 36 | 37 | required init?(coder aDecoder: NSCoder) { 38 | super.init(coder: aDecoder) 39 | setup() 40 | } 41 | 42 | private func setup() { 43 | let flowLayout = UICollectionViewFlowLayout() 44 | flowLayout.scrollDirection = .horizontal 45 | flowLayout.itemSize = CGSize(width: width, height: AlbumCategoryView.height) 46 | flowLayout.minimumLineSpacing = 0.0 47 | 48 | collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) 49 | collectionView?.registerCell(AlbumCategoryCell.self, isNib: false) 50 | collectionView?.backgroundColor = .white 51 | collectionView?.showsVerticalScrollIndicator = false 52 | collectionView?.showsHorizontalScrollIndicator = false 53 | collectionView?.delegate = self 54 | collectionView?.dataSource = self 55 | collectionView?.translatesAutoresizingMaskIntoConstraints = false 56 | addSubview(collectionView!) 57 | 58 | // AutoLayout 59 | collectionView?.topAnchor.constraint(equalTo: topAnchor).isActive = true 60 | collectionView?.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true 61 | collectionView?.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true 62 | collectionView?.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 63 | } 64 | } 65 | 66 | extension AlbumCategoryView: UICollectionViewDataSource, UICollectionViewDelegate { 67 | func numberOfSections(in collectionView: UICollectionView) -> Int { 68 | return 1 69 | } 70 | 71 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 72 | return datas.count 73 | } 74 | 75 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 76 | let index = indexPath.item 77 | let cell = collectionView.dequeueCell(AlbumCategoryCell.self, indexPath: indexPath) 78 | cell.data = datas[index] 79 | return cell 80 | } 81 | 82 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 83 | delegate?.albumCategoryView(self, didSelectedAt: indexPath.item) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Widget/AlbumDoneView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumDoneView.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/3/10. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol AlbumDoneViewDelegate: class { 12 | func albumDoneViewDidClicked(_ albumDoneView: AlbumDoneView) 13 | } 14 | 15 | class AlbumDoneView: UIView { 16 | 17 | /// width,value = 34.0 18 | static let width: CGFloat = 34.0 19 | 20 | /// height,value = 34.0 21 | static let height: CGFloat = 34.0 22 | 23 | private var doneButton: UIButton? 24 | private var imageView: UIImageView? 25 | private var numberLabel: UILabel? 26 | 27 | private let textColor: UIColor = UIColor(hex: "1a1a1a") 28 | 29 | /// Background color,default = #ffffff 30 | var bgColor: UIColor = .white { 31 | didSet { backgroundColor = bgColor} 32 | } 33 | 34 | /// Selected photo of first,default = nil 35 | var image: UIImage? { 36 | didSet { imageView?.image = image } 37 | } 38 | 39 | /// Selected count,default = 0 40 | var number: Int = 0 { 41 | didSet { numberLabel?.text = "( \(number) )"} 42 | } 43 | 44 | weak var delegate: AlbumDoneViewDelegate? 45 | 46 | override init(frame: CGRect) { 47 | super.init(frame: frame) 48 | setup() 49 | } 50 | 51 | convenience init() { 52 | self.init(frame: .zero) 53 | } 54 | 55 | required init?(coder aDecoder: NSCoder) { 56 | super.init(coder: aDecoder) 57 | setup() 58 | } 59 | 60 | private func setup() { 61 | backgroundColor = bgColor 62 | 63 | let margin: CGFloat = 20.0 64 | imageView = UIImageView(frame: .zero) 65 | imageView?.contentMode = .scaleAspectFit 66 | imageView?.layer.cornerRadius = 5.0 67 | imageView?.layer.masksToBounds = true 68 | imageView?.translatesAutoresizingMaskIntoConstraints = false 69 | addSubview(imageView!) 70 | 71 | numberLabel = UILabel(frame: .zero) 72 | numberLabel?.textColor = textColor 73 | numberLabel?.font = UIFont.systemFont(ofSize: 15.0, weight: .medium) 74 | numberLabel?.translatesAutoresizingMaskIntoConstraints = false 75 | addSubview(numberLabel!) 76 | 77 | let padding: CGFloat = 3.0 78 | doneButton = UIButton(type: .system) 79 | doneButton?.setImage(UIImage.bundle(image: .done), for: .normal) 80 | doneButton?.imageEdgeInsets = UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding) 81 | doneButton?.tintColor = textColor 82 | doneButton?.addTarget(self, action: #selector(done(_:)), for: .touchUpInside) 83 | doneButton?.translatesAutoresizingMaskIntoConstraints = false 84 | addSubview(doneButton!) 85 | 86 | // AutoLayout 87 | imageView?.widthAnchor 88 | .constraint(equalToConstant: AlbumDoneView.width) 89 | .isActive = true 90 | imageView?.heightAnchor 91 | .constraint(equalToConstant: AlbumDoneView.height) 92 | .isActive = true 93 | imageView?.topAnchor 94 | .constraint(equalTo: topAnchor, constant: 10.0) 95 | .isActive = true 96 | imageView?.leadingAnchor 97 | .constraint(equalTo: leadingAnchor, constant: margin) 98 | .isActive = true 99 | 100 | numberLabel?.centerYAnchor 101 | .constraint(equalTo: imageView!.centerYAnchor) 102 | .isActive = true 103 | numberLabel?.leadingAnchor 104 | .constraint(equalTo: imageView!.trailingAnchor, constant: 10.0) 105 | .isActive = true 106 | 107 | doneButton?.centerYAnchor 108 | .constraint(equalTo: imageView!.centerYAnchor) 109 | .isActive = true 110 | doneButton?.trailingAnchor 111 | .constraint(equalTo: trailingAnchor, constant: -margin) 112 | .isActive = true 113 | } 114 | 115 | @objc private func done(_ btn: UIButton) { 116 | delegate?.albumDoneViewDidClicked(self) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Widget/AlbumSelectedButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumSelectedButton.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/4/23. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | class AlbumSelectedButton: UIButton { 13 | 14 | @IBInspectable var borderColor: UIColor = .white { 15 | didSet { setNeedsDisplay() } 16 | } 17 | 18 | @IBInspectable var strokeWidth: CGFloat = 3.0 { 19 | didSet { setNeedsDisplay() } 20 | } 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | setup() 25 | } 26 | 27 | override func awakeFromNib() { 28 | super.awakeFromNib() 29 | setup() 30 | } 31 | 32 | required public init?(coder aDecoder: NSCoder) { 33 | super.init(coder: aDecoder) 34 | setup() 35 | } 36 | 37 | private func setup() { 38 | backgroundColor = UIColor.clear 39 | layer.cornerRadius = 5.0 40 | layer.masksToBounds = true 41 | } 42 | 43 | override func draw(_ rect: CGRect) { 44 | super.draw(rect) 45 | 46 | guard let ctx = UIGraphicsGetCurrentContext() else { return } 47 | 48 | // draw translucent background 49 | ctx.setFillColor(UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.35).cgColor) 50 | ctx.addRect(rect) 51 | ctx.fillPath() 52 | 53 | // draw ☑️ 54 | ctx.setLineCap(.round) 55 | ctx.setLineJoin(.round) 56 | ctx.setLineWidth(strokeWidth) 57 | ctx.setStrokeColor(borderColor.cgColor) 58 | 59 | let perW = rect.width / 10 60 | let perH = rect.height / 10 61 | 62 | ctx.move(to: CGPoint(x: perW * 4, y: perH * 5)) 63 | ctx.addLine(to: CGPoint(x: perW * 5, y: perH * 7)) 64 | 65 | ctx.move(to: CGPoint(x: perW * 5, y: perH * 7)) 66 | ctx.addLine(to: CGPoint(x: perW * 7, y: perH * 4)) 67 | 68 | ctx.strokePath() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/EasyAlbum/Widget/AlbumToast.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumToast.swift 3 | // EasyAlbum 4 | // 5 | // Created by Ray on 2019/4/24. 6 | // Copyright © 2019 Ray. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AlbumToast: UIView { 12 | 13 | private var messageLabel: UILabel! 14 | private weak var navigationVC: UINavigationController? 15 | private var barTintColor: UIColor? 16 | 17 | /// message font size,default = UIFont.systemFont(ofSize: 16.0, weight: .medium) 18 | var font: UIFont = UIFont.systemFont(ofSize: 16.0, weight: .medium) { 19 | didSet { messageLabel.font = font } 20 | } 21 | 22 | /// message,default = nil 23 | var message: String? = nil { 24 | didSet { messageLabel.text = message } 25 | } 26 | 27 | /// mesage color,default = .white 28 | var textColor: UIColor = .white { 29 | didSet { messageLabel.textColor = textColor } 30 | } 31 | 32 | /// message background color,default = .black 33 | var toastBackgroundColor: UIColor = .black { 34 | didSet { backgroundColor = toastBackgroundColor } 35 | } 36 | 37 | /// message auto dismiss,default = true 38 | var autoCancel: Bool = true 39 | 40 | /// animate duration,default:0.25 41 | private var duration: TimeInterval = 0.25 42 | 43 | /// message show duration,default = 2s 44 | private var stayDuration: TimeInterval = 2 45 | 46 | private var timer: Timer? 47 | 48 | convenience init(navigationVC: UINavigationController?, barTintColor: UIColor?) { 49 | self.init(frame: .zero) 50 | self.navigationVC = navigationVC 51 | self.barTintColor = barTintColor 52 | setup() 53 | } 54 | 55 | private func setup() { 56 | messageLabel = UILabel() 57 | messageLabel.textColor = textColor 58 | messageLabel.font = font 59 | messageLabel.numberOfLines = 2 60 | messageLabel.textAlignment = .center 61 | messageLabel.translatesAutoresizingMaskIntoConstraints = false 62 | addSubview(messageLabel) 63 | 64 | // AutoLayout 65 | messageLabel.heightAnchor 66 | .constraint(equalToConstant: 24.0) 67 | .isActive = true 68 | messageLabel.leadingAnchor 69 | .constraint(equalTo: leadingAnchor, constant: 5.0) 70 | .isActive = true 71 | messageLabel.trailingAnchor 72 | .constraint(equalTo: trailingAnchor, constant: -5.0) 73 | .isActive = true 74 | messageLabel.bottomAnchor 75 | .constraint(equalTo: bottomAnchor, constant: -5.0) 76 | .isActive = true 77 | 78 | backgroundColor = toastBackgroundColor 79 | isHidden = true 80 | } 81 | 82 | private func createTimer() { 83 | timer = Timer(timeInterval: stayDuration, 84 | target: self, 85 | selector: #selector(hide(_:)), 86 | userInfo: nil, 87 | repeats: false) 88 | RunLoop.current.add(timer!, forMode: .common) 89 | } 90 | 91 | private func destroyTimer() { 92 | timer?.invalidate() 93 | timer = nil 94 | } 95 | 96 | @objc private func hide(_ timer: Timer) { 97 | hide() 98 | } 99 | 100 | public func show(with message: String = "", autoCancel: Bool = true) { 101 | self.autoCancel = autoCancel 102 | if !message.isEmpty { messageLabel.text = message } 103 | 104 | // Restart 105 | if !isHidden { 106 | destroyTimer() 107 | if autoCancel { 108 | createTimer() 109 | } 110 | return 111 | } 112 | 113 | isHidden.toggle() 114 | navigationVC?.navigationBar.barTintColor = toastBackgroundColor 115 | frame = CGRect(origin: CGPoint(x: 0.0, y: -frame.height), size: frame.size) 116 | 117 | UIView.animate(withDuration: duration, animations: { 118 | self.frame = CGRect(origin: .zero, size: self.frame.size) 119 | }) { (finished) in 120 | if self.autoCancel { self.createTimer() } 121 | } 122 | } 123 | 124 | public func hide() { 125 | if isHidden { return } 126 | 127 | UIView.animate(withDuration: duration, animations: { 128 | self.frame = CGRect(origin: CGPoint(x: 0.0, y: -self.frame.height), size: self.frame.size) 129 | }) { (finished) in 130 | self.navigationVC?.navigationBar.barTintColor = self.barTintColor 131 | self.isHidden.toggle() 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Tests/EasyAlbumTests/EasyAlbumTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import EasyAlbum 3 | 4 | final class EasyAlbumTests: XCTestCase { 5 | 6 | static var allTests = [ 7 | ("test_image_use_bundle", test_image_use_bundle), 8 | ] 9 | 10 | func test_image_use_bundle() { 11 | XCTAssertNotNil(UIImage.bundle(image: .close)) 12 | XCTAssertNotNil(UIImage.bundle(image: .camera)) 13 | XCTAssertNotNil(UIImage.bundle(image: .done)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/EasyAlbumTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(EasyAlbumTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import EasyAlbumTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += EasyAlbumTests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------