├── .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 |
3 |
4 |
5 | [](https://travis-ci.org/ray00178/EasyAlbum)     
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 | 
16 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------