How can I add a gradient that spans two views? - ios

I know how to do (1) but how can I do (2)?
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 50)];
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = view.bounds;
gradient.colors = #[(id)[UIColor blueColor].CGColor, (id)[UIColor redColor].CGColor];
[view.layer insertSublayer:gradient atIndex:0];

There are several ways you could do this. Here's one way:
Create a UIView subclass named GradientView to manage the gradient layer. This is helpful because it means you can use the normal UIKit techniques to manage the gradient layout (auto layout constraints, autoresizing masks, UIKit animations).
For each view that should participate in the common gradient, add a GradientView subview. Set up each GradientView's colors, locations, and start and end points identically.
For each view that should participate in the common gradient, turn on clipsToBounds.
Use auto layout constraints to make each GradientView span all of the participating superviews. (It's important to understand that constraints can cross superview/subview boundaries).
With this approach, auto layout takes care of making the gradient cover all of the views even if they change size or move around. For example, you won't have to do anything special to make the gradients animate nicely when the user rotates the device.
Thus, for your two-view example, I'm proposing that you set up a view hierarchy like this:
In the view debugger screenshot above, I disabled clipping. You can see that the two gradient views have identical gradients and share the same screen space. The topGradient is a subview of topView and bottomGradient is a subview of bottomView.
If we turn clipping on, you'll only see the part of topGradient that fits inside topView's bounds, and you'll only see the part of bottomGradient that fits inside bottomView's bounds. Here's what it looks like with clipping enabled:
And here's a screen shot of my test program in the simulator:
Here's the source code for GradientView:
#interface GradientView: UIView
#property (nonatomic, strong, readonly) CAGradientLayer *gradientLayer;
#end
#implementation GradientView
+ (Class)layerClass { return CAGradientLayer.class; }
- (CAGradientLayer *)gradientLayer { return (CAGradientLayer *)self.layer; }
#end
Here's the code I used to create all of the views:
- (void)viewDidLoad {
[super viewDidLoad];
UIView *topView = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 100, 50)];
topView.layer.cornerRadius = 10;
topView.clipsToBounds = YES;
UIView *topGradient = [self newGradientView];
[topView addSubview:topGradient];
[self.view addSubview:topView];
UIView *bottomView = [[UIView alloc] initWithFrame:CGRectMake(20, 90, 100, 50)];
bottomView.layer.cornerRadius = 10;
bottomView.clipsToBounds = YES;
UIView *bottomGradient = [self newGradientView];
[bottomView addSubview:bottomGradient];
[self.view addSubview:bottomView];
[self constrainView:topGradient toCoverViews:#[topView, bottomView]];
[self constrainView:bottomGradient toCoverViews:#[topView, bottomView]];
}
- (GradientView *)newGradientView {
GradientView *gv = [[GradientView alloc] initWithFrame:CGRectZero];
gv.translatesAutoresizingMaskIntoConstraints = NO;
gv.gradientLayer.colors = #[(__bridge id)UIColor.blueColor.CGColor, (__bridge id)UIColor.redColor.CGColor];
return gv;
}
And here's how I create the constraints that make a GradientView (or any view) cover a set of views:
- (void)constrainView:(UIView *)coverer toCoverViews:(NSArray<UIView *> *)coverees {
for (UIView *coveree in coverees) {
NSArray<NSLayoutConstraint *> *cs;
cs = #[
[coverer.leftAnchor constraintLessThanOrEqualToAnchor:coveree.leftAnchor],
[coverer.rightAnchor constraintGreaterThanOrEqualToAnchor:coveree.rightAnchor],
[coverer.topAnchor constraintLessThanOrEqualToAnchor:coveree.topAnchor],
[coverer.bottomAnchor constraintGreaterThanOrEqualToAnchor:coveree.bottomAnchor]];
[NSLayoutConstraint activateConstraints:cs];
cs = #[
[coverer.leftAnchor constraintEqualToAnchor:coveree.leftAnchor],
[coverer.rightAnchor constraintEqualToAnchor:coveree.rightAnchor],
[coverer.topAnchor constraintEqualToAnchor:coveree.topAnchor],
[coverer.bottomAnchor constraintEqualToAnchor:coveree.bottomAnchor]];
for (NSLayoutConstraint *c in cs) { c.priority = UILayoutPriorityDefaultHigh; }
[NSLayoutConstraint activateConstraints:cs];
}
}
The greaterThanOrEqual/lessThanOrEqual constraints, which (by default) have required priority, ensure that coverer covers the entire frame of each coveree. The equal constraints, which have lower priority, then ensure that coverer occupies the minimum space required to cover each coveree.

