views:

408

answers:

1

I'm working on a few basic apps/utilities/games with the iPhone to get a better grip on it.

I currently have a setup where a CGRect moves wherever the finger moves, however I don't want the CGRect to go outside the bounds of the view.

Original Method

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    startLocation = [touch locationInView:self];
    if (startLocation.x < 33){
     touchLocation.x = 33;
      //NSLog(@"Touch At:%f, %f", touchLocation.x, touchLocation.y);
     [self setNeedsDisplay];
    }
    if (startLocation.x > 288){
     touchLocation.x = 288;
      //NSLog(@"Touch At:%f, %f", touchLocation.x, touchLocation.y);
     [self setNeedsDisplay];
    }
    if (startLocation.y < 84){
     touchLocation.y = 84;
      //NSLog(@"Touch At:%f, %f", touchLocation.x, touchLocation.y);
     [self setNeedsDisplay];
    }
    if (startLocation.y > 460){
     touchLocation.y = 460;
      //NSLog(@"Touch At:%f, %f", touchLocation.x, touchLocation.y);
     [self setNeedsDisplay];
    }
    else {
     touchLocation = startLocation;
     [self setNeedsDisplay];
    }
}

Problem

I've tried a few different methods of this, including using && and making it just one statement. The boundaries are obviously hard coded, and I'm not sure that manually setting the touchLocation is the best way to keep it in bounds.

This all seems silly to me when I can easily self.view.bounds and get the bounding CGRect. How do I make use of bounds to do this? Is there a better way to prevent it from happening other than setting touchLocation manually? Can I somehow place a hard limit on it instead?

What about a switch statement? Would that work better than the attempted-ifs?


Attempt

I am now attempting to utilize @Brad Goss's method posted below, however it isn't working?

Touch Started: (319.000000,350.000000)
Touch Result: (319.000000,350.000000)
Boundaries calculated: X:(0.000000,288.000000) Y(0.000000,328.000000)

Touch Started is the actual starting touch. Touch Result should be the changed result.

The above log is showing it outside of the defined bounds, aka not working. I'm not going to repost the code he wrote because it is the exact same.


Attempt Problem

I've discovered the problem. This is the same type situation as above, but with logging for the individual values as they are set.

Touch Started: (293.000000,341.000000)
min x:288.000000
max x:293.000000
min y:328.000000
max y:341.000000
Touch Result: (293.000000,341.000000)
Boundaries calculated: X:(0.000000,288.000000) Y(0.000000,328.000000)

I haven't figured it out, I posted it here as soon as I discovered it. Otherwise I'd forget to update at all, ADD heh.

I'm going to go back and see exactly what's going on in each of the possible situations.


Mistake

Found another problem, the code that calculates the boundaries subtracts from the maximums, but doesn't add to the minimums. This is important as this is what the boundary is for, to stop the rect from going off the screen.

maxX = b.origin.x + b.size.width/* - [box bounds].size.width */;
minX = b.origin.x/* + [box bounds].size.width */;
maxY = b.origin.y + b.size.height/* - [box bounds].size.height */;
minY = b.origin.y?/* + [box bounds].size.height */;

With this fixed, I can continue working on the original problem.


Mistake 2

Alright, I've fixed another problem. I was adding to the y value when it was being displayed. I forgot to add it into this new code/mention it. The code now looks like this:

maxX = b.origin.x + b.size.width/* - [box bounds].size.width */;
minX = b.origin.x/* + [box bounds].size.width */;
maxY = b.origin.y + b.size.height/* - [box bounds].size.height + 20*/;
minY = b.origin.y/* + [box bounds].size.height + 20*/;

The size of the circle being displayed is inside of a 64px CGRect, so 20px was sufficient to display the circle just above the thumb. Though this is fixed, it still hasn't helped to solve our original problem.

Upper Left corner:

Touch Started: (14.000000,20.000000)
min x:14.000000
max x:32.000000
min y:20.000000
max y:84.000000
Touch Result: (32.000000,84.000000)
Boundaries calculated: X:(32.000000,288.000000) Y(84.000000,276.000000)

It works as it should, but I am confused as to why the correct values are coming out of MAX instead of MIN. Something is mixed up here.

Upper right corner:

Touch Started: (303.000000,21.000000)
min x:288.000000
max x:303.000000
min y:21.000000
max y:84.000000
Touch Result: (303.000000,84.000000)
Boundaries calculated: X:(32.000000,288.000000) Y(84.000000,276.000000)

It's preventing me from going off the top of the screen, again from MAX instead of MIN. I am still able to fly off the right, as shown above.

Bottom right corner:

Touch Started: (317.000000,358.000000)
min x:288.000000
max x:317.000000
min y:276.000000
max y:358.000000
Touch Result: (317.000000,358.000000)
Boundaries calculated: X:(32.000000,288.000000) Y(84.000000,276.000000)

It's failing on both directions. Now the actual values are coming from MAX, and the maximum values are coming from MIN. WTF?

Bottom left corner:

Touch Started: (8.000000,354.000000)
min x:8.000000
max x:32.000000
min y:276.000000
max y:354.000000
Touch Result: (32.000000,354.000000)
Boundaries calculated: X:(32.000000,288.000000) Y(84.000000,276.000000)

Predictably, it only worked for the minimum value. I'm still very confused as to what these functions are supposed to do. Insight would be much appreciated!


SOLVED!

Our long road draws to a close, thankfully. So, because I had no clue, here is what MIN and MAX do. It wasn't initially obvious to me due to the confusion that had taken place.

