How to control drawing of NSTableView in NSScrollView? - cocoa

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.

Related

Subview loaded inside UIView does not constrain to UIView bounds

I have created the following scene:
Application scene
The scene contains a stackView with all other views contained inside of it. The grey area is a UIView which gets three subviews added to it during viewDidLoad. I want each of these views to fill the UIView and be constrained to its bounds. When one of the three buttons is clicked, the corresponding view is brought to the front of for display. The issue I am facing is that the subviews are not being constrained to the UIView. I've tried quite a few different options but cannot seem to get the effect I am looking for. The red in the following image is the background of one of the loaded subviews.
iPhone 5
On larger devices, the subviews overextend the boundary on the right side as well. How do I constrain the subViews to my UIView boundary?
Set the frame of the 3 views you want to be constrained with the bounds of the grey superview. This can be done in layoutSubviews of the grey view or in viewDidLayoutSubviews of the viewController.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
subview0.frame = greyView.bounds
subview1.frame = greyView.bounds
subview2.frame = greyView.bounds
}
or within the custom class for the grey view (if you have defined a custom class)
override func layoutSubviews() {
super.layoutSubviews()
subview0.frame = bounds
subview1.frame = bounds
subview2.frame = bounds
}
Can also be done by using constraints to constrain the subviews to their superview.

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 disable NSBox from code

I put several controls (button,textfield,...) in a NSBox. is it possible to disable NSBox that user can't access controls (means can't click on button or write in textfield)?
how about nsview ?
An NSBox is basically just a view with a border, there's no way to "disable" it. If you want to disable all the controls in a box, you could loop through all its subviews and disable them , or another way I've done this is to put an overlay view over the whole box and override mouseDown in that overlay (to capture any mouseDown events so they aren't queued in the event loop). You can also give the overlay a semi-transparent white color so the box has a disabled appearance.
Or, if you have a custom NSBox, you can override NSView's -hitTest: (conditionally)
- (NSView *)hitTest:(NSPoint)aPoint {
if (!enabled) return nil;
else return [super hitTest:aPoint];
}
To stop the window from sending events to all your subviews.
To provide visual feedback, conditionally drawing some sort of overlay in the custom NSBox's -drawRect method would work.
Yes, you just need to look at the subviews of the NSBox, which is typically just one NSView, and then your actual controls will be under the subviews of that.
Here's a quick C-style function I wrote to enable/disable most common UI controls, including NSBox...
void SetObjEnabled(NSObject * Obj, bool Enabled)
{
//Universal way to enable/disable a UI object, including NSBox contents
NSControl * C = (NSControl *)Obj;
if([C respondsToSelector:#selector(setEnabled:)])
[C setEnabled:Enabled];
if([C.className compare:#"NSTextField"] == NSOrderedSame)
{
NSTextField * Ct = (NSTextField*)C;
if(!Enabled)
[Ct setTextColor:[NSColor disabledControlTextColor]];
else //Enabled
[Ct setTextColor:[NSColor controlTextColor]];
}
else if([C.className compare:#"NSBox"] == NSOrderedSame)
{
NSBox * Cb = (NSBox*)C;
//There is typically just one subview at this level
for(NSView * Sub in Cb.subviews)
{
//Here is where we'll get the actual objects within the NSBox
for(NSView * SubSub in Sub.subviews)
SetObjEnabled(SubSub, Enabled);
}
}
}

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