Recognize a path of user gesture - objective-c

I'm working on an app with 9 views on screen, and I want the users to connect the views in a way they want, and record their sequence as password.
But I don't know which gesture recognizer I should use.
Should I use CMUnistrokeGestureRecognizer or combination of several swipe gesture or anything else?
Thanks.

You could use a UIPanGestureRecognizer, something like:
CGFloat const kMargin = 10;
- (void)viewDidLoad
{
[super viewDidLoad];
// create a container view that all of our subviews for which we want to detect touches are:
CGFloat containerWidth = fmin(self.view.bounds.size.width, self.view.bounds.size.height) - kMargin * 2.0;
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(kMargin, kMargin, containerWidth, containerWidth)];
container.backgroundColor = [UIColor darkGrayColor];
[self.view addSubview:container];
// now create all of the subviews, specifying a tag for each; and
CGFloat cellWidth = (containerWidth - (4.0 * kMargin)) / 3.0;
for (NSInteger column = 0; column < 3; column++)
{
for (NSInteger row = 0; row < 3; row++)
{
UIView *cell = [[UIView alloc] initWithFrame:CGRectMake(kMargin + column * (cellWidth + kMargin),
kMargin + row * (cellWidth + kMargin),
cellWidth, cellWidth)];
cell.tag = row * 3 + column;
cell.backgroundColor = [UIColor lightGrayColor];
[container addSubview:cell];
}
}
// finally, create the gesture recognizer
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self
action:#selector(handlePan:)];
[container addGestureRecognizer:pan];
}
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
static NSMutableArray *gesturedSubviews;
// if we're starting a gesture, initialize our list of subviews that we've gone over
if (gesture.state == UIGestureRecognizerStateBegan)
{
gesturedSubviews = [NSMutableArray array];
}
// now figure out whether:
// (a) are we over a subview; and
// (b) is this a different subview than we last were over
CGPoint location = [gesture locationInView:gesture.view];
for (UIView *subview in gesture.view.subviews)
{
if (CGRectContainsPoint(subview.frame, location))
{
if (subview != [gesturedSubviews lastObject])
{
[gesturedSubviews addObject:subview];
// an example of the sort of graphical flourish to give the
// some visual cue that their going over the subview in question
// was recognized
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionAutoreverse
animations:^{
subview.alpha = 0.5;
}
completion:^(BOOL finished){
subview.alpha = 1.0;
}];
}
}
}
// finally, when done, let's just log the subviews
// you would do whatever you would want here
if (gesture.state == UIGestureRecognizerStateEnded)
{
NSLog(#"We went over:");
for (UIView *subview in gesturedSubviews)
{
NSLog(#" %d", subview.tag);
}
// you might as well clean up your static variable when you're done
gesturedSubviews = nil;
}
}
Obviously, you would create your subviews any way you want, and keep track of them any way you want, but the idea is to have subviews with unique tag numbers, and the gesture recognizer would just see which order you go over them in a single gesture.
Even if I didn't capture precisely what you want, it at least shows you how you can use a pan gesture recognizer to track the movement of your finger from one subview to another.
Update:
If you wanted to draw a path on the screen as the user is signing in, you could create a CAShapeLayer with a UIBezierPath. I'll demonstrate that below, but as a caveat, I feel compelled to point out that this might not be a great security feature: Usually with password entry, you'll show the user enough so that they can confirm that they're doing what they want, but not enough so that someone can glance look over their shoulder and see what the whole password was. When entering a text password, usually iOS momentarily shows you the last key you hit, but quickly turns that into an asterisk so that, at no point, can you see the whole password. Hence my initial suggestion.
But if you really have your heart set on showing the user the path as they draw it, you could use something like the following. First, this requires Quartz 2D. Thus add the QuartzCore.framework to your project (see Linking to a Library or Framework). Second, import the QuartCore headers:
#import <QuartzCore/QuartzCore.h>
Third, replace the pan handler with something like:
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
static NSMutableArray *gesturedSubviews;
static UIBezierPath *path = nil;
static CAShapeLayer *shapeLayer = nil;
// if we're starting a gesture, initialize our list of subviews that we've gone over
if (gesture.state == UIGestureRecognizerStateBegan)
{
gesturedSubviews = [NSMutableArray array];
}
// now figure out whether:
// (a) are we over a subview; and
// (b) is this a different subview than we last were over
CGPoint location = [gesture locationInView:gesture.view];
for (UIView *subview in gesture.view.subviews)
{
if (!path)
{
// if the path hasn't be started, initialize it and the shape layer
path = [UIBezierPath bezierPath];
[path moveToPoint:location];
shapeLayer = [[CAShapeLayer alloc] init];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 2.0;
[gesture.view.layer addSublayer:shapeLayer];
}
else
{
// otherwise add this point to the layer's path
[path addLineToPoint:location];
shapeLayer.path = path.CGPath;
}
if (CGRectContainsPoint(subview.frame, location))
{
if (subview != [gesturedSubviews lastObject])
{
[gesturedSubviews addObject:subview];
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionAutoreverse
animations:^{
subview.alpha = 0.5;
}
completion:^(BOOL finished){
subview.alpha = 1.0;
}];
}
}
}
// finally, when done, let's just log the subviews
// you would do whatever you would want here
if (gesture.state == UIGestureRecognizerStateEnded)
{
// assuming the tags are numbers between 0 and 9 (inclusive), we can build the password here
NSMutableString *password = [NSMutableString string];
for (UIView *subview in gesturedSubviews)
[password appendFormat:#"%c", subview.tag + 48];
NSLog(#"Password = %#", password);
// clean up our array of gesturedSubviews
gesturedSubviews = nil;
// clean up the drawing of the path on the screen the user drew
[shapeLayer removeFromSuperlayer];
shapeLayer = nil;
path = nil;
}
}
That yields a path that the user draws as the gesture proceeds:
Rather than showing the path the user draws with each and every movement of the user's finger, maybe you just draw the lines between the center of the subviews, such as:
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
static NSMutableArray *gesturedSubviews;
static UIBezierPath *path = nil;
static CAShapeLayer *shapeLayer = nil;
// if we're starting a gesture, initialize our list of subviews that we've gone over
if (gesture.state == UIGestureRecognizerStateBegan)
{
gesturedSubviews = [NSMutableArray array];
}
// now figure out whether:
// (a) are we over a subview; and
// (b) is this a different subview than we last were over
CGPoint location = [gesture locationInView:gesture.view];
for (UIView *subview in gesture.view.subviews)
{
if (CGRectContainsPoint(subview.frame, location))
{
if (subview != [gesturedSubviews lastObject])
{
[gesturedSubviews addObject:subview];
if (!path)
{
// if the path hasn't be started, initialize it and the shape layer
path = [UIBezierPath bezierPath];
[path moveToPoint:subview.center];
shapeLayer = [[CAShapeLayer alloc] init];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 2.0;
[gesture.view.layer addSublayer:shapeLayer];
}
else
{
// otherwise add this point to the layer's path
[path addLineToPoint:subview.center];
shapeLayer.path = path.CGPath;
}
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionAutoreverse
animations:^{
subview.alpha = 0.5;
}
completion:^(BOOL finished){
subview.alpha = 1.0;
}];
}
}
}
// finally, when done, let's just log the subviews
// you would do whatever you would want here
if (gesture.state == UIGestureRecognizerStateEnded)
{
// assuming the tags are numbers between 0 and 9 (inclusive), we can build the password here
NSMutableString *password = [NSMutableString string];
for (UIView *subview in gesturedSubviews)
[password appendFormat:#"%c", subview.tag + 48];
NSLog(#"Password = %#", password);
// clean up our array of gesturedSubviews
gesturedSubviews = nil;
// clean up the drawing of the path on the screen the user drew
[shapeLayer removeFromSuperlayer];
shapeLayer = nil;
path = nil;
}
}
That yields something like:
You have all sorts of options, but hopefully you now have the building blocks so you can design your own solution.

Forgive me Rob, pure Plagiarism here :) Needed the same code in swift 3.0 :) so I translated this great little example you wrote into swift 3.0.
ViewController.swift
import UIKit
class ViewController: UIViewController {
static let kMargin:CGFloat = 10.0;
override func viewDidLoad()
{
super.viewDidLoad()
// create a container view that all of our subviews for which we want to detect touches are:
let containerWidth = fmin(self.view.bounds.size.width, self.view.bounds.size.height) - ViewController.kMargin * 2.0
let container = UIView(frame: CGRect(x: ViewController.kMargin, y: ViewController.kMargin, width: containerWidth, height: containerWidth))
container.backgroundColor = UIColor.darkGray
view.addSubview(container)
// now create all of the subviews, specifying a tag for each; and
let cellWidth = (containerWidth - (4.0 * ViewController.kMargin)) / 3.0
for column in 0 ..< 3 {
for row in 0 ..< 3 {
let cell = UIView(frame: CGRect(x: ViewController.kMargin + CGFloat(column) * (cellWidth + ViewController.kMargin), y: ViewController.kMargin + CGFloat(row) * (cellWidth + ViewController.kMargin), width: cellWidth, height: cellWidth))
cell.tag = row * 3 + column;
container.addSubview(cell)
}
}
// finally, create the gesture recognizer
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
container.addGestureRecognizer(pan)
}
func handlePan(gesture: UIPanGestureRecognizer)
{
var gesturedSubviews : [UIView] = []
// if we're starting a gesture, initialize our list of subviews that we've gone over
if (gesture.state == .began)
{
gesturedSubviews.removeAll()
}
let location = gesture.location(in: gesture.view)
for subview in (gesture.view?.subviews)! {
if (subview.frame.contains(location)) {
if (subview != gesturedSubviews.last) {
gesturedSubviews.append(subview)
subview.backgroundColor = UIColor.blue
}
}
// finally, when done, let's just log the subviews
// you would do whatever you would want here
if (gesture.state != .recognized)
{
print("We went over:");
for subview in gesturedSubviews {
print(" %d", (subview as AnyObject).tag);
}
// you might as well clean up your static variable when you're done
}
}
}
}
Update: Almost that is; I tried to translate the update too, but my translation missed something and didn't work, so I searched around SO and crafted a similar if slightly different final solution.
import UIKit
import QuartzCore
class ViewController: UIViewController {
static let kMargin:CGFloat = 10.0;
override func viewDidLoad()
{
super.viewDidLoad()
// create a container view that all of our subviews for which we want to detect touches are:
let containerWidth = fmin(self.view.bounds.size.width, self.view.bounds.size.height) - ViewController.kMargin * 2.0
let container = UIView(frame: CGRect(x: ViewController.kMargin, y: ViewController.kMargin, width: containerWidth, height: containerWidth))
container.backgroundColor = UIColor.darkGray
view.addSubview(container)
// now create all of the subviews, specifying a tag for each; and
let cellWidth = (containerWidth - (4.0 * ViewController.kMargin)) / 3.0
for column in 0 ..< 3 {
for row in 0 ..< 3 {
let cell = UIView(frame: CGRect(x: ViewController.kMargin + CGFloat(column) * (cellWidth + ViewController.kMargin), y: ViewController.kMargin + CGFloat(row) * (cellWidth + ViewController.kMargin), width: cellWidth, height: cellWidth))
cell.tag = row * 3 + column;
container.addSubview(cell)
}
}
// finally, create the gesture recognizer
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
container.addGestureRecognizer(pan)
}
// Here's a Swift 3.0 version based on Rajesh Choudhary's answer:
func drawLine(onLayer layer: CALayer, fromPoint start: CGPoint, toPoint end:CGPoint) {
let line = CAShapeLayer()
let linePath = UIBezierPath()
linePath.move(to: start)
linePath.addLine(to: end)
line.path = linePath.cgPath
line.fillColor = nil
line.lineWidth = 8
line.opacity = 0.5
line.strokeColor = UIColor.red.cgColor
layer.addSublayer(line)
}
var gesturedSubviews : [UIView] = []
var startX: CGFloat!
var startY: CGFloat!
var endX: CGFloat!
var endY: CGFloat!
func handlePan(gesture: UIPanGestureRecognizer) {
if (gesture.state == .began)
{
gesturedSubviews = [];
let location = gesture.location(in: gesture.view)
print("location \(location)")
startX = location.x
startY = location.y
}
if (gesture.state == .changed) {
let location = gesture.location(in: gesture.view)
print("location \(location)")
endX = location.x
endY = location.y
drawLine(onLayer: view.layer, fromPoint: CGPoint(x: startX, y: startY), toPoint: CGPoint(x:endX, y:endY))
startX = endX
startY = endY
}
if (gesture.state == .ended) {
let location = gesture.location(in: gesture.view)
print("location \(location)")
drawLine(onLayer: view.layer, fromPoint: CGPoint(x: startX, y: startY), toPoint: CGPoint(x:location.x, y:location.y))
}
// now figure out whether:
// (a) are we over a subview; and
// (b) is this a different subview than we last were over
let location = gesture.location(in: gesture.view)
print("location \(location)")
for subview in (gesture.view?.subviews)! {
if subview.frame.contains(location) {
if (subview != gesturedSubviews.last) {
gesturedSubviews.append(subview)
subview.backgroundColor = UIColor.blue
subview.alpha = 1.0
}
}
}
}
}

