UIScrollView has an excellent contentInset property which tells the view, which portion is visible on the screen. I have an MKMapView which is partially covered by a translucent view. I want the map to be visible under the view. I have to display several annotations on the map, and I want to zoom to them using -setRegion:animated:, but the map view does not respect that it is partially covered, therefore some of my annotations will be covered by the translucent view.
Is there any way to tell the map, to calculate like the scroll view does using contentInset?
UPDATE: This is what I've tried:
- (MKMapRect)mapRectForAnnotations
{
if (self.trafik) {
MKMapPoint point = MKMapPointForCoordinate(self.trafik.coordinate);
MKMapPoint deltaPoint;
if (self.map.userLocation &&
self.map.userLocation.coordinate.longitude != 0) {
MKCoordinateSpan delta = MKCoordinateSpanMake(fabsf(self.trafik.coordinate.latitude-self.map.userLocation.coordinate.latitude),
fabsf(self.trafik.coordinate.longitude-self.map.userLocation.coordinate.longitude));
deltaPoint = MKMapPointForCoordinate(CLLocationCoordinate2DMake(delta.latitudeDelta, delta.longitudeDelta));
} else {
deltaPoint = MKMapPointForCoordinate(CLLocationCoordinate2DMake(0.01, 0.01));
}
return MKMapRectMake(point.x, point.y, deltaPoint.x, deltaPoint.y);
} else {
return MKMapRectNull;
}
}
Use UIViews's layoutMargins.
E.g. This will force the current user's position pin to move 50pts up.
mapView.layoutMargins = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 100.0, right: 0.0)
You can do the following but it could mess with other views in your UIViewController that use bottomLayoutGuide. You'll have to test it to find out.
Override bottomLayoutGuide in the UIViewController that has your map as a subview and return a MyLayoutGuide object that looks like:
#interface MyLayoutGuide : NSObject <UILayoutSupport>
#property (nonatomic) CGFloat length;
-(id)initWithLength:(CGFloat)length;
#end
#implementation MyLayoutGuide
#synthesize length = _length;
#synthesize topAnchor = _topAnchor;
#synthesize bottomAnchor = _bottomAnchor;
#synthesize heightAnchor = _heightAnchor;
- (id)initWithLength:(CGFloat)length
{
if (self = [super init]) {
_length = length;
}
return self;
}
#end
bottomLayoutGuide that insets the MKMapView by 50 points:
- (id)bottomLayoutGuide
{
CGFloat bottomLayoutGuideLength = 50.f;
return [[MyLayoutGuide alloc] initWithLength:bottomLayoutGuideLength];
}
You can force this "inset" to be calculated again by calling setNeedsLayout on your MKMapView in the event that your time table on the bottom changes size. We've created a helper in our MKMapView subclass that can be called from the parent UIViewController:
- (void)updateBottomLayoutGuides
{
// this method ends up calling -(id)bottomLayoutGuide on its UIViewController
// and thus updating where the Legal link on the map should be.
[self.mapView setNeedsLayout];
}
Answer adapted from this answer.
Related
I am trying to create a round button which is only clickable in its boundaries.
What I have done
// imported QuartzCore
#import <QuartzCore/QuartzCore.h>
//created an IBOutlet for the button
IBOutlet NSButton* btn;
//defined the button height width (same)
#define ROUND_BUTTON_WIDTH_HEIGHT 150
//set corner radius
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
btn.frame = CGRectMake(0, 0, ROUND_BUTTON_WIDTH_HEIGHT, ROUND_BUTTON_WIDTH_HEIGHT);
btn.layer.cornerRadius = ROUND_BUTTON_WIDTH_HEIGHT/2.0f;
}
//set view layer to YES
Problem
The button is clickable outside its boundaries.
When I am setting its position and when I am resizing the window it is getting back to its right position (the actual positon of the button is center of the window)
I have also tried to subclass NSButton and assign the class to the button but results are the same.
#import "roundBtn.h"
#import <QuartzCore/QuartzCore.h>
#implementation roundBtn
#define ROUND_BUTTON_WIDTH_HEIGHT 142
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// Drawing code here.
self.frame = CGRectMake(0, 0, ROUND_BUTTON_WIDTH_HEIGHT, ROUND_BUTTON_WIDTH_HEIGHT);
self.layer.cornerRadius = ROUND_BUTTON_WIDTH_HEIGHT/2.0f;
}
#end
You can either reimplement the button behavior using a custom NSView and then it will respect the mouseDown events of a masksToBounds layer. NSButton doesn't work this way so the only way to tell your subclassed button that it won't be hit outside of the circle is to override the -hitTest: method to detect hits within the circle:
- (NSView *)hitTest:(NSPoint)aPoint {
NSPoint p = [self convertPoint:aPoint fromView:self.superview];
CGFloat midX = NSMidX(self.bounds);
CGFloat midY = NSMidY(self.bounds);
if (sqrt(pow(p.x-midX, 2) + pow(p.y-midY, 2)) < self.bounds.size.width/2.0) {
return self;
}
return nil;
}
In overriding this, you are telling the button that it will only register a hit if it is within the circle (using a little trigonometry). Returning self indicates the button was hit, returning nil indicates it was not hit.
I have a custom NSView subclass which has a border around itself. The border is drawn inside this view. Is it possible to respect this borders with auto layout?
For example, when I place the subview to my custom view and set constraints like this:
#"H:|-(myViewSubView)-|" (not #"H:|-(myViewBorderWidth)-(myViewSubView)-(myViewBorderWidth)-|")
#"V:|-(myViewSubView)-|"
the layout must be:
Horizontal: |-(myViewBorderWidth)-|myViewSubview|-(myViewBorderWidth)-|
Vertical: |-(myViewBorderWidth)-|myViewSubview|-(myViewBorderWidth)-|
I've tried to overwrite -bounds method in my view to return the bounds rect without the borders, but it doesn't help.
UPDATE
I just noticed that your question is talking about NSView (OS X), not UIView (iOS). Well, this idea should still be applicable, but you won't be able to drop my code into your project unchanged. Sorry.
ORIGINAL
Consider changing your view hierarchy. Let's say your custom bordered view is called BorderView. Right now you're adding subviews directly to BorderView and creating constraints between the BorderView and its subviews.
Instead, give the BorderView a single subview, which it exposes in its contentView property. Add your subviews to the contentView instead of directly to the BorderView. Then the BorderView can lay out its contentView however it needs to. This is how UITableViewCell works.
Here's an example:
#interface BorderView : UIView
#property (nonatomic, strong) IBOutlet UIView *contentView;
#property (nonatomic) UIEdgeInsets borderSize;
#end
If we're using a xib, then we have the problem that IB doesn't know that it should add subviews to the contentView instead of directly to the BorderView. (It does know this for UITableViewCell.) To work around that, I've made contentView an outlet. That way, we can create a separate, top-level view to use as the content view, and connect it to the BorderView's contentView outlet.
To implement BorderView this way, we'll need an instance variable for each of the four constraints between the BorderView and its contentView:
#implementation BorderView {
NSLayoutConstraint *topConstraint;
NSLayoutConstraint *leftConstraint;
NSLayoutConstraint *bottomConstraint;
NSLayoutConstraint *rightConstraint;
UIView *_contentView;
}
The contentView accessor can create the content view on demand:
#pragma mark - Public API
- (UIView *)contentView {
if (!_contentView) {
[self createContentView];
}
return _contentView;
}
And the setter can replace an existing content view, if there is one:
- (void)setContentView:(UIView *)contentView {
if (_contentView) {
[self destroyContentView];
}
_contentView = contentView;
[self addSubview:contentView];
}
The borderSize setter needs to arrange for the constraints to be updated and for the border to be redrawn:
- (void)setBorderSize:(UIEdgeInsets)borderSize {
if (!UIEdgeInsetsEqualToEdgeInsets(borderSize, _borderSize)) {
_borderSize = borderSize;
[self setNeedsUpdateConstraints];
[self setNeedsDisplay];
}
}
We'll need to draw the border in drawRect:. I'll just fill it with red:
- (void)drawRect:(CGRect)rect {
CGRect bounds = self.bounds;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:bounds];
[path appendPath:[UIBezierPath bezierPathWithRect:UIEdgeInsetsInsetRect(bounds, self.borderSize)]];
path.usesEvenOddFillRule = YES;
[path addClip];
[[UIColor redColor] setFill];
UIRectFill(bounds);
}
Creating the content view is trivial:
- (void)createContentView {
_contentView = [[UIView alloc] init];
[self addSubview:_contentView];
}
Destroying it is slightly more involved:
- (void)destroyContentView {
[_contentView removeFromSuperview];
_contentView = nil;
[self removeConstraint:topConstraint];
topConstraint = nil;
[self removeConstraint:leftConstraint];
leftConstraint = nil;
[self removeConstraint:bottomConstraint];
bottomConstraint = nil;
[self removeConstraint:rightConstraint];
rightConstraint = nil;
}
The system will automatically call updateConstraints before doing layout and drawing if somebody has called setNeedsUpdateConstraints, which we did in setBorderSize:. In updateConstraints, we'll create the constraints if necessary, and update their constants based on borderSize. We also tell the system not to translate the autoresizing masks into constraints, because that tends to create unsatisfiable constraints.
- (void)updateConstraints {
self.translatesAutoresizingMaskIntoConstraints = NO;
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
[super updateConstraints];
if (!topConstraint) {
[self createContentViewConstraints];
}
topConstraint.constant = _borderSize.top;
leftConstraint.constant = _borderSize.left;
bottomConstraint.constant = -_borderSize.bottom;
rightConstraint.constant = -_borderSize.right;
}
All four constraints are created the same way, so we'll use a helper method:
- (void)createContentViewConstraints {
topConstraint = [self constrainContentViewAttribute:NSLayoutAttributeTop];
leftConstraint = [self constrainContentViewAttribute:NSLayoutAttributeLeft];
bottomConstraint = [self constrainContentViewAttribute:NSLayoutAttributeBottom];
rightConstraint = [self constrainContentViewAttribute:NSLayoutAttributeRight];
}
- (NSLayoutConstraint *)constrainContentViewAttribute:(NSLayoutAttribute)attribute {
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:_contentView attribute:attribute relatedBy:NSLayoutRelationEqual toItem:self attribute:attribute multiplier:1 constant:0];
[self addConstraint:constraint];
return constraint;
}
#end
I have put a complete working example in this git repository.
For future reference, you can override NSView.alignmentRectInsets to affect the position of the layout guides:
Custom views that draw ornamentation around their content can override
this property and return insets that align with the edges of the
content, excluding the ornamentation. This allows the constraint-based
layout system to align views based on their content, rather than just
their frame.
Link to documentation:
https://developer.apple.com/documentation/appkit/nsview/1526870-alignmentrectinsets
Have you tried setting the intrinsic size to include the border size?
- (NSSize)intrinsicContentSize
{
return NSMakeSize(width+bordersize, height+bordersize);
}
then you would set the content compression resistance priorities in both directions to be required:
[self setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintHorizontal];
[self setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintVertical];
The one solution I found is to overload the addConstraint: method and modify constraints before they'll be added:
- (void)addConstraint:(NSLayoutConstraint *)constraint
{
if(constraint.firstItem == self || constraint.secondItem == self) {
if(constraint.firstAttribute == NSLayoutAttributeLeading) {
constraint.constant += self.leftBorderWidth;
} else if (constraint.firstAttribute == NSLayoutAttributeTrailing) {
constraint.constant += self.rightBorderWidth;
} else if (constraint.firstAttribute == NSLayoutAttributeTop) {
constraint.constant += self.topBorderWidth;
} else if (constraint.firstAttribute == NSLayoutAttributeBottom) {
constraint.constant += self.bottomBorderWidth;
}
}
[super addConstraint:constraint];
}
And then also handle this constraints in xxxBorderWidth setters.
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.
I have a text field with a background but to make it look right the text field needs to have some padding on the left side of it a bit like the NSSearchField does. How would I give the text field some padding on the left?
smorgan's answer points us in the right direction, but it took me quite a while to figure out how to restore the customized textfield's ability to display a background color -- you must call setBorder:YES on the custom cell.
This is too late to help Joshua, but here's the how you implement the customized cell:
#import <Foundation/Foundation.h>
// subclass NSTextFieldCell
#interface InstructionsTextFieldCell : NSTextFieldCell {
}
#end
#import "InstructionsTextFieldCell.h"
#implementation InstructionsTextFieldCell
- (id)init
{
self = [super init];
if (self) {
// Initialization code here. (None needed.)
}
return self;
}
- (void)dealloc
{
[super dealloc];
}
- (NSRect)drawingRectForBounds:(NSRect)rect {
// This gives pretty generous margins, suitable for a large font size.
// If you're using the default font size, it would probably be better to cut the inset values in half.
// You could also propertize a CGFloat from which to derive the inset values, and set it per the font size used at any given time.
NSRect rectInset = NSMakeRect(rect.origin.x + 10.0f, rect.origin.y + 10.0f, rect.size.width - 20.0f, rect.size.height - 20.0f);
return [super drawingRectForBounds:rectInset];
}
// Required methods
- (id)initWithCoder:(NSCoder *)decoder {
return [super initWithCoder:decoder];
}
- (id)initImageCell:(NSImage *)image {
return [super initImageCell:image];
}
- (id)initTextCell:(NSString *)string {
return [super initTextCell:string];
}
#end
(If, like Joshua, you only want an inset at the left, leave the origin.y and height as is, and add the same amount to the width -- not double -- as you do to the origin.x.)
Assign the customized cell like this, in the awakeFromNib method of the window/view controller that owns the textfield:
// Assign the textfield a customized cell, inset so that text doesn't run all the way to the edge.
InstructionsTextFieldCell *newCell = [[InstructionsTextFieldCell alloc] init];
[newCell setBordered:YES]; // so background color shows up
[newCell setBezeled:YES];
[self.tfSyncInstructions setCell:newCell];
[newCell release];
Use a custom NSTextFieldCell that overrides drawingRectForBounds:. Have it inset the rectangle by however much you want, then pass than new rectangle to [super drawingRectForBounds:] to get the normal padding, and return the result of that call.
Using interface builder you can select the corners an object should stick to when resizing. How can you do this programatically?
I find that the autoresizingBit masks are horribly named, so I use a category on NSView to make things a little more explicit:
// MyNSViewCategory.h:
#interface NSView (myCustomMethods)
- (void)fixLeftEdge:(BOOL)fixed;
- (void)fixRightEdge:(BOOL)fixed;
- (void)fixTopEdge:(BOOL)fixed;
- (void)fixBottomEdge:(BOOL)fixed;
- (void)fixWidth:(BOOL)fixed;
- (void)fixHeight:(BOOL)fixed;
#end
// MyNSViewCategory.m:
#implementation NSView (myCustomMethods)
- (void)setAutoresizingBit:(unsigned int)bitMask toValue:(BOOL)set
{
if (set)
{ [self setAutoresizingMask:([self autoresizingMask] | bitMask)]; }
else
{ [self setAutoresizingMask:([self autoresizingMask] & ~bitMask)]; }
}
- (void)fixLeftEdge:(BOOL)fixed
{ [self setAutoresizingBit:NSViewMinXMargin toValue:!fixed]; }
- (void)fixRightEdge:(BOOL)fixed
{ [self setAutoresizingBit:NSViewMaxXMargin toValue:!fixed]; }
- (void)fixTopEdge:(BOOL)fixed
{ [self setAutoresizingBit:NSViewMinYMargin toValue:!fixed]; }
- (void)fixBottomEdge:(BOOL)fixed
{ [self setAutoresizingBit:NSViewMaxYMargin toValue:!fixed]; }
- (void)fixWidth:(BOOL)fixed
{ [self setAutoresizingBit:NSViewWidthSizable toValue:!fixed]; }
- (void)fixHeight:(BOOL)fixed
{ [self setAutoresizingBit:NSViewHeightSizable toValue:!fixed]; }
#end
Which can then be used as follows:
[someView fixLeftEdge:YES];
[someView fixTopEdge:YES];
[someView fixWidth:NO];
See the setAutoresizingMask: method of NSView and the associated resizing masks.
Each view has the mask of flags, controlled by setting a the autoresizingMask property with the OR of the behaviors you want from the resizing masks. In addition, the superview needs to be configured to resize its subviews.
Finally, in addition to the basic mask-defined resizing options, you can fully control the layout of subviews by implementing -resizeSubviewsWithOldSize:
#e.James answer gave me an idea to simply create a new enum with more familiar naming:
typedef NS_OPTIONS(NSUInteger, NSViewAutoresizing) {
NSViewAutoresizingNone = NSViewNotSizable,
NSViewAutoresizingFlexibleLeftMargin = NSViewMinXMargin,
NSViewAutoresizingFlexibleWidth = NSViewWidthSizable,
NSViewAutoresizingFlexibleRightMargin = NSViewMaxXMargin,
NSViewAutoresizingFlexibleTopMargin = NSViewMaxYMargin,
NSViewAutoresizingFlexibleHeight = NSViewHeightSizable,
NSViewAutoresizingFlexibleBottomMargin = NSViewMinYMargin
};
Also, from my research, I discovered that #James.s has a serious bug in the NSView additions. The coordinate system in Cocoa has a flipped y-axis in terms of iOS coordinates system. Hence, in order to fix the bottom and top margin, you should write:
- (void)fixTopEdge:(BOOL)fixed
{ [self setAutoresizingBit:NSViewMaxYMargin toValue:!fixed]; }
- (void)fixBottomEdge:(BOOL)fixed
{ [self setAutoresizingBit:NSViewMinYMargin toValue:!fixed]; }
From the cocoa docs:
NSViewMinYMargin
The bottom margin between the receiver and its superview is flexible.
Available in OS X v10.0 and later.