UIView hitTest:withEvent: and pointInside:withEvent - objective-c

I'm working with Mixare AR SDK for iOS and I need to solve some bugs that happends, one of them is show the information of a POI when the POI's view is tapped.
Prelude:
Mixare has an overlay UIView within MarkerView views are placed, MarkerView views are moving around the screen to geolocate the POIs and each one has two subviews, an UIImageView and an UILabel.
Issue:
Now, for example, there are 3 visible POIs in the screen, so there are 3 MarkerView as overlay subviews. If you touch anywhere in the overlay, a info view associated to a random POI of which are visible is showed.
Desired:
I want that the associated POI's info is shown only when the user tapped a MarkerView
Let's work. I've see that MarkerView inherits from UIView and implements hitTest:withEvent
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
viewTouched = (MarkerView*)[super hitTest:point withEvent:event];
return self;
}
I've put a breakpoint and hitTest is called once for each visible MarkerView but loadedView always is null so I can't work with it, so I've tried to check if the hit point is inside the MarkerView frame implementing pointInside:withEvent: by this way
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(#"ClassName: %#", [[self class] description]);
NSLog(#"Point Inside: %f, %f", point.x, point.y);
NSLog(#"Frame x: %f y: %f widht:%f height:%f", self.frame.origin.x, self.frame.origin.y, self.frame.size.width, self.frame.size.height);
if (CGRectContainsPoint(self.frame, point))
return YES;
else
return NO;
return YES;
}
But this function always returns NO, even when I touch the MarkerView. When I check the log I saw that X and Y point values has negative values sometimes and width and height of the view are very small, 0.00022 or similar instead of 100 x 150 that I set the MarkerView frame on its initialization.
Here you are a extract of my log in which you can see the class name, the point and the MarkerView frame values.
ClassName: MarkerView
2011-12-29 13:20:32.679 paisromanico[2996:707] Point Inside: 105.224899, 49.049023
2011-12-29 13:20:32.683 paisromanico[2996:707] Frame x: 187.568573 y: 245.735138 widht:0.021862 height:0.016427
I'm very lost with this issue so any help will be welcome. Thanks in advance for any help provided and I'm sorry about this brick :(
Edit:
At last I've found that the problem is not in hitTest:withEvent: or pointInside:withEvent, problem is with CGTransform that applies to the MarkerView for scaling based on distande and rotating the view, if I comment any code related to this, the Mixare AR SDK works fine, I mean, info view is shown correctly if you touch a marker and doesn't do anything if any other place in the screen is touched.
So, by the moment, I've not solved the problem but I applied a patch removing the CGTransform related code in AugmentedViewController.m class - (void)updateLocations:(NSTimer *)timer function
- (void)updateLocations:(NSTimer *)timer {
//update locations!
if (!ar_coordinateViews || ar_coordinateViews.count == 0) {
return;
}
int index = 0;
NSMutableArray * radarPointValues= [[NSMutableArray alloc]initWithCapacity:[ar_coordinates count]];
for (PoiItem *item in ar_coordinates) {
MarkerView *viewToDraw = [ar_coordinateViews objectAtIndex:index];
viewToDraw.tag = index;
if ([self viewportContainsCoordinate:item]) {
CGPoint loc = [self pointInView:ar_overlayView forCoordinate:item];
CGFloat scaleFactor = 1.5;
if (self.scaleViewsBasedOnDistance) {
scaleFactor = 1.0 - self.minimumScaleFactor * (item.radialDistance / self.maximumScaleDistance);
}
float width = viewToDraw.bounds.size.width ;//* scaleFactor;
float height = viewToDraw.bounds.size.height; // * scaleFactor;
viewToDraw.frame = CGRectMake(loc.x - width / 2.0, loc.y-height / 2.0, width, height);
/*
CATransform3D transform = CATransform3DIdentity;
//set the scale if it needs it.
if (self.scaleViewsBasedOnDistance) {
//scale the perspective transform if we have one.
transform = CATransform3DScale(transform, scaleFactor, scaleFactor, scaleFactor);
}
if (self.rotateViewsBasedOnPerspective) {
transform.m34 = 1.0 / 300.0;
double itemAzimuth = item.azimuth;
double centerAzimuth = self.centerCoordinate.azimuth;
if (itemAzimuth - centerAzimuth > M_PI) centerAzimuth += 2*M_PI;
if (itemAzimuth - centerAzimuth < -M_PI) itemAzimuth += 2*M_PI;
double angleDifference = itemAzimuth - centerAzimuth;
transform = CATransform3DRotate(transform, self.maximumRotationAngle * angleDifference / (VIEWPORT_HEIGHT_RADIANS / 2.0) , 0, 1, 0);
}
viewToDraw.layer.transform = transform;
*/
//if we don't have a superview, set it up.
if (!(viewToDraw.superview)) {
[ar_overlayView addSubview:viewToDraw];
[ar_overlayView sendSubviewToBack:viewToDraw];
}
} else {
[viewToDraw removeFromSuperview];
viewToDraw.transform = CGAffineTransformIdentity;
}
[radarPointValues addObject:item];
index++;
}
float radius = [[[NSUserDefaults standardUserDefaults] objectForKey:#"radius"] floatValue];
if(radius <= 0 || radius > 100){
radius = 5.0;
}
radarView.pois = radarPointValues;
radarView.radius = radius;
[radarView setNeedsDisplay];
[radarPointValues release];
}
Any CoreGrapics or UI expert could give us his point of view about this issue??

You should either try to hittest as attached:
if ([self pointInside:point withEvent:event]) {
// do something
}
I would suggest you add the hit test on the superview, and do the following in the hit test of the parent of the markerViews
if ([markerView pointInside:point withEvent:event]) {
// extract the tag and show the relevant info
}
Hope this helps

Related

Keeping the contents of a scaled NSScrollView centered and visible when resizing the window

I am trying to magnify an NSScrollView which contains NSTextView and keep it centered to its content at all times. The NSTextView has left/right insets to keep the word wrapping consistent and to keep the paragraphs nicely at the center of the view.
Both [NSScrollView scaleUnitSquareToSize:...] and setMagnification:... have their own quirks and problems, but for now setMagnification seems a better option, as it is not relative.
Here's what happens (among other strange stuff):
On resizing, I update the insets:
CGFloat inset = self.textScrollView.frame.size.width / 2 - _documentWidth / 2;
self.textView.textContainerInset = NSMakeSize(inset, TEXT_INSET_TOP);
self.textView.textContainer.size = NSMakeSize(_documentWidth, self.textView.textContainer.size.height);
Zooming in:
CGFloat magnification = [self.textScrollView magnification];
NSPoint center = NSMakePoint(self.textScrollView.frame.size.width / 2, self.textScrollView.frame.size.height / 2);
if (zoomIn) magnification += .05; else magnification -= .05;
[self.textScrollView setMagnification:magnification centeredAtPoint:center];
Everything kind of works for a while. Sometimes, depending on from which window corner the window is resized, the ScrollView loses its center, and I haven't found a solution for re-centering the view of a magnified NSScrollView.
After magnification, layout constraints can get broken too when resizing the window, especially when the textContainer is clipped out of view, and the app crashes with the following error:
*** Assertion failure in -[NSISLinearExpression addVariable:coefficient:], /Library/Caches/com.apple.xbs/Sources/Foundation/Foundation-1349.91/Layout.subproj/IncrementalSimplex/NSISLinearExpression.m:716
One problem might be that I am setting the insets according to UIScrollView frame size, because the contained NSTextView's coordinates don't seem to be relative but absolute after magnification.
Is there any safe way to magnifying this sort of view and keeping it centered to its content at all times? And why are my constraints breaking?
I've run into similar problems, and unfortunately I ended up doing the centering myself. Here are some of the highlights of my solution.
needs recursion prevention! (otherwise stackoverflow :)
create a non-drawable NSView as the documentView, and then add your drawable view as a subview which is centered manually, and manually set the frame to the visibleRect of the parent.
override visibleRect, call it a second time if its invalid, and debug to make sure it is valid!
zooming layered backed views sux. You could try using an NSTiledLayer, but I've tried and abandoned that solution multiple times.
Code below:
#interface FlippedParentView : NSView
#end
#implementation FlippedParentView
- (BOOL) isFlipped { return YES; }
#end
- (void)awakeFromNib
{
[self resetMouseInfo];
[[self window] setAcceptsMouseMovedEvents:YES];
needsFullRedraw = YES;
[self setAcceptsTouchEvents:YES];
// problem: when zoomed-in, CALayer backed NSOpenGLView becomes too large
// and hurts performance.
// solution: create a fullsizeView for the NSScrollView to resize,
// and make NSOpenGLView a subview. Keep NSOpenGLView size the same as visibleRect,
// positioning it as needed on the fullsizeView.
NSScrollView *scrollvw = [self enclosingScrollView];
[scrollvw setBackgroundColor:[NSColor darkStrokeColor]];
fullsizeView = [[FlippedParentView alloc] initWithFrame: [self frame]];
[scrollvw setDocumentView:fullsizeView];
[fullsizeView setAutoresizesSubviews:NO];
//printf("mask %d\n", [self autoresizingMask]);
[fullsizeView setAutoresizingMask: NSViewHeightSizable | NSViewWidthSizable | NSViewMinYMargin | NSViewMaxYMargin | NSViewMaxXMargin | NSViewMinXMargin];
[self setAutoresizingMask: NSViewNotSizable];
[fullsizeView addSubview:self];
}
- (NSRect) visibleRect
{
NSRect visRect = [super visibleRect];
if ( visRect.size.width == 0 )
{
visRect = [[self superview] visibleRect];
if ( visRect.size.width == 0 )
{
// this jacks up everything
DUMP( #"bad visibleRect" );
}
visRect.origin = NSZeroPoint;
}
return visRect;
}
- (void) _my_zoom: (double)newZoom
{
mouseFocusPt = [self focusPt];
NSRect oldVisRect = [[self superview] visibleRect];
if ( newZoom < 1.0 )
newZoom = 1.0;
if ( newZoom > kZoomFactorMax ) newZoom = kZoomFactorMax;
float xpct = (mouseFocusPt.x - oldVisRect.origin.x) /
( NSMaxX(oldVisRect) - oldVisRect.origin.x );
float ypct = (mouseFocusPt.y - oldVisRect.origin.y) /
( NSMaxY(oldVisRect) - oldVisRect.origin.y );
float oldZoom = zoomFactor;
zoomFactor = newZoom;
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Stay locked on users' relative mouse location, so user can zoom in and back out without
// the view scrolling out from under the mouse location.
NSPoint newFocusPt = NSMakePoint (mouseFocusPt.x * newZoom/oldZoom,
mouseFocusPt.y * newZoom/oldZoom) ;
NSRect myFrame = fullsizeFrame; // [self frame];
float marginPercent = (myFrame.size.height - drawableSizeWithMargins.height) / drawableSizeWithMargins.height;
[self updateContext];
NSRect newVisRect;
newVisRect.size = [self visibleRect].size;
newVisRect.origin.x = (newFocusPt.x) - (xpct * newVisRect.size.width);
//DLog( #"xpct %0.2f, zoomFactor %0.2f, newVisRect.origin.x %0.2f", xpct, zoomFactor, newVisRect.origin.x);
myFrame = fullsizeFrame; // [self frame];
float marginPercent2 = (myFrame.size.height - drawableSizeWithMargins.height) / drawableSizeWithMargins.height;
float marginDiff = (marginPercent - marginPercent2) * drawableSizeWithMargins.height;
newVisRect.origin.y = (newFocusPt.y ) - (ypct * newVisRect.size.height) - marginDiff;
//DLog( #"ypct %0.2f, zoomFactor %0.2f, newVisRect.origin.y %0.2f", ypct, zoomFactor, newVisRect.origin.y);
//DLog( #"marginPercent %0.2f newVisRect %#", marginPercent, NSStringFromRect(newVisRect) );
if ( newVisRect.origin.x < 1 ) newVisRect.origin.x = 1;
if ( newVisRect.origin.y < 1 ) newVisRect.origin.y = 1;
// NSLog( #"zoom scrollRectToVisible %# bounds %#", NSStringFromRect(newVisRect), NSStringFromRect([[self superview] bounds]) );
// if ( iUseMousePt || isSlider )
[[self superview] scrollRectToVisible:newVisRect];
}
// - zoomFactor of 1.0 is defined as the zoomFactor needed to show entire selected context within visibleRect,
// including margins of 5% of the context size
// - zoomFactor > 1.0 will make pixels look bigger (view a subsection of a larger total drawableSize)
// - zoomFactor < 1.0 will make pixels look smaller (selectedContext size will be less than drawableSize)
-(void)updateContext
{
static BOOL sRecursing = NO;
if ( sRecursing ) return; // prevent recursion
sRecursing = YES;
//NSRect scrollRect = [[self superview] frame];
NSRect clipViewRect = [[[self enclosingScrollView] contentView] frame];
NSRect visRect = [[self superview] visibleRect]; // careful... visibleRect is sometimes NSZeroRect
float layoutWidth = clipViewRect.size.width;
float layoutHeight = clipViewRect.size.height;
marginPct = layoutHeight / (layoutHeight - (overlayViewMargin*2) );
// Satisfy the constraints fully-zoomed-out case:
// 1) the drawable rect is centered in the view with at margins.
// Allow for 5% margins (1.025 = 2.5% left, right, top, bottom)
// 2) guarantee the drawable rect does not overlap the mini-map in upper right corner.
NSRect baseRect = NSZeroRect;
baseRect.size = visRect.size;
NSRect drawableBaseRect = getCenteredRectFloat(baseRect, metaUnionRect.size );
//drawableSizeWithMargins = nsIntegralSize( nsScaleSize( drawableBaseRect.size, zoomFactor ) );
drawableSizeWithMargins = nsScaleSize( drawableBaseRect.size, zoomFactor );
// drawableSize will NOT include the margins. We loop until we've satisfied
// the constraints above.
drawableSize = drawableSizeWithMargins;
do
{
NSSize shrunkSize;
shrunkSize.width = layoutWidth / marginPct;
shrunkSize.height = layoutHeight / marginPct;
//drawableSize = nsIntegralSize( nsScaleSize( drawableBaseRect.size, zoomFactor / marginPct ));
drawableSize = nsScaleSize( drawableBaseRect.size, zoomFactor / marginPct );
[self calculateMiniMapRect]; // get approx. size. Will calculate once more below.
NSRect shrunkRect = getCenteredRectNoScaling(baseRect, shrunkSize );
// DLog( #"rough miniMapRect %# shrunk %#", NSStringFromRect(miniMapRect), NSStringFromRect(shrunkRect));
// make sure minimap doesn't overlap drawable when you scroll to top-left
NSRect topMiniMapRect = miniMapRect;
topMiniMapRect.origin.x -= visRect.origin.x;
topMiniMapRect.origin.y = 0;
if ( !NSIntersectsRect( topMiniMapRect, shrunkRect ) )
{
topMarginPercent = fabs(shrunkRect.origin.y - drawableBaseRect.origin.y) / baseRect.size.height;
break;
}
float topMarginOffset = shrunkRect.size.height + (baseRect.size.height * 0.025);
shrunkRect.origin.y = NSMaxY(baseRect) - topMarginOffset;
if ( !NSIntersectsRect( topMiniMapRect, shrunkRect ) )
{
topMarginPercent = fabs(shrunkRect.origin.y - drawableBaseRect.origin.y) / baseRect.size.height;
break;
}
marginPct *= 1.025;
} while (1);
fullsizeFrame.origin = NSZeroPoint;
fullsizeFrame.size.width = fmax(drawableSizeWithMargins.width, layoutWidth);
fullsizeFrame.size.height = fmax(drawableSizeWithMargins.height, layoutHeight);
[fullsizeView setFrame:fullsizeFrame];
NSRect myNewFrame = [fullsizeView visibleRect];
if (myNewFrame.size.width > 0)
[self setFrame: myNewFrame]; //NSView
sRecursing = NO;
}

contentView is shorter than UIScrollView

I am able to scroll all the way down with the code below:
- (void)viewDidLayoutSubviews {
dispatch_async (dispatch_get_main_queue(), ^ {
CGFloat scrollViewHeight = 0.0f;
for (UIView* view in self.scrollView.subviews) {
scrollViewHeight += view.frame.size.height;
}
[self.scrollView setContentSize:(CGSizeMake(320, scrollViewHeight))];
});
}
but when I scroll, the contentView (in gray) is short and everything below it does not work:
EDIT:
EDIT Two things might be wrong with the layout. (1) The single subview of the scroll view (gray) needs to be large enough to encompass it's subviews, and (2) as the OP implies, so must the scrollView contentSize.
UIView *grayView = // get a pointer to that grayView
CGFloat maxContentY = 0.0;
for (UIView* view in grayView.subviews) {
maxContentY = MAX(maxContentY, CGRectGetMaxY(view.frame));
}
grayView.frame = CGRectMake(0,0, 320,maxContentY);
If that's the only subview of the scrollView, then the contentSize is easy:
[self.scrollView setContentSize:grayView.frame.size];
If there are other subviews, beside the gray one, then my original answer holds also, size the grayView as above and figure the max extent of the other subviews:
We typically aim to make the contentSize.height large enough to encompass the bottom edge of the bottom-most subview.
That's different calculation than the sum of the heights of the subviews, as the OP has it. (Consider a single subview at offset y == 1000 and height == 2. We want a height of 1002, not 2), so...
- (void)viewDidLayoutSubviews {
dispatch_async (dispatch_get_main_queue(), ^ {
CGFloat maxY = 0.0f;
for (UIView* view in self.scrollView.subviews) {
maxY = MAX(maxY, CGRectGetMaxY(view.frame));
}
[self.scrollView setContentSize:(CGSizeMake(320, maxY))];
});
}
The async invocation is harmless but hopefully unnecessary. Either the loop runs in trivial time, or we've added too many subviews.
try this one
- (void)viewDidLayoutSubviews {
dispatch_async (dispatch_get_main_queue(), ^ {
CGRect contentRect = CGRectZero;
for (UIView *view in self.scrollView.subviews)
contentRect = CGRectUnion(contentRect, view.frame);
[self.scrollView setContentSize:contentRect.size];
});
}

NSScrollView infinite / endless scroll | subview reuse

I'm searching for a way to implement something like reusable cells for UI/NSTableView but for NSScrollView. Basically I want the same like the WWDC 2011 video "Session 104 - Advanced Scroll View Techniques" but for Mac.
I have several problems realizing this. The first: NSScrollView doesn't have -layoutSubviews. I tried to use -adjustScroll instead but fail in setting a different contentOffset:
- (NSRect)adjustScroll:(NSRect)proposedVisibleRect {
if (proposedVisibleRect.origin.x > 600) {
// non of them work properly
// proposedVisibleRect.origin.x = 0;
// [self setBoundsOrigin:NSZeroPoint];
// [self setFrameOrigin:NSZeroPoint];
// [[parentScrollView contentView] scrollPoint:NSZeroPoint];
// [[parentScrollView contentView] setBoundsOrigin:NSZeroPoint];
}
return proposedVisibleRect;
}
The next thing I tried was to set a really huge content view with a width of millions of pixel (which actually works in comparison to iOS!) but now the question is, how to install a reuse-pool?
Is it better to move the subviews while scrolling to a new position or to remove all subviews and insert them again? and how and where should I do that?
As best I can tell, -adjustScroll: is not where you want to tap into the scrolling events because it doesn't get called universally. I think -reflectScrolledClipView: is probably a better hookup point.
I cooked up the following example that should hit the high points of one way to do a view-reusing scroll view. For simplicity, I set the dimensions of the scrollView's documentView to "huge", as you suggest, rather than trying to "fake up" the scrolling behavior to look infinite. Obviously drawing the constituent tile views for real is up to you. (In this example I created a dummy view that just fills itself with red with a blue outline to convince myself that everything was working.) It came out like this:
// For the header file
#interface SOReuseScrollView : NSScrollView
#end
// For the implementation file
#interface SOReuseScrollView () // Private
- (void)p_updateTiles;
#property (nonatomic, readonly, retain) NSMutableArray* p_reusableViews;
#end
// Just a small diagnosting view to convince myself that this works.
#interface SODiagnosticView : NSView
#end
#implementation SOReuseScrollView
#synthesize p_reusableViews = mReusableViews;
- (void)dealloc
{
[mReusableViews release];
[super dealloc];
}
- (NSMutableArray*)p_reusableViews
{
if (nil == mReusableViews)
{
mReusableViews = [[NSMutableArray alloc] init];
}
return mReusableViews;
}
- (void)reflectScrolledClipView:(NSClipView *)cView
{
[super reflectScrolledClipView: cView];
[self p_updateTiles];
}
- (void)p_updateTiles
{
// The size of a tile...
static const NSSize gGranuleSize = {250.0, 250.0};
NSMutableArray* reusableViews = self.p_reusableViews;
NSRect documentVisibleRect = self.documentVisibleRect;
// Determine the needed tiles for coverage
const CGFloat xMin = floor(NSMinX(documentVisibleRect) / gGranuleSize.width) * gGranuleSize.width;
const CGFloat xMax = xMin + (ceil((NSMaxX(documentVisibleRect) - xMin) / gGranuleSize.width) * gGranuleSize.width);
const CGFloat yMin = floor(NSMinY(documentVisibleRect) / gGranuleSize.height) * gGranuleSize.height;
const CGFloat yMax = ceil((NSMaxY(documentVisibleRect) - yMin) / gGranuleSize.height) * gGranuleSize.height;
// Figure out the tile frames we would need to get full coverage
NSMutableSet* neededTileFrames = [NSMutableSet set];
for (CGFloat x = xMin; x < xMax; x += gGranuleSize.width)
{
for (CGFloat y = yMin; y < yMax; y += gGranuleSize.height)
{
NSRect rect = NSMakeRect(x, y, gGranuleSize.width, gGranuleSize.height);
[neededTileFrames addObject: [NSValue valueWithRect: rect]];
}
}
// See if we already have subviews that cover these needed frames.
for (NSView* subview in [[[self.documentView subviews] copy] autorelease])
{
NSValue* frameRectVal = [NSValue valueWithRect: subview.frame];
// If we don't need this one any more...
if (![neededTileFrames containsObject: frameRectVal])
{
// Then recycle it...
[reusableViews addObject: subview];
[subview removeFromSuperview];
}
else
{
// Take this frame rect off the To-do list.
[neededTileFrames removeObject: frameRectVal];
}
}
// Add needed tiles from the to-do list
for (NSValue* neededFrame in neededTileFrames)
{
NSView* view = [[[reusableViews lastObject] retain] autorelease];
[reusableViews removeLastObject];
if (nil == view)
{
// Create one if we didnt find a reusable one.
view = [[[SODiagnosticView alloc] initWithFrame: NSZeroRect] autorelease];
NSLog(#"Created a view.");
}
else
{
NSLog(#"Reused a view.");
}
// Place it and install it.
view.frame = [neededFrame rectValue];
[view setNeedsDisplay: YES];
[self.documentView addSubview: view];
}
}
#end
#implementation SODiagnosticView
- (void)drawRect:(NSRect)dirtyRect
{
// Draw a red tile with a blue border.
[[NSColor blueColor] set];
NSRectFill(self.bounds);
[[NSColor redColor] setFill];
NSRectFill(NSInsetRect(self.bounds, 2,2));
}
#end
This worked pretty well as best I could tell. Again, drawing something meaningful in the reused views is where the real work is here.
Hope that helps.

Change the height of NavigationBar and UIBarButtonItem elements inside it in Cocoa Touch

I suppose it's not strictly in line with Apple guidelines but I guess it must be possible somehow. I'd like to change the height of navigation bar inside UINavigationController and the height of UIBarButtonItem elements inside that bar.
Using a trick from this question I managed to change the height of navigation bar but I can see no way of adjusting the height of bar button items.
If anyone knows how to change the size of bar button items, please help me out.
This is my solution. It works very well.
#interface UINavigationBar (CustomHeight)
#end
#implementation UINavigationBar (CustomHeight)
- (CGSize)sizeThatFits:(CGSize)size {
// Change navigation bar height. The height must be even, otherwise there will be a white line above the navigation bar.
CGSize newSize = CGSizeMake(self.frame.size.width, 40);
return newSize;
}
-(void)layoutSubviews {
[super layoutSubviews];
// Make items on navigation bar vertically centered.
int i = 0;
for (UIView *view in self.subviews) {
NSLog(#"%i. %#", i, [view description]);
i++;
if (i == 0)
continue;
float centerY = self.bounds.size.height / 2.0f;
CGPoint center = view.center;
center.y = centerY;
view.center = center;
}
}
Maybe this tutorial about a customized navbar will help you: Recreating the iBooks wood themed navigation bar
If you create a BarButtonItem with a UIImageView you can maybe change the framesize/boundsize of the custom UIImageView
UIImageView* imageView = [[[UIImageView alloc] initWithFrame:navigationController.navigationBar.frame] autorelease];
imageView.contentMode = UIViewContentModeLeft;
imageView.image = [UIImage imageNamed:#"NavBar-iPhone.png"];
[navigationController.navigationBar insertSubview:imageView atIndex:0];
So for your need you would give the -initWithFrame method appropriate values.
static CGFloat const CustomNavigationBarHeight = 74;
#implementation WTNavigationBar
- (CGSize)sizeThatFits:(CGSize)size{
size.width = 1024;
size.height = CustomNavigationBarHeight;
return size;
}
-(void)layoutSubviews {
[super layoutSubviews];
for (UIView *view in self.subviews) {
SFLog(#"view.class=%#",[view class]);
if ([view isKindOfClass:NSClassFromString(#"UINavigationItemButtonView")]) {
float centerY = self.bounds.size.height / 2.0f;
CGPoint center = view.center;
center.y = centerY;
view.center = center;
}
}
}
#end
in my iPad app,which has a fixed landscape orientation,I found I have to hardcode the size's width
I managed to do something similar by subclassing UINavigationBar and overriding -layoutSubviews. The code looks like:
-(void)layoutSubviews {
[super layoutSubviews];
int i = 0;
for (UIView *view in self.subviews) {
NSLog(#"%i. %#", i++, [view description]);
if ([view isKindOfClass:NSClassFromString(#"UINavigationButton")]) {
view.frame = CGRectMake(0, 0, 100, 50);
}
}
}
If you need to know how to subclass UINavigationBar, have a look at this very good answer.
I am not really sure about the NSClassFromString(#"UINavigationButton")] part. It works, but I did this as an experiment, and I'm not sure if this will get approved by Apple. I hope someone with a better knowledge might shed some light.
For the UINavigationbar
In iOS SDK 4.3 and beyond, there is a way (hack) to change the height of the UINavigationBar.
To change the height of UINavigationController, change its frame size in viewWillAppear:animated: function. Then, the height will stay customized throughout whole app.
For the UIBarButtonItems
I've actually run into this myself and the only thing I could come up with was leveraging initWithCustomView and passing in a UIButton with a defined frame.
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
/*
* Insert button styling
*/
button.frame = CGRectMake(0, 0, width, height);
UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button];
Otherwise UIBarButtonItem only has a width property that can be set but unfortunately not a height property. Another nifty thing I've done with initWithCustomView is to pass in a toolbar with a button and other things like activity indicators. Hope this helps.
How badly do you want this? And, how thin (or thick) do you want to make your navbar?
One approach would be to set the transform of the navbar to scale and translate it. If you scale it too much the title and button text will look wonky, but if you only need to shave a few pixels you might be allright.
Here's the result of scaling it to be 75% of full height (33 pixels tall):
And the code that produced this:
- (void)viewDidLoad
{
[super viewDidLoad];
self.navigationItem.title = #"Thin Navigation Bar";
self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle: #"Press Me" style:UIBarButtonItemStyleBordered target: nil action: NULL ] autorelease];
CGFloat scale = .75;
CGFloat cy = 44.0 - ( 44.0 * scale );
self.navigationController.navigationBar.transform = CGAffineTransformScale( CGAffineTransformMakeTranslation( 0, -cy / 2.0 ), 1.0, scale ) ;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
CGFloat scale = .75;
CGFloat cy = 44.0 - ( 44.0 * scale );
CGRect r = self.view.frame;
r.origin.y -= cy;
r.size.height += cy;
self.view.frame = r;
}
Now, this does have a number of problems, which may or may not be solvable. #1 is that you're fighting with the UINavigationController to size and position the navbar and the view-controller views. Animating between view controllers that use this technique is likely going to look weird.
I'd be curious if you could solve the related issues...
One last thought: If you dont use a UINavigationController then there really aren't a whole lot of issues with this other than squished text. Or, you could use a navigation controller but hide the default navbar, and add the thin navbar to each of your child-view controller views. You could even subclass UINavigationBar and set the transform from within:
#interface TSThinNavBar : UINavigationBar
{
}
#end
#implementation TSThinNavBar
// assuming we'll always be constructed from a nib
- (id) initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder: aDecoder];
if ( self != nil )
{
CGFloat scale = .75;
CGFloat cy = 44.0 - ( 44.0 * scale );
self.transform = CGAffineTransformScale( CGAffineTransformMakeTranslation( 0, -cy / 2.0 ), 1.0, scale ) ;
}
return self;
}
#end

NSTextFieldCell vertical alignment, solutions seem to squash the horizontal alignment

I have a NSTextFieldCell that I wish to display with middle vertical alignment. Thanks to an older question here and a blog entry I have two working solutions.
However, both solutions seem to squash my ability to set the cell as right aligned. Can anyone help me make either of these solutions support both forms of alignment?
Here is the code for one solution:
#implementation MiddleAlignedTextFieldCell
- (NSRect)titleRectForBounds:(NSRect)theRect {
NSRect titleFrame = [super titleRectForBounds:theRect];
NSSize titleSize = [[self attributedStringValue] size];
titleFrame.origin.y = theRect.origin.y - .5 + (theRect.size.height - titleSize.height) / 2.0;
return titleFrame;
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSRect titleRect = [self titleRectForBounds:cellFrame];
[[self attributedStringValue] drawInRect:titleRect];
}
#end
The alternative solution is (obtained from this blog):
#implementation RSVerticallyCenteredTextFieldCell
- (NSRect)drawingRectForBounds:(NSRect)theRect
{
NSRect newRect = [super drawingRectForBounds:theRect];
if (mIsEditingOrSelecting == NO)
{
// Get our ideal size for current text
NSSize textSize = [self cellSizeForBounds:theRect];
// Center that in the proposed rect
float heightDelta = newRect.size.height - textSize.height;
if (heightDelta > 0)
{
newRect.size.height -= heightDelta;
newRect.origin.y += (heightDelta / 2);
}
}
return newRect;
}
- (void)selectWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject start:(int)selStart length:(int)selLength
{
aRect = [self drawingRectForBounds:aRect];
mIsEditingOrSelecting = YES;
[super selectWithFrame:aRect inView:controlView editor:textObj delegate:anObject start:selStart length:selLength];
mIsEditingOrSelecting = NO;
}
- (void)editWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject event:(NSEvent *)theEvent
{
aRect = [self drawingRectForBounds:aRect];
mIsEditingOrSelecting = YES;
[super editWithFrame:aRect inView:controlView editor:textObj delegate:anObject event:theEvent];
mIsEditingOrSelecting = NO;
}
#end
I'm posting this answer to the question since it does work, however, I find the fact that I couldn't find another way to check the alignment setting from IB is very annoying. Accessing _cFlags just seems a little dirty, and I'd love to find a cleaner method.
Based on the code posted earlier from this blog entry.
- (NSRect)drawingRectForBounds:(NSRect)theRect
{
// Get the parent's idea of where we should draw
NSRect newRect = [super drawingRectForBounds:theRect];
if (mIsEditingOrSelecting == NO)
{
// Get our ideal size for current text
NSSize textSize = [self cellSizeForBounds:theRect];
// Center that in the proposed rect
float heightDelta = newRect.size.height - textSize.height;
if (heightDelta > 0)
{
newRect.size.height -= heightDelta;
newRect.origin.y += (heightDelta / 2);
}
// For some reason right aligned text doesn't work. This section makes it work if set in IB.
// HACK: using _cFlags isn't a great idea, but I couldn't find another way to find the alignment.
// TODO: replace _cFlags usage if a better solution is found.
float widthDelta = newRect.size.width - textSize.width;
if (_cFlags.alignment == NSRightTextAlignment && widthDelta > 0) {
newRect.size.width -= widthDelta;
newRect.origin.x += widthDelta;
}
}
return newRect;
}
You can use NSParagraphStyle/NSMutableParagraphStyle to set the alignment (and other attributes). Add an appropriately-configured NSParagraphStyle object to the full range of your attributed string.
There are a couple of potential solutions posted in a similar question which I asked a while back.
In all honesty, I still use the undocumented _cFlags.vCentered boolean (tsk tsk, bad programmer!) to get the job done. It's simple, and it works. I'll reinvent the wheel later on if I have to.
update:
OK, I think I've figured it out. Both solutions rely on a call to super to get the default rect, and then modify origin.y and size.height to perform the vertical centering. The calls to super, however, return a rectangle whose width has already been adjusted to fit the text horizontally.
The solution is to use origin.x and size.width from the bounds rect that is passed in to the method:
In solution #1:
- (NSRect)titleRectForBounds:(NSRect)theRect {
NSRect titleFrame = [super titleRectForBounds:theRect];
NSSize titleSize = [[self attributedStringValue] size];
// modified:
theRect.origin.y += (theRect.size.height - titleSize.height)/2.0 - 0.5;
return theRect;
}
In solution #2:
- (NSRect)drawingRectForBounds:(NSRect)theRect
{
NSRect newRect = [super drawingRectForBounds:theRect];
// modified:
newRect.origin.x = theRect.origin.x;
newRect.size.width = theRect.size.width;
if (mIsEditingOrSelecting == NO)
{
// Get our ideal size for current text
NSSize textSize = [self cellSizeForBounds:theRect];
// Center that in the proposed rect
float heightDelta = newRect.size.height - textSize.height;
if (heightDelta > 0)
{
newRect.size.height -= heightDelta;
newRect.origin.y += (heightDelta / 2);
}
}
return newRect;
}