How to pin width of subviews relative? - objective-c

I have one view controller with two subviews.
I would like to pin both of the subviews to have the same relative width.
Like this:
Thanks!

You need to pin subviews to top, left, right sides. Also set equal width property.
You should look at this tutorial, there is a good example of equal width containers.

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

Related

iOS UICollectionViewCell resizable dashed border

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

Objactive C UISlider changing minimum track image only

Im trying to setMinimumTrackImage of the slider using an image with CAGradientLayer, lets say using blue and red colors.
what happens is that the full track gets the gradient color, its starts with red, and sliding the Thumb to the right reveals the blue.
I want the color to start from red to blue up to the Thumb, ands "stretch" as the Thumb moves.
any ideas ? I though about setting the slider.maximumValue = slider...width
and change the gradient image as I listen to the slider value change but it didn't work
I don't think you'll be successful trying to set the min track image.
Options are a completely custom slider...
or
Set the min track image to a clear image, add an imageView with the gradient image behind the slider, stretch the frame of the imageView to match the thumb movement.
Here's an example:
and the code (just a starting point... would be much better to wrap it into a subclass):
SliderTestViewController.h
//
// SliderTestViewController.h
//
// Created by Don Mag on 10/31/19.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
#interface SliderTestViewController : UIViewController
#end
NS_ASSUME_NONNULL_END
SliderTestViewController.m
//
// SliderTestViewController.m
//
// Created by Don Mag on 10/31/19.
//
#import "SliderTestViewController.h"
#import "UIImage+Utils.h"
#interface SliderTestViewController ()
#property (strong, nonatomic) UIImageView *theFakeSliderTrackImageView;
#property (strong, nonatomic) UISlider *theSlider;
#property (strong, nonatomic) NSLayoutConstraint *imgWidthConstraint;
#end
#implementation SliderTestViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// instantiate a slider
_theSlider = [UISlider new];
// instantiate an image view to use as our custom / fake "left side" of the slider track
_theFakeSliderTrackImageView = [UIImageView new];
// we want the image to stretch
_theFakeSliderTrackImageView.contentMode = UIViewContentModeScaleToFill;
// create a horizontal gradient image to use for our "left side" of the slider track
// the image will be stretched... using a width of 128 seems reasonable
UIImage *gradImg = [UIImage gradientImageWithSize:CGSizeMake(128.0, 4.0) startColor:[UIColor blueColor] endColor:[UIColor redColor] startPoint:CGPointMake(0.0, 0.0) endPoint:CGPointMake(1.0, 0.0)];
// set the gradient image to our image view
_theFakeSliderTrackImageView.image = gradImg;
// create a clear image to use for the slider's min track image
UIImage *clearImg = [UIImage imageWithColor:[UIColor clearColor] size:CGSizeMake(1.0, 1.0)];
// set min track image to clear image
[_theSlider setMinimumTrackImage:clearImg forState:UIControlStateNormal];
// set max track image if desired
// [_theSlider setMaximumTrackImage:anImage forState:UIControlStateNormal];
_theFakeSliderTrackImageView.translatesAutoresizingMaskIntoConstraints = NO;
_theSlider.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:_theFakeSliderTrackImageView];
[self.view addSubview:_theSlider];
[NSLayoutConstraint activateConstraints:#[
// constrain the slider centerY with 20-pts leading / trailing
[_theSlider.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor],
[_theSlider.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20.0],
[_theSlider.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20.0],
// constrain image view centerY to slider centerY
[_theFakeSliderTrackImageView.centerYAnchor constraintEqualToAnchor:_theSlider.centerYAnchor constant:0.0],
// constrain image view leading to slider leading
[_theFakeSliderTrackImageView.leadingAnchor constraintEqualToAnchor:_theSlider.leadingAnchor constant:0.0],
// image view height to 5-pts (adjust as desired)
[_theFakeSliderTrackImageView.heightAnchor constraintEqualToConstant:5.0],
]];
// init imageView width constraint to 0.0
_imgWidthConstraint = [_theFakeSliderTrackImageView.widthAnchor constraintEqualToConstant:0.0];
_imgWidthConstraint.active = YES;
[_theSlider addTarget:self action:#selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self updateSliderGradientImage];
}
- (void)updateSliderGradientImage {
// set "fake track" imageView width to origin.x of thumb rect (plus 2 for good measure)
CGRect trackRect = [_theSlider trackRectForBounds:_theSlider.bounds];
CGRect thumbRect = [_theSlider thumbRectForBounds:_theSlider.bounds trackRect:trackRect value:_theSlider.value];
_imgWidthConstraint.constant = thumbRect.origin.x + 2;
}
- (void)sliderChanged:(id)sender {
[self updateSliderGradientImage];
}
#end
UIImage+Utils.h
//
// UIImage+Utils.h
//
// Created by Don Mag on 10/31/19.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
#interface UIImage (Utils)
+ (nullable UIImage *)imageWithColor:(UIColor *)color size:(CGSize)size;
+ (nullable UIImage *)gradientImageWithSize:(CGSize)size startColor:(UIColor *)startColor endColor:(UIColor *)endColor startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint;
#end
NS_ASSUME_NONNULL_END
UIImage+Utils.m
//
// UIImage+Utils.m
//
// Created by Don Mag on 10/31/19.
//
#import "UIImage+Utils.h"
#implementation UIImage (Utils)
+ (UIImage *)imageWithColor:(UIColor *)color size:(CGSize)size {
if (!color || size.height < 1 || size.width < 1)
return nil;
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size];
UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull context) {
[color setFill];
[context fillRect:renderer.format.bounds];
}];
return image;
}
+ (UIImage *)gradientImageWithSize:(CGSize)size startColor:(UIColor *)startColor endColor:(UIColor *)endColor startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint {
if (!startColor || !endColor)
return nil;
CFArrayRef colors = (__bridge CFArrayRef) [NSArray arrayWithObjects:
(id)startColor.CGColor,
(id)endColor.CGColor,
nil];
CGGradientRef g = CGGradientCreateWithColors(nil, colors, nil);
startPoint.x *= size.width;
startPoint.y *= size.height;
endPoint.x *= size.width;
endPoint.y *= size.height;
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size];
UIImage *gradientImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
CGContextDrawLinearGradient(rendererContext.CGContext, g, startPoint, endPoint, kCGGradientDrawsAfterEndLocation);
}];
return gradientImage;
}
#end

