Animating Auto Layout changes concurrently with NSPopover contentSize change - cocoa

I'm attempting to reproduce the iTunes 11 behavior of navigable views within a popover. I can't seem to find a way to get my animation to happen at the same time as the popover's contentSize change happens, though.
The basic setup I have is a custom view subclass MyPopoverNavigationView with two subviews: the old and new views that I want the popover to navigate between. The popover's contentViewController has a MyPopoverNavigationView instance as its view. I do this:
// Configure constraints how I want them to show the new popover view
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *ctx) {
[ctx setDuration:0.25];
[ctx setAllowsImplicitAnimation:YES];
[self layoutSubtreeIfNeeded];
} completionHandler:nil];
As far as I can tell from the Auto Layout WWDC 2012 videos, this is the recommended way to animate changes to views' frames as a result of constraint changes. It works, but the animation happens in two phases:
First, the popover's contentSize will change to accommodate the new view that I'm moving to (before that view becomes visible, so it partially obscures the existing content).
Second, the views animate as I expect, so that the constraints system I installed is satisfied.
From setting some breakpoints, it looks like -layoutSubtreeIfNeeded eventually calls a private method on the popover called _fromConstraintsSetWindowFrame:, which does the popover size animation outside my animation group. My context's duration isn't respected, and my animations don't happen until the popover's size change is complete.
How can I get my views to animate together with the popover's size change?

Turns out the trick is to explicitly set the popover's contentSize property outside of the animation and completion blocks. The relevant snippet from the sample GitHub project I put together to figure it out looks like:
// Configure constraints for post-navigation view layout
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *ctx) {
[ctx setDuration:0.25];
[ctx setAllowsImplicitAnimation:YES];
[self layoutSubtreeIfNeeded];
} completionHandler:^{
// Tear down some leftover constraints from before the transition
}];
// Explicitly set popover's contentSize so its animation happens simultaneously
containingPopover.contentSize = postTransitionView.frame.size;

This works fine for me on Sierra:
let deltaHeight = 8
let contentSize = popover.contentSize
NSAnimationContext.runAnimationGroup({ (context) -> Void in
context.allowsImplicitAnimation = true
popover.contentSize = NSSize(width: contentSize.width, height: contentSize.height+deltaHeight)
})

Related

Can UIPopoverPresentationController be forced to reposition popover instead of resizing it?

