Photo Editing Extension - Revert Image to Original - ios

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.

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!

Unable to pick image from gallery XCUITest

I use XCUITest for testing an app that allows the user to select an avatar by picking a photo from the gallery. When I tap on the button that opens the gallery window, I can see the elements in debugDescription. There is a table that contains the folders with photos. The problem is when I tap for the first time on any cell the test fails with error:
Assertion Failure: UserProfileAndSettingsTests.swift:434: Failed to get matching snapshot: No matches found for Element at index 2 from input {(
Table
)}".
If I put a breakpoint there, the second time I tap on any cell, it works.
The command is the following:
XCUIApplication().tables.element(boundBy: 2).cells.element(boundBy: 1).tap()
If before the command I put the line: XCUIApplication().tables.element(boundBy: 2).cells.element(boundBy: 1), it doesn't fail. It fails when trying to tap().
Looks like a timing issue. Familiarize yourself with the XCUIElement class, specifically with this:
/** Waits the specified amount of time for the element's exist property to be true and returns false if the timeout expires without the element coming into existence. */
open func waitForExistence(timeout: TimeInterval) -> Bool
You should be able to do something like this:
let element = XCUIApplication().tables.element(boundBy: 2).cells.element(boundBy: 1)
if element.waitForExistence(timeout: 2) {
element.tap()
}
I recommend making friends with this method, and also other similar methods and expectations, to be able to do convenient stuff like this (self in this context is a XCTestCase):
func waitUntilTappable(_ element:XCUIElement, timeout: TimeInterval = 2) {
let tappableExpectation = self.expectation(for: NSPredicate(format: "isHittable == true"),
evaluatedWith: element)
self.wait(for: [tappableExpectation], timeout: timeout.rawValue)
}

Swift: Printing without alert box

I use the following codes for printing in the app:
init() {
self.printInfo.outputType = UIPrintInfoOutputType.photo
self.printInfo.orientation = UIPrintInfoOrientation.landscape
self.printController.printInfo = self.printInfo
self.printer = UIPrinter(url: URL(string: printIP)!)
// where printIP is a string that give the internal IP of the printer
debugPrint(printIP)
}
func print(image: UIImage) -> Bool {
self.printController.printingItem = image
printController.print(to: printer, completionHandler: {(controller, success, error) -> Void in
if success {
debugPrint("Printing Completed.")
} else {
debugPrint("Printing Failed.")
}
})
return true
}
It can print successfully. However, when the function is triggered, there is an alert box indicating that it is contacting to the Printer, and printing. Is there any method to avoid the pop up of this alert box? I want the printing done at the back without showing anything on the screen that interfere the user experience (I want to play a movie when the printer is working at the back).
Thanks.
From iOS 8 there is a way to print without any presentation of the
printing UI. Instead of presenting the UI each time the user presses a
print button, you can provide a way for your users to select a printer
somewhere in your app with the easy-to-use UIPrinterPickerController.
It accepts an optional UIPrinter instance in its constructor for a
pre-selection, uses the same presentation options as explained above,
and has a completion handler for when the user has selected her
printer:
Swift 3
let printerPicker = UIPrinterPickerController(initiallySelectedPrinter: savedPrinter)
printerPicker.present(animated: true) {
(printerPicker, userDidSelect, error) in
if userDidSelect {
self.savedPrinter = printerPicker.selectedPrinter
}
}
Now you can tell your UIPrintInteractionController to print directly by calling printToPrinter(:completionHandler:) with the saved printer instead of using one of the present... methods.
Source:- http://nshipster.com/uiprintinteractioncontroller/

Uploading Files onto Firebase is skipping lines of code?

I am relatively new to swift and Firebase but I am definitely encountering a weird problem. What seems to be happening after messing around in the debugger is that the following function seems to be exhibiting weird behavior such as skipping the line storageRef.put()
So whats been happening is this, this function is triggered when the user clicks on a save button. As I observe in the debugger, storageRef is called but the if else statements are never invoked. Then, after my function returns the object which wasn't properly initalized, it then returns into the if else statement with the proper values... By then it is too late as the value returned and uploaded to the database is already incorrect..
func toAnyObject() -> [String : Any] {
beforeImageUrl = ""
let storageRef = FIRStorage.storage().reference().child("myImage.png")
let uploadData = UIImagePNGRepresentation(beforeImage!)
storageRef.put(uploadData!, metadata: nil) { (metadata, error) in
if (error != nil) {
print(error)
} else {
self.beforeImageUrl = (metadata?.downloadURL()?.absoluteString)!
print("upload complete: \(metadata?.downloadURL())")
}
}
let firebaseJobObject : [String: Any] = ["jobType" : jobType! as Any,
"jobDescription" : jobDescription! as Any,
"beforeImageUrl" : beforeImageUrl! as Any,]
return firebaseJobObject
}
Consider a change in your approach here. The button target-action is typical of a solution that requires an immediate response.
However, when you involve other processes (via networks) like the - (FIRStorageUploadTask *)putData:(NSData *)uploadData method above, then you must use some form of delegation to perform the delayed action whenever the server side method returns a value.
Keep in mind that when you are trying to use the above method, that it is not meant for use with large files. You should use - (FIRStorageUploadTask *)putFile:(NSURL *)fileURL method.
I'd suggest that you rework the solution to ensure that the follow-up action only happens when the put succeeds or fails. Keep in mind that network traffic means that the upload could take some time. If you want to validate that the put is completing with a success or failure, just add a breakpoint at the appropriate location inside the completion block and run the method on a device with/and without network access (to test both code flows).

