I am implementing the new feature in iOS 9 for new iPads of having multitasking of 2 apps with split screen.
I can't seem to find a completion method of when the split screen adjustment has finished after user has dragged the app view size handle.
How exactly do I notify my app that my app is being run in a split screen?
NOTE: This is different from UISplitViewController. I am referring to the multitasking of 2 apps in split screen, not UISplitViewController
There are multiple ways of responding to changes to the Split Screen view configuration. Depending on how you want to respond, you may have to use different approaches.
First of all, preferably use Autolayout and Storyboard constraints that allow your views to naturally resize appropriately to most, or all, view size changes.
In some situations, you may need to make layout changes when the Split Screen changes significantly and moves your available screen space from full screen, or almost full screen, to what iOS calls a Compact presentation. If the view handle is dragged enough to cross this boundary, you can get a callback via this view controller method:
Obj-C
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) {
NSLog(#"split screen area now compact!");
} else if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular) {
NSLog(#"split screen area now regular!");
}
}
Swift:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
if self.traitCollection.horizontalSizeClass == .compact {
print("split screen area now compact!")
}
}
These methods are called when changing from Compact to Regular size, or vice versa. Not for every adjustment to the Split Screen view handle. In other situations, different code is needed.
As is the case with handling rotations programmatically, you may want to implement this method, which will provide you the final view size:
viewWillTransitionToSize:withTransitionCoordinator:
This method will get called even if the change is not large enough to cross the Regular-Compact boundary. Inspect the size parameter passed into your method, and see the example, which shows where you can place code that is called before, during, and after the transition.
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
// Code here will execute before the rotation begins.
// Equivalent to placing it in the deprecated method -[willRotateToInterfaceOrientation:duration:]
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Place code here to perform animations during the rotation.
// You can pass nil or leave this block empty if not necessary.
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Code here will execute after the rotation has finished.
// Equivalent to placing it in the deprecated method -[didRotateFromInterfaceOrientation:]
}];
}
More Reading
I think it's also useful to make use of trait variations and size classes in your storyboards, although that's not a specific answer to this question.
When the user alters the split screen sizes the root view will layout it's subviews for the new size, so you can implement viewDidlayoutSubviews in your view controller.
-(void)viewDidLayoutSubviews
{
if(!CGSizeEqualToSize(self.lastFrameSize,self.view.frame.size))
{
NSLog(#"Size changed:(%f x %f)",self.view.frame.size.width, self.view.frame.size.height);
}
self.lastFrameSize=self.view.frame.size;
}
Related
I'm working on an adopting multitasking to support split view for app, but I find the traitCollectionDidChange not called when app is on the right.
Does anyone have idea about this?
You may override traitCollectionDidChange in your ViewController.
However.
traitCollectionDidChange called when you change from one split view mode to another. For example from 50/50 view to 33/66. It so NOT called when you enter multitasking mode or exit it.
If you need to handle all events including entering and exiting multitasking mode, use viewWillTransitionToSize:withTransitionCoordinator: instead:
// put this in your ViewController code
-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
// TODO: put your code here (runs BEFORE transition complete)
}
If you want your code called AFTER the transition compelete:
-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
// TODO: put your code here (runs AFTER transition complete)
}];
}
Have you tried the viewWillTransitionToSize method? This is used to notify the container that the size of its view is about to change.
Objective-C
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
Swift
func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animateAlongsideTransition(nil, completion: {
if UIDevice.currentDevice().orientation.isLandscape.boolValue {
print("landscape")
} else {
print("portrait")
}
}
If anyone still doubt that is the point:
// This method called every time user changes separator position or when user rotates device
-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
// Always call super in those methods
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
// Before this call your layout is old, status bar orientation will return value before rotation
[coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
// Code here will be executed during transform. Status bar orientation is new, your view size already changed (in general).
// Setup required animations or custom views transitions
}
completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
// Split view transition complete or device rotated
}];
}
Also there is a method traitCollectionDidChange:, but it will be called only when horizontal size class actually changed. For example, if your app presenting from right side in split view mode, traitCollectionDidChange: will not be called when user changed separator position. But if your app presenting from left, it will be called always in portrait mode, and in landscape for transitions (50/50) <-> (66/33)
In view controller, we should call
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
to get notified when the orientation/multi window view changes.
As per documentation of UITraitEnvironment:
The system calls this method when the iOS interface environment changes. Implement this method in view controllers and views, according to your app’s needs, to respond to such changes. For example, you might adjust the layout of the subviews of a view controller when an iPhone is rotated from portrait to landscape orientation. The default implementation of this method is empty.
At the beginning of your implementation, call super to ensure that interface elements higher in the view hierarchy have an opportunity to adjust their layout first.
I have an app with many different buttons arranged in a calculator like, square / rectangular format. It is actually extremely similar to the default iOS calculator. There are approximately 6 rows with 4 columns each of buttons.
Problem
The problem I am having involves the buttons in the bottom row (approximately the bottom 10th of the screen on an iPhone 4). They do not behave normally when pressed in the sense that when pressed, they have to be pressed and held (for roughly just under a second) to register a "button press". This is opposed to the standard short tap.
No other buttons besides this bottom row behave in this fashion.
Additionally, if these buttons are tapped on their upper edge, they behave normally, responding as soon as they are touched. This leads me to believe that the buttons themselves are not the problems but there is some problem with the layout of my views.
It should be also noted that this problem is only present on physical devices. On the simulator, the buttons behave normally.
Context
The view containing these buttons is not the root view controller of the app. Instead it is transitioned to as so (nothing fancy here):
[self presentViewController:navController animated:YES completion:nil];
Where self is the root view controller
The view controller I am having problems with is contained within a navigation controller and is presented modally by the root view controller which you can see above.
What I have tried so far
Turning auto layout on and off: same problem
Rearranging hierarchy of views: I moved the problematic buttons on top of and behind all other
views with the same result: same problem
Multiple devices (iPhone 4, 4s, 5): same problem (although buttons respond normally on both 3.5 inch and 4 inch simulators)
Testing other apps (when buttons in this region are pressed on other apps, they behave normally)
Additional Information
Everything is laid out in Interface Builder for the problematic view controller
All of the buttons are system buttons with standard settings and are all exactly the same besides their text.
All of the elements of the screen (buttons, labels, etc. ) are subviews of the "view"
The buttons are flush against each other and should not overlap more than one or two pixels.
The problematic buttons have dimensions: 80 width X 44 height.
The problematic buttons are flush against the bottom of the screen
In addition to the buttons, there is one UIImage and several labels however these are at the top of the screen and do not overlap with any of the buttons in any way.
The cause for this issue is that Apple seems to place a GestureRecognizer at the bottom of the screen that delays touches in any other view.
After fiddling around with gesture recognizers on the App's windows I came up with a solution that incorporates a subclass of UIButton:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL inside = [super pointInside: point withEvent: event];
if (inside && !self.isHighlighted && event.type == UIEventTypeTouches)
{
self.highlighted = YES;
}
return inside;
}
The given method is getting called although touchesBegan: is called delayed. A check if the view is at the bottom of the screen may be suitable to prevent any side effects that may occur with this fix.
This sounds like an interaction between the buttons and the UIScreenEdgePanGestureRecognizer (or whatever it is) that is responsible for detecting that the user wants to bring up the system's Control Center.
There are actually two potential issues here:
There can be an interaction (i.e. conflict) between the possibility of a gesture directed at your app and a gesture directed at the system. If you have gesture recognizers, you might have to use delegate methods to mediate between them and the system's gesture recognizers.
There is a well-established bug where a tap near the screen edge (i.e. in the screen edge gesture recognizer's "zone") works but it causes the button to misbehave physically, i.e. it doesn't look as if it's been tapped even though logging shows that it has (see my answer here: https://stackoverflow.com/a/22000692/341994).
answer of Lukas in swift and deselect highlighted
extension UIButton {
public override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
var inside = super.pointInside(point, withEvent: event)
if inside != highlighted && event?.type == .Touches {
highlighted = inside
}
return inside
}
}
Swift 3 Solution
extension UIControl {
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let inside = super.point(inside: point, with: event)
if inside != isHighlighted && event?.type == .touches {
isHighlighted = inside
}
return inside
}
}
I've written a full solution in swift based on Luka's answer. Just make any conflicting buttons conform to this class and the problem will be gone:
class BorderBugFixButton : UIButton {
override func awakeFromNib() {
super.awakeFromNib()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "unHighlight", name: UIApplicationWillResignActiveNotification, object: nil)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
let inside = super.pointInside(point, withEvent: event)
if inside != highlighted && event?.type == .Touches {
highlighted = inside
}
return inside
}
internal func unHighlight() {
highlighted = false
}
}
P.S.: For those of you that don't like Storyboards/Xib's, just migrate the implementation of awakeFromNib() to init()
I've run into this numerous times when working with updating older Storyboards to iOS 7+, usually when the ViewController in question has a form of a UIScrollView. Double check these 2 settings in your Storyboard on the ViewController Object (Not the view, the one with the yellow circle). When I unchecked the 'Extend Edges' under top & bottom bars, the scrollView's frame was adjusted down by 64 points (height of Nav & Status bars).
After setting the space between NavBar.bottom and scrollView.top back to 0, the button started working. This might be due to the fact the scrollView.frame.bottom was 64 pixels above the bottom of the window, so touches in that area were disregarded because they were technically out of the scrollView's frame but still displayed visually for some reason.
iOS 9.3, Xcode 7.3
I would suggest that you should make a category to the UIButton class that implements Lukas' answer. For instructions on how to create a category see this post: How do I create a category in Xcode 6 or higher?
Give it an appropriate name with the traditional "+" sign, i.e. if you name it "BottomOfScreen", then the resulting file name will be "UIButton+BottomOfScreen".
If you are using objective-c, then you will get a *.h and *.m files with he new category.
*.h
#import <UIKit/UIKit.h>
#interface UIButton (BottomOfScreen)
#end
*.m
#import "UIButton+BottomOfScreen.h"
#implementation UIButton (BottomOfScreen)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL inside = [super pointInside:point withEvent:event];
if (inside && !self.isHighlighted && (event.type == UIEventTypeTouches))
{
self.highlighted = true;
}
else
{
nil;
}
return inside;
}
#end
I was able to fix this issue by disabling delaysTouchesBegan in viewWillAppear
self.navigationController?.interactivePopGestureRecognizer?.delaysTouchesBegan = false
I've prepared this answer in the hopes that someone else might find it helpful.
My problem was a little more difficult to discover the cause, but way easier to resolve. The button in question was in a custom sideBarView and XIB with four other buttons that I had programmatically initialized and loaded into the viewController. The top four worked fine. Only the bottom didn't seem to work...
CAUSE: The CGRect defined programmatically for the custom sideBarView was actually smaller in height than the XIB needed. However, since the sideBarView wasn't clipToBounds, it showed it the lowest button, but any taps on it were registered as taps on the view below and not as taps on the lowest button.
To discover this, I checked the 3D viewer, the order of the objects in the XIB and even took comparison snaps of each of the buttons in the simulator with Color-blended layers selected and with breakpoints on didTap... it wasn't until I shortened the spaces between the constraints between each button and discovered that nothing but only the top of the lowest button would accept the tap, which gave me the clue that it was height limitation somewhere (like the initialization code).
I wish to create an app that supports both Portrait and Landscape presentation.
There is no difference between the UI objects when rotating - they remain the same and function the same way in both states.
The only thing that should change is where each UI object is located on screen:
I assume that using Autolayout and Constraint won't work here since the objects actually change their location.
I wish to work in the same VC (.h and .m files), and if possible, design the screens UI in Storyboard - if it's not possible, than design one of them as a .xib file is cool as well.
The most important thing is: How to write the code that supports that?
One way, is to set the new position and dimensions on rotating your view programatically.
You can use these functions:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return YES;
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
[self yourNewPositioningFunction:[self interfaceOrientation]];
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
[self yourNewPositioningFunction:toInterfaceOrientation];
}
I have a view that has two tables. In the story board, I have two separate views, one horizontal and the other vertical. When I need to navigate to the view, the code detects the orientation and brings up the appropriate view (and does so on an orientation change.
I have the following code in my method:
-(void)viewDidAppear:(BOOL)animated{
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
if(orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight){
if(tableHeight2 > 324){
tableHeight2 =325;
}
table1.frame = CGRectMake(table1.frame.origin.x, table1.frame.origin.y, table1.frame.size.width, tableHeight1);
table2.frame = CGRectMake(table2.frame.origin.x, table1.frame.origin.y + 20 + tableHeight1, table2.frame.size.width, tableHeight2);
}else {
if(tableHeight2 > 500){
tableHeight2 = 500;
}
table1.frame = CGRectMake(table1.frame.origin.x, table1.frame.origin.y, table1.frame.size.width, tableHeight1);
table2.frame = CGRectMake(table2.frame.origin.x, table1.frame.origin.y + 50 + tableHeight1, table2.frame.size.width, tableHeight2);
}
}
This works wonderfully when I press a button to navigate to the view. It adds up all of the cell heights and makes the first table the appropriate height, then moves the second table 50 pixels below the first table. It also makes sure the second table doesn't extend beyond the visible screen area.
When the orientation changes, I the following code is executed:
-(void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration {
InitViewController *ini;
ini = [self.storyboard instantiateViewControllerWithIdentifier:#"Init"];
ini.location = MenuName;
[self presentViewController:ini animated:NO completion:nil];
}
This should do the same thing that pressing a barbuttonitem does: change to InitViewController while sending the StoryboardID to it in the ini.location variable. The code for the navigation buttons is pretty much identical to the code in willAnimateRotationToInterfaceOrientation. InitViewController then determines the orientation and sends the app to the correct storyboard UIView.
It does send it to the right view, I can tell based on the table widths. What it doesn't do is change the height of the first (top) table, table1. The first table retains the size it was given in the storyboard.
If there is area of code you think I need to post to get a better picture, let me know I'll be happy to add it. Any help, insight, or even just trial-and-error suggestions would be appreciated.
*Note: I have tried to change willAnimateRotationToInterfaceOrientation to ViewDidLayoutSubviews, to not effect.
Well, it seems a very small change fixed it. I noticed that the code on the navigation buttons had YES under "animate" for the view change, and the willAnimateRotationToInterfaceOrientation "animated:NO". I changed it to "YES" and that fixed it. Not sure why yet, perhaps it affects how the method displays the view or affects the load order, but there it is.
I want to create a UIVIew that will not rotate when I will call to shouldAutorotateToInterfaceOrientation , and other subviews will rotate.
and i want to keep the shouldAutorotateToInterfaceOrientation suppot, and not use notification.
thanks
Be sure to define exactly what you mean by having a view "not rotate" when the device is rotated. Rotation can mean several things, depending on which coordinate system to which you refer. A better way to think about it is simply, what do you want your view to look like for each device orientation.
Just to remind, shouldAutorotateTo... is sent to your view controller by the system. You don't invoke it yourself. It doesn't cause rotation. It lets the system ask your view controller what orientations it supports.
Your VC should answer YES for all orientations it supports. A supported orientation is one where the view changes layout in response to a device orientation change, so if any layout change occurs for a given orientation, then the answer to shouldAutorotateTo is probably YES.
Altering subview layout for a given interface orientation is mostly your responsibility. Views have an autoresizingMask which is a bit vector describing some options for sizing and positioning relative to their parent, and this is often adequate. The way to fully control layout on orientation change is by implementing willAnimateRotationToInterfaceOrientation.
For example, here's a fairly permissive shouldAutorotate, enabling all but one orientation...
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
And here's how you would control how subviews layout on rotation...
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
UIView *testView = [self.view viewWithTag:16];
if (UIInterfaceOrientationIsLandscape(toInterfaceOrientation)) {
// change frames here to make the ui appear according to your spec
// including however you define "not rotating" for each view
self.subviewA.frame = .....
self.subviewB.frame = .....
} else {
self.subviewA.frame = .....
self.subviewB.frame = .....
}
}
If you want one UIView not to Rotate with orientation, one of the easy solution is to add that view to Application top Window like this. Because window dont rotate with device orientations.
[[[[UIApplication sharedApplication]windows]objectAtIndex:0]addSubview:customView];