Last active
January 12, 2021 06:56
-
-
Save NeilsUltimateLab/e158df6a1219505ecb03509b435e17cb to your computer and use it in GitHub Desktop.
UIImagePicker protocol in SwiftUI with permission checks.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AppError.swift | |
// ImagePickerDisplayer | |
// | |
// Created by Neil on 03/12/20. | |
// | |
import Foundation | |
enum AppError: Equatable, Error { | |
case authentication(String) | |
case message(String) | |
case canNotParse | |
case somethingWentWrong | |
} | |
extension AppError { | |
var isAuthentication: Bool { | |
switch self { | |
case .authentication: | |
return true | |
default: | |
return false | |
} | |
} | |
var title: String? { | |
switch self { | |
case .authentication: | |
return "Authentication Required" | |
case .message: | |
return nil | |
case .canNotParse: | |
return "Oops" | |
case .somethingWentWrong: | |
return "Oops" | |
} | |
} | |
var message: String? { | |
switch self { | |
case .authentication(let message): | |
return message | |
case .message(let message): | |
return message | |
case .canNotParse: | |
return "Something went wrong from our side." | |
case .somethingWentWrong: | |
return "Something went wrong from our side." | |
} | |
} | |
} | |
#if canImport(SwiftUI) | |
import SwiftUI | |
#if canImport(SwiftUI) | |
import SwiftUI | |
extension AppError { | |
func alert(primaryButton: String = "Ok", primaryAction: @escaping (()->Void) = {}, secondaryButton: String = "Cancel", secondaryAction: @escaping (()->Void) = {}) -> Alert { | |
Alert( | |
title: Text(self.title ?? ""), | |
message: Text(self.message ?? ""), | |
primaryButton: .default(Text(primaryButton), action: primaryAction), | |
secondaryButton: Alert.Button.cancel(Text(secondaryButton), action: secondaryAction) | |
) | |
} | |
} | |
#endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import SwiftUI | |
// MARK: - ImagePicker | |
struct ImagePicker: UIViewControllerRepresentable { | |
var sourceType: UIImagePickerController.SourceType | |
var onSelection: ((Result<URL, AppError>)->Void)? | |
typealias UIViewControllerType = UIImagePickerController | |
func makeUIViewController(context: Context) -> UIImagePickerController { | |
let controller = UIImagePickerController() | |
controller.delegate = context.coordinator | |
controller.sourceType = self.sourceType | |
return controller | |
} | |
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { | |
} | |
class Coordinator: NSObject { | |
var onSelection: ((Result<URL, AppError>)->Void)? | |
init(onSelection: ((Result<URL, AppError>) -> Void)?) { | |
self.onSelection = onSelection | |
} | |
} | |
func makeCoordinator() -> Coordinator { | |
Coordinator(onSelection: self.onSelection) | |
} | |
} | |
// MARK: - UIImagePickerControllerDelegate | |
extension ImagePicker.Coordinator: UIImagePickerControllerDelegate, UINavigationControllerDelegate { | |
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { | |
picker.dismiss(animated: true, completion: nil) | |
} | |
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { | |
if let imageURL = info[.imageURL] as? URL { | |
self.onSelection?(.success(imageURL)) | |
} else { | |
self.onSelection?(.failure(.message("Something went wrong"))) | |
} | |
picker.dismiss(animated: true, completion: nil) | |
} | |
} | |
// MARK: - Sheet Modifier | |
struct ImagePickerSheetModifier: ViewModifier, ImagePickerPermissionRequesting { | |
var title: String = "Select Image" | |
@Binding var isPresented: Bool | |
var onResult: ((Result<URL, AppError>)->Void)? | |
@State private var showingAlert: Bool = false | |
@State private var alert: Alert! | |
@State private var isSheetPresented: Bool = false | |
@State private var sourceType: UIImagePickerController.SourceType = .photoLibrary { | |
didSet { | |
self.checkPermission(for: sourceType) | |
} | |
} | |
func body(content: Content) -> some View { | |
content | |
.actionSheet(isPresented: $isPresented, content: { | |
ActionSheet(title: Text(title), message: nil, buttons: buttons) | |
}) | |
.sheet(isPresented: $isSheetPresented, content: { | |
ImagePicker(sourceType: self.sourceType, onSelection: self.onResult) | |
}) | |
.alert(isPresented: $showingAlert, content: { | |
alert | |
}) | |
} | |
var buttons: [ActionSheet.Button] { | |
var buttons: [ActionSheet.Button] = [.cancel()] | |
if UIImagePickerController.isSourceTypeAvailable(.camera) { | |
buttons.append( | |
.default(Text("Camera"), action: { | |
self.sourceType = .camera | |
}) | |
) | |
} | |
if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { | |
buttons.append( | |
.default(Text("Photo Library"), action: { | |
self.sourceType = .photoLibrary | |
}) | |
) | |
} | |
return buttons | |
} | |
private func checkPermission(for sourceType: UIImagePickerController.SourceType) { | |
switch sourceType { | |
case .camera: | |
self.cameraAccessPermissionCheck { (success) in | |
if success { | |
self.isSheetPresented.toggle() | |
} else { | |
self.alert = self.alert(library: "Camera", feature: "Camera", action: "Turn on the Switch") | |
self.showingAlert.toggle() | |
} | |
} | |
case .photoLibrary: | |
self.photosAccessPermissionCheck { (success) in | |
if success { | |
self.isSheetPresented.toggle() | |
} else { | |
self.alert = self.alert(library: "Photos", feature: "Photo Library", action: "Select Photos") | |
self.showingAlert.toggle() | |
} | |
} | |
case .savedPhotosAlbum: | |
break | |
@unknown default: | |
break | |
} | |
} | |
} | |
extension View { | |
func imagePicker(title: String = "Select Image", isPresented: Binding<Bool>, onSelection: @escaping (Result<URL, AppError>)->Void) -> some View { | |
self.modifier(ImagePickerSheetModifier(title: title, isPresented: isPresented, onResult: onSelection)) | |
} | |
} | |
extension ImagePickerPermissionRequesting where Self: ViewModifier { | |
func alert(library: String, feature: String, action: String) -> Alert { | |
let appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? "App" | |
let title = "\"\(appName)\" Would Like to Access the \(library)" | |
let message = "Please enable \(library) access from Settings > \(appName) > \(feature) to \(action)" | |
return Alert( | |
title: Text(title), | |
message: Text(message), | |
primaryButton: .default(Text("Open Settings"), action: { UIApplication.shared.openSettings() }), | |
secondaryButton: .cancel() | |
) | |
} | |
} | |
protocol ImagePickerDisplaying: ImagePickerPermissionRequesting { | |
func pickerAction(sourceType : UIImagePickerController.SourceType) | |
func alertForPermissionChange(forFeature feature: String, library: String, action: String) | |
} | |
extension ImagePickerPermissionRequesting where Self: UIViewController { | |
func alertForPermissionChange(forFeature feature: String, library: String, action: String) { | |
let settingsAction = UIAlertAction(title: "Open Settings", style: .default) { (_) in | |
UIApplication.shared.openSettings() | |
} | |
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) | |
// Please enable camera access from Settings > reiwa.com > Camera to take photos | |
let appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? "App" | |
let alert = UIAlertController( | |
title: "\"\(appName)\" Would Like to Access the \(library)", | |
message: "Please enable \(library) access from Settings > \(appName) > \(feature) to \(action) photos", | |
preferredStyle: .alert) | |
alert.addAction(settingsAction) | |
alert.addAction(cancelAction) | |
self.present(alert, animated: true, completion: nil) | |
} | |
} | |
// MARK: - UIApp Open Settings | |
extension UIApplication { | |
func openSettings() { | |
let urlString = UIApplication.openSettingsURLString | |
guard let url = URL(string: urlString) else { return } | |
guard UIApplication.shared.canOpenURL(url) else { return } | |
UIApplication.shared.open(url, options: [:], completionHandler: nil) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct UsageView: View { | |
@State private var isPickerPresented: Bool = false | |
@State private var selectedImageURL: URL? | |
@State private var pickerErrorAlert: Alert! | |
@State private var isErrorPresented: Bool = false | |
var body: some View { | |
VStack { | |
if let selectedImageURL = self.selectedImageURL, let image = UIImage(contentsOfFile: selectedImageURL.path) { | |
Image(uiImage: image) | |
.resizable() | |
.scaledToFill() | |
.clipShape(Circle()) | |
.frame(width: 100, height: 100) | |
} | |
Text("Open Picker") | |
.padding() | |
.onTapGesture { | |
isPickerPresented.toggle() | |
} | |
.imagePicker(isPresented: $isPickerPresented) { (result) in | |
self.handleImageSelection(result: result) | |
} | |
.alert(isPresented: $isErrorPresented, content: { | |
pickerErrorAlert | |
}) | |
} | |
} | |
private func handleImageSelection(result: Result<URL, AppError>) { | |
switch result { | |
case .success(let imageURL): | |
self.selectedImageURL = imageURL | |
case .failure(let error): | |
self.pickerErrorAlert = error.alert() | |
self.isErrorPresented.toggle() | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment