In Xcode 5.0.2 I have created a blank Master Detail application for iPhone and it works okay in the simulator:
When the iPhone app is being started or awaken from a background, I would like to display a modal view with a Label "Loading..." in its middle, fetch a web page (in this test case; in the real app this will be game updates and player scores) and then dismiss the modal view on the web page fetch completion or error or timeouts.
So I have created 2 new files, LoadingViewController.h and LoadingViewController.m (with no custom code by me for now).
And since it is Xcode version 5, there are no xib files, but a Main.storyboard - so I have dragged a View Controller from the Object Library onto the storyboard. Then on the right side I have selected the LoadingViewController class as the Custom Class in the Identity Inspector:
And finally I have added 3 methods to the AppDelegate.m:
- (void)applicationDidBecomeActive:(UIApplication *)application
{
[self showLoadingView];
}
- (void)applicationWillResignActive:(UIApplication *)application
{
[self dismissLoadingView];
}
- (void)showLoadingView
{
NSLog(#"%s", __PRETTY_FUNCTION__);
[self fetchHttp];
LoadingViewController *other = [[LoadingViewController alloc] init];
[self.window.rootViewController presentViewController:other animated:YES completion:nil];
}
- (void)dismissLoadingView
{
NSLog(#"%s", __PRETTY_FUNCTION__);
[self.window.rootViewController dismissViewControllerAnimated:YES completion:nil];
}
- (void)fetchHttp
{
NSLog(#"%s", __PRETTY_FUNCTION__);
NSString *urlAsString = #"http://stackoverflow.com";
NSURL *url = [NSURL URLWithString:urlAsString];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection
sendAsynchronousRequest:urlRequest
queue:queue
completionHandler:^(NSURLResponse *response,
NSData *data,
NSError *error) {
if ([data length] > 0 &&
error == nil) {
NSString *html = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
NSLog(#"HTML = %u", [html length]);
}
else if ([data length] == 0 &&
error == nil) {
NSLog(#"Nothing was downloaded.");
}
else if (error != nil) {
NSLog(#"Error happened = %#", error);
}
// XXX how to dismiss the modal view here, it's a different thread?
}];
}
Unfortunately now I get a black screen in the simulator and the following output:
2013-12-01 22:37:01.332 LoadingTest[3840:a0b] -[AppDelegate showLoadingView]
2013-12-01 22:37:01.334 LoadingTest[3840:a0b] -[AppDelegate fetchHttp]
2013-12-01 22:37:01.857 LoadingTest[3840:a0b] Unbalanced calls to begin/end appearance transitions for <UINavigationController: 0x8c74f10>.
2013-12-01 22:37:01.870 LoadingTest[3840:4607] HTML = 196885
It is difficult for me to understand, how to use storyboard here (and I'd like to use it, if possible) - because the books I'm reading (in O'Reilly Safari) all talk about xib files (probably for the older Xcode versions?).
And also I don't understand how to dismiss the modal view from my completionHandler since it is in a different thread and I probably shouldn't call dismissViewControllerAnimated from there?
UPDATE:
I've added a "Storyboard ID" to my View: loadingView and the following code to AppDelegate.m:
- (void)showLoadingView
{
NSLog(#"%s", __PRETTY_FUNCTION__);
[self fetchHttp];
UIStoryboard *board = [self.window.rootViewController storyboard]; //[UIStoryboard storyboardWithName:#"Main.storyboard" bundle:nil];
LoadingViewController *other = [board instantiateViewControllerWithIdentifier:#"loadingView"];
[self.window.rootViewController presentViewController:other animated:YES completion:nil];
}
- (void)fetchHttp
{
NSLog(#"%s", __PRETTY_FUNCTION__);
NSString *urlAsString = #"http://stackoverflow.com";
NSURL *url = [NSURL URLWithString:urlAsString];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
[NSURLConnection
sendAsynchronousRequest:urlRequest
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response,
NSData *data,
NSError *error) {
if ([data length] > 0 &&
error == nil) {
NSString *html = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
NSLog(#"HTML = %u", [html length]);
}
else if ([data length] == 0 &&
error == nil) {
NSLog(#"Nothing was downloaded.");
}
else if (error != nil) {
NSLog(#"Error happened = %#", error);
}
[self dismissLoadingView];
}];
}
but now I get the warning below and the loadingView isn't dismissed (probably because the web page loads faster than the modal view is shown?):
2013-12-03 01:49:12.208 LoadingTest[631:70b] -[AppDelegate showLoadingView]
2013-12-03 01:49:12.210 LoadingTest[631:70b] -[AppDelegate fetchHttp]
2013-12-03 01:49:12.756 LoadingTest[631:70b] HTML = 200949
2013-12-03 01:49:12.757 LoadingTest[631:70b] -[AppDelegate dismissLoadingView]
2013-12-03 01:49:12.757 LoadingTest[631:70b] Warning: Attempt to dismiss from view controller <UINavigationController: 0x8a70ce0> while a presentation or dismiss is in progress!
2013-12-03 01:49:12.844 LoadingTest[631:70b] Unbalanced calls to begin/end appearance transitions for <UINavigationController: 0x8a70ce0>.
First thing, when you instantiate a controller in a storyboard, you don't use alloc init, you use the UIStoryboard method instantiateViewControllerWithIdentifier:. You need to give your controller a "Storyboard ID", which I can see from your image that you haven't done yet (and if you don't understand storyboards, read Apple's documents about it).
You can dismiss your modal view from the completion handler -- the handler is the code that's called after the async operation finishes, so you you should use [NSOperationQueue mainQueue] as the queue argument.
Related
I have a 'Terms and Conditions' controller I represent in the first launch of the app, but when opening the app in the first time from the App Store page the Terms and Conditions' controller not shown- only after I close the app and reopen it from the device itself (not the App Store page) then the controller is shown.
this code is from the launched screen controller:
- (void)viewDidLoad {
[self agreedToServerTerms];
}
- (void)agreedToServerTerms {
[[HttpUtils instance] httpRequest:TERMS_AGREEMENT_URL :params completionHandler:^(NSData *data, NSError *error) {
#try {
bool acceptTerms = false;
if (error) {
[Utils log:[NSString stringWithFormat:#"agreedToServerTerms error=%#", error]];
} else {
NSDictionary *jsonData = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!error) {
acceptTerms = [jsonData.allValues[0] boolValue];
if (!acceptTerms) {
TermsAndConditionController *tac = [[TermsAndConditionController alloc] initWithNibName:#"TermsAndConditionController" bundle:nil];
tac.delegate = self;
[[SlideNavigationController sharedInstance] pushViewController:tac animated:false];
}
else {
[self performSelectorInBackground:#selector(initialApp) withObject:nil];
}
} else {
[Utils log:[NSString stringWithFormat:#"parsing jsonData error = %#" ,error]];
}
}
} #catch (NSException *e) {
[Utils log:[NSString stringWithFormat:#"Exception occurred: %#, %#", e, [e userInfo]]];
}
}];
}
from App delegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
LaunchScreenController *bbp = [[LaunchScreenController alloc] initWithNibName:#"LaunchScreenController" bundle:nil];
[[SlideNavigationController sharedInstance] pushViewController:bbp animated:YES];
if (_launchedURL) {
bbp.launchedURL = _launchedURL;
}
[self.window addSubview:bbp.view];
}
herewith how I do almost the exact same thing. I think you need to rework yours, it could be a bit different, but this may help.
In the first VC that my app displays, in its viewWillAppear message, I have the following code
// Terms of use
if ( [NSUserDefaults.standardUserDefaults integerForKey:#"tou"] != 20170614 )
{
[self performSegueWithIdentifier:#"tou" sender:nil];
[NSUserDefaults.standardUserDefaults setInteger:20170614 forKey:#"tou"];
}
This uses user defaults to note if the terms of use have ever been presented and, if so, it marks #"tou" with some arbitrary value. In your case you can set the logic in your controller and only mark it once the user accepts.
This requires a segue in the storyboard called #"tou" that will present your T&C controller and you may need to prevent exit if the user does not agree, but the idea is to segue away from your first VC if the user did not agree yet to present the user with the T&C rather than injecting it into the app delegate as you do at present.
I have a UIView that contains a progress bar. What I want to do is simple, I have a button, user clicks that button, app downloads file and show progress in progress bar. I am able to do this when the user clicks the download button the first time. But when the user clicks the second time to download again, NSURLSession delegates are not called.
My UIView .m
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self configure];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
[self configure];
}
return self;
}
-(void)configure
{
[self createSpinner];
[self createProgressBar];
NSArray *URLs = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
self.docDirectoryURL = [URLs objectAtIndex:0];
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"com.tinkytickles"];
sessionConfiguration.HTTPMaximumConnectionsPerHost = 1;
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
-(void)createSpinner
{
[self setBackgroundColor:[UIColor colorWithWhite:1.0f alpha:0.5f]];
spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self addSubview:spinner];
[spinner setColor:original_new_dark_grey];
[spinner setUserInteractionEnabled:NO];
[spinner setCenter:CGPointMake([[UIScreen mainScreen] bounds].size.width/2, [[UIScreen mainScreen] bounds].size.height/2)];
[spinner setFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
[spinner startAnimating];
}
-(void)createProgressBar
{
self.progressBar = [[TYMProgressBarView alloc] initWithFrame:CGRectMake(0, 0, 280, 15)];
[self.progressBar setBarBackgroundColor:[UIColor whiteColor]];
[self.progressBar setBarBorderColor:original_new_dark_grey];
[self.progressBar setBarFillColor:original_new_dark_grey];
[self.progressBar setBarBorderWidth:1.0f];
[self addSubview:self.progressBar];
[self.progressBar setCenter:CGPointMake([[UIScreen mainScreen] bounds].size.width/2, [[UIScreen mainScreen] bounds].size.height/2)];
[self.progressBar setHidden:YES];
self.label = [[UILabel alloc] initWithFrame:CGRectMake(self.progressBar.frame.origin.x, self.progressBar.frame.origin.y - 30, self.progressBar.frame.size.width, 25)];
[self.label setText:NSLocalizedString(locDownloading, nil)];
[self.label setTextAlignment:NSTextAlignmentCenter];
[self.label setTextColor:original_new_dark_grey];
[self.label setFont:quicksand_14];
[self addSubview:self.label];
[self.label setHidden:YES];
}
-(void)showProgressBarWithProgress:(CGFloat)progress withText:(NSString *)text
{
[spinner setHidden:YES];
[self.label setText:[NSString stringWithFormat:NSLocalizedString(locDownloadingAt, nil), text]];
[self.label setHidden:NO];
[self.progressBar setHidden:NO];
[self.progressBar setProgress:progress];
}
-(void)stopAnimating
{
[spinner stopAnimating];
}
-(void)startDownloadingURL:(PromoterDownloadInfo *)downloadInfo
{
info = downloadInfo;
if (!info.isDownloading)
{
if (info.taskIdentifier == -1)
{
info.downloadTask = [self.session downloadTaskWithURL:[NSURL URLWithString:info.downloadSource]];
info.taskIdentifier = info.downloadTask.taskIdentifier;
[info.downloadTask resume];
}
else
{
info.downloadTask = [self.session downloadTaskWithResumeData:info.taskResumeData];
[info.downloadTask resume];
info.taskIdentifier = info.downloadTask.taskIdentifier;
}
}
else
{
[info.downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
if (resumeData != nil) {
info.taskResumeData = [[NSData alloc] initWithData:resumeData];
}
}];
}
info.isDownloading = !info.isDownloading;
}
-(void)stopDownload:(PromoterDownloadInfo *)downloadInfo
{
if (!info.isDownloading)
{
if (info.taskIdentifier == -1)
{
info.downloadTask = [self.session downloadTaskWithURL:[NSURL URLWithString:info.downloadSource]];
}
else
{
info.downloadTask = [self.session downloadTaskWithResumeData:info.taskResumeData];
}
info.taskIdentifier = info.downloadTask.taskIdentifier;
[info.downloadTask resume];
info.isDownloading = YES;
}
[self stopAnimating];
[self removeFromSuperview];
}
#pragma mark - NSURLSession Delegate method implementation
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *destinationFilename = downloadTask.originalRequest.URL.lastPathComponent;
NSURL *destinationURL = [self.docDirectoryURL URLByAppendingPathComponent:destinationFilename];
if ([fileManager fileExistsAtPath:[destinationURL path]]) {
[fileManager removeItemAtURL:destinationURL error:nil];
}
BOOL success = [fileManager copyItemAtURL:location
toURL:destinationURL
error:&error];
if (success) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self stopAnimating];
[self removeFromSuperview];
}];
}
else
{
NSLog(#"Unable to copy temp file. Error: %#", [error localizedDescription]);
}
}
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
if (error != nil) {
NSLog(#"Download completed with error: %#", [error localizedDescription]);
}
else{
NSLog(#"Download finished successfully.");
}
}
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
if (totalBytesExpectedToWrite == NSURLSessionTransferSizeUnknown) {
NSLog(#"Unknown transfer size");
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
info.downloadProgress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;
[self showProgressBarWithProgress:info.downloadProgress withText:info.fileTitle];
});
}
}
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
// Check if all download tasks have been finished.
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([downloadTasks count] == 0) {
if (appDelegate.backgroundTransferCompletionHandler != nil) {
// Copy locally the completion handler.
void(^completionHandler)() = appDelegate.backgroundTransferCompletionHandler;
// Make nil the backgroundTransferCompletionHandler.
appDelegate.backgroundTransferCompletionHandler = nil;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// Call the completion handler to tell the system that there are no other background transfers.
completionHandler();
// Show a local notification when all downloads are over.
UILocalNotification *localNotification = [[UILocalNotification alloc] init];
localNotification.alertBody = NSLocalizedString(locDownloadComplete, nil);
[[UIApplication sharedApplication] presentLocalNotificationNow:localNotification];
}];
}
}
}];
}
I use this UIView like this:
PromoterDownloadInfo *info = [[PromoterDownloadInfo alloc] initWithFileTitle:self.title andDownloadSource:#"https://www.mywebsite.com/file.zip"];
PromotersDownloadView *downloadView = [[PromotersDownloadView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[self.navigationController.view addSubview:downloadView];
[downloadView startDownloadingURL:info];
The first time I clicked the download button it works great. The second time NSURLSession only didCompleteWithError method gets called. Here is what I get from log the second time:
2016-05-12 00:50:47.440 APP[32990:1230071] A background URLSession with identifier com.app already exists!
2016-05-12 00:50:50.614 APP[32990:1230386] Download finished successfully.
What am I doing wrong? I tried to create NSURLSessionConfiguration only once but this way no delegate method gets called. What should I do?
You said:
The first time I clicked the download button it works great. ... Here is what I get from log the second time:
2016-05-12 00:50:47.440 APP[32990:1230071] A background URLSession with identifier com.app already exists!<br />
That error is pointing out that you want to instantiate only one background NSURLSession for a given identifier (and you generally only need/want a single background session). If you were going to instantiate multiple ones, you'd give them unique identifiers, but handling background sessions is complicated enough without unnecessarily having multiple sessions. I'd suggest that you only want a single background session.
You said:
I tried to create NSURLSessionConfiguration only once but this way no delegate method gets called.
Yes, you should have one session configuration. And, just as importantly, only one background session object.
I suspect that there's an issue with your delegate object not being able to keep track of which view it should be updating. Or perhaps you lost reference to your session object and your reference was nil. It could be a couple of different things, and it's hard to know without seeing how you did this.
I'd suggest moving this session configuration code out of the view, and have some shared instance that you can reference anywhere (e.g. a singleton works well, so you can instantiate it from wherever it's first needed, whether from a view or from the app delegate's handleEventsForBackgroundURLSession method).
The only challenge then is how to keep track of which views are keeping track of which network requests. Do you want to have a single view that will keep track of all incomplete requests, regardless of when this view is instantiated? If so, you can use NSNotificationCenter notifications (that way, any view that wants to be notified of progress updates can just observe your custom notification). Or does a given view only care about requests that you initiated from that view?In that case, you might maintain dictionary that maps taskIdentifier values to which view or object needs to know about the status updates (what way you can have your session object keep track of which views care about which tasks). It just depends upon your app's requirements.
Hi I am attempting to add in Braintrees drop In UI. I am following their guide on their website and have successfully added their cocoa pods to my project. I am now attempting to test out their Drop in UI. However when I attempt to execute the example code I get the following error.
Warning: Attempt to present <UINavigationController: 0x7dc09a00>
on <CheckOutViewController: 0x7c26ca20>
whose view is not in the window hierarchy!
Here is my source code I'm using
#import "CheckOutViewController.h"
#import "BraintreeCore.h"
#import "BraintreeUI.h"
#interface CheckOutViewController ()<BTDropInViewControllerDelegate>
#property (nonatomic, strong) BTAPIClient *braintreeClient;
#end
#implementation CheckOutViewController
- (void)viewDidLoad {
[super viewDidLoad];
// TODO: Switch this URL to your own authenticated API
/* NSURL *clientTokenURL = [NSURL URLWithString:#"https://braintree-sample-merchant.herokuapp.com/client_token"];
NSMutableURLRequest *clientTokenRequest = [NSMutableURLRequest requestWithURL:clientTokenURL];
[clientTokenRequest setValue:#"text/plain" forHTTPHeaderField:#"Accept"];
[[[NSURLSession sharedSession] dataTaskWithRequest:clientTokenRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// TODO: Handle errors
*/
NSString *clientToken = [[NSString alloc] init];
clientToken = #"eyJ2ZXJzaW9uIjoyLCJhdXRob3JpemF0aW9uRmluZ2VycHJpbnQiOiJiMzJlMDJmNmJkNjNkM2M5MzY2ZDg0YzEyNmI3ZDIyNmE0YTJjZDliYTQ1ZWNlYjk5ZGE5ZTY3NTlkOTAzYTgyfGNyZWF0ZWRfYXQ9MjAxNi0wMi0xOFQwNToxNDowOS44NzM0MDUzNTUrMDAwMFx1MDAyNm1lcmNoYW50X2lkPTM0OHBrOWNnZjNiZ3l3MmJcdTAwMjZwdWJsaWNfa2V5PTJuMjQ3ZHY4OWJxOXZtcHIiLCJjb25maWdVcmwiOiJodHRwczovL2FwaS5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tOjQ0My9tZXJjaGFudHMvMzQ4cGs5Y2dmM2JneXcyYi9jbGllbnRfYXBpL3YxL2NvbmZpZ3VyYXRpb24iLCJjaGFsbGVuZ2VzIjpbXSwiZW52aXJvbm1lbnQiOiJzYW5kYm94IiwiY2xpZW50QXBpVXJsIjoiaHR0cHM6Ly9hcGkuc2FuZGJveC5icmFpbnRyZWVnYXRld2F5LmNvbTo0NDMvbWVyY2hhbnRzLzM0OHBrOWNnZjNiZ3l3MmIvY2xpZW50X2FwaSIsImFzc2V0c1VybCI6Imh0dHBzOi8vYXNzZXRzLmJyYWludHJlZWdhdGV3YXkuY29tIiwiYXV0aFVybCI6Imh0dHBzOi8vYXV0aC52ZW5tby5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tIiwiYW5hbHl0aWNzIjp7InVybCI6Imh0dHBzOi8vY2xpZW50LWFuYWx5dGljcy5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tIn0sInRocmVlRFNlY3VyZUVuYWJsZWQiOnRydWUsInRocmVlRFNlY3VyZSI6eyJsb29rdXBVcmwiOiJodHRwczovL2FwaS5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tOjQ0My9tZXJjaGFudHMvMzQ4cGs5Y2dmM2JneXcyYi90aHJlZV9kX3NlY3VyZS9sb29rdXAifSwicGF5cGFsRW5hYmxlZCI6dHJ1ZSwicGF5cGFsIjp7ImRpc3BsYXlOYW1lIjoiQWNtZSBXaWRnZXRzLCBMdGQuIChTYW5kYm94KSIsImNsaWVudElkIjpudWxsLCJwcml2YWN5VXJsIjoiaHR0cDovL2V4YW1wbGUuY29tL3BwIiwidXNlckFncmVlbWVudFVybCI6Imh0dHA6Ly9leGFtcGxlLmNvbS90b3MiLCJiYXNlVXJsIjoiaHR0cHM6Ly9hc3NldHMuYnJhaW50cmVlZ2F0ZXdheS5jb20iLCJhc3NldHNVcmwiOiJodHRwczovL2NoZWNrb3V0LnBheXBhbC5jb20iLCJkaXJlY3RCYXNlVXJsIjpudWxsLCJhbGxvd0h0dHAiOnRydWUsImVudmlyb25tZW50Tm9OZXR3b3JrIjp0cnVlLCJlbnZpcm9ubWVudCI6Im9mZmxpbmUiLCJ1bnZldHRlZE1lcmNoYW50IjpmYWxzZSwiYnJhaW50cmVlQ2xpZW50SWQiOiJtYXN0ZXJjbGllbnQzIiwiYmlsbGluZ0FncmVlbWVudHNFbmFibGVkIjp0cnVlLCJtZXJjaGFudEFjY291bnRJZCI6ImFjbWV3aWRnZXRzbHRkc2FuZGJveCIsImN1cnJlbmN5SXNvQ29kZSI6IlVTRCJ9LCJjb2luYmFzZUVuYWJsZWQiOmZhbHNlLCJtZXJjaGFudElkIjoiMzQ4cGs5Y2dmM2JneXcyYiIsInZlbm1vIjoib2ZmIn0=";
NSLog(#"here we go again");
self.braintreeClient = [[BTAPIClient alloc] initWithAuthorization:clientToken];
// As an example, you may wish to present our Drop-in UI at this point.
// Continue to the next section to learn more...
// }] resume];
NSLog(#"HMM");
[self here];
}
-(void)here{
BTDropInViewController *dropInViewController = [[BTDropInViewController alloc]
initWithAPIClient:self.braintreeClient];
dropInViewController.delegate = self;
// This is where you might want to customize your view controller (see below)
// The way you present your BTDropInViewController instance is up to you.
// In this example, we wrap it in a new, modally-presented navigation controller:
UIBarButtonItem *item = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
target:self
action:#selector(userDidCancelPayment)];
dropInViewController.navigationItem.leftBarButtonItem = item;
UINavigationController *navigationController = [[UINavigationController alloc]
initWithRootViewController:dropInViewController];
[self presentViewController:navigationController animated:YES completion:nil];
}
- (void)userDidCancelPayment {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)dropInViewController:(BTDropInViewController *)viewController
didSucceedWithTokenization:(BTPaymentMethodNonce *)paymentMethodNonce {
// Send payment method nonce to your server for processing
[self postNonceToServer:paymentMethodNonce.nonce];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)postNonceToServer:(NSString *)paymentMethodNonce {
// Update URL with your server
NSURL *paymentURL = [NSURL URLWithString:#"https://your-server.example.com/checkout"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:paymentURL];
request.HTTPBody = [[NSString stringWithFormat:#"payment_method_nonce=%#", paymentMethodNonce] dataUsingEncoding:NSUTF8StringEncoding];
request.HTTPMethod = #"POST";
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// TODO: Handle success and failure
}] resume];
}
#end
Can anyone help explain how I fix this problem?
Here is Braintrees documentation I am following
https://developers.braintreepayments.com/start/hello-client/ios/v4#present-drop-in-ui
The problem can be rectified by presenting the view controller outside the super viewDidLoad method
Specifically viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
showDropIn(clientTokenOrTokenizationKey: clientToken)
}
When a cell is selected in my UITableView, I set self.itemURL to that cell's respective URL, then segue to WebViewController and load up that URL.
Here's the logic that happens when a cell is tapped:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (_matchCenterDone == YES) {
self.itemURL = _matchCenterArray[indexPath.section][#"Top 3"][indexPath.row+1][#"Item URL"];
NSLog(#"The URL IS:'%#", self.itemURL);
[self performSegueWithIdentifier:#"WebViewSegue" sender:self];
}
}
The NSLog prints out The URL IS:'http://www.ebay.com/itm/NEW-Sony-XPERIA-Z3-Compact-D5803-FACTORY-UNLOCKED-/131554310432?pt=LH_DefaultDomain_0, which tells me that self.itemURL does in fact exist, and is a properly formatted URL.
Before I segue to WebViewController, I do this:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"WebViewS egue"]){
// Opens item in browser
WebViewController *controller = (WebViewController *) segue.destinationViewController;
controller.itemURL = self.itemURL;
}
}
Here's what WebViewControllers viewDidLoad function looks like:
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"The url in webview is: '%#'", self.itemURL);
// Initialize UIWebView
self.myWebView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 60, self.view.frame.size.width,
self.view.frame.size.height)];;
self.myWebView.delegate = self;
[self.view addSubview:self.myWebView];
// set the url
NSURL *url = [NSURL URLWithString:self.itemURL];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// make url request
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if ([data length] > 0 && error == nil) {
[self.myWebView loadRequest:request];
[activityIndicator stopAnimating];
}
else if (error != nil) NSLog(#"Error: %#", error);
}];
[self.myWebView setScalesPageToFit:YES];
}
The NSLog above logs out The url in webview is: '(null)' and the console logs out an error stating Error: Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo=0x7fcbfa5e7f10 {NSLocalizedDescription=unsupported URL, NSUnderlyingError=0x7fcbfa64bec0 "unsupported URL"}
What's happening between my initial view controller and WebViewController that's causing self.itemURL become null, and therefore unable to load the url?
There's a space in your segue identifier string in your prepareForSegue: method:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"WebViewS egue"]){
// ^ space here
// Opens item in browser
WebViewController *controller = (WebViewController *) segue.destinationViewController;
controller.itemURL = self.itemURL;
}
}
You are segueing to an instance of WebViewController, but its itemURL property is not being set because the code in that if statement is not being executed.
Take out the space and it should work.
I've been having intermittent problems with NSURLConnection requests timing out in our iPhone app. It seems to be occurring more of late. Once it enters this state, it stays in that state. The only resolution seems to be killing the app and restarting it.
Observations:
The core code that executes the NSURLConnection has not changed (except for some custom user-agent code recently added).
Have yet to find a reproducible case, but timeouts seem to occur after the app has been sitting in the background for a while, particularly if running on 3G (no WiFi).
Apache on server is logging no requests from client while it's experiencing these timeouts.
Some indications that other apps, like Mail and Safari are affected (i.e., experiencing timeouts), although not consistently.
3G coverage is solid where I'm at, not to rule out a transitory issue triggering the problem (assumed not likely).
All requests are going to our own API server, and are restful POST requests.
We use our own NSTimer-based timeout, due to the issues with timeoutInterval and POST requests. I've tried playing around with increasing the timeout value -- problem still occurs.
Other miscellaneous stuff:
App was recently converted to ARC.
Running app under iOS 5.1.1.
App uses latest versions of UrbanAirship, TestFlight and Flurry SDKs.
Also using ARC branch of TouchXML to parse responses.
As you can see below, the code runs on the main thread. I assumed something is blocking on that thread, but the stack traces I see when suspending the app suggest the main thread is fine. I take it that NSURLConnection is using its own thread and that must be blocked.
#define relnil(v) (v = nil)
- (id) initWebRequestController
{
self = [super init];
if (self)
{
//setup a queue to execute all web requests on synchronously
dispatch_queue_t aQueue = dispatch_queue_create("com.myapp.webqueue", NULL);
[self setWebQueue:aQueue];
}
return self;
}
- (void) getStuffFromServer
{
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^{
dispatch_sync([self webQueue], ^{
error_block_t errorBlock = ^(MyAppAPIStatusCode code, NSError * error){
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webRequestController:self didEncounterErrorGettingPointsWithCode:code andOptionalError:error];
});
};
parsing_block_t parsingBlock = ^(CXMLDocument * doc, error_block_t errorHandler){
NSError * error = nil;
CXMLNode * node = [doc nodeForXPath:#"apiResult/data/stuff" error:&error];
if (error || !node) {
errorHandler(MyAppAPIStatusCodeFailedToParse, error);
}
else {
stuffString = [node stringValue];
}
if (stuffString) {
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webRequestController:self didFinishGettingStuff:stuffString];
});
}
else {
errorHandler(MyAppAPIStatusCodeFailedToParse, error);
}
};
NSURL * url = [[NSURL alloc] initWithString:[NSString stringWithFormat:MyAppURLFormat_MyAppAPI, #"stuff/getStuff"]];
NSMutableURLRequest * urlRequest = [[NSMutableURLRequest alloc] initWithURL:url];
NSMutableDictionary * postDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:
[[NSUserDefaults standardUserDefaults] objectForKey:MyAppKey_Token], #"token",
origin, #"from",
destination, #"to",
transitTypeString, #"mode",
time, #"time",
nil];
NSString * postString = [WebRequestController httpBodyFromDictionary:postDictionary];
[urlRequest setHTTPBody:[postString dataUsingEncoding:NSUTF8StringEncoding]];
[urlRequest setHTTPMethod:#"POST"];
if (urlRequest)
{
[self performAPIRequest:urlRequest withRequestParameters:postDictionary parsing:parsingBlock errorHandling:errorBlock timeout:kTimeout_Standard];
}
else
{
errorBlock(MyAppAPIStatusCodeInvalidRequest, nil);
}
relnil(url);
relnil(urlRequest);
});
});
}
- (void) performAPIRequest: (NSMutableURLRequest *) request
withRequestParameters: (NSMutableDictionary *) requestParameters
parsing: (parsing_block_t) parsingBlock
errorHandling: (error_block_t) errorBlock
timeout: (NSTimeInterval) timeout
{
NSAssert([self apiConnection] == nil, #"Requesting before previous request has completed");
NSString * postString = [WebRequestController httpBodyFromDictionary:requestParameters];
[request setHTTPBody:[postString dataUsingEncoding:NSUTF8StringEncoding]];
NSString * erVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleShortVersionString"];
NSString * erBuildVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleVersion"];
if ([erBuildVersion isEqualToString:erVersion] || [erBuildVersion isEqualToString:#""]) {
erBuildVersion = #"";
} else {
erBuildVersion = [NSString stringWithFormat:#"(%#)", erBuildVersion];
}
NSString * iosVersion = [[UIDevice currentDevice] systemVersion];
NSString * userAgent = [NSString stringWithFormat:#"MyApp/%#%# iOS/%#", erVersion, erBuildVersion, iosVersion];
[request setValue:userAgent forHTTPHeaderField:#"User-Agent"];
[request setTimeoutInterval:(timeout-3.0f)];
dispatch_sync(dispatch_get_main_queue(), ^{
NSURLConnection * urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
if (urlConnection)
{
[self setApiConnection:urlConnection];
requestParseBlock = [parsingBlock copy];
requestErrorBlock = [errorBlock copy];
NSMutableData * aMutableData = [[NSMutableData alloc] init];
[self setReceivedData:aMutableData];
relnil(aMutableData);
[urlConnection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[urlConnection start];
relnil(urlConnection);
NSTimer * aTimer = [NSTimer scheduledTimerWithTimeInterval:timeout target:self selector:#selector(timeoutTimerFired:) userInfo:nil repeats:NO];
[self setTimeoutTimer:aTimer];
}
else
{
errorBlock(MyAppAPIStatusCodeInvalidRequest, nil);
}
});
//we want the web requests to appear synchronous from outside of this interface
while ([self apiConnection] != nil)
{
[NSThread sleepForTimeInterval:.25];
}
}
- (void) timeoutTimerFired: (NSTimer *) timer
{
[[self apiConnection] cancel];
relnil(apiConnection);
relnil(receivedData);
[self requestErrorBlock](MyAppAPIStatusCodeTimeout, nil);
requestErrorBlock = nil;
requestParseBlock = nil;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[self requestErrorBlock](MyAppAPIStatusCodeFailedToConnect, error);
relnil(apiConnection);
relnil(receivedData);
[[self timeoutTimer] invalidate];
relnil(timeoutTimer);
requestErrorBlock = nil;
requestParseBlock = nil;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[receivedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
MyAppAPIStatusCode status = MyAppAPIStatusCodeFailedToParse;
CXMLDocument *doc = [[self receivedData] length] ? [[CXMLDocument alloc] initWithData:[self receivedData] options:0 error:nil] : nil;
DLog(#"response:\n%#", doc);
if (doc)
{
NSError * error = nil;
CXMLNode * node = [doc nodeForXPath:#"apiResult/result" error:&error];
if (!error && node)
{
status = [[node stringValue] intValue];
if (status == MyAppAPIStatusCodeOK)
{
[self requestParseBlock](doc, [self requestErrorBlock]);
}
else if (status == MyAppAPIStatusCodeTokenMissingInvalidOrExpired)
{
[Definitions setToken:nil];
[self requestMyAppTokenIfNotPresent];
[Definitions logout];
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webRequestControllerDidRecivedExpiredTokenError:self];
});
}
else
{
[self requestErrorBlock](status, nil);
}
}
else
{
[self requestErrorBlock](status, nil);
}
}
else
{
status = MyAppAPIStatusCodeUnexpectedResponse;
[self requestErrorBlock](status, nil);
}
relnil(doc);
relnil(apiConnection);
relnil(receivedData);
[[self timeoutTimer] invalidate];
relnil(timeoutTimer);
requestErrorBlock = nil;
requestParseBlock = nil;
}
URLs below are some screenshots of the queues/threads when the app was in the problematic state. Note, I believe thread 10 is related to the cancel performed on the previous timeout, although the mutex wait is curious. Also, the bit in thread 22 about Flurry does not consistently appear when experiencing the problem on other occasions.
Stack trace screenshots:
http://img27.imageshack.us/img27/5614/screenshot20120529at236.png
http://img198.imageshack.us/img198/5614/screenshot20120529at236.png
Perhaps I'm overlooking something obviously wrong in those traces, as I'm relatively new to iOS/Apple development.
All of this would be much simpler to solve if I had the source for NSURLConnection and related code, but such as it is, I'm taking stabs in the dark at this point.
Removing the TestFlight 1.0 SDK seemed to fix the problem. TestFlight also confirmed that they're working on a fix. Given that it's been over a month since the bug was originally confirmed by others, I wonder how close we are to getting a fix.