Center UIImageView within a UIScrollView with a larger contentSize - ios

I have a full screen scrollView, to which I add an imageView as subview. I want the imageView to be centered and scaled filling the scrollView's size (that is the screen size) at the beginning, but then to allow the user to scroll the image in both directions (vertical and horizontal) with equal offsets at left, right, top and bottom.
I mean: I've set the scroll view's contentSize to be CGSize(width: screenWidth + 200, height: screenHeight + 200), and if I run the app, I see that I am able to scroll those 200 pts of offset only to the right and to the bottom of the image. I'd like the image to be centered in the content size, and to be able to scroll it horizontally to both to the left and to the right with offset 100 pts each side (similar thing with top and bottom when scrolling vertically).
How could I achieve this?
Note: I'm setting all the UI in code, I'm not using storyboards nor xib files

You may find it easier / more intuitive to use constraints and auto-layout rather than screenWidth and screenHeight:
//
// CenteredScrollViewController.swift
// SW4Temp
//
// Created by Don Mag on 4/18/18.
//
import UIKit
class CenteredScrollViewController: UIViewController {
let theScrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.green
return v
}()
let theImageView: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.blue
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// add the scrollView to the main view
view.addSubview(theScrollView)
// add the imageView to the scrollView
theScrollView.addSubview(theImageView)
// pin the scrollView to all four sides
theScrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0).isActive = true
theScrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0).isActive = true
theScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0).isActive = true
theScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0).isActive = true
// constrain the imageView's width and height to the scrollView's width and height
theImageView.widthAnchor.constraint(equalTo: theScrollView.widthAnchor, multiplier: 1.0).isActive = true
theImageView.heightAnchor.constraint(equalTo: theScrollView.heightAnchor, multiplier: 1.0).isActive = true
// set the imageView's top / bottom / leading / trailing anchors
// this *also* determines the scrollView's contentSize (scrollable area)
// with 100-pt padding on each side
theImageView.topAnchor.constraint(equalTo: theScrollView.topAnchor, constant: 100.0).isActive = true
theImageView.bottomAnchor.constraint(equalTo: theScrollView.bottomAnchor, constant: -100.0).isActive = true
theImageView.leadingAnchor.constraint(equalTo: theScrollView.leadingAnchor, constant: 100.0).isActive = true
theImageView.trailingAnchor.constraint(equalTo: theScrollView.trailingAnchor, constant: -100.0).isActive = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// set the scrollView's contentOffset (to center the imageView)
theScrollView.contentOffset = CGPoint(x: 100, y: 100)
}
}

You can move only down and right because your current content offset is 0,0 so top left - thus you can move down 200 and right 200.
What you want is to be scrolled 1/2 of vertical padding and 1/2 of horizontal padding, so in your case you would do scrollView.contentOffset = CGPoint(x: 100, y: 100)
Also for everything to work, UIImageView has to be same size as scrollView's contentSize, so bigger than screen size.
Given the comments what I think you want is the image to fill the screen and then user could scroll outside of bounds of the image, then you just need to make UIImageView's size be size of the screen its x and y coordinates to be same as contentOffset of the scrollView so (100, 100).
Here is the video of the sample app doing this:
https://dzwonsemrish7.cloudfront.net/items/2v361r2p0O2j1D3x3W10/Screen%20Recording%202018-04-19%20at%2002.32%20PM.mov

try this in
Swift 4.* or 5.*
let maxScale = self.imageScrollView.maximumZoomScale
let minScale = self.imageScrollView.minimumZoomScale
if let imageSize = imageView.image?.size{
let topOffset: CGFloat = (boundsSize.height - minScale * imageSize.height ) / 2
let leftOffset: CGFloat = (boundsSize.width - minScale * imageSize.width ) / 2
self.imageScrollView.contentInset = UIEdgeInsets(top: topOffset, left: leftOffset, bottom: 0, right: 0)
}

Related

Understanding layout anchors in Swift

