iOS UICollectionViewCell resizable dashed border - uicollectionview

I have a UICollectionView where some of the cells should have a dashed border and some of them should have a solid border. Also, the cells can be of varying size depending on the content that is present in the data model.
The problem I am having is that I cannot get the dashed border to be the same size as the collection view cell and again, the cell size can change based on the content. But basically, the cell should either have a dashed border or a solid border. The solid border is easy to get to resize to the correct size.
Here is a picture of what it looks like right now. The dashed border is colored green just to make it easier to see.
Here is the view hierarchy debug view. There are two dashed borders here because I have been experimenting. The green border is a sublayer on the UICollectionViewCell's root layer. The grey border is a sublayer of a separate view that is a subview of the collection view cell's contentView property.
Code
Approach 1 - add a dedicated view with a sublayer
Here I am trying to add a UIView subclass that has a dashed border. Then, when I need to show the dashed border or hide the dashed border, I just set the hidden property of the view accordingly. This works fine, except I cannot get the dashed border sublayer to resize.
The view is resizing to be the correct width and height based on the AutoLayout constraints, as can be seen in the view hierarchy debugger screenshot above. But the sublayer is still the original size (approximately 50px x 50px, which I guess is coming from the UICollectionView because I am not specifying that size anywhere).
For this implementation, I have a custom UIView subclass called MyResizableSublayerView. It overrides layoutSublayersOfLayer to handle the resizing of the sublayer, or at least that is what is supposed to be happening, but clearly it is not working.
But then the MyResizableSublayerView class is used in the collection view cell to add the dashed border to the view hierarchy.
MyResizableSublayerView
#interface MyResizableSublayerView : UIView
#property (strong, nonatomic) CAShapeLayer *borderLayer;
+ (instancetype)viewWithBorderSublayer:(CAShapeLayer *)shapeLayer;
#end
#implementation MyResizableSublayerView
+ (instancetype)viewWithBorderSublayer:(CAShapeLayer *)shapeLayer {
CIResizableSublayerView *view = [[MyResizableSublayerView alloc] init];
view.borderLayer = shapeLayer;
return view;
}
- (void)setBorderLayer:(CAShapeLayer *)borderLayer {
if (self->_borderLayer) {
[self->_borderLayer removeFromSuperlayer];
}
self->_borderLayer = borderLayer;
[self.layer addSublayer:self->_borderLayer];
}
- (void)layoutSublayersOfLayer:(CALayer *)layer {
[super layoutSublayersOfLayer:layer];
self.borderLayer.frame = layer.bounds;
}
#end
MyCollectionViewCell
#interface MyCollectionViewCell ()
#property (strong, nonatomic) MyResizableSublayerView *unavailableBorderView;
#end
#implementation MyCollectionViewCell
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self initialize];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self initialize];
}
return self;
}
- (void)initialize {
self.contentView.layer.cornerRadius = 4.0f;
self.contentView.layer.borderWidth = 1.0f;
//... add other subviews
[self.contentView addSubview:self.unavailableBorderView];
[NSLayoutConstraint activateConstraints:#[
[self.contentView.widthAnchor constraintLessThanOrEqualToConstant:250.0],
[self.contentView.widthAnchor constraintGreaterThanOrEqualToConstant:100.0],
[self.unavailableBorderView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor],
[self.unavailableBorderView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor],
[self.unavailableBorderView.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor],
[self.unavailableBorderView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor],
//... constraints for other views
]];
}
- (MyResizableSublayerView *)unavailableBorderView {
if (!self->_unavailableBorderView) {
CAShapeLayer *layer = [CAShapeLayer layer];
layer.strokeColor = [UIColor colorWithRed:0xE0/255.0 green:0xE0/255.0 blue:0xE0/255.0 alpha:1.0].CGColor;
layer.lineWidth = 4.0;
layer.lineJoin = kCALineJoinRound;
layer.fillColor = [UIColor clearColor].CGColor;
layer.lineDashPattern = #[#4, #4];
layer.frame = self.contentView.bounds;
layer.path = [UIBezierPath bezierPathWithRoundedRect:self.contentView.bounds cornerRadius:self.contentView.layer.cornerRadius].CGPath;
self->_unavailableBorderView = [MyResizableSublayerView viewWithBorderSublayer:layer];
self->_unavailableBorderView.translatesAutoresizingMaskIntoConstraints = NO;
self->_unavailableBorderView.layer.cornerRadius = self.contentView.layer.cornerRadius;
self->_unavailableBorderView.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.0];
}
return self->_unavailableBorderView;
}
//... more logic
#end
Approach 2 - add directly to the UICollectionViewCell
For this approach, I add the CAShapeLayer directly to the UICollectionViewCell and then override the layoutSublayersOfLayer to try to resize the dashed border sublayer, but this is not working either.
#interface MyCollectionViewCell ()
#property (strong, nonatomic) CAShapeLayer *unavailableBorderLayer;
#end
#implementation MyCollectionViewCell
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self initialize];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self initialize];
}
return self;
}
- (void)initialize {
self.contentView.layer.cornerRadius = 4.0f;
self.contentView.layer.borderWidth = 1.0f;
//... add other subviews
[NSLayoutConstraint activateConstraints:#[
[self.contentView.widthAnchor constraintLessThanOrEqualToConstant:250.0],
[self.contentView.widthAnchor constraintGreaterThanOrEqualToConstant:100.0],
//... constraints for other views
]];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.strokeColor = [UIColor greenColor].CGColor;
layer.lineWidth = 2.0;
layer.lineJoin = kCALineJoinRound;
layer.fillColor = [UIColor clearColor].CGColor;
layer.lineDashPattern = #[#4, #4];
layer.frame = self.contentView.bounds;
layer.path = [UIBezierPath bezierPathWithRoundedRect:self.contentView.bounds cornerRadius:self.contentView.layer.cornerRadius].CGPath;
self->_unavailableBorderLayer = layer;
[self.layer addSublayer:self->_unavailableBorderLayer];
}
- (void)layoutSublayersOfLayer:(CALayer *)layer {
[super layoutSublayersOfLayer:layer];
self.unavailableBorderLayer.frame = self.bounds;
}
//... more logic
#end
Questions
I have a couple of questions about this.
What is wrong with my code that is not allowing the dashed border to resize to be the same size as the collection view cell?
Which approach is the best approach to add a dashed border to the collection view cell. Or is there a better approach than the ones that I have listed here? Again my goal is to be able to show or hide the dashed border and for it to be the same size as the collection view cell, which is dynamically sized.

