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
Related
I am working on an app that will start out by requesting access to the users contacts and then display information about the first contact in the UI.
func requestContactsAccess() {
let store = CNContactStore()
store.requestAccess(for: .contacts, completionHandler: {_,_ in })
DispatchQueue.main.async {
let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
if authorizationStatus == .authorized {
self.retrieveContacts()
self.updateScreen()
} else {
self.alert()
}
}
The problem I am running into is that the UI tries to update before the app gets user permission to access the contacts. I tried to get around this by using a completion handler to call the function to update the UI after fetching the contacts but this causes the error when updating my label "UILabel.text must be used from main thread only"
TLDR: How can I ensure I have retrieved the device contacts before updating the UI?
#RickyMo 's answer was what fixed it for me.
Solution:
func requestContactsAccess() {
let store = CNContactStore()
store.requestAccess(for: .contacts, completionHandler: {_,_ in
DispatchQueue.main.async {
let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
if authorizationStatus == .authorized {
self.retrieveContacts()
self.updateScreen()
} else {
self.alert()
}
}
})
}
On iOS app with GoogleInteractiveMediaAds integrated "Skip Ad" button doesn't work. Meanwhile manual call adsManager.skip() works perfectly. The button itself reacts to the tuches because it changes bounds and seems highlighted. Unfortunately, I haven't found anything according to handle tap manually, so maybe somebody has already been in this situation and could help with it.
guard
let adInformation = delegate?.latestAdInformation(), let url = adInformation.urlForIMA,
let adContainer = delegate?.videoAdDisplayContainerView()
else { return }
switch adInformation.adsType {
case .interstitials, .none:
self.play(ignoreAds: true)
return
case .prerolls, .all:
fallthrough
#unknown default:
break
}
let adDisplayContainer = IMAAdDisplayContainer(adContainer: adContainer, companionSlots: nil)
let request = IMAAdsRequest(
adTagUrl: url,
adDisplayContainer: adDisplayContainer,
contentPlayhead: contentPlayhead,
userContext: nil)
adsLoader.requestAds(with: request)
func adsLoader(_ loader: IMAAdsLoader!, adsLoadedWith adsLoadedData: IMAAdsLoadedData!) {
// Grab the instance of the IMAAdsManager and set ourselves as the delegate
adsManager = adsLoadedData.adsManager
adsManager?.delegate = self
// Create ads rendering settings and tell the SDK to use the in-app browser.
let adsRenderingSettings = IMAAdsRenderingSettings()
if let vc = delegate?.adWebControllerPreferredOpenViewController() {
adsRenderingSettings.webOpenerPresentingController = vc
}
// Initialize the ads manager.
adsManager?.initialize(with: adsRenderingSettings)
}
func adsLoader(_ loader: IMAAdsLoader!, failedWith adErrorData: IMAAdLoadingErrorData!) {
self.play(ignoreAds: true)
}
func adsManager(_ adsManager: IMAAdsManager!, didReceive event: IMAAdEvent!) {
if event.type == IMAAdEventType.LOADED {
adsManager.start()
}
}
func adsManager(_ adsManager: IMAAdsManager!, didReceive error: IMAAdError!) {
self.play(ignoreAds: true)
}
func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager!) {
self.pause(ignoreAds: true)
}
func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager!) {
self.play(ignoreAds: true)
}
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()
I'm trying to implement a custom Image Picker Controller, or rather a video picker controller, here is the code for fetching all videos in photo library:
fileprivate func fetchVideos() {
let allVideos = PHAsset.fetchAssets(with: .video, options: assetsFetchOptions())
DispatchQueue.global(qos: .background).async {
allVideos.enumerateObjects({ (asset, count, stop) in
let imageManager = PHImageManager.default()
imageManager.requestAVAsset(forVideo: asset, options: nil, resultHandler: { (asset, audioMix, info) in
guard let asset = asset as? AVURLAsset, let image = asset.previewImage() else { return }
print("Got a video: \(asset.duration.cmTimeString)")
// let medium = Medium(previewImage: image, duration: asset.duration)
// self.media.append(medium)
})
})
}
}
Very obviously, this needs user's permission to access the photo library, and for the first time we do this it will pop up the alert for the user to give permission.
However, after given permission, the controller does not actually start to load the videos. I need to dismiss the controller and represent it for the second time (when the permission was already give) in order to start to load the video properly.
Question: how to load videos properly immediately after being given the permission without having to dismiss and present it for a second time (just like the built-in UIImagePickerController?
This can be done by checking if there is authorization and if not, using the PHPhotoLibrary.requestAuthorization with completion handler to monitor the result.
Something like this:
if PHPhotoLibrary.authorizationStatus() == .authorized {
// Load image/ video function
} else {
PHPhotoLibrary.requestAuthorization { status in
if status == .authorized {
// Load image/ video function
} else {
// Show error message
}
}
}
please check this link which has code :-
https://github.com/VishveshLad/VLCustomImagePicker/blob/main/CustomImagePicker/CustomImagePicker/CustomImagePickerVC.swift
if PHPhotoLibrary.authorizationStatus() == .authorized {
//load data
} else {
PHPhotoLibrary.requestAuthorization({ (status: PHAuthorizationStatus) -> Void in
if status == .authorized {
//load data
} else {
// show error
}
})
}
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)
}
}