I am using this method to ask a nearby device to join the session:
When I do it I also start spinning an indicator
[browser invitePeer:key
toSession:session
withContext:nil
timeout:30];
Is there a method called in the moment of timeout? what if the other device goes out of range?
EDIT:
I notice that this state is never called:
if (state == MCSessionStateConnecting) {
NSLog(#"CONNECTING %#", peerID);
}
in case of timeouts on the browser side, you need to watch for the MCSessionStateNotConnected state. i do something like this:
- (void)session:(MCSession *)session
peer:(MCPeerID *)peerID
didChangeState:(MCSessionState)state
{
if (state == MCSessionStateNotConnected)
{
if (self.isWaitingForInvitation)
{
UIAlertView *alertView = [[UIAlertView alloc]
initWithTitle:NSLocalizedString(#"ERROR_TITLE", nil)
message:NSLocalizedString(#"ERROR_TEXT", nil)
delegate:self
cancelButtonTitle:NSLocalizedString(#"NO", #"Não")
otherButtonTitles:NSLocalizedString(#"YES", #"Sim"),
nil];
dispatch_sync(dispatch_get_main_queue(), ^{
[alertView show];
});
self.isWaitingForInvitation = NO;
}
}
use the dispatch_sync to make the alert popup right away.
Using a timer with a timer interval matching timeout parameter could be better idea.
Related
I have a weird issue with Zebra printer. On the big picture, I have codes to fetch items to print one by one from the queue. So if when the printing initiated, and there's 3 items on the queue, the code will loop and fetch the first data in the queue, send it to the printer, and delete the first data in the queue. Sort of like dequeueing.
The problem is, if it's the code that looping and sending the data directly to the printer, the printer will only print the first item. The next item is gone, even though NSLog shows that the printer connection opened, data sent, printing successful, and printer connection closed, for every single item.
But if each time the code print one label, and then the app shows message box like "press OK to print next label", and then the user tap the OK button, it can print the second and the rest of the label after every tap of the button.
I have then tried to emulate this. I've tried to use timer to "push the button programmatically" [btnPrint sendActionsForControlEvents:UIControlEventTouchUpInside], I also use timer to call the function directly, or giving delay to the thread, but none works. It has to be initiated from a button which tapped from human touch. I don't know why.
Here's the code:
// main function to print
-(void) printLabel {
if ([dataToPrint count] > 0) {
[self printWithData:[dataToPrint objectAtIndex:0]];
}
}
-(void)printWithData:(NSString*) data;
{
NSString *zplString = data;
// do something with zplString
NSLog(#"Sending data to printer");
printHandler = [[PrintingHandler alloc] init];
[printHandler setDelegate:self];
[printHandler initialize];
[printHandler printToSerial:bluetoothSerialNumber withData:zplString];
}
// delegate to call if the print is success
-(void) printIsSuccess
{
[dataToPrint removeObjectAtIndex:0];
// in here, I just use sleep code instead of button tap emulation to avoid unnecessarily too long code
[NSThread sleepForTimeInterval:2.0f];
[self printLabel];
}
// this is method inside PrintingHandler class that get called by PrintingHandler (printToSerial:)
-(void) printLabelWithData:(NSString*) zplData toPrinter:(NSString*) serial withSender:(id) sender
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {
// Instantiate connection to Zebra Bluetooth accessory
id<ZebraPrinterConnection, NSObject> thePrinterConn = [[MfiBtPrinterConnection alloc] initWithSerialNumber:serial];
// Open the connection - physical connection is established here.
BOOL success = [thePrinterConn open];
NSError *error = nil;
// Send the data to printer as a byte array.
success = success && [thePrinterConn write:[zplData dataUsingEncoding:NSUTF8StringEncoding] error:&error];
[NSThread sleepForTimeInterval:1.0f];
//Dispath GUI work back on to the main queue!
dispatch_async(dispatch_get_main_queue(), ^{
if (success != YES || error != nil) {
[delegate printFailed];
UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:#"Error" message:[error localizedDescription] delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil];
[errorAlert show];
[errorAlert release];
}
else if (success != YES) {
NSLog(#"Print is not success, but no error raised");
[delegate printSuccess];
}
else
{
NSLog(#"Print is success");
[delegate printSuccess];
}
});
// Close the connection to release resources.
NSLog(#"printer connection closed");
[thePrinterConn close];
[thePrinterConn release];
});
}
Sorry, I've found the solution. The problem is it's too fast between opening the connection to the printer and sending data to the printer. I was putting some delay, but I put the delay at wrong position.
So the step to print currently is:
Open bluetooth printer connection
Send data
Delay
Close printer connection
When the correct one should be:
Open bluetooth printer connection
Delay
Send data
Close printer connection
Here I put the answer so this can help other people with same problem.
-(void) printLabelWithData:(NSString*) zplData toPrinter:(NSString*) serial withSender:(id) sender
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {
// Instantiate connection to Zebra Bluetooth accessory
id<ZebraPrinterConnection, NSObject> thePrinterConn = [[MfiBtPrinterConnection alloc] initWithSerialNumber:serial];
// Open the connection - physical connection is established here.
BOOL success = [thePrinterConn open];
NSError *error = nil;
[NSThread sleepForTimeInterval:1.0f]; // this is the important one
// Send the data to printer as a byte array.
success = success && [thePrinterConn write:[zplData dataUsingEncoding:NSUTF8StringEncoding] error:&error];
//Dispath GUI work back on to the main queue!
dispatch_async(dispatch_get_main_queue(), ^{
if (success != YES || error != nil) {
[delegate printFailed];
UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:#"Error" message:[error localizedDescription] delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil];
[errorAlert show];
[errorAlert release];
}
else if (success != YES) {
NSLog(#"Print is not success, but no error raised");
[delegate printSuccess];
}
else
{
NSLog(#"Print is success");
[delegate printSuccess];
}
});
// Close the connection to release resources.
NSLog(#"printer connection closed");
[thePrinterConn close];
[thePrinterConn release];
});
}
I've done more than three days worth of research to fix my problem and I haven't seen anyone with a solution to my problem. The Browser invites an Advertiser, the Advertiser accepts, and the MCSession changes to a connected state. However, once the MCBrowserViewController is closed (by either the cancel or the done button), the MCSession disconnects. As long as I don't close the MCBrowserViewController, the MCSession stays connected. I don't understand why or how this works and I've even tried debugging the process, but it got too deep into threads for me to understand.
Please tell me it's just something wrong with my code.
-(void)setUpMultiPeer{
self.myPeerID = [[MCPeerID alloc] initWithDisplayName:pos];
self.mySession = [[MCSession alloc] initWithPeer:self.myPeerID];
self.browserVC = [[MCBrowserViewController alloc] initWithServiceType:#"svctype" session:self.mySession];
self.advertiser = [[MCAdvertiserAssistant alloc] initWithServiceType:#"svctype" discoveryInfo:nil session:self.mySession];
self.browserVC.delegate = self;
self.mySession.delegate = self;
}
-(void)dismissBrowserVC{
[self.browserVC dismissViewControllerAnimated:YES completion:nil];
}
-(void)browserViewControllerDidFinish:(MCBrowserViewController *)browserVC{
[self dismissBrowserVC];
}
-(void)browserViewControllerWasCancelled:(MCBrowserViewController *)browserViewController{
[self dismissBrowserVC];
}
-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{
if (state == MCSessionStateConnected) {
NSLog(#"Connected!");
//Not entirely sure about this next line...
self.mySession = session;
}
else if (state == MCSessionStateNotConnected){
NSLog(#"Disconnected");
dispatch_async(dispatch_get_main_queue(), ^(void) {
UIAlertView *alert = [[UIAlertView alloc]initWithTitle: #"Somebody Left!"
message: [[NSString alloc] initWithFormat:#"%#", [peerID displayName]]
delegate: nil
cancelButtonTitle:#"Got it"
otherButtonTitles:nil];
[alert show];
});
}
}
//Called by a UIButton
-(IBAction)browseGO:(id)sender {
[self setUpMultiPeer];
[self presentViewController:self.browserVC animated:YES completion:nil];
}
//Called by a UISwitch
-(IBAction)advertiseSwitch:(id)sender {
if (_advertiseSwitcher.on) {
[self setUpMultiPeer];
[self.advertiser start];
}
else{
[self.advertiser stop];
}
}
I have also attempted using a unique MCSession for each the Browser and the Advertiser, but with no success.
What I did to solve my problem was start over from ground zero. Waiting on an answer from StackOverflow and from the Apple Developer Forum was taking too long, so I went back to what worked at the very beginning and I will build up from there once more.
Here is a link for an awesome tutorial that I found. I hope this helps someone solve their problem.
However, if anyone sees anything utterly wrong with my code in the question PLEASE DO TELL! I want to know what was causing that bug so that I can learn from my mistakes.
Thank you for stopping by and reading this question.
I am trying to setup Reachability using the new 2.0 AFNetworking.
In my AppDelegate I initialise the sharedManager.
// Instantiate Shared Manager
[AFNetworkReachabilityManager sharedManager];
Then in the relevant VC method I check to see if isReachable:
// Double check with logging
if ([[AFNetworkReachabilityManager sharedManager] isReachable]) {
NSLog(#"IS REACHABILE");
} else {
NSLog(#"NOT REACHABLE");
}
At present this is not working as expected in the simulator, but I imagine this would need to be tested on device and not simulator.
Question
What I would like to do is monitor the connectivity within the VC. So I run the following in the viewDidLoad:
// Start monitoring the internet connection
[[AFNetworkReachabilityManager sharedManager] startMonitoring];
How would I then register for the changes? What is/would be called once the network connection changes I cannot see this from the documentation.
As you can read in the AFNetworking read me page
[[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
NSLog(#"Reachability: %#", AFStringFromNetworkReachabilityStatus(status));
}];
Here's also a link to the official documentation.
I have a singleton AFHTTPRequestOperationManager class. In the singleton has a method:
+(void)connectedCompletionBlock:(void(^)(BOOL connected))block {
[[AFNetworkReachabilityManager sharedManager] startMonitoring];
[[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
BOOL con = NO;
NSLog(#"Reachability: %#", AFStringFromNetworkReachabilityStatus(status));
if (status == AFNetworkReachabilityStatusReachableViaWWAN || status == AFNetworkReachabilityStatusReachableViaWiFi) {
con = YES;
}
if (block) {
[[AFNetworkReachabilityManager sharedManager] stopMonitoring];
block(con);
}
}];
}
Before make a request you call this method that return a block indicating if internet is reachable:
[TLPRequestManager connectedCompletionBlock:^(BOOL connected) {
if (connected) {
// Make a request
}else{
UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:#"Notice" message:#"Internet is not available." delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles: nil];
[alertView show];
}
}];
I was just going through your question and all the answers. After that I decided to do all these things once. So, in my existing project I just included the AFNetworking through cocoa-pods and here is the solution which is woking for me completely.
Solution -- First of all AFNetworkReachabilityManager is a singleton class. You don't need to do AppDelegate initialisation for sharedManager.
//[AFNetworkReachabilityManager sharedManager];
#import <AFNetworkReachabilityManager.h>
- (void)viewDidLoad {
//Starting the network monitoring process
[[AFNetworkReachabilityManager sharedManager]startMonitoring];
//Checking the Internet connection...
[[AFNetworkReachabilityManager sharedManager]setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status){
if (status == AFNetworkReachabilityStatusReachableViaWWAN || status == AFNetworkReachabilityStatusReachableViaWiFi) {
UIAlertView *alertNetFound = [[UIAlertView alloc]initWithTitle:#"Network Found" message:#"Please Wait Until It is loading" delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[alertNetFound show];
}else{
UIAlertView *alertNetNotFound = [[UIAlertView alloc]initWithTitle:#"No Internet" message:#"Please Check Your Internet Connection Honey" delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[alertNetNotFound show];
}
}];
So, in this case every time the device connects to a network, it will do the startMonitoring process first and after that it will hit the status block every time and will display alert according to the status.
You can do anything according to your choice by replacing the alerts on the status block. I used this to load an webpage automatically from local storage but I removed that code for simplicity.
Its even working with my simulator and Mac mini..
Thanks
Hope this helped.
I use this in the app delegate ->
func reachablityCode() {
AFNetworkReachabilityManager.sharedManager()
AFNetworkReachabilityManager.sharedManager().startMonitoring()
AFNetworkReachabilityManager.sharedManager().setReachabilityStatusChangeBlock({(status) in
let defaults = NSUserDefaults.standardUserDefaults()
if status == .NotReachable {
defaults.setBool(false, forKey:REACHABLE_KEY)
}
else {
defaults.setBool(false, forKey: REACHABLE_KEY)
}
defaults.synchronize()
})
}
And then this in the base file ->
func isReachable() -> Bool {
return NSUserDefaults.standardUserDefaults().boolForKey(REACHABLE_KEY)
}
I want to start a task that runs on another thread "just in case it is needed" to minimize the time the user will have to wait on it later. If there is time for it to complete, the user will not have to wait, but if it has not completed then waiting would be necessary.
Something like, opening a database in viewDidLoad: that will be needed when and if the user pushes a button on the screen. If I wait to open the database until the user actually pushes the button there is a lag. So I want to open it early. Since I don't know how long it will take to open and I don't know how long until the user hits the button, I need a way of saying, if that other task has not completed yet then wait, otherwise just go ahead.
For example:
#implementation aViewController
- (void) viewDidLoad {
[self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
if( err ) NSLog( #"There was a problem opening the database" );
}];
}
- (IBAction) goButtonTouched: (id) sender {
// Wait here until the database is open and ready to use.
if( ???DatabaseNotAvailableYet??? ) {
[self putSpinnerOnScreen];
???BlockProgressHereUntilDatabaseAvailable???
[self takeSpinnerOffScreen];
}
// Use the database...
NSManagedObjectContext *context = [self theDatabaseContext];
// Build the search request for the attribute desired
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
request.predicate = [NSPredicate predicateWithFormat: #"dId == %#", sender.tag];
request.sortDescriptors = nil;
// Perform the search
NSError *error = nil;
NSArray *matches = [context executeFetchRequest: request error: &error];
// Use the search results
if( !matches || matches.count < 1 ) {
NSLog( #"Uh oh, got a nil back from my Destination fetch request!" );
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"No Info"
message: #"The database did not have information for this selection"
delegate: nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[alert show];
} else {
MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
movc.destDetails = [matches lastObject];
[self.navigationController pushViewController: movc animated: YES];
}
}
#end
My hope is that there is never a spinner on the screen and never any delay for the user but, since I don't know how long it will take for the database connection to be established, I have to be prepared for it not being ready when the user pushes the button.
I can't use the call back for when openOrCreateDbWithCompletionHandler: completes since I don't want to do anything then, only when the user pushes the button.
I thought about using a semaphore but it seems like I would only signal it once (in the completion handler of the openOrCreateDbWithCompletionHandler: call) but would wait on it every time a button was pushed. That seems like it would only work for the first button push.
I thought about using dispatch_group_async() for openOrCreateDbWithCompletionHandler: then dispatch_group_wait() in goButtonTouched: but since openOrCreateDbWithCompletionHandler: does its work on another thread and returns immediately, I don't think the wait state would be set.
I can simply set a my own flag, something like before the openOrCreateDbWithCompletionHandler:, self.notOpenYet = YES;, then in its completion handler do self.notOpenYet = NO;, then in goButtonTouched: replace ???DatabaseNotAvailableYet??? with self.notOpenYet, but then how do I block progress on its state? Putting in loops and timers seems kludgy since I don't know if the wait will be nanoseconds or seconds.
This seems like a common enough situation, I am sure that you have all done this sort of thing commonly and it is poor education on my side, but I have searched stackOverflow and the web and have not found a satisfying answer.
I think, blocking execution is a bad habit unless you are building your own event loop, which is rarely necessary. You also don't need to do any GCD stuff in your case. Just get a feeling for async.
The following should work:
#implementation aViewController
- (void) viewDidLoad {
self.waitingForDB = NO;
self.databaseReady = NO;
[self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
if( err ){
NSLog( #"There was a problem opening the database" )
}else{
[self performSelectorOnMainThread:#selector(handleDatabaseReady) withObject:nil waitUntilDone:NO];
};
}];
}
- (void)handleDatabaseReady{
self.databaseReady = YES;
if(self.waitingForDB){
[self takeSpinnerOffScreen];
[self go];
}
}
- (IBAction) goButtonTouched: (id) sender {
// Wait here until the database is open and ready to use.
if( !self.databaseReady ) {
self.waitingForDB = YES;
[self putSpinnerOnScreen];
else{
[self go];
}
}
-(void)go{
// Use the database...
NSManagedObjectContext *context = [self theDatabaseContext];
// Build the search request for the attribute desired
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
request.predicate = [NSPredicate predicateWithFormat: #"dId == %#", sender.tag];
request.sortDescriptors = nil;
// Perform the search
NSError *error = nil;
NSArray *matches = [context executeFetchRequest: request error: &error];
// Use the search results
if( !matches || matches.count < 1 ) {
NSLog( #"Uh oh, got a nil back from my Destination fetch request!" );
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"No Info"
message: #"The database did not have information for this selection"
delegate: nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[alert show];
} else {
MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
movc.destDetails = [matches lastObject];
[self.navigationController pushViewController: movc animated: YES];
}
}
#end
Performing the call to handleDatabaseReady on the main thread guarantees that no race conditions in setting/reading your new properties will appear.
I'd go with the flag. You don't want to block the UI, just show the spinner and return from the goButtonTouched. However, you do need to cancel the spinner, if it is active, in openOrCreateDbWithCompletionHandler:.
This is rather a simple scenario. You make a method that does the stuff. Lets call it doStuff. From main thread, you call performSelectorInBackgroundThread:#selector(doStuff). Do not enable the button by default. Enable it at the end of doStuff so that user won't tap on it until you are ready. To make it more appealing, you can place a spinner in the place of the button and then replace it with the button when doStuff completes.
There are a number of classes and APIs you can use to achieve this kind of thing. You can use NSThread with synchronization primitives like semaphores and events to wait for it to finish when the user actually presses the button. You can use an NSOperation subclass (with an NSOperationQueue), and you can use GCD queues.
I would suggest you take a look at some the information in the Concurrency Programming Guide from Apple.
In your case you would probably be best served adding the operation to a GCD background queue using dispatch_async in combination with a semaphore which you can wait on when the user taps the button. You can check out the question "How do I wait for an asynchronously dispatched block to finish?" for an example.
I'm trying to make sure that I always have an up to date cached copy of a UIWebView using the following code:
// Set URL to help file on server
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"%#", HELP_FILE_URL]];
// Check network reachability
wifiReach = [Reachability reachabilityWithHostName:[NSString stringWithFormat:#"%#", SERVER_URL]];
netStatus = [wifiReach currentReachabilityStatus];
// Start activity indicators
[self startAnimation];
// Verify current help file cache if we have network connection...
if (netStatus == ReachableViaWWAN || netStatus == ReachableViaWiFi) {
helpFileRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadRevalidatingCacheData timeoutInterval:30];
} else {
// Network NOT reachable - show (local) cache if it exists
helpFileRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataDontLoad timeoutInterval:30];
}
// Show help file in a web view
[webView loadRequest:helpFileRequest];
It works fine in most cases except for when I go to Airplane mode without terminating the app. Once in Airplane mode the cached webView shows up fine BUT the UIWebView delegate
(void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
is also triggered which I don't want. I only want that to be triggered if the cache is empty! How can I achieve that? (If I terminate the app it works fine.) It's a small detail but I really would like it to work correctly :)
OK - I solved it by identifying the error code in the UIWebView delegate method - see below. I found that the error code is -1008 when the cache is empty ("resource unavailable") and -1009 with data in the cache ("The Internet connection appears to be offline."). Both cases offline, in Airplane mode.
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
NSLog(#"%# : %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
[self stopAnimation];
// No help to display - cache is empty and no Internet connection...
if ([error code] == -1008) {
NSString *alertMessage = #"To make Help available offline, you need to view it at least once when connected to the Internet.";
UIAlertView *alertView =
[[UIAlertView alloc] initWithTitle:#"Help Unavailable"
message:alertMessage
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alertView show];
}
NSLog( #"Error code:%d, %#", [error code], [error localizedDescription]);
}