It's not quite clear what you're doing with constraints on the content view ... however, if you are getting the layout you want, except for the dashed borders, give this a try.
First, instead of layoutSublayersOfLayer, use:
- (void)layoutSubviews {
[super layoutSubviews];
_unavailableBorderLayer.frame = self.bounds;
}

Related

How to pin width of subviews relative?

I have one view controller with two subviews.
I would like to pin both of the subviews to have the same relative width.
Like this:
Thanks!
You need to pin subviews to top, left, right sides. Also set equal width property.
You should look at this tutorial, there is a good example of equal width containers.
Here is a simple version of doing the same thing with code,
#interface ViewController ()
#property (nonatomic, weak) UIView *view1;
#property (nonatomic, weak) UIView *view2;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self prepareView];
[self setupConstraints];
}
- (void)prepareView
{
UIView *view1 = [self createView];
UIView *view2 = [self createView];
[self.view addSubview:view1];
[self.view addSubview:view2];
self.view1 = view1;
self.view2 = view2;
}
- (void)setupConstraints
{
NSDictionary *views = #{
#"view1": self.view1,
#"view2": self.view2
};
NSString *horizontalFormat = #"H:|[view1][view2(==view1)]|";
NSString *verticalFormat = #"V:|[view1]|";
NSArray *horizontalConstraints;
NSArray *verticalConstraints;
horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:horizontalFormat
options:NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom
metrics:nil
views:views];
verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:verticalFormat
options:0
metrics:nil
views:views];
[self.view addConstraints:horizontalConstraints];
[self.view addConstraints:verticalConstraints];
}
- (UIView *)createView
{
UIView *view = [[UIView alloc ] initWithFrame:CGRectZero];
view.translatesAutoresizingMaskIntoConstraints = NO;
view.backgroundColor = [self randomColor];
return view;
}
- (UIColor *)randomColor
{
float red = arc4random_uniform(255) / 255.0;
float green = arc4random_uniform(255) / 255.0;
float blue = arc4random_uniform(255) / 255.0;
return [UIColor colorWithRed:red
green:green
blue:blue
alpha:1.0];
}
#end
The horizontalConstraint's options pin both the views top and bottom, while the format string also says that both the views has the same width. We have the first view pinned to the left edge, second view pinned to the right edge and both of them are equal, and also their top and bottom are pinned. Now, we need to tell the view that one of them is pinned to the top edge of the superView and bottom edge of the superView, which the verticalFormat describes. Now, that we have the views with equal widths, their top is pinned to superView's top and bottom to superView's bottom, the subviews will have layout as you have described. It would be quite easy to setup the constraints in storyboard knowing the above details.
You can also look at my previous answer which preserves the views position on rotation IOS AutoLayout Change possition on Rotation.