I am using auto layout with Storyboard. I present a popoverPresentationController from a cell rect:
NumberController * viewController = [self.storyboard instantiateViewControllerWithIdentifier:#"NumberController"];
UIPopoverPresentationController *pc = [viewController popoverPresentationController];
pc.delegate = self;
pc.permittedArrowDirections = UIPopoverArrowDirectionAny;
pc.sourceView = tableView;
pc.sourceRect = [tableView rectForRowAtIndexPath:indexPath];
[self.navigationController presentViewController:viewController animated:animated completion:nil];
The popover presents on an iPad in portrait mode with the arrow up.
I rotate the iPad to landscape mode. The popoverPresentationController keeps the same sourceView/sourceRect and properly points to the cell. It also keeps the up arrow.
But it is now at the bottom of the view, so the popover resizes to a shorter height. This is not desired behavior.
If the popover were simply to move to a new position and change the arrow direction, it would not need to resize at all. This is the desired behavior.
I thought the following method might permit me to make changes, but it is not called since the sourceView rect does not change:
- (void)popoverController:(UIPopoverController *)popoverController
willRepositionPopoverToRect:(inout CGRect *)rect
inView:(inout UIView **)view {
}
I have tried to reset the permittedArrowDirections (in preferredContentSize, because this seemed like the most logical place). This does not work (the popover still resizes):
- (CGSize) preferredContentSize {
[super preferredContentSize];
self.popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirectionUnknown;
return CGSizeMake(DEFAULT_POPOVER_WIDTH,DEFAULT_POPOVER_HEIGHT);
}
I simply cannot find a way to force the popoverPresentationController to change arrow direction and reposition the popover instead of resizing the popover. I am beginning to think it is not even possible - but I still hold out hope that I am just missing something.
EDIT: In the meantime, it has occurred to me that maybe a popover is not the best way to present this view if I don't want it resized in iPad. I am going to try it with UIModalPresentationFormSheet presentation. But I would still like to find an answer to this question.
I just ran into the problem where
- (void)popoverController:(UIPopoverController *)popoverController
willRepositionPopoverToRect:(inout CGRect *)rect
inView:(inout UIView **)view {
was not being called because my view controller was detached. There may be a view in your view hierarchy whose view controller has not been added as a child view controller.
I thought the following method might permit me to make changes, but it is not called since the sourceView rect does not change
The sourceView rect does not have to change, just the interface orientation. From the UIPopoverControllerDelegate documentation:
For popovers that were presented using the presentPopoverFromRect:inView:permittedArrowDirections:animated: method, the popover controller calls this method when the interface orientation changes.

Pinch Zoom into UIImageView inside a UITableViewCell

I am creating a tableview where I will have an UIImageViewinside a UITableViewCell that I would like to allow the user to zoom into. I have been able to create this effect inside a UIScrollView but have struggled to get it right for zooming into a single UITableViewCell.
At the bottom I have included the code that has worked for me for the UIScrollView. The challenge starts with what to return for the viewForZoomingInScrollView. If I try to give this method the UIImageView from the cell It throws an errors as it appears the viewForZoomingInScrollView is called before the table is loaded thus it can not find the cell I am referencing. If I pass the entire view itself self.view I can scroll but the entire tableview is zoomed (where I would like to zoom just the UIImageView inside the cell) and when I release the zoom I get an error Terminating app due to uncaught exception 'CALayerInvalidGeometry', reason: 'CALayer bounds contains NaN: [nan nan; 375 667] which is clearly not the correct path to take.
Any help or guidance here would be appreciated.
- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imageView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self centerScrollViewContents];
}
- (void)centerScrollViewContents {
CGSize boundsSize = zoomTable.bounds.size;
CGRect contentsFrame = self.imageView.frame;
if (contentsFrame.size.width < boundsSize.width) {
contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0f;
} else {
contentsFrame.origin.x = 0.0f;
}
if (contentsFrame.size.height < boundsSize.height) {
contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0f;
} else {
contentsFrame.origin.y = 0.0f;
}
self.imageView.frame = contentsFrame;
}
It sounds to me as though you want something like what Facebook or Twitter have, where you tap on an image and it zooms to fit the screen.
What you need to do is to consider that as a navigational step -- i.e. conceptually similar to what happens if you select a row in a standard table view and it pushes a new view controller onto the navigation controller stack, except that you probably want to present modally using a custom transition.
In the simple case, this would mean adding a tap gesture recogniser to the cell's image view; for the full effect you would add a pinch gesture recogniser to make an interactive transition.
I'd recommend watching the following WWDC videos:
2013 Session 218 "Custom Transitions Using View Controllers"
2014 Session 214 "View Controller Advancements in iOS 8"
2014 Session 228 "A Look Inside Presentation Controllers"
From your question What I understand is that you wanted to implement the functionality of Zoom in and Zoom out for ImageView. You first implemented that in a sample where you had a ScrollView Inside a ViewController and then ImageView inside that ScrollView. Things worked for you as expected. But then when you wanted the same functionality inside the TableViewCell, things didn't work for you.
So, I am suggesting you to use a View inside ScrollView as a ZoomView, and that ZoomView should have the ImageView as subview. And return that ZoomView in the method:
- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView { return self.ZoomView; }

How to control drawing of NSTableView in NSScrollView?