You can do this by adding a view on top of the view with the gradient, then cutting out the shapes by making a mask out of a UIBezierPath, then adding that to the view on top (let's call it topView):
let yourPath: UIBezierPath = //create the desired bezier path for your shapes
let mask = CAShapeLayer()
mask.path = yourPath.cgPath
topView.layer.mask = mask

Related

multiple punch-out style mask?

I've done simple CALayer masks before but I think I'm getting confused on what they do. I'm trying to have a punch out effect with several (2) views.
Here's what I have so far. I'm looking to have a white square with punched out label and image (so you can see the brown background through it. Where am I going wrong?
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor brownColor];
self.viewToPunch = [[UIView alloc] initWithFrame:CGRectZero];
[self.view addSubview:self.viewToPunch];
self.viewToPunch.backgroundColor = [UIColor whiteColor];
self.punchLabel = [[UILabel alloc] initWithFrame:CGRectZero];
self.punchLabel.textColor = [UIColor blackColor];
self.punchLabel.font = [UIFont boldSystemFontOfSize:20.0];
self.punchLabel.text = #"punch";
self.punchLabel.textAlignment = NSTextAlignmentCenter;
self.punchImage = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:#"plus"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
[self.punchImage setContentMode:UIViewContentModeCenter];
self.viewsToPunch = #[self.punchLabel,self.punchImage];
[self punch:self.viewToPunch withUIViews:self.viewsToPunch];
}
- (void)punch:(UIView *) viewToPunch withUIViews:(NSArray *)viewsToPunch
{
CALayer *punchMask = [CALayer layer];
punchMask.frame = viewToPunch.frame;
NSMutableArray *sublayers = [[NSMutableArray alloc] init];
for (UIView *views in viewsToPunch){
[sublayers addObject:views.layer];
}
punchMask.sublayers = sublayers;
punchMask.masksToBounds = YES;
viewToPunch.layer.mask = punchMask;
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
self.viewToPunch.frame = CGRectMake(50, 50, 100, 100);
self.punchLabel.frame = CGRectMake(0, 0, 100, 100);
self.punchImage.frame = CGRectMake(0, 0, self.viewToPunch.frame.size.width, 40.);
[self punch:self.viewToPunch withUIViews:self.viewsToPunch];
}
So not only do the frames seem to be off, it seems to be the opposite of a punch out. How do I invert the mask and fix up the frames?
Thanks a lot for any help! I put it in a method punch:withUIViews: so I can hopefully reuse it in other areas.
When you apply a mask to a CALayer, it only gets drawn in the parts where the mask is not transparent. But you're simply applying an empty (transparent) mask, with the wrong coordinates (which is why your view isn't completely transparent: the mask isn't covering the view completely; it should be punchMask.frame = viewToPunch.bounds;)
You might want to look into CAShapeLayer and assign it a path. Use that as mask layer.
For example, see CAShapeLayer mask view or Getting Creative with CALayer Masks (cached blog post).
I tried to combine mask of CAGradationLayer and CAShapeLayer, and It is possible.
https://stackoverflow.com/a/32220792/3276863

How to round off one corner of a resizable UIView in IOS?

