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.
Related
I'm making an app in Xcode for Mojave OSX.
I want to make a resize of a collectionview and items position of that.
Currently I call to invalidateLayout() method, but it recalculate and set sizes and positions without animations.
In the GIF you could see what is the current behavior, I need that the last item go to second row, in an animated way.
For this i try to override this methods of NSCollectionViewFlowLayout
open override func prepare(forAnimatedBoundsChange oldBounds: NSRect){
}
open override func finalizeAnimatedBoundsChange() {
}
But these never get called, and I don't know what code I need to animate this transition.
(Moved from question to an answer.)
You could override the viewDidLayout of your viewController (or assign an observer to the frame view) and execute this:
collectionView.animator().performBatchUpdates(nil)
This reloads your collectionviewLayout with an animation.
I have a "two-panel selector" or "shoe box" implemented with an NSSplitView and two NSOutlineViews, one for each split item in the NSSplitView. Basically it looks like this:
[Window Toolbar]
| side | main |
The sidebar can be toggled by
func toggleSidebar() {
self.splitViewItems[0].animator().isCollapsed =
!self.splitViewItems[0].animator().isCollapsed
}
The problem
The animation is not smooth, especially when the "main"'s content are scrolled up below the toolbar (with the translucency effect).
Attempts
I have a customized subclass of NSTableHeaderCell with an overriding draw(withFrame:in:). I noticed the draw(withFrame:in:) method is called (per frame I guess) during the animation.
I also set the headerView of the NSOutlineView to be layer-backed, and set it as:
self.outlineView.headerView!.layerContentsRedrawPolicy = .onSetNeedsDisplay
self.outlineView.headerView!.layerContentsPlacement = .left
Other LayerContentsRedrawPolicys have been tried, not effective to the problem.
macOS's Finder
Finder has a sidebar hiding functionality. The animation is butter smooth. When in the outline mode, the column header has some suspicious transition going on.
Though we can't see how Finder optimized the animation, at least it shows that smooth animation is possible.
Any suggestions or answers will be appreciated.
I've been trying to add a custom view I've created in Interface Builder to an NSStackView, but am failing miserably.
This is my view controller. It currently does nothing but initialize the view from the NIB:
class ServiceControlViewController : NSViewController {
init() {
super.init(nibName: "ServiceControlView", bundle: nil)!
}
}
And here's how I'm trying to add the newly created view to the stack view (this happens in the app delegate):
#IBOutlet weak var stackView: NSStackView!
#IBAction func addButtonClicked(sender: AnyObject) {
let viewController = ServiceControlViewController()
stackView.addView(viewController.view, in: .top)
stackView.layoutSubtreeIfNeeded()
}
When I print the frame of the created view before it is added to the stack view it has the correct size. If I print it after, it is squashed to zero height. I suspect there is some auto-resizing magic going on that I don't understand, but I haven't been able to fix it.
Interestingly enough, if I set the ServiceControlViewController's view to e.g. be an NSButton, then it is correctly added and not squashed to zero height. It only happens with the Custom Views I create.
PS: My InterfaceBuilder-created view simply contains a bunch of buttons and a label:
PPS: I've added an MWE
It seems like there are no constraints on that custom view to make it larger than zero size.
You'll have to determine what its sizing behavior is, e.g. is it always the same size, or resizable with a minimum, etc.
And then in IB you can build the constraints necessary to create that effect. e.g.:
Alternatively, you could put these controls into a stack view to get a similar result.
Interestingly enough, if I set the ServiceControlViewController's view to e.g. be an NSButton, then it is correctly added and not squashed to zero height. It only happens with the Custom Views I create.
Correct. This is because an NSButton has something your custom NSViews do not have: an intrinsicContentSize. This is what the stack view uses to size and position your added views.
So, override intrinsicContentSize in your custom views and all will be well.
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
}
}
Summary: I want to replicate the accessibility behaviour of a UIAlertView, where the background view is still visible but VoiceOver does not interact with it.
Detail: I have implemented accessibility for an iPhone app, but have one problem remaining. In some cases I display a large view on top of all others (partially transparent, covering most of the original view) containing labels and a close button. i.e. basically a custom popup/alert view. The problem is, VoiceOver continues to reveal the views/controls underneath it.
One method to prevent the hidden views from being revealed by VoiceOver is to set the whole custom view background to be accessible. However, this isn't really what we want as this containing view shouldn't really be interacted with by the user, only its subviews (labels/buttons) should.
I think you should use this on your top laying view:
Objective-C
- (BOOL)accessibilityViewIsModal {
return YES;
}
Swift
accessibilityViewIsModal = true
This makes every element of the View Controller that is hidden unaccessible.
An implementation could be to set it to true when you show the view and set it to false when you dismiss that view.
More info
Note: Requires iOS5 and up
Swift 4
In swift try this:
Before your view is presented setup your viewController’s view like this:
yourViewController.view.accessibilityViewIsModal = true
Also try setting the self.view.accessibilityViewIsModal to true in viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
view.accessibilityViewIsModal = true
}
It also might help if you send a screen chances notification when your modal or pop up view is appearing by adding this to the viewWillAppear:
UIAccessibility.post(notification: .screenChanged, argument: nil)
You can set the following properties on the view overlaying the background:
view.isAccessibilityElement = false;
view.isAccessibilityViewModal = true;
Does this work?
In obj-c:
view.isAccessibilityElement = NO;
view.accessibilityViewIsModal = YES;
When you hide the item, you can set isAccessibilityItem to NO.