Center Content of Uiscrollview When Smaller

Center content of UIScrollView when smaller

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.

How to center content in a UIScrollView with contentOffset

I think you want:

CGFloat newContentOffsetX = (self.contentSize.width/2) - (self.bounds.size.width/2);

diagram:

|------------------------self.contentSize.width------------------------|
|-----self.width-----|
|-------- offset --------|
^ center

Keep zoomable image in center of UIScrollView

I think I got it. The solution is to use the scrollViewDidEndZooming method of the delegate and in that method set contentInset based on the size of the image. Here's what the method looks like:

- (void)scrollViewDidEndZooming:(UIScrollView *)aScrollView withView:(UIView *)view atScale:(float)scale {
CGSize imgViewSize = imageView.frame.size;
CGSize imageSize = imageView.image.size;

CGSize realImgSize;
if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
realImgSize = CGSizeMake(imgViewSize.width, imgViewSize.width / imageSize.width * imageSize.height);
}
else {
realImgSize = CGSizeMake(imgViewSize.height / imageSize.height * imageSize.width, imgViewSize.height);
}

CGRect fr = CGRectMake(0, 0, 0, 0);
fr.size = realImgSize;
imageView.frame = fr;

CGSize scrSize = scrollView.frame.size;
float offx = (scrSize.width > realImgSize.width ? (scrSize.width - realImgSize.width) / 2 : 0);
float offy = (scrSize.height > realImgSize.height ? (scrSize.height - realImgSize.height) / 2 : 0);
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.25];
scrollView.contentInset = UIEdgeInsetsMake(offy, offx, offy, offx);
[UIView commitAnimations];
}

Note that I'm using animation on setting the inset, otherwise the image jumps inside the scrollview when the insets are added. With animation it slides to the center. I'm using UIView beginAnimation and commitAnimation instead of animation block, because I need to have the app run on iphone 3.

UIScrollView Increase and center content

What worked for me in the end was refactoring my architechture a bit, to that I had the variables I needed. But the property which I ended up setting is scrollView.contentOffset, just for future reference :)

Is it Possible to Center Content in a UIScrollView Like Apple's Photos App?

Here is the best solution I could come up with for 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.

If anyone can come up with a better way to do it, I would love to hear it.

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

Here is how I calculate the the min zoom scale in the viewDidLoad method.

CGSize photoSize = [temporaryImage size];
CGSize screenSize = [[self view] bounds].size;

CGFloat widthRatio = screenSize.width / photoSize.width;
CGFloat heightRatio = screenSize.height / photoSize.height;
initialZoom = (widthRatio > heightRatio) ? heightRatio : widthRatio;
[myScrollView setMinimumZoomScale:initialZoom];
[myScrollView setZoomScale:initialZoom];
[myScrollView setMaximumZoomScale:3.0];

Centering and scrolling UIScrollView content with dimensions both larger and smaller

After stepping back from this, getting a better understanding of UIScrollView, and rethinking, I have solved my problem.

For starters, layoutSubviews is the wrong way to go, at least for what I need.

Trying to resize the UIScrollView and update constraints seemed more trouble than it was worth.

For whatever reason, it didn't initially occur to me than I could use both contentOffset and contentInset at the same time (thought it was either/or for some reason), but that was my exact solution.

CGRect rectContent = CGRectMake(0, 0, image.size.width, image.size.height);

UIImageView *imageView = [[UIImageView alloc] initWithFrame:rectContent];
self.scrollView.contentSize = rectContent.size;

CGFloat fOffsetWidth = (rectContent.size.width < self.scrollView.bounds.size.width) ? (self.scrollView.bounds.size.width - rectContent.size.width)/2 : 0;
CGFloat fOffsetHeight = (rectContent.size.height < self.scrollView.bounds.size.height) ? (self.scrollView.bounds.size.height - rectContent.size.height)/2 : 0;

self.scrollView.contentInset = UIEdgeInsetsMake(fOffsetHeight, fOffsetWidth, fOffsetHeight, fOffsetWidth);
self.scrollView.contentOffset = CGPointMake((rectContent.size.width - self.scrollView.bounds.size.width)/2, (rectContent.size.height - self.scrollView.bounds.size.height)/2);

imageView.image = image;

[self.scrollView addSubview:imageView];

All image dimension possibilities (larger/smaller, one/both) are centered in the scroll view, and a larger image dimension is scrollable while a smaller dimension remains centered.

Perfect!

Centering a variable width column of text in a UIScrollView using Auto Layout

This one is actually pretty straightforward! I've included a storyboard file as a demo. The key bits for the text inside of the content view:

  • Leading/Trailing Margins >= 10
  • Width >= 300
  • Width <= 500
  • Center Align

No priority changes are required, unless you have other content that is going to take precedence.

Sample Image

Constraints in ScrollView for centering and scrolling at the same time

Project demonstrating: https://github.com/Aquilosion/TestScrollViewConstraints

This is possible, although I've not been able to stop Xcode warning me that the constraints are incorrect (even though I'm pretty sure they're fine). What you need is:

  1. Scroll View
  2. Content View
  3. Actual Content

