Skip to content

Instantly share code, notes, and snippets.

@NeilsUltimateLab
Last active January 12, 2021 06:56
Show Gist options
  • Save NeilsUltimateLab/e158df6a1219505ecb03509b435e17cb to your computer and use it in GitHub Desktop.
Save NeilsUltimateLab/e158df6a1219505ecb03509b435e17cb to your computer and use it in GitHub Desktop.
UIImagePicker protocol in SwiftUI with permission checks.
//
// 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
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)
}
}
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