So, I have this ViewController where I render an image inside a subview.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let childView = UIView()
childView.backgroundColor = .red
let imageView = UIImageView(image: UIImage(systemName: "tray"))
imageView.contentMode = .scaleAspectFill
childView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.heightAnchor.constraint(equalToConstant: 100.0)
])
view.addSubview(childView)
childView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
childView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor)
])
}
}
As you can see for some reason, the image moves to the left of screen. What is the cause of that?
One more thing I noticed is the subview should have a background color of red as specified, but somehow it's transparent. Why is that?
I expect the result to be something like,
You are adding way too few constraints. The horizontal position and size of the childView are not constrained at all, so the size just defaults to (0, 0), making the view not visible at all, which is why you don't see the red background.
First, let's constrain the horizontal position. This seems to be what you intended:
NSLayoutConstraint.activate([
// wouldn't it be better to use safeAreaLayoutGuide?
childView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
childView.leftAnchor.constraint(equalTo: view.layoutMarginsGuide.leftAnchor)
])
Then, the size of the childView should be the same as the imageView, so you should activate these constraints too:
imageView.leftAnchor.constraint(equalTo: childView.leftAnchor),
imageView.rightAnchor.constraint(equalTo: childView.rightAnchor),
imageView.topAnchor.constraint(equalTo: childView.topAnchor),
imageView.bottomAnchor.constraint(equalTo: childView.bottomAnchor),
There is one more thing though - the size of the image view at this point is not what you expect. The image view has a height of 100, but its width is still 24, which is the intrinsic size of the "tray" image. scaleAspectFill does scale the image to the size you want, but the views' widths stay at 24, and since the scaling is done from the centre of the view, the scaled up image appears to be "off centred".
I think that in general, you'll just have to manually calculate the width you want:
let width = 100 * image.size.width / image.size.height
and constrain both width and height:
imageView.heightAnchor.constraint(equalToConstant: 100.0),
imageView.widthAnchor.constraint(equalToConstant: width),
However, with SF symbols, you can get a bigger image simply by:
let image = UIImage(systemName: "tray", withConfiguration: UIImage.SymbolConfiguration(pointSize: 100))
You don't need any height or width constraints on the image view.

UIScrollView: Lock scrolling to vertical axis when zoomed in without changing the contentView

