How to mimic UITableView iOS7 default line separator style? - ios

I have a custom UITableViewCell in which I want to draw a vertical separator, similar to the default horizontal ones in iOS7. Currently I use this code when I configure the cell:
UIView *lineView = [[UIView alloc] initWithFrame:CGRectMake(cell.contentView.bounds.size.width - rightButtonWidth, 0, 1, cell.contentView.bounds.size.height)];
lineView.backgroundColor = [UIColor lightGrayColor];
lineView.autoresizingMask = 0x3f;
[cell.contentView addSubview:lineView];
As seen in the image, the default separator is rendered at 1 pixel height, whereas mine gets two pixels wide. I tried setting the width to .5 points instead, but then the line is not rendered at all.
Also the color is off, obviously not lightGrayColor. Is there a color constant in UIColor that matches? Edit: the color is RGB 207,207,210 which does not seem to be listed in UIColor.h.

Your problem is because the view will have a width on retina of 2px if the width is set to 1px. What I would suggest is to create a subclass of UIView, let's call it CustomDivider and in -layoutSubviews you will do something like this:
-(void)layoutSubviews {
[super layoutSubviews];
if([self.constraints count] == 0) {
CGFloat width = self.frame.size.width;
CGFloat height = self.frame.size.height;
if(width == 1) {
width = width / [UIScreen mainScreen].scale;
}
if (height == 0) {
height = 1 / [UIScreen mainScreen].scale;
}
if(height == 1) {
height = height / [UIScreen mainScreen].scale;
}
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, width, height);
}
else {
for(NSLayoutConstraint *constraint in self.constraints) {
if((constraint.firstAttribute == NSLayoutAttributeWidth || constraint.firstAttribute == NSLayoutAttributeHeight) && constraint.constant == 1) {
constraint.constant /=[UIScreen mainScreen].scale;
}
}
}
}
The code snippet above will check which dimension (width or height) is less or equal to 1 and it will resize it depending on the screen resolution. Also this code will work with autolayout (tested).
This approach will work from IB and from code.

Like #danypata did subclass works fine but my situation was to fix exist code quick and simple. Alternatively,
- (CGFloat)defaultOnePixelConversion
{
return 1.f / [UIScreen mainScreen].scale;
}
then directly apply to the line view.
UIView *lineView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, /*1*/[self defaultOnePixelConversion], height);
the result came out not much different than default UITableView's separator.

If you use custom UITableViewCells you can do it via their .xib. Just add a UIView with the color you want, make it one pixel and position it where you want. No need to do it programmatically.
In your code snippet you should add this line:
lineView.clipsToBounds = YES;

Here is the swift 4 version:
override func layoutSubviews() {
super.layoutSubviews()
if self.constraints.count == 0 {
var width = self.frame.size.width
var height = self.frame.size.height
if width == 1{
width = width / UIScreen.main.scale
}
if height == 0{
height = 1 / UIScreen.main.scale
}
if height == 1 {
height = height / UIScreen.main.scale
}
self.frame = CGRect(x:self.frame.origin.x, y:self.frame.origin.y, width:width, height:height);
}
else{
for constraint:NSLayoutConstraint in self.constraints{
if(constraint.firstAttribute == NSLayoutAttribute.width || constraint.firstAttribute == NSLayoutAttribute.height) && constraint.constant == 1{
constraint.constant /= UIScreen.main.scale
}
}
}
}

Related

How do I make the contents of a UIScrollView scroll vertically rather than horizontally?

I'm using JTCalendar to build a custom calendar app, and it's set to scroll through the months horizontally by default. From my understanding, setting it to scroll vertically instead would entail laying out the contents (months) in a vertical fashion.
The author of JTcalendar suggested this, but it's unclear how exactly contentOffset should be modified for this purpose. Here are the functions that contain contentOffset:
JTCalendar.m:
- (void)scrollViewDidScroll:(UIScrollView *)sender
{
if(self.calendarAppearance.isWeekMode){
return;
}
if(sender == self.menuMonthsView && self.menuMonthsView.scrollEnabled){
self.contentView.contentOffset = CGPointMake(sender.contentOffset.x * calendarAppearance.ratioContentMenu, self.contentView.contentOffset.y);
}
else if(sender == self.contentView && self.contentView.scrollEnabled){
self.menuMonthsView.contentOffset = CGPointMake(sender.contentOffset.x / calendarAppearance.ratioContentMenu, self.menuMonthsView.contentOffset.y);
}
}
JTCalendarContentView.m:
- (void)configureConstraintsForSubviews
{
self.contentOffset = CGPointMake(self.contentOffset.x, 0); // Prevent bug when contentOffset.y is negative
CGFloat x = 0;
CGFloat width = self.frame.size.width;
CGFloat height = self.frame.size.height;
for(UIView *view in monthsViews){
view.frame = CGRectMake(x, 0, width, height);
x = CGRectGetMaxX(view.frame);
}
self.contentSize = CGSizeMake(width * NUMBER_PAGES_LOADED, height);
}
In scrollViewDidScroll:
This line:
CGPointMake(sender.contentOffset.x * calendarAppearance.ratioContentMenu, self.contentView.contentOffset.y);
Should probably be something like this:
CGPointMake(sender.contentOffset.x, self.contentView.contentOffset.y * calendarAppearance.ratioContentMenu);
And this line:
self.menuMonthsView.contentOffset = CGPointMake(sender.contentOffset.x / calendarAppearance.ratioContentMenu, self.menuMonthsView.contentOffset.y);
Should probably be this:
self.menuMonthsView.contentOffset = CGPointMake(sender.contentOffset.x, self.menuMonthsView.contentOffset.y / calendarAppearance.ratioContentMenu);
In configureConstraintsForSubviews there are a few places that might need modifying. Not sure about the following line since it was set to fix a specific bug, so you could just comment it out for now and see what happens:
// Probably comment this out
self.contentOffset = CGPointMake(self.contentOffset.x, 0); // Prevent bug when contentOffset.y is negative
This block of code:
for(UIView *view in monthsViews){
view.frame = CGRectMake(x, 0, width, height);
x = CGRectGetMaxX(view.frame);
}
Should probably be something like this: (rename the x variable to y)
for(UIView *view in monthsViews){
view.frame = CGRectMake(0, y, width, height);
y = CGRectGetMaxY(view.frame);
}
Last, this line:
self.contentSize = CGSizeMake(width * NUMBER_PAGES_LOADED, height);
Should probably be:
self.contentSize = CGSizeMake(width, height * NUMBER_PAGES_LOADED);
I have not tested any of this but based on the code you posted and the fact that I have used JTCal in the past, this should put you on the right path.

