I have a view hierarchy which contains smaller views on a scroll view. Each view can have subviews in it such as buttons etc.
For some reason, buttons on the view aren't clicked; exploring this further showed that while the scroll view receives the touchBegan event, the button does not. Calling the hitTest:event: message shows that the button is not returned, even though it is within the limits.
I've included a log output describing the touch's location on the scroll view, the item returned from hitTest, the touch's location if I called locationInView: using the expected item, and the hierarchy of the expected item (with frames printed). From this output I can deduce that the button should have been called...
Can anyone explain this? Am I missing something?
touched ({451, 309}) on <VCViewContainersView: 0x4b31ee0; frame = (0 0; 748 1024); transform = [0, 1, -1, 0, 0, 0]; autoresize = W+H; layer = <CALayer: 0x4b32130>> (location in expected item: {17, 7.5})
expected touched item is:
view: <UIButtonLabel: 0x482b920; frame = (32 5; 36 19); text = 'Click'; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x4831370>>, layer transform: [1, 0, 0, 1, 0, 0]
view: <UIRoundedRectButton: 0x482c100; frame = (50 50; 100 30); opaque = NO; layer = <CALayer: 0x482c450>>, layer transform: [1, 0, 0, 1, 0, 0]
view: <UIImageView: 0x480f290; frame = (0 0; 320 255); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x480e840>>, layer transform: [1, 0, 0, 1, 0, 0]
view: <VCViewContainer: 0x4b333c0; frame = (352 246.5; 320 471.75); layer = <CALayer: 0x4b33d50>>, layer transform: [1, 0, 0, 1, 0, 0]
view: <UIScrollView: 0x4b32600; frame = (0 0; 1024 748); clipsToBounds = YES; autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x4b32780>>, layer transform: [1, 0, 0, 1, 0, 0]
view: <VCViewsContainerView: 0x4b31ee0; frame = (0 0; 748 1024); transform = [0, 1, -1, 0, 0, 0]; autoresize = W+H; layer = <CALayer: 0x4b32130>>, layer transform: [0, 1, -1, 0, 0, 0]
view: <UIWindow: 0x4b1d590; frame = (0 0; 768 1024); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x4b1d6d0>>, layer transform: [1, 0, 0, 1, 0, 0]
Update: Other than the UIWindow and VCViewsContainerView, all views are created programmatically using initWithFrame: or in the case of the button, buttonWithType:. The VCViewContainer is initialized using CGRectZero and when the UIImageView is created, its frame is set to the image's size + additional space for labels on the bottom of it.
Update 2: When calling [self.layer hitTest:location] with the same location, I get the layer of the correct view! What's going on here...?
hitTest:withEvent: starts at the window. Each view tests its subviews before testing itself, and so on, recursively. If a view's userInteractionEnabled is NO, however, it returns nil from hitTest:withEvent:, without testing its subviews. Such a view is certainly hit-tested, but it immediately replies that neither it nor any of its subviews is the hit view.
You UIScrollView has its userInteractinonEnabled set to NO. Thus, when the VCViewContainersView tests its subview the UIScrollView, and the UIScrollView returns nil because its userInteractionEnabled is NO, the VCViewContainersView uses pointInside:withEvent: on itself, finds that the touch is within itself, and returns itself as the hit view (and the search ends). This explains the result you are getting.
The reason this doesn't happen when you do the hit-test by way of the layers is that layers are not touchable and know nothing about the rules for touches, so they ignore userInteractionEnabled, which is a view feature, not a layer feature. Layer hit-testing is sort of a kludge, intended only for when a view contains a whole layer hierarchy (without a view hierarchy) and you want to simulate that a particular layer is touchable. The docs do tell you that the logic is different for a layer's hitTest: than it is for a view's hitTest:withEvent:, though they fail to explain exactly how.
I don't know why you have set your scroll view to be non-touchable, but if that's important to you, you can override hitTest:withEvent: in a UIScrollView subclass so that it tests its subviews but returns nil if all of them return nil.
You might want to try subclassing your UIScrollView to receive touches on objects within it.
By default your touches will be received by your UIScrollView, so if you want to receive touches on objects inside of it, you need to set that up explicitly.
Here's a working subclass to do just that. Thanks to the SO community for this, because I believe I originally found something like this here, just can't dig up the original post now.
Here's the .h:
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#interface TouchScroller : UIScrollView
{
}
#end
And the .m:
#import "TouchScroller.h"
#implementation TouchScroller
- (id)initWithFrame:(CGRect)frame
{
return [super initWithFrame:frame];
}
- (void) touchesEnded: (NSSet *) touches withEvent: (UIEvent *) event
{
// If not dragging, send event to next responder
if (!self.dragging)
[self.nextResponder touchesEnded: touches withEvent:event];
else
[super touchesEnded: touches withEvent: event];
}
#end
That should do it.
If I understand view stack correctly then your button is subview of some UIImageView - it has userInteractionEnabled property set to NO (by default) - so the image view and all its subviews won't receive any touch events.
Setting image view's userInteractionEnabled property to YES must solve the problem
Related
I'm creating a sudoku style swift app using 81 buttons (organized in stack views). I'm referencing the buttons using their tag value 0 to 80 which aligns with the arrays I'm using to store the values. But, the first button is causing problems, I think because there are UILayoutGuide objects that also have tag 0.
This solution was working but I've just upgraded to Xcode Version 8.0 (8A218a) and converted to swift 3.
I added the following code to the viewDidLoad function of the view controller:
// Get the subviews of the view
var subviews = view.subviews
// Return if there are no subviews
if subviews.count == 0 {
return
}
for subview : AnyObject in subviews{
// Do what you want to do with the subview
print("\(subview.tag) - \(subview)")
}
This produces the following log:
Optional(200) - UIImageView: 0x7ff3e240f3b0; frame = (-4 0; 383 667); autoresize = RM+BM; userInteractionEnabled = NO; tag = 200; layer =
Optional(200) - UIStackView: 0x7ff3e240f590; frame = (7 103; 360 360); opaque = NO; autoresize = RM+BM; tag = 200; layer = CATransformLayer: 0x6100002316a0
Optional(200) - UIImageView: 0x7ff3e24111b0; frame = (0 47; 375 436); autoresize = RM+BM; userInteractionEnabled = NO; tag = 200; layer = CALayer: 0x6100002344e0
Optional(200) - UIToolbar: 0x7ff3e2637ff0; frame = (0 623; 375 44); opaque = NO; autoresize = RM+BM; tag = 200; layer = CALayer: 0x6000000343e0
Optional(200) - UIImageView: 0x7ff3e26015f0; frame = (63 228; 248 211); autoresize = RM+BM; userInteractionEnabled = NO; tag = 200; layer = CALayer: 0x600000034ea0
Optional(200) - UILabel: 0x7ff3e263e2e0; frame = (117 313; 141 41); text = 'Nice Work'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; tag = 200; layer = _UILabelLayer: 0x600000093060
Optional(200) - UIStackView: 0x7ff3e263e750; frame = (127 491; 120 120); opaque = NO; autoresize = RM+BM; tag = 200; layer = CATransformLayer: 0x600000035520
Optional(0) - _UILayoutGuide: 0x7ff3e263ee80; frame = (0 0; 0 0); hidden = YES; layer = CALayer: 0x600000035bc0
Optional(0) - _UILayoutGuide: 0x7ff3e263f030; frame = (0 0; 0 0); hidden = YES; layer = CALayer: 0x600000035c60
So the last 2 output lines are my suspected UILayoutGuide objects. I didn't code them and they are not objects in the hierarchy. How do I see them/get rid of them?
If you just want to prevent the loop from adding the _UILayoutGuide (which is of class UILayoutSupport), do this:
// Get the subviews of the view
var subviews = view.subviews
// Return if there are no subviews
if subviews.count == 0 {
return
}
for subview : AnyObject in subviews{
// Do what you want to do with the subview
print("\(subview.tag) - \(subview)")
// Check for whether is it a UILayout class or not
if(subview is UILayoutSupport){
//This section is executed when UILayoutGuide will come into play
print("Don't add it to the stack")
}else{
// Write your code in this section
}
}
you can also print your stack, check for UILayoutGuide in it and remove it.
Simple answer is that the UILayoutGuides don't have tags. When you print the subview.tag its showing tag = 0 when there is no tag. So the issue with button 0 is not related to the UILayoutGuide.
I didn't figure out what the issue was with the tag 0 button but recoded to avoid using tag 0. Not exactly fixed but the solution works.
Typically, the scrollView's content view is a rectangle. But I would like to implement that is not a rectangle.... For example....
The yellow, Grid 6 is the current position...Here is the example flow:
User swipe to left. (cannot scroll to left) Current: 6.
User swipe to right. (scroll to right) Current: 7.
User swipe to down. (scroll to down) Current: 8.
User swipe to down. (cannot scroll to down) Current: 8.
As you can see, the Content view of the scrollView is not rectangle. Any ideas on how to implement it? Thanks.
This is an interesting idea to implement. I can think of a few approaches that might work. I tried out one, and you can find my implementation in my github repository here. Download it and try it out for yourself.
My approach is to use a normal UIScrollView, and constrain its contentOffset in the delegate's scrollViewDidScroll: method (and a few other delegate methods).
Preliminaries
First, we're going to need a constant for the page size:
static const CGSize kPageSize = { 200, 300 };
And we're going to need a data structure to hold the current x/y position in the grid of pages:
typedef struct {
int x;
int y;
} MapPosition;
We need to declare that our view controller conforms to the UIScrollViewDelegate protocol:
#interface ViewController () <UIScrollViewDelegate>
#end
And we're going to need instance variables to hold the grid (map) of pages, the current position in that grid, and the scroll view:
#implementation ViewController {
NSArray *map_;
MapPosition mapPosition_;
UIScrollView *scrollView_;
}
Initializing the map
My map is just an array of arrays, with a string name for each accessible page and [NSNull null] at inaccessible grid positions. I'll initialize the map from my view controller's init method:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
[self initMap];
}
return self;
}
- (void)initMap {
NSNull *null = [NSNull null];
map_ = #[
#[ #"1", null, #"2"],
#[ #"3", #"4", #"5" ],
#[ null, #"6", #"7" ],
#[ null, null, #"8" ],
];
mapPosition_ = (MapPosition){ 0, 0 };
}
Setting up the view hierarchy
My view hierarchy will look like this:
top-level view (gray background)
scroll view (transparent background)
content view (tan background)
page 1 view (white with a shadow)
page 2 view (white with a shadow)
page 3 view (white with a shadow)
etc.
Normally I'd set up some of my views in a xib, but since it's hard to show xibs in a stackoverflow answer, I'll do it all in code. So in my loadView method, I first set up a “content view” that will live inside the scroll view. The content view will contain a subviews for each page:
- (void)loadView {
UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, [map_[0] count] * kPageSize.width, map_.count * kPageSize.height)];
contentView.backgroundColor = [UIColor colorWithHue:0.1 saturation:0.1 brightness:0.9 alpha:1];
[self addPageViewsToContentView:contentView];
Then I'll create my scroll view:
scrollView_ = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, kPageSize.width, kPageSize.height)];
scrollView_.delegate = self;
scrollView_.bounces = NO;
scrollView_.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin
| UIViewAutoresizingFlexibleRightMargin
| UIViewAutoresizingFlexibleTopMargin
| UIViewAutoresizingFlexibleBottomMargin);
I add the content view as a subview of the scroll view and set up the scroll view's content size and offset:
scrollView_.contentSize = contentView.frame.size;
[scrollView_ addSubview:contentView];
scrollView_.contentOffset = [self contentOffsetForCurrentMapPosition];
Finally, I create my top-level view and give it the scroll view as a subview:
UIView *myView = [[UIView alloc] initWithFrame:scrollView_.frame];
[myView addSubview:scrollView_];
myView.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1];
self.view = myView;
}
Here's how I compute the scroll view's content offset for the current map position, and for any map position:
- (CGPoint)contentOffsetForCurrentMapPosition {
return [self contentOffsetForMapPosition:mapPosition_];
}
- (CGPoint)contentOffsetForMapPosition:(MapPosition)position {
return CGPointMake(position.x * kPageSize.width, position.y * kPageSize.height);
}
To create subviews of the content view for each accessible page, I loop over the map:
- (void)addPageViewsToContentView:(UIView *)contentView {
for (int y = 0, yMax = map_.count; y < yMax; ++y) {
NSArray *mapRow = map_[y];
for (int x = 0, xMax = mapRow.count; x < xMax; ++x) {
id page = mapRow[x];
if (![page isKindOfClass:[NSNull class]]) {
[self addPageViewForPage:page x:x y:y toContentView:contentView];
}
}
}
}
And here's how I create each page view:
- (void)addPageViewForPage:(NSString *)page x:(int)x y:(int)y toContentView:(UIView *)contentView {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectInset(CGRectMake(x * kPageSize.width, y * kPageSize.height, kPageSize.width, kPageSize.height), 10, 10)];
label.text = page;
label.textAlignment = NSTextAlignmentCenter;
label.layer.shadowOffset = CGSizeMake(0, 2);
label.layer.shadowRadius = 2;
label.layer.shadowOpacity = 0.3;
label.layer.shadowPath = [UIBezierPath bezierPathWithRect:label.bounds].CGPath;
label.clipsToBounds = NO;
[contentView addSubview:label];
}
Constraining the scroll view's contentOffset
As the user moves his finger around, I want to prevent the scroll view from showing an area of its content that doesn't contain a page. Whenever the scroll view scrolls (by updating its contentOffset), it sends scrollViewDidScroll: to its delegate, so I can implement scrollViewDidScroll: to reset the contentOffset if it goes out of bounds:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint contentOffset = scrollView_.contentOffset;
First, I want to constrain contentOffset so the user can only scroll horizontally or vertically, not diagonally:
CGPoint constrainedContentOffset = [self contentOffsetByConstrainingMovementToOneDimension:contentOffset];
Next, I want to constrain contentOffset so that it only shows parts of the scroll view that contain pages:
constrainedContentOffset = [self contentOffsetByConstrainingToAccessiblePoint:constrainedContentOffset];
If my constraints modified contentOffset, I need to tell the scroll view about it:
if (!CGPointEqualToPoint(contentOffset, constrainedContentOffset)) {
scrollView_.contentOffset = constrainedContentOffset;
}
Finally, I update my idea of the current map position based on the (constrained) contentOffset:
mapPosition_ = [self mapPositionForContentOffset:constrainedContentOffset];
}
Here's how I compute the map position for a given contentOffset:
- (MapPosition)mapPositionForContentOffset:(CGPoint)contentOffset {
return (MapPosition){ roundf(contentOffset.x / kPageSize.width),
roundf(contentOffset.y / kPageSize.height) };
}
Here's how I constrain the movement to just horizontal or vertical and prevent diagonal movement:
- (CGPoint)contentOffsetByConstrainingMovementToOneDimension:(CGPoint)contentOffset {
CGPoint baseContentOffset = [self contentOffsetForCurrentMapPosition];
CGFloat dx = contentOffset.x - baseContentOffset.x;
CGFloat dy = contentOffset.y - baseContentOffset.y;
if (fabsf(dx) < fabsf(dy)) {
contentOffset.x = baseContentOffset.x;
} else {
contentOffset.y = baseContentOffset.y;
}
return contentOffset;
}
Here's how I constrain contentOffset to only go where there are pages:
- (CGPoint)contentOffsetByConstrainingToAccessiblePoint:(CGPoint)contentOffset {
return [self isAccessiblePoint:contentOffset]
? contentOffset
: [self contentOffsetForCurrentMapPosition];
}
Deciding whether a point is accessible turns out to be the tricky bit. It's not enough to just round the point's coordinates to the nearest potential page center and see if that rounded point represents an actual page. That would, for example, let the user drag left/scroll right from page 1, revealing the empty space between pages 1 and 2, until page 1 is half off the screen. We need to round the point down and up to potential page centers, and see if both rounded points represent valid pages. Here's how:
- (BOOL)isAccessiblePoint:(CGPoint)point {
CGFloat x = point.x / kPageSize.width;
CGFloat y = point.y / kPageSize.height;
return [self isAccessibleMapPosition:(MapPosition){ floorf(x), floorf(y) }]
&& [self isAccessibleMapPosition:(MapPosition){ ceilf(x), ceilf(y) }];
}
Checking whether a map position is accessible means checking that it's in the bounds of the grid and that there's actually a page at that position:
- (BOOL)isAccessibleMapPosition:(MapPosition)p {
if (p.y < 0 || p.y >= map_.count)
return NO;
NSArray *mapRow = map_[p.y];
if (p.x < 0 || p.x >= mapRow.count)
return NO;
return ![mapRow[p.x] isKindOfClass:[NSNull class]];
}
Forcing the scroll view to rest at page boundaries
If you don't need to force the scroll view to rest at page boundaries, you can skip the rest of this. Everything I described above will work without the rest of this.
I tried setting pagingEnabled on the scroll view to force it to come to rest at page boundaries, but it didn't work reliably, so I have to enforce it by implementing more delegate methods.
We'll need a couple of utility functions. The first function just takes a CGFloat and returns 1 if it's positive and -1 otherwise:
static int sign(CGFloat value) {
return value > 0 ? 1 : -1;
}
The second function takes a velocity. It returns 0 if the absolute value of the velocity is below a threshold. Otherwise, it returns the sign of the velocity:
static int directionForVelocity(CGFloat velocity) {
static const CGFloat kVelocityThreshold = 0.1;
return fabsf(velocity) < kVelocityThreshold ? 0 : sign(velocity);
}
Now I can implement one of the delegate methods that the scroll view calls when the user stops dragging. In this method, I set the targetContentOffset of the scroll view to the nearest page boundary in the direction that the user was scrolling:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
if (fabsf(velocity.x) > fabsf(velocity.y)) {
*targetContentOffset = [self contentOffsetForPageInHorizontalDirection:directionForVelocity(velocity.x)];
} else {
*targetContentOffset = [self contentOffsetForPageInVerticalDirection:directionForVelocity(velocity.y)];
}
}
Here's how I find the nearest page boundary in a horizontal direction. It relies on the isAccessibleMapPosition: method, which I already defined earlier for use by scrollViewDidScroll::
- (CGPoint)contentOffsetForPageInHorizontalDirection:(int)direction {
MapPosition newPosition = (MapPosition){ mapPosition_.x + direction, mapPosition_.y };
return [self isAccessibleMapPosition:newPosition] ? [self contentOffsetForMapPosition:newPosition] : [self contentOffsetForCurrentMapPosition];
}
And here's how I find the nearest page boundary in a vertical direction:
- (CGPoint)contentOffsetForPageInVerticalDirection:(int)direction {
MapPosition newPosition = (MapPosition){ mapPosition_.x, mapPosition_.y + direction };
return [self isAccessibleMapPosition:newPosition] ? [self contentOffsetForMapPosition:newPosition] : [self contentOffsetForCurrentMapPosition];
}
I discovered in testing that setting targetContentOffset did not reliably force the scroll view to come to rest on a page boundary. For example, in the iOS 5 simulator, I could drag right/scroll left from page 5, stopping halfway to page 4, and even though I was setting targetContentOffset to page 4's boundary, the scroll view would just stop scrolling with the 4/5 boundary in the middle of the screen.
To work around this bug, we have to implement two more UIScrollViewDelegate methods. This one is called when the touch ends:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
[scrollView_ setContentOffset:[self contentOffsetForCurrentMapPosition] animated:YES];
}
}
And this one is called when the scroll view stops decelerating:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
CGPoint goodContentOffset = [self contentOffsetForCurrentMapPosition];
if (!CGPointEqualToPoint(scrollView_.contentOffset, goodContentOffset)) {
[scrollView_ setContentOffset:goodContentOffset animated:YES];
}
}
The End
As I said at the beginning, you can download my test implementation from my github repository and try it out for yourself.
That's all, folks!
I'm assuming you're using the UIScrollView in paged mode (swipe to show an entire new screen).
With a bit jiggery-pokery you can achieve the effect you want.
The trick is to ensure that whatever square you're currently viewing, you have the UIScrollView configured so that only the visible central view, and the surrounding view that you could scroll too, are added to the scroll view (and at the correct offset). You also must ensure that the size of the scrollable content (and the current offset) is set correctly, to prevent scrolling in a direction that would take you to no content.
Example: suppose you're viewing square 6 currently. At that point, your scroll view would just have 4 views added to it: 4, 5, 6 and 7, in the correct relative offsets. And you set the content size of the scroll view to be equivelant to 2 x 2 squares size. This will prevent scrolling down or to the left (where there are no tiles) but will allow scrolling in the correct direction.
You'll need your delegate to detect scrollViewDidEndDecelerating:. In that instance, you then have to set up your views, content offset, and content size as described above, for the new location.
as topic... I want a simple to change the color of AlertView, but after googling, I find it is trouble, anyway, can give a tip on this problem ?
I also trace the UIAlertView, the structure will be like as follows :
CAlertView: 0x6463050; baseClass = UIAlertView; frame = (3.8 175.1; 312.4 129.8); transform = [1.1, 0, 0, 1.1, 0, 0]; opaque = NO; tag = -1; animations = { transform=<CABasicAnimation: 0x616cfb0>; opacity=<CABasicAnimation: 0x616d050>; }; layer = <CALayer: 0x6407f90>>
above is base view
subViews as follows:
"<UIImageView: 0x6470dc0; frame = (0 0; 284 118); opaque = NO; autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x6470df0>>",
"<UILabel: 0x64649c0; frame = (12 15; 260 0); text = ''; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x6444490>>",
"<UILabel: 0x6454180; frame = (12 22; 260 21); text = 'Submit successfully!'; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x6453660>>",
"<UIThreePartButton: 0x64690f0; frame = (11 59; 262 43); opaque = NO; tag = 1; layer = <CALayer: 0x6468800>>"
I think that the UIImageView is the background view, I try to change the color of the view, but it does not effect the color of the UIAlertView...
anyone can share a good and simple way to change the color of the alert view ?
thanks for your time .
Regards
This is app going to be submitted to the app store? If so, I can almost guarentee that Apple will reject your app on Human Interface Guidelines.
You would probably be better creating your own custom version of a UIAlertView and showing that instead (you would also have a lot more control over the appearance and behaviour of it).
My goal is simple; I want to create a program that displays an UIImage, and when swiped from bottom to top, displays another UIImage. The images here could be a happy face/sad face. The sad face should be the starting point, the happy face the end point. When swiping your finger the part below the finger should be showing the happy face.
So far I tried solving this with the frame and bounds properties of the UIImageview I used for the happy face image.
What this piece of code does is wrong, because the transition starts in the center of the screen and not the bottom. Notice that the origin of both frame and bounds are at 0,0...
I have read numerous pages about frames and bounds, but I don't get it. Any help is appreciated!
The loadimages is called only once.
- (void)loadImages {
sadface = [UIImage imageNamed:#"face-sad.jpg"];
happyface = [UIImage imageNamed:#"face-happy.jpg"];
UIImageView *face1view = [[UIImageView alloc]init];
face1view.image = sadface;
[self.view addSubview:face1view];
CGRect frame;
CGRect contentRect = self.view.frame;
frame = CGRectMake(0, 0, contentRect.size.width, contentRect.size.height);
face1view.frame = frame;
face2view = [[UIImageView alloc]init];
face2view.layer.masksToBounds = YES;
face2view.contentMode = UIViewContentModeScaleAspectFill;
face2view.image = happyface;
[self.view addSubview:face2view];
frame = CGRectMake(startpoint.x, 0, contentRect.size.width, contentRect.size.height);
face2view.frame = frame;
face2view.clipsToBounds = YES;
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint movepoint = [[touches anyObject] locationInView: self.view];
NSLog(#"movepoint: %f %f", movepoint.x, movepoint.y);
face2view.bounds = CGRectMake(0, 0, 320, 480 - movepoint.y);
}
The UIImages and UIImageViews are properly disposed of in the dealloc function.
Indeed, you seem to be confused about frames and bounds. In fact, they are easy. Always remember that any view has its own coordinate system. The frame, center and transform properties are expressed in superview's coordinates, while the bounds is expressed in the view's own coordinate system. If a view doesn't have a superview (not installed into a view hierarchy yet), it still has a frame. In iOS the frame property is calculated from the view's bounds, center and transform. You may ask what the hell frame and center mean when there's no superview. They are used when you add the view to another view, allowing to position the view before it's actually visible.
The most common example when a view's bounds differ from its frame is when it is not in the upper left corner of its superview: its bounds.origin may be CGPointZero, while its frame.origin is not. Another classic example is UIScrollView, which frequently modifies its bounds.origin to make subviews scroll (in fact, modifying the origin of the coordinate system automatically moves every subview without affecting their frames), while its own frame is constant.
Back to your code. First of all, when you already have images to display in image views, it makes sense to init the views with their images:
UIImageView *face1view = [[UIImageView alloc] initWithImage: sadface];
That helps the image view to immediately size itself properly. It is not recommended to init views with -init because that might skip some important code in their designated initializer, -initWithFrame:.
Since you add face1view to self.view, you should really use its bounds rather than its frame:
face1view.frame = self.view.bounds;
Same goes for the happier face. Then in -touchesMoved:… you should either change face2view's frame to move it inside self.view or (if self.view does not contain any other subviews besides faces) modify self.view's bounds to move both faces inside it together. Instead, you do something weird like vertically stretching the happy face inside face2view. If you want the happy face to slide from the bottom of self.view, you should initially set its frame like this (not visible initially):
face2view.frame = CGRectOffset(face2view.frame, 0, CGRectGetHeight(self.view.bounds));
If you choose to swap faces by changing image views' frames (contrasted with changing self.view's bounds), I guess you might want to change both the views' frame origins, so that the sad face slides up out and the happy face slides up in. Alternatively, if you want the happy face to cover the sad one:
face2view.frame = face1view.frame;
Your problem seems to have something to do with the face2view.bounds in touchesMoved.
You are setting the bounds of this view to the rect, x:0, y:0, width:320, height:480 - y
x = 0 == left on the x axis
y = 0 == top on the y axis
So you are putting this image frame at the upper left corner, and making it fill the whole view. That's not what you want. The image simply becomes centered in this imageView.
I have a UIView inside a UIScrollView. Whenever the UIScrollView zoom changes, I want to redraw the entire UIView at the new zoom level.
In iOS < 3.2, I was doing this by resizing the UIView within the UIScrollView to make it the new size, and then setting the transform back to Identity, so that it wouldn't try to resize it further. However, with iOS >= 3.2, changing the identity also changes the UIScrollView's zoomScale property.
The result is that whenever I zoom (say 2x), I adjust the embedded UIView to be the appropriate size, and redraw it. However now (since I reset the transform to Identity), the UIScrollView thinks its once again at zoomScale 1, rather than zoomScale 2. So if I have my maxZoomScale set at 2, it will still try zooming further, which is wrong.
I thought about using the CATiledLayer, but I don't think this is sufficient for me, since I want to redraw after every zoom, not just at certain zoom thresholds like it tries to do.
Does anyone know how to do the proper redrawing of the UIView on a zoom?
Tom,
Your question is a bit old, but I came up with a solution for this, so I figured I'd put in an answer in case it helps you or anyone else. The basic trick is to reset the scroll view's zoomScale to 1, and then adjust the minimumZoomScale and maximumZoomScale so that the user can still zoom in and out as expected.
In my implementation, I've subclassed UIScrollView and set it to be its own delegate. In my subclass, I implement the two delegate methods you need for zooming (shown below). contentView is a property I added to my UIScrollView subclass that in order to give it the view that actually displays the content.
So, my init method looks something like this (kMinimumZoomScale and kMaximumZoomScale are #define's at the top of the class):
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.autoresizesSubviews = YES;
self.showsVerticalScrollIndicator = YES;
self.showsHorizontalScrollIndicator = NO;
self.bouncesZoom = YES;
self.alwaysBounceVertical = YES;
self.delegate = self;
self.minimumZoomScale = kMinimumZoomScale;
self.maximumZoomScale = kMaximumZoomScale;
}
return self;
}
Then I implement the standard UIScrollView delegate methods for zooming. My ContentView class has a property called zoomScale that tells it what scale to use for displaying its content. It uses that in its drawRect method to resize the content as appropriate.
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)aScrollView {
return contentView;
}
- (void)scrollViewDidEndZooming:(UIScrollView *)aScrollView withView:(UIView *)view atScale:(float)scale {
CGFloat oldZoomScale = contentView.zoomScale;
CGSize size = self.bounds.size;
// Figure out where the scroll view was centered so that we can
// fix up its offset after adjusting the scaling
CGPoint contentCenter = self.contentOffset;
contentCenter.x += size.width / (oldZoomScale * scale) / 2;
contentCenter.y += size.height / (oldZoomScale * scale) / 2;
CGFloat newZoomScale = scale * oldZoomScale;
newZoomScale = MAX(newZoomScale, kMinimumZoomscale);
newZoomScale = MIN(newZoomScale, kMaximumZoomscale);
// Set the scroll view's zoom scale back to 1 and adjust its minimum and maximum
// to allow the expected amount of zooming.
self.zoomScale = 1.0;
self.minimumZoomScale = kMinimumZoomScale / newZoomScale;
self.maximumZoomScale = kMaximumZoomScale / newZoomScale;
// Tell the contentView about its new scale. My contentView.zoomScale setter method
// calls setNeedsDisplay, but you could also call it here
contentView.zoomScale = newZoomScale;
// My ContentView class overrides sizeThatFits to give its expected size with
// zoomScale taken into account
CGRect newContentSize = [contentView sizeThatFits];
// update the content view's frame and the scroll view's contentSize with the new size
contentView.frame = CGRectMake(0, 0, newContentSize.width, newContentSize.height);
self.contentSize = newContentSize;
// Figure out the new contentOffset so that the contentView doesn't appear to move
CGPoint newContentOffset = CGPointMake(contentCenter.x - size.width / newZoomScale / 2,
contentCenter.y - size.height / newZoomScale / 2);
newContentOffset.x = MIN(newContentOffset.x, newContentSize.width - size.width);
newContentOffset.x = MAX(0, newContentOffset.x);
newContentOffset.y = MIN(newContentOffset.y, newContentSize.height - .size.height);
newContentOffset.y = MAX(0, newContentOffset.y);
[self setContentOffset:newContentOffset animated:NO];
}
In my case, I have an image view and on top of the image view I have several other imageviews. This is the implementation I came up with and works fine:
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(double)scale{
NSLog(#"finished zooming");
NSLog(#"position x :%f",pin1.frame.origin.x);
NSLog(#"position y :%f",pin1.frame.origin.y);
CGRect frame = pin1.frame;
frame.origin.x = pin1.frame.origin.x * scale;
frame.origin.y = pin1.frame.origin.y * scale;
pin1.frame = frame;
NSLog(#"position x new :%f",pin1.frame.origin.x);
NSLog(#"position y new:%f",pin1.frame.origin.y);
}