I'm using this code to round off one corner of my UIView:
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
self.view.bounds byRoundingCorners:(UIRectCornerTopLeft) cornerRadii:
CGSizeMake(10.0, 10.0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.view.bounds;
maskLayer.path = maskPath.CGPath;
self.view.layer.mask = maskLayer;
self.view.layer.masksToBounds = NO;
This code works, as long as I don't ever resize the view. If I make the view larger, the new area does not appear because it's outside the bounds of the mask layer (this mask layer does not automatically resize itself with the view). I could just make the mask as large as it will ever need to be, but it could be full-screen on the iPad so I'm worried about performance with a mask that big (I'll have more than one of these in my UI). Also, a super-sized mask wouldn't work for the situation where I need the upper right corner (alone) to be rounded off.
Is there a simpler, easier way to achieve this?
Update: here is what I'm trying to achieve: http://i.imgur.com/W2AfRBd.png (the rounded corner I want is circled here in green).
I have achieved a working version of this, using a subclass of UINavigationController and overriding viewDidLayoutSubviews like so:
- (void)viewDidLayoutSubviews {
CGRect rect = self.view.bounds;
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:rect
byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(8.0, 8.0)];
self.maskLayer = [CAShapeLayer layer];
self.maskLayer.frame = rect;
self.maskLayer.path = maskPath.CGPath;
self.view.layer.mask = self.maskLayer;
}
I then instantiate my UINavigationController subclass with my view controller, and then I offset the frame of the nav controller's view by 20px (y) to expose the status bar and leave a 44-px high navigation bar, as shown in the picture.
The code is working, except that it doesn't handle rotation very well at all. When the app rotates, viewDidLayoutSubviews gets called before the rotation and my code creates a mask that fits the view after rotation; this creates an undesirable blockiness to the rotation, where bits that should be hidden are exposed during the rotation. Also, whereas the app's rotation is perfectly smooth without this mask, with the mask being created the rotation becomes noticeably jerky and slow.
The iPad app Evomail also has rounded corners like this, and their app suffers from the same problem.
The problem is, CoreAnimation properties do not animate in UIKit animation blocks. You need to create a separate animation which will have the same curve and duration as the UIKit animation.
I created the mask layer in viewDidLoad. When the view is about to be layout, I only modify the path property of the mask layer.
You do not know the rotation duration inside the layout callback methods, but you do know it right before rotation (and before layout is triggered), so you can keep it there.
The following code works well.
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
//Keep duration for next layout.
_duration = duration;
}
-(void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
UIBezierPath* maskPath = [UIBezierPath bezierPathWithRoundedRect:self.view.bounds byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(10, 10)];
CABasicAnimation* animation;
if(_duration > 0)
{
animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[animation setDuration:_duration];
//Set old value
[animation setFromValue:(id)((CAShapeLayer*)self.view.layer.mask).path];
//Set new value
[animation setToValue:(id)maskPath.CGPath];
}
((CAShapeLayer*)self.view.layer.mask).path = maskPath.CGPath;
if(_duration > 0)
{
[self.view.layer.mask addAnimation:animation forKey:#"path"];
}
//Zero duration for next layout.
_duration = 0;
}
I know this is a pretty hacky way of doing it but couldn't you just add a png over the top of the corner?
Ugly I know, but it won't affect performance, rotation will be fine if its a subview and users won't notice.
Two ideas:
Resize the mask when the view is resized. You don't get automatic resizing of sublayers the way you get automatic resizing of subviews, but you still get an event, so you can do manual resizing of sublayers.
Or... If this a view whose drawing and display you are in charge of, make the rounding of the corner a part of how you draw the view in the first place (by clipping). That is in fact the most efficient approach.
You could subclass the view you are using and override "layoutSubviews"method. This one gets called everytime your view dimensions change.
Even if "self.view"(referenced in your code) is your viewcontroller's view, you can still set this view to a custom class in your storyboard. Here's the modified code for the subclass:
- (void)layoutSubviews {
[super layoutSubviews];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
self.bounds byRoundingCorners:(UIRectCornerTopLeft) cornerRadii:
CGSizeMake(10.0, 10.0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
self.layer.masksToBounds = NO;
}
I think you should create a custom view that updates itself any time it is needed, which means anytime that setNeedsDisplay is called.
What I'm suggesting is to create a custom UIView subclass to be implemented as follows:
// OneRoundedCornerUIView.h
#import <UIKit/UIKit.h>
#interface OneRoundedCornerUIView : UIView //Subclass of UIView
#end
// OneRoundedCornerUIView.m
#import "OneRoundedCornerUIView.h"
#implementation OneRoundedCornerUIView
- (void) setFrame:(CGRect)frame
{
[super setFrame:frame];
[self setNeedsDisplay];
}
// Override drawRect as follows.
- (void)drawRect:(CGRect)rect
{
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
self.bounds byRoundingCorners:(UIRectCornerTopLeft) cornerRadii:
CGSizeMake(10.0, 10.0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
self.layer.masksToBounds = NO;
}
#end
Once you've done this you simply need to make your view an OneRoundedCornerUIView instance instead of an UIView one and your view will be updated smoothly every time you resize or change its frame. I've just done some testing and it seems to work perfectly.
This solution can also be easily customised in order to have a view for which you can easily set which corners should be on and which corners should not from your View Controller. Implementation as follows:
// OneRoundedCornerUIView.h
#import <UIKit/UIKit.h>
#interface OneRoundedCornerUIView : UIView //Subclass of UIView
// This properties are declared in the public API so that you can setup from your ViewController (it also works if you decide to add/remove corners at any time as the setter of each of these properties will call setNeedsDisplay - as shown in the implementation file)
#property (nonatomic, getter = isTopLeftCornerOn) BOOL topLeftCornerOn;
#property (nonatomic, getter = isTopRightCornerOn) BOOL topRightCornerOn;
#property (nonatomic, getter = isBottomLeftCornerOn) BOOL bottomLeftCornerOn;
#property (nonatomic, getter = isBottomRightCornerOn) BOOL bottomRightCornerOn;
#end
// OneRoundedCornerUIView.m
#import "OneRoundedCornerUIView.h"
#implementation OneRoundedCornerUIView
- (void) setFrame:(CGRect)frame
{
[super setFrame:frame];
[self setNeedsDisplay];
}
- (void) setTopLeftCornerOn:(BOOL)topLeftCornerOn
{
_topLeftCornerOn = topLeftCornerOn;
[self setNeedsDisplay];
}
- (void) setTopRightCornerOn:(BOOL)topRightCornerOn
{
_topRightCornerOn = topRightCornerOn;
[self setNeedsDisplay];
}
-(void) setBottomLeftCornerOn:(BOOL)bottomLeftCornerOn
{
_bottomLeftCornerOn = bottomLeftCornerOn;
[self setNeedsDisplay];
}
-(void) setBottomRightCornerOn:(BOOL)bottomRightCornerOn
{
_bottomRightCornerOn = bottomRightCornerOn;
[self setNeedsDisplay];
}
// Override drawRect as follows.
- (void)drawRect:(CGRect)rect
{
UIRectCorner topLeftCorner = 0;
UIRectCorner topRightCorner = 0;
UIRectCorner bottomLeftCorner = 0;
UIRectCorner bottomRightCorner = 0;
if (self.isTopLeftCornerOn) topLeftCorner = UIRectCornerTopLeft;
if (self.isTopRightCornerOn) topRightCorner = UIRectCornerTopRight;
if (self.isBottomLeftCornerOn) bottomLeftCorner = UIRectCornerBottomLeft;
if (self.isBottomRightCornerOn) bottomRightCorner = UIRectCornerBottomRight;
UIRectCorner corners = topLeftCorner | topRightCorner | bottomLeftCorner | bottomRightCorner;
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
self.bounds byRoundingCorners:(corners) cornerRadii:
CGSizeMake(10.0, 10.0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
self.layer.masksToBounds = NO;
}
#end
I'm a fan of doing what #Martin suggests. As long as there isn't animated content behind the rounded-corner then you can pull this off - even with a bitmap image displayed behind the frontmost view needing the rounded corner.
I created a sample project to mimic your screenshot. The magic happens in a UIView subclass called TSRoundedCornerView. You can place this view anywhere you want - above the view you want to show a rounded corner on, set a property to say what corner to round (adjust the radius by adjusting the size of the view), and setting a property that is the "background view" that you want to be visible in the corner.
Here's the repo for the sample: https://github.com/TomSwift/testRoundedCorner
And here's the drawing magic for the TSRoundedCornerView. Basically we create an inverted clip path with our rounded corner, then draw the background.
- (void) drawRect:(CGRect)rect
{
CGContextRef gc = UIGraphicsGetCurrentContext();
CGContextSaveGState(gc);
{
// create an inverted clip path
// (thanks rob mayoff: http://stackoverflow.com/questions/9042725/drawrect-how-do-i-do-an-inverted-clip)
UIBezierPath* bp = [UIBezierPath bezierPathWithRoundedRect: self.bounds
byRoundingCorners: self.corner // e.g. UIRectCornerTopLeft
cornerRadii: self.bounds.size];
CGContextAddPath(gc, bp.CGPath);
CGContextAddRect(gc, CGRectInfinite);
CGContextEOClip(gc);
// self.backgroundView is the view we want to show peering out behind the rounded corner
// this works well enough if there's only one layer to render and not a view hierarchy!
[self.backgroundView.layer renderInContext: gc];
//$ the iOS7 way of rendering the contents of a view. It works, but only if the UIImageView has already painted... I think.
//$ if you try this, be sure to setNeedsDisplay on this view from your view controller's viewDidAppear: method.
// CGRect r = self.backgroundView.bounds;
// r.origin = [self.backgroundView convertPoint: CGPointZero toView: self];
// [self.backgroundView drawViewHierarchyInRect: r
// afterScreenUpdates: YES];
}
CGContextRestoreGState(gc);
}
I thought about this again and I think there is a simpler solution. I updated my sample to showcase both solutions.
The new solution is to simply create a container view that has 4 rounded corners (via CALayer cornerRadius). You can size that view so only the corner you're interested in is visible on screen. This solution doesn't work well if you need 3 corners rounded, or two opposite (on the diagonal) corners rounded. I think it works in most other cases, including the one you've described in your question and screenshot.
Here's the repo for the sample: https://github.com/TomSwift/testRoundedCorner
Try this. Hope this will helps you.
UIView* parent = [[UIView alloc] initWithFrame:CGRectMake(10,10,100,100)];
parent.clipsToBounds = YES;
UIView* child = [[UIView alloc] new];
child.clipsToBounds = YES;
child.layer.cornerRadius = 3.0f;
child.backgroundColor = [UIColor redColor];
child.frame = CGRectOffset(parent.bounds, +4, -4);
[parent addSubView:child];
If you want to do it in Swift I could advice you to use an extension of an UIView. By doing so all subclasses will be able to use the following method:
import QuartzCore
extension UIView {
func roundCorner(corners: UIRectCorner, radius: CGFloat) {
let maskPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSizeMake(radius, radius))
var maskLayer = CAShapeLayer()
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
}
}
self.anImageView.roundCorner(UIRectCorner.TopRight, radius: 10)

UIScrollView zooming out of a view with a -ve origin

I have a UIScrollView. In this I have a UIView which has a frame with a negative origin - I need to limit the scroll view so that you can't scroll around the entire view..
I have implemented Zoom in this scrollview.
When Zooming the Scroll view will adjust the size of the Zoomable view according to the scale. BUT IT DOES NOT ADJUST THE ORIGIN.
So if I have a view with a frame of {0, -500}, {1000, 1000}
The I zoom out to a scale of 0.5, this will give me a new frame of {0, -500}, {500, 500}
Clearly this is not good, the entire view is zoomed out of the scrollview. I want the frame to be {0, -250}, {500, 500}
I can fix things a bit in the scrollViewDidZoom method by adjusting the origin correctly.. This does work, but the zoom is not smooth.. Changing the origin here causes it to jump.
I notice in the documentation for UIView it says (regarding the frame property):
Warning: If the transform property is not the identity transform, the
value of this property is undefined and therefore should be ignored.
Not quite sure why that is.
Am I approaching this problem wrong? What is the best way to fix it?
Thanks
Below is some source code from the test app I am using:
In the ViewController..
- (void)viewDidLoad
{
[super viewDidLoad];
self.bigView = [[BigView alloc] initWithFrame: CGRectMake(0, -400, 1000, 1000)];
[self.bigScroll addSubview: bigView];
self.bigScroll.delegate = self;
self.bigScroll.minimumZoomScale = 0.2;
self.bigScroll.maximumZoomScale = 5;
self.bigScroll.contentSize = bigView.bounds.size;
}
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return bigView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
// bigView.frame = CGRectMake(0, -400 * scrollView.zoomScale,
// bigView.frame.size.width, bigView.frame.size.height);
bigView.center = CGPointMake(500 * scrollView.zoomScale, 100 * scrollView.zoomScale);
}
And then in the View...
- (void)drawRect:(CGRect)rect
{
// Drawing code
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(ctx, [UIColor whiteColor].CGColor);
CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
CGContextFillRect(ctx, CGRectMake(100, 500, 10, 10));
for (int i = 0; i < 1000; i += 100) {
CGContextStrokeRect(ctx, CGRectMake(0, i, 1000, 3));
}
}
Note that here the jumpiness is more apparent at larger zoom scales. In my real app where there is much more drawing and processing going on the jump is more apparent at all times.
You don't have to use the frame property - and should not, given Apple's very firm warning. In such cases you can usually use bounds and center to achieve your result.
In your case you can ignore all of the subview's properties. Assuming that your subview is the viewForZoomingInScrollView you can use the scrollView's contentOffset and zoomScale properties
- (void) setMinOffsets:(UIScrollView*)scrollView
{
CGFloat minOffsetX = MIN_OFFSET_X*scrollView.zoomScale;
CGFloat minOffsetY = MIN_OFFSET_Y*scrollView.zoomScale;
if ( scrollView.contentOffset.x < minOffsetX
|| scrollView.contentOffset.y < minOffsetY ) {
CGFloat offsetX = (scrollView.contentOffset.x > minOffsetX)?
scrollView.contentOffset.x : minOffsetX;
CGFloat offsetY = (scrollView.contentOffset.y > minOffsetY)?
scrollView.contentOffset.y : minOffsetY;
scrollView.contentOffset = CGPointMake(offsetX, offsetY);
}
}
Call it from both scrollViewDidScroll and scrollViewDidZoom in your scrollView delegate. This should work smoothly, but if you have doubts you can also implement it by subclassing the scrollView and invoking it with layoutSubviews. In their PhotoScroller example, Apple centers a scrollView's content by overriding layoutSubviews - although maddeningly they ignore their own warnings and adjust the subview's frame property to achieve this.
update
The above method eliminates the 'bounce' as the scrollView hits it's limits. If you want to retain the bounce, you can directly alter the view's center property instead:
- (void) setViewCenter:(UIScrollView*)scrollView
{
UIView* view = [scrollView subviews][0];
CGFloat centerX = view.bounds.size.width/2-MIN_OFFSET_X;
CGFloat centerY = view.bounds.size.height/2-MIN_OFFSET_Y;
centerX *=scrollView.zoomScale;
centerY *=scrollView.zoomScale;
view.center = CGPointMake(centerX, centerY);
}
update 2
From your updated question (with code), I can see that neither of these solutions fix you problem. What seems to be happening is that the greater you make your offset, the jerkier the zoom movement becomes. With an offset of 100points the action is still quite smooth, but with an offset of 500points, it is unacceptably rough. This is partly related to your drawRect routine, and partly related to (too much) recalculation going on in the scrollView to display the right content. So I have another solution…
In your viewController, set your customView's bounds/frame origin to the normal (0,0). We will offset the content using layers instead. You will need to add the QuartzCore framework to your project, and #import it into your custom view.
In the custom view initialise two CAShapeLayers - one for the box, the other for the lines. If they share the same fill and stroke you would only need one CAShapeLayer (for this example I changed your fill and stroke colors). Each CAShapeLayer comes with it's own CGContext, which you can initialise once per layer with colors, linewidths etc. Then to make a CAShapelayer do it's drawing all you have to do is set it's path property with a CGPath.
#import "CustomView.h"
#import <QuartzCore/QuartzCore.h>
#interface CustomView()
#property (nonatomic, strong) CAShapeLayer* shapeLayer1;
#property (nonatomic, strong) CAShapeLayer* shapeLayer2;
#end
#implementation CustomView
#define MIN_OFFSET_X 100
#define MIN_OFFSET_Y 500
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self initialiseLayers];
}
return self;
}
- (void) initialiseLayers
{
CGRect layerBounds = CGRectMake( MIN_OFFSET_X,MIN_OFFSET_Y
, self.bounds.size.width + MIN_OFFSET_X
, self.bounds.size.height+ MIN_OFFSET_Y);
self.shapeLayer1 = [[CAShapeLayer alloc] init];
[self.shapeLayer1 setFillColor:[UIColor clearColor].CGColor];
[self.shapeLayer1 setStrokeColor:[UIColor yellowColor].CGColor];
[self.shapeLayer1 setLineWidth:1.0f];
[self.shapeLayer1 setOpacity:1.0f];
self.shapeLayer1.anchorPoint = CGPointMake(0, 0);
self.shapeLayer1.bounds = layerBounds;
[self.layer addSublayer:self.shapeLayer1];
Setting the bounds is the critical bit. Unlike views, which clip their subviews, CALayers will draw beyond the bounds of their superlayer. You are going to start drawing MIN_OFFSET_Y points above the top of your View and MIN_OFFSET_X to the left. This allows you to draw content beyond your scrollView's content view without the scrollView having to do any extra work.
Unlike views, a superlayer does not automatically clip the contents of sublayers that lie outside its bounds rectangle. Instead, the superlayer allows its sublayers to be displayed in their entirety by default.
(Apple Docs, Building a Layer Hierarchy)
self.shapeLayer2 = [[CAShapeLayer alloc] init];
[self.shapeLayer2 setFillColor:[UIColor blueColor].CGColor];
[self.shapeLayer2 setStrokeColor:[UIColor clearColor].CGColor];
[self.shapeLayer2 setLineWidth:0.0f];
[self.shapeLayer2 setOpacity:1.0f];
self.shapeLayer2.anchorPoint = CGPointMake(0, 0);
self.shapeLayer2.bounds = layerBounds;
[self.layer addSublayer:self.shapeLayer2];
[self drawIntoLayer1];
[self drawIntoLayer2];
}
Set a bezier path for each shape layer, then pass it in:
- (void) drawIntoLayer1 {
UIBezierPath* path = [[UIBezierPath alloc] init];
[path moveToPoint:CGPointMake(0,0)];
for (int i = 0; i < self.bounds.size.height+MIN_OFFSET_Y; i += 100) {
[path moveToPoint:
CGPointMake(0,i)];
[path addLineToPoint:
CGPointMake(self.bounds.size.width+MIN_OFFSET_X, i)];
[path addLineToPoint:
CGPointMake(self.bounds.size.width+MIN_OFFSET_X, i+3)];
[path addLineToPoint:
CGPointMake(0, i+3)];
[path closePath];
}
[self.shapeLayer1 setPath:path.CGPath];
}
- (void) drawIntoLayer2 {
UIBezierPath* path = [UIBezierPath bezierPathWithRect:
CGRectMake(100+MIN_OFFSET_X, MIN_OFFSET_Y, 10, 10)];
[self.shapeLayer2 setPath:path.CGPath];
}
This obviates the need for drawRect - you only need to redraw your layers if you change the path property. Even if you do change the path property as often as you would call drawRect, the drawing should now be significantly more efficient. And as path is an animatable property, you also get animation thrown in for free if you need it.
In your case we only need to set the path once, so all of the work is done once, on initialisation.
Now you can remove any centering code from your scrollView delegate methods, it isn't needed any more.

