├── .gitignore
├── static
├── sample.gif
└── sample.png
├── Tests
├── LinuxMain.swift
└── PhotoLibraryPickerTests
│ ├── XCTestManifests.swift
│ └── PhotoLibraryPickerTests.swift
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Package.swift
├── README.md
└── Sources
└── PhotoLibraryPicker
└── PhotoLibraryPicker.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/static/sample.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moifort/swiftUI-photo-library-picker/HEAD/static/sample.gif
--------------------------------------------------------------------------------
/static/sample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moifort/swiftUI-photo-library-picker/HEAD/static/sample.png
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import PhotoLibraryPickerTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += PhotoLibraryPickerTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tests/PhotoLibraryPickerTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(PhotoLibraryPickerTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tests/PhotoLibraryPickerTests/PhotoLibraryPickerTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import PhotoLibraryPicker
3 |
4 | final class PhotoLibraryPickerTests: XCTestCase {
5 | func testExample() {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | }
10 |
11 | static var allTests = [
12 | ("testExample", testExample),
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
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: "PhotoLibraryPicker",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
13 | .library(
14 | name: "PhotoLibraryPicker",
15 | targets: ["PhotoLibraryPicker"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
24 | .target(
25 | name: "PhotoLibraryPicker",
26 | dependencies: []),
27 | .testTarget(
28 | name: "PhotoLibraryPickerTests",
29 | dependencies: ["PhotoLibraryPicker"]),
30 | ],
31 | swiftLanguageVersions: [
32 | .version("5.1")
33 | ]
34 | )
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | # Photo Library Picker for SwiftUI
3 |
4 | 
5 | 
6 |
7 | ## Installation with Swift Package Manager
8 |
9 | Swift Package Manager is integrated within Xcode 11:
10 |
11 | 1. File → Swift Packages → Add Package Dependency...
12 | 2. Paste the repository URL: https://github.com/moifort/swiftUI-photo-library-picker.git
13 | 3. Add `NSPhotoLibraryUsageDescription` to `info.plist`
14 |
15 | ## Usage
16 |
17 | ```swift
18 | import SwiftUI
19 | import PhotoLibraryPicker // Add import
20 |
21 | struct ContentView : View {
22 | @State var showActionSheet: Bool = false
23 | @State var pictures = [Picture]()
24 |
25 | var body: some View {
26 | VStack {
27 | Button(action: {self.showActionSheet.toggle()}) {
28 | Image(systemName: "plus")
29 | .padding()
30 | .background(Color.secondary)
31 | .mask(Circle())
32 | }.sheet(isPresented: self.$showActionSheet) {PhotoLibraryPicker(self.$pictures)}
33 | List {
34 | ForEach(pictures) { picture in
35 | picture.toImage() // You can fix the size by default width: 100, height: 100
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
42 |
43 | struct ContentView_Previews: PreviewProvider {
44 | static var previews: some View {
45 | Group {
46 | ContentView().environment(\.colorScheme, .dark)
47 | ContentView()
48 | }
49 |
50 | }
51 | }
52 | ```
53 |
54 | ## Thanks
55 |
56 | * To @dillidon for this [project](https://github.com/dillidon/alerts-and-pickers)
57 |
58 |
--------------------------------------------------------------------------------
/Sources/PhotoLibraryPicker/PhotoLibraryPicker.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Foundation
3 | import UIKit
4 | import Photos
5 | import AudioToolbox
6 |
7 | extension UIApplication {
8 |
9 | class func topViewController(_ viewController: UIViewController? = UIApplication.shared.windows.first?.rootViewController) -> UIViewController? {
10 | if let nav = viewController as? UINavigationController {
11 | return topViewController(nav.visibleViewController)
12 | }
13 | if let tab = viewController as? UITabBarController {
14 | if let selected = tab.selectedViewController {
15 | return topViewController(selected)
16 | }
17 | }
18 | if let presented = viewController?.presentedViewController {
19 | return topViewController(presented)
20 | }
21 |
22 | return viewController
23 | }
24 | }
25 |
26 | extension UIViewController {
27 |
28 | var alertController: UIAlertController? {
29 | guard let alert = UIApplication.topViewController() as? UIAlertController else { return nil }
30 | return alert
31 | }
32 | }
33 |
34 | extension Array {
35 |
36 | @discardableResult
37 | mutating func append(_ newArray: Array) -> CountableRange {
38 | let range = count..<(count + newArray.count)
39 | self += newArray
40 | return range
41 | }
42 |
43 | @discardableResult
44 | mutating func insert(_ newArray: Array, at index: Int) -> CountableRange {
45 | let mIndex = Swift.max(0, index)
46 | let start = Swift.min(count, mIndex)
47 | let end = start + newArray.count
48 |
49 | let left = self[0.. (_ element: T) {
56 | let anotherSelf = self
57 |
58 | removeAll(keepingCapacity: true)
59 |
60 | anotherSelf.each { (index: Int, current: Element) in
61 | if (current as! T) !== element {
62 | self.append(current)
63 | }
64 | }
65 | }
66 |
67 | func each(_ exe: (Int, Element) -> ()) {
68 | for (index, item) in enumerated() {
69 | exe(index, item)
70 | }
71 | }
72 | }
73 |
74 | extension UIColor {
75 |
76 | /// SwifterSwift: https://github.com/SwifterSwift/SwifterSwift
77 | /// Hexadecimal value string (read-only).
78 | public var hexString: String {
79 | let components: [Int] = {
80 | let c = cgColor.components!
81 | let components = c.count == 4 ? c : [c[0], c[0], c[0], c[1]]
82 | return components.map { Int($0 * 255.0) }
83 | }()
84 | return String(format: "#%02X%02X%02X", components[0], components[1], components[2])
85 | }
86 |
87 | /// SwifterSwift: https://github.com/SwifterSwift/SwifterSwift
88 | /// Short hexadecimal value string (read-only, if applicable).
89 | public var shortHexString: String? {
90 | let string = hexString.replacingOccurrences(of: "#", with: "")
91 | let chrs = Array(string)
92 | guard chrs[0] == chrs[1], chrs[2] == chrs[3], chrs[4] == chrs[5] else { return nil }
93 | return "#\(chrs[0])\(chrs[2])\(chrs[4])"
94 | }
95 |
96 | /// Color to Image
97 | func toImage(size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {
98 | let rect:CGRect = CGRect(origin: .zero, size: size)
99 | UIGraphicsBeginImageContextWithOptions(rect.size, true, 0)
100 | self.setFill()
101 | UIRectFill(rect)
102 | let image = UIGraphicsGetImageFromCurrentImageContext()
103 | UIGraphicsEndImageContext()
104 | return image! // was image
105 | }
106 |
107 | /// SwifterSwift: https://github.com/SwifterSwift/SwifterSwift
108 | /// RGB components for a Color (between 0 and 255).
109 | ///
110 | /// UIColor.red.rgbComponents.red -> 255
111 | /// UIColor.green.rgbComponents.green -> 255
112 | /// UIColor.blue.rgbComponents.blue -> 255
113 | ///
114 | public var rgbComponents: (red: Int, green: Int, blue: Int) {
115 | var components: [CGFloat] {
116 | let c = cgColor.components!
117 | if c.count == 4 {
118 | return c
119 | }
120 | return [c[0], c[0], c[0], c[1]]
121 | }
122 | let r = components[0]
123 | let g = components[1]
124 | let b = components[2]
125 | return (red: Int(r * 255.0), green: Int(g * 255.0), blue: Int(b * 255.0))
126 | }
127 |
128 | /// SwifterSwift: https://github.com/SwifterSwift/SwifterSwift
129 | /// RGB components for a Color represented as CGFloat numbers (between 0 and 1)
130 | ///
131 | /// UIColor.red.rgbComponents.red -> 1.0
132 | /// UIColor.green.rgbComponents.green -> 1.0
133 | /// UIColor.blue.rgbComponents.blue -> 1.0
134 | ///
135 | public var cgFloatComponents: (red: CGFloat, green: CGFloat, blue: CGFloat) {
136 | var components: [CGFloat] {
137 | let c = cgColor.components!
138 | if c.count == 4 {
139 | return c
140 | }
141 | return [c[0], c[0], c[0], c[1]]
142 | }
143 | let r = components[0]
144 | let g = components[1]
145 | let b = components[2]
146 | return (red: r, green: g, blue: b)
147 | }
148 |
149 | /// SwifterSwift: https://github.com/SwifterSwift/SwifterSwift
150 | /// Get components of hue, saturation, and brightness, and alpha (read-only).
151 | public var hsbaComponents: (hue: CGFloat, saturation: CGFloat, brightness: CGFloat, alpha: CGFloat) {
152 | var h: CGFloat = 0.0
153 | var s: CGFloat = 0.0
154 | var b: CGFloat = 0.0
155 | var a: CGFloat = 0.0
156 |
157 | self.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
158 | return (hue: h, saturation: s, brightness: b, alpha: a)
159 | }
160 |
161 | /// Random color.
162 | public static var random: UIColor {
163 | let r = Int(arc4random_uniform(255))
164 | let g = Int(arc4random_uniform(255))
165 | let b = Int(arc4random_uniform(255))
166 | return UIColor(red: r, green: g, blue: b)
167 | }
168 | }
169 |
170 | // MARK: - Initializers
171 | public extension UIColor {
172 |
173 | convenience init(hex: Int, alpha: CGFloat) {
174 | let r = CGFloat((hex & 0xFF0000) >> 16)/255
175 | let g = CGFloat((hex & 0xFF00) >> 8)/255
176 | let b = CGFloat(hex & 0xFF)/255
177 | self.init(red: r, green: g, blue: b, alpha: alpha)
178 | }
179 |
180 | convenience init(hex: Int) {
181 | self.init(hex: hex, alpha: 1.0)
182 | }
183 |
184 | /**
185 | Creates an UIColor from HEX String in "#363636" format
186 |
187 | - parameter hexString: HEX String in "#363636" format
188 | - returns: UIColor from HexString
189 | */
190 | convenience init(hexString: String) {
191 |
192 | let hexString: String = (hexString as NSString).trimmingCharacters(in: .whitespacesAndNewlines)
193 | let scanner = Scanner(string: hexString as String)
194 |
195 | if hexString.hasPrefix("#") {
196 | scanner.currentIndex = scanner.string.startIndex
197 | }
198 | var color: UInt64 = 0
199 | scanner.scanHexInt64(&color)
200 |
201 | let mask = 0x000000FF
202 | let r = Int(color >> 16) & mask
203 | let g = Int(color >> 8) & mask
204 | let b = Int(color) & mask
205 |
206 | let red = CGFloat(r) / 255.0
207 | let green = CGFloat(g) / 255.0
208 | let blue = CGFloat(b) / 255.0
209 | self.init(red:red, green:green, blue:blue, alpha:1)
210 | }
211 |
212 | /// Create UIColor from RGB values with optional transparency.
213 | ///
214 | /// - Parameters:
215 | /// - red: red component.
216 | /// - green: green component.
217 | /// - blue: blue component.
218 | /// - transparency: optional transparency value (default is 1)
219 | convenience init(red: Int, green: Int, blue: Int, transparency: CGFloat = 1) {
220 | assert(red >= 0 && red <= 255, "Invalid red component")
221 | assert(green >= 0 && green <= 255, "Invalid green component")
222 | assert(blue >= 0 && blue <= 255, "Invalid blue component")
223 | var trans: CGFloat {
224 | if transparency > 1 {
225 | return 1
226 | } else if transparency < 0 {
227 | return 0
228 | } else {
229 | return transparency
230 | }
231 | }
232 | self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: trans)
233 | }
234 | }
235 |
236 | // MARK: - Properties
237 | public extension UIView {
238 |
239 | /// Size of view.
240 | var size: CGSize {
241 | get {
242 | return self.frame.size
243 | }
244 | set {
245 | self.width = newValue.width
246 | self.height = newValue.height
247 | }
248 | }
249 |
250 | /// Width of view.
251 | var width: CGFloat {
252 | get {
253 | return self.frame.size.width
254 | }
255 | set {
256 | self.frame.size.width = newValue
257 | }
258 | }
259 |
260 | /// Height of view.
261 | var height: CGFloat {
262 | get {
263 | return self.frame.size.height
264 | }
265 | set {
266 | self.frame.size.height = newValue
267 | }
268 | }
269 | }
270 |
271 | @IBDesignable
272 | extension UIView {
273 |
274 | @IBInspectable
275 | /// Should the corner be as circle
276 | public var circleCorner: Bool {
277 | get {
278 | return min(bounds.size.height, bounds.size.width) / 2 == cornerRadius
279 | }
280 | set {
281 | cornerRadius = newValue ? min(bounds.size.height, bounds.size.width) / 2 : cornerRadius
282 | }
283 | }
284 |
285 | @IBInspectable
286 | /// Corner radius of view; also inspectable from Storyboard.
287 | public var cornerRadius: CGFloat {
288 | get {
289 | return layer.cornerRadius
290 | }
291 | set {
292 | layer.cornerRadius = circleCorner ? min(bounds.size.height, bounds.size.width) / 2 : newValue
293 | //abs(CGFloat(Int(newValue * 100)) / 100)
294 | }
295 | }
296 |
297 | @IBInspectable
298 | /// Border color of view; also inspectable from Storyboard.
299 | public var borderColor: UIColor? {
300 | get {
301 | guard let color = layer.borderColor else {
302 | return nil
303 | }
304 | return UIColor(cgColor: color)
305 | }
306 | set {
307 | guard let color = newValue else {
308 | layer.borderColor = nil
309 | return
310 | }
311 | layer.borderColor = color.cgColor
312 | }
313 | }
314 |
315 | @IBInspectable
316 | /// Border width of view; also inspectable from Storyboard.
317 | public var borderWidth: CGFloat {
318 | get {
319 | return layer.borderWidth
320 | }
321 | set {
322 | layer.borderWidth = newValue
323 | }
324 | }
325 |
326 | @IBInspectable
327 | /// Shadow color of view; also inspectable from Storyboard.
328 | public var shadowColor: UIColor? {
329 | get {
330 | guard let color = layer.shadowColor else {
331 | return nil
332 | }
333 | return UIColor(cgColor: color)
334 | }
335 | set {
336 | layer.shadowColor = newValue?.cgColor
337 | }
338 | }
339 |
340 | @IBInspectable
341 | /// Shadow offset of view; also inspectable from Storyboard.
342 | public var shadowOffset: CGSize {
343 | get {
344 | return layer.shadowOffset
345 | }
346 | set {
347 | layer.shadowOffset = newValue
348 | }
349 | }
350 |
351 | @IBInspectable
352 | /// Shadow opacity of view; also inspectable from Storyboard.
353 | public var shadowOpacity: Double {
354 | get {
355 | return Double(layer.shadowOpacity)
356 | }
357 | set {
358 | layer.shadowOpacity = Float(newValue)
359 | }
360 | }
361 |
362 | @IBInspectable
363 | /// Shadow radius of view; also inspectable from Storyboard.
364 | public var shadowRadius: CGFloat {
365 | get {
366 | return layer.shadowRadius
367 | }
368 | set {
369 | layer.shadowRadius = newValue
370 | }
371 | }
372 |
373 | @IBInspectable
374 | /// Shadow path of view; also inspectable from Storyboard.
375 | public var shadowPath: CGPath? {
376 | get {
377 | return layer.shadowPath
378 | }
379 | set {
380 | layer.shadowPath = newValue
381 | }
382 | }
383 |
384 | @IBInspectable
385 | /// Should shadow rasterize of view; also inspectable from Storyboard.
386 | /// cache the rendered shadow so that it doesn't need to be redrawn
387 | public var shadowShouldRasterize: Bool {
388 | get {
389 | return layer.shouldRasterize
390 | }
391 | set {
392 | layer.shouldRasterize = newValue
393 | }
394 | }
395 |
396 | @IBInspectable
397 | /// Should shadow rasterize of view; also inspectable from Storyboard.
398 | /// cache the rendered shadow so that it doesn't need to be redrawn
399 | public var shadowRasterizationScale: CGFloat {
400 | get {
401 | return layer.rasterizationScale
402 | }
403 | set {
404 | layer.rasterizationScale = newValue
405 | }
406 | }
407 |
408 | @IBInspectable
409 | /// Corner radius of view; also inspectable from Storyboard.
410 | public var maskToBounds: Bool {
411 | get {
412 | return layer.masksToBounds
413 | }
414 | set {
415 | layer.masksToBounds = newValue
416 | }
417 | }
418 | }
419 |
420 |
421 | final class CurrencyTableViewCell: UITableViewCell {
422 |
423 | static let identifier = String(describing: CurrencyTableViewCell.self)
424 |
425 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
426 | super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
427 | selectionStyle = .none
428 | backgroundColor = nil
429 | contentView.backgroundColor = nil
430 | }
431 |
432 | required init?(coder aDecoder: NSCoder) {
433 | fatalError("init(coder:) has not been implemented")
434 | }
435 |
436 | override func layoutSubviews() {
437 | super.layoutSubviews()
438 | }
439 |
440 | override func setSelected(_ selected: Bool, animated: Bool) {
441 | super.setSelected(selected, animated: animated)
442 | accessoryType = selected ? .checkmark : .none
443 | }
444 | }
445 |
446 | class ItemWithImage: UICollectionViewCell {
447 |
448 | static let identifier = String(describing: CurrencyTableViewCell.self)
449 |
450 | lazy var imageView: UIImageView = {
451 | $0.backgroundColor = .clear
452 | $0.contentMode = .scaleAspectFill
453 | $0.maskToBounds = true
454 | return $0
455 | }(UIImageView())
456 |
457 | lazy var unselectedCircle: UIView = {
458 | $0.backgroundColor = .clear
459 | $0.borderWidth = 2
460 | $0.borderColor = .white
461 | $0.maskToBounds = false
462 | return $0
463 | }(UIView())
464 |
465 | lazy var selectedCircle: UIView = {
466 | $0.backgroundColor = .clear
467 | $0.borderWidth = 2
468 | $0.borderColor = .white
469 | $0.maskToBounds = false
470 | return $0
471 | }(UIView())
472 |
473 | lazy var selectedPoint: UIView = {
474 | $0.backgroundColor = UIColor(hex: 0x007AFF)
475 | return $0
476 | }(UIView())
477 |
478 | fileprivate let inset: CGFloat = 8
479 |
480 | public required init?(coder aDecoder: NSCoder) {
481 | super.init(coder: aDecoder)
482 | setup()
483 | }
484 |
485 | public override init(frame: CGRect) {
486 | super.init(frame: frame)
487 | setup()
488 | }
489 |
490 | fileprivate func setup() {
491 | backgroundColor = .clear
492 |
493 | let unselected: UIView = UIView()
494 | unselected.addSubview(imageView)
495 | unselected.addSubview(unselectedCircle)
496 | backgroundView = unselected
497 |
498 | let selected: UIView = UIView()
499 | selected.addSubview(selectedCircle)
500 | selected.addSubview(selectedPoint)
501 | selectedBackgroundView = selected
502 | }
503 |
504 | override public func layoutSubviews() {
505 | super.layoutSubviews()
506 | layout()
507 | }
508 |
509 | func layout() {
510 | imageView.frame = contentView.frame
511 | updateAppearance(forCircle: unselectedCircle)
512 | updateAppearance(forCircle: selectedCircle)
513 | updateAppearance(forPoint: selectedPoint)
514 | }
515 |
516 | override func sizeThatFits(_ size: CGSize) -> CGSize {
517 | contentView.size = size
518 | layout()
519 | return size
520 | }
521 |
522 | func updateAppearance(forCircle view: UIView) {
523 | view.frame.size = CGSize(width: 28, height: 28)
524 | view.frame.origin.x = imageView.bounds.width - unselectedCircle.bounds.width - inset
525 | view.frame.origin.y = inset
526 | view.circleCorner = true
527 | view.shadowColor = UIColor.black.withAlphaComponent(0.4)
528 | view.shadowOffset = .zero
529 | view.shadowRadius = 4
530 | view.shadowOpacity = 0.2
531 | view.shadowPath = UIBezierPath(roundedRect: unselectedCircle.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: unselectedCircle.bounds.width / 2, height: unselectedCircle.bounds.width / 2)).cgPath
532 | view.shadowShouldRasterize = true
533 | view.shadowRasterizationScale = UIScreen.main.scale
534 | }
535 |
536 | func updateAppearance(forPoint view: UIView) {
537 | view.frame.size = CGSize(width: unselectedCircle.width - unselectedCircle.borderWidth * 2, height: unselectedCircle.height - unselectedCircle.borderWidth * 2)
538 | view.center = selectedCircle.center
539 | view.circleCorner = true
540 | }
541 | }
542 |
543 | extension UIView {
544 |
545 | func searchVisualEffectsSubview() -> UIVisualEffectView? {
546 | if let visualEffectView = self as? UIVisualEffectView {
547 | return visualEffectView
548 | } else {
549 | for subview in subviews {
550 | if let found = subview.searchVisualEffectsSubview() {
551 | return found
552 | }
553 | }
554 | }
555 | return nil
556 | }
557 |
558 | /// This is the function to get subViews of a view of a particular type
559 | /// https://stackoverflow.com/a/45297466/5321670
560 | func subViews(type : T.Type) -> [T]{
561 | var all = [T]()
562 | for view in self.subviews {
563 | if let aView = view as? T{
564 | all.append(aView)
565 | }
566 | }
567 | return all
568 | }
569 |
570 |
571 | /// This is a function to get subViews of a particular type from view recursively. It would look recursively in all subviews and return back the subviews of the type T
572 | /// https://stackoverflow.com/a/45297466/5321670
573 | func allSubViewsOf(type : T.Type) -> [T]{
574 | var all = [T]()
575 | func getSubview(view: UIView) {
576 | if let aView = view as? T{
577 | all.append(aView)
578 | }
579 | guard view.subviews.count>0 else { return }
580 | view.subviews.forEach{ getSubview(view: $0) }
581 | }
582 | getSubview(view: self)
583 | return all
584 | }
585 | }
586 |
587 | // MARK: - Initializers
588 | extension UIAlertController {
589 |
590 | /// Create new alert view controller.
591 | ///
592 | /// - Parameters:
593 | /// - style: alert controller's style.
594 | /// - title: alert controller's title.
595 | /// - message: alert controller's message (default is nil).
596 | /// - defaultActionButtonTitle: default action button title (default is "OK")
597 | /// - tintColor: alert controller's tint color (default is nil)
598 | convenience init(style: UIAlertController.Style, source: UIView? = nil, title: String? = nil, message: String? = nil, tintColor: UIColor? = nil) {
599 | self.init(title: title, message: message, preferredStyle: style)
600 |
601 | // TODO: for iPad or other views
602 | let isPad: Bool = UIDevice.current.userInterfaceIdiom == .pad
603 | let root = UIApplication.shared.windows.first?.rootViewController?.view
604 |
605 | //self.responds(to: #selector(getter: popoverPresentationController))
606 | if let source = source {
607 | print("----- source")
608 | popoverPresentationController?.sourceView = source
609 | popoverPresentationController?.sourceRect = source.bounds
610 | } else if isPad, let source = root, style == .actionSheet {
611 | print("----- is pad")
612 | popoverPresentationController?.sourceView = source
613 | popoverPresentationController?.sourceRect = CGRect(x: source.bounds.midX, y: source.bounds.midY, width: 0, height: 0)
614 | //popoverPresentationController?.permittedArrowDirections = .down
615 | popoverPresentationController?.permittedArrowDirections = .init(rawValue: 0)
616 | }
617 |
618 | if let color = tintColor {
619 | self.view.tintColor = color
620 | }
621 | }
622 | }
623 |
624 |
625 | // MARK: - Methods
626 | extension UIAlertController {
627 |
628 | /// Present alert view controller in the current view controller.
629 | ///
630 | /// - Parameters:
631 | /// - animated: set true to animate presentation of alert controller (default is true).
632 | /// - vibrate: set true to vibrate the device while presenting the alert (default is false).
633 | /// - completion: an optional completion handler to be called after presenting alert controller (default is nil).
634 | public func show(animated: Bool = true, vibrate: Bool = false, style: UIBlurEffect.Style? = nil, completion: (() -> Void)? = nil) {
635 |
636 | /// TODO: change UIBlurEffectStyle
637 | if let style = style {
638 | for subview in view.allSubViewsOf(type: UIVisualEffectView.self) {
639 | subview.effect = UIBlurEffect(style: style)
640 | }
641 | }
642 |
643 | DispatchQueue.main.async {
644 | UIApplication.shared.windows.first?.rootViewController?.present(self, animated: animated, completion: completion)
645 | if vibrate {
646 | AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
647 | }
648 | }
649 | }
650 |
651 | /// Add an action to Alert
652 | ///
653 | /// - Parameters:
654 | /// - title: action title
655 | /// - style: action style (default is UIAlertActionStyle.default)
656 | /// - isEnabled: isEnabled status for action (default is true)
657 | /// - handler: optional action handler to be called when button is tapped (default is nil)
658 | func addAction(image: UIImage? = nil, title: String, color: UIColor? = nil, style: UIAlertAction.Style = .default, isEnabled: Bool = true, handler: ((UIAlertAction) -> Void)? = nil) {
659 | //let isPad: Bool = UIDevice.current.userInterfaceIdiom == .pad
660 | //let action = UIAlertAction(title: title, style: isPad && style == .cancel ? .default : style, handler: handler)
661 | let action = UIAlertAction(title: title, style: style, handler: handler)
662 | action.isEnabled = isEnabled
663 |
664 | // button image
665 | if let image = image {
666 | action.setValue(image, forKey: "image")
667 | }
668 |
669 | // button title color
670 | if let color = color {
671 | action.setValue(color, forKey: "titleTextColor")
672 | }
673 |
674 | addAction(action)
675 | }
676 |
677 | /// Set alert's title, font and color
678 | ///
679 | /// - Parameters:
680 | /// - title: alert title
681 | /// - font: alert title font
682 | /// - color: alert title color
683 | func set(title: String?, font: UIFont, color: UIColor) {
684 | if title != nil {
685 | self.title = title
686 | }
687 | setTitle(font: font, color: color)
688 | }
689 |
690 | func setTitle(font: UIFont, color: UIColor) {
691 | guard let title = self.title else { return }
692 | let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: color]
693 | let attributedTitle = NSMutableAttributedString(string: title, attributes: attributes)
694 | setValue(attributedTitle, forKey: "attributedTitle")
695 | }
696 |
697 | /// Set alert's message, font and color
698 | ///
699 | /// - Parameters:
700 | /// - message: alert message
701 | /// - font: alert message font
702 | /// - color: alert message color
703 | func set(message: String?, font: UIFont, color: UIColor) {
704 | if message != nil {
705 | self.message = message
706 | }
707 | setMessage(font: font, color: color)
708 | }
709 |
710 | func setMessage(font: UIFont, color: UIColor) {
711 | guard let message = self.message else { return }
712 | let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: color]
713 | let attributedMessage = NSMutableAttributedString(string: message, attributes: attributes)
714 | setValue(attributedMessage, forKey: "attributedMessage")
715 | }
716 |
717 | /// Set alert's content viewController
718 | ///
719 | /// - Parameters:
720 | /// - vc: ViewController
721 | /// - height: height of content viewController
722 | func set(vc: UIViewController?, width: CGFloat? = nil, height: CGFloat? = nil) {
723 | guard let vc = vc else { return }
724 | setValue(vc, forKey: "contentViewController")
725 | if let height = height {
726 | vc.preferredContentSize.height = height
727 | preferredContentSize.height = height
728 | }
729 | }
730 | }
731 |
732 |
733 | public struct Assets {
734 |
735 | /// Requests access to the user's contacts
736 | ///
737 | /// - Parameter requestGranted: Result as Bool
738 | public static func requestAccess(_ requestGranted: @escaping (PHAuthorizationStatus) -> ()) {
739 | PHPhotoLibrary.requestAuthorization { status in
740 | requestGranted(status)
741 | }
742 | }
743 |
744 | /// Result Enum
745 | ///
746 | /// - Success: Returns Array of PHAsset
747 | /// - Error: Returns error
748 | public enum FetchResults {
749 | case success(response: [PHAsset])
750 | case error(error: Error)
751 | }
752 |
753 | public static func fetch(_ completion: @escaping (FetchResults) -> Void) {
754 | guard PHPhotoLibrary.authorizationStatus() == .authorized else {
755 | let error: NSError = NSError(domain: "PhotoLibrary Error", code: 1, userInfo: [NSLocalizedDescriptionKey: "No PhotoLibrary Access"])
756 | completion(FetchResults.error(error: error))
757 | return
758 | }
759 |
760 | DispatchQueue.global(qos: .userInitiated).async {
761 | let fetchResult = PHAsset.fetchAssets(with: .image, options: PHFetchOptions())
762 |
763 | if fetchResult.count > 0 {
764 | var assets = [PHAsset]()
765 | fetchResult.enumerateObjects { object, _, _ in
766 | assets.insert(object, at: 0)
767 | }
768 |
769 | DispatchQueue.main.async {
770 | completion(FetchResults.success(response: assets))
771 | }
772 | }
773 | }
774 | }
775 |
776 | /// Result Enum
777 | ///
778 | /// - Success: Returns UIImage
779 | /// - Error: Returns error
780 | public enum ResolveResult {
781 | case success(response: UIImage?)
782 | case error(error: Error)
783 | }
784 |
785 | public static func resolve(asset: PHAsset, size: CGSize = PHImageManagerMaximumSize, completion: @escaping (_ image: UIImage?) -> Void) {
786 | let imageManager = PHImageManager.default()
787 |
788 | let requestOptions = PHImageRequestOptions()
789 | requestOptions.deliveryMode = .highQualityFormat
790 | requestOptions.resizeMode = .exact
791 | requestOptions.isNetworkAccessAllowed = true
792 |
793 | imageManager.requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: requestOptions) { image, info in
794 | if let info = info, info["PHImageFileUTIKey"] == nil {
795 | DispatchQueue.main.async {
796 | completion(image)
797 | }
798 | }
799 | }
800 | }
801 |
802 | /// Result Enum
803 | ///
804 | /// - Success: Returns Array of UIImage
805 | /// - Error: Returns error
806 | public enum ResolveResults {
807 | case success(response: [UIImage])
808 | case error(error: Error)
809 | }
810 |
811 | public static func resolve(assets: [PHAsset], size: CGSize = CGSize(width: 720, height: 1280), completion: @escaping (_ images: [UIImage]) -> Void) -> [UIImage] {
812 | let imageManager = PHImageManager.default()
813 | let requestOptions = PHImageRequestOptions()
814 | requestOptions.isSynchronous = true
815 |
816 | var images = [UIImage]()
817 | for asset in assets {
818 | imageManager.requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: requestOptions) { image, _ in
819 | if let image = image {
820 | images.append(image)
821 | }
822 | }
823 | }
824 |
825 | DispatchQueue.main.async {
826 | completion(images)
827 | }
828 |
829 | return images
830 | }
831 | }
832 |
833 | extension UIAlertController {
834 |
835 | /// Add PhotoLibrary Picker
836 | ///
837 | /// - Parameters:
838 | /// - flow: scroll direction
839 | /// - pagging: pagging
840 | /// - images: for content to select
841 | /// - selection: type and action for selection of image/images
842 |
843 | func addPhotoLibraryPicker(flow: UICollectionView.ScrollDirection, paging: Bool, selection: PhotoLibraryPickerViewController.Selection) {
844 | let selection: PhotoLibraryPickerViewController.Selection = selection
845 | var asset: PHAsset?
846 | var assets: [PHAsset] = []
847 |
848 | let buttonAdd = UIAlertAction(title: "Add", style: .default) { action in
849 | switch selection {
850 |
851 | case .single(let action):
852 | action?(asset)
853 |
854 | case .multiple(let action):
855 | action?(assets)
856 | }
857 | }
858 | buttonAdd.isEnabled = false
859 |
860 | let vc = PhotoLibraryPickerViewController(flow: flow, paging: paging, selection: {
861 | switch selection {
862 | case .single(_):
863 | return .single(action: { new in
864 | buttonAdd.isEnabled = new != nil
865 | asset = new
866 | })
867 | case .multiple(_):
868 | return .multiple(action: { new in
869 | buttonAdd.isEnabled = new.count > 0
870 | assets = new
871 | })
872 | }
873 | }())
874 |
875 | if UIDevice.current.userInterfaceIdiom == .pad {
876 | vc.preferredContentSize.height = vc.preferredSize.height * 0.9
877 | vc.preferredContentSize.width = vc.preferredSize.width * 0.9
878 | } else {
879 | vc.preferredContentSize.height = vc.preferredSize.height
880 | }
881 |
882 | addAction(buttonAdd)
883 | set(vc: vc)
884 | }
885 | }
886 |
887 | final class PhotoLibraryPickerViewController: UIViewController {
888 |
889 | public typealias SingleSelection = (PHAsset?) -> Swift.Void
890 | public typealias MultipleSelection = ([PHAsset]) -> Swift.Void
891 |
892 | public enum Selection {
893 | case single(action: SingleSelection?)
894 | case multiple(action: MultipleSelection?)
895 | }
896 |
897 | // MARK: UI Metrics
898 |
899 | var preferredSize: CGSize {
900 | return UIScreen.main.bounds.size
901 | }
902 |
903 | var columns: CGFloat {
904 | switch layout.scrollDirection {
905 | case .vertical: return UIDevice.current.userInterfaceIdiom == .pad ? 3 : 2
906 | case .horizontal: return 1
907 | default:
908 | return UIDevice.current.userInterfaceIdiom == .pad ? 3 : 2
909 | }
910 | }
911 |
912 | var itemSize: CGSize {
913 | switch layout.scrollDirection {
914 | case .vertical:
915 | return CGSize(width: view.bounds.width / columns, height: view.bounds.width / columns)
916 | case .horizontal:
917 | return CGSize(width: view.bounds.width, height: view.bounds.height / columns)
918 | default:
919 | return CGSize(width: view.bounds.width / columns, height: view.bounds.width / columns)
920 | }
921 | }
922 |
923 | // MARK: Properties
924 |
925 | fileprivate lazy var collectionView: UICollectionView = { [unowned self] in
926 | $0.dataSource = self
927 | $0.delegate = self
928 | $0.register(ItemWithImage.self, forCellWithReuseIdentifier: String(describing: ItemWithImage.self))
929 | $0.showsVerticalScrollIndicator = false
930 | $0.showsHorizontalScrollIndicator = false
931 | $0.decelerationRate = UIScrollView.DecelerationRate.fast
932 | $0.contentInsetAdjustmentBehavior = .always
933 | $0.bounces = true
934 | $0.backgroundColor = .clear
935 | $0.maskToBounds = false
936 | $0.clipsToBounds = false
937 | return $0
938 | }(UICollectionView(frame: .zero, collectionViewLayout: layout))
939 |
940 | fileprivate lazy var layout: UICollectionViewFlowLayout = {
941 | $0.minimumInteritemSpacing = 0
942 | $0.minimumLineSpacing = 0
943 | $0.sectionInset = .zero
944 | return $0
945 | }(UICollectionViewFlowLayout())
946 |
947 | fileprivate var selection: Selection?
948 | fileprivate var assets: [PHAsset] = []
949 | fileprivate var selectedAssets: [PHAsset] = []
950 |
951 | // MARK: Initialize
952 |
953 | required public init(flow: UICollectionView.ScrollDirection, paging: Bool, selection: Selection) {
954 | super.init(nibName: nil, bundle: nil)
955 |
956 | self.selection = selection
957 | self.layout.scrollDirection = flow
958 |
959 | self.collectionView.isPagingEnabled = paging
960 |
961 | switch selection {
962 |
963 | case .single(_):
964 | collectionView.allowsSelection = true
965 | case .multiple(_):
966 | collectionView.allowsMultipleSelection = true
967 | }
968 | }
969 |
970 | required init?(coder aDecoder: NSCoder) {
971 | fatalError("init(coder:) has not been implemented")
972 | }
973 |
974 | deinit {
975 | print("has deinitialized")
976 | }
977 |
978 | override func loadView() {
979 | view = collectionView
980 | }
981 |
982 | override func viewDidLoad() {
983 | super.viewDidLoad()
984 | updatePhotos()
985 | }
986 |
987 | func updatePhotos() {
988 | checkStatus { [unowned self] assets in
989 | self.assets.removeAll()
990 | self.assets.append(contentsOf: assets)
991 | self.collectionView.reloadData()
992 | }
993 | }
994 |
995 | func checkStatus(completionHandler: @escaping ([PHAsset]) -> ()) {
996 | switch PHPhotoLibrary.authorizationStatus() {
997 |
998 | case .notDetermined:
999 | /// This case means the user is prompted for the first time for allowing contacts
1000 | Assets.requestAccess { [unowned self] status in
1001 | self.checkStatus(completionHandler: completionHandler)
1002 | }
1003 |
1004 | case .authorized:
1005 | /// Authorization granted by user for this app.
1006 | DispatchQueue.main.async {
1007 | self.fetchPhotos(completionHandler: completionHandler)
1008 | }
1009 |
1010 | case .denied, .restricted:
1011 | /// User has denied the current app to access the contacts.
1012 | let productName = Bundle.main.infoDictionary!["CFBundleName"]!
1013 | let alert = UIAlertController(style: .alert, title: "Permission denied", message: "\(productName) does not have access to contacts. Please, allow the application to access to your photo library.")
1014 | alert.addAction(title: "Settings", style: .destructive) { action in
1015 | if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
1016 | UIApplication.shared.open(settingsURL)
1017 | }
1018 | }
1019 | alert.addAction(title: "OK", style: .cancel) { [unowned self] action in
1020 | self.alertController?.dismiss(animated: true)
1021 | }
1022 | alert.show()
1023 | default:
1024 | break
1025 | }
1026 | }
1027 |
1028 | func fetchPhotos(completionHandler: @escaping ([PHAsset]) -> ()) {
1029 | Assets.fetch { [unowned self] result in
1030 | switch result {
1031 |
1032 | case .success(let assets):
1033 | completionHandler(assets)
1034 |
1035 | case .error(let error):
1036 | let alert = UIAlertController(style: .alert, title: "Error", message: error.localizedDescription)
1037 | alert.addAction(title: "OK") { [unowned self] action in
1038 | self.alertController?.dismiss(animated: true)
1039 | }
1040 | alert.show()
1041 | }
1042 | }
1043 | }
1044 | }
1045 |
1046 | // MARK: - CollectionViewDelegate
1047 |
1048 | extension PhotoLibraryPickerViewController: UICollectionViewDelegate {
1049 |
1050 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
1051 | let asset = assets[indexPath.item]
1052 | switch selection {
1053 |
1054 | case .single(let action)?:
1055 | action?(asset)
1056 |
1057 | case .multiple(let action)?:
1058 | selectedAssets.contains(asset)
1059 | ? selectedAssets.remove(asset)
1060 | : selectedAssets.append(asset)
1061 | action?(selectedAssets)
1062 |
1063 | case .none: break }
1064 | }
1065 |
1066 | func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
1067 | let asset = assets[indexPath.item]
1068 | switch selection {
1069 | case .multiple(let action)?:
1070 | selectedAssets.contains(asset)
1071 | ? selectedAssets.remove(asset)
1072 | : selectedAssets.append(asset)
1073 | action?(selectedAssets)
1074 | default: break }
1075 | }
1076 | }
1077 |
1078 | // MARK: - CollectionViewDataSource
1079 |
1080 | extension PhotoLibraryPickerViewController: UICollectionViewDataSource {
1081 |
1082 | func numberOfSections(in collectionView: UICollectionView) -> Int {
1083 | return 1
1084 | }
1085 |
1086 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
1087 | return assets.count
1088 | }
1089 |
1090 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
1091 | guard let item = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ItemWithImage.self), for: indexPath) as? ItemWithImage else { return UICollectionViewCell() }
1092 | let asset = assets[indexPath.item]
1093 | Assets.resolve(asset: asset, size: item.bounds.size) { new in
1094 | item.imageView.image = new
1095 | }
1096 | return item
1097 | }
1098 | }
1099 |
1100 | // MARK: - CollectionViewDelegateFlowLayout
1101 |
1102 | extension PhotoLibraryPickerViewController: UICollectionViewDelegateFlowLayout {
1103 |
1104 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
1105 | return itemSize
1106 | }
1107 | }
1108 |
1109 | struct ViewControllerWrapper: UIViewControllerRepresentable {
1110 | typealias UIViewControllerType = PhotoLibraryPickerViewController
1111 | @Binding var assets : [PHAsset]
1112 |
1113 |
1114 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> ViewControllerWrapper.UIViewControllerType {
1115 | return PhotoLibraryPickerViewController(flow: .vertical, paging: false, selection: .multiple{ assets in self.assets = assets })
1116 | }
1117 |
1118 |
1119 | func updateUIViewController(_ uiViewController: ViewControllerWrapper.UIViewControllerType, context: UIViewControllerRepresentableContext) {
1120 | //
1121 | }
1122 | }
1123 |
1124 |
1125 | @available(iOS 13.0, *)
1126 | public struct PhotoLibraryPicker: View {
1127 | @Environment(\.presentationMode) var presentationMode
1128 | @Binding var images : [Picture]
1129 | @State var assets = [PHAsset]()
1130 |
1131 | public init(_ images: Binding<[Picture]>) {
1132 | self._images = images
1133 | }
1134 |
1135 | var saveButton: some View {
1136 | Button(action: {
1137 | self.images = self.assets.map { Picture(asset: $0) }
1138 | self.presentationMode.wrappedValue.dismiss()
1139 | }, label: { Text("Save") }).disabled(assets.isEmpty)
1140 | }
1141 |
1142 | var cancelButton: some View {
1143 | Button(action: {
1144 | self.images = [Picture]()
1145 | self.presentationMode.wrappedValue.dismiss() },
1146 | label: { Text("Cancel") })
1147 | }
1148 |
1149 | public var body: some View {
1150 | NavigationView {
1151 | ViewControllerWrapper(assets: $assets)
1152 | .navigationBarTitle(Text("Photos"), displayMode: .inline)
1153 | .navigationBarItems(leading: cancelButton, trailing: saveButton)
1154 | }
1155 | }
1156 | }
1157 |
1158 | @available(iOS 13.0, *)
1159 | public struct Picture : Identifiable {
1160 | public let id = UUID()
1161 | public let asset: PHAsset
1162 |
1163 | public func toImage(width: Int = 100, height: Int = 100, mode: PHImageContentMode = .aspectFit) -> Image {
1164 | let manager = PHImageManager.default()
1165 | let option = PHImageRequestOptions()
1166 | var image = UIImage()
1167 | option.isSynchronous = true
1168 | manager.requestImage(for: asset, targetSize: CGSize(width: width, height: height), contentMode: mode, options: option, resultHandler: {(result, info)->Void in
1169 | image = result!
1170 | })
1171 | return Image(uiImage: image)
1172 | }
1173 | }
1174 |
1175 | struct PhotoLibraryPicker_Previews: PreviewProvider {
1176 | static var previews: some View {
1177 | PhotoLibraryPicker(.constant([Picture]()))
1178 | }
1179 | }
1180 |
1181 |
1182 |
--------------------------------------------------------------------------------