I've made a test view controller to demonstrate this. The view hierarchy should look like this:

View hierarchy

My test view controller looks like this:

View controller

The blue area is the scroll view with the content view, and the white area is the "actual content" (with the label in it). If you enable multiple lines on the label, you get the following:

View controller with multi-line label

To get the effect, you need to lock the content view on all four sides to the view controller, and make it equal widths and equal heights. You need to modify the 'equal height constraint' so that it's 'equal to or more than 0', instead of the default 'equals 0'.

Meanwhile, in the 'actual content' view, you need to 'align Y' with its superview (the content view), and lock the top and bottom to its superview, but modify them again so that they are 'greater than or equal to' constraints. After that, it should stay central if too small, or expand properly when it gets bigger.

Center UIView vertically in scroll view when its dynamic Labels are small enough, but align it to the top once they are not

You can accomplish this by embedding the labels in a stack view and embedding the stack view in a UIView. The label text will expand the stack view vertically, which will expand the content view vertically, which will control the scroll view's .contentSize.

Sample Image

Black is the scroll view; blue is the content view; stack view only shows as thin gray outline; labels are yellow, green and cyan. The background colors just make it easier to see what's what.

Bunch of steps, but should be clear:

  • add a scrollView, set constraints as normal
  • add a UIView to scrollView - name it "contentView"
  • set constraints 0 for top/leading/trailing/bottom of contentView to scrollView
  • set width and height of contentView equal to width and height of scrollView
  • add a stackView to contentView
  • set stackView to Vertical / Fill / Fill / Spacing: 20
  • set stackView constraints top: 8, bottom: 8, leading: 40, trailing: 40 to contentView
  • set stackView centerY constraint to contentView
  • add three labels to stackView
  • set fonts and text, set number of lines = 0 for center and bottom labels
  • change stackView top and bottom constraints to >= 8
  • change contentView height constraint to Priority: 250
  • I think that's everything....

Sample Image

Setting the height Priority of the contentView to 250 will allow it to expand vertically based on the text in the labels.

Setting top and bottom stackView constraints to >= 8 will "push" the top and bottom of the contentView, but allow extra space when you don't have enough text to exceed the vertical bounds.

Results:

Sample Image
Sample Image

Here's a storyboard with everything in place for reference:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14109" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="SeU-GX-TTY">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="bCz-Kd-LLi">
<objects>
<viewController id="SeU-GX-TTY" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="qjW-fW-J5n">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Zj2-9M-SP5" userLabel="scrollView">
<rect key="frame" x="0.0" y="40" width="375" height="627"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Pmb-IH-ckB" userLabel="contentView">
<rect key="frame" x="0.0" y="0.0" width="375" height="627"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="EfQ-93-hcI" userLabel="stackView">
<rect key="frame" x="40" y="164" width="295" height="299.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Anger" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Sxz-f7-zjR" userLabel="topLabel">
<rect key="frame" x="0.0" y="0.0" width="295" height="43"/>
<color key="backgroundColor" red="0.99953407049999998" green="0.98835557699999999" blue="0.47265523669999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="36"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="STy-4u-e1W" userLabel="centerLabel">
<rect key="frame" x="0.0" y="63" width="295" height="183"/>
<color key="backgroundColor" red="0.83216959239999999" green="0.98548370600000001" blue="0.47333085539999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<string key="text">Anger is an intense emotion defined as a response to a perceived provocation, the invasion of one's boundaries, or a threat. From an evolutionary standpoint, anger servers to mobilise psychological resources in order to address the threat/invasion. Anger is directed at an individual of equal status.</string>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="749" text="Based on information from Wikipedia. APA DIctionary of Psycology" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="u3i-zP-e1M" userLabel="bottomLabel">
<rect key="frame" x="0.0" y="266" width="295" height="33.5"/>
<color key="backgroundColor" red="0.45138680930000002" green="0.99309605359999997" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" red="0.46202266219999999" green="0.83828371759999998" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="EfQ-93-hcI" secondAttribute="trailing" constant="40" id="4HE-oJ-RE3"/>
<constraint firstItem="EfQ-93-hcI" firstAttribute="centerY" secondItem="Pmb-IH-ckB" secondAttribute="centerY" id="H9O-jj-a7A"/>
<constraint firstItem="EfQ-93-hcI" firstAttribute="top" relation="greaterThanOrEqual" secondItem="Pmb-IH-ckB" secondAttribute="top" constant="8" id="cKe-DN-Lbn"/>
<constraint firstItem="EfQ-93-hcI" firstAttribute="leading" secondItem="Pmb-IH-ckB" secondAttribute="leading" constant="40" id="f4g-6a-VqH"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="EfQ-93-hcI" secondAttribute="bottom" constant="8" id="meR-gT-OVG"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="Pmb-IH-ckB" firstAttribute="top" secondItem="Zj2-9M-SP5" secondAttribute="top" id="HCI-bq-7ur"/>
<constraint firstAttribute="trailing" secondItem="Pmb-IH-ckB" secondAttribute="trailing" id="Tdl-c0-GAV"/>
<constraint firstItem="Pmb-IH-ckB" firstAttribute="width" secondItem="Zj2-9M-SP5" secondAttribute="width" id="Zj9-ND-Fqt"/>
<constraint firstItem="Pmb-IH-ckB" firstAttribute="leading" secondItem="Zj2-9M-SP5" secondAttribute="leading" id="ckv-wi-E1z"/>
<constraint firstItem="Pmb-IH-ckB" firstAttribute="height" secondItem="Zj2-9M-SP5" secondAttribute="height" priority="250" id="jpK-HZ-vva"/>
<constraint firstAttribute="bottom" secondItem="Pmb-IH-ckB" secondAttribute="bottom" id="psz-UW-bNp"/>
</constraints>
</scrollView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Zj2-9M-SP5" firstAttribute="top" secondItem="Xr7-LW-bbC" secondAttribute="top" constant="20" id="EgA-Bk-3fC"/>
<constraint firstItem="Zj2-9M-SP5" firstAttribute="leading" secondItem="qjW-fW-J5n" secondAttribute="leading" id="MBG-pL-R8Q"/>
<constraint firstItem="Xr7-LW-bbC" firstAttribute="bottom" secondItem="Zj2-9M-SP5" secondAttribute="bottom" id="e9K-6A-Y9F"/>
<constraint firstItem="Xr7-LW-bbC" firstAttribute="trailing" secondItem="Zj2-9M-SP5" secondAttribute="trailing" id="yfs-wt-Br8"/>
</constraints>
<viewLayoutGuide key="safeArea" id="Xr7-LW-bbC"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="lHx-xL-Vx5" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="225" y="106"/>
</scene>
</scenes>
</document>