The NSURLSession Task file download occurs after the program needs to use the file

I have defined an NSURLSession with a download task. I would like to download a file and then use values in the file in my drawing style kit. The session and the drawing style kit do run on the synchronous queues, unfortunately the task runs on an NSOperationQueue which does not run until after the view is created. Therefore, the view does not contain the correct file information. Is there a coding solution or is it a logic issue and does anyone have suggestions for improving the logic. I am a newbie so any suggestions are most welcome.
class SomeView: UIView, NSURLSessionDelegate, NSURLSessionTaskDelegate
{
var session: NSURLSession!
required init(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
// Create the session configuration
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
configuration.timeoutIntervalForRequest = 15.0
// Create the session
session = NSURLSession(configuration: configuration,
delegate: self,
delegateQueue: nil)
}
override func drawRect(rect: CGRect)
{
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)
dispatch_sync(queue,
{
// Define the data source at www.powerball.com/powerball/winnums-text.txt
let url = NSURL(string: "http://www.powerball.com/powerball/winnums-text.txt")
// Create the download task for the session
let task = self.session.downloadTaskWithURL(url!, completionHandler:
{[weak self] (url: NSURL!,
response: NSURLResponse!,
error: NSError!) in
if error == nil
{
let fileManager = NSFileManager()
//Get the path for the caches folder
var error: NSError?
var cachePath = fileManager.URLForDirectory(
NSSearchPathDirectory.CachesDirectory,
inDomain: NSSearchPathDomainMask.UserDomainMask,
appropriateForURL: url,
create: true,
error: &error)!
// Assign consistent file name
var fileName = "winnums-text"
// Apend the file name to the cache path
var filePath = cachePath.URLByAppendingPathComponent(fileName)
// Move the file to the file path
fileManager.moveItemAtURL(url, toURL: filePath, error: nil)
println(filePath)
}
else
{
println("Connection failed") //add user notification later
}
//Clean up sessions
self!.session.finishTasksAndInvalidate()
}) //Task
task.resume()
}) // Dispatch queue
dispatch_sync(queue,
{
// Only override drawRect: if you perform custom drawing.
SomeappStyleKit.drawMain(frame: self.bounds)
}) // Dispatch queue
}
}
Ok, there are several things ... well ... ill-chosen in my humble opinion I'd say :)
Here is how I would approach that problem.
Your basic idea is:
"Have a custom view, that displays stuff according to some information downloaded from the net"
First, we should see what we need:
You need a custom view
You need some form of asynchronous downloading
You need some controller that coordinates
you need to decide, how the whole thing should behave:
Is the view only to be created when information is downloaded?
Is the view already there in the beginning, and what is it showing meanwhile?
What triggers downloading? Some controller? The view really shouldn't.
Now we do NOT want to mix any of the asynchronous downloading stuff into the view. The view should only be dumb and display what it is given. (I have to admit I don't know what your view will actually display, so there might be cases where this could be considered valid, but most likely not)
Therefore I'd suggest you create a separate class that handles the downloading and a separate class that is your view.
You could create a NSOperation subclass that does all the asynchronous stuff and is enqueued/kicked off by your controller (See Ray Wenderlich's NSOperation Tutorial for example) or you can do the asynchronous stuff inside your controller using GCD, and in the completion handler (in either case) tell the view to redisplay itself.
The view could display a placeholder while the content is not downloaded, and by calling a function from the outside (the controller would call this) could re-render itself when new content arrives. One thing is for sure: DrawRect needs to be as fast as possible!
So you might want to have an if statement in there, and only if the content is fetched (the view could have a Bool or Enum that indicates this state), you call a specialized drawing method of your view, and otherwise just draw the placeholder.
And don't use dispatch_sync anywhere in there... in fact, don't use it anywhere at all. You really want to be asynchronous in most cases (as usual, exceptions apply). In this special case inside drawRect you really MUST not use any GCD functions like this, as drawRect is called on the main thread afaik and you must not do any drawing outside the main thread.
I hope this gets you started. Otherwise I might find the time to come up with some code later this week.
Edit: You could just use the NSURLDownloadTask you have, but in a controller. A separate NSOperation subclass might not be the best option here and unnecessary.

Resources