My issue is this: whenever an iPhone user is in call, or is using his or her phone as a hotspot, the iOS 7 status bar is enlarged, thus pushing my Phonegap application's UIWebView off the bottom of the screen. The enlarged status bar is termed the "in-call status bar". See below image:
Stack Overflow answers I have tried to remedy this:
Iphone- How to resize view when call status bar is toggled?
How In-Call status bar impacts UIViewController's view size ? (and how to handle it properly)
Additionally, there does not seem to be any sort of event fired by Phonegap that informs me of the status bar's change. Listening to the Phonegap "pause" event is useless, as 1) it's known to have quirks in iOS and 2) it doesn't really cover the hotspot case.
My Objective-C skills are very minimal, and I only resort to asking this sort of question after putting in the requisite 4+ hours Googling, Stack Overflowing, wailing, etc...
Gods of Stack Overflow, render unto me thine bounteous nerd fury.
Came up with the following solution based on Jef's suggestions. What you'll want to do is the following:
Observe the native didChangeStatusBarFrame delegate
Get size information about the statusbar via native statusBarFrame
Expose information to your webview by triggering an event that passes it
I have setup a Github repo with all the code you find in this answer.
Setup notification in AppDelegate
// Appdelegate.m
- (void)application:(UIApplication *)application didChangeStatusBarFrame:(CGRect)oldStatusBarFrame
{
NSMutableDictionary *statusBarChangeInfo = [[NSMutableDictionary alloc] init];
[statusBarChangeInfo setObject:#"statusbarchange"
forKey:#"frame"];
[[NSNotificationCenter defaultCenter] postNotificationName:#"statusbarchange"
object:self
userInfo:statusBarChangeInfo];
}
Make statusBarChange selector available
// MainViewController.h
#protocol StatusBarChange <NSObject>
-(void)onStatusbarChange:(NSNotification*)notification;
#end
Setup the listener. This gets the origin and size dictionaries from statusBarFrame whenever it changes and fires an event in the webview passing along this data.
// MainViewController.m
- (void)onStatusbarChange:(NSNotification*)notification
{
// Native code for
NSMutableDictionary *eventInfo = [self getStatusBarInfo];
[self notifiy:notification.name withInfo:eventInfo];
}
- (void)notifiy:(NSString*)event withInfo:(NSMutableDictionary*)info
{
NSString *json = [self toJSON:info];
NSString *cmd = [NSString stringWithFormat:#"cordova.fireWindowEvent('\%#\', %#)", event, json];
[self.webView stringByEvaluatingJavaScriptFromString:cmd];
}
- (NSMutableDictionary *)getStatusBarInfo
{
CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame];
NSMutableDictionary *statusBarInfo = [[NSMutableDictionary alloc] init];
NSMutableDictionary *size = [[NSMutableDictionary alloc] init];
NSMutableDictionary *origin = [[NSMutableDictionary alloc] init];
size[#"height"] = [NSNumber numberWithInteger:((int) statusBarFrame.size.height)];
size[#"width"] = [NSNumber numberWithInteger:((int) statusBarFrame.size.width)];
origin[#"x"] = [NSNumber numberWithInteger:((int) statusBarFrame.origin.x)];
origin[#"y"] = [NSNumber numberWithInteger:((int) statusBarFrame.origin.y)];
statusBarInfo[#"size"] = size;
statusBarInfo[#"origin"] = origin;
return statusBarInfo;
}
- (NSString *) toJSON:(NSDictionary *)dictionary {
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary options:NSJSONWritingPrettyPrinted error:&error];
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
All this allows you to listen for window.statusbarchange event, e.g. like this:
// www/js/index.js
window.addEventListener('statusbarchange', function(e){
// Use e.size.height to adapt to the changing status bar
}, false)
I'd say that this always happens when you return from background, no?
In others words is it possible for the bar to enlarge without your app being at least briefly pushed to the background by incoming call etc?
If so surely you can query the status bar height in your delegates -(void)will(orDid)resume and adjust accordingly?
If it does happen without leaving the foreground that make a little more difficult, we'll need to work out which notifications to observe for, I know there's an audioSession interruption notif in the case of incoming calls, not sure about the hotspot thing but surely there is a notification for that too..
Edit ok here they are, choose one of these notifications to observe..
UIApplicationWillChangeStatusBarFrameNotification
UIApplicationDidChangeStatusBarFrameNotification
Or implement one of these callbacks in your delegate
-application:willChangeStatusBarFrame:
-application:didChangeStatusBarFrame:
Related
I seem to be having the exact opposite problem that pops up my question searches. I have a splitViewController that seems to be having a slow time updating its master view, after I call setNeedsDisplay. Eventually the update request gets drawn, but it is randomly between 5 to 150 seconds after the change should occur.
If I immediately rotate the iPad, the view changes are immediately reflected.
The layout is:
SVC - Detail VC
\
+-Navigation VC
\
MasterVC
+--UILabel (hidden/unhidden)
|
+--UIButton
All I want to do is hide/unhide a label in the MastVC when an action takes place in the MasterVC. On viewDidLoad, the label is hidden. When a button is pushed on the MasterVC, the label is unhidden and then things just don't go.
I have set everything under the sun to "setNeedsDisplay", but nothing makes it happen as fast as it should. If I even pushed all the setNeedsDisplay methods into dispatch_async(dispatch_get_main_queue(), ^{ ... }; there are no immediate results (not that I'm on a different thread, but it seemed like a good thing to try after reading similar questions).
I have made these calls from the SplitVC, the NavVC, the Master, each subVC, each subView, I even set up a Notification Center call from the Master to the SVC to have the SVC do the update specifically after the label was flagged as unhidden.
This all started to seem exceedingly off track, just to show/hide a simple label. Especially when all I have to do for the label show up properly is to just rotate the iPad.
As I said, the label eventually shows in the right spot, so it isn't off frame or opaque = 0 or something like that.
When I push the connect button, I make a call to Bluetooth Central Manager. Once the BCM connects to the device, I get a NC key/value that confirms connection. This triggers the label to be unhid.
-(void) receiveBCMNotification: (NSNotification *) notification {
NSDictionary *userInfo = notification.userInfo;
NSLog(#"got a BCM notice: %#",userInfo);
if ([[userInfo allKeys] containsObject:ddkBltCentralManagerStatusKey]) {
if ([[userInfo valueForKey:ddkBltCentralManagerStatusKey] isEqualToString:ddvBltCentralMangerScanningStarted]) {
[self.refreshAvailablePatches beginRefreshing];
}
if ([[userInfo valueForKey:ddkBltCentralManagerStatusKey] isEqualToString:ddvBltCentralMangerScanningEnded]) {
[self.refreshAvailablePatches endRefreshing];
[self.availablePatchesTableView reloadData];
}
if ([[userInfo valueForKey:ddkBltCentralManagerStatusKey] isEqualToString:ddvBltCentralMangerDeviceConnected]) {
self.connectedToPatchVisual.hidden = NO;
[self.view setNeedsDisplay];
NSDictionary *newInfo = [NSDictionary dictionaryWithObject:ddvMasterSideViewNeedsDisplay forKey:ddkMasterSideViewNeedsDisplay];
[[NSNotificationCenter defaultCenter] postNotificationName: ncMasterSideNotifications object:nil userInfo:newInfo];
}
}
}
I created my iMessage extension, when I try to open it, the first screen appears but it is totally frozen, and it does not react in any way.
I've put logs in the viewDidLoad of that first view and nothing appears there, after a few seconds I can already see those logs.
To make the application freezing lose that status, user has to slide screen left or right and back again.
I've tried looking all over the web for someone who happens to be the same, but I could not find anything.
It does not come to mind more screenshots or portions of code add, if you think I should provide some additional information, just let me know
Any help would be appreciated.
Thank you.
UPDATE:
This is my Project Structure.
This is my viewDidLoad code.
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"here viewDidLoad iMessage ext~~~!");
[self applyCornerRadiusToBtn];
[self registerPresentationAction];
NSDictionary *user = [self getUserInfoFromHostApp];
if (user) {
NSLog(#"Here != null user info");
//It is assumed that when you enter this point and run this log, the app should navigate to the next screen, but it does not.
[self performSegueWithIdentifier:#"goToYoutubeListIm" sender:nil];
} else {
NSLog(#"Here userInfo null");
}
}
- (NSDictionary *)getUserInfoFromHostApp
{
NSUserDefaults *myDefaults = [[NSUserDefaults alloc] initWithSuiteName:#"group.com.xxxxx"];
NSDictionary *userNameSaved = [myDefaults objectForKey:#"userInfoExt"];;
NSLog(#"userNameSaved in xxxx Ext ==> %#",userNameSaved);
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:#"group.com.xxxx"];
NSLog(#"groupURL ==> %#",groupURL);
return userNameSaved;
}
For all concerned I have found the problem or problems to be accurate.
1) I was creating my controllers type MSMessagesAppViewController. Apparently there should only be one controller of this type.
2) I had logic in the viewDidAppear in my MSMessagesAppViewController. For some strange reason this also caused the problem, I had to get the logic out there and force the user to interact with a button to execute the logic that was in the didAppear
So I'm making a game that involves wireless communication between multiple iPhones, with one being the host. I am attempting to do so via the MultipeerConnectivity framework, and I've made a MCManager class (an instance of which I put into appDelegate so it's available throughout the app) to handle sending data from one system to another. This is how sending is implemented in my code:
- (void) sendState: (NSString*) str;
//used by the host to send commands to the other connected systems
{
if(appDelegate.mcManager.connected && iAmHost){
NSData *dataToSend = [str dataUsingEncoding: NSUTF8StringEncoding];
NSArray *allPeers = appDelegate.mcManager.session.connectedPeers;
NSError *error;
[appDelegate.mcManager.session sendData:dataToSend
toPeers:allPeers
withMode:MCSessionSendDataReliable
error:&error];
if (error) {
NSLog(#"%#", [error localizedDescription]);
}
}
}
and when the subordinate systems receive the data, MCManager sends the Notification Center a notification and my class, which is looking for that particular notification, grabs it and executes this:
-(void)didReceiveDataWithNotification:(NSNotification *)notification{
if(!iAmHost){
NSData *receivedData = [[notification userInfo] objectForKey:#"data"];
NSString *action = [[NSString alloc] initWithData: receivedData encoding:NSUTF8StringEncoding];
NSLog(#"Recieved:");
NSLog(action); //for debugging purposes, and figuring out timing
//decide how to act depending on the string given
if([action containsString:#"ChangeMaxScore"]){
//the string was formatted as, for example, "ChangeMaxScore105"
NSString* valueStr = [action substringFromIndex:14];
maxScore = (int)[valueStr integerValue];
[self changeMaxScore]; //this method changes the label text that shows the user the value of maxScore
}
else if([action containsString:#"ChangePlayerNo"]){
//strings are formatted as "ChangePlayerNo2" for the second segment in a segmented control with the segments "2", "3", "4"
//so it would be referring to four players
NSString *valueStr = [action substringFromIndex:14];
[playerNumberSegmentedControl setSelectedSegmentIndex: [valueStr integerValue]];
playerNumber = playerNumberSegmentedControl.selectedSegmentIndex + 2;
[self changePlayerNumber];
//Players can either be human or a type of AI (AI-1,AI-2,etc.)
//the segmented control where you choose this is invisible unless that player number is playing
//so this method sets that segmented control visible and interactable (by adding it to the view)
//and removes those segmented controls not in use from the view
}
else if([action containsString:#"ChangeP0State"]){
//changes the value of the first player's segmented control (Human, AI-1, AI-2, etc.
NSString* valueStr = [action substringFromIndex:13];
AIControl0.selectedSegmentIndex = (int)[valueStr integerValue];
}
...
else if([action containsString:#"StartGame"])
[self newGame];
//this method starts the game and, in the process, pushes another view controller
}
}
My issue is that these actions, on the receiving end, are very laggy. For changing the number of players, for instance, the receiver NSLogs "Received: ChangePlayerNo1", and the segmented control on-screen changes its selected segment to the second one, but the stuff that's supposed to show up at that point...doesn't. And when I send the "StartGame" command, the receiver NSLogs that it has received it, I have to wait thirty seconds for it to actually start the game like it was asked.
This delay makes it very hard to test whether my wireless methods are working or not (it works on the host's side, mostly because the host is changing them manually, not responsively - also, all of this works on the other side of my program, which is just the game without wireless support, with several players/AIs on a single screen) and, if not fixed, will definitely prevent the app from being used easily.
I'm curious what causes this and what I can do to fix it. Thank you!
I'm trying to test out the iOS 8.1 handoff feature with NSUserActivity between my iPhone and my iPad. For this, I tried both implementing my own solution, and to use Apple's PhotoHandoff project. However, it's not working.
If I provide a webpageURL, the handover works fine, but when I try to use userData or addUserInfoEntriesFromDictionary nothing works, and I can't for the life of me figure out what the catch is to make the data work.
Sample code:
NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:#"com.company.MyTestApp.activity"];
activity.title = #"My Activity";
activity.userInfo = # {};
// activity.webpageURL = [NSURL URLWithString:#"http://google.com"];
self.userActivity = activity;
[self.userActivity becomeCurrent];
[self.userActivity addUserInfoEntriesFromDictionary:# { #"nanananan": #[ #"totoro", #"monsters" ] }];
(I'm also unable to make it work with a Mac app with a corresponding activity type)
I hope you found the solution already, but in case somebody stumbles upon this problem too, here is a solution. (Actually not very different from the previous answer)
Create user activity without userInfo, it will be ignored:
NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:#"..."];
activity.title = #"Test activity";
activity.delegate = self;
activity.needsSave = YES;
self.userActivity = activity;
[self.userActivity becomeCurrent];
Implement the delegate to react to needSave events:
- (void)userActivityWillSave:(NSUserActivity *)userActivity {
userActivity.userInfo = #{ #"KEY" : #"VALUE" };
}
When needsSave is set to YES this method will be called and userActivity will be updated.
Hope this helps.
To update the activity object’s userInfo dictionary, you need to configure its delegate and set its needsSave property to YES whenever the userInfo needs updating.
This process is described in the best practices section of the Adopting Handoff guide.
For example, with a simple UITextView, you need to specify the activity type ("com.company.app.edit") identifier in the Info.plist property list file in the NSUserActivityTypes array, then:
- (NSUserActivity *)customUserActivity
{
if (!_customUserActivity) {
_customUserActivity = [[NSUserActivity alloc] initWithActivityType:#"com.company.app.edit"];
_customUserActivity.title = #"Editing in app";
_customUserActivity.delegate = self;
}
return _customUserActivity;
}
- (void)textViewDidBeginEditing:(UITextView *)textView
{
[self.customUserActivity becomeCurrent];
}
- (void)textViewDidChange:(UITextView *)textView
{
self.customUserActivity.needsSave = YES;
}
- (BOOL)textViewShouldEndEditing:(UITextView *)textView
{
[self.customUserActivity invalidate];
return YES;
}
- (void)userActivityWillSave:(NSUserActivity *)userActivity
{
[userActivity addUserInfoEntriesFromDictionary:#{ #"editText" : self.textView.text }];
}
FWIW, I was having this issue. I was lucky that one of my Activity types worked and the other didn't:
Activity: Walking
(UserInfo x1,y1)
(UserInfo x2,y2)
(UserInfo x3,y3)
Activity: Standing
(UserInfo x4,y4)
Activity: Walking
etc.
I got userInfo if the handoff occured when standing but not walking. I got other properties such as webpageURL in all cases; just userInfo came through null.
The fix for me was to invalidate & recreate the NSUserActivity object every time (e.g. when Walking to x2/y2 from x1/y1), instead of only when Activity type changed (e.g. from walking to standing). This is very much not the way the doc is written, but fixed the issue on iOS 9.
UPDATE: This workaround doesn't work on iOS 8. You need to implement this via the userActivityWillSave delegate, as gregoryM specified. Per Apple's doc:
To update the activity object’s userInfo dictionary efficiently,
configure its delegate and set its needsSave property to YES whenever
the userInfo needs updating. At appropriate times, Handoff invokes the
delegate’s userActivityWillSave: callback, and the delegate can update
the activity state.
This isn't a "best practice", it is required!
[Note: issue occurred on iOS 9 devices running code built on Xcode 6.x. Haven't tested Xcode 7 yet, and issue may not occur on iOS 8.]
This is not the No video, audio only problem. It's just the opposite.
The problem arises when using iOS 5.0. iPads running 4.3 or lower play the same video files flawlessly.
since iOS 5 changed the way stuff is initialized for MPMoviePlayerControllers, I had to do some SDK based programming in order to get the video to be displayed. Before implementing the snippet I'm showing next, the video and it's controls won't even show up on the screen. The controller would only show a black square with the size and origin of given CGRect frame.
The way I handle it is the following:
The video files are located on the documents folder. So the NSURL has to be initialized as fileURLWithPath. Once that's done, I proceed to initialized the controller with a given frame. Since it wouldn't work otherwise, the view will only add the player once it has changed its loadState. That's achieve by subscribing to a notification. the subscriber selector performs the addition of the controller's view to the parent view on the main thread since the notification could be handled from other threads.
Initializing and adding video to the view:
-(void)addVideo:(NSString*) videoName onRect:(CGRect)rect {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
iPadMagazineAppDelegate *appDelegate = GET_APP_DELEGATE;
NSArray *dirArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *dirName = [dirArray objectAtIndex:0];
// get directory name for this issue
NSURL *baseURL;
/*
BUGFIX: Video does not work on iOS 5.0
*/
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"5.0")){
baseURL = [[NSURL fileURLWithPath:dirName]URLByAppendingPathComponent:[appDelegate.currentIssue getIssueDirectoryName ]];
}else {
baseURL = [[NSURL URLWithString:dirName] URLByAppendingPathComponent:[appDelegate.currentIssue getIssueDirectoryName]];
}
/* end BUGFIX: Video does not work on iOS 5.0 */
NSURL *videoURL = [baseURL URLByAppendingPathComponent:videoName];
MPMoviePlayerController * movieController= [[MPMoviePlayerController alloc]initWithContentURL:videoURL];
// set frame for player
movieController.view.frame = rect;
// set auto resizing masks
[movieController.view setAutoresizingMask:UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight];
// don't auto play.
[movieController setShouldAutoplay:NO];
[movieController setUseApplicationAudioSession:YES];
/*
BUGFIX: Video does not work on iOS 5.0
*/
if (SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(#"5.0")) {
[movieController prepareToPlay];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(loadVideo:) name:MPMoviePlayerLoadStateDidChangeNotification object:movieController];
}else {
[pdfView addSubview:movieController.view];
[pdfView bringSubviewToFront: movieController.view];
}
/* end BUGFIX: Video does not work on iOS 5.0 */
[_moviePlayerViewControllerArray addObject:movieController];
[movieController release];
[pool release];
}
notification handler:
-(void)loadVideo:(NSNotification*)notification {
for (MPMoviePlayerController *movieController in _moviePlayerViewControllerArray) {
if (movieController.loadState != MPMovieLoadStateUnknown) {
[pdfView performSelectorOnMainThread:#selector(addSubview:) withObject:movieController.view waitUntilDone:YES];
[pdfView performSelectorOnMainThread:#selector(bringSubviewToFront:) withObject:movieController.view waitUntilDone:YES];
[[NSNotificationCenter defaultCenter] removeObserver:self name:MPMoviePlayerLoadStateDidChangeNotification object:movieController];
}
}
}
Thank you for reading this huge question. I appreciate your answers.
cheers.
Try this:
Set MPMoviePlayerController's property "useApplicationAudioSession" to "NO".
Apparently there's a bug, but it's not related to the MPMoviePlayerController, but to iOS 5 itself.
My iPad was muted from the switch but still played audio from iPod app anyway so I didn't realized that it was that way, so MPMoviePlayerController was fine, but part of the OS did not notice that the iPad was muted.
I've filed the corresponding bug on Apple's bug tracker. Bug ID# 10368531.
I Apologize if I've wasted your time.
UPDATE: Got feedback from apple for the bug. It's expected behavior. :\