I am developing an app extension that takes an url and upload it to a web service.
If there are errors in the upload request, an Alert should pop up and when the user dismisses it, the extension should complete.
Profiling this code with instruments show a memory leak with two NSISLinearexpression objects.
I found that the incriminating code is found in the UIAlertAction that dismisses the alert: without an action attached to the alert the leak disappear.
I'm supposing for some reason calling:
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
causes troubles with the dismiss of the UIAlertController.
Why is that happening?
Here is my code:
import UIKit
import Social
class ShareViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
fetchStuff()
}
private func sendAlert(alertMessage:String) {
print("alerting")
let alert = UIAlertController(title: "Send video to Kodi", message: alertMessage, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.default) {
UIAlertAction in
print("Cancel Pressed")
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
private func fetchStuff() -> Void {
print("fetching")
guard let extensionItem = extensionContext?.inputItems[0] as? NSExtensionItem else {
print("Unable to get extensionItem")
return
} // check for only 1 attachment
let itemProvider = extensionItem.attachments as! [NSItemProvider]
let item = itemProvider.first
if (item?.hasItemConformingToTypeIdentifier("public.url"))! {
item?.loadItem(forTypeIdentifier: "public.url", options: nil, completionHandler:
{ [weak self] (item: NSSecureCoding?, error: Error?) -> Void in
if let url = item as? NSURL {
print(url.absoluteString!)
self?.sendAlert(alertMessage: "test")
}
})
}
else {
return
}
return
}
}
I just had a similar issue.
The cause of the issue for me was that the CoreData manager we built worked off the main dispatch queue. So when the core data manager called our completion block it was actually on a different queue. I added:
DispatchQueue.main.async { }
Around my dialog creation and present calls and the leak disappeared. Hope it helps :)
Related
I got simple UIViewController + UIAlert extension:
extension UIViewController {
func alert(title: String = "", message: String){
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: Localized.ok(), style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
}
Within the ViewController I got a method:
func findUser() {
userService.findUser { (userinfo, error) in
if error != nil {
if let errText = error?.localizedDescription {
self.alert(message: errText)
}
self.doAuth()
return
}
}
}
this doAuth() method should redirect to loginViewController using:
navigationController?.pushViewController(loginViewController, animated: false)
The problem is, that in this scenario, this push doesn't work (nothing appears) (I click OK button on the alert, alert dissapears but loginViewController is not pushed)
I refactored extension a little bit:
extension UIViewController {
func alert(title: String = "", message: String, action completion: (() -> Void)? = nil){
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: Localized.ok(), style: .default, handler: { _ in
completion?()
}))
present(alert, animated: true, completion: nil)
}
}
so findUser() method is calling doAuth() in differently:
func findUser() {
userService.findUser { (userinfo, error) in
if error != nil {
if let errText = error?.localizedDescription {
self.alert(message: errText){ [weak self] in
self?.doAuth()
}
}
return
}
}
}
and it works!
Problem is I have no idea why. And what could have happened in the first scenario?
I feel it should be some simple explanation, but I can't figure it out.
Edit
The explanation is simple and was printed in the console:
pushViewController:animated: called on <UINavigationController 0x7f86050b4400>
while an existing transition or presentation is occurring; the navigation stack
will not be updated.
So doAuth() (with pushing VC method) was called while alert was visible/presented, so alert took the focus and VC couldn't be pushed.
cc: #Paulw11 # cookednick
Problem is navigating and presenting in same if statement
If error != nil , means it only show the alert not try to doAuth().
But you are calling doAuth() in same if block then it is trying to present alert as well as to navigate
func findUser() {
userService.findUser { (userinfo, error) in
if error != nil {
if let errText = error?.localizedDescription {
self.alert(message: errText)
}
return
}
//Out side if block
self.doAuth()
}
}
I am calling a method that executes a URLSession but before it does anything, presents a UIAlertController blocking the UI until some sort of response from the request is achieved. Logic tells me that dismissing the UIAlertController in the completion block of that method where it is called on the main thread would be the best option. Am I wrong to assume this? Apparently so, as variably the presented UIAlertController will indeed display, but never dismiss. Help?
Block:
getCostandIV { output in
let cost = output["ask"] as! NSNumber
let IV = output["IV"] as! NSNumber
self.enteredCost = cost.stringValue
self.enteredIV = IV.stringValue
DispatchQueue.main.async {
self.progress.dismiss(animated: true, completion: nil)
self.tableView.reloadSections(IndexSet(integer: 1), with: UITableView.RowAnimation.none)
self.canWeSave()
}
}
Function:
func getCostandIV (completionBlock: #escaping (NSMutableDictionary) -> Void) -> Void {
DispatchQueue.main.async {
self.progress = UIAlertController(title: "Retrieving ask price and volatility...", message: nil, preferredStyle: UIAlertController.Style.alert)
self.present(self.progress, animated: true, completion: nil)
}
guard let url = URL(string: "https://api.tdameritrade.com/v1/marketdata/chains?apikey=test&symbol=\(symbol)&contractType=\(type)&strike=\(selectedStrike)&fromDate=\(selectedExpiry)&toDate=\(selectedExpiry)") else {
return
}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let dataResponse = data,
error == nil else {
//print(error?.localizedDescription ?? "Response Error")
DispatchQueue.main.async {
self.presentedViewController?.dismiss(animated: true, completion: {
let alert = UIAlertController(title: "There was an error retrieving ask price and volatility.", message: "Please try again later.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
self.present(alert, animated: true)
})
}
return }
do{
//here dataResponse received from a network request
let jsonResponse = try JSONSerialization.jsonObject(with:
dataResponse, options: [])
// //print(jsonResponse) //Response result
guard let jsonDict = jsonResponse as? NSDictionary else {
return
}
// //print(jsonDict)
var strikeMap : NSDictionary = [:]
if self.type == "CALL" {
strikeMap = jsonDict["callExpDateMap"] as! NSDictionary
} else {
strikeMap = jsonDict["putExpDateMap"] as! NSDictionary
}
self.strikes.removeAllObjects()
let inner = strikeMap.object(forKey: strikeMap.allKeys.first ?? "<#default value#>") as! NSDictionary
let innerAgain = inner.object(forKey: inner.allKeys.first ?? "<#default value#>") as! NSArray
let dict : NSDictionary = innerAgain[0] as! NSDictionary
let dict2 = ["ask" : dict["ask"] as! NSNumber, "IV" : dict["volatility"] as! NSNumber] as NSMutableDictionary
completionBlock(dict2)
} catch let parsingError {
print("Error", parsingError)
}
}
task.resume()
}
Edit: Using self.presentedViewController?.dismiss(animated: true, completion: nil) did not fix the issue. In addition, the completion block of the dismiss function for self.progress is not being called.
Edit 2: presentedViewController right before dismiss code in the callback is nil, even though present is called on the alert controller before dismiss?
use this , for dismiss your alert you should add dismiss method in an async block and for setting timer for that you should tell async block to start being async from now to 5 seconds and after that do some thing :
alert.addAction(UIAlertAction(title: "ok", style: .default,
handler: nil))
viewController.present(alert, animated: true, completion: nil)
// change to desired number of seconds (in this case 5 seconds)
let when = DispatchTime.now() + 5
DispatchQueue.main.asyncAfter(deadline: when){
// your code with delay
alert.dismiss(animated: true, completion: nil)
}
If you call the getCostandIV method more than once, the second alert won't be presented and self.progress will have the reference of unpresented alert.
Change
self.progress.dismiss(animated: true, completion: nil)
To
self.presentedViewController?.dismiss(animated: true, completion: nil)
Your alert would be dismissed only if everything goes well.
I suggest you to change your function to something like this:
func getCostandIV (completionBlock: #escaping (NSMutableDictionary?, Error?) -> Void) -> Void
and make sure that your completionBlock is called when your guard statements fail or an error is thrown. In your current code the alert is only dismissed when it network request fails, but not when something goes wrong when parsing JSON.
I have an app where a user uploads a file in the background that usually takes a couple of seconds. The upload is kicked off when they tap a "Done" button and that also dismisses the view controller. What I would I would like to happen is an alert comes up when the download is done. I thought I would just add the code below to the upload function but it isn't working. How can I have an alert box appear to confirm that that the upload was successful?
#IBAction func tapDone(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
if let image = newImage {
submit2Parse(image: image)
}
}
func submit2Parse (image: UIImage) {
if let pfImage = image2PFFile(image: image) {
// Insert PFFile into parse server
let submittedImage = PFObject(className: "Images")
submittedImage["imageFile"] = pfImage
submittedImage["type"] = "submittedFromUserHome"
submittedImage["bride"] = brideSwitch.isOn
submittedImage["groom"] = groomSwitch.isOn
submittedImage["user"] = userSwitch.isOn
submittedImage["picturePeriod"] = pickerSelected
submittedImage["uploadedByUserId"] = PFUser.current()?.objectId ?? "" submittedImage["uploadedByUserName"] = PFUser.current()?.username ?? ""
if !txtIsPlaceHolder { submittedImage["description"] = imageDescription.text }
submittedImage.saveInBackground { (success, error) in
if success {
let message = "Save in bg worked"
print(message)
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertAction.Style.default, handler: {
(action) in
self.dismiss(animated: true, completion: nil)
}))
self.present(alert,animated: true, completion: nil)
} else {
print(error?.localizedDescription ?? "")
}
}
}
}
The code gives me this build error:
Implicit use of 'self' in closure; use 'self.' to make capture semantics explicit
In your tapDone method, you need to utilize the completion of the dismissal of your controller, like so:
self.dismiss(animated: true) {
if let image = newImage {
self.submit2Parse(image: image)
}
}
This only means that the self.submit2Parse(image: image) will ONLY be executed after you dismiss the controller. Additionally, the error
Implicit use of 'self' in closure; use 'self.' to make capture
semantics explicit
only means that you need to use self. to explicitly call a variable or method when you're inside a closure. So your submit2Parse... would now be like this:
self.submit2Parse(image: image).
If I understood your question correctly you want to dismiss your SecondViewController when user tap on tapDone button. And after that once your image upload complete then you want to present alert for success.
But if you dismiss it once button tapped you won't get any alert because your SecondViewController is no longer in window hierarchy and if you will try to present the alert you will get an error in Console like:
Attempt to present on
whose view is not in the
window hierarchy!
To solve this problem you need to present a alert from your first view controller which will appear after you dismiss your second view controller and you can achieve it with delegate.
Consider the below example:
First of all declare a protocol outside your FirstViewController:
protocol UplaodSuccessDelegate:class {
func uploadSuccess(message: String)
}
then confirm your delegate to same class:
class ViewController: FirstViewController, UplaodSuccessDelegate {
then you need to pass the delegate when you present SecondViewController:
let vc = self.storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
vc.delegate = self. //Pass delegate
self.present(vc, animated: true, completion: nil)
and add delegate method to same class:
func uploadSuccess(message: String) {
let message = "Save in bg worked"
print(message)
let alert = UIAlertController(title: "title", message: message, preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertAction.Style.default, handler: {
(action) in
}))
self.present(alert,animated: true, completion: nil)
}
now in your SecondViewController you need to add
weak var delegate: UplaodSuccessDelegate?
and in your tapDone method replace a code with:
self.dismiss(animated: true) {
if let image = newImage {
submit2Parse(image: image)
}
}
and once your upload complete you need to call delegate method (once your upload completes) like:
self.delegate?.uploadSuccess(message: "your message")
This will call your delegate method from FirstViewController.
When you drop the function below into the viewController that initiates the data upload in the background, and then call this function in the closure that fires when the submit completes it is able to successfully create an alert even thought the initial view controller was dismissed (or long dismissed if it was a long upload).
// This is the function that performs my background upload
func submit2Parse (image: UIImage) {
if let pfImage = image2PFFile(image: image) {
// Insert PFFile into parse server
let submittedImage = PFObject(className: "Images")
submittedImage["imageFile"] = pfImage
submittedImage["imageCreateDt"] = newImageCreateDate
submittedImage["type"] = "submittedFromUserHome"
submittedImage["bride"] = brideSwitch.isOn
submittedImage["groom"] = groomSwitch.isOn
submittedImage["user"] = userSwitch.isOn
submittedImage["picturePeriod"] = pickerSelected
submittedImage["uploadedByUserId"] = PFUser.current()?.objectId ?? ""
submittedImage["uploadedByUserName"] = PFUser.current()?.username ?? ""
if !txtIsPlaceHolder { submittedImage["description"] = imageDescription.text }
// Get the image timestamp, every photo has one
// How do you get the thumbnail image too?
submittedImage.saveInBackground { (success, error) in
if success {
let message = "Save in bg worked"
print(message)
self.showAlertFromAppDelegates()
} else {
print(error?.localizedDescription ?? "")
}
}
}
}
// This is the function that creates the alert
func showAlertFromAppDelegates(){
var topWindow: UIWindow? = UIWindow(frame: UIScreen.main.bounds)
topWindow?.rootViewController = UIViewController()
topWindow?.windowLevel = UIWindow.Level.alert + 1
let alert: UIAlertController = UIAlertController(title: "Upload Complete", message: "Your image was successfully submitted!", preferredStyle: .alert)
alert.addAction(UIAlertAction.init(title: "OK", style: .default, handler: { (alertAction) in
topWindow?.isHidden = true
topWindow = nil
}))
topWindow?.makeKeyAndVisible()
topWindow?.rootViewController?.present(alert, animated: true, completion:nil)
}
I declared a global variable for UIAlertViewController for me to be able to show and dismiss it in different method inside my class.
I displayed two kinds of alert: First, alert with button which will be displayed when an error is encountered or to display an information message. Second is an alert without button which will be displayed like a progress message.
Here is the sample code:
private var alert: UIAlertController? // global declaration
private func showProgressMessage(sender viewController: UIViewController, message alertMessage: String)
{
DispatchQueue.main.async
{
self.alert= UIAlertController(title: "", message: alertMessage, preferredStyle: .alert)
viewController.present(self.alert!, animated: true, completion: nil)
}
}
private func showAlertMessage(sender viewController: UIViewController, title alertTitle: String, message alertMessage: String)
{
DispatchQueue.main.async
{
self.alert= UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)
self.alert!.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
viewController.present(self.alert!, animated: true, completion: nil)
}
}
private func method1()
{
DispatchQueue.global().async
{
// some code here
self.showProgressMessage(sender: self, message: "Processing...")
// some code here
}
}
private func method2()
{
// some code here
self.alert!.dismiss(animated: false)
{
self.showAlertMessage(sender: self, message: "Done")
}
self.displayOtherViewController()
}
private func displayOtherViewController()
{
self.alert?.dismiss(animated: false)
{
if let viewController = self.storyboard?.instantiateViewController(withIdentifier: "Sample")
{
let view = viewController as! SampleViewController
view .modalTransitionStyle = .crossDissolve
self.present(view , animated: true, completion: nil)
}
}
}
In method2, displaying the alert again will take a few seconds to display, same with the view controller.
What is the proper way to show and dismis the UIAlertController in Swift 4?
Seems like your code is initiated from a background thread.
Even dismiss must be called on main thread
Try this:
private func method2() {
DispatchQueue.main.async {
self.alert!.dismiss(animated: false) {
self.showAlertMessage(sender: self, message: "Done")
}
self.displayOtherViewController()
}
}
I have a variable videoURL of type NSURL.
If I call println(videoURL) it would return something like this:
http://files.parsetfss.com/d540f71f-video.mp4
I have a button set up that should take this videoURL and save the video to the user's camera roll.
The best I have done is this:
UISaveVideoAtPathToSavedPhotosAlbum(videoPath: String!, completionTarget: AnyObject!, completionSelector: Selector, contextInfo: UnsafeMutablePointer<Void>)
While I'm not even sure if this will work or not, I can't figure out how to convert videoFile:NSURL into a videoPath.
Any help is appreciated on this.
Edit:
The following is unsuccessful:
UISaveVideoAtPathToSavedPhotosAlbum(videoURL.relativePath, self, nil, nil)
AssetsLibrary is deprecated
1: import Photos
import Photos
2: Use this code to save video from url to camera library.
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(nsUrlToYourVideo)
}) { saved, error in
if saved {
let alertController = UIAlertController(title: "Your video was successfully saved", message: nil, preferredStyle: .Alert)
let defaultAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertController.addAction(defaultAction)
self.presentViewController(alertController, animated: true, completion: nil)
}
}
Swift 3 & Swift 4
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: urlToYourVideo)
}) { saved, error in
if saved {
let alertController = UIAlertController(title: "Your video was successfully saved", message: nil, preferredStyle: .alert)
let defaultAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(defaultAction)
self.present(alertController, animated: true, completion: nil)
}
}
The accepted answer no longer works with Swift 3.0 & iOS 10.
First, you need to set the following permission in your app's plist file:
Privacy - Photo Library Usage Description
Provide a string that is presented to the user explaining why you are requesting the permission.
Next, import photos:
import Photos
Finally, here is the updated code for Swift 3.0:
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: fileURL)
}) { saved, error in
if saved {
let alertController = UIAlertController(title: "Your video was successfully saved", message: nil, preferredStyle: .alert)
let defaultAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(defaultAction)
self.present(alertController, animated: true, completion: nil)
}
}
To save video from NSURL to user camera roll
func video(videoPath: NSString, didFinishSavingWithError error: NSError?, contextInfo info: AnyObject)
{
if let _ = error {
print("Error,Video failed to save")
}else{
print("Successfully,Video was saved")
}
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let conversationField = self.conversation {
if (mediaType?.isEqual((kUTTypeMovie as NSString) as String))!
{
let theVideoURL: URL? = (info[UIImagePickerControllerMediaURL] as? URL)
if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum((theVideoURL?.path)!))
{
UISaveVideoAtPathToSavedPhotosAlbum((theVideoURL?.path)!, self, #selector(ConversationDetailsViewController.video(videoPath:didFinishSavingWithError:contextInfo:)), nil)
}
}
self.dismiss(animated: true, completion: nil)
}
Reference from:: https://www.raywenderlich.com/94404/play-record-merge-videos-ios-swift
Try this instead for saving video in photo library in swift 4.2 and above
func requestAuthorization(completion: #escaping ()->Void) {
if PHPhotoLibrary.authorizationStatus() == .notDetermined {
PHPhotoLibrary.requestAuthorization { (status) in
DispatchQueue.main.async {
completion()
}
}
} else if PHPhotoLibrary.authorizationStatus() == .authorized{
completion()
}
}
func saveVideoToAlbum(_ outputURL: URL, _ completion: ((Error?) -> Void)?) {
requestAuthorization {
PHPhotoLibrary.shared().performChanges({
let request = PHAssetCreationRequest.forAsset()
request.addResource(with: .video, fileURL: outputURL, options: nil)
}) { (result, error) in
DispatchQueue.main.async {
if let error = error {
print(error.localizedDescription)
} else {
print("Saved successfully")
}
completion?(error)
}
}
}
}
Use of function
self.saveVideoToAlbum(/* pass your final url to save */) { (error) in
//Do what you want
}
Don't forgot to import Photos and add Privacy - Photo Library Usage Description to your info.plist
deprecated as of iOS 9
1: import AssetsLibrary
import AssetsLibrary
2: Use this code to save video from url to camera library.
ALAssetsLibrary().writeVideoAtPathToSavedPhotosAlbum(outputFileURL, completionBlock: nil)
Just use it and paste your video's url:
PHPhotoLibrary.sharedPhotoLibrary().performChanges({ () -> Void in
let createAssetRequest: PHAssetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(NSURL(string: /* your url */)!)!
createAssetRequest.placeholderForCreatedAsset
}) { (success, error) -> Void in
if success {
//popup alert success
}
else {
//popup alert unsuccess
}
}