This is my first app and I'm wondering if I have made a mistake with regards to using URLSession.shared dataTask because I am not seeing my app get new data which is frequently updated. I see the new json instantly in my browser when I refresh, but, my app apparently does not see it.
Will it ever get the new json data from my server without uninstalling the app?
There are some some similar question topics, such as How to disable caching from NSURLSessionTask however, I do not want to disable all caching. Instead, I want to know how this default object behaves in this scenario - How long is it going to cache it? If indeed the answer is forever, or until they update or reinstall the app, then I will want to know how to reproduce the normal browser based cache behavior, using if-modified-since header, but that is not my question here.
I call my download() function below gratuitously after the launch sequence.
func download(_ ch: #escaping (_ data: Data?, _ respone: URLResponse?, _ error: Error?) -> (), completionHandler: #escaping (_ sessionError: Error?) -> ()) {
let myFileURL: URL? = getFileURL(filename: self.getFilename(self.jsonTestName))
let myTestURL = URL(string:getURLString(jsonTestName))
let session = URLSession.shared
// now we call dataTask and we see a CH and it calls My CH
let task = session.dataTask(with: myTestURL!) { (data, response, error) // generic CH for dataTask
in
// my special CH
ch(data,response,error) // make sure the file gets written in this ch
}
task.resume() // no such thing as status in async here
}
Within the completion handler which I pass to download, I save the data with this code from "ch":
DispatchQueue.main.async {
let documentController = UIDocumentInteractionController.init(url: myFileURL!)
documentController.delegate = self as UIDocumentInteractionControllerDelegate
}
and then finally, I read the data within that same completion handler from disk as such:
let data = try Data(contentsOf: myFileURL!)
For clarification, my complete calling function from which I call download() with completion handler code.
func get_test(){ // download new tests
let t = testOrganizer
let myFileURL: URL? = t.getFileURL(filename:t.getFilename(t.jsonTestName))
t.download( { (data,response,error)
in
var status: Int! = 0
status = (response as? HTTPURLResponse)?.statusCode
if(status == nil) {
status = 0
}
if(error != nil || (status != 200 && status != 304)) {
let alertController = UIAlertController(title: "Error downloading", message:"Could not download updated test data. HTTP Status: \(status!)", preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default,handler: nil))
self.present(alertController, animated: true, completion: nil)
self.p.print("END OF COMPLETION HANDLER")
}
else {
let status = (response as! HTTPURLResponse).statusCode
print("Success: status = ", status)
self.p.print("WRITING FILE IN COMPLETION HANDLER")
do {
try data!.write(to: myFileURL!)
DispatchQueue.main.async {
let documentController = UIDocumentInteractionController.init(url: myFileURL!)
documentController.delegate = self as UIDocumentInteractionControllerDelegate
}
} catch {
// // _ = completionHandler(NSError(domain:"Write failed", code:190, userInfo:nil))
print("error writing file \(myFileURL!) : \(error)")
}
self.myJson = self.testOrganizer.readJson()
self.p.print("END OF COMPLETION HANDLER")
}
}, completionHandler: {
sessionError in
if(sessionError == nil) {
print("Downloaded and saved file successfully")
} else {
let alertController = UIAlertController(title: "get_tests", message:
"Failed to download new tests - " + sessionError.debugDescription, preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default,handler: nil))
self.present(alertController, animated: true, completion: nil)
}
})
}
Related
I'm attempting to show a loading spinner when I'm doing some network calls when my app first starts up from being closed. These network calls usually take a very small amount of time because they are GETs on a json string and some processing on them, but if they take longer than usual, I don't want my users trying to maneuver in the app without the data they need being there. So, I'm trying to show a spinner when these calls are going on. But the spinner never shows up. I had this working before I changed a lot of stuff, and now it's not working again, and I can't for the life of me figure out why.
Here's my viewDidLoad() method in my HomeViewController, where this information is pulled from the API and loaded into CoreData.
override func viewDidLoad() {
super.viewDidLoad()
self.showSpinner(onView: self.view)
let teamsByConferenceNetworkManager = TeamsByConferenceNetworkManager()
teamsByConferenceNetworkManager.getTeamsByConference(completion: { (data, error) in
guard let data = data else {
os_log("Could not unwrap teamsByConference data in LoginViewController.viewDidLoad()", type: .debug)
self.removeSpinner()
let _ = UIAlertAction(title: "Network unavailable", style: .cancel, handler: { (alert) in
alert.isEnabled = true
})
return
}
let dataModelManager = DataModelManager.shared
DispatchQueue.main.sync {
dataModelManager.loadTeamNamesByConference(teamNamesByConferenceName: data)
dataModelManager.loadGamesFromCoreData()
}
if let _ = dataModelManager.allGames {
self.removeSpinner()
return
} else {
let gamesNetworkManager = GamesNetworkManager()
gamesNetworkManager.getGames { (data, error) in
guard let data = data else {
os_log("Could not unwrap games data in LoginViewController.viewDidLoad()", type: .debug)
self.removeSpinner()
let _ = UIAlertAction(title: "Network unavailable", style: .cancel, handler: { (alert) in
alert.isEnabled = true
})
return
}
DispatchQueue.main.sync {
dataModelManager.loadGames(gameApiResponses: data)
}
}
}
})
self.removeSpinner()
}
You need to remove this
DispatchQueue.main.sync {
dataModelManager.loadGames(gameApiResponses: data)
}
}
}
})
self.removeSpinner(). <<<<<< this line
}
as the call is asynchronous and you remove the spinner directly after you add it with self.showSpinner(onView: self.view)
I am currently working on a IOS App using Swift 3. I am working on the login system. Verification of logging in works fine. The logic is that, if login succeeds, it goes to the next Screen. However, if user does not exist, it should display an error message with UIAlert. But when I try to display the UIAlert, i get an error that says "Assertion failure in -[UIKeyboardTaskQueue waitUntilAllTasksAreFinished]"
//Getting data from database
func getData() -> Void {
let url: String = "http://localhost/fridge_app/login.php" //this will be changed to the path where service.php lives
//created NSURL
let requestURL = NSURL(string: url)
//creating NSMutableURLRequest
var request = URLRequest(url: requestURL! as URL)
//setting the method to post
request.httpMethod = "POST"
//Getting values from textfield
let usernameVal = username.text
let passwordVal = password.text
//creating the post parameter by concatenating the keys and values from text field
let postString = "username=\(usernameVal!)&password=\(passwordVal!)";
print(postString)
request.httpBody = postString.data(using: String.Encoding.utf8)
//creating a task to send the post request
let task = URLSession.shared.dataTask(with: request as URLRequest){
data, response, error in
//exiting if there is some error
if error != nil{
print("error is \(error)")
return;
}
// Print out response string
var responseString: NSString?;
responseString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
if(responseString == "invalid"){
self.isValid = false;
print(self.isValid)
}
if self.checkLogin(data: responseString!) == true {
self.performSegue(withIdentifier: "profileViewController", sender: self)
}
else{
print("Hello")
// It prints hello fine, but when it tries to run the showAlert function it fails
self.showAlert()
}
//print("responseString = \(self.responseString)")
}
//executing the task
task.resume()
}
This is the alert function
/*
* Show UIAlert Message
*/
func showAlert() -> Void{
let alert = UIAlertController(title: "User Does Not Exist",
message: "",
preferredStyle: UIAlertControllerStyle.alert)
let loginFail = UIAlertAction(title: "Close", style: .default, handler: nil);
alert.addAction(loginFail);
present(alert, animated: true)
}
This is method is called when user clicks login.
Unless you take special steps, the completion handlers for the tasks you submit to NSURLSession get run on a background thread. That means that any UI calls you do must be sent to the main thread or they don't work correctly (And may crash your app.)
The code in your completion handler is doing more UI work than just invoking an alert. You're also invoking a segue. If your completion handler doesn't do time-consuming work you might want to wrap the whole thing in a GCD call to the main thread:
DispatchQueue.main.async() {
//Put the entire body of your completion handler in here
}
Otherwise, you'll need to individually wrap each UI call in a call to the main queue like above.
EDIT:
Looking at your specific completion handler, you have one if/then/else block that does UIKit calls:
if self.checkLogin(data: responseString!) {
self.performSegue(withIdentifier: "profileViewController",
sender: self)
} else {
print("Hello")
self.showAlert()
}
So just wrap that part in a call to the main queue:
DispatchQueue.main.async() {
if self.checkLogin(data: responseString!) {
self.performSegue(withIdentifier: "profileViewController",
sender: self)
} else {
print("Hello")
self.showAlert()
}
}
I am relatively new to iOS development and Swift but I have an app I'm working on which is supposed to record the activity on the screen and save the resulting video to the camera roll. I am using ReplayKit.
What is working now:
This is the code I have beginning the recording and ending the recording
the startRecording() function is run by a button that says "start" and the stopRecording() function is called by a button that says "stop".
var preview : RPPreviewViewController?
func startRecording() {
let recorder = RPScreenRecorder.sharedRecorder()
recorder.startRecordingWithMicrophoneEnabled(true) {
[unowned self] (error) in
print(recorder)
if let unwrappedError = error {
print(unwrappedError.localizedDescription)
}
}
}
func stopRecording() {
let recorder = RPScreenRecorder.sharedRecorder()
recorder.stopRecordingWithHandler {
[unowned self] (preview, error) in
if let unwrappedError = error {
print(unwrappedError.localizedDescription)
}
if let unwrappedPreview = preview {
print("end")
unwrappedPreview.previewControllerDelegate = self
unwrappedPreview.modalPresentationStyle=UIModalPresentationStyle.FullScreen
self.presentViewController(unwrappedPreview, animated: true, completion: nil)
}
}
The screen records fine. I have a button which says "Finish" which will call the stopRecording() function. When that button is clicked, a preview will show up which will play the recorded video and allow the user to manually edit and save the video.
What I'm trying to do:
I need to make the button simply save the video as is to the camera roll. I want to bypass the preview screen which allows the user to edit and manually save. Is this possible? If so, how would you approach the problem?
The preview is of type RPPreviewViewController? and try as I might, I just can't seem to access the video for saving. Since ReplayKit is an extension of UIKit, I tried using the
UISaveVideoAtPathToSavedPhotosAlbum(_ videoPath: String, _ completionTarget: AnyObject?, _ completionSelector: Selector, _ contextInfo: UnsafeMutablePointer<Void>)
method but none of those attributes exist!
If you need anymore info, please let me know. If I'm an idiot, please let me know! This is my first post here so be nice! and Thanks.
As mentioned by Geoff H, Replay Kit 2 now allows you to record the screen and save it either within your app or to the gallery without having to use the preview.
The documentation is sparse but after some trial and experiment the below code works in iOS 12.
Note this only captures video and not audio, although that should be straightforward to add, and you may want to add more error checking if using it. The functions below can be triggered by UI buttons, for example.
#objc func startRecording() {
//Use ReplayKit to record the screen
//Create the file path to write to
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
self.videoOutputURL = URL(fileURLWithPath: documentsPath.appendingPathComponent("MyVideo.mp4"))
//Check the file does not already exist by deleting it if it does
do {
try FileManager.default.removeItem(at: videoOutputURL)
} catch {}
do {
try videoWriter = AVAssetWriter(outputURL: videoOutputURL, fileType: AVFileType.mp4)
} catch let writerError as NSError {
os_log("Error opening video file", writerError);
videoWriter = nil;
return;
}
//Create the video settings
let videoSettings: [String : Any] = [
AVVideoCodecKey : AVVideoCodecType.h264,
AVVideoWidthKey : 1920, //Replace as you need
AVVideoHeightKey : 1080 //Replace as you need
]
//Create the asset writer input object whihc is actually used to write out the video
//with the video settings we have created
videoWriterInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoSettings);
videoWriter.add(videoWriterInput);
//Tell the screen recorder to start capturing and to call the handler when it has a
//sample
RPScreenRecorder.shared().startCapture(handler: { (cmSampleBuffer, rpSampleType, error) in
guard error == nil else {
//Handle error
os_log("Error starting capture");
return;
}
switch rpSampleType {
case RPSampleBufferType.video:
os_log("writing sample....");
if self.videoWriter.status == AVAssetWriter.Status.unknown {
if (( self.videoWriter?.startWriting ) != nil) {
os_log("Starting writing");
self.videoWriter.startWriting()
self.videoWriter.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(cmSampleBuffer))
}
}
if self.videoWriter.status == AVAssetWriter.Status.writing {
if (self.videoWriterInput.isReadyForMoreMediaData == true) {
os_log("Writting a sample");
if self.videoWriterInput.append(cmSampleBuffer) == false {
print(" we have a problem writing video")
}
}
}
default:
os_log("not a video sample, so ignore");
}
} )
}
#objc func stoprecording() {
//Stop Recording the screen
RPScreenRecorder.shared().stopCapture( handler: { (error) in
os_log("stopping recording");
})
self.videoWriterInput.markAsFinished();
self.videoWriter.finishWriting {
os_log("finished writing video");
//Now save the video
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: self.videoOutputURL)
}) { 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)
}
if error != nil {
os_log("Video did not save for some reason", error.debugDescription);
debugPrint(error?.localizedDescription ?? "error is nil");
}
}
}
I too wanted to do what you have asked, but as of now RPScreenRecorder doesn't provide any of those functionalities.
Yes, you can. Check this ReplayKit2 Swift 4:
https://medium.com/#giridharvc7/replaykit-screen-recording-8ee9a61dd762
Once you have the file, it shouldn't be too much trouble to save it to the camera roll with something along the lines of:
static func saveVideo(url: URL, returnCompletion: #escaping (String?) -> () ) {
DispatchQueue.global(qos: .userInitiated).async {
guard let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
if !FileManager.default.fileExists(atPath: documentsDirectoryURL.appendingPathComponent(url.lastPathComponent).path) {
URLSession.shared.downloadTask(with: url) { (location, response, error) -> Void in
guard let location = location else { return }
let destinationURL = documentsDirectoryURL.appendingPathComponent(response?.suggestedFilename ?? url.lastPathComponent)
do {
try FileManager.default.moveItem(at: location, to: destinationURL)
PHPhotoLibrary.requestAuthorization({ (authorizationStatus: PHAuthorizationStatus) -> Void in
if authorizationStatus == .authorized {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: destinationURL)}) { completed, error in
DispatchQueue.main.async {
if completed { returnCompletion(url.lastPathComponent)
} else {
returnCompletion(nil)
}
}
}
}
})
returnCompletion(url.lastPathComponent)
} catch {
returnCompletion(nil)
}
}.resume()
} else {
returnCompletion(nil)
}
}
}
I am running into an error, when it hits:
self.videoWriterInput.markAsFinished();
It is giving me :
-[AVAssetWriterInput markAsFinished] Cannot call method when status is 0
I want to ask question about new UIAlertController. How can I detect the errors on custom class to show alert view to users ? I want to execute my alert view when the switch case goes the default statement. There is my NetworkOperation class which is custom class with closures for download some JSON data from web.
class NetworkOperation {
lazy var config: NSURLSessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
lazy var session: NSURLSession = NSURLSession(configuration: self.config)
let mainURL: NSURL
typealias JSONWeatherDataDictionary = ([String: AnyObject]?) -> Void
init(url: NSURL) {
mainURL = url
}
func downloadWeatherDataFromWeb(completion: JSONWeatherDataDictionary) {
let request = NSURLRequest(URL: mainURL)
let dataTask = session.dataTaskWithRequest(request) {
(let data, response, error) in
if let httpResponse = response as? NSHTTPURLResponse {
switch httpResponse.statusCode {
case 200:
// HTTP Response success use the weather data !
do {
let weatherDataDictionary = try NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers) as? [String: AnyObject]
completion(weatherDataDictionary)
} catch {
print("Data can't get")
}
default:
// I want to execute my alert view in there. showAlertView()
print("Http Response failed with this code: \(httpResponse.statusCode)")
}
} else {
print("HTTP Response convert fail !")
}
}
dataTask.resume()
}
}
If the default case is executed, how can I see my alert view in view controller ? I tried to solve this problem with UIAlertViewDelegate but its deprecated for iOS9 so I want to learn best common way to solve this problem.
Thank you everyone...
If all you would like to do is to present an alert that lets the user know about the error, providing along a cancel button to dismiss the button, you could make use of UIAlertController.
I tested the following code with a HTTP URL, causing it to fail, the alert view pops up displaying the status code; and upon tapping on the "cancel" button, dismisses itself.
func downloadWeatherDataFromWeb(completion: JSONWeatherDataDictionary)
{
let request = NSURLRequest(URL: mainURL)
let dataTask = session.dataTaskWithRequest(request)
{
(let data, response, error) in
if let httpResponse = response as? NSHTTPURLResponse
{
switch httpResponse.statusCode
{
case 200:
do
{
let weatherDataDictionary = try NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers) as? [String: AnyObject]
completion(weatherDataDictionary)
}
catch
{
print("Could not retrieve data")
}
default:
print("Http Response failed with the following code: \(httpResponse.statusCode)")
let alert = UIAlertController(title: "HTTP Error", message:String(httpResponse.statusCode), preferredStyle: UIAlertControllerStyle.Alert)
//set up cancel action
let cancelAction: UIAlertAction = UIAlertAction(title: "Cancel", style: .Cancel)
{
action -> Void in
//should you want to carry some other operations upon canceling
}
//add the action
alert.addAction(cancelAction)
dispatch_async(dispatch_get_main_queue(),
{
self.presentViewController(alert, animated:true, completion:
{
//alert controller presented
})
})
}
}
else
{
print("HTTP Response conversion failed!")
}
}
dataTask.resume()
}
Update: in response to your comment
Please add a UIViewController variable:
class NetworkOperation
{
var viewController : UIViewController = UIViewController()
....
And make modification to the default: case above:
dispatch_async(dispatch_get_main_queue(),
{
self.addChildViewController(self.viewController)
self.view.addSubview(self.viewController.view)
self.viewController.didMoveToParentViewController(self)
self.viewController.presentViewController(alert, animated:true,
completion:
{
//alert controller presented
})
})
I just took the liberty of testing it a minute ago; it pops and dismisses.
Another Update:
Then, we could do the following:
dispatch_async(dispatch_get_main_queue(),
{
let topViewController = UIApplication.sharedApplication().keyWindow?.rootViewController
topViewController!.presentViewController(alert, animated:true,
completion:
{
//alert controller presented
})
})
I am needing to implement a progress bar that takes into account a couple of factors.
I have three different classes, my ViewController, a Networking class to handle the network calls and a dataManager class to handle all the db operations.
Now my progressView lives in my viewcontroller and I am looking at a way of updating it as each of the different operations are performed in the other classes.
I am using Alamofire so I know I can use .progress{} to catch the value of the JSON progress but that would also mean exposing the ViewController to the Networking class, which I assume is bad practice?
I think this should be achieved using completion handlers but as I have already setup another thread for handling the JSON / DB operation I'm not wanting to over complicate it anymore than I need to
Networking:
func makeGetRequest(url : String, params : [String : String]?, completionHandler: (responseObject: JSON?, error: NSError?) -> ()) -> Request? {
return Alamofire.request(.GET, url, parameters: params, encoding: .URL)
.progress { _, _, _ in
//bad practice?
progressView.setProgress(request.progress.fractionCompleted, animated: true)
}
.responseJSON { request, response, data, error in completionHandler(
responseObject:
{
let json = JSON(data!)
if let anError = error
{
println(error)
}
else if let data: AnyObject = data
{
let json = JSON(data)
}
return json
}(),
error: error
)
}
}
ViewController:
dataManager.loadData({(finished: Bool, error:NSError?) -> Void in
if let errorMessage = error{
self.syncProgress.setProgress(0, animated: true)
let alertController = UIAlertController(title: "Network Error", message:
errorMessage.localizedDescription, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default,handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
}
if finished{
for i in 0..<100 {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), {
sleep(1)
dispatch_async(dispatch_get_main_queue(), {
self.counter++
return
})
})
}
}
})
As you can see I am waiting on the finished boolean in the datamanger class to be set before updating the progress bar. The thing is, dataManager makes a call to networking and performs a bunch of other stuff before it finishes, it would be handy to update the progress bar along the way but I'm not sure of the best approach?
DataManager:
func loadData(completion: (finished: Bool, error: NSError?) -> Void) {
var jsonError: NSError?
networking.makeGetRequest(jobsUrl, params: nil) { json, networkError in
//....
}
I'm not too familiar with swift so I can't give you a code example but the way I would do this is create a protocol on your Networking class NetworkingDelegate and implement that protocol in your ViewController. The protocol method would be something like (in objective-c) NetworkingRequestDidUpdatProgress:(float progress)
This is assuming your ViewController calls Networking.makeGetRequest. If it's another class, you would implement the delegate in that class, or you could bubble up the delegate calls to your ViewController through the DataManager class.