Change height of inputAccessoryView issue

When I change the height of inputAccessoryView in iOS 8, the inputAccessoryView not go to the right origin, but covers the keyboard.
Here are some code snippets:
in table view controller
- (UIView *)inputAccessoryView {
if (!_commentInputView) {
_commentInputView = [[CommentInputView alloc] initWithFrame:CGRectMake(0, 0, [self width], 41)];
[_commentInputView setPlaceholder:NSLocalizedString(#"Comment", nil) andButtonTitle:NSLocalizedString(#"Send", nil)];
[_commentInputView setBackgroundColor:[UIColor whiteColor]];
_commentInputView.hidden = YES;
_commentInputView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleBottomMargin;
}
return _commentInputView;
}
in CommentInputView
#when the textview change height
- (void)growingTextView:(HPGrowingTextView *)growingTextView willChangeHeight:(float)height {
if (height > _textView_height) {
[self setHeight:(CGRectGetHeight(self.frame) + height - _textView_height)];
[self reloadInputViews];
}
}
in UIView Category from ios-helpers
- (void)setHeight: (CGFloat)heigth {
CGRect frame = self.frame;
frame.size.height = heigth;
self.frame = frame;
}
Finally, i found the answer. In ios8, apple add a NSContentSizeLayoutConstraints to inputAccessoryView and set a constant with 44. You can't remove this constaint, because ios8 use it to calculate the height of inputAccessoryView. So, the only solution is to change value of this constant.
Example
in ViewDidAppear
- (void)viewDidAppear:(BOOL)animated {
if ([self.inputAccessoryView constraints].count > 0) {
NSLayoutConstraint *constraint = [[self.inputAccessoryView constraints] objectAtIndex:0];
constraint.constant = CommentInputViewBeginHeight;
}
}
change inputAccessoryView height when the textview height changed
- (void)growingTextView:(HPGrowingTextView *)growingTextView willChangeHeight:(float)height {
NSLayoutConstraint *constraint = [[self constraints] objectAtIndex:0];
float new_height = height + _textView_vertical_gap*2;
[UIView animateWithDuration:0.2 animations:^{
constraint.constant = new_height;
} completion:^(BOOL finished) {
[self setHeight:new_height];
[self reloadInputViews];
}];
}
That is.
One way you can update the constraint mentioned in Yijun's answer when changing the height of the inputAccessoryView is by overwriting setFrame: on your inputAccessoryView. This doesn't rely on the height constraint being the first in the array.
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
for (NSLayoutConstraint *constraint in self.constraints) {
if (constraint.firstAttribute == NSLayoutAttributeHeight) {
constraint.constant = frame.size.height;
break;
}
}
}
The first answer didn't totally solve my problem but gave me a huge hint.
Apple did add a private constraint to the accessory view, but you cannot find it in the constraint list of the accessory view. You have to search for it from its superview. It killed my a few hours.
After reading the answer above, which is a great find, I was concerned that relying on the constraint you need to change being [0] or firstObject is an implementation detail that's likely to change under us in the future.
After doing a bit of debugging, I found that the Apple-added constraints on the accessory input view seem to have a priority of 76. This is a crazy low value and not one of the listed enums in the documentation for priority.
Given this low priority value it seems like a cleaner solution to simply conditionally add/remove another constraint with a high priority level, say UILayoutPriorityDefaultHigh when you want to resize the view?
For Xcode 11.2 and swift 5 this function will update inputAccessoryView constraints even in animation block
func updateInputContainerConstraints() {
if let accessoryView = inputAccessoryView,
let constraint = accessoryView.superview?.constraints.first(where: { $0.identifier == "accessoryHeight" }) {
constraint.isActive = false
accessoryView.layoutIfNeeded()
constraint.constant = accessoryView.bounds.height
constraint.isActive = true
accessoryView.superview?.addConstraint(constraint)
accessoryView.superview?.superview?.layoutIfNeeded()
}
}
Try this:
_vwForSendChat is the input accessory view
_txtViewChatMessage is the textview inside input accessory view
-(void)textViewDidChange:(UITextView *)textView {
CGFloat fixedWidth = textView.frame.size.width;
CGSize newSize = [textView sizeThatFits:CGSizeMake(fixedWidth, MAXFLOAT)];
CGRect newFrame = textView.frame;
newFrame.size = CGSizeMake(fmaxf(newSize.width, fixedWidth), newSize.height);
if (newFrame.size.height < 40) {
_vwForSendChat.frame = CGRectMake(0, 0, self.view.frame.size.width, 40);
} else {
if (newFrame.size.height > 200) {
_vwForSendChat.frame = CGRectMake(0, 0, self.view.frame.size.width, 200);
} else {
_vwForSendChat.frame = CGRectMake(0, 0, self.view.frame.size.width, newFrame.size.height);
}
}
[self.txtViewChatMessage reloadInputViews];
}

Multiline UILabels With Auto layout

I have been trying for the last 2 days to get a UITableViewCell to do the following manual calculation.
CGFloat availableWidthForValueLabel = availableWidth * 0.7f;
CGSize valueLabelFitSize = [_valueLabel sizeThatFits:CGSizeMake(availableWidthForValueLabel, MAXFLOAT)];
_valueLabel.frame = CGRectMake(availableWidth - valueLabelFitSize.width, 0, valueLabelFitSize.width, valueLabelFitSize.height);
CGFloat availableWidthForKeyLabel = availableWidth - CGRectGetWidth(_valueLabel.frame);
CGSize keyLabelFitSize = [_keyLabel sizeThatFits:CGSizeMake(availableWidthForKeyLabel, MAXFLOAT)];
CGFloat keyLabelX = CGRectGetMinX(_valueLabel.frame) - keyLabelFitSize.width;
I have two multi-lined labels (key, value) in a view.
The value is anchored to the right of the view the Key is anchored to the left of the view with right aligned text.
I want to evaluate the values width/height first with a maximum width of 0.7 * view width then want the key to take up the rest of the available space in the view.
This works fine for single lines labels, but as not for multi-lined labels.
I have tried setting the preferredMaxLayoutWidth of the value label in both the updateConstraints and layoutSubview in the cell but what seems to happen is the hight of the cell grow incorrectly so big gaps appear in the cell.
- (void)updateConstraints
{
[super updateConstraints];
// value width
CGFloat width = ceil(self.keyValueSection.bounds.size.width * kValueMaxLenghtOfParentView);
if (self.valueLabel.preferredMaxLayoutWidth != width )
{
self.valueLabel.preferredMaxLayoutWidth = width;
}
// dec width
if (!self.decSection.hidden)
{
width = ceil(self.holderSection.bounds.size.width * kDecMaxLengthOfParentView);
if (self.decLabel.preferredMaxLayoutWidth != width )
{
self.decLabel.preferredMaxLayoutWidth = width;
}
}
// key width
width = floor(self.keyValueSection.bounds.size.width - kKeyValueGap - self.valueLabel.bounds.size.width);
if(self.keyLabel.preferredMaxLayoutWidth != width)
{
self.keyLabel.preferredMaxLayoutWidth = width;
}
}
I have tried everything I can find on the web and it all nearly works. I have tried setting the hugging and compression ratio but.
While this might not be the most elegant solution, i have fond a way to do what i want.
I use Autolayout to position the labels using constraints for vertical spacing >=0 top and bottom and centre Y alignment. I then specific the constraints for leading and trailing between the labels.
I then give each label a height and width constraint, and use the sizeTofit method to workout what the constants should be and update them when I set the labels text/atttributed text properties.
self.keyLabel.text = model.lineKey;
if ([model.lineValue isKindOfClass:[NSString class]]) {
self.valueLabel.text = model.lineValue;
}
else if ([model.lineValue isKindOfClass:[NSAttributedString class]])
{
self.valueLabel.attributedText = model.lineValue;
}
else
{
self.valueLabel.text = nil;
self.valueLabel.attributedText = nil;
}
self.keyValueGap.constant = kKeyValueGap;
CGFloat availableWidth = CGRectGetWidth(self.keyValueSection.bounds);
CGFloat availableWidthForValueLabel = availableWidth * 0.5f;
CGSize valueLabelFitSize = [self.valueLabel sizeThatFits:CGSizeMake(availableWidthForValueLabel, MAXFLOAT)];
self.valueHeight.constant = valueLabelFitSize.height;
self.valueWidth.constant = valueLabelFitSize.width;
CGFloat availableWidthForkeyLabel = availableWidth - valueLabelFitSize.width - kKeyValueGap;
CGSize keyLabelFitSize = [self.keyLabel sizeThatFits:CGSizeMake(availableWidthForkeyLabel, MAXFLOAT)];
self.keyHeight.constant = keyLabelFitSize.height;
self.keyWidth.constant = availableWidthForkeyLabel;
If anyone else has any better ideas :) - i would love to hear them as i do not like manually calculations while using autolayout

How to determine margin of a grouped UITableView (or better, how to set it)?

The grouped UITableView places a margin between the edge of the view and the table cells. Annoyingly (for me) this margin is some function of the width of the view.
In my application I have two UITableViews of different widths that I am looking to align the cell edges of.
Is it possible to get this margin? Or better is it possible to set this margin?
cheers,
--Ben
Grouped TableView Width in Relation to Cell Margin
TBWidth (0 to 20)
Left Margin = TBWidth - 10
TBWidth (20 to 400)
Left Margin = 10
TBWidth (401 to 546)
Left Margin = 31
TBWidth (547 to 716)
Left Margin = apx 6% of tableView
TBWidth (717 to 1024)
Left Margin = 45
- (float) groupedCellMarginWithTableWidth:(float)tableViewWidth
{
float marginWidth;
if(tableViewWidth > 20)
{
if(tableViewWidth < 400)
{
marginWidth = 10;
}
else
{
marginWidth = MAX(31, MIN(45, tableViewWidth*0.06));
}
}
else
{
marginWidth = tableViewWidth - 10;
}
return marginWidth;
}
By creating (and using!) a subclass of UITableViewCell, you can possibly achieve what you're looking for. Just move the contentview and backgroundview around in layoutsubviews, as in the code sample below which shifts the visible parts of the cell 20pt to the right.
- (void) layoutSubviews
{
NSLog (#"Cell/contentview/backgroundview before:\n%#\n%#\n%#", self, self.contentView, self.backgroundView);
[super layoutSubviews];
CGRect frame = self.backgroundView.frame;
frame.origin.x += 20;
frame.size.width -= 20;
self.backgroundView.frame = frame;
frame = self.contentView.frame;
frame.origin.x += 20;
frame.size.width -= 20;
self.contentView.frame = frame;
NSLog (#"Cell/contentview/backgroundview after:\n%#\n%#\n%#", self, self.contentView, self.backgroundView);
}
I was using Matthew Thomas implementation but it's unreliable (it's broken if you use autorotation in your controllers) and has a lot of hardcoded code... I realized that there is a very simple solution, my implementation is a single line of code:
- (float)cellMargins
{
return self.backgroundView.frame.origin.x * 2;
}
This is far better IMO :)
EDIT: my implementation was unreliable too (it does not work properly when switching to/from editing mode), this is my final implementation (I handled right and left margins separately):
- (float)leftMargin
{
return self.contentView.frame.origin.x;
}
- (float)rightMargin
{
CGRect frame = self.contentView.frame;
float containerWidth = frame.size.width;
float margin = self.frame.size.width - (containerWidth + frame.origin.x);
return margin;
}
- (float)cellMargins
{
return ([self leftMargin] + [self rightMargin]);
}
Updated Matthew Thomas's code for cell margins calculation.
Created via UITableView's category.
This can be useful, when you need to determine text height in cell, and you dont' know width of that cell.
So, actual cell can be calculated like
cell.width = tableView.width - tableView.cellsMargin * 2;
An here's the code
#implementation UITableView (CellsMargins)
// This is black magic
// from
// http://stackoverflow.com/questions/4708085/how-to-determine-margin-of-a-grouped-uitableview-or-better-how-to-set-it
- (CGFloat)cellsMargin {
// No margins for plain table views
if (self.style == UITableViewStylePlain) {
return 0;
}
// iPhone always have 10 pixels margin
if (! isIPad()) {
return 10;
}
CGFloat tableWidth = self.frame.size.width;
// Really small table
if (tableWidth <= 20) {
return tableWidth - 10;
}
// Average table size
if (tableWidth < 400) {
return 10;
}
// Big tables have complex margin's logic
// Around 6% of table width,
// 31 <= tableWidth * 0.06 <= 45
CGFloat marginWidth = tableWidth * 0.06;
marginWidth = MAX(31, MIN(45, marginWidth));
return marginWidth;
}
#end
This is now accessible through a property of UITableViewCell:
#property(nonatomic) UIEdgeInsets separatorInset
Apple Docs - UITableViewCell - seperatorInset
More accurate left margin for the 'stretchy' zone instead of the 'apx. 6% calc.'
Side note: the iPad implements additional section top and bottom padding in addition to the left margin padding, it's 10.0f below 400.0f table width and 31.0f otherwise.
-(CGFloat)leftMarginForTableView:(UITableView*)tableView
{
if (tableView.style != UITableViewStyleGrouped) return 0;
CGFloat widthTable = tableView.bounds.size.width;
if (isPhone) return (10.0f);
if (widthTable <= 400.0f) return (10.0f);
if (widthTable <= 546.0f) return (31.0f);
if (widthTable >= 720.0f) return (45.0f);
return (31.0f + ceilf((widthTable - 547.0f)/13.0f));
}
I would like to supplement Matthew's answer with a code to set-up fixed margin.
Subclass UITableViewCell.
Implement setFrame:
- (void)setFrame:(CGRect)frame
{
CGFloat inset = 15.0;
CGFloat tableWidth = 400.0;
frame.origin.x -= [self groupedCellMarginWithTableWidth:tableWidth] - inset;
frame.size.width = frame.size.width + 2 * ([self groupedCellMarginWithTableWidth:tableWidth] - inset);
[super setFrame:frame];
}
This will set left and right margin for 15.0 while table width may vary.
Just in case anyone needs this, the function for the section title left margin when obtained from tableView:titleForHeaderInSection: seems to be:
- (float)marginForSectionTitleOnGroupedTableView {
float width = self.width;
if (width <= 400) return 19;
else if (width >= 401 && width < 547) return 40;
else if (width >= 547 && width < 560) return 41;
else if (width >= 560 && width < 573) return 42;
else if (width >= 573 && width < 586) return 43;
else if (width >= 586 && width < 599) return 44;
else if (width >= 599 && width < 612) return 45;
else if (width >= 612 && width < 625) return 46;
else if (width >= 625 && width < 639) return 47;
else if (width >= 639 && width < 652) return 48;
else if (width >= 652 && width < 665) return 49;
else if (width >= 665 && width < 678) return 50;
else if (width >= 678 && width < 691) return 51;
else if (width >= 691 && width < 704) return 52;
else if (width >= 704 && width < 717) return 53;
// if (width >= 717)
return 54;
}
For widths greater than 401, the margin seems to be around 7.5% of the table view width; but when trying to align some other view with the section title, the alignment didn't work as expected; so the verbose approach seems to work better.
I have tried a different approach. But not sure whether it always work. I had a grouped UITableView and needed to make customized Header View. But as you know, when implementing viewForHeader inSection method we can not determine the margins for our created view.
So, what I have done is firstly add a CGRect property to ViewController .m file:
#property(nonatomic)CGRect groupedCellRectangle;
and in UITableViewDelegate:
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
static BOOL groupFrameInitialized = NO;
if(!groupFrameInitialized){
groupFrameInitialized = YES;
self.groupedCellRectangle = cell.contentView.frame;
}
}
after then, in my viewForHeader inSection method:
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(self.groupedCellRectangle.origin.x, 0, self.groupedCellRectangle.size.width, 28)];
[v setBackgroundColor:[UIColor clearColor]];
UILabel *label = [[UILabel alloc] initWithFrame:v.frame];
label.text = #"KAF";
[v addSubview:label];
return v;
}
the result is as I assumed. groupedCellRectangle had successfully stored CGRect value with margin.
The idea behind this approach was UITableView always call viewForHeader method after willDisplay call.
Hope that helps...