UIImageView show image out of bounds in a UICollectionViewCell sub class

I have to create a customize UICollectionViewCell to show a image and a name for the image, And on top of the image, there is a frame image which is 2 pixels bigger in width and height than the image. However, no matter what I do, the image seems to be bigger than the frame image. I did the same thing for the table view, and it works perfect.
Here is the code:
//GridCell.h
#interface GridCell : UICollectionViewCell
#property(nonatomic, strong) UILabel *lblName;
#property(nonatomic, strong) UIImageView *image;
#end
//GridCell.m
#import "GridCell.h"
#implementation GridCell
#synthesize image, lblName;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
UIImage *bg = [UIImage imageNamed:#"borderUIimgLg.png"];
UIImageView *bgImage = [[UIImageView alloc] initWithFrame:CGRectMake(0.0, 0.0, frame.size.width, frame.size.width)];
[bgImage setImage:bg];
[bgImage setContentMode:UIViewContentModeScaleAspectFill];
NSLog(#"BG Image size %f, %f", bgImage.frame.size.width, bgImage.frame.size.height);
UIImageView *contentImage = [[UIImageView alloc] initWithFrame:CGRectMake(2.0, 2.0, frame.size.width-4.0, frame.size.width-4.0)];
[contentImage setContentMode:UIViewContentModeScaleAspectFill];
[contentImage setClipsToBounds:YES];
self.image = contentImage;
[self.contentView addSubview:self.image];
[self.contentView addSubview:bgImage];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(2.0, frame.size.width, frame.size.width - 4.0, 21.0)];
[label setFont:[UIFont fontWithName:#"HelveticaNeue-Bold" size:11.0]];
[label setTextAlignment:NSTextAlignmentCenter];
[label setBackgroundColor:[UIColor clearColor]];
self.lblName = label;
[self.contentView addSubview:self.lblName];
}
return self;
}
The UICollectionViewCell has a size of 67 x 100, so in the code, the bgImage supposed to be always to be 67 x 67 and its origin is (0,0) and the contentImage supposed to have a frame of (0,0,63,63). By debugging, it seems correct. However, the conentimage always bigger than the bgImage. The original size of the image is 80 x 80 though. I have tried setClipToBounds,
setContentViewMode on either the cell.ContentView or the imageView, but none works.
A screenshot about the problem is attached.
Any help is appreciated.
This might have found the answer by now, try:
contentImage.clipsToBounds = YES;
you are using 2 times frame.size.width instead of frame.size.height on the other one
edit, try this:
in the cell, when using initWithFrame methods, use the self.bounds property of the cell.
if you init the imageview inside the border use CGRectInset method to initialize the imageview with smaller bounds and set the imageviews center to same as the contentview of the cell
Try changing UIViewContentModeScaleAspectFill to UIViewContentModeScaleAspectFit.

UIPageControl doesn't properly react to taps

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

How do I center a UIImageView within a full-screen UIScrollView?

In my application, I would like to present the user with a full-screen photo viewer much like the one used in the Photos app. This is just for a single photo and as such should be quite simple. I just want the user to be able to view this one photo with the ability to zoom and pan.
I have most of it working. And, if I do not center my UIImageView, everything behaves perfectly. However, I really want the UIImageView to be centered on the screen when the image is sufficiently zoomed out. I do not want it stuck to the top-left corner of the scroll view.
Once I attempt to center this view, my vertical scrollable area appears to be greater than it should be. As such, once I zoom in a little, I am able to scroll about 100 pixels past the top of the image. What am I doing wrong?
#interface MyPhotoViewController : UIViewController <UIScrollViewDelegate>
{
UIImage* photo;
UIImageView *imageView;
}
- (id)initWithPhoto:(UIImage *)aPhoto;
#end
#implementation MyPhotoViewController
- (id)initWithPhoto:(UIImage *)aPhoto
{
if (self = [super init])
{
photo = [aPhoto retain];
// Some 3.0 SDK code here to ensure this view has a full-screen
// layout.
}
return self;
}
- (void)dealloc
{
[photo release];
[imageView release];
[super dealloc];
}
- (void)loadView
{
// Set the main view of this UIViewController to be a UIScrollView.
UIScrollView *scrollView = [[UIScrollView alloc] init];
[self setView:scrollView];
[scrollView release];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Initialize the scroll view.
CGSize photoSize = [photo size];
UIScrollView *scrollView = (UIScrollView *)[self view];
[scrollView setDelegate:self];
[scrollView setBackgroundColor:[UIColor blackColor]];
// Create the image view. We push the origin to (0, -44) to ensure
// that this view displays behind the navigation bar.
imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0, -44.0,
photoSize.width, photoSize.height)];
[imageView setImage:photo];
[scrollView addSubview:imageView];
// Configure zooming.
CGSize screenSize = [[UIScreen mainScreen] bounds].size;
CGFloat widthRatio = screenSize.width / photoSize.width;
CGFloat heightRatio = screenSize.height / photoSize.height;
CGFloat initialZoom = (widthRatio > heightRatio) ? heightRatio : widthRatio;
[scrollView setMaximumZoomScale:3.0];
[scrollView setMinimumZoomScale:initialZoom];
[scrollView setZoomScale:initialZoom];
[scrollView setBouncesZoom:YES];
[scrollView setContentSize:CGSizeMake(photoSize.width * initialZoom,
photoSize.height * initialZoom)];
// Center the photo. Again we push the center point up by 44 pixels
// to account for the translucent navigation bar.
CGPoint scrollCenter = [scrollView center];
[imageView setCenter:CGPointMake(scrollCenter.x,
scrollCenter.y - 44.0)];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[[[self navigationController] navigationBar] setBarStyle:UIBarStyleBlackTranslucent];
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleBlackTranslucent animated:YES];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[[[self navigationController] navigationBar] setBarStyle:UIBarStyleDefault];
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault animated:YES];
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return imageView;
}
#end
This code should work on most versions of iOS (and has been tested to work on 3.1 upwards).
It's based on the Apple WWDC code for PhotoScroller.
Add the below to your subclass of UIScrollView, and replace tileContainerView with the view containing your image or tiles:
- (void)layoutSubviews {
[super layoutSubviews];
// center the image as it becomes smaller than the size of the screen
CGSize boundsSize = self.bounds.size;
CGRect frameToCenter = tileContainerView.frame;
// center horizontally
if (frameToCenter.size.width < boundsSize.width)
frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2;
else
frameToCenter.origin.x = 0;
// center vertically
if (frameToCenter.size.height < boundsSize.height)
frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2;
else
frameToCenter.origin.y = 0;
tileContainerView.frame = frameToCenter;
}
Have you checked out the UIViewAutoresizing options?
(from the documentation)
UIViewAutoresizing
Specifies how a view is automatically resized.
enum {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
typedef NSUInteger UIViewAutoresizing;
Are you using IB to add the scroll view? Change the autosizing options of the scrollview to the attached image.
I think the reason behind it is because the zoomScale applies to the whole contentSize, regardless of the actual size of the subview inside the scrollView (in your case it's an imageView). The contentSize height seems to be always equal or greater than the height of the scrollView frame, but never smaller. So when applying a zoom to it, the height of the contentSize gets multiplied by the zoomScale factor as well, that's why you're getting an extra 100-something pixels of vertical scroll.
You probably want to set the bounds of the scroll view = bounds of the image view, and then center the scroll view in its containing view. If you place a view inside a scroll view at an offset from the top, you will get that empty space above it.