I am calling this code in viewController2:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
if UIDevice.currentDevice().orientation.isLandscape.boolValue {
rotatedBool = true
tableViewConstraint.constant = -12
} else {
rotatedBool = false
tableViewConstraint.constant = 21
}
}
However, if I rotate the app from another view prior to having loaded this view (viewController2), I get a crash.
"Unexpectedly found nil while unwrapping optional value." (tableViewConstraint)
I dont understand why this code (in viewController2) gets called from another view.
Does anyone has an idea ?
Thanks,
It can't be called without calling the ViewController2 I replicated your problem and, as expected, nothing was called inside the ViewController2
You must have other codes that call it
Related
I'm looking for a way to make a custom transition when changing the focus from or to the UITabBar.
I'm currently trying this by overriding the didUpdateFocus method but I seem to be unable to check if the the tabBar is focused.
The tabBar itself seem to never be in the "focused" state:
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinatora)
{
super.didUpdateFocus(in: context, with: coordinator)
print(self.tabBar.isFocused) // always false
}
When checking the UIFocusUpdateContext the currently focused element is a UITabBarButton.
But I'm unable to check if the context is an instance of UITabBarButton because that type isn't available:
context.nextFocusedItem is UITabBarButton // can't compile
I'm really stuck here and would love any suggestions on how to tackle this one.
Thanks.
This answer probably won't help the OP (it was asked more than a year ago), but as I was struggling with the same issue, a presentation from WWDC17 helped me figure it out:
UIView, UIViewController, UIWindow, etc. all conform to the UIFocusEnvironment protocol. The tabBar itself never actually receives direct focus, so the correct way to check if one of its buttons has focus is by checking if its environment contains the UIFocusItem:
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if let nextFocusedItem = context.nextFocusedItem,
self.tabBar.contains(nextFocusedItem) {
//Do some stuff
}
}
I'm sure the asker figured this out ages ago, but I thought I'd throw this answer up for anyone else that happens to have the same question later.
First add extension for UIView if it has superview:
public func hasSuperView(_ view: UIView) -> Bool {
if self == view {
return true
}
if let superview = superview {
return superview.hasSuperView(view)
}
return false
}
Then add extension for UIFocusUpdateContext to check if view has focus:
public func viewHasFocus(_ view: UIView) -> Bool {
guard let nextFocusedView = nextFocusedView else { return false }
return nextFocusedView.hasSuperView(view) || view.hasSuperView(nextFocusedView)
}
Then use context in this method to check if tabbar has focus:
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if context.viewHasFocus(tabBar) {}
}
Also please override this method in your UITabBarController subclass
I have created extension on UIView which will check if there is focus somewhere in its hierarchy:
func containsFocus() -> Bool {
if self.isFocused { return true }
else {
for subview in self.subviews {
if subview.containsFocus() { return true }
}
return false
}
}
This way you can check if your UITabBar has focus.
Note:
This is very expensive way of checking if your view has focus, because it is recursive function and it will potentially traverse the whole hierarchy of your view.
Tip:
If you are trying to implement custom transition when selecting tabs in UITabBar you should use UITabBarDelegate method:
func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem)
If not, just ignore this tip.
Introduction
I'm creating a calendar app in which one of the screens has a landscape view and a portrait view. For simplicity picture the iOS apple calendar in which the landscape view is a week view (i.e. completely different than the portrait view).
Issue
I'm getting a sense of bad code structure and potential loss of efficiency in my current code. Since I basically use the users battery and CPU for the week view concurrently with the portrait view even though not everyone uses the week view. What is the better practice in implementing a different presentation depending on device rotation?
Question
Which pattern would be the more efficient approach? Is there a third option I haven't considered that would result in better performance?
My attempts
(I've also included a code example (below) that shows my implementation of these attempts in code.)
Two UIViewControllers that is segued and "popped" depending on conditions of device orientation in viewWillTransition(). Although that became quickly out of hand since the method triggers in all view controller currently in memory/navigationStack, resulting in additional copies of viewControllers in the navigation stack if you swap between right landscape and left landscape.
Using one UIViewController and two UIView subclass that is initialized and communicating to the view controller through the delegate-protocol pattern. In which during the viewWillTransition() I simply animate an alpha change between the two UIViews depending on the device orientation.
Code example
(I have provided two simplification to illustrate my attempts described above, methods such as dataSource and delegate methods for UICollectionViews are not included are not included in the example below.)
Attempt 1:
class PortraitCalendar: UIViewController {
let portraitCalendarView : MonthCalendar = {
// Setup of my portrait calendar, it is a UICollectionView subclass.
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(portraitCalendarView)
// Additional setup..
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if UIDevice.current.orientation.isLandscape {
performSegue(withIdentifier: "toLandscapeCalendar", sender: nil)
}
}
}
class LandscapeCalendar: UIViewController {
let landscapeView : LandscapeView = {
// Setup of the landscape view, a UICollectionView subclass.
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(landscapeView)
// Additional setup..
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if UIDevice.current.orientation.isPortrait {
navigationController?.popViewController(animated: true)
}
}
}
Attempt 2:
class PortraitCalendar: UIViewController, LandscapeCalendarDelegate {
let portraitCalendarView : MonthCalendar = {
// Setup of my portrait calendar, it is a UICollectionView subclass.
}
// UIView subclass with a UICollectionView within it as a week calendar.
let landscapeCalendar = LandscapeView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(portraitCalendarView)
view.addSubview(landscapeCalendar)
landscapeCalendar.alpha = 0
portraitCalendarView.alpha = 1
// Constraints and additional setup as well of course.
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if UIDevice.current.orientation.isLandscape {
navigationController?.isToolbarHidden = true
self.view.layoutIfNeeded()
landscapeCalendarDelegate?.splitCalendarViewWillAppear()
UIView.animate(withDuration: 0.1) {
self.portraitCalendarView.alpha = 0
self.landscapeCalendar.alpha = 1
}
} else {
self.portraitCalendarView.alpha = 1
self.landscapeCalendar.alpha = 0
}
}
}
Thanks for reading my question.
I'd definitely go for an option number 2.
That way you encapsulate all the logic related to the calendar, for example for adding event or displaying it, in one view controller, without the need to reimplement the sane logic somewhere else (eg other view controller with landscape mode). Having two views for a different layout modes is not THAT easy to maintain, but if that's the only way to show the difference between the modes it really is a fine solution. And it's much easier to maintain than two view controllers with the very similar logic.
I've found this question and I've tried to implement the solution that has been given. However I run into a problem.
My initial view controller has two container views who both have their own view controller. I've created a root view controller that is assigned to the initial view controller. The code in this class looks like this.
class RootViewController: UIViewController {
var willTransitionToPortrait: Bool!
var traitCollection_CompactRegular: UITraitCollection!
var traitCollection_AnyAny: UITraitCollection!
override func viewDidLoad() {
super.viewDidLoad()
setupReferenceSizeClasses()
// Do any additional setup after loading the view.
}
override func viewWillAppear(animated: Bool) {
willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
willTransitionToPortrait = size.height > size.width
}
func setupReferenceSizeClasses(){
let traitCollection_hCompact = UITraitCollection(horizontalSizeClass: .Compact)
let traitCollection_vRegular = UITraitCollection(verticalSizeClass: .Regular)
traitCollection_CompactRegular = UITraitCollection(traitsFromCollections: [traitCollection_hCompact, traitCollection_vRegular])
let traitCollection_hAny = UITraitCollection(horizontalSizeClass: .Unspecified)
let traitCollection_vAny = UITraitCollection(verticalSizeClass: .Unspecified)
traitCollection_AnyAny = UITraitCollection(traitsFromCollections: [traitCollection_hAny, traitCollection_vAny])
}
override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection? {
let traitCollectionForOverride = ((willTransitionToPortrait) != nil) ? traitCollection_CompactRegular : traitCollection_AnyAny
return traitCollectionForOverride;
}
However when I run it the size class won't respons like it should. One of the container view controllers will start acting weird in both landscape and portrait mode like can be seen below.
When I don't assign the rootviewcontroller it will look like this
While it should look like this in portrait mode
Does anyone know what might be going wrong here? Why it doesn't change the size class like desired.
EDIT
Like #Muhammad Yawar Ali asked here are screenshots from the position of all the size classes I've set. I have no warnings or errors on any constraints so these screenshots contain the updated views.
I hope this shows everything that is needed.
EDIT:
for some reason I'm unable to put in all the screenshots
On the viewWillTransitionToSize you need to call also super, to pass the event to the next responder (your rootviewcontroller)...
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
willTransitionToPortrait = size.height > size.width
}
Realize this is over two years old but...
I just ran across what I think is a similar issue. What you may be forgetting is that 'overrideTraitCollectionForChildViewController' only overrides the views children, so this method won't do anything with the containers since they are located at the root.
I solved this putting my two containers in a UIStackView in Interface Builder and made a property of this stack in code and then updated the axis depending on the orientation. For example, in Objective-C:
#property (weak, nonatomic) IBOutlet UIStackView *rootStack;
// ...
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) {
return [super overrideTraitCollectionForChildViewController:childViewController];
}
if (CGRectGetWidth(self.view.bounds) < CGRectGetHeight(self.view.bounds)) {
self.rootStack.axis = UILayoutConstraintAxisVertical;
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
}
else {
self.rootStack.axis = UILayoutConstraintAxisHorizontal;
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
If you have any constraints that are different between portrait and landscape you will need to adjust those in code as well.
I suppose you could also solve this by embedding the view controller with the containers in another view controller.
I have cloned your code from repository : https://github.com/MaikoHermans/sizeClasses.git
And editted code put the below code in you controller it will work fine & will not effect your design in iPads.
import UIKit
class RootViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection? {
if view.bounds.width < view.bounds.height {
return UITraitCollection(horizontalSizeClass: .Unspecified)
} else {
return UITraitCollection(horizontalSizeClass: .Regular)
}
}
}
You can try with this code but There is an issue i believe its not updating traits properly for ipads and view layout remains same but looks good. I have tried multiple ways but not succeeded yet will update my answer.
I'm trying to adjust some views when the iPhone changes it's orientation from portrait to landscape and vice versa. Everything works fine on iOS8 but unfortunately the changes aren't animated, but happen immediately, on iOS9. Here is the code that I have in a modal UIViewController presented via a custom transition:
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.AllButUpsideDown
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
return .Portrait
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
let changeToPortrait = size.width < size.height
coordinator.animateAlongsideTransition({ (context) -> Void in
//show portrait
if changeToPortrait {
//do view changes...
}
//show landscape
else {
//undo view changes...
}
}) { (context) -> Void in
print("done")
}
}
If I print coordinator.isAnimated() it says false and therefore the coordinator.transitionDuration() is also 0.0.
What do I have to do to animate the transition changes?
Thanks for your help!
This behavior is likely because you set a custom UIWindow subclass to the window property of the AppDelegate inside the application:didFinishLaunchingWithOptions: method, as telomere mentioned in another answer.
If you need to use a UIWindow subclass in iOS 9, implement a custom getter for the window property of the AppDelegate to maintain orientation change animations.
Apple's documentation for UIApplicationDelegate's window property says:
"...you must implement the getter method of this property and use it
to create and return your custom window."
Common practice is often to set the window property directly in application:didFinishLaunchingWithOptions:. Instead, in your AppDeleate, implement the custom getter like this (Thanks to Tomas Camin for the code, found here):
Objective-C
- (MyCustomWindow *)window
{
static MyCustomWindow *customWindow = nil;
if (!customWindow) customWindow = [[MyCustomWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
return customWindow;
}
Swift
var customWindow: MyCustomWindow?
var window: UIWindow? {
get {
customWindow = customWindow ?? MyCustomWindow(frame: UIScreen.mainScreen().bounds)
return customWindow
}
set { }
}
Have a look at your application:didFinishLaunchingWithOptions: handler. It seems that if you manually create your application's UIWindow object, this triggers the iOS9 rotation issue.
I'm not sure of the exact reason, but in our app we were able to fix the issue by removing the code which manipulated the UIWindow and instead let the storyboard system initialise it.
The today widget is drawn correctly when it is added to the today view. But if you user comes back to it later, the viewDidLoad function is not called and it is showing stale data. Should viewDidLoad be called everytime? Is there an iOS 9 / Xcode 7 beta 6 bug?
Edit:
Added that widgetPerformUpdateWithCompletionHandler not called either. I have breakpoints set and print functions
func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)) {
print("in widgetPerformUpdateWithCompletionHandler")
fetch()
completionHandler(NCUpdateResult.NewData)
}
When you scroll a widget off and back on screen, the same controller instance will be reused for a short amount of time (appears to be ~30 seconds in my testing), and viewDidLoad and widgetPerformUpdateWithCompletionHandler: will not be called.
However, viewWillAppear and viewDidAppear will be called every time your widget is displayed.
Posting my own answer, but would like discussion on this code - should it be there or how to properly do it?. We had in this method, and by removing it the widget began to work correctly
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
if let safeCoordinator = coordinator as UIViewControllerTransitionCoordinator?
{
print("coordinator != nil")
safeCoordinator.animateAlongsideTransition({ context in
self.tableView.frame = CGRectMake(0, 0, size.width, size.height)
}, completion: nil)
}
else
{
print("coordinator == nil")
}
}