Related

Apply and animate many constraints to outlet collection of uiviews

I have an outlet collection of labels. The labels are in stack views parented by a stack view. When the view loads I'd like to have each label fade in and move slightly to the right one after the other. I can apply the constraint in a loop to offset it. But only one will animate back to the final position.
-(void)viewDidLoad {
[super viewDidLoad];
for (UILabel *lbl in _constructionlabels) {
lbl.alpha = 0.0;
leadingCnst=[NSLayoutConstraint
constraintWithItem:lbl
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:[lbl superview]
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:-25];
[self.view addConstraint:leadingCnst];
}
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
leadingCnst.constant = 0;
[UIView animateWithDuration:0.33 delay:2 options:UIViewAnimationOptionCurveEaseOut animations:^{
for (UILabel *lbl in self->_constructionlabels) {
lbl.alpha = 1.0;
}
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
}];
}
How can I apply constraints to every needed label, and then animate all of them one after the other?
Keep references to each label constraint and start all animations at once, each with a delay.
// Declare array to hold references to constraints
NSMutableArray* _labelConstraints = [NSMutableArray array];
-(void) viewDidLoad {
[super viewDidLoad];
for (UILabel * lbl in _constructionlabels) {
lbl.alpha = 0.0;
NSLayoutConstraint* leadingCnst = [NSLayoutConstraint
constraintWithItem: lbl
attribute: NSLayoutAttributeLeading
relatedBy: NSLayoutRelationEqual
toItem: [lbl superview]
attribute: NSLayoutAttributeLeading
multiplier: 1.0
constant: -25
];
[self.view addConstraint: leadingCnst];
// Add constraint reference
[_labelConstraints addObject: #(leadingCnst)];
}
}
-(void) viewDidAppear: (BOOL) animated {
[super viewDidAppear: animated];
for (i = 0; i < [_constructionlabels count]; i++) {
// Get label
Label* lbl = [_constructionlabels objectAtIndex:i];
// Get constraint
NSLayoutConstraint* labelConstraint = [_labelConstraints objectAtIndex:i];
// Animate
[UIView animateWithDuration: 0.33 delay: i options: UIViewAnimationOptionCurveEaseOut animations: ^ {
lbl.alpha = 1.0;
labelConstraint.constant = 0;
[self.view layoutIfNeeded];
}
completion: ^ (BOOL finished) {}
];
}
}
Note: This is just a proof of concept - you may want to refactor the code.
(It's been a while since I wrote ObjC, if you let me know any mistakes I'll correct them.)
You will need to put a recursive function to animate each label one by one without for loop. Add completion block in the animation.
func animate(_ label: UILabel, completion: #escaping () -> Void) {
UIView.animate(withDuration: 1, animations: {
// add animation here
}, completion: { _ in
completion()
})
}
let labelCollection = [UILabel]()
var index = 0
func startAnimation() {
animate(labelCollection[index], completion: {
if self.index < self.labelCollection.count - 1 {
self.index = self.index + 1
self.startAnimation()
}
})
}

CAShapeLayer Shadow with UIBezierPath

This question continues from a previous answer.
I have the following CAShapeLayer:
- (CAShapeLayer *)gaugeCircleLayer {
if (_gaugeCircleLayer == nil) {
_gaugeCircleLayer = [CAShapeLayer layer];
_gaugeCircleLayer.lineWidth = self.gaugeWidth;
_gaugeCircleLayer.fillColor = [UIColor clearColor].CGColor;
_gaugeCircleLayer.strokeColor = self.gaugeTintColor.CGColor;
_gaugeCircleLayer.strokeStart = 0.0f;
_gaugeCircleLayer.strokeEnd = self.value;
_gaugeCircleLayer.lineCap = kCALineCapRound;
_gaugeCircleLayer.path = [self insideCirclePath].CGPath;
CAShapeLayer *cap = [CAShapeLayer layer];
cap.shadowColor = [UIColor blackColor].CGColor;
cap.shadowRadius = 8.0;
cap.shadowOpacity = 0.9;
cap.shadowOffset = CGSizeMake(0, 0);
cap.fillColor = [UIColor grayColor].CGColor;
[_gaugeCircleLayer addSublayer:cap];
}
return _gaugeCircleLayer;
}
Then I have the following UIBezierPath:
- (UIBezierPath *)insideCirclePath {
CGPoint arcCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:arcCenter
radius:CGRectGetWidth(self.bounds) / 2.0f
startAngle:(3.0f * M_PI_2)
endAngle:(3.0f * M_PI_2) + (2.0f * M_PI)
clockwise:YES];
return path;
}
This produces something like the following:
What I am trying to produce with the cap sublayer is the drop shadow at the end and I'd also be interested to know how to get a GradientLayer working similar to the image below:
The problem is that the sublayer is not appearing anywhere and I'm not quite sure why. I'm also not 100% sure on whether or not this is the best way to produce the desired effect.
UPDATE:
The following bit of code creates a cap, though I'm not quite sure how to add it to my UIBezierPath properly:
let cap = CAShapeLayer()
cap.shadowColor = UIColor.blackColor().CGColor
cap.shadowRadius = 8.0
cap.shadowOpacity = 0.9
cap.shadowOffset = CGSize(width: 0, height: 0)
cap.path = UIBezierPath(ovalInRect: CGRectMake(0, 40, 20, 20)).CGPath
cap.fillColor = UIColor.grayColor().CGColor
layer.addSublayer(cap)
I don't know if this will be useful to you, since it doesn't use the CHCircleGaugeView. I was working on several projects related to this question, so I mashed them together and made some changes to produce a progress view that has a color gradient background with a tip that overlaps the end at 100%. I haven't gotten to the point where I make the rounded tip disappear at 0%, but I'll get there eventually. Here are a couple of views of it at 2 different progress levels,
The view is created with a polar gradient drawn in drawRect, masked by an annulus. The rounded tip is a separate layer that's a half circle on the end of a line connected to the center of the circle that's revolved around its center with a transform based on the progress level. Here's the code,
#implementation AnnulusProgressView{ // subclass of UIView
int slices;
CGFloat circleRadius;
CAShapeLayer *maskLayer;
CGFloat segmentAngle;
CGFloat startAngle;
CAShapeLayer *tipView;
NSMutableArray *colors;
int sign;
}
-(instancetype)initWithFrame:(CGRect)frame wantsBackgroundRing:(BOOL)wantsBackground backgroundRingColor:(UIColor *)ringColor {
if (self = [super initWithFrame:frame]) {
slices = 360;
_ringThickness = 12;
circleRadius = self.bounds.size.width/2;
_startColor = [UIColor colorWithHue:0.24 saturation:1 brightness:0.8 alpha:1];
_endColor = [UIColor colorWithHue:0.03 saturation:1 brightness:1 alpha:1];
[self setupColorArray];
_backgroundRing = wantsBackground? [self createBackgroundRingWithColor:ringColor] : nil;
self.layer.mask = [self createAnnulusMask];
}
return self;
}
-(void)setStartColor:(UIColor *)startColor {
_startColor = startColor;
[self setupColorArray];
}
-(void)setEndColor:(UIColor *)endColor {
_endColor = endColor;
[self setupColorArray];
}
-(void)setupColorArray {
colors = [NSMutableArray new];
CGFloat startHue, startSaturation, startBrightness, startAlpha, endHue, endSaturation, endBrightness, endAlpha;
[self.startColor getHue:&startHue saturation:&startSaturation brightness:&startBrightness alpha:&startAlpha];
[self.endColor getHue:&endHue saturation:&endSaturation brightness:&endBrightness alpha:&endAlpha];
for(int i=0;i<slices;i++){
CGFloat hue = startHue + (endHue - startHue)*i/slices;
CGFloat brightness = startBrightness + (endBrightness - startBrightness)*i/slices;
CGFloat saturation = startSaturation + (endSaturation - startSaturation)*i/slices;
CGFloat alpha = startAlpha + (endAlpha - startAlpha)*i/slices;
UIColor *color = [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:alpha];
[colors addObject:color];
}
self.progress = _progress;
}
-(UIView *)createBackgroundRingWithColor:(UIColor *) color {
UIView *bgRing = [[UIView alloc] initWithFrame:self.frame];
bgRing.backgroundColor = color;
bgRing.layer.mask = [self createAnnulusMask];
[(CAShapeLayer *)bgRing.layer.mask setStrokeEnd:startAngle + 2*M_PI ];
return bgRing;
}
-(void)didMoveToSuperview {
if (self.backgroundRing) [self.superview insertSubview:self.backgroundRing belowSubview:self];
tipView = [self tipView];
[self.superview.layer addSublayer:tipView];
}
-(CAShapeLayer *)tipView {
CAShapeLayer *tip = [CAShapeLayer layer];
tip.frame = self.frame;
tip.fillColor = [UIColor redColor].CGColor;
UIBezierPath *shape = [UIBezierPath bezierPath];
CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
[shape moveToPoint:center];
CGPoint bottomPoint = CGPointMake(center.x, center.y + circleRadius - self.ringThickness*2);
CGFloat tipCenterY = bottomPoint.y + self.ringThickness - 1;
[shape addLineToPoint: bottomPoint];
[shape addLineToPoint:CGPointMake(bottomPoint.x+2, bottomPoint.y)];
double fractionAlongTangent = self.ringThickness;
[shape addCurveToPoint:CGPointMake(center.x, center.y + circleRadius) controlPoint1:CGPointMake(center.x - self.ringThickness*1.5, tipCenterY - fractionAlongTangent) controlPoint2:CGPointMake(center.x - self.ringThickness*1.5, tipCenterY + fractionAlongTangent)];
[shape closePath];
tip.path = shape.CGPath;
tip.shadowColor = [UIColor darkGrayColor].CGColor;
tip.shadowOpacity = 0.8;
tip.shadowOffset = CGSizeMake(-6, 0);
tip.shadowRadius = 2;
return tip;
}
- (void)setProgress:(CGFloat)progress{
sign = (progress >= _progress)? 1 : -1;
[self animateProgressTo:#(progress)];
}
-(void)animateProgressTo:(NSNumber *) newValueNumber{
float newValue = newValueNumber.floatValue;
_progress = (_progress + (sign * 0.1) > 1)? 1 : _progress + (sign * 0.1);
if ((_progress > newValue && sign == 1) || (_progress < newValue && sign == -1)) {
_progress = newValue;
}
maskLayer.strokeEnd = _progress;
tipView.transform = CATransform3DMakeRotation(-(1 - _progress + 0.002) * M_PI*2, 0, 0, 1); //the 0.002 closes a small gap between the tip and the annulus strokeEnd
int i = (int)(_progress*(slices - 1));
tipView.fillColor = ((UIColor *)colors[i]).CGColor;
if (sign == 1) {
if (_progress < newValue) {
[self performSelector:#selector(animateProgressTo:) withObject:#(newValue) afterDelay:0.05];
}
}else{
if (_progress > newValue) {
[self performSelector:#selector(animateProgressTo:) withObject:#(newValue) afterDelay:0.05];
}
}
NSLog(#"%f",_progress);
}
- (CAShapeLayer *)createAnnulusMask {
maskLayer = [CAShapeLayer layer];
maskLayer.frame = self.bounds;
segmentAngle = 2*M_PI/(slices);
startAngle = M_PI_2;
CGFloat endAngle = startAngle + 2*M_PI;
maskLayer.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)) radius:circleRadius - self.ringThickness startAngle:startAngle endAngle:endAngle clockwise:YES].CGPath;
maskLayer.fillColor = [UIColor clearColor].CGColor;
maskLayer.strokeColor = [UIColor blackColor].CGColor;
maskLayer.lineWidth = self.ringThickness * 2;
maskLayer.strokeEnd = self.progress;
return maskLayer;
}
-(void)drawRect:(CGRect)rect{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetAllowsAntialiasing(ctx, NO);
CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
for(int i=0;i<slices;i++){
CGContextSaveGState(ctx);
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:center];
[path addArcWithCenter:center radius:circleRadius startAngle:startAngle endAngle:startAngle+segmentAngle clockwise:YES];
[path addClip];
[colors[i] setFill];
[path fill];
CGContextRestoreGState(ctx);
startAngle += segmentAngle;
}
}
This should resolve the problem.
Only remaining issue is the animation, whereby the cap is not animated.
The trick was to add the cap to the end of the gauge, and update it when the value of the gauge changed. To calculate the location, a little math magic was used. It needs to be under the gauge, so the cap is added in the trackCircleLayer
//
// CHCircleGaugeView.m
//
// Copyright (c) 2014 ChaiOne <http://www.chaione.com/>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#import "CHCircleGaugeView.h"
#import "CHCircleGaugeViewDebugMacros.h"
#import <CoreText/CoreText.h>
#import <QuartzCore/QuartzCore.h>
static CGFloat const CHKeyAnimationDuration = 0.5f;
static CGFloat const CHKeyDefaultValue = 0.0f;
static CGFloat const CHKeyDefaultFontSize = 32.0f;
static CGFloat const CHKeyDefaultTrackWidth = 0.5f;
static CGFloat const CHKeyDefaultGaugeWidth = 2.0f;
static NSString * const CHKeyDefaultNAText = #"n/a";
static NSString * const CHKeyDefaultNoAnswersText = #"%";
#define CHKeyDefaultTrackTintColor [UIColor blackColor]
#define CHKeyDefaultGaugeTintColor [UIColor blackColor]
#define CHKeyDefaultTextColor [UIColor blackColor]
#interface CHCircleGaugeView ()
#property (nonatomic, strong) CAShapeLayer *trackCircleLayer;
#property (nonatomic, strong) CAShapeLayer *gaugeCircleLayer;
// ADDED
#property (nonatomic, strong) CAShapeLayer *capLayer;
// END ADDED
#property (nonatomic, strong) UILabel *valueTextLabel;
#end
#implementation CHCircleGaugeView
#pragma mark - View Initialization
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initSetup];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self initSetup];
}
return self;
}
- (void)initSetup {
_state = CHCircleGaugeViewStateNA;
_value = CHKeyDefaultValue;
_trackTintColor = CHKeyDefaultTrackTintColor;
_gaugeTintColor = CHKeyDefaultGaugeTintColor;
_textColor = CHKeyDefaultTextColor;
_font = [UIFont systemFontOfSize:CHKeyDefaultFontSize];
_trackWidth = CHKeyDefaultTrackWidth;
_gaugeWidth = CHKeyDefaultGaugeWidth;
_notApplicableString = CHKeyDefaultNAText;
_noDataString = CHKeyDefaultNoAnswersText;
[self createGauge];
}
- (void)createGauge {
[self.layer addSublayer:self.trackCircleLayer];
[self.layer addSublayer:self.gaugeCircleLayer];
[self addSubview:self.valueTextLabel];
[self setupConstraints];
}
- (void)setupConstraints {
NSDictionary *viewDictionary = #{#"valueText" : self.valueTextLabel};
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[valueText]|" options:0 metrics:nil views:viewDictionary]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[valueText]|" options:0 metrics:nil views:viewDictionary]];
}
#pragma mark - Property Setters
- (void)setState:(CHCircleGaugeViewState)state {
if (_state != state) {
_state = state;
switch (state) {
case CHCircleGaugeViewStateNA: {
[self updateGaugeWithValue:0 animated:NO];
break;
}
case CHCircleGaugeViewStatePercentSign: {
[self updateGaugeWithValue:0 animated:NO];
break;
}
case CHCircleGaugeViewStateScore: {
[self updateGaugeWithValue:self.value animated:NO];
break;
}
default: {
ALog(#"Missing gauge state.");
break;
}
}
}
}
- (void)setValue:(CGFloat)value {
[self setValue:value animated:NO];
}
- (void)setValue:(CGFloat)value animated:(BOOL)animated {
self.state = CHCircleGaugeViewStateScore;
if (value != _value) {
[self willChangeValueForKey:NSStringFromSelector(#selector(value))];
value = MIN(1.0f, MAX(0.0f, value));
[self updateGaugeWithValue:value animated:animated];
_value = value;
[self didChangeValueForKey:NSStringFromSelector(#selector(value))];
}
}
- (void)setUnitsString:(NSString *)unitsString {
if ([_unitsString isEqualToString:unitsString] == NO) {
_unitsString = [unitsString copy];
self.valueTextLabel.attributedText = [self formattedStringForValue:self.value];
}
}
- (void)updateGaugeWithValue:(CGFloat)value animated:(BOOL)animated {
self.valueTextLabel.attributedText = [self formattedStringForValue:value];
BOOL previousDisableActionsValue = [CATransaction disableActions];
[CATransaction setDisableActions:YES];
self.gaugeCircleLayer.strokeEnd = value;
// ADDED
_capLayer.path = [self capPathForValue:value].CGPath;
// END ADDED
if (animated) {
self.gaugeCircleLayer.strokeEnd = value;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = CHKeyAnimationDuration;
pathAnimation.fromValue = [NSNumber numberWithFloat:self.value];
pathAnimation.toValue = [NSNumber numberWithFloat:value];
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.gaugeCircleLayer addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
}
[CATransaction setDisableActions:previousDisableActionsValue];
}
- (void)setTrackTintColor:(UIColor *)trackTintColor {
if (_trackTintColor != trackTintColor) {
_trackTintColor = trackTintColor;
self.trackCircleLayer.strokeColor = trackTintColor.CGColor;
}
}
- (void)setGaugeTintColor:(UIColor *)gaugeTintColor {
if (_gaugeTintColor != gaugeTintColor) {
_gaugeTintColor = gaugeTintColor;
self.gaugeCircleLayer.strokeColor = gaugeTintColor.CGColor;
// ADDED
self.capLayer.fillColor = gaugeTintColor.CGColor;
// END ADDED
}
}
- (void)setTrackWidth:(CGFloat)trackWidth {
if (_trackWidth != trackWidth) {
_trackWidth = trackWidth;
self.trackCircleLayer.lineWidth = trackWidth;
[self.layer layoutSublayers];
}
}
- (void)setGaugeWidth:(CGFloat)gaugeWidth {
if (_gaugeWidth != gaugeWidth) {
_gaugeWidth = gaugeWidth;
self.gaugeCircleLayer.lineWidth = gaugeWidth;
[self.layer layoutSublayers];
}
}
- (void)setTextColor:(UIColor *)textColor {
if (_textColor != textColor) {
_textColor = textColor;
self.valueTextLabel.textColor = textColor;
}
}
- (void)setFont:(UIFont *)font {
if (_font != font) {
_font = font;
self.valueTextLabel.font = font;
}
}
- (void)setGaugeStyle:(CHCircleGaugeStyle)gaugeStyle {
if (_gaugeStyle != gaugeStyle) {
_gaugeStyle = gaugeStyle;
[self.layer layoutSublayers];
}
}
#pragma mark - Circle Shapes
- (CAShapeLayer *)trackCircleLayer {
if (_trackCircleLayer == nil) {
_trackCircleLayer = [CAShapeLayer layer];
_trackCircleLayer.lineWidth = self.trackWidth;
_trackCircleLayer.fillColor = [UIColor clearColor].CGColor;
_trackCircleLayer.strokeColor = self.trackTintColor.CGColor;
_trackCircleLayer.path = [self insideCirclePath].CGPath;
// ADDED
_capLayer = [CAShapeLayer layer];
_capLayer.shadowColor = [UIColor blackColor].CGColor;
_capLayer.shadowRadius = 8.0;
_capLayer.shadowOpacity = 0.9;
_capLayer.shadowOffset = CGSizeMake(0, 0);
_capLayer.fillColor = self.gaugeTintColor.CGColor;
_capLayer.path = [self capPathForValue:self.value].CGPath;
[_trackCircleLayer addSublayer:_capLayer];
// END ADDED
}
return _trackCircleLayer;
}
- (CAShapeLayer *)gaugeCircleLayer {
if (_gaugeCircleLayer == nil) {
_gaugeCircleLayer = [CAShapeLayer layer];
_gaugeCircleLayer.lineWidth = self.gaugeWidth;
_gaugeCircleLayer.fillColor = [UIColor clearColor].CGColor;
_gaugeCircleLayer.strokeColor = self.gaugeTintColor.CGColor;
_gaugeCircleLayer.strokeStart = 0.0f;
_gaugeCircleLayer.strokeEnd = self.value;
_gaugeCircleLayer.path = [self circlPathForCurrentGaugeStyle].CGPath;
}
return _gaugeCircleLayer;
}
// ADDED
- (UIBezierPath *)capPathForValue:(float)value {
CGPoint arcCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
CGFloat radius = CGRectGetWidth(self.bounds) / 2.0f;
float angle = value * 360.0;
float x = radius * sin(angle*M_PI/180.0);
float y = radius * cos(angle*M_PI/180.0);
CGPoint capArcCenter = CGPointMake(arcCenter.x + x, arcCenter.y - y);
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:capArcCenter
radius:self.gaugeWidth*_capLayer.shadowRadius / 2.0f
startAngle:(3.0f * M_PI_2)
endAngle:(3.0f * M_PI_2) + (2.0f * M_PI)
clockwise:YES];
return path;
}
// END ADDED
- (UIBezierPath *)circlPathForCurrentGaugeStyle {
switch (self.gaugeStyle) {
case CHCircleGaugeStyleInside: {
return [self insideCirclePath];
}
case CHCircleGaugeStyleOutside: {
return [self outsideCirclePath];
}
default: {
return nil;
}
}
}
- (UIBezierPath *)insideCirclePath {
CGPoint arcCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:arcCenter
radius:CGRectGetWidth(self.bounds) / 2.0f
startAngle:(3.0f * M_PI_2)
endAngle:(3.0f * M_PI_2) + (2.0f * M_PI)
clockwise:YES];
return path;
}
- (UIBezierPath *)outsideCirclePath {
CGPoint arcCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
CGFloat radius = (CGRectGetWidth(self.bounds) / 2.0f) + (self.trackWidth / 2.0f) + (self.gaugeWidth / 2.0f);
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:arcCenter
radius:radius
startAngle:(3.0f * M_PI_2)
endAngle:(3.0f * M_PI_2) + (2.0f * M_PI)
clockwise:YES];
return path;
}
#pragma mark - Text Label
- (UILabel *)valueTextLabel {
if (_valueTextLabel == nil) {
_valueTextLabel = [[UILabel alloc] init];
[_valueTextLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
_valueTextLabel.textAlignment = NSTextAlignmentCenter;
_valueTextLabel.attributedText = [self formattedStringForValue:self.value];
}
return _valueTextLabel;
}
- (NSAttributedString *)formattedStringForValue:(CGFloat)value {
NSAttributedString *valueString;
NSDictionary *stringAttributes = #{
NSForegroundColorAttributeName : self.textColor,
NSFontAttributeName : self.font
};
switch (self.state) {
case CHCircleGaugeViewStateNA: {
valueString = [[NSAttributedString alloc] initWithString:self.notApplicableString attributes:stringAttributes];
break;
}
case CHCircleGaugeViewStatePercentSign: {
valueString = [[NSAttributedString alloc] initWithString:self.noDataString attributes:stringAttributes];
break;
}
case CHCircleGaugeViewStateScore: {
NSString *suffix = self.unitsString ? self.unitsString : #"";
valueString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:#"%.0f %#", value * 100.0f, suffix]
attributes:stringAttributes];
break;
}
default: {
ALog(#"Missing gauge state.");
break;
}
}
return valueString;
}
#pragma mark - KVO
// Handling KVO notifications for the value property, since
// we're proxying with the setValue:animated: method.
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:NSStringFromSelector(#selector(value))]) {
return NO;
} else {
return [super automaticallyNotifiesObserversForKey:key];
}
}
#pragma mark - CALayerDelegate
- (void)layoutSublayersOfLayer:(CALayer *)layer {
[super layoutSublayersOfLayer:layer];
if (layer == self.layer) {
self.trackCircleLayer.path = [self insideCirclePath].CGPath;
self.gaugeCircleLayer.path = [self circlPathForCurrentGaugeStyle].CGPath;
}
}
#end

UIPinchGestureRecognizer trouble

Ok, i've read a few posts on this one (ex. UIImageView Gestures (Zoom, Rotate) Question) but I can't seem to fix my problem.
I have the following setup: an SKScene, an SKNode _backgroundLayer and 9 SKSpriteNodes that are tiles that make up the background and are attached to the _backgroundLayer.
Since these 9 tiles make a 3x3 square and they are quite large, I need to be able to zoom in and look at other SKSpriteNodes that will be on top of these 9 background images.
There are two problems:
1) When I pinch to zoom in or zoom out it seems like it is zooming in/out from location (0,0) of the _backgroundLayer and not from the touch location.
2) I have added some bounds so that the user can not scroll out of the 9 background images. In general it works. However, if I zoom in then move towards the top of the 9 background images and then zoom out the bounding conditions go berserk and the user can see the black space outside the background images. I need a way to limit the amount of zooming out that the user can do depending on where he's at.
Any ideas? Thanks!
I include my code below:
#import "LevelSelectScene.h"
#import "TurtleWorldSubScene.h"
#interface LevelSelectScene ()
#property (nonatomic, strong) SKNode *selectedNode;
#end
#implementation LevelSelectScene
{
SKNode *_backgroundLayer;
}
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
/* Setup your scene here */
_backgroundLayer = [SKNode node];
_backgroundLayer.name = #"backgroundLayer";
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
[_backgroundLayer setScale:0.76];
} else if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone && IS_WIDESCREEN) {
} else {
[_backgroundLayer setScale:0.36];
}
[self addChild:_backgroundLayer];
SKTexture *backgroundTexture = [SKTexture textureWithImageNamed:#"levelSelect"];
int textureID = 0;
for (int i = 0; i<3; i++) {
for (int j = 0; j<3; j++) {
SKSpriteNode *background = [SKSpriteNode spriteNodeWithTexture:backgroundTexture];
background.anchorPoint = CGPointZero;
background.position = CGPointMake((background.size.width)*i, (background.size.height)*j);
background.zPosition = 0;
background.name = [NSString stringWithFormat:#"background%d", textureID];
textureID++;
[_backgroundLayer addChild:background];
}
}
[TurtleWorldSubScene displayTurtleWorld:self];
}
return self;
}
- (void)didMoveToView:(SKView *)view {
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanFrom:)];
[[self view] addGestureRecognizer:panGestureRecognizer];
//UITapGestureRecognizer * tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
// [self.view addGestureRecognizer:tapRecognizer];
UIPinchGestureRecognizer *pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handlePinch:)];
[[self view] addGestureRecognizer:pinchGestureRecognizer];
}
- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
CGPoint touchLocation = [recognizer locationInView:recognizer.view];
touchLocation = [self convertPointFromView:touchLocation];
SKNode *node = [self nodeAtPoint:touchLocation];
_selectedNode = node;
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:recognizer.view];
translation = CGPointMake(translation.x, -translation.y);
CGPoint initialPosition = CGPointAdd(_backgroundLayer.position, translation);
_backgroundLayer.position = [self boundLayerPos:initialPosition];
[recognizer setTranslation:CGPointZero inView:recognizer.view];
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
float scrollDuration = 0.2;
CGPoint velocity = [recognizer velocityInView:recognizer.view];
CGPoint pos = [_backgroundLayer position];
CGPoint p = CGPointMultiplyScalar(velocity, scrollDuration);
CGPoint newPos = CGPointMake(pos.x + p.x, pos.y - p.y);
newPos = [self boundLayerPos:newPos];
[_backgroundLayer removeAllActions];
SKAction *moveTo = [SKAction moveTo:newPos duration:scrollDuration];
[moveTo setTimingMode:SKActionTimingEaseOut];
[_backgroundLayer runAction:moveTo];
}
}
- (void)handlePinch:(UIPinchGestureRecognizer *) recognizer
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
if(_backgroundLayer.xScale*recognizer.scale < 0.76) {
//SKSpriteNode *backgroundTile = (SKSpriteNode *)[_backgroundLayer childNodeWithName:#"background0"];
[_backgroundLayer setScale:0.76];
} else if(_backgroundLayer.xScale*recognizer.scale > 2) {
[_backgroundLayer setScale:2.0];
} else {
[_backgroundLayer runAction:[SKAction scaleBy:recognizer.scale duration:0]];
}
} else if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone && IS_WIDESCREEN) {
} else {
if(_backgroundLayer.xScale*recognizer.scale < 0.36) {
[_backgroundLayer setScale:0.36];
} else if(_backgroundLayer.xScale*recognizer.scale > 2) {
[_backgroundLayer setScale:2.0];
} else {
[_backgroundLayer runAction:[SKAction scaleBy:recognizer.scale duration:0]];
}
}
recognizer.scale = 1;
}
- (CGPoint)boundLayerPos:(CGPoint)newPos {
SKSpriteNode *backgroundTile = (SKSpriteNode *)[_backgroundLayer childNodeWithName:#"background0"];
CGPoint retval = newPos;
retval.x = MIN(retval.x, 0);
retval.x = MAX(retval.x, -(backgroundTile.size.width*_backgroundLayer.xScale*3)+self.size.width);
retval.y = MIN(retval.y, 0);
retval.y = MAX(retval.y, -(backgroundTile.size.height*_backgroundLayer.xScale*3)+self.size.height);
return retval;
}

creating a line between two UIlabels using the long press gesture recognizer

i am developing a ER diagram editor, i have a bunch of draggable UILabels but all of them have the same name. i want to be able to create a line between two UIlabels when both are pressed together using the long press gesture recognizer. any help will be most appreciated
You can create your long press gesture on the superview shared by these two labels, e.g.:
UILongPressGestureRecognizer *twoTouchLongPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:#selector(handleLongPress:)];
twoTouchLongPress.numberOfTouchesRequired = 2;
[self.view addGestureRecognizer:twoTouchLongPress];
You can then write a gesture handler:
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateBegan)
{
CGPoint location0 = [gesture locationOfTouch:0 inView:gesture.view];
CGPoint location1 = [gesture locationOfTouch:1 inView:gesture.view];
if ((CGRectContainsPoint(self.label0.frame, location0) && CGRectContainsPoint(self.label1.frame, location1)) ||
(CGRectContainsPoint(self.label1.frame, location0) && CGRectContainsPoint(self.label0.frame, location1)))
{
NSLog(#"success; draw your line");
}
else
{
NSLog(#"failure; don't draw your line");
}
}
}
In the updated comments, you suggest that you're creating a local UILabel variable, and then adding the resulting label to the view. That's fine, but you really want to maintain a backing model, that captures what you're doing in the view. For simplicity's sake, let me assume that you'll have array of these labels, e.g.:
#property (nonatomic, strong) NSMutableArray *labels;
Which you then initialize at some point (e.g. viewDidLoad):
self.labels = [[NSMutableArray alloc] init];
Then as you add labels to your view, add a reference to them in your array:
UILabel *label = [[UILabel alloc]initWithFrame:CGRectMake(xVal, yVal, 200.0f, 60.0f)];
label.text = sentence;
label.layer.borderColor = [UIColor blueColor].CGColor;
label.layer.borderWidth = 0.0;
label.backgroundColor = [UIColor clearColor];
label.font = [UIFont systemFontOfSize:19.0f];
[self.view addSubview:label];
[self.labels addObject:label];
Then, your gesture can do something like:
- (UILabel *)labelForLocation:(CGPoint)location
{
for (UILabel *label in self.labels)
{
if (CGRectContainsPoint(label.frame, location))
return label; // if found one, return that `UILabel`
}
return nil; // if not, return nil
}
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateBegan)
{
CGPoint location0 = [gesture locationOfTouch:0 inView:gesture.view];
CGPoint location1 = [gesture locationOfTouch:1 inView:gesture.view];
UILabel *label0 = [self labelForLocation:location0];
UILabel *label1 = [self labelForLocation:location1];
if (label0 != nil && label1 != nil && label0 != label1)
{
NSLog(#"success; draw your line");
}
else
{
NSLog(#"failure; don't draw your line");
}
}
}
Frankly, I'd rather see this backed by a proper model, but that's a more complicated conversation beyond the scope of a simple Stack Overflow answer. But hopefully the above gives you an idea of what it might look like. (BTW, I just typed in the above without assistance of Xcode, so I'll apologize in advance for typos.)

iOS: Multiple intersecting views

I am making a day view calendar just like the native iPhone calendar. I am trying to position the tiles the same as in the native calendar, side by side, if they are the same size and same time.
However, I can only figure out how to do it to 2 tiles and not multiple tiles. In the attached image I have 4 tiles. One that expands slightly into the other 3. I then have the first tile on the far left and the second tile just after the first one. Now I need to figure out how to add the additional tiles?
How would I do this for more than 2 tiles?
About the image: If you can't see it the 3rd tile is ontop of the 2nd tile (you can see it is a bit darker since they are on top of each other.
- (void)layoutSubviews
{
// Set the main
for (UIView *view in self.subviews) {
APCalendarDayTile *tile = (APCalendarDayTile *)view;
CGFloat startPos = [APCalendarCurrentDayView yAxisForTime:[APCalendarCurrentDayView minutesToTime:tile.appointment.startDate]];
CGFloat endPos = [APCalendarCurrentDayView yAxisForTime:[APCalendarCurrentDayView minutesToTime:tile.appointment.endDate]];
tile.frame = CGRectMake(kLeftSideBuffer, startPos, (self.bounds.size.width - kLeftSideBuffer) , endPos - startPos);
tile.backgroundColor = [UIColor colorWithHexString:tile.appointment.appointmentColor];
}
for (UIView *view in self.subviews) {
APCalendarDayTile *tile = (APCalendarDayTile *)view;
if ([self viewIntersectsWithAnotherView:tile]) {
}
}
}
- (BOOL)viewIntersectsWithAnotherView:(UIView*)selectedView{
NSArray *subViewsInView=[self subviews];// I assume self is a subclass
// of UIViewController but the view can be
//any UIView that'd act as a container
//for all other views.
for (UIView *theView in subViewsInView){
if (![selectedView isEqual:theView]) {
if(CGRectIntersectsRect(selectedView.frame, theView.frame)) {
if ((selectedView.frame.origin.y == theView.frame.origin.y) && (selectedView.frame.size.height == theView.frame.size.height)) {
if (theView.frame.size.width == self.bounds.size.width - kLeftSideBuffer) {
theView.frame = CGRectMake(theView.frame.origin.x, selectedView.frame.origin.y, theView.frame.size.width / 2, selectedView.frame.size.height);
}
selectedView.frame = CGRectMake(theView.frame.origin.x + theView.frame.size.width, selectedView.frame.origin.y, theView.frame.size.width, selectedView.frame.size.height);
return YES;
}
}
}
}
return NO;
}
It appears that your test
if ((selectedView.frame.origin.y == theView.frame.origin.y) && (selectedView.frame.size.height == theView.frame.size.height))
Is only applied to views of equal y origin and height. I would solve this problem using the following pseudo code:
initialize an empty arranged subviews array
initialize a nil previous subview
for every subview
if the subview intersects with the previous subview
ensure the subview and the previous subview are added to the arranged subviews array
else if the arranged subviews array is not empty
arrange the subviews in the array across the width of their superview
empty the arranged subview array
Ok,
I sorta took SaltyMule's approach however, his pseudo code didn't make sense in the if / else.
- (void)layoutSubviews
{
// Set the main
for (UIView *view in self.subviews) {
APCalendarDayTile *tile = (APCalendarDayTile *)view;
CGFloat startPos = [APCalendarCurrentDayView yAxisForTime:[APCalendarCurrentDayView minutesToTime:tile.appointment.startDate]];
CGFloat endPos = [APCalendarCurrentDayView yAxisForTime:[APCalendarCurrentDayView minutesToTime:tile.appointment.endDate]];
tile.frame = CGRectMake(kLeftSideBuffer, startPos, (self.bounds.size.width - kLeftSideBuffer) , endPos - startPos);
tile.backgroundColor = [UIColor colorWithHexString:tile.appointment.appointmentColor];
}
[sameTimeAppointments removeAllObjects];
for (UIView *view in self.subviews) {
APCalendarDayTile *tile = (APCalendarDayTile *)view;
if ([self viewIntersectsWithAnotherView:tile]) {
if ([sameTimeAppointments objectForKey:[NSString stringWithFormat:#"%f", tile.frame.origin.y]] != nil) {
NSMutableArray *tempArray = [[sameTimeAppointments objectForKey:[NSString stringWithFormat:#"%f", tile.frame.origin.y]] mutableCopy];
[tempArray addObject:tile];
[sameTimeAppointments setValue:tempArray forKey:[NSString stringWithFormat:#"%f", tile.frame.origin.y]];
} else {
[sameTimeAppointments setValue:[NSMutableArray arrayWithObject:tile] forKey:[NSString stringWithFormat:#"%f", tile.frame.origin.y]];
}
}
}
for (NSString *currentDict in sameTimeAppointments) {
NSArray *currentAppointments = [sameTimeAppointments objectForKey:currentDict];
float tileWidth = ((self.frame.size.width - kLeftSideBuffer) / [currentAppointments count]);
for (int i = 0; i < [currentAppointments count]; i++) {
APCalendarDayTile *tile = [currentAppointments objectAtIndex:i];
float xPos = 0.0 + kLeftSideBuffer;
if (i != 0) {
xPos = (((APCalendarDayTile *)[currentAppointments objectAtIndex:i - 1]).frame.origin.x + tileWidth);
}
tile.frame = CGRectMake(xPos, tile.frame.origin.y, tileWidth, tile.frame.size.height);
[self bringSubviewToFront:tile];
}
}
}
- (BOOL)viewIntersectsWithAnotherView:(UIView*)selectedView{
NSArray *subViewsInView=[self subviews];// I assume self is a subclass
// of UIViewController but the view can be
//any UIView that'd act as a container
//for all other views.
for (UIView *theView in subViewsInView){
if (![selectedView isEqual:theView]) {
if(CGRectIntersectsRect(selectedView.frame, theView.frame)) {
if ((selectedView.frame.origin.y == theView.frame.origin.y) && (selectedView.frame.size.height == theView.frame.size.height)) {
return YES;
}
}
}
}
return NO;
}