Why photos from gallery does not appear after permissions giving? - ios

I'm using this GitHub project's controller for displaying user's Photo Gallery inside of UICollectionView. Everything is okay there, but except one: If I run it for the first time, it asks me about permissions. I accept it, but after that I cannot get my images from the gallery. It displays nothing.
But when I close my app and run again it can get my photos from the gallery. So, this problem appears just on the first launch.
Can anyone help me, how can I solve it?
UPDATE
I suppose, that this observer works incorrect:
func photoLibraryDidChange(changeInstance: PHChange) {
let changeDetails = changeInstance.changeDetailsForFetchResult(images)
self.images = changeDetails!.fetchResultAfterChanges
dispatch_async(dispatch_get_main_queue()) {
// Loop through the visible cell indices
let indexPaths = self.imagesCollectionView.indexPathsForVisibleItems()
for indexPath in indexPaths as [NSIndexPath]! {
if changeDetails!.changedIndexes!.containsIndex(indexPath.item) {
let cell = self.imagesCollectionView?.cellForItemAtIndexPath(indexPath) as! ImagesCollectionViewCell
cell.imageAsset = changeDetails!.fetchResultAfterChanges[indexPath.item] as? PHAsset
}
}
}
}
So, I give permissions, but it doesn't show me my photos. For this I need to relaunch my app
If you want any code - I can share it. But I'm using the above github controller which I've attached above.

Since it works correctly when you relaunch the app it sounds like you are probably asking for permissions at the same time you are displaying the view controller/collection view. The problem is that the collection view doesn't know to reload once the user has selected to give your app access to their photos. In the PHPhotoLibrary.requestAuthorization call back you need to tell your view controller/collection view to reload once they have Authorized your app. Small example below:
PHPhotoLibrary.requestAuthorization { status in
switch status {
case .Authorized:
/* RELOAD COLLECTION VIEW HERE */
case .Restricted:
<#your code#>
case .Denied:
<#your code#>
default:
// place for .NotDetermined - in this callback status is already determined so should never get here
break
}
}

Related

Why does my realm notification token only trigger on my active device and not all devices with the open realm?