Setting corner radius on a cell kills UICollectionView's performance

I have a UICollectionView that has only a few cells (about 20). Performance for this collection works great. However, as soon as I try to round the corners of the UICollectionViewCells that are being rendered by this view, my performance takes a significant hit. In my cell's init method, this is the only line I add to cause this:
[self.layer setCornerRadius:15];
Since this is in the init method and I am reusing the cells properly, I don't see why this should be causing me issue.
I have tried adjusting the rasterization and opacity of the sell using multiple combinations of the following, with still no effect:
[self.layer setMasksToBounds:YES];
[self.layer setCornerRadius:15];
[self.layer setRasterizationScale:[[UIScreen mainScreen] scale]];
self.layer.shouldRasterize = YES;
self.layer.opaque = YES;
Is their some setting or trick to improve the performance of a UICollectionView that has cells with rounded corners?
As #Till noted in comments, a prerendered image should solve your performance problem. You can put all the corner rounding, shadowing, and whatever other special effects into that instead of needing CA to render them on the fly.
Prerendered images don't lock you into a static content size, either: look into the UIImage resizable image stuff. (That's still way faster than CA rendering every frame.)
I have found that this is caused entirely because of the call to dequeuereusablecellwithidentifier. Each time this is called, the cell with rounded corners needs to be re-rendered. If the collection view did not remove them from the view when the item scrolled off the screen, then the performance would not be affected (as long as their wasn't too many items in the collection that is). Seems like a double edged sword - both ways have their limits.
There is a code for UIView subclass, which is provide you a view with opaque rounded borders and transparent hole in the middle.
You should create needed view as usual and after that you can add view with hole over your view. Visualisation is here.
It works if you use one-colored background for UICollectionView or UITableView, you can add following subview for each cell:
#interface TPRoundedFrameView : UIView
#property (assign, nonatomic) CGFloat cornerRadius;
#property (strong, nonatomic) UIColor * borderColor;
#end
#implementation TPRoundedFrameView
- (instancetype)init {
if ((self = [super init])) {
self.opaque = NO;
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
UIBezierPath * path = [UIBezierPath bezierPathWithRect:rect];
UIBezierPath * innerPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:self.cornerRadius];
[path appendPath:[innerPath bezierPathByReversingPath]];
[self.borderColor set];
[path fill];
}
#end
Example for target cell class:
- (void)awakeFromNib {
[super awakeFromNib];
// creating holed view with rounded corners
self.myRoundedView.backgroundColor = [UIColor whiteColor];
TPRoundedFrameView * roundedFrame = [TPRoundedFrameView new];
roundedFrame.cornerRadius = 5.f;
roundedFrame.borderColor = [UIColor groupTableViewBackgroundColor];
// add borders to your view with appropriate constraints
[self.myRoundedView addSubview:roundedFrame];
roundedFrame.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary * views = NSDictionaryOfVariableBindings(roundedFrame);
NSArray * horizontal = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[roundedFrame]-0-|" options:0 metrics:nil views:views];
NSArray * vertical = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[roundedFrame]-0-|" options:0 metrics:nil views:views];
[self.myRoundedView addConstraints:horizontal];
[self.myRoundedView addConstraints:vertical];
}
Result:
Table with rounded views as cells
I fixed all of my performance borderRadius issues by applying this radius on the contentView instead of the cell itself.
self.contentView.layer.borderWidth = 1.0f;
self.contentView.layer.cornerRadius = 5.0f;
self.contentView.layer.borderColor = [UIColor colorWithRed:202/255. green:202/255. blue:202/255. alpha:1.0].CGColor;

Finger Painting from a UIView Class to Another UIView Class

I have a UIViewController Class which holds 2 custom UIView classes which are:
ItemView
DrawingView
in UIViewController ViewDidLoad method I have the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
item = [[ItemView alloc] initWithFrame:CGRectMake(self.view.frame.size.width/2, self.view.frame.size.height/2, 230, 230)];
item.opaque = NO;
[self.view addSubview:item];
drawing = [[DrawingView alloc] initWithFrame:self.view.frame];
[self.view insertSubview:drawing aboveSubview:item];
[item release];
[drawing release];
}
The DrawingView class gets UITouches and accordingly draw on the screen.
My question is:
I can draw all over the screen except for on top of ItemView class object. DrawingView class cannot draw on to ItemView.
Let me explain it in another word:
The DrawView class works on the screen except for the area of itemView subview. On top of itemView, DrawView cannot make any draws but other than itemView area, it makes drawing.
What can I do?
What can be the problem?
How can I make drawing on all screen including the itemView area added by addSubview on ViewController class?
EDIT: I am adding a screenshot in order to explain the problem better
EDIT 2: I found that the problem is related to opacity. In the ItemView class, I changed the added UIImageView object alpha value to 0.5f.
The result is semi-transparent view and now my finger drawing is visible. However, this is not what I want. I want to draw on top of the view. I do not want to play with the alpha value.
Finally, I found the answer myself. It is not exactly what I wanted but my solution is based on CALayer.
Instead of using a custom UIView class like ItemView I added the following class method in ItemView Class:
+(CALayer *)imgLayer
{
UIImage *img = [UIImage imageNamed:#"face.png"];
CALayer *layer = [CALayer layer];
CGFloat nativeWidth = CGImageGetWidth(img.CGImage);
CGFloat nativeHeight = CGImageGetHeight(img.CGImage);
CGRect startFrame = CGRectMake(0, 0, nativeWidth, nativeHeight);
layer.contents = (id)img.CGImage;
layer.frame = startFrame;
return layer;
}
In My UIVIewController Class I called my static function as following:
CAGradientLayer *bgLayer = [GradientView greyGradient];
bgLayer.frame = self.view.bounds;
[self.view.layer insertSublayer:bgLayer atIndex:0];
CALayer *imgLayer =[ItemView imgLayer];
imgLayer.frame = self.view.bounds;
[self.view.layer insertSublayer:imgLayer above:bgLayer];
And it works! now I can make drawings on my image.

Resources