What’s the best way to dynamically lock scrolling in a UIScrollView to the vertical axis depending on the zoom scale?
I want to allow scrolling a large canvas in any direction when zoomed out (scrollView.zoomScale < 1.0)
but prevent horizontal scrolling completely when zoomed in (scrollView.zoomScale == 1.0).
The challenge here is that UIScrollView doesn’t seem to have a built-in setting to limit scrolling to one direction if the contentView is larger than the viewport in both directions. I would like to use the same large contentView but disallow horizontal scrolling when zoomed in.
(I know about scrollView.isDirectionalLockEnabled, but that’s not what I need: It only checks whether the user’s pan gesture has a dominant scrolling direction and then dynamically locks scrolling to either direction.)
Thanks!
If I understand your goal correctly...
You have a "contentView" that is larger than the scroll view
if the zoom scale is 1.0, only allow vertical scrolling
if the zoom scale is less than 1.0, allow both vertical and horizontal scrolling
So, if we have a scroll view frame size of 388 x 661 and a "contentView" with a size of 2100 x 2100, we start like this at zoom scale 1.0 (the bright-green is the scroll view frame):
and only vertical scrolling is allowed.
If the user zooms-out to, say, 0.8 scale:
both vertical and horizontal scrolling is allowed.
If the user then zooms-in back to 1.0 scale:
we're back to only vertical scrolling.
You can accomplish that by conforming your controller to UIScrollViewDelegate, assign self as the scrollView's delegate, add a "last scrollView content offset X" var, and then implement scrollViewDidScroll():
var lastOffsetX: CGFloat = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// if zoom scale is 1.0
// don't allow horizontal scrolling
if scrollView.zoomScale == 1.0 {
scrollView.contentOffset.x = lastOffsetX
return
}
// zoom scale is less than 1.0, so
// allow the scroll and update lastX
lastOffsetX = scrollView.contentOffset.x
}
Here's a complete example you can try out:
class ExampleViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemYellow
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .systemTeal
return v
}()
let infoLabel: UILabel = {
let v = UILabel()
return v
}()
// we'll use this to track the current content X offset
var lastOffsetX: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
scrollView.addSubview(contentView)
view.addSubview(scrollView)
view.addSubview(infoLabel)
[contentView, scrollView, infoLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
// let's add a 8 x 8 "grid" of labels to the content view
let outerVerticalStack = UIStackView()
outerVerticalStack.axis = .vertical
outerVerticalStack.spacing = 20
outerVerticalStack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(outerVerticalStack)
var j: Int = 1
for _ in 1...8 {
let rowStack = UIStackView()
rowStack.axis = .horizontal
rowStack.spacing = 20
rowStack.distribution = .fillEqually
for _ in 1...8 {
let v = UILabel()
v.font = .systemFont(ofSize: 48.0, weight: .regular)
v.text = "\(j)"
v.textAlignment = .center
v.backgroundColor = .green
v.widthAnchor.constraint(equalToConstant: 240.0).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
rowStack.addArrangedSubview(v)
j += 1
}
outerVerticalStack.addArrangedSubview(rowStack)
}
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -120.0),
contentView.topAnchor.constraint(equalTo: contentG.topAnchor),
contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
outerVerticalStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20.0),
outerVerticalStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20.0),
outerVerticalStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20.0),
outerVerticalStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20.0),
// put the info label below the scroll view
infoLabel.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 20.0),
infoLabel.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
infoLabel.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
])
// we'll update min zoom in viewDidAppear
// (after all views have been laid out)
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 1.0
// we need to disable zoom bouncing, or
// we get really bad positioning effect
// when zooming in past 1.0
scrollView.bouncesZoom = false
// assign the delegate
scrollView.delegate = self
// update the info label
updateInfo()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// update min zoom scale so we can only "zoom out" until
// the content view fits the scroll view frame
if scrollView.minimumZoomScale == 1.0 {
print(contentView.frame.size)
let xScale = scrollView.frame.width / contentView.frame.width
let yScale = scrollView.frame.height / contentView.frame.height
scrollView.minimumZoomScale = min(xScale, yScale)
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return contentView
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// if zoom scale is 1.0
// don't allow horizontal scrolling
if scrollView.zoomScale == 1.0 && !scrollView.isZooming {
scrollView.contentOffset.x = lastOffsetX
return
}
// zoom scale is less than 1.0, so
// allow the scroll and update lastX
lastOffsetX = scrollView.contentOffset.x
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateInfo()
}
func updateInfo() {
let s = String(format: "%0.4f", scrollView.zoomScale)
infoLabel.text = "Zoom Scale: \(s)"
}
}

iOS - UIView of same small width does not layout on smaller devices

I have an UIView that is defined below -
private let verticalSeparatorLine: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.alpha = 0.94
view.backgroundColor = .lightGray
return view
}()
This is how I add it to the viewcontroller's view.
override func viewDidLoad() {
super.viewDidLoad()
addTestView()
}
private func addTestView() {
view.addSubview(verticalSeparatorLine)
NSLayoutConstraint.activate([
verticalSeparatorLine.centerX.constraint(to: view.centerX),
verticalSeparatorLine.bottomAnchor.constraint(to: view.bottomAnchor),
verticalSeparatorLine.widthAnchor.constraint(0.3),
verticalSeparatorLine.heightAnchor.constraint(70)
])
}
If I add this verticalSeparatorLine to the ViewController's view in a larger device like iPhone X, the view lays out as expected. In smaller devices the view's width is zero. If I increase the width to say 2, it appears on both the devices.
Am I missing something?
Update:
If I replace with leadingAnchor, verticalSeparatorLine is visible with a width of 0.5.
verticalSeparatorLine.leadingAnchor.constraint(to: view.leadingAnchor, constant: 10),
It depends on the device's resolution, if UIScreen.main.scale == 2 means you set width 1 equals to 2 pixel's width, so 0.3(0.6pixel) can't be displayed, but in scale3 device, 0.33 just equals to 1 pixel.

scale image to size with aspectFit

