Apply and animate many constraints to outlet collection of uiviews - objective-c

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()
}
})
}

Related

Animating a subview with CentreY constraint

I have a UITableViewCell with two UILabels and a UISwitch. The labels are multilined and placed on top of each other. The UISwitch is centred vertically. When I turn off the switch I hide one of the labels and the height of the row changes. This causes the switch to jump though I call:
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
Here is my tableviewcell class implementation:
- (void)setBounds:(CGRect)bounds
{
[super setBounds:bounds];
self.contentView.frame = self.bounds;
}
- (void)setUpView{
self.numberLabel.text = [NSString stringWithFormat:#"Quote number %ld", [self.cellData[#"rowNumber"] integerValue]];
[self.controlSwitch setOn:[self.cellData[#"checked"] boolValue]];
[UIView animateWithDuration:0.5 animations:^{
if ([self.controlSwitch isOn]) {
self.quoteLabel.text = self.cellData[#"quote"];
self.quoteLabel.hidden = NO;
self.bottomConst.constant = 10;
}else{
self.quoteLabel.text = #"";
self.quoteLabel.hidden = YES;
self.bottomConst.constant = 0;
}
}];
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.selectionStyle = UITableViewCellSelectionStyleNone;
[self.contentView updateConstraintsIfNeeded];
[self.contentView layoutIfNeeded];
self.numberLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.numberLabel.frame);
self.quoteLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.quoteLabel.frame);
}
- (IBAction)removeQuote:(id)sender {
[self.delegate updateCell:self];
}
The constraint settings on the UITableViewCell looks like this.
When the switch is turned on, I'm simply unhide the Quote label and set the text.
Here is the link on how it looks like. Is this the default behavior or is there a way I can control the movement of the switch when the row animates?
Try this:
...
[self.contentView updateConstraintsIfNeeded];
[UIView beginAnimations:nil context:nil];
[self.contentView layoutIfNeeded];
[UIView commitAnimations];
...

UILabel stops animations

I have 5 UIImageViews getting animated down the screen. If one is pressed, and it meets the requirements, it will add 1 to your score, using this:
self.score.text = #(self.score.text.integerValue + 1).stringValue;
But when the text in the UILabel updates, all the animations stop abruptly and the remaining UIImageViews disappear. But, the animation only restarts after a few seconds, as if the images are becoming hidden. The code to animate the images and change image(They are the same for each one):
- (void) squareOneMover {
NSUInteger r = arc4random_uniform(3);
[self.squareOne setHidden:NO];
CGPoint originalPosition = self.squareOne.center;
CGPoint position = self.squareOne.center;
originalPosition.y = -55;
position.y += 790;
[self.squareOne setCenter:originalPosition];
[UIView animateWithDuration:r + 3
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
[self.squareOne setCenter:position];
}
completion:^(BOOL complete) {
if (complete) {
[self squareOneColour];
[self squareOneMover];
}
}
];
}
- (void) squareOneColour {
NSUInteger r = arc4random_uniform(5);
[self.squareOne setImage:[self.colorArray objectAtIndex:r]];
}
Anyone have a solution? And if by changing the text in a UILabel is supposed to stop animations (I don't know why they would do so) can someone provide a workaround to make keeping score possible.
Edit: I created a button that would increase the integer value of score. This means I can change the text in the UILabel manually. The animations still stopped the moment the text changed.
Edit 2: Method for the pressed event:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint touchLocation = [touch locationInView:self.view];
NSUInteger a = [self.colorArray indexOfObject:self.squareOne.image];
NSUInteger b = [self.iconColorArray indexOfObject:self.icon.image];
if ([self.squareOne.layer.presentationLayer hitTest:touchLocation]) {
_squareOne.hidden = YES;
}
if (a!=b) {
if (self.squareOne.hidden==YES) {
NSLog(#"NO");
}
}
if (a==b) {
if ([self.squareOne.layer.presentationLayer hitTest:touchLocation]) {
self.score.text = #(self.score.text.integerValue + 1).stringValue;
}
}
if ([self.squareTwo.layer.presentationLayer hitTest:touchLocation]) {
_squareTwo.hidden = YES;
}
}
Looking at Matt's answer, how would I animate the UIImageView by "changing its constraints"?
Constraints:
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-25-[btn1]-25-[btn2(==btn1)]-25-[btn3(==btn1)]-25-[btn4(==btn1)]-25-[btn5(==btn1)]-25-|" options:0 metrics:nil views:views]];
[self.view addConstraint:[NSLayoutConstraint
constraintWithItem:self.squareOne
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:0.0
constant:-55.0]];
The problem is this line:
[self.squareOne setCenter:position];
You are animating the position of squareOne by setting its position. But meanwhile you also have constraints that also position squareOne. That fact remains concealed until you change the text of the label; that triggers layout, and now the constraints all assert themselves, putting an end to everything else that was going on.
One solution is to animate the position of squareOne by changing its constraints. Now when layout is triggered, the existing constraints will match the situation because they are the only force that is positioning things.
//what all are you checking with the if's?
//I'm just curious there's a lot going on here
**//if this evaluates true
if ([self.squareOne.layer.presentationLayer hitTest:touchLocation]) {
_squareOne.hidden = YES;
}**
if (a!=b) {
if (self.squareOne.hidden==YES) {
NSLog(#"NO");
}
}
if (a==b) {
** //this will evaluate true also
if ([self.squareOne.layer.presentationLayer hitTest:touchLocation]) {
self.score.text = #(self.score.text.integerValue + 1).stringValue;
}**
}
//this works fine even when I added everything from interface builder which almost never happens
Update
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#property (strong, nonatomic)UILabel *label;
#property int tapCount;
#end
#implementation ViewController
-(void)viewDidLoad{
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]init];
[tap addTarget:self action:#selector(userTap:)];
[self.view addGestureRecognizer:tap];
}
-(UILabel*)label{
if (!_label) {
_label = [[UILabel alloc]init];
_label.text = #"taps:%3i",0;
[_label sizeToFit];
_label.center = self.view.center;
[self.view addSubview:_label];
}
return _label;
}
-(void)userTap:(UITapGestureRecognizer*)sender {
NSLog(#"tap");
self.tapCount ++;
[UIView animateWithDuration:1
animations:^{
self.label.text = [NSString stringWithFormat:#"taps:%i",self.tapCount];
self.label.center = [sender locationInView:self.view];
}
completion:nil];
}
#end
**just cleaning up the code a bit for future onlookers **
direct copy and paste only delete everything inside ViewController.m
on a new project and paste this in it's place
Instead of animating the constraints by hand, you can also have UIImageView generate constraints during animation (which does not get broken by a sibling UILabel). Like in this question.

Animating UITableViewCells one by one

I am trying animation, where the UITableView is already in place,but the subview (which itself contains the cell's content view) for each of the UITableViewCells is outside the window bounds. It is outside the screen. When the view loads, the cell animate from left to right, one by one, till the rightmost x value of the frame touches the rightmost x value of the UITableView.
The animation is such that, once first cell takes off, the second cell takes off before the first cell is finished with it's animation. Similarly, the third cell takes off before the second is finished with it's animation. However, i am not able to achieve this, and i all my rows animate all-together.
-(void)animateStatusViewCells:(SupportTableViewCell *)cell
{
__block CGPoint center;
[UIView animateWithDuration:0.55 delay:0.55 options:UIViewAnimationOptionCurveEaseIn animations:^{
center = cell.statusView.center;
center.x += (cell.frame.size.width - 20);
cell.statusView.center = center;
}completion:^(BOOL success){
NSLog(#"Translation successful!!!");
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
//[self animateStatusViewCells:cell];
});
}];
}
-(void)animateSupportTableView:(UITableView *)myTableView
{
NSMutableArray *cells = [[NSMutableArray alloc] init];
NSInteger count = [myTableView numberOfRowsInSection:0];
for (int i = 0; i < count; i++) {
[cells addObject:[myTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]]];
}
for (SupportTableViewCell *cell in cells) {
[self animateStatusViewCells:cell];
}
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self animateSupportTableView:_statusTableView];
}
Here's the initWithStyle method for the custom UITableViewCell:
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// Initialization code
_statusView = [[UIView alloc] initWithFrame:self.frame];
self.contentView.backgroundColor = [UIColor greenColor];
self.contentView.alpha = 0.7;
CGPoint cellCenter = self.center;
cellCenter.x = self.center.x - (self.frame.size.width - 20);
_statusView.center = cellCenter;
[self addSubview:_statusView];
[_statusView addSubview:self.contentView];
}
return self;
}
You need to use a different delay value for each animation. Instead of using a constant delay of .55, pass in the cell number to your animate method, and use something like:
CGFloat delay = .55 + cellNumber * cellDelay;
[UIView animateWithDuration: 0.55 delay: delay options:UIViewAnimationOptionCurveEaseIn animations:
^{
//animation code
}
];
For what its worth, this is a clean solution:
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
let trans = CATransform3DMakeTranslation(-cell.frame.size.width, 0.0, 0.0)
cell.layer.transform = trans
UIView.beginAnimations("Move", context: nil)
UIView.setAnimationDuration(0.3)
UIView.setAnimationDelay(0.1 * Double(indexPath.row))
cell.layer.transform = CATransform3DIdentity
UIView.commitAnimations()
}
try this
declare this somewhere.
int c=0;
-(void)animateStatusViewCells:(SupportTableViewCell *)cell
{
__block CGPoint center;
[UIView animateWithDuration:0.55 delay:0.55 options:UIViewAnimationOptionCurveEaseIn animations:^{
center = cell.statusView.center;
center.x += (cell.frame.size.width - 20);
cell.statusView.center = center;
}completion:^(BOOL success){
c++;
if(c<[cells count]){
[self animateStatusViewCells:[cells objectAtIndex:c]];
}
}];
}
-(void)animateSupportTableView:(UITableView *)myTableView
{
NSMutableArray *cells = [[NSMutableArray alloc] init];
NSInteger count = [myTableView numberOfRowsInSection:0];
for (int i = 0; i < count; i++) {
[cells addObject:[myTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]]];
}
[self animateStatusViewCells:[cells objectAtIndex:c]];
}