MAX(x,y) will output the larger of the two numbers
// MAX(13,37) = 37
MIN(x,y) will output the smallest of the two numbers
// MIN(13,37) = 13

So this is what you have to do to make use of these functions correctly in this situation:

1. Calculate the Maximum & Minimum values for X and Y.

The lowest value for each is calculated as

view.origin + keepOnScreenRect.width

The highest value of each is calculated as

view.origin + view.size.height/width - keepOnScreenRect.height/width

Don't forget the offset for the finger!
If you are doing what I did, and have the keepOnScreenRect positioned slightly above the user's finger, you should only be adding the extra offset to the Maximum value of Y, as the minimum value of Y is the bottom of the screen/view, which you are physically unable to go lower than 0 at.

If you're wondering why this is done, it is because the user cannot see through his/her finger.

2. Get the location of the touch on touchesBegan:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    startTouchLocation = [touch locationInView:self];
    NSLog(@"Touch Started: (%f,%f)", startTouchLocation.x, startTouchLocation.y);

3. Apply the mathematic constraints:

First, we get the MAX, or highest value, of both the lowest possible value according to our limits, minX, and startTouchLocation, for X. This calculates the limit for the left side of the screen. The result of this is set to currentTouchLocation.

currentTouchLocation.x = MAX(minX, startTouchLocation.x);

Second, we get the MIN, or lowest value, of both the highest possible value according to our limits, maxX, and currentTouchLocation, for X. Note that we aren't using the location of the original touch, but instead the resulting location of the MAX function we just used. This value has already been checked for the left side of the screen, so we check the right.

currentTouchLocation.x = MIN(maxX, currentTouchLocation.x);

That gives us an X location that is guaranteed to be within our limits. We then perform the same on Y.

currentTouchLocation.y = MAX(minY, startTouchLocation.y);
currentTouchLocation.y = MIN(maxY, currentTouchLocation.y);

With all this finished, we can now tell the view to redraw itself and no longer fear having our lovely little rectangle cut off in any way.

[self setNeedsDisplay];


Finished Product

/* Generates the limits based on the view's size, taking into account 
   the width/height of the rect being displayed in it. This should only
   be run once, unless the size of the view changes, in which case limits
   would have to be recalculated. The same goes for if you were to change
   the size of the displaying rect after calculation */

- (void)boundsCalculation {
    CGRect viewBounds = [self bounds];

     // calculate constraints
    maxX = viewBounds.origin.x + viewBounds.size.width - 32;
    minX = viewBounds.origin.x + 32;
    maxY = viewBounds.origin.y + viewBounds.size.height - 32;
    minY = viewBounds.origin.y + 84;
    NSLog(@"Boundaries calculated: X:(%f,%f) Y(%f,%f)", minX, maxX, minY, maxY);
}

/* Magic goodness that happens when the user touches the screen.
   Note that we don't calculate the bounds every time the user touches
   the screen, that would be silly. */

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    startTouchLocation = [touch locationInView:self];
    NSLog(@"Touch Started: (%f,%f)", startTouchLocation.x, startTouchLocation.y);

     // apply constraints
            // highest x value between the lowest allotted x value and touch
    currentTouchLocation.x = MAX(minX, startTouchLocation.x);
            // lowest x value between the highest allotted x value and touch
    currentTouchLocation.x = MIN(maxX, currentTouchLocation.x);
            // highest y value between the lowest allotted y value and touch
    currentTouchLocation.y = MAX(minY, startTouchLocation.y);
            // lowest y value between the highest allotted y value and touch
    currentTouchLocation.y = MIN(maxY, currentTouchLocation.y);
           // NSLog(@"Touch Result: (%f,%f)", currentTouchLocation.x, currentTouchLocation.y);

           // If you don't do this you won't see anything
    [self setNeedsDisplay];
}

Today was a good day for learning.

+1  A: 

You definitely want to use the bounds of it's superview to constrain it's movement.

Do not call setNeedsDisplay that often. Call it once at the end of the method. You are potentially redrawing needlessly.

I'd use something like the following to accomplish this.

// define these variables where ever you'd like
static float maxX = 0.;
static float maxY = 0.;
static float minX = 0.;
static float minY = 0.;

- (void)setupView {
    CGRect b = [self bounds];

    // calculate constraints
    maxX = b.origin.x + b.size.width/* - [box bounds].size.width */;
    minX = b.origin.x;
    maxY = b.origin.y + b.size.height/* - [box bounds].size.height */;
    minY = b.origin.y;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    startLocation = [touch locationInView:self];

    // apply constraints
    touchLocation.x = MIN(maxX, startLocation.x);
    touchLocation.x = MAX(minX, startLocation.x);
    touchLocation.y = MIN(maxY, startLocation.y);
    touchLocation.y = MAX(minY, startLocation.y);

    [self setNeedsDisplay];
}
Brad Goss
For some reason if I threw it at the end like you did, it wouldn't work. Thanks though, I'll be trying this very soon.
Sneakyness
When does setupView happen? How is it different from viewDidLoad?
Sneakyness
setupView is never called automatically. I mean't for you to place that code where ever you setup your view. So viewDidLoad is where you'd want to place it. Or call setupView from viewDidLoad, or viewWillAppear:
Brad Goss
oh alright, I was thinking it was yet another built in method. Thanks.
Sneakyness
I've figured everything out now! Thanks for the inspiration! You forgot to use the calculated value after the first function on each.
Sneakyness
Hey Sneakyness, glad you've solved the issue and happy to have helped.
Brad Goss