I am building a app where you can drag images inside a view. Currently it look like this:
As you can see I marked the background color from the image view in green. The image view has contentMode aspectFit and it is 40 pixels smaller than the black view behind it.
I want the that the image is the full length and width of the image view. The contentMode should be aspectFit, that nothing is cut away from the image. Is it possible to resize the image, that it has 20 pixels or a bit more space from the view?
#adri567 You should use UIViewContentModeScaleToFill property like
imageView.contentMode = .scaleToFill
Try with this!
If you want to keep the image at the same size, but don't want to stretched it. your solution is something else.
Display your image as .aspectFit as in the question
the green view that you display replace it with the same image in .aspectFill but blur it as much as it looks good.
Simple math can solve this.
for shortcuts: H -> height , W -> Width
We know that general formula for this is: h1 / w1 = h2 / w2
Hscreen / Wscreen = Himage / Wimage
so we know screen width, image height and image width.
we can get screen width as -> view.frame.width
also we can get image size as -> image.size.width and image.size.height
Hscreen = (Himage) * (WScreen) / Wimage
..
you can use Hscreen to imageViews height anchor.
One approach...
embed a UIImageView in a (green) UIView
constrain the imageView on all 4 sides + 20-pts "padding"
constrain the width of the greenView (or its leading and trailing)
constrain the Y position of the greenView (top or centerY)
constrain the height of the imageView with a multiplier based on the image width and height
Here is a simple example:
class ViewController: UIViewController {
let imgView: UIImageView = {
let v = UIImageView()
v.contentMode = .scaleToFill
return v
}()
let greenView: UIView = {
let v = UIView()
v.backgroundColor = .green
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// replace with your image name
guard let img = UIImage(named: "bkg640x360") else {
fatalError("Could not load image!")
}
view.backgroundColor = .black
// set the imgView's image
imgView.image = img
// use auto-layout constraints
imgView.translatesAutoresizingMaskIntoConstraints = false
greenView.translatesAutoresizingMaskIntoConstraints = false
// add imgView to greenview
greenView.addSubview(imgView)
// add greenView to self.view
view.addSubview(greenView)
// we want to respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain greenView leading and trailing to view (safeArea)
greenView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
greenView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
// constrain greenView centerY to view centerY
greenView.centerYAnchor.constraint(equalTo: g.centerYAnchor, constant: 0.0),
// constrain imgView to all 4 sides of greenView with 20-pts "padding"
imgView.topAnchor.constraint(equalTo: greenView.topAnchor, constant: 20.0),
imgView.bottomAnchor.constraint(equalTo: greenView.bottomAnchor, constant: -20.0),
imgView.leadingAnchor.constraint(equalTo: greenView.leadingAnchor, constant: 20.0),
imgView.trailingAnchor.constraint(equalTo: greenView.trailingAnchor, constant: -20.0),
// constrain imgView proportional height equal to image height / width
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: img.size.height / img.size.width),
])
}
}
The result, using a 640 x 360 image:
and using a 512 x 512 (square) image:
These are my source images:

Autolayout programmatic aspect ratio setting

I am trying to add a subview to view and define autolayout constraints, including aspect ratio. But aspect ratio that I see at runtime is not what I defined in constraints. What am I doing wrong? As you can see in code, background view height should be 0.5 of background view width, but that's not the case here in the screenshot. Here is my code:
class ViewController: UIViewController {
private var backgroundView:UIView?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.backgroundColor = UIColor.orange
backgroundView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
backgroundView?.backgroundColor = UIColor.black.withAlphaComponent(1.0)
backgroundView?.layer.borderColor = UIColor.white.cgColor
backgroundView?.layer.borderWidth = 1.5
backgroundView?.layer.cornerRadius = 4
backgroundView?.clipsToBounds = true
backgroundView?.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(backgroundView!)
backgroundView?.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1.0).isActive = true
backgroundView?.heightAnchor.constraint(equalTo: backgroundView!.widthAnchor, multiplier: 0.5).isActive = true
backgroundView?.centerXAnchor.constraint(equalTo: view!.centerXAnchor, constant: 0).isActive = true
backgroundView?.topAnchor.constraint(equalTo: view!.topAnchor, constant: 4).isActive = true
}
}
Here is the screenshot:
"background view height should be 0.5 of background view width"
Your screenshot size is 1334 x 750
Your backgroundView - including the border - is 1334 x 667
1334 * 0.5 == 667
So, you are getting exactly what you are asking for.
Try to change height constraint to set:
backgroundView?.heightAnchor.constraint(equalTo: view!.heightAnchor, multiplier: 0.5).isActive = true
NB: You are getting the exact result which you are looking for. It has the height that's half of its width. There is nothing wrong with the screenshot.

Resources