I'm moving over to UIPresentationController based presentations for my view controllers but have run into some confusion with the API.
I have a custom sidebar style view controller presentation (similar to the LookInside WWDC 2014 demo code).
This class cluster (UIPresentationController, UIViewControllerTransitioningDelegate, and UIViewControllerAnimatedTransitioning) presents a view controller as a sidebar from the edge of the screen on regular size class views, and presents the same view controller as full screen on compact size class views.
Testing this on the Resizable iPad target shows the correct behaviour: I set the horizontal size class to "Compact" and my view controller switches from sidebar to full screen.
However, I want more granularity. I would like to use the sidebar-style view controller presentation on iPhone 6 and 6+ when the device is in landscape orientation, and use the full-screen style presentation for all iPhones in portrait orientation.
So in my method
- (void) viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
I implemented some logic to detect whether the sidebar will occupy too much of the screen, let's say I use the following condition:
//If my sidebar is going to occupy more than half the new width of the view...
if( self.sidebarTransitionController.width > size.width / 2.0 )
{
//Override the presentation controller's trait collection with Compact horizontal size class
sidebarPresentationController.overrideTraitCollection = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
}
else
{
//Otherwise override the trait collection with Regular
sidebarPresentationController.overrideTraitCollection = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
However this does nothing. The documentation for UIPresentationController.overrideTraitCollection states:
Use this property to specify any traits that you want to apply to the presented and presenting view controllers. The traits you specify override any existing traits currently in effect for the view controllers. The default value of this property is nil.
Assigning a new value to this property causes the presentation controller to transition to the new set of traits, which could result in animations to the presented interface.
Assigning the new value to the presentation controller does not cause my presented interface to change in any way. (Even if I assign the overrideTraitCollection when the UIPresentationController is created from within the UIViewControllerTransitioningDelegate object.)
What am I missing? Is it possible to perform adaptive presentation with UIPresentationController on a more granular level?
Is it possible to perform adaptive presentation with UIPresentationController on a more granular level?
Not easily.
I suggest one of these options:
Give up on control and accept UIKit’s limited adaptivity: you can change to a full screen presentation or present a different view controller for a particular trait collection. Go with this to ship your app faster.
Use presentations but work against UIKit. One way is to override viewWillTransitionToSize:withTransitionCoordinator: and dismiss and then re-present the presented view controller, making any changes you want such as providing a different presentation style or presentation controller. This could give okay results without taking too much time.
Use view controller containment. This is about the lowest level you can go while sticking with UIKit best practices. Your main view controller becomes a child of a container view controller, and instead of presenting you ask the container to show the other view controller. Go with this if the app should be custom and exquisite, and you can spend the time to make it just right.
Use:
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
traitCollection:(UITraitCollection *)traitCollection NS_AVAILABLE_IOS(8_3);
It's called on rotation even if the size class hasn't changed so is a good place to do you idiom/orientation specific adaptation. Remember that iPhone 6 can run in zoomed in mode.
I was running into the same issue. It's possible to interpret device orientation from the size classes, although not completely unambiguously, but the following worked for my purposes.
From Programming iOS 9: Dive Deep into Views, View Controllers and Frameworks, an excellent book full of important details like this:
horizontalSizeClass, verticalSizeClass
A UIUserInterfaceSizeClass value, either .Regular or .Compact. These are called size classes. The size classes, in combination, have the following meanings:
Both vertical and horizontal size classes are .Regular: We're running on an iPad
The vertical size class is .Regular, but the horizontal size class is .Compact: We're running on an iPhone with the app in portrait orientation. (Alternatively we might be running on an iPad in a splitscreen iPad multitasking configuration; see Chapter 9).
Both the vertical and horizontal size classes are .Compact: We're running on an iPhone (except iPhone 6 plus) with the app in landscape orientation.
The vertical size class is .Compact and the horizontal size class is .Regular: We're running on an iPhone 6 plus in landscape orientation.
e.g. in the View Controller:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowComposeView" {
segue.destinationViewController.presentationController!.delegate = self
segue.destinationViewController.modalPresentationStyle = .PageSheet
}
}
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
// If we do an adaptive presentation, and adapt from Page Sheet to Form Sheet,
// then on iPhone 6 we will get the nice rounded corners of the nav bar
// in both portrait and landscape. (From pg. 298 of Programming iOS 9)
// We want this behaviour on iPhone in Portrait orientation only.
if traitCollection.horizontalSizeClass == .Compact && traitCollection.verticalSizeClass == .Regular {
return .FormSheet
}
else {
return .PageSheet
}
}
Related
I have built an app that uses no interface builder using Snapkit to create my Auto-Layout constraints. Everything looks fine in portrait, however a few screen's need some landscape specific constraints.
I've searched here and Google in general for a quick intro on doing this, but couldn't really find anything that was applicable (everything I found was based on using IB or used size classes instead of orientation - I specifically want landscape, not compact vs regular).
So, all my auto-layout constraints are set up in viewDidLoad at the moment. No doubt at very least, the ones that will be orientation dependant need moving to some kind of delegate/callback method on UIViewController, but I don't know know what that is...
How do I detect an orientation change in order to change my constraints?
How do I get the current orientation (so when I first load the view controller I can set the right constraints... Or is the function from my above question always called at least once for each VC on load?)
Outside of a ViewController, such as custom UIView's, how do I detect the orientation change? Should i send out a custom notification event? I'd rather not have my UIViewController tell every subview it has that orientation has changed.
Thanks for any help :)
I would personally use this function:
override func viewWillTransitionToSize(size: CGSize,
withTransitionCoordinator coordinator:
UIViewControllerTransitionCoordinator) {}
And then when those changes are detected call setNeedsLayout and layoutIfNeeded which should trigger a redraw of all of your subviews which can then handle setting constraints for specific orientations
I've included a link to a video that shows what problem I'm having:
https://dl.dropboxusercontent.com/u/39330138/Bug_Demo1.mov
There are two View Controllers, the first is non blurred and less important. When the plus button is clicked, the app segues to a new controller (without animating) and in prepareForSegue() I use UIGraphicsBeginImageContext and UIGraphicsGetImageFromCurrentImageContext to capture a UIImage from the current view and pass it on to the next one.
When the new view appears I use UIVisualEffectView to create a blur view and add it as a subview to the Image View that is the 'background'. Then, its opacity is animated at the same time the 2 views and 2 buttons are animated on screen with UIView animation and springWithDamping, giving the illusion of the view blurring over and items animating over the top.
The top view has a UITextField embedded in it which, when tapped calls becomeFirstResponder() and makes all overlaid (New Session, Tag & Button) views including the Visual Effect View imbedded in the background Image View disappear.
The reason I go into so much detail is because I'm not sure what exactly the problem is. However, I have a suspicion that it is to do with the AutoLayout/Size Classes in Xcode 6.
Does anyone know why this might be happening and how to fix it?
If you need additional information, just let me know.
Thanks!
EDIT:
When I log the views after I click on the TextField, all the frames seem the same.
EDIT 2:
Here's a link to a demo project will all the functionality from the video:
https://dl.dropboxusercontent.com/u/39330138/DEMO%20APP.zip
There are a couple of things happening here, but the main culprit is your use of viewDidLayoutSubviews(). This is called any time the system has to reevaluate the layout. I see you're setting your UIVisualEffectView's alpha to 0 in that method:
if !returningFromTagView {
blurView.alpha = 0
}
I think you're intending this to be called just once before the view appears because I see you animate the alpha to 1 in viewDidAppear(animated: Bool). However, any time the system reevaluates layout for any reason, viewDidLayoutSubviews() is called and the alpha on blurView is going back to 0 if returningFromTagView is false. That's why when you summon the keyboard (triggering a layout reevaluation), this view disappears. Xcode also warns you about making the alpha 0 in the console (it breaks the visual effect until the opacity returns to 1). Put the code above in the viewDidLoad() method instead, and you'll see blurView come back. The alpha only needs to be set to 0 once when the view loads.
The issue with the other views is a bit tougher to see, but the culprit again is your use of viewDidLayoutSubviews(). I imagine that you're puzzled why the views don't appear even after you've been very thorough in your keyboardNotfication() method to set the frames, bring the views to the front, make sure they aren't hidden, and then log this all. But after the keyboardNotification() method finishes, the layout system once again is triggered, and I see that you're nudging the views' frames here and there:
if returningFromTagView {
setX(-titleView.frame.size.width, v: titleView)
setX(-tagView.frame.size.width, v: tagView)
setX(-(cancelButton.frame.size.width + 20 + nextButton.frame.size.width), v: cancelButton)
setX(-nextButton.frame.size.width, v: nextButton)
} else {
setX(-titleView.frame.size.width, v: titleView)
setX(view.frame.size.width, v: tagView)
setX(-cancelButton.frame.size.width, v: cancelButton)
setX(view.frame.size.width, v: nextButton)
}
You're moving the views offscreen every time a layout change is made! Pause the program after you summon the keyboard and look at your view hierarchy using Xcode 6's great new Capture View Hierarchy ability. It's in Debug > View Debugging > Capture View Hierarchy. Those views are just hiding off to the side.
I image you're trying to do this just once when the view appears in order to support your transition animations, but it gets called whether the view is just appearing or if a small change like the keyboard is appearing. I suggest that you implement these animations another way, like using the views' transforms or using autolayout constraints (though you have a lot of missing constraints in the storyboard) to do your animation. viewDidLayoutSubviews() is really a place to fudge things here and there in your layout after the layout system has done its work. You should have a good reason for using it. It has the nice feature of overriding your autolayout constraints and letting you animate those views without toying with the constraints (because the method happens after the updateConstraints() and layoutSubviews() methods), and that's why we can't put the above code in a method like viewWillAppear(animated: Bool) instead (because autolayout constraints would counter the animation during layout later), but viewDidLayoutSubviews() just is not a method that's meant to support basic animations.
In spite of that, here's something simple to get your app going again and for you to see what's going on:
Make a property var comingFromSessionView: Bool property for your NewSessionVC view controller. In the prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) of SessionVC, add nextVC.comingFromSessionView = true
Then change the code block from viewDidLayoutSubviews() above to this:
if returningFromTagView {
setX(-titleView.frame.size.width, v: titleView)
setX(-tagView.frame.size.width, v: tagView)
setX(-(cancelButton.frame.size.width + 20 + nextButton.frame.size.width), v: cancelButton)
setX(-nextButton.frame.size.width, v: nextButton)
} else if comingFromSessionView {
setX(-titleView.frame.size.width, v: titleView)
setX(view.frame.size.width, v: tagView)
setX(-cancelButton.frame.size.width, v: cancelButton)
setX(view.frame.size.width, v: nextButton)
}
We'll switch these Bools to false during viewDidAppear after it's done with them:
override func viewDidAppear(animated: Bool) {
...
if returningFromTagView {
...
returningFromTagView = false
} else if comingFromSessionView {
...
comingFromSessionView = false
}
}
Now when the keyboard is summoned, your views are right where you left them!
The code above isn't great. I'd rather stay away from viewDidLayoutSubviews() for doing these animations. But hopefully you can see what's going on now. Your viewsDidLayoutSubviews() has been whisking away your views.
Im new with iphone programming and im facing issues with the views.
I can not respent the view properly, for example in this case. I have an ViewController with a tabbar at the bottom. This controller have other 4 controllers where i show each of them when a user clicks on the icons.
But when a new view from one of the 4 controllers appears on the main controller, i get a line under the view. In the picture is a purple one.
When i change orientation, this empty line appears in the same place. Some times, when i enter with landscape orientation in the app, this line appears at the left.
I'm working here with IB. so, can someone point me here to the right direction? :S
On shouldAutorotateTo.. method i have YES, do i have to manage the views and change size and place every time the method is executed?
do i have to create 2 NIB for each controller, being one for each orientation option??
edit: i changed the picture to be more clear and show other "bug".
On 1 i enter to the view on portrait orientation. change it and still having a wear line up stares. All my nibs are on portrait orientation and 320 and 480 size.
On 2 i enter to the view on landscape but the nib loads as portrait. When i change orientation, it does not expand at it should.
How do i fix this 2nd issue?
i don't fill like having 2 nibs per controller :S
Thx in advance !
Short answer for Your autorotation problem:
You can either create two nibs (and switch between them, when autorotation occurs), or use just one nib. This depends on your goal. If you want to create a totally different view (like music app shows cover flow on landscape, and song on portrait) you should create two nibs.
If your view is more like the message app you could easily use just one nib. Take a look at the "Size Inspector" (5th tab in Xcode 4), the red lines guide you. The apple documentation for Xcode 4 interface builder is a good start to get you into that behavior.
That space in your view looks like the exact height of the status bar in iOS, review your NIBs in IB to see if you have a simulated status bar for your view controllers. If you do, turn it off and you will likely see that your view is not sized right. Being that it's height is 460 rather than 480.
What is the best way to implement a look and feel similar to the PPD Clinical Trials Mobile app (allows search for clinical trials) where it appears they have implemented a UINavigationController and UITabBar controller as overlays over the MainWindow and a high res background.
Also as a follow up to this question is there anywhere that has some stock iPhone 4 graphics (buttons, icons, and especially backgrounds) that can be used for mocking stuff up?
EDIT 0: By the way, I don't know that they use a UITabBarController for the bottom, but from all indications they do use a UINavigationController up top. It behaves as you would expect.
It seems to me that the PPD Clinical Trials Mobile app is composed of the following elements:
a modal view controller on top of the background;
the modal view controller has got a frame smaller than the screen size;
the modal view controller contains a UINavigationController;
the effect of transparency is obtained by setting the alpha property of the views inside the modal view controller to something below 1.0 (which means opaque).
Stock graphics/components:
http://spoiledmilk.dk/blog/iphone-ui-design
http://www.teehanlax.com/blog/iphone-gui-psd-30/
Icon sets:
http://glyphish.com/
As for the screenshot, I reckon you could accomplish it's contents in the following way:
Background: Static image, created in Photoshop or similar (pixlr.com as a great free online alternative)
Navigation bar: Overridden the default UINavigationBar and set the frames of the components differently.
Selection menus: Overridden default class objects again.
Hope this was of some help!
I need to develop an iPad application which should manage the two orientation mode (landscape and portrait).
According the official Apple iOS documentation, there are 2 ways to proceed.
-The first one consists in adjusting views element when the rotation event is received. The main advantage is that we had only one ViewController
-The second one consists in displaying a specific ViewController for each orientation. Therefore, we have 2 ViewControllers.
The second approach seems to be nice, but I'am afraid by the numbers of ViewController that will be needed. What's more, the "data synchronisation logic" in the ViewControllers will have to be duplicated (or isolated from the ViewController) to be used in both orientation.
The application I need to develop will contain many "full custom elements" and many ViewControllers.
If anyone has advices or experience feedback, it would be really appreciated ;)
Thank's for reading !
The second way should rather be: using 2 different views (one for portrait, one for landscape) and swapping the view controller's view in willRotateToInterfaceOrientation:. No need to duplicate your data logic.
Which way to use? I would say: it depends.
If the lanscape and the portrait modes differ only by the position / size of views, I use the first one (plus you'll get nice animations of the frame changes)
If landscape and portrait are too different, I prefer the second one.
I usually solve this by taking advantage of the autoresizing techniques in the view combined with the implementation of willAutorotateToInterfaceOrientation and willAnimateRotationToInterfaceOrientation methods in the view controller.
With autoresizing techniques you can easily resize standard UI elements provided by Apple. If your UI elements doesn't have an impossible layout, you can apply the autoresizing techniques to them too. You must set the autoresizesSubviews property to YES in the parent view controller and select an autoresizing behaviour for each subview. For example, if you want it to resize to the right maintaining the view centered, you can apply the autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin mask in the subview.
If the autoresizing techniques doesn't do the trick, then you will need to resize each conflicting view separately by implementing the - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration in your view controller. If you have "full custom elements", you will need to resize them this way.
In my particular experience, I prefer to have only one view controller and one view for all orientations and manage them with these two techniques.
Hope this helps you!