Update view from controller (objective c)

I have a situation like this:
If I click on left foot button I want that my view in the second controller show a left foot image, and if I click on right button the view should show right foot image.
In the first controller I wrote this to select right or left:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
RSViewController *vc = [segue destinationViewController];
if ([[segue identifier] isEqualToString:#"Left-Foot"])
[vc setLeft:YES];
else
[vc setLeft:NO];
}
In the second controller I have a method which set a boolean value
- (void)setLeft:(BOOL)left
{
if (left) {
_left = left;
NSLog(#"LEFT UPDATED IN RSVIEWCONTROLLER");
}else {
_left = left;
NSLog(#"RIGHT UPDATED IN RSVIEWCONTROLLER");
}
}
and in the same controller I create my view in viewdidload with my one init:
- (void)viewDidLoad
{
NSLog(#"meeeea");
[super viewDidLoad];
CGRect rect = CGRectMake(0, 0, 100, 100);
self.animatedCircleView = [[RSAnimatedCircleView alloc] initWithFrameFoot:rect foot:YES];
NSLog(#"Viewdidnoload");
}
and in my view (small blue rectangle in the picture):
- (void)setup:(BOOL)left
{
self.leftFoot = left;
}
- (void)setLeftFoot:(BOOL)leftFoot
{
_leftFoot = leftFoot;
}
- (void)awakeFromNib
{
[self setup:_leftFoot];
}
- (id)initWithFrameFoot:(CGRect)frame foot:(BOOL)left
{
self = [super initWithFrame:frame];
[self setup:left];
self.leftFoot = left;
if (self.leftFoot)
NSLog(#"Left == TRUE");
return self;
}
- (RSFootView *)footView
{
CGRect rect = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
if (self.leftFoot) {
NSLog(#"Now left is yes");
}else {
NSLog(#"left is no");
}
_footView = [[RSFootView alloc] initWithFrame:rect withLeftFoot:self.leftFoot];
[_footView setBackgroundColor:[UIColor clearColor]];
return _footView;
}
- (void)drawRect:(CGRect)rect
{
if (self.leftFoot)
NSLog(#"left ada");
else
NSLog(#"right masd");
CGFloat x = self.bounds.size.height;
CGFloat y = self.bounds.size.width;
CGFloat radius = x/2 - 15;
CGPoint center = CGPointMake(x/2, y/2);
CGFloat startAngle = DEGREES_TO_RADIANS(START_ANGLE);
CGFloat endAngle = DEGREES_TO_RADIANS(END_ANGLE);
// Drawing the sector
UIBezierPath *sector = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES];
sector.lineWidth = 4;
[[UIColor grayColor] setStroke];
[sector stroke];
[self setBackgroundColor:[UIColor clearColor]];
[sector addClip];
[self addSubview:self.footView];
}
in the method "drawRect" the leftfoot value is always false. I do not know why is like this... can anyone help me?
Thank you
You are setting your view to "left" each time in the following line.
self.animatedCircleView = [[RSAnimatedCircleView alloc]
initWithFrameFoot:rect foot:YES];
This is a typical error that becomes very probable when not giving your variables and methods understandable and intuitive names. Expect this type of error to occur frequently if you do not mend your ways.
I would give your view an enum property that states explicitly what foot it is:
typedef enum {LeftFoot, RightFoot} FootType;
#property (strong, nonatomic) FootType footType;
Also, you should revise your method names. I would completely do away with the initWithFrameFoot method and just set a default foot in your override for initWithFrame. (Leaving it at 0 amounts to LeftFoot.) With #synthesize you also do not need a setter or getter. Delete them.
If you want a dedicated initializer, make sure the variable passed are actually preceded by the proper words.
-(id) initWithFrame:(CGRect)rect andFootType:(FootType)footType {
self = [super initWithFrame:rect];
if (self) {
_footType = footType;
}
return self;
}
After using #synthesize you can set the correct foot in prepareForSegue:
footViewController.footType = rightFoot;

how to add lines in UITextView programmatically [duplicate]

I have a UITextView where the user can create notes and save into a plist file.
I want to be able to show lines just like a normal notebook. The problem I have is
that the text won't align properly.
The image below explains the problem quite well.
This is the background I use to create the lines like the Notes.app
This is my code for creating the background for my UITextView:
textView.font = [UIFont fontWithName:#"MarkerFelt-Thin" size:19.0];
textView.backgroundColor = [UIColor colorWithPatternImage: [UIImage imageNamed: #"Notes.png"]];
I know that the UIFont.lineHeight property is only available in > iOS 4.x.
So I wonder if there is another solution to my problem?
You should try and draw your lines programmatically rather than using an image. Here's some sample code of how you could accomplish that. You can subclass UITextView and override it's drawRect: method.
NoteView.h
#import <UIKit/UIKit.h>
#interface NoteView : UITextView <UITextViewDelegate> {
}
#end
NoteView.m
#import "NoteView.h"
#implementation NoteView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor colorWithRed:1.0f green:1.0f blue:0.6f alpha:1.0f];
self.font = [UIFont fontWithName:#"MarkerFelt-Thin" size:19];
}
return self;
}
- (void)drawRect:(CGRect)rect {
//Get the current drawing context
CGContextRef context = UIGraphicsGetCurrentContext();
//Set the line color and width
CGContextSetStrokeColorWithColor(context, [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.2f].CGColor);
CGContextSetLineWidth(context, 1.0f);
//Start a new Path
CGContextBeginPath(context);
//Find the number of lines in our textView + add a bit more height to draw lines in the empty part of the view
NSUInteger numberOfLines = (self.contentSize.height + self.bounds.size.height) / self.font.leading;
//Set the line offset from the baseline. (I'm sure there's a concrete way to calculate this.)
CGFloat baselineOffset = 6.0f;
//iterate over numberOfLines and draw each line
for (int x = 0; x < numberOfLines; x++) {
//0.5f offset lines up line with pixel boundary
CGContextMoveToPoint(context, self.bounds.origin.x, self.font.leading*x + 0.5f + baselineOffset);
CGContextAddLineToPoint(context, self.bounds.size.width, self.font.leading*x + 0.5f + baselineOffset);
}
//Close our Path and Stroke (draw) it
CGContextClosePath(context);
CGContextStrokePath(context);
}
#end
MyViewController.h
#import <UIKit/UIKit.h>
#import "NoteView.h"
#interface MyViewController : UIViewController <UITextViewDelegate> {
NoteView *note;
}
#property (nonatomic, retain) NoteView *note;
#end
MyViewController.m
#import "MyViewController.h"
#import "NoteView.h"
#define KEYBOARD_HEIGHT 216
#implementation MyViewController
#synthesize note;
- (void)loadView {
[super loadView];
self.note = [[[NoteView alloc] initWithFrame:self.view.bounds] autorelease];
[self.view addSubview:note];
note.delegate = self;
note.text = #"This is the first line.\nThis is the second line.\nThis is the ... line.\nThis is the ... line.\nThis is the ... line.\nThis is the ... line.\nThis is the ... line.\n";
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
[note setNeedsDisplay];
}
- (void)textViewDidBeginEditing:(UITextView *)textView {
CGRect frame = self.view.bounds;
frame.size.height -= KEYBOARD_HEIGHT;
note.frame = frame;
}
- (void)textViewDidEndEditing:(UITextView *)textView {
note.frame = self.view.bounds;
}
- (void)dealloc {
[note release];
[super dealloc];
}
Take a look at Apple's documentation for Managing the Keyboard, specifically "Moving Content That Is Located Under the Keyboard". It explains how to listen for NSNotifcations and adjust your views properly.
I think the problem is with your image, the yellow space over the line is creating the problem.
You should edit the image.
And nice work.

UIPageControl doesn't properly react to taps

In my TestApp, I created a class "Carousel" which should let me create a swipe menu with a UIPageControl in an easy way (by simply creating an instance of the class Carousel).
Carousel is a subclass of UIView
At init, it creates an UIView, containing UIScrollView, UIPageControl
I can add further UIViews to the scroll view
I don't know if this is the proper way to do it, but my example worked quite well in my TestApp. Swiping between pages works perfectly and the display of the current page in the UIPageControl is correct.
If there were not one single problem: The UIPageControl sometimes reacts to clicks/taps (I only tested in Simulator so far!), sometimes it doesn't. Let's say most of the time it doesn't. I couldn't find out yet when it does, for me it's just random...
As you can see below, I added
[pageControl addTarget:self action:#selector(pageChange:) forControlEvents:UIControlEventValueChanged];
to my code. I thought this would do the proper handling of taps? But unfortunately, pageChange doesn't get always called (so the value of the UIPageControl doesn't change every time I click).
I would appreciate any input on this because I couldn't find any solution on this yet.
This is what I have so far:
Carousel.h
#import <UIKit/UIKit.h>
#interface Carousel : UIView {
UIScrollView *scrollView;
UIPageControl *pageControl;
BOOL pageControlBeingUsed;
}
- (void)addView:(UIView *)view;
- (void)setTotalPages:(NSUInteger)pages;
- (void)setCurrentPage:(NSUInteger)current;
- (void)createPageControlAt:(CGRect)cg;
- (void)createScrollViewAt:(CGRect)cg;
#end
Carousel.m
#import "Carousel.h"
#implementation Carousel
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Create a scroll view
scrollView = [[UIScrollView alloc] init];
[self addSubview:scrollView];
scrollView.delegate = (id) self;
// Init Page Control
pageControl = [[UIPageControl alloc] init];
[pageControl addTarget:self action:#selector(pageChange:) forControlEvents:UIControlEventValueChanged];
[self addSubview:pageControl];
}
return self;
}
- (IBAction)pageChange:(id)sender {
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * pageControl.currentPage;
frame.origin.y = 0;
[scrollView scrollRectToVisible:frame animated:TRUE];
NSLog(#"%i", pageControl.currentPage);
}
- (void)addView:(UIView *)view {
[scrollView addSubview:view];
}
- (void)createPageControlAt:(CGRect)cg {
pageControl.frame = cg;
}
- (void)setTotalPages:(NSUInteger)pages {
pageControl.numberOfPages = pages;
}
- (void)setCurrentPage:(NSUInteger)current {
pageControl.currentPage = current;
}
- (void)createScrollViewAt:(CGRect)cg {
[scrollView setPagingEnabled:TRUE];
scrollView.frame = cg;
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width*pageControl.numberOfPages, scrollView.frame.size.height);
[scrollView setShowsHorizontalScrollIndicator:FALSE];
[scrollView setShowsVerticalScrollIndicator:FALSE];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
float frac = scrollView.contentOffset.x / scrollView.frame.size.width;
NSUInteger page = lround(frac);
pageControl.currentPage = page;
}
#end
ViewController.m (somewhere in viewDidLoad)
Carousel *carousel = [[Carousel alloc] initWithFrame:CGRectMake(0, 0, 320, 460)];
for (int i=0; i<5; i++) {
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(i * 320, 0, 320, 420)];
UIColor *color;
if(i%3==0) color = [UIColor blueColor];
else if(i%3==1) color = [UIColor redColor];
else color = [UIColor purpleColor];
view.backgroundColor = color;
[carousel addView:view];
view = nil;
}
[carousel setTotalPages:5];
[carousel setCurrentPage:0];
[carousel createPageControlAt:CGRectMake(0,420,320,40)];
[carousel createScrollViewAt:CGRectMake(0,0,320,420)];
Your code is correct. Most likely the frame of your pageControl is pretty small, so theres not a lot of area to look for touch events. You would need to increase the size of the height of pageControl in order to make sure taps are recognized all of the time.

NSScrollview with NSGradient

I have a nsscroll view in my application and i made a subclass of nsscrollview to add a nsgradient but it doesn't work this is my code in my implementation file:
#import "scrollview.h"
#implementation scrollview
#synthesize startingColor;
#synthesize endingColor;
#synthesize angle;
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
[self setStartingColor:[NSColor colorWithCalibratedRed:0.941 green:0.941 blue:0.941 alpha:1]];
[self setEndingColor:[NSColor colorWithCalibratedRed:0.6588 green:0.6588 blue:0.6588 alpha:1]];
[self setAngle:90];
}
return self;
}
- (void)drawRect:(NSRect)rect {
NSBezierPath* roundRectPath = [NSBezierPath bezierPathWithRoundedRect: [self bounds] xRadius:10 yRadius:10];
[roundRectPath addClip];
if (endingColor == nil || [startingColor isEqual:endingColor]) {
// Fill view with a standard background color
[startingColor set];
NSRectFill(rect);
}
else {
// Fill view with a top-down gradient
// from startingColor to endingColor
NSGradient* aGradient = [[NSGradient alloc]
initWithStartingColor:startingColor
endingColor:endingColor];
[aGradient drawInRect:[self bounds] angle:angle];
}
}
The first step is to create a custom NSView subclass that draws a gradient:
GradientBackgroundView.h:
#interface GradientBackgroundView : NSView
{}
#end
GradientBackgroundView.m:
#import "GradientBackgroundView.h"
#implementation GradientBackgroundView
- (void) drawRect:(NSRect)dirtyRect
{
NSGradient *gradient = [[[NSGradient alloc] initWithStartingColor:[NSColor redColor] endingColor:[NSColor greenColor]] autorelease];
[gradient drawInRect:[self bounds] angle:90];
}
#end
The next step is to make the scroll view's document view an instance of this class (instead of plain NSView).
In IB, double-click your scroll view, and in the Identity pane set the Class to GradientBackgroundView.
From this point on, things are handled pretty much in the standard way. You can add subviews to the document view, resize it, etc. Here's a screenshot: