Pinch Zoom into UIImageView inside a UITableViewCell - uiscrollview

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; }

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.

Issue with portrait display after playing landscape video

I have an iPhone app that displays files to the user (videos, pdf files and documents). When you click on a file thumbnail in my FileViewController I call UIDocumentInteractionController to present the file to the user. All app screens are locked to portrait orientation apart from the document controller, that can be rotated to view landscape. This has all worked fine in previous releases, but it has suddenly changed and broken, I presume when iOS 8 was released.
Now, if I play a video and rotate it to landscape and hit done, the video is removed from the screen and the FileViewController nib that called the document the view is cut off half way down the screen.
I've done some testing and have printed out the view bounds width and height. Testing on an iPhone 4S, when I first load the FileViewController the width and height is 320 x 480, correct.
When I play a video in landscape mode and hit done, the underlaying view is automatically rotated back to portrait as landscape isn't support in this view, but when printing out the screen bounds the width is 480 and height 320. It still thinks the app is in landscape even though it's been moved back to portrait. The screen itself is the right way round, just cut off after 320 pixels!
I've been trying to manipulate the orientation of alsorts of controls but I can't see what's even generating the issue, let alone fixing it.
This is how I have implemented by orientations:
The App has been set up to support ALL orientations.
I have subclasses the tab bar controller to specify the following:
- (BOOL)shouldAutorotate
{
return YES;
}
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return UIInterfaceOrientationPortrait;
}
Within FileViewController I call the following code:
- (void) Show_File:(File *)file
{
NSURL *targetURL = [NSURL fileURLWithPath:filePath];
UIDocumentInteractionController *document = [UIDocumentInteractionController interactionControllerWithURL: targetURL];
document.delegate = self;
[document presentPreviewAnimated: YES];
}
- (UIViewController *)documentInteractionControllerViewControllerForPreview: (UIDocumentInteractionController *) controller
{
AppDelegate *theDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
return theDelegate.fileNavController.childViewControllerForStatusBarHidden;
}
Any ideas? I've search around but I can't find anyone having this particular issue. Thanks.
Just for anyone else experiencing this issue, I raised a support ticket with Apple and apparently it is an iOS bug....
"This is a bug QLPreviewController (which is used by UIDocumentInteractionController to display previews)"
I have a work around which is working ok for now. In my subclass of UITabBarController I've incldued this code:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.transitionCoordinator && self.presentedViewController.presentationController {
self.view.frame = self.presentedViewController.presentationController.containerView.bounds;
}
}

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.

scrolling a UIScrollView on the click of a button

I would like to know if it is possible for me to scroll a UIScrollView on the click of a UIButton, and if so, would someone be able to tell me how to.
I am currently using the below code to see if in the scrollview more content is there to its left and if it is there, display an image which would tell the users that there is more content if they scroll to the left.
I would like to implement a functionality where I add a UIButton instead of the image and when more content is available on the left and when the user clicks the button, the scrollview would scroll to its left.
Code:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView1
{
if (scrollView1.contentOffset.x == scrollView1.contentSize.width - scrollView1.frame.size.width)
{
// reached the right
self.imageView.hidden = YES;
}
else
{
self.imageView.hidden = NO;
}
}
It would be great if someone could help me out on this.
You can easily make the scrollview scroll by using the scrollRectToVisible:animated: of UIScrollView. You just have to enter the rectangle where you want it to scroll and use YES for animated. For instance:
[yourScrollView scrollRectToVisible:CGRectMake(10,10,100,100) animated:YES];
You could also try setContentOffset, just like this:
[yourScrollView setContentOffset: CGPointMake(0, 0) animated: YES]
Simply call this method in the IBAction of your button. Replace the yourScrollView with the UIScrollView instance you created yourself. This will automatically scroll your scrollView to the left.
Hope this may help.

How to put a disclosure triangle's title to the right of the triangle?

In the HIG's example of how to use disclosure triangles, it shows a label directly to the right of the triangle.
However, when I throw one of these onto my view in Interface Builder, the text is centered on top of the triangle. I've searched the NSButton API docs, and poked at everything I can find in IB, but nothing I try will put the text to the right of the triangle. What am I missing?
What I generally do is use 2 buttons: one disclosure button and another button for the label:
While you can use a text field for the label, I prefer using a button and setting the button to call performClick: on the disclosure triangle. This makes for a much larger target area to be able to click on than a tiny triangle. (Users with trackpads will thank you).
To set up the button, change it so it looks like this:
Then set its action:
I'm not sure if there's an actual way to get the button to show both properly (without subclassing I mean), since I've generally just used separate items to give the effect. (I just checked and there is indeed a Carbon disclosure control that has both the triangle and the label built-in).
The Carbon control has the right idea where clicking on the label will automatically trigger the control. In some places (notably the re-written Cocoa Finder), you can see that you don't get that behavior for free (unless you use a button like I've shown). I still have an open bug on that one (rdar://6828042): BugID 6828042: 10.6 (10A335) Finder: Inspector's disclsr. triangle's text label not toggleable". ;-)
Have you tried just using a triangle and using a separate label?
The disclosure triangle widget is drawn by the button's bezel, centered in the available space. To create a disclosure triangle button which also has a title, you just need to subclass NSButtonCell and make sure the bezel is restricted to the left side of the button and that the title avoids the bezel. Then add your button in IB, expand it and set your title, and set the class of the cell. Unfortunately, IB won't know how to display your subclass and will put the triangle in the middle of the button. Just make sure it's big enough.
In Objective-C:
#interface TitledDisclosureTriangleButtonCell : NSButtonCell
#end
#define TRIANGLE_PADDING 15.f
#implementation TitledDisclosureTriangleButtonCell
- (NSRect)titleRectForBounds:(NSRect)theRect
{
NSRect titleRect = [super titleRectForBounds:theRect];
titleRect.origin.x = TRIANGLE_PADDING;
titleRect.size.width = NSWidth(titleRect) - TRIANGLE_PADDING;
return titleRect;
}
- (void)drawBezelWithFrame:(NSRect)frame inView:(NSView *)controlView
{
NSRect bezelFrame = frame;
bezelFrame.size.width = TRIANGLE_PADDING;
[super drawBezelWithFrame:bezelFrame inView:controlView];
}
#end
And in Swift:
let TRIANGLE_PADDING: CGFloat = 15
class TitledDisclosureTriangleButtonCell: NSButtonCell
{
override func titleRectForBounds(theRect: NSRect) -> NSRect {
var titleRect = super.titleRectForBounds(theRect)
titleRect.origin.x = TRIANGLE_PADDING
titleRect.size.width = titleRect.size.width - TRIANGLE_PADDING
return titleRect
}
override func drawBezelWithFrame(frame: NSRect, inView controlView: NSView) {
var bezelFrame = frame
bezelFrame.size.width = TRIANGLE_PADDING
super.drawBezelWithFrame(bezelFrame, inView: controlView)
}
}

Resources