How can I get NSTableView to always show the same columns regardless of the horizontal scroller position? In the rightmost visible column I have custom cell views. I want the horizontal scroller to control what is being drawn in these custom views. The vertical scrolling should work normally.
I have tried several approaches without much success. For example, I can control the knob proportion of the horizontal scroller by making the table view wider, or by making the scroll view think its document view is actually wider than it is. One way is subclassing NSClipView and overriding -documentRect as follows:
-(NSRect)documentRect {
NSRect rect = [super documentRect];
rect.size.width += [[NSApp delegate] hiddenRangeWidth];
return rect;
}
However, while the scroller knob looks as it should and I can drag it right without moving the table view, when I start scrolling in another direction, the knob returns to the left edge. I also have the problem that I can't get the horizontal scroller to appear automatically. This happens with the original classes as well, not just with my custom clip view. Could these problems be related?
I have also tried replacing the document view with a custom view that acts as a proxy between the clip view and the table view. Its -drawRect: calls the table view's -drawRect:. However, nothing is drawn. I guess this is because the table view now has no superview. If the table view were added to this proxy view as a subview, it would move with it. How would I make it stationary in horizontal axis?
So, to reiterate:
What is the best way to make a table view scrollable, while always showing the same columns regardless of the horizontal scroller position?
What is the best way to get the scroller position and knob proportion? Should I add an observer for the NSViewBoundsDidChangeNotification from NSClipView?
I finally managed to solve the problem by letting the scroll view and table view behave normally, and adding an NSScroller. In order to make hiding the scroller easier, I decided to use Auto Layout and add it in Interface Builder. (The Object library doesn't include a scroller, but you can add a custom view and set its class to NSScroller.) I set the height of the scroller as a constraint, and bound the scroller and the constraint to outlets in code:
#property (nonatomic, retain) IBOutlet NSScroller *scroller;
#property (nonatomic, unsafe_unretained) IBOutlet NSLayoutConstraint *scrollerHeightConstraint;
Now I can make the scroller visible or hide it when necessary:
if (_zoomedIn) {
_scrollerHeightConstraint.constant = [NSScroller scrollerWidthForControlSize:NSRegularControlSize scrollerStyle:NSScrollerStyleOverlay];
[_scroller setKnobProportion:(_visibleRange / _maxVisibleRange)];
[_scroller setDoubleValue:_visibleRangePosition];
[_scroller setEnabled:YES];
} else {
_scrollerHeightConstraint.constant = 0.0;
}
Here the properties visibleRange, maxVisibleRange and visibleRangePosition are the length of the visible range (represented by the scroller knob), the total range (represented by the scroller slot), and the start of the visible range (the knob position), respectively. These can be read by binding the scroller's sent action to the following method in Interface Builder:
- (IBAction)scrollAction:(id)sender {
switch (self.scroller.hitPart) {
case NSScrollerNoPart:
break;
case NSScrollerDecrementPage:
_visibleRangePosition = MAX(_visibleRangePosition - _visibleRange / _maxVisibleRange, 0.0);
self.scroller.doubleValue = _visibleRangePosition;
break;
case NSScrollerIncrementPage:
_visibleRangePosition = MIN(_visibleRangePosition + _visibleRange / _maxVisibleRange, 1.0);
self.scroller.doubleValue = _visibleRangePosition;
break;
case NSScrollerKnob:
case NSScrollerKnobSlot:
_visibleRangePosition = self.scroller.doubleValue;
break;
default:
NSLog(#"unsupported scroller part code %lu", (unsigned long)self.scroller.hitPart);
}
// Make the custom cell views draw themselves here.
}
In order to get the scrolling work with gestures, we need to implement -scrollWheel: in the custom cell view class:
- (void)scrollWheel:(NSEvent *)event {
if (event.deltaX != 0.0) {
NSScroller *scroller = appDelegate.scroller;
if (scroller.isEnabled) {
double delta = event.deltaX / (NSWidth(scroller.bounds) * (1.0 - scroller.knobProportion));
scroller.doubleValue = MIN(MAX(scroller.doubleValue - delta, 0.0), 1.0);
}
}
if (event.deltaY != 0.0) {
[self.nextResponder scrollWheel:event];
}
}
I thought I could've just passed the event to the scroller, but apparently it doesn't handle the event. The above code doesn't seem to handle bounce back, and momentum scrolling doesn't always work. Sometimes the knob just halts in the middle of the motion. I believe this has to do with the scroller style being NSScrollerStyleLegacy by default. Setting it to NSScrollerStyleOverlay would require changes to the layout, so I haven't tried it yet.
Another problem is that the scrollers don't blend into each other in the corner like they do in a scroll view (see below). Maybe NSScrollerStyleOverlay would fix this, too.

scrollRectToVisible UITextField doesn't scroll with Autolayout

[self.scrollView scrollRectToVisible:textField.bounds animated:YES];
I can't seem to get my UIScrollView to scroll at all so that it doesn't obscure my UITextField. I thought that scrollRectToVisible would be my savior but it looks like a no go. Maybe I'm missing something like translating the coordinates of my textField to my scrollView. Either way check out my sample project.
https://github.com/stevemoser/Programming-iOS-Book-Examples/tree/master/ch20p573scrollViewAutoLayout2
Oh, and this project might be missing the delegate connection but I checked that and it still doesn't scroll.
I've seen other questions similar to this but none that mention Autolayout.
I was having issues with scrollRectToVisible:: as well after converting to Auto Layout. I just changed it to a direct call to setContentOffset:: and it started working again.
I had the same problem, I wanted to scroll an autolayouted UITextEdit into view without making it the first responder.
For me the issue was that the bounds of the UITextField were set later on during the auto layout pass, so if you do it immediately after setting up the layout the bounds are not valid yet.
To workaround I did create a descendant of UITextField, did overwrite setBounds: and added a 0 timer to scroll into view "later on" (You can't scroll in that moment because the auto layout pass of the system might no be finished at that point)
#interface MyTextField: UITextField
{
bool _scrollIntoView;
}
..
#end
#implementation MyTextField
-(void)setBounds:(CGRect)bounds
{
bool empty=CGRectIsEmpty(self.bounds);
bool isFirstResponder=self.isFirstResponder;
[super setBounds:bounds];
if (empty && !isFirstResponder && _scrollIntoView)
[self performSelector:#selector(scrollIntoViewLater) withObject:nil afterDelay:0];
else if (empty && isFirstResponder)
[self performSelector:#selector(becomeFirstResponder) withObject:nil afterDelay:0];
}
-(void)scrollIntoViewLater
{
CGRect r=[scrollView convertRect:self.bounds fromView:self];
[scrollView scrollRectToVisible:r animated:TRUE];
}
#end
If the field should be additionally editable with the on screen keyboard, simply call becomeFirstResponder later on: it scrolls automagically into view above the keyboard using the private scrollTextFieldToVisible API which in turn calls scrollRectToVisible:animated: of the scrollview.
Your sample link is broken btw...

Cocoa WebView scrollbars won't disappear

I load the webview and set allowsScrolling to NO, but webview still shows scroll bars... Banging your head on your computer hurts a lot more now that MacBooks have sharp metal edges.
My code:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
NSString *webFolder = #"file:///<WebFolderPath>";
[[[productWeb mainFrame] frameView] setAllowsScrolling:NO];
[productWeb setFrameLoadDelegate:self];
[[productWeb mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[webFolder stringByAppendingString:#"webpage.html"]]]];
}
I even setup the frame loading delegate to report about the scrolling status:
- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
{
NSLog(#"Scrolling %#",[[frame frameView] allowsScrolling] ? #"Allowed" : #"Not Allowed");
[[frame frameView] setAllowsScrolling:NO];
NSLog(#"Scrolling %#",[[frame frameView] allowsScrolling] ? #"Allowed" : #"Not Allowed");
}
Which still gives me the unhappy:
2010-08-24 15:20:09.102 myApp[30437:a0f] Scrolling Allowed
2010-08-24 15:20:09.104 myApp[30437:a0f] Scrolling Not Allowed
And yet the scrollbars continue to show! Hopefully, it is something stupid I'm doing as I don't want to get any more blood on my laptop.
I found I had to edit the HTML of the page I was trying to display to make sure that it was setup to take the full screen (and not more)... there was a DIV that had a minimum width set. Once I made sure the body had height = 100% and that none of the DIV's had a fixed or minimum width set that was smaller then the box I wanted to show it in everything came together :)
Are you trying to prevent the user from scrolling the view at all? You can just set productWeb.userInteractionEnabled = NO. Or are you just trying to prevent the bars from showing when the user is scrolling?
Here's another thing you can try: inject some JavaScript into your UIWebView that disables the touchmove event:
[productWeb stringByEvaluatingJavaScriptFromString:#"document.ontouchmove = function(e){e.preventDefault();}"];
This leaves the user interaction enabled, but should prevent any scrolling.
Try this if you use Swift:
import Foundation
import Cocoa
import WebKit
class WebViewRep: WebView {
//... hidden code
required init?(coder: NSCoder) {
super.init(coder: coder)
//Disable scrolling
self.mainFrame.frameView.allowsScrolling = false
//Sets current object as the receiver of delegates
self.policyDelegate = self
self.frameLoadDelegate = self
//Load homepage
self.loadHomePage()
}
//... hidden code
override func webView(sender: WebView!, didFinishLoadForFrame frame: WebFrame!) {
//Completely disable scroll
frame.frameView.allowsScrolling = false
self.stringByEvaluatingJavaScriptFromString("document.documentElement.style.overflow = 'hidden';")
}
}
Sadly, it's not enough to use allowScrolling. It works sometimes, but not always. So, you have to manipulate loaded page too.
Of course, don't forget to set WebViewRep as custom class for your WebView.
I'm guessing your web view is in a nib that's being loaded before -applicationDidFinishLaunching: is called.
A quick fix:
In your nib, select the window and deselect "Visible at Launch" attribute.
Then after you call -setAllowsScrolling:, call -[self.window makeKeyAndOrderFront:nil] to show the window.
A better fix is to move your window loading code from the app delegate to a window controller subclass, do the call to -setAllowsScrolling: in -[NSWindowController windowDidLoad], and call -[NSWindowController showWindow:] from your app delegate to show the window.

Resources