And here's a quick example replicating that layout / functionality via code only:

//
// ScrollWorkViewController.swift
//
// Created by DonMag on 6/12/19.
//

import UIKit

class ScrollWorkViewController: UIViewController {

let theScrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .red
return v
}()

let contentView: UIView = {
let v = UIView()
v.backgroundColor = UIColor(red: 0.25, green: 0.25, blue: 1.0, alpha: 1.0)
return v
}()

let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 20
return v
}()

let topLabel: UILabel = {
let v = UILabel()
v.font = UIFont.boldSystemFont(ofSize: 32.0)
v.backgroundColor = .yellow
return v
}()

let centerLabel: UILabel = {
let v = UILabel()
v.font = UIFont.systemFont(ofSize: 17.0)
v.numberOfLines = 0
v.backgroundColor = .green
return v
}()

let bottomLabel: UILabel = {
let v = UILabel()
v.font = UIFont.systemFont(ofSize: 14.0)
v.numberOfLines = 0
v.backgroundColor = .cyan
return v
}()

override func viewDidLoad() {
super.viewDidLoad()

[theScrollView, contentView, stackView, topLabel, centerLabel, bottomLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}

view.addSubview(theScrollView)
theScrollView.addSubview(contentView)
contentView.addSubview(stackView)
stackView.addArrangedSubview(topLabel)
stackView.addArrangedSubview(centerLabel)
stackView.addArrangedSubview(bottomLabel)

let contentViewHeightConstraint = contentView.heightAnchor.constraint(equalTo: theScrollView.heightAnchor, constant: 0.0)
contentViewHeightConstraint.priority = .defaultLow

NSLayoutConstraint.activate([

// constrain all 4 sides of the scroll view to the safe area
theScrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0.0),
theScrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0.0),
theScrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0.0),
theScrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0.0),

// constrain all 4 sides of the content view to the scroll view
contentView.topAnchor.constraint(equalTo: theScrollView.topAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: theScrollView.bottomAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: theScrollView.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: theScrollView.trailingAnchor, constant: 0.0),

// constrain width of content view to width of scroll view
contentView.widthAnchor.constraint(equalTo: theScrollView.widthAnchor, constant: 0.0),

// constrain the stack view >= 8-pts from the top
// <= minus 8-pts from the bottom
// 40-pts leading and trailing
stackView.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 8.0),
stackView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -8.0),
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 40.0),
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -40.0),

// constrain stack view centerY to contentView centerY
stackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0.0),

// activate the contentView's height constraint
contentViewHeightConstraint,

])

topLabel.text = "Anger"
bottomLabel.text = "Based on information from Wikipedia APA Dictionary of Psychology"

// a sample paragraph of text
let centerSampleText = "Anger is an intense emotion defined as a response to a perceived provocation, the invasion of one’s boundaries, or a threat. From an evolutionary standpoint, anger servers to mobilise psychological resources in order to address the threat/invasion. Anger is directed at an individual of equal status."

// change to repeat the center-label sample text
let numberOfParagraphs = 2

var s = ""

for i in 1...numberOfParagraphs {
s += "\(i). " + centerSampleText
if i < numberOfParagraphs {
s += "\n\n"
}
}

centerLabel.text = s

}

}


Related Topics



Leave a reply



Submit