Center content of UIScrollView when smaller

I have a UIImageView inside a UIScrollView which I use for zooming and scrolling. If the image / content of the scroll view is bigger than the scroll view, everything works fine. However, when the image becomes smaller than the scroll view, it sticks to the top left corner of the scroll view. I would like to keep it centered, like the Photos app.
Any ideas or examples about keeping the content of the UIScrollView centered when it's smaller?
I am working with iPhone 3.0.
The following code almost works. The image returns to the top left corner if I pinch it after reaching the minimum zoom level.
- (void)loadView {
[super loadView];
// set up main scroll view
imageScrollView = [[UIScrollView alloc] initWithFrame:[[self view] bounds]];
[imageScrollView setBackgroundColor:[UIColor blackColor]];
[imageScrollView setDelegate:self];
[imageScrollView setBouncesZoom:YES];
[[self view] addSubview:imageScrollView];
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"WeCanDoIt.png"]];
[imageView setTag:ZOOM_VIEW_TAG];
[imageScrollView setContentSize:[imageView frame].size];
[imageScrollView addSubview:imageView];
CGSize imageSize = imageView.image.size;
[imageView release];
CGSize maxSize = imageScrollView.frame.size;
CGFloat widthRatio = maxSize.width / imageSize.width;
CGFloat heightRatio = maxSize.height / imageSize.height;
CGFloat initialZoom = (widthRatio > heightRatio) ? heightRatio : widthRatio;
[imageScrollView setMinimumZoomScale:initialZoom];
[imageScrollView setZoomScale:1];
float topInset = (maxSize.height - imageSize.height) / 2.0;
float sideInset = (maxSize.width - imageSize.width) / 2.0;
if (topInset < 0.0) topInset = 0.0;
if (sideInset < 0.0) sideInset = 0.0;
[imageScrollView setContentInset:UIEdgeInsetsMake(topInset, sideInset, -topInset, -sideInset)];
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return [imageScrollView viewWithTag:ZOOM_VIEW_TAG];
}
/************************************** NOTE **************************************/
/* The following delegate method works around a known bug in zoomToRect:animated: */
/* In the next release after 3.0 this workaround will no longer be necessary */
/**********************************************************************************/
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
[scrollView setZoomScale:scale+0.01 animated:NO];
[scrollView setZoomScale:scale animated:NO];
// END Bug workaround
CGSize maxSize = imageScrollView.frame.size;
CGSize viewSize = view.frame.size;
float topInset = (maxSize.height - viewSize.height) / 2.0;
float sideInset = (maxSize.width - viewSize.width) / 2.0;
if (topInset < 0.0) topInset = 0.0;
if (sideInset < 0.0) sideInset = 0.0;
[imageScrollView setContentInset:UIEdgeInsetsMake(topInset, sideInset, -topInset, -sideInset)];
}
I've got very simple solution!
All you need is to update the center of your subview (imageview) while zooming in the ScrollViewDelegate.
If zoomed image is smaller than scrollview then adjust subview.center else center is (0,0).
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
UIView *subView = [scrollView.subviews objectAtIndex:0];
CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);
subView.center = CGPointMake(scrollView.contentSize.width * 0.5 + offsetX,
scrollView.contentSize.height * 0.5 + offsetY);
}
#EvelynCordner's answer was the one that worked best in my app. A lot less code than the other options too.
Here's the Swift version if anyone needs it:
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let offsetX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
let offsetY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
scrollView.contentInset = UIEdgeInsets(top: offsetY, left: offsetX, bottom: 0, right: 0)
}
Okay, I've been fighting this for the past two days on and off and having finally come to a pretty reliable (so far...) solution I thought I should share it and save others some pain. :) If you do find a problem with this solution please shout!
I've basically gone through what everyone else has: searching StackOverflow, the Apple Developer Forums, looked at the code for three20, ScrollingMadness, ScrollTestSuite, etc. I've tried enlarging the UIImageView frame, playing with the UIScrollView's offset and/or insets from the ViewController, etc. but nothing worked great (as everyone else has found out too).
After sleeping on it, I tried a couple of alternative angles:
Subclassing the UIImageView so it alters it's own size dynamically - this didn't work well at all.
Subclassing the UIScrollView so it alters it's own contentOffset dynamically - this is the one that seems to be a winner for me.
With this subclassing UIScrollView method I'm overriding the contentOffset mutator so it isn't setting {0,0} when the image is scaled smaller than the viewport - instead it's setting the offset such that the image will be kept centred in the viewport. So far, it always seems to work. I've checked it with wide, tall, tiny & large images and doesn't have the "works but pinch at minimum zoom breaks it" issue.
I've uploaded an example project to github that uses this solution, you can find it here: http://github.com/nyoron/NYOBetterZoom
For a solution better suited for scroll views that use autolayout, use content insets of the scroll view rather than updating the frames of your scroll view's subviews.
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);
self.scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0.f, 0.f);
}
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 the photoscoller.
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;
}
Currently I'm subclassing UIScrollView and overriding setContentOffset: to adjust the offset based on contentSize. It works both with pinch and programatic zooming.
#implementation HPCenteringScrollView
- (void)setContentOffset:(CGPoint)contentOffset
{
const CGSize contentSize = self.contentSize;
const CGSize scrollViewSize = self.bounds.size;
if (contentSize.width < scrollViewSize.width)
{
contentOffset.x = -(scrollViewSize.width - contentSize.width) / 2.0;
}
if (contentSize.height < scrollViewSize.height)
{
contentOffset.y = -(scrollViewSize.height - contentSize.height) / 2.0;
}
[super setContentOffset:contentOffset];
}
#end
In addition to being short and sweet, this code produces a much smoother zoom than #Erdemus solution. You can see it in action in the RMGallery demo.
I've spent a day fighting with this issue, and ended up implementing the scrollViewDidEndZooming:withView:atScale: as follows:
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
CGFloat screenWidth = [[UIScreen mainScreen] bounds].size.width;
CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
CGFloat viewWidth = view.frame.size.width;
CGFloat viewHeight = view.frame.size.height;
CGFloat x = 0;
CGFloat y = 0;
if(viewWidth < screenWidth) {
x = screenWidth / 2;
}
if(viewHeight < screenHeight) {
y = screenHeight / 2 ;
}
self.scrollView.contentInset = UIEdgeInsetsMake(y, x, y, x);
}
This ensures that when the image is smaller than the screen, there's still adequate space around it so you can position it to the exact place you want.
(assuming that your UIScrollView contains an UIImageView to hold the image)
Essentially, what this does is check whether your image view's width / height is smaller that the screen's width / height, and if so, create an inset of half the screen's width / height (you could probably make this larger if you want the image to go out of the screen bounds).
Note that since this is a UIScrollViewDelegate method, don't forget to add it to your view controller's declaration, so to avoid getting a build issue.
If contentInset is not needed for anything else, it can be used to center scrollview's content.
class ContentCenteringScrollView: UIScrollView {
override var bounds: CGRect {
didSet { updateContentInset() }
}
override var contentSize: CGSize {
didSet { updateContentInset() }
}
private func updateContentInset() {
var top = CGFloat(0)
var left = CGFloat(0)
if contentSize.width < bounds.width {
left = (bounds.width - contentSize.width) / 2
}
if contentSize.height < bounds.height {
top = (bounds.height - contentSize.height) / 2
}
contentInset = UIEdgeInsets(top: top, left: left, bottom: top, right: left)
}
}
Advantage if this approach is that you can still use contentLayoutGuide to place content inside scrollview
scrollView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
imageView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
imageView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor)
])
or just drag and drop the content in Xcode's Interface Builder.
Apple has released the 2010 WWDC session videos to all members of the iphone developer program. One of the topics discussed is how they created the photos app!!! They build a very similar app step by step and have made all the code available for free.
It does not use private api either. I can't put any of the code here because of the non disclosure agreement, but here is a link to the sample code download. You will probably need to login to gain access.
http://connect.apple.com/cgi-bin/WebObjects/MemberSite.woa/wa/getSoftware?code=y&source=x&bundleID=20645
And, here is a link to the iTunes WWDC page:
http://insideapple.apple.com/redir/cbx-cgi.do?v=2&la=en&lc=&a=kGSol9sgPHP%2BtlWtLp%2BEP%2FnxnZarjWJglPBZRHd3oDbACudP51JNGS8KlsFgxZto9X%2BTsnqSbeUSWX0doe%2Fzv%2FN5XV55%2FomsyfRgFBysOnIVggO%2Fn2p%2BiweDK%2F%2FmsIXj
Ok, this solution is working for me. I have a subclass of UIScrollView with a reference to the UIImageView it is displaying. Whenever the UIScrollView zooms, the contentSize property is adjusted. It is in the setter that I scale the UIImageView appropriately and also adjust its center position.
-(void) setContentSize:(CGSize) size{
CGSize lSelfSize = self.frame.size;
CGPoint mid;
if(self.zoomScale >= self.minimumZoomScale){
CGSize lImageSize = cachedImageView.initialSize;
float newHeight = lImageSize.height * self.zoomScale;
if (newHeight < lSelfSize.height ) {
newHeight = lSelfSize.height;
}
size.height = newHeight;
float newWidth = lImageSize.width * self.zoomScale;
if (newWidth < lSelfSize.width ) {
newWidth = lSelfSize.width;
}
size.width = newWidth;
mid = CGPointMake(size.width/2, size.height/2);
}
else {
mid = CGPointMake(lSelfSize.width/2, lSelfSize.height/2);
}
cachedImageView.center = mid;
[super setContentSize:size];
[self printLocations];
NSLog(#"zoom %f setting size %f x %f",self.zoomScale,size.width,size.height);
}
Evertime I set the image on the UIScrollView I resize it. The UIScrollView in the scrollview is also a custom class I created.
-(void) resetSize{
if (!scrollView){//scroll view is view containing imageview
return;
}
CGSize lSize = scrollView.frame.size;
CGSize lSelfSize = self.image.size;
float lWidth = lSize.width/lSelfSize.width;
float lHeight = lSize.height/lSelfSize.height;
// choose minimum scale so image width fits screen
float factor = (lWidth<lHeight)?lWidth:lHeight;
initialSize.height = lSelfSize.height * factor;
initialSize.width = lSelfSize.width * factor;
[scrollView setContentSize:lSize];
[scrollView setContentOffset:CGPointZero];
scrollView.userInteractionEnabled = YES;
}
With these two methods I am able to have a view that behaves just like the photos app.
The way I've done this is to add an extra view into the hierarchy:
UIScrollView -> UIView -> UIImageView
Give your UIView the same aspect ratio as your UIScrollView, and centre your UIImageView into that.
Just the approved answer in swift, but without subclassing using the delegate
func centerScrollViewContents(scrollView: UIScrollView) {
let contentSize = scrollView.contentSize
let scrollViewSize = scrollView.frame.size;
var contentOffset = scrollView.contentOffset;
if (contentSize.width < scrollViewSize.width) {
contentOffset.x = -(scrollViewSize.width - contentSize.width) / 2.0
}
if (contentSize.height < scrollViewSize.height) {
contentOffset.y = -(scrollViewSize.height - contentSize.height) / 2.0
}
scrollView.setContentOffset(contentOffset, animated: false)
}
// UIScrollViewDelegate
func scrollViewDidZoom(scrollView: UIScrollView) {
centerScrollViewContents(scrollView)
}
I know some answers above are right, but I just want to give my answer with some explanation, the comments will make you understand why we do like this.
When I load the scrollView for the first time, I write the following code to make it center, please notice we set contentOffset first, then contentInset
scrollView.maximumZoomScale = 8
scrollView.minimumZoomScale = 1
// set vContent frame
vContent.frame = CGRect(x: 0,
y: 0 ,
width: vContentWidth,
height: vContentWidth)
// set scrollView.contentSize
scrollView.contentSize = vContent.frame.size
//on the X direction, if contentSize.width > scrollView.bounds.with, move scrollView from 0 to offsetX to make it center(using `scrollView.contentOffset`)
// if not, don't need to set offset, but we need to set contentInset to make it center.(using `scrollView.contentInset`)
// so does the Y direction.
let offsetX = max((scrollView.contentSize.width - scrollView.bounds.width) * 0.5, 0)
let offsetY = max((scrollView.contentSize.height - scrollView.bounds.height) * 0.5, 0)
scrollView.contentOffset = CGPoint(x: offsetX, y: offsetY)
let topX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
let topY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
scrollView.contentInset = UIEdgeInsets(top: topY, left: topX, bottom: 0, right: 0)
Then, when I pinch vContent, I write the following code to make it center.
func scrollViewDidZoom(_ scrollView: UIScrollView) {
//we just need to ensure that the content is in the center when the contentSize is less than scrollView.size.
let topX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
let topY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
scrollView.contentInset = UIEdgeInsets(top: topY, left: topX, bottom: 0, right: 0)
}
You could watch the contentSize property of the UIScrollView (using key-value observing or similar), and automatically adjust the contentInset whenever the contentSize changes to be less than the size of the scroll view.
One elegant way to center the content of UISCrollView is this.
Add one observer to the contentSize of your UIScrollView, so this method will be called everytime the content change...
[myScrollView addObserver:delegate
forKeyPath:#"contentSize"
options:(NSKeyValueObservingOptionNew)
context:NULL];
Now on your observer method:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// Correct Object Class.
UIScrollView *pointer = object;
// Calculate Center.
CGFloat topCorrect = ([pointer bounds].size.height - [pointer viewWithTag:100].bounds.size.height * [pointer zoomScale]) / 2.0 ;
topCorrect = ( topCorrect < 0.0 ? 0.0 : topCorrect );
topCorrect = topCorrect - ( pointer.frame.origin.y - imageGallery.frame.origin.y );
// Apply Correct Center.
pointer.center = CGPointMake(pointer.center.x,
pointer.center.y + topCorrect ); }
You should change the [pointer
viewWithTag:100]. Replace by your
content view UIView.
Also change imageGallery pointing to your window size.
This will correct the center of the content everytime his size change.
NOTE: The only way this content don't works very well is with standard zoom functionality of the UIScrollView.
This is my solution to that problem which works pretty fine for any kind of view inside a scrollview.
-(void)scrollViewDidZoom:(__unused UIScrollView *)scrollView
{
CGFloat top;
CGFloat left;
CGFloat bottom;
CGFloat right;
if (_scrollView.contentSize.width < scrollView.bounds.size.width) {
DDLogInfo(#"contentSize %#",NSStringFromCGSize(_scrollView.contentSize));
CGFloat width = (_scrollView.bounds.size.width-_scrollView.contentSize.width)/2.0;
left = width;
right = width;
}else {
left = kInset;
right = kInset;
}
if (_scrollView.contentSize.height < scrollView.bounds.size.height) {
CGFloat height = (_scrollView.bounds.size.height-_scrollView.contentSize.height)/2.0;
top = height;
bottom = height;
}else {
top = kInset;
right = kInset;
}
_scrollView.contentInset = UIEdgeInsetsMake(top, left, bottom, right);
if ([self.tiledScrollViewDelegate respondsToSelector:#selector(tiledScrollViewDidZoom:)])
{
[self.tiledScrollViewDelegate tiledScrollViewDidZoom:self];
}
}
There are a plenty of solutions here, but I'd risk putting here my own. It's good for two reasons: it doesn't mess zooming experience, as would do updating image view frame in progress, and also it respects original scroll view insets (say, defined in xib or storyboard for graceful handling of semi-transparent toolbars etc).
First, define a small helper:
CGSize CGSizeWithAspectFit(CGSize containerSize, CGSize contentSize) {
CGFloat containerAspect = containerSize.width / containerSize.height,
contentAspect = contentSize.width / contentSize.height;
CGFloat scale = containerAspect > contentAspect
? containerSize.height / contentSize.height
: containerSize.width / contentSize.width;
return CGSizeMake(contentSize.width * scale, contentSize.height * scale);
}
To retain original insets, defined field:
UIEdgeInsets originalScrollViewInsets;
And somewhere in viewDidLoad fill it:
originalScrollViewInsets = self.scrollView.contentInset;
To place UIImageView into UIScrollView (assuming UIImage itself is in loadedImage var):
CGSize containerSize = self.scrollView.bounds.size;
containerSize.height -= originalScrollViewInsets.top + originalScrollViewInsets.bottom;
containerSize.width -= originalScrollViewInsets.left + originalScrollViewInsets.right;
CGSize contentSize = CGSizeWithAspectFit(containerSize, loadedImage.size);
UIImageView *imageView = [[UIImageView alloc] initWithFrame:(CGRect) { CGPointZero, contentSize }];
imageView.autoresizingMask = UIViewAutoresizingNone;
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.image = loadedImage;
[self.scrollView addSubview:imageView];
self.scrollView.contentSize = contentSize;
[self centerImageViewInScrollView];
scrollViewDidZoom: from UIScrollViewDelegate for that scroll view:
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
if (scrollView == self.scrollView) {
[self centerImageViewInScrollView];
}
}
An finally, centering itself:
- (void)centerImageViewInScrollView {
CGFloat excessiveWidth = MAX(0.0, self.scrollView.bounds.size.width - self.scrollView.contentSize.width),
excessiveHeight = MAX(0.0, self.scrollView.bounds.size.height - self.scrollView.contentSize.height),
insetX = excessiveWidth / 2.0,
insetY = excessiveHeight / 2.0;
self.scrollView.contentInset = UIEdgeInsetsMake(
MAX(insetY, originalScrollViewInsets.top),
MAX(insetX, originalScrollViewInsets.left),
MAX(insetY, originalScrollViewInsets.bottom),
MAX(insetX, originalScrollViewInsets.right)
);
}
I didn't test orientation change yet (i.e. proper reaction for resizing UIScrollView itself), but fix for that should be relatively easy.
You'll find that the solution posted by Erdemus does work, but… There are some cases where the scrollViewDidZoom method does not get invoked & your image is stuck to the top left corner. A simple solution is to explicitly invoke the method when you initially display an image, like this:
[self scrollViewDidZoom: scrollView];
In many cases, you may be invoking this method twice, but this is a cleaner solution than some of the other answers in this topic.
Apple's Photo Scroller Example does exactly what you are looking for. Put this in your UIScrollView Subclass and change _zoomView to be your UIImageView.
-(void)layoutSubviews{
[super layoutSubviews];
// center the zoom view as it becomes smaller than the size of the screen
CGSize boundsSize = self.bounds.size;
CGRect frameToCenter = self.imageView.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;
}
self.imageView.frame = frameToCenter;
}
Apple's Photo Scroller Sample Code
To make the animation flow nicely, set
self.scrollview.bouncesZoom = NO;
and use this function (finding the center using the method at this answer)
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
[UIView animateWithDuration:0.2 animations:^{
float offsetX = MAX((scrollView.bounds.size.width-scrollView.contentSize.width)/2, 0);
float offsetY = MAX((scrollView.bounds.size.height-scrollView.contentSize.height)/2, 0);
self.imageCoverView.center = CGPointMake(scrollView.contentSize.width*0.5+offsetX, scrollView.contentSize.height*0.5+offsetY);
}];
}
This creates the bouncing effect but doesn't involve any sudden movements beforehand.
In case your inner imageView has initial specific width(eg 300) and you just want to center its width only on zoom smaller than its initial width this might help you also.
func scrollViewDidZoom(scrollView: UIScrollView){
if imageView.frame.size.width < 300{
imageView.center.x = self.view.frame.width/2
}
}
Here's the current way I'm making this work. It's better but still not perfect. Try setting:
myScrollView.bouncesZoom = YES;
to fix the problem with the view not centering when at minZoomScale.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGSize screenSize = [[self view] bounds].size;//[[UIScreen mainScreen] bounds].size;//
CGSize photoSize = [yourImage size];
CGFloat topInset = (screenSize.height - photoSize.height * [myScrollView zoomScale]) / 2.0;
CGFloat sideInset = (screenSize.width - photoSize.width * [myScrollView zoomScale]) / 2.0;
if (topInset < 0.0)
{ topInset = 0.0; }
if (sideInset < 0.0)
{ sideInset = 0.0; }
[myScrollView setContentInset:UIEdgeInsetsMake(topInset, sideInset, -topInset, -sideInset)];
ApplicationDelegate *appDelegate = (ApplicationDelegate *)[[UIApplication sharedApplication] delegate];
CGFloat scrollViewHeight; //Used later to calculate the height of the scrollView
if (appDelegate.navigationController.navigationBar.hidden == YES) //If the NavBar is Hidden, set scrollViewHeight to 480
{ scrollViewHeight = 480; }
if (appDelegate.navigationController.navigationBar.hidden == NO) //If the NavBar not Hidden, set scrollViewHeight to 360
{ scrollViewHeight = 368; }
imageView.frame = CGRectMake(0, 0, CGImageGetWidth(yourImage)* [myScrollView zoomScale], CGImageGetHeight(yourImage)* [myScrollView zoomScale]);
[imageView setContentMode:UIViewContentModeCenter];
}
Also, I do the following to prevent the image from sticking a the side after zooming out.
- (void) scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
myScrollView.frame = CGRectMake(0, 0, 320, 420);
//put the correct parameters for your scroll view width and height above
}
Okay, I think I've found a pretty good solution to this problem. The trick is to constantly readjust the imageView's frame. I find this works much better than constantly adjusting the contentInsets or contentOffSets. I had to add a bit of extra code to accommodate both portrait and landscape images.
Here's the code:
- (void) scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
CGSize screenSize = [[self view] bounds].size;
if (myScrollView.zoomScale <= initialZoom +0.01) //This resolves a problem with the code not working correctly when zooming all the way out.
{
imageView.frame = [[self view] bounds];
[myScrollView setZoomScale:myScrollView.zoomScale +0.01];
}
if (myScrollView.zoomScale > initialZoom)
{
if (CGImageGetWidth(temporaryImage.CGImage) > CGImageGetHeight(temporaryImage.CGImage)) //If the image is wider than tall, do the following...
{
if (screenSize.height >= CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale]) //If the height of the screen is greater than the zoomed height of the image do the following...
{
imageView.frame = CGRectMake(0, 0, 320*(myScrollView.zoomScale), 368);
}
if (screenSize.height < CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale]) //If the height of the screen is less than the zoomed height of the image do the following...
{
imageView.frame = CGRectMake(0, 0, 320*(myScrollView.zoomScale), CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale]);
}
}
if (CGImageGetWidth(temporaryImage.CGImage) < CGImageGetHeight(temporaryImage.CGImage)) //If the image is taller than wide, do the following...
{
CGFloat portraitHeight;
if (CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale] < 368)
{ portraitHeight = 368;}
else {portraitHeight = CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale];}
if (screenSize.width >= CGImageGetWidth(temporaryImage.CGImage) * [myScrollView zoomScale]) //If the width of the screen is greater than the zoomed width of the image do the following...
{
imageView.frame = CGRectMake(0, 0, 320, portraitHeight);
}
if (screenSize.width < CGImageGetWidth (temporaryImage.CGImage) * [myScrollView zoomScale]) //If the width of the screen is less than the zoomed width of the image do the following...
{
imageView.frame = CGRectMake(0, 0, CGImageGetWidth(temporaryImage.CGImage) * [myScrollView zoomScale], portraitHeight);
}
}
[myScrollView setZoomScale:myScrollView.zoomScale -0.01];
}
Just disable the pagination, so it'll work fine:
scrollview.pagingEnabled = NO;
I had the exact same problem. Here is how I solved
This code should get called as the result of scrollView:DidScroll:
CGFloat imageHeight = self.imageView.frame.size.width * self.imageView.image.size.height / self.imageView.image.size.width;
BOOL imageSmallerThanContent = (imageHeight < self.scrollview.frame.size.height) ? YES : NO;
CGFloat topOffset = (self.imageView.frame.size.height - imageHeight) / 2;
// If image is not large enough setup content offset in a way that image is centered and not vertically scrollable
if (imageSmallerThanContent) {
topOffset = topOffset - ((self.scrollview.frame.size.height - imageHeight)/2);
}
self.scrollview.contentInset = UIEdgeInsetsMake(topOffset * -1, 0, topOffset * -1, 0);
Although the question is a bit old yet the problem still exists. I solved it in Xcode 7 by making the vertical space constraint of the uppermost item (in this case the topLabel) to the superViews (the scrollView) top an IBOutlet and then recalculating its constant every time the content changes depending on the height of the scrollView's subviews (topLabel and bottomLabel).
class MyViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var topLabel: UILabel!
#IBOutlet weak var bottomLabel: UILabel!
#IBOutlet weak var toTopConstraint: NSLayoutConstraint!
override func viewDidLayoutSubviews() {
let heightOfScrollViewContents = (topLabel.frame.origin.y + topLabel.frame.size.height - bottomLabel.frame.origin.y)
// In my case abs() delivers the perfect result, but you could also check if the heightOfScrollViewContents is greater than 0.
toTopConstraint.constant = abs((scrollView.frame.height - heightOfScrollViewContents) / 2)
}
func refreshContents() {
// Set the label's text …
self.view.layoutIfNeeded()
}
}
A Swift version to just subclass UIScrollView and lauout the subview by yourself. It works pretty smooth.
import UIKit
class CenteringScrollView: UIScrollView {
override func layoutSubviews() {
super.layoutSubviews()
if zoomScale < 1.0 {
if let subview = self.subviews.first {
subview.center.x = self.center.x
}
}
}
}

Resources