Recognize a path of user gesture

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
}
}
}
}
}

Recursive method containing UIAnimation block, how to properly release

I did a small "loader" that I can stick on the UI when I need it. Much like the UISpinnerView.
It has some logic (I simplified the code in the block for this post) that require it to be recursively called. I do it like this:
- (void) blink {
tick++;
if (tick > kNumberOfLights)
tick = 0;
UIView *lightOne = [self viewWithTag:kLightStartIndex];
UIView *lightTwo = [self viewWithTag:kLightStartIndex+1];
[UIView animateWithDuration:0.5
delay: 0.0
options: UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveEaseOut
animations:^{
if (tick == 0) {
[lightOne setAlpha:kLightOn];
[lightTwo setAlpha:kLightOff];
} else if (tick == 1) {
[lightOne setAlpha:kLightOff];
[lightTwo setAlpha:kLightOn];
}
}
completion:^(BOOL finished){
[self blink];
}];
}
The method [self blink] is called when the view is added to a super view.
There are no objects retained in the Loader class, so when I remove it in the super view
it is released. The problem is that if the animation block is running when I release the view, the completion block will call a deallocated object and cause the error:
(20380,0xa0a29540) malloc: * mmap(size=2097152) failed (error
code=12)
* error: can't allocate region
In the console and the error:
__[LoaderClass blink]_block_invoke_2
in the Debug Navigator.
How do I make sure to correctly tear down the view when it is removed from the super view?
Overriding release is usually a bad idea, but this case might be an exception to the rule.
Instance variables:
BOOL isBlinking;
BOOL releaseWhenDoneAnimating;
Initialize isBlinking = NO and releaseWhenDoneAnimating = NO.
- (void)release
{
if(!isBlinking) {
[super release];
return;
}
releaseWhenDoneAnimating = YES;
}
- (void) blink {
isBlinking = YES;
if(releaseWhenDoneAnimating) {
[super release];
return;
}
tick++;
if (tick > kNumberOfLights)
tick = 0;
UIView *lightOne = [self viewWithTag:kLightStartIndex];
UIView *lightTwo = [self viewWithTag:kLightStartIndex+1];
[UIView animateWithDuration:0.5
delay: 0.0
options: UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveEaseOut
animations:^{
if (tick == 0) {
[lightOne setAlpha:kLightOn];
[lightTwo setAlpha:kLightOff];
} else if (tick == 1) {
[lightOne setAlpha:kLightOff];
[lightTwo setAlpha:kLightOn];
}
}
completion:^(BOOL finished){
[self blink];
}];
}