HIScrollView is a view that allows the user to scroll another view. It contains one or more scroll bar views along with a content view, and manages the interaction between the scroll bars and the content (e.g., by adjusting the size/position of the scroll bars as the content changes).
To be scrollable, a content view must:
The HIScrollView model is that the content view's frame is expanded to fit within the confines of the scroll view content area (i.e., within the frame of the scroll view, minus the space required for scroll bars).
The frame of the content view remains fixed, and to produce scrolling behaviour the content view exposes two "image size" and "image origin" values to the scroll view. These values are used by the scroll view to adjust its scroll bars, and by the content view to adjust its drawing/hit testing.
This model is different from Cocoa's NSScrollView class, where the content view frame holds the scrollable "image size" and scrolling is performed by translating this frame within the scroll view. These two models are functionally equivalent, however the HIScrollView model makes it easier to support zoomable views.
E.g., the "zoom factor" can be multiplied in by the content view when responding to kEventScrollableGetInfo, and incorporated into the Quartz transform when drawing. This ensures that when zoomed, the view content remains in the same location within the image and is scaled automatically when drawing - with NSScrollView, the content view must be resized and the view's content repositioned whenever the zoom factor changes.
Unfortunately Interface Builder has limited support for HIScrollView, and currently (Xcode 2.4) can only embed an HIImageView and HITextView object within an HIScrollView. This embedding can be done using "Layout/Embed in/ScrollView" submenu. To embed any other kind of view, such as a custom view, you must create the HIScrollView at run time.
The simplest strategy is to create your content view with Interface Builder, and then invoke an "embed the content view inside a similarly-sized scroll view" function after creating the window. This can be done with:
See Scrolling a Custom HIView for additional notes.
//=============================================================================
// HIViewEmbedInScrollView : Embed a view within a scroll view.
//-----------------------------------------------------------------------------
OSStatus HIViewEmbedInScrollView(HIViewRef contentView, OptionBits theOptions)
{ HIViewRef scrollView;
HILayoutInfo layoutInfo;
HIRect theRect;
OSStatus theErr;
// Get the state we need
layoutInfo.version = kHILayoutInfoVersionZero;
theErr = HIViewGetLayoutInfo(contentView, &layoutInfo);
theErr |= HIViewGetFrame( contentView, &theRect);
// Create the scroll view
theErr = HIScrollViewCreate(theOptions, &scrollView);
if (theErr != noErr)
return(theErr);
// Place the scroll in the content view's parent
theErr |= HIViewAddSubview(HIViewGetSuperview(contentView), scrollView);
theErr |= HIViewSetFrame( scrollView, &theRect);
theErr |= HIViewSetLayoutInfo(scrollView, &layoutInfo);
// Place the content view in the scroll view
theErr |= HIViewRemoveFromSuperview(contentView);
theErr |= HIViewAddSubview(scrollView, contentView);
// Request an update
theErr |= HIViewSetVisible( scrollView, true);
theErr |= HIViewSetNeedsDisplay(contentView, true);
return(theErr);
}
HIScrollView supports two [semi-documented] tags that allow the scroll bars to be inset. These tags can be manipulated with:
//=============================================================================
// HIScrollViewSetInset : Set the scroll bar insets for a scroll view.
//-----------------------------------------------------------------------------
OSStatus HIScrollViewSetInset(HIViewRef scrollView, float insetH, float insetV)
{ OSStatus theErr;
// Set the insets
theErr = SetControlData(scrollView, kControlNoPart, 'hsin', sizeof(insetH), &insetH);
theErr |= SetControlData(scrollView, kControlNoPart, 'vsin', sizeof(insetV), &insetV);
return(theErr);
}
One common use for this feature is to inset the horizontal scroll bar, and place a small status placard at the bottom left of a window rather than using the entire window width for a status bar.
Once a view has been inserted into an HIScrollView, there is currently no API to retrieve the content view from the HIScrollView. This can be achieved with:
//=============================================================================
// HIScrollViewGetContent : Get the content view from a scroll view.
//-----------------------------------------------------------------------------
// Note : Requested as rdar://4153633.
//-----------------------------------------------------------------------------
HIViewRef HIScrollViewGetContent(HIViewRef scrollView)
{ HIViewRef subView, contentView;
CFIndex n, numViews;
EventRef theEvent;
OSStatus theErr;
// Create the event
theErr = CreateEvent(NULL, kEventClassScrollable, kEventScrollableGetInfo,
0, kEventAttributeNone, &theEvent);
if (theErr != noErr)
return(NULL);
// Search the sub-views
//
// To locate the content view, we need to iterate the sub-views of the
// scroll view, querying each one for their scrollable state.
//
// This relies on the other children of the scroll view (e.g., the scroll
// bar views) not responding to this event.
numViews = HIViewCountSubviews(scrollView);
contentView = NULL;
for (n = 0; n < numViews && contentView == NULL; n++)
{
theErr = HIViewGetIndexedSubview(scrollView, n, &subView);
if (theErr == noErr)
{
theErr = SendEventToEventTarget(theEvent, HIViewGetEventTarget(subView));
if (theErr == noErr)
contentView = subView;
}
}
// Clean up
ReleaseEvent(theEvent);
return(contentView);
}
HIScrollViewSetScrollBarAutoHide can be used to requested that an HIScrollView automatically show/hide its scroll bars based on the visible content of the scrolled view (an example of this effect can be seen in /Applications/Preview).
This API determines when to show/hide the scroll bars by comparing the visible content size to the maximum content size of the scrolled view.
Unfortunately the "maximum content size" is not determined by the kEventParamImageSize parameter of the kEventScrollableGetInfo event, but by the view's maximum size constraint (rdar://4083159).
If the size of the scrollable view can change over time (e.g., due to zooming or a change in content), and you wish to obtain auto-hiding scroll bars, your content view must also handle kEventControlGetSizeConstraints and set the maximum size constraint to the same value it returns for kEventParamImageSize on the kEventScrollableGetInfo event.