I'm newly learning iOS/swift programming and developing an application using MongoDB Realm and using Realm sync. I'm new to programming and realm, so please feel free to correct any terminology. My question is about listening for realm notifications, which I see referred to as change listeners and notification tokens. Regardless, here is the info:
My application has a list of locations with a status (confirmed/pending/cancelled). I open this list from my realm as a realm managed object and create my notification handler:
//This is called in it's own function, but assigns the locations
locations = publicRealm?.objects(Location.self)
//This is run after the function is called
self?.notificationToken = self?.locations!.observe { [weak self] (_) in
self?.tableView.reloadData()
print("Notification Token!!!")
I then populate my table view and let a user tap on a location, which passes the location and realm to another view controller where the user can update the status. That update is made in a separate view controller.
do{
try publicRealm?.write {
selectedLocation?.statusMessage = locationStatusTextField.text!
selectedLocation?.status = selectedStatus
}
}catch{
print("Error saving location data: \(error)")
}
At this point my notification token is successfully triggered on the device where I am making the location update. The change is shown immediately. However there is no notification token or realm refresh that happens on any other open devices that are showing the locations table view. They do not respond to the change, and will only respond to it if I force realm.refresh(). The change is showing in Atlas on MongoDB server, though.
I am testing on multiple simulators and my own personal phone as well, all in Xcode.
I'm very confused how my notification token can trigger on one device but not another.
When I first started the project it was a much simpler realm model and I could run two devices in simulator and updating one would immediately fire a change notification and cause the second device to show the correct notification.
I have since updated to a newer realm version and also made the realm model more complicated. Though for this purpose I am trying to keep it simple by doing all changes via swift and in one realm.
I also have realm custom user functions running and changing data but I think reading the docs I am realizing that those will not trigger a notification - I'm not sure if that's true though? I just know right now that if I change data in the DB via a user function no notifications are triggered anywhere - but if I do realm.refresh() then the change shows.
What is it that I am missing in how I am using these notifications?
***Updating information on Public Realm:
Save the realm:
var publicRealm:Realm?
Login as an anon user and then open the realm:
let configuration = user.configuration(partitionValue: "PUBLIC")
Realm.asyncOpen(configuration: configuration) { [weak self](result) in
DispatchQueue.main.async {
switch result {
case .failure(let error):
fatalError("Failed to open realm: \(error)")
case .success(let publicRealm):
self!.publicRealm = publicRealm
guard let syncConfiguration = self?.publicRealm?.configuration.syncConfiguration else {
fatalError("Sync configuration not found! Realm not opened with sync?")
}
It is after this realm opening that the locations are loaded and notification token is created.
I use a segue to pass the location object and realm to the next VC:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationVC = segue.destination as! UpdateLocationViewController
destinationVC.delegate = self
if let indexPath = tableView.indexPathForSelectedRow {
destinationVC.selectedLocation = locations?[indexPath.row]
}
if indexPathRow != nil {
destinationVC.selectedLocation = locations?[indexPathRow!]
}
destinationVC.publicRealm = self.publicRealm
}
Some notes:
original public realm opened by anon user
only a logged in user can click on a location... so in the 'UpdateLocation' VC that gets passed the public realm I am a logged in user. But, I'm just using the Dev mode of Realm to allow me to read/write however I like... and I am writing straight to that public realm to try and keep it simple. (I have a custom user function that writes to both public and the user's org realm, but I stopped trying to use the function for now)
I identify the object to update based on the passed in location object from the first VC
I needed to use try! when making my write call rather than just try. I updated my write blocks as such:
try! publicRealm?.write {
selectedLocation?.statusMessage = locationStatusTextField.text!
selectedLocation?.status = selectedStatus
}
Just wanted to follow up in case anyone finds this. Thank you!

How do you redirect user to settings after denying permissions for Save Image from UIActivityController?

After a user takes a photo in my app, there is a UIUserActivity that is presented that allows users to save and image. A pop up comes up and asks for permission to write the photo to the users photo album. However, if the user denies there is no way to prompt the user to be redirected to the photo library. PHPhoto authorization status is always returning undetermined even though permissions were already asked for. I am looking for a few things to be satisfied:
the user should be able to deny but continue to be prompted to give permission to the app if they want to save their photo.
if the user denies, I want the option of "save photo" to remain in the UIUserActivity as a potential option
How can I accomplish this?
I've tried using the PHPhotoLibrary authorization status, but it always returns undetermined. I've tried checking for .undetermined and using the PHPhotoLibrary to request access to the user's camera roll, however if the user denies at this point then the option to save photo is completely removed from the UIUserActivity pop up.
code:
activityViewController.completionWithItemsHandler = { activity, success, items, error in
if success {
if let activity = activity {
...
case .saveToCameraRoll:
handleCameraRollPermission()
func handleCameraRollPermission(status: PHAuthorizationStatus? = nil, completion: #escaping ((Bool) -> Void)) {
let authorizationStatus = status ?? PHPhotoLibrary.authorizationStatus()
switch authorizationStatus {
case .denied, .restricted:
showPermissionMissingAlert(completion: completion)
case .notDetermined:
PHPhotoLibrary.requestAuthorization { (status) in
switch(status) {
case .denied, .restricted, .notDetermined:
self.showPermissionMissingAlert(completion: completion)
default:
break
}
}
default:
completion(true)
}
}
This might be a bit overdoing it, but you could create a custom action section for the activityView. You can find more about that here and scroll down to the Adding a custom action section.
Basically you would make a custom save button that would persist regardless of permission status. When it is tapped you can check their authorization status and go from there.

Photo Editing Extension - Revert Image to Original

I'm developing a photo editing extension for iOS, bundled in a container app that provides the same photo editing functionality.
In order to reuse code, I have a view controller class that adopts the required PHContentEditingController protocol, and I subclassed it for use both as the main interface of the app extension, and as the "working screen" of the container app.
On the editing extension, the controller's methods are called by the Photos app's editing session as described in Apple's documentation and various tutorials you can find around the web.
On the container app, on the other hand, I first obtain a PHAsset instance by means of the UIImagePickerController class, and directly start the editing session manually on my "work" view controller like this:
// 'work' is my view controller which adopts
// `PHContentEditingController`. 'workNavigation'
// embeds it.
let options = PHContentEditingInputRequestOptions()
options.canHandleAdjustmentData = { (adjustmentData) in
return work.canHandle(adjustmentData)
}
asset.requestContentEditingInput(with: options, completionHandler: { [weak self] (input, options) in
// (Called on the Main thread on iOS 10.0 and above)
guard let this = self else {
return
}
guard let editingInput = input else {
return
}
work.asset = asset
work.startContentEditing(with: editingInput, placeholderImage: editingInput.displaySizeImage!)
this.present(workNavigation, animated: true, completion: nil)
})
When the user finishes editing, the work view controller calls finishContentEditing(completionHandler: on itself to finish the session:
self.finishContentEditing(completionHandler: {(output) in
// nil output results in "Revert" prompt.
// non-nil output results in "Modify" prompt.
let library = PHPhotoLibrary.shared()
library.performChanges({
let request = PHAssetChangeRequest(for: self.asset)
request.contentEditingOutput = output
}, completionHandler: {(didSave, error) in
if let error = error {
// handle error...
} else if didSave {
// proceed after saving...
} else {
// proceed after cancellation...
}
})
})
Within the editing session, the user can 'clear' the previous edits passed as adjustment data, effectively reverting the image to its original state.
I've noticed that, if I finish the editing by calling the completion handler passed to finishContentEditing(completionHandler:) with nil as its argument (instead of a valid PHContentEditingOutput object), the Photos framework will prompt the user to "revert" the image instead of "modifying" it:
func finishContentEditing(completionHandler: #escaping (PHContentEditingOutput?) -> Void) {
guard let editingInput = self.editingInput, let inputURL = editingInput.fullSizeImageURL else {
return completionHandler(nil)
}
if editingInput.adjustmentData != nil && hasUnsavedEdits == false {
// We began with non-nil adjustment data but now have
// no outstanding edits - means REVERT:
return completionHandler(nil)
}
// (...proceed with writing output to url...)
However, this only works when running from the container app. If I try the same trick from the extension (i.e., load an image that contains previous edits, reset them, and tap 'Done') I get the dreaded "Unable to Save Changes" message...
What is the correct way to revert previous edits to an image from within a photo editing extension?
Months later, still no answer, so I begrudgingly adopted this workaround (which is still preferable to an error alert):
When the user taps "Done" from the Photos extension UI and the image has no edits applied to it (either because the user reset previous edits, or didn't apply any changes to a brand new image), perform the following actions from within finishContentEditing(completionHandler:):
Create adjustment data that amounts to no visible changes at all ("null effect") and archive it as Data.
Create a PHAdjustmentData instance with the "null effect" data from above, with the formatVersion and formatIdentifier properly set.
Create a PHContentEditingOutput instance from the editing input passed at the beginning of the session (as usual), and set the adjustment data created above.
Read the unmodified image from the inputURL property of the editing input, and write it unmodified to the url specified by the PHContentEditingOutput instance's renderedContentURL property.
Call the completionHandler block, passing the editing output instance (as normal).
The result: The image is saved in its original state (no effects applied), and no alerts or errors occur.
The drawback: The library asset remains in the 'edited' state (because we passed non-nil editing output and adjustment data, there was no other choice), so the next time the user tries to edit it from Photos.app, the red 'Revert' button will be present:
However, choosing 'revert' results in no visible changes to the image data (duh!), which can be confusing to the user.
—-
Update
I checked what the built-in “Markup” extension does:
...and it is consistent with my workaround above, so I guess this is the best that can be done.

UICollectionView removing an item issue

I have created a CollectionView where displays the photos from the camera roll, just like the default photo app of iPhone (one cell per photo)
I need to implement a delete functionality, so I successfully created a delete action. The problem is when I am trying to reload the data of the collection view.
To give an example I want to mimic the default's photo app behavior on delete.
Below I have the code which crashes. It is executed after successfully delete action.
func removeFromCollectionView() {
let ip = self._collectionView.indexPathsForVisibleItems
self._itemsCount? -= 1
_collectionView.deleteItems(at: ip)
_collectionView.performBatchUpdates({
self._collectionView.reloadItems(at: ip)
}, completion: nil)
}
The error is:
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.
Also, I have tried to wrap the code into
DispatchQueue.main.async { }
I tried to be explanatory let me know if you need more, does anyone has an idea? Thanks in advance.
EDIT:
I did the below changes based on comments. The code does not crash but does not delete the photo
func removeFromCollectionView() {
let ip = self._collectionView.indexPathsForVisibleItems
self._itemsCount? -= 1
_collectionView.performBatchUpdates({
self._collectionView.deleteItems(at: ip)
}, completion: { finished in
if finished {
print("finished")
self._collectionView.reloadData()
}
})
}

Why does Example app using Photos framework use stopCachingImagesForAllAssets after each change?

So I'm looking at the Photos API and Apple's sample code here
https://developer.apple.com/library/ios/samplecode/UsingPhotosFramework/Introduction/Intro.html
and its conversion to swift here
https://github.com/ooper-shlab/SamplePhotosApp-Swift
I have integrated the code into my project so that a collectionView is successfully updating itself form the library as I take photos. There is one quirk: Sometimes cells are blank, and it seems to be connected to stopCachingImagesForAllAssets which Apple calls each time the library is updated at the end of photoLibraryDidChange delegate method.
I can remove the line and it fixes the problem, but surely there is a reason Apple put it there in the first place? I am concerned with memory usage.
// MARK: - PHPhotoLibraryChangeObserver
func photoLibraryDidChange(changeInstance: PHChange) {
// Check if there are changes to the assets we are showing.
guard let
assetsFetchResults = self.assetsFetchResults,
collectionChanges = changeInstance.changeDetailsForFetchResult(assetsFetchResults)
else {return}
/*
Change notifications may be made on a background queue. Re-dispatch to the
main queue before acting on the change as we'll be updating the UI.
*/
dispatch_async(dispatch_get_main_queue()) {
// Get the new fetch result.
self.assetsFetchResults = collectionChanges.fetchResultAfterChanges
let collectionView = self.pictureCollectionView!
if !collectionChanges.hasIncrementalChanges || collectionChanges.hasMoves {
// Reload the collection view if the incremental diffs are not available
collectionView.reloadData()
} else {
/*
Tell the collection view to animate insertions and deletions if we
have incremental diffs.
*/
collectionView.performBatchUpdates({
if let removedIndexes = collectionChanges.removedIndexes
where removedIndexes.count > 0 {
collectionView.deleteItemsAtIndexPaths(removedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
if let insertedIndexes = collectionChanges.insertedIndexes
where insertedIndexes.count > 0 {
collectionView.insertItemsAtIndexPaths(insertedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
if let changedIndexes = collectionChanges.changedIndexes
where changedIndexes.count > 0 {
collectionView.reloadItemsAtIndexPaths(changedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
}, completion: nil)
}
self.resetCachedAssets() //perhaps prevents memory warning but causes the empty cells
}
}
//MARK: - Asset Caching
private func resetCachedAssets() {
self.imageManager?.stopCachingImagesForAllAssets()
self.previousPreheatRect = CGRectZero
}
I was having the same result.
Here's what fixed the issue for me:
Since performBatchUpdates is asynchronous, the resetCachedAssets gets executed possibly while the delete/insert/reload is happening, or even between those.
That didn't sound nice to me. So I moved the line:
self.resetCachedAssets()
to the first line of the dispatch_async block.
I hope this helps you too.

Resources