In my app I want to ask for camera access when the user pressed a camera button, which would take him to an AVFoundation based camera live preview.
That view is presented using a "present modally segue".
So in my ViewController I currently override the shouldPerformSegue() and return true if the user gives permission or has granted it already, otherwise false.
If the user didn't grant access I am showing an Alert in which he can go to settings to change the permission. That is done in showPermissionInfo().
My problem is, that AVCaptureDevice.requestAccess is called asynchronously and thus hasCameraPermission is not set to true before I'm checking for it.
Is there a way to call these restriction accesses in a blocking way?
Thank you!
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
if identifier == "Modally_ToCameraViewController"
{
var hasCameraPermission = false
if AVCaptureDevice.authorizationStatus(for: .video) == .authorized
{
hasCameraPermission = true
} else {
AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
if granted {
hasCameraPermission = true
} else {
hasCameraPermission = false
}
})
}
if(!hasCameraPermission){
showPermissionInfo()
}
return hasCameraPermission
}
return true
}
One easy solution would be to create a semaphore and wait on it until the completion closure is called. semaphore.wait will block the current thread until semaphore.signal is called.
let semaphore = DispatchSemaphore(value: 0)
AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
if granted {
hasCameraPermission = true
} else {
hasCameraPermission = false
}
semaphore.signal()
})
semaphore.wait()
Related
I use new API for requesting of access for an assets library of a user:
PHPhotoLibrary.requestAuthorization(for: .readWrite) {
[weak self] (status:PHAuthorizationStatus) in
print("called");
}
iOS displays alert with access levels:
If the user selects Select Photos..., iOS will show a picker:
If user swipes down on a title of the picker, the photos library will not call the handler block.
Implementation of the photoLibraryDidChange
func photoLibraryDidChange(_ changeInstance: (PHChange)) {
guard let fetchResult = self.fetchResult else {
return
}
if changeInstance.changeDetails(for: fetchResult) != nil {
DispatchQueue.main.async {
self.delegate?.photosLibraryChanged(self)
}
}
}
Does someone faced with this situation? And how I can fix it?
Example: https://github.com/K-Be/NewInPhotoKitInIOS14
I have a method with a completion that returns a bool. But I cant figure out how to get around this error.
Error
UISwitch.isOn must be used from main thread only
Button Action
#IBAction func notificationSwitch(_ sender: Any) {
LocalNotification().checkEnabled(completion: { (success) -> Void in
// When download completes,control flow goes here.
if success == false{
print("Cant Turn On")
self.notificationToggle.isOn = false
} else {
print("Can Turn On")
if self.notificationToggle.isOn == true {
self.notificationToggle.isOn = false
} else {
self.notificationToggle.isOn = true
}
}
})
}
also already tried wrapping the LocalNotifications().... in DispatchQueue.main.async but still get the same error
You're almost there. It is not the checkEnabled that needs to be wrapped in the call to get onto the main thread, but the stuff "inside" it:
LocalNotification().checkEnabled(completion: { (success) -> Void in
DispatchQueue.main.async {
if success == false {
I have a split view controller with the top view controller set to a table view controller that is to display a list of playlists for selection. The first time the app is loaded it asks for music access permission. Answering yes does give it permission but the table view displays no playlists. I end up having to kill the app and run it again. Am I asking for music library permission the wrong place? It is in that top view controller's viewWillAppear and store the playlists I'm using (since some are screened out) in a class of playlists.
override func viewWillAppear(_ animated: Bool) {
self.clearsSelectionOnViewWillAppear = self.splitViewController!.isCollapsed
super.viewWillAppear(animated)
checkMediaAccessAndSetup()
}
func checkMediaAccessAndSetup() {
let authorizationStatus = MPMediaLibrary.authorizationStatus()
switch authorizationStatus {
case .notDetermined:
// Show the permission prompt.
MPMediaLibrary.requestAuthorization({[weak self] (newAuthorizationStatus: MPMediaLibraryAuthorizationStatus) in
// Try again after the prompt is dismissed.
self?.checkMediaAccessAndSetup()
})
case .denied, .restricted:
// Do not use MPMediaQuery.
return
default:
// Proceed as usual.
break
}
// Do stuff with MPMediaQuery
self.setupPlaylistStore()
tableView.reloadData()
}
The chief problems with your code are
You are completely failing to grapple with the fact that the requestAuthorization completion function is called on a background thread. You need to step out to the main thread to do work on the interface.
You have omitted the all-important .authorized case. When you have work to do that depends upon your authorization status, you must do it now if you are authorized, but after authorization if you are not determined.
Thus, this is the correct scheme for a coherent authorization check (where f() is the thing you always want to do if you can):
let status = MPMediaLibrary.authorizationStatus()
switch status {
case .authorized:
f()
case .notDetermined:
MPMediaLibrary.requestAuthorization() { status in
if status == .authorized {
DispatchQueue.main.async {
f()
}
}
}
// ...
}
If you abstract this code into a utility method, where f can be anything, you can do this everywhere in your app where authorization might be necessary — not merely at startup.
Thanks for the comments, it gave me clues of the multi-threading that was going on and I was able to fix it with a timer call if there were no playlists in the class to keep on checking and reload the table data.
override func viewWillAppear(_ animated: Bool) {
self.clearsSelectionOnViewWillAppear = self.splitViewController!.isCollapsed
super.viewWillAppear(animated)
checkMediaAccess()
self.setupPlaylistStore()
tableView.reloadData()
if store.allPlaylists.count < 1 {
playlistTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.playlistTimerCall), userInfo: nil, repeats: true)
}
}
#objc func playlistTimerCall() {
self.setupPlaylistStore()
if store.allPlaylists.count > 1 {
tableView.reloadData()
playlistTimer?.invalidate()
}
}
func checkMediaAccess() {
let authorizationStatus = MPMediaLibrary.authorizationStatus()
switch authorizationStatus {
case .notDetermined:
// Show the permission prompt.
MPMediaLibrary.requestAuthorization({[weak self] (newAuthorizationStatus: MPMediaLibraryAuthorizationStatus) in
// Try again after the prompt is dismissed.
self?.checkMediaAccess()
})
case .denied, .restricted:
// Do not use MPMediaQuery.
return
default:
// Proceed as usual.
break
}
}
func setupPlaylistStore() {
// purge store
store.clearAllPlaylists()
// create a query of media items in playlist
let myPlayListsQuery = MPMediaQuery.playlists()
if myPlayListsQuery.collections != nil {
playlists = myPlayListsQuery.collections!
}
// add playlists to MyPlaylist(s)
if playlists.count > 0 {
for index in 0...playlists.count - 1 {
let playlist = playlists[index]
store.addPlaylist(playlist: playlist as! MPMediaPlaylist)
}
}
var toBeRemoved = [Int]()
let defaults = UserDefaults.standard
if defaults.bool(forKey: "exclude_smart_playlists") {
//smart
for index in 0...(playlists.count - 1) {
let playlist = playlists[index]
let theAttributes = playlist.value(forProperty: MPMediaPlaylistPropertyPlaylistAttributes) as! Int
if theAttributes == 2 {
toBeRemoved.append(index)
}
}
}
if defaults.bool(forKey: "exclude_folders") {
//folders
for index in 0...(playlists.count - 1) {
let playlist = playlists[index]
let isFolder = playlist.value(forProperty: "isFolder")
let stringIsFolder = String("\(String(describing: isFolder))")
if ((stringIsFolder.range(of: "1")) != nil) {
toBeRemoved.append(index)
}
}
}
//sort from the last to the first so i don't reindex
let reverseSortedPlaylists = toBeRemoved.sorted(by: >)
// remove the unwanted playlists
for list in reverseSortedPlaylists {
store.removePlaylist(index: list)
}
}
i have used LocalAuthentication in my app for Touch ID authentication but Touch ID alert display in every UIViewController for authentication, so how to call only once in a UIViewController?
You just create a BOOL variable and set the value is FALSE in appDelegate and when authenticate in one viewController, update the value of the BOOL variable to TRUE. In every viewController check the BOOL variable wheather it is TRUE or FALSE and code accordingly.
Use this simple API which've written to ease your usage:
import UIKit
import LocalAuthentication
typealias completionHandler = (evaluationComplete: Bool) -> ()
func showTouchID(Reason reason: String, _ shouldShow: Bool, SuperDone : completionHandler){
guard shouldShow == true else{
return
}
let bike = LAContext()
guard bike.canEvaluatePolicy(.DeviceOwnerAuthenticationWithBiometrics, error: nil) else{
SuperDone(evaluationComplete: false)
return
bike.evaluatePolicy(.DeviceOwnerAuthenticationWithBiometrics, localizedReason: reason) {(_, error) in
dispatch_async(dispatch_get_main_queue(), {
guard error != nil else{
SuperDone(evaluationComplete: false)
return
}
SuperDone(evaluationComplete: true)
})
}
}
Use it wherever you want to like:
showTouchID(Reason: "Are", true){ success in
if success{
}
else{
}
}
I am developing a very simple video app. I use the official control: UIImagePickerController.
Here is the problem. When presenting the UIImagePickerController for the first time, the iOS will ask for the permission. The user can click yes or no. If the user clicks no, the control is not dismissed. Instead, if the user keeps clicking the start button, the timers go on while the screen is always black, and the user can't stop the timers or go back. The only thing the user can do is to kill the app. The next time the UIImagePickerController is presented, it is still a black screen and the user can't go back if clicking start.
I was wondering if it's a bug. Is there any way we can detect the permission of the camera so that we can decide to show the UIImagePickerController or not?
Check the AVAuthorizationStatus and handle the cases properly.
NSString *mediaType = AVMediaTypeVideo;
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:mediaType];
if(authStatus == AVAuthorizationStatusAuthorized) {
// do your logic
} else if(authStatus == AVAuthorizationStatusDenied){
// denied
} else if(authStatus == AVAuthorizationStatusRestricted){
// restricted, normally won't happen
} else if(authStatus == AVAuthorizationStatusNotDetermined){
// not determined?!
[AVCaptureDevice requestAccessForMediaType:mediaType completionHandler:^(BOOL granted) {
if(granted){
NSLog(#"Granted access to %#", mediaType);
} else {
NSLog(#"Not granted access to %#", mediaType);
}
}];
} else {
// impossible, unknown authorization status
}
Swift 4 and newer
Make sure to:
import AVFoundation
The code below checks for all possible permission states:
let cameraMediaType = AVMediaType.video
let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: cameraMediaType)
switch cameraAuthorizationStatus {
case .denied: break
case .authorized: break
case .restricted: break
case .notDetermined:
// Prompting user for the permission to use the camera.
AVCaptureDevice.requestAccess(for: cameraMediaType) { granted in
if granted {
print("Granted access to \(cameraMediaType)")
} else {
print("Denied access to \(cameraMediaType)")
}
}
}
Since iOS 10 you need to specify
NSCameraUsageDescription key in your Info.plist to be able ask for camera access, otherwise your app will crash at runtime. See APIs Requiring Usage Descriptions.
An interesting sidenote from Apple Developer forum:
The system actually kills your app if the user toggles your app's
access to camera in Settings. The same applies to any protected
dataclass in the Settings→Privacy section.
Swift Solution
extension AVCaptureDevice {
enum AuthorizationStatus {
case justDenied
case alreadyDenied
case restricted
case justAuthorized
case alreadyAuthorized
case unknown
}
class func authorizeVideo(completion: ((AuthorizationStatus) -> Void)?) {
AVCaptureDevice.authorize(mediaType: AVMediaType.video, completion: completion)
}
class func authorizeAudio(completion: ((AuthorizationStatus) -> Void)?) {
AVCaptureDevice.authorize(mediaType: AVMediaType.audio, completion: completion)
}
private class func authorize(mediaType: AVMediaType, completion: ((AuthorizationStatus) -> Void)?) {
let status = AVCaptureDevice.authorizationStatus(for: mediaType)
switch status {
case .authorized:
completion?(.alreadyAuthorized)
case .denied:
completion?(.alreadyDenied)
case .restricted:
completion?(.restricted)
case .notDetermined:
AVCaptureDevice.requestAccess(for: mediaType, completionHandler: { (granted) in
DispatchQueue.main.async {
if granted {
completion?(.justAuthorized)
} else {
completion?(.justDenied)
}
}
})
#unknown default:
completion?(.unknown)
}
}
}
And then in order to use it you do
AVCaptureDevice.authorizeVideo(completion: { (status) in
//Your work here
})
As an addition to the answer from #Raptor the following should be mentioned. You may receive the following error starting with iOS 10: This application is modifying the autolayout engine from a background thread after the engine was accessed from the main thread. This can lead to engine corruption and weird crashes.
To fix this, make sure you handle the results from the main thread as follows (Swift 3):
private func showCameraPermissionPopup() {
let cameraMediaType = AVMediaTypeVideo
let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(forMediaType: cameraMediaType)
switch cameraAuthorizationStatus {
case .denied:
NSLog("cameraAuthorizationStatus=denied")
break
case .authorized:
NSLog("cameraAuthorizationStatus=authorized")
break
case .restricted:
NSLog("cameraAuthorizationStatus=restricted")
break
case .notDetermined:
NSLog("cameraAuthorizationStatus=notDetermined")
// Prompting user for the permission to use the camera.
AVCaptureDevice.requestAccess(forMediaType: cameraMediaType) { granted in
DispatchQueue.main.sync {
if granted {
// do something
} else {
// do something else
}
}
}
}
}
Specify NSCameraUsageDescription key in Info.plist first.
Then check AVAuthorizationStatus if Authorised then present the UIImagePickerController. It will work.
Swift: Using AVFoundation
Add AVFoundation to Target -> Build Phases -> Link Binary with Libraries.
import AVFoundation on ViewController.
On Info.plist, Add the following:
On View Controller:
#IBAction func cameraButtonClicked(sender: AnyObject) {
let authorizationStatus = AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)
print(authorizationStatus.rawValue)
if AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo) == AVAuthorizationStatus.Authorized{
self.openCameraAfterAccessGrantedByUser()
}
else
{
print("No Access")
dispatch_async(dispatch_get_main_queue()) { [unowned self] in
AVCaptureDevice.requestAccessForMediaType(AVMediaTypeVideo, completionHandler: { (granted :Bool) -> Void in
if granted == true
{
// User granted
self.openCameraAfterAccessGrantedByUser()
}
else
{
// User Rejected
alertToEncourageCameraAccessWhenApplicationStarts()
}
});
}
}
//Open camera
func openCameraAfterAccessGrantedByUser()
{
if(UIImagePickerController .isSourceTypeAvailable(UIImagePickerControllerSourceType.Camera)){
self.cameraAndGalleryPicker!.sourceType = UIImagePickerControllerSourceType.Camera
cameraAndGalleryPicker?.delegate = self
cameraAndGalleryPicker?.allowsEditing = false
cameraAndGalleryPicker!.cameraCaptureMode = .Photo
cameraAndGalleryPicker!.modalPresentationStyle = .FullScreen
presentViewController(self.cameraAndGalleryPicker!, animated: true, completion: nil)
}
else
{
}
}
//Show Camera Unavailable Alert
func alertToEncourageCameraAccessWhenApplicationStarts()
{
//Camera not available - Alert
let cameraUnavailableAlertController = UIAlertController (title: "Camera Unavailable", message: "Please check to see if it is disconnected or in use by another application", preferredStyle: .Alert)
let settingsAction = UIAlertAction(title: "Settings", style: .Destructive) { (_) -> Void in
let settingsUrl = NSURL(string:UIApplicationOpenSettingsURLString)
if let url = settingsUrl {
dispatch_async(dispatch_get_main_queue()) {
UIApplication.sharedApplication().openURL(url)
}
}
}
let cancelAction = UIAlertAction(title: "Okay", style: .Default, handler: nil)
cameraUnavailableAlertController .addAction(settingsAction)
cameraUnavailableAlertController .addAction(cancelAction)
self.window?.rootViewController!.presentViewController(cameraUnavailableAlertController , animated: true, completion: nil)
}