How to scroll images horizontally in iOS swift - ios

I have an imageview called "cardImgView" in that I want to load two images by scrolling horizontally, I have tried the following way, in this case I can able to scroll only to up and down and the images also not changing, anyone
have idea how to do this correctly.
let img: UIImage = self.dataDict.object(forKey: kCardImgFront) as! UIImage
let img2:UIImage = self.dataDict.object(forKey: kCardImgBack) as! UIImage
imgArray = [img, img2]
for i in 0..<imgArray.count{
cardImgView?.image = imgArray[i]
scrollView.contentSize.width = scrollView.frame.width * CGFloat(i + 1)
scrollView.addSubview(cardImgView!)
}
thanks in advance.

First, as I commented, you are currently using a single UIImageView --- so each time through your for-loop you are just replacing the .image of that one image view.
Second, you will be much better off using auto-layout and constraints, instead of trying to explicitly set frames and the scrollView's contentSize.
Third, UIStackView is ideal for your use case - adding multiple images that you want to horizontally scroll.
So, the general idea is:
add a scroll view
add a stack view to the scroll view
use constraints to make the stack view control the scroll view's contentSize
create a new UIImageView for each image
add each image view to the stack view
Here is a simple example that you can run in a Playground page to see how it works. If you add your own images named image1.png and image2.png to the playground's resources, they will be used (otherwise, this example creates solid blue and solid green images):
import UIKit
import PlaygroundSupport
// UIImage extension to create a new, solid-color image
public extension UIImage {
public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
let rect = CGRect(origin: .zero, size: size)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
color.setFill()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let cgImage = image?.cgImage else { return nil }
self.init(cgImage: cgImage)
}
}
class TestViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// create a UIScrollView
let scrollView = UIScrollView()
// we will set the auto-layout constraints
scrollView.translatesAutoresizingMaskIntoConstraints = false
// set background color so we can see the scrollView when the images are scrolled
scrollView.backgroundColor = .orange
// add the scrollView to the view
view.addSubview(scrollView)
// pin scrollView 20-pts from top/bottom/leading/trailing
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20.0).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20.0).isActive = true
// create an array of empty images in case this is run without
// valid images in the resources
var imgArray = [UIImage(color: .blue), UIImage(color: .green)]
// if these images exist, load them and replace the blank images in imgArray
if let img1: UIImage = UIImage(named: "image1"),
let img2: UIImage = UIImage(named: "image2") {
imgArray = [img1, img2]
}
// create a UIStackView
let stackView = UIStackView()
// we can use the default stackView properties
// but can change axis, alignment, distribution, spacing, etc if desired
// we will set the auto-layout constraints
stackView.translatesAutoresizingMaskIntoConstraints = false
// add the stackView to the scrollView
scrollView.addSubview(stackView)
// with auto-layout, scroll views use the content's constraints to
// determine the contentSize,
// so pin the stackView to top/bottom/leading/trailing of the scrollView
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 0.0).isActive = true
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0.0).isActive = true
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 0.0).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0.0).isActive = true
// loop through the images
for img in imgArray {
// create a new UIImageView
let imgView = UIImageView(image: img)
// we will set the auto-layout constraints, and allow the stackView
// to handle the placement
imgView.translatesAutoresizingMaskIntoConstraints = false
// set image scaling as desired
imgView.contentMode = .scaleToFill
// add the image view to the stackView
stackView.addArrangedSubview(imgView)
// set imgView's width and height to the scrollView's width and height
imgView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 1.0).isActive = true
imgView.heightAnchor.constraint(equalTo: scrollView.heightAnchor, multiplier: 1.0).isActive = true
}
}
}
let vc = TestViewController()
vc.view.backgroundColor = .red
PlaygroundPage.current.liveView = vc

I modified my code and tried as follows and its working now. with page contrlller
let imgArray = [UIImage]()
let img: UIImage = self.dataDict.object(forKey: kCardImgFront) as! UIImage
let img2:UIImage = self.dataDict.object(forKey: kCardImgBack) as! UIImage
imgArray = [img, img2]
for i in 0..<imgArray.count {
let imageView = UIImageView()
imageView.image = imgArray[i]
let xPosition = self.view.frame.width * CGFloat(i)
imageView.frame = CGRect(x: xPosition, y: 0, width:
self.scrollView.frame.width + 50, height: self.scrollView.frame.height)
scrollView.contentSize.width = scrollView.frame.width * CGFloat(i + 1)
scrollView.addSubview(imageView)
}
self.scrollView.delegate = self
func scrollViewDidScroll(_ scrollView: UIScrollView){
pageController.currentPage = Int(self.scrollView.contentOffset.x /
CGFloat(4))
}

I think you need to set a proper frame for the cardImgView. It would be something like
cardImgView.frame = CGRect(x: scrollView.frame.width * CGFloat(i), y: 0, width: scrollView.frame.width, height: scrollView.frame.height)
Finally, after the for loop, you need to set scroll view's content size:
scrollView.contentSize.width = scrollView.frame.width * imgArray.count
Hope this helps.

I have written scrolling images horizontally in swift. Please check with this:
import UIKit
class ViewController: UIViewController,UIScrollViewDelegate {
#IBOutlet weak var Bannerview: UIView!
var spinner = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
var loadingView: UIView = UIView()
var loadinglabel: UILabel = UILabel()
var nextPage :Int!
var titlelab :UILabel!
var bannerimg :UIImageView!
var scroll :UIScrollView!
var viewPanel :UIView!
var pgCtr:UIPageControl!
var bannerArr:[String]!
var imgUrlstr :NSString!
var screenSize: CGRect!
var screenWidth: CGFloat!
var screenHeight: CGFloat!
func uicolorFromHex(rgbValue:UInt32)->UIColor
{
let red = CGFloat((rgbValue & 0xFF0000) >> 16)/256.0
let green = CGFloat((rgbValue & 0xFF00) >> 8)/256.0
let blue = CGFloat(rgbValue & 0xFF)/256.0
return UIColor(red:red, green:green, blue:blue, alpha:1.0)
}
override func viewWillAppear(_ animated: Bool)
{
screenSize = UIScreen.main.bounds
screenWidth = screenSize.width
screenHeight = screenSize.height
bannerArr = ["image1.jpeg","image2.jpeg","image3.jpeg","images4.jpeg","images5.jpeg"]
self.bannerview()
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.navigationController?.navigationBar.isTranslucent = false
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
}

Related

UIImageView and vector images: how to adjust for zooming to avoid blurry image

I have a vector image named Link in my asset catalog (in my case contained in a pdf, setup as preserve vector data) and I use an UIImageView to show this image in my view hierarchy:
let image = UIImage(named: "Link")
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.image = image
And this is the simple view hierarchy:
As you can see I use .scaleAspectFit and when I set a frame for imageView the UIImageView renders the image in the desired resolution to look sharp on screen. So far so good.
In order to zoom an scroll I use an UIScrollView. After zooming in the image in the imageView appears blurry due to obviously having too low resolution for the higher zoom:
Is there any straight forward way to adjust UIImageView so that it renders the image in a higher resolution?
Discussion:
Using .scaleAspectFit the renderered image resolution resolution seems to depend on the frame set for imageView. So setting a bigger frame and scaling the view using its transform property works, but is also rather tedious.
Another way is manually rendering the image at an apporpriate solution from the vector image using UIGraphicsImageRenderer or similar. This is currenlty my preferred solution, but I am wondering if there is an easier way using UIImageView.
Here is sample code (just replace your main ViewController with this in a new project), which shows the blurry image when zooming in (just use any vector image named Link in your assets and choose render as "Template Image"):
class ViewController: UIViewController, UIScrollViewDelegate {
let contentView: UIView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 1000.0, height: 1000.0))
let scrollView: UIScrollView = {
let view = UIScrollView(frame: CGRect(x: 0.0, y: 0.0, width: 400.0, height: 400.0))
view.minimumZoomScale = 0.5
view.maximumZoomScale = 10.0
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
self.scrollView.delegate = self
self.contentView.backgroundColor = UIColor.green.withAlphaComponent(0.2)
// setup view hierarchy
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(self.scrollView)
self.scrollView.addSubview(self.contentView)
// constraints
self.scrollView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
self.scrollView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
self.scrollView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
self.scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
self.contentView.leftAnchor.constraint(equalTo: self.scrollView.leftAnchor).isActive = true
self.contentView.rightAnchor.constraint(equalTo: self.scrollView.rightAnchor).isActive = true
self.contentView.topAnchor.constraint(equalTo: self.scrollView.topAnchor).isActive = true
self.contentView.bottomAnchor.constraint(equalTo: self.scrollView.bottomAnchor).isActive = true
self.contentView.widthAnchor.constraint(equalToConstant: 10.0 * self.view.frame.width).isActive = true
self.contentView.heightAnchor.constraint(equalToConstant: 10.0 * self.view.frame.height).isActive = true
// setup imageView
let image = UIImage(named: "Link")
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.image = image
imageView.tintColor = UIColor.black
imageView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(imageView)
// add constraints
imageView.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor).isActive = true
imageView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true
imageView.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
imageView.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let zoomScale = 8.0
self.scrollView.contentOffset = CGPoint(x: 0.5 * (self.contentView.frame.width - self.scrollView.frame.width), y: 0.5 * (self.contentView.frame.height - self.scrollView.frame.height))
self.scrollView.zoomScale = zoomScale
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.contentView
}
}

How to set the corner radius to 50% [duplicate]

This question already has answers here:
Set UIView's corner radius to half of the view's width automatically
(3 answers)
Closed 11 months ago.
I have an image view inside a collection view cell. I would like to set the corner radius of the image to 50% of its width (so it's a circle). How can I do this?
Here's my code so far
//
// CategoryCell.swift
// UICollectionViewDemo
//
import UIKit
final class Category3Cell: UICollectionViewCell {
private enum Constants {
// MARK: contentView layout constants
static let contentViewCornerRadius: CGFloat = 0.0
// MARK: imageView layout constants
static let imageWidth: CGFloat = 90.0
static let imageHeight: CGFloat = 90.0
// MARK: Generic layout constants
static let verticalSpacing: CGFloat = 10.0
static let horizontalPadding: CGFloat = 16.0
static let nameImagePadding: CGFloat = 20.0
}
public var categoryKey : String = "";
private let imageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = 45
imageView.layer.masksToBounds = true
return imageView
}()
private let name: UILabel = {
let label = UILabel(frame: .zero)
label.textAlignment = .center
label.numberOfLines = 0
label.font = UIFont(name: "CeraPro-Regular", size: 17);
return label
}()
override init(frame: CGRect) {
super.init(frame: .zero)
setupViews()
setupLayouts()
}
private func setupViews() {
contentView.clipsToBounds = true
contentView.layer.cornerRadius = Constants.contentViewCornerRadius
contentView.backgroundColor = .clear
contentView.isUserInteractionEnabled = true
contentView.addSubview(imageView)
contentView.addSubview(name)
}
private func setupLayouts() {
imageView.translatesAutoresizingMaskIntoConstraints = false
name.translatesAutoresizingMaskIntoConstraints = false
// Layout constraints for `imageView`
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.heightAnchor.constraint(equalToConstant: Constants.imageWidth),
imageView.heightAnchor.constraint(equalToConstant: Constants.imageHeight)
])
// Layout constraints for `usernameLabel`
NSLayoutConstraint.activate([
name.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.horizontalPadding),
name.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.horizontalPadding),
name.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: Constants.nameImagePadding)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup(image: String, nameOf: String, key: String) {
imageView.image = UIImage.init(named: image)
name.text = nameOf
categoryKey = key
}
}
extension Category3Cell: ReusableView {
static var identifier: String {
return String(describing: self)
}
}
you need first the clipsToBounds set to true and then if you know the image size, you can set its layer.cornerRadius to half of that size.
Alternatively you can use the layoutSubviews method, and in its override access the imageView bounds.height and use half of this for the corner radius.
Try this code:
imageView.layer.cornerRadius = imageView.frame.height / 2
Set width and height at first, then set imageView.layer.cornerRadius = imageView.frame.height / 2.
class ViewController: UIViewController {
private let imageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.contentMode = .scaleAspectFit
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
imageView.frame = CGRect(x: 100, y: 100, width: 100, height: 100);
imageView.layer.cornerRadius = 45
imageView.layer.masksToBounds = true
self.view.addSubview(imageView)
imageView.image = UIImage.init(named: "+")
}
}

Setting width constraint to a UIView with a function call doesn't work anymore. iOS ( Xcode 12.0.1 )

I had the following code in Swift to fill a status bar within its container, in relation to the completion of a quiz percentage by changing its width dynamically and it worked fine in 2018:
func updateUI() {
questionCounter.text = "\(Texts.questionCounter) \(questionNumber + 1)"
progressBar.frame.size.width = (containerOfBar.frame.size.width / CGFloat(allQuestions.list.count)) * CGFloat(questionNumber)
}
The instantiation of the elements have been made by closures in this way:
private let containerOfBar: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
view.layer.cornerRadius = 8
view.layer.borderColor = UIColor.white.cgColor
view.layer.borderWidth = 2
return view
}()
private let progressBar: UIView = {
let bar = UIView()
bar.backgroundColor = .blue
bar.translatesAutoresizingMaskIntoConstraints = false
return bar
}()
The auto-layout graphic constraints for the container and the bar, have been set in the following code only without a storyboard.
The bar itself:
progressBar.leadingAnchor.constraint(equalTo: containerOfBar.leadingAnchor, constant: 2),
progressBar.topAnchor.constraint(equalTo: containerOfBar.topAnchor, constant: 2),
progressBar.bottomAnchor.constraint(equalTo: containerOfBar.bottomAnchor, constant: 2),
The container of the bar:
containerOfBar.centerXAnchor.constraint(equalTo: optionsViewContainer.centerXAnchor),
containerOfBar.topAnchor.constraint(equalTo: optionsView[enter image description here][1].bottomAnchor, constant: self.view.frame.size.height/42),
containerOfBar.bottomAnchor.constraint(equalTo: optionsViewContainer.bottomAnchor, constant: -self.view.frame.size.height/42),
containerOfBar.widthAnchor.constraint(equalTo: optionsViewContainer.widthAnchor, multiplier: 0.3),
In the link, there is the image of the completion bar drawn by code.
Can't understand why the frame.width property doesn't work anymore, maybe a change in constraints workflow logic that I am missing...
I tried also to use the code of the function separately, but it seems like frame.width is not dynamically usable anymore.
Any suggestions?
You are mixing constraints with explicit frame settings, which won't give you the desired results. Each time auto-layout updates the screen, it will reset your progressBar.frame.size.width back to its constraint value -- in this case, it will be Zero because you didn't give it one.
A better approach is to set a Width Anchor on the progressBar. Make it equal to the Width Anchor of containerOfBar, with a multiplier of the percent of progress, and a constant of -4 (so you have 2-pts on each side).
Here's an example. It uses a questionCounter of 10 ... each time you tap the screen, it will increment the "current question number" and update the progress bar:
class ProgViewController: UIViewController {
private let containerOfBar: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
view.layer.cornerRadius = 8
view.layer.borderColor = UIColor.white.cgColor
view.layer.borderWidth = 2
return view
}()
private let progressBar: UIView = {
let bar = UIView()
bar.backgroundColor = .blue
bar.translatesAutoresizingMaskIntoConstraints = false
return bar
}()
private let questionCounter: UILabel = {
let v = UILabel()
v.backgroundColor = .cyan
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var numberOfQuestions = 10
var questionNumber = 0
// width constraint of progressBar
var progressBarWidthConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
containerOfBar.addSubview(progressBar)
view.addSubview(containerOfBar)
view.addSubview(questionCounter)
// create width constraint of progressBar
// start at 0% (multiplier: 0)
// this will be changed by updateUI()
progressBarWidthConstraint = progressBar.widthAnchor.constraint(equalTo: containerOfBar.widthAnchor, multiplier: 0, constant: -4)
progressBarWidthConstraint.priority = .defaultHigh
NSLayoutConstraint.activate([
progressBarWidthConstraint,
progressBar.leadingAnchor.constraint(equalTo: containerOfBar.leadingAnchor, constant: 2),
progressBar.topAnchor.constraint(equalTo: containerOfBar.topAnchor, constant: 2),
progressBar.bottomAnchor.constraint(equalTo: containerOfBar.bottomAnchor, constant: -2),
//The container of the bar:
containerOfBar.centerXAnchor.constraint(equalTo: view.centerXAnchor),
containerOfBar.topAnchor.constraint(equalTo: view.topAnchor, constant: 100),
containerOfBar.heightAnchor.constraint(equalToConstant: 50),
containerOfBar.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9),
// label under the container
questionCounter.topAnchor.constraint(equalTo: containerOfBar.bottomAnchor, constant: 8.0),
questionCounter.leadingAnchor.constraint(equalTo: containerOfBar.leadingAnchor),
questionCounter.trailingAnchor.constraint(equalTo: containerOfBar.trailingAnchor),
])
// every time we tap on the screen, we'll increment the question number
let tap = UITapGestureRecognizer(target: self, action: #selector(self.nextQuestion(_:)))
view.addGestureRecognizer(tap)
updateUI()
}
#objc func nextQuestion(_ g: UITapGestureRecognizer) -> Void {
// increment the question number
questionNumber += 1
// don't exceed number of questions
questionNumber = min(numberOfQuestions - 1, questionNumber)
updateUI()
}
func updateUI() {
questionCounter.text = "Question: \(questionNumber + 1) of \(numberOfQuestions) total questions."
// get percent completion
// for example, if we're on question 4 of 10,
// percent will be 0.4
let percent: CGFloat = CGFloat(questionNumber + 1) / CGFloat(numberOfQuestions)
// we can't change the multiplier directly, so
// deactivate the width constraint
progressBarWidthConstraint.isActive = false
// re-create it with current percentage of width
progressBarWidthConstraint = progressBar.widthAnchor.constraint(equalTo: containerOfBar.widthAnchor, multiplier: percent, constant: -4)
// activate it
progressBarWidthConstraint.isActive = true
// don't mix frame settings with auto-layout constraints
//progressBar.frame.size.width = (containerOfBar.frame.size.width / CGFloat(allQuestions.list.count)) * CGFloat(questionNumber)
}
}
It will look like this:

Zoom and scroll ImageView inside the ScrollView

The screen has an aimView centered. I need to correct the ScrollView:
After zoom - the image should be centered horizontally / vertically
if there are distances from the imageView to the edges of the screen
After the zoom, it should be possible to scroll the ScrollView so
that any part of the imageView can get under the aimView
When opening the screen, the zoom was set so that the image took up the
maximum possible area
now it looks like this:
class ScrollViewController: UIViewController, UIScrollViewDelegate {
var scrollView: UIScrollView!
var imageView: UIImageView!
var image: UIImage!
var aimView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView()
scrollView.delegate = self
setupScrollView()
image = #imageLiteral(resourceName: "apple")
imageView = UIImageView(image: image)
setupImageView()
aimView = UIView()
setupAimView()
}
func setupScrollView() {
scrollView.backgroundColor = .yellow
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
scrollView.maximumZoomScale = 10
scrollView.minimumZoomScale = 0.1
scrollView.zoomScale = 1.0
}
func setupImageView() {
imageView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: image.size.width),
imageView.heightAnchor.constraint(equalToConstant: image.size.height),
imageView.topAnchor.constraint(equalTo: scrollView.topAnchor),
imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor)
])
}
func setupAimView() {
aimView.translatesAutoresizingMaskIntoConstraints = false
aimView.backgroundColor = .green
aimView.alpha = 0.7
aimView.isUserInteractionEnabled = false
view.addSubview(aimView)
NSLayoutConstraint.activate([
aimView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
aimView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100),
aimView.widthAnchor.constraint(equalTo: aimView.heightAnchor),
aimView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
// MARK: - UIScrollViewDelegate
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
imageView
}
}
There are a few ways to approach this... one way:
use a UIView as the scroll view's "content"
constrain that "content" view on all 4 sides to the scroll view's content layout guide
embed the imageView in that "content" view
constrain the Top and Leading of the imageView so it will appear at the bottom-right corner of the "aim" view, when the content view is scrolled to 0,0
constrain the Trailing and Bottom of the imageView so it will appear at the top-left corner of the "aim" view, when the content view is scrolled to its max x and y
To give you an idea...
The dashed-outline rect is the scroll view frame. The green rect is the "aim" view. The yellow rect is the "content" view.
We won't be able to use the scroll view's built-in zooming, because it would also "zoom" the space between the image view's edges and the content view. Instead, we can add a UIPinchGestureRecognizer to the scroll view. When the user pinches to zoom, we'll take the gesture's .scale value and use that to change the width and height constants of the imageView. Since we've constrained that imageView to the content view, the content view will grow / shrink without changing the spacing on the sides.
Here is an example implementation (it requires an asset image named "apple"):
class PinchScroller: UIScrollView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:)))
self.addGestureRecognizer(pinchGesture)
}
var scaleStartCallback: (()->())?
var scaleChangeCallback: ((CGFloat)->())?
// assuming minimum scale of 1.0
var minScale: CGFloat = 1.0
// assuming maximum scale of 5.0
var maxScale: CGFloat = 5.0
private var curScale: CGFloat = 1.0
#objc private func handlePinchGesture(_ gesture:UIPinchGestureRecognizer) {
if gesture.state == .began {
// inform controller scaling started
scaleStartCallback?()
}
if gesture.state == .changed {
// inform controller the scale changed
let val: CGFloat = gesture.scale - 1.0
let scale = min(maxScale, max(minScale, curScale + val))
scaleChangeCallback?(scale)
}
if gesture.state == .ended {
// update current scale value
let val: CGFloat = gesture.scale - 1.0
curScale = min(maxScale, max(minScale, curScale + val))
}
}
}
class AimViewController: UIViewController {
var scrollView: PinchScroller!
var imageView: UIImageView!
var contentView: UIView!
var aimView: UIView!
var imageViewTopConstraint: NSLayoutConstraint!
var imageViewLeadingConstraint: NSLayoutConstraint!
var imageViewTrailingConstraint: NSLayoutConstraint!
var imageViewBottomConstraint: NSLayoutConstraint!
var imageViewWidthConstraint: NSLayoutConstraint!
var imageViewHeightConstraint: NSLayoutConstraint!
var imageViewWidthFactor: CGFloat = 1.0
var imageViewHeightFactor: CGFloat = 1.0
override func viewDidLoad() {
super.viewDidLoad()
// make sure we can load the image
guard let img = UIImage(named: "apple") else {
fatalError("Could not load image!!!")
}
scrollView = PinchScroller()
imageView = UIImageView()
contentView = UIView()
aimView = UIView()
[scrollView, imageView, contentView, aimView].forEach {
$0?.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(imageView)
scrollView.addSubview(aimView)
// init image view width constraint
imageViewWidthConstraint = imageView.widthAnchor.constraint(equalToConstant: 0.0)
imageViewHeightConstraint = imageView.heightAnchor.constraint(equalToConstant: 0.0)
// to handle non-1:1 ratio images
if img.size.width > img.size.height {
imageViewHeightFactor = img.size.height / img.size.width
} else {
imageViewWidthFactor = img.size.width / img.size.height
}
// init image view Top / Leading / Trailing / Bottom constraints
imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0)
imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0.0)
imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0.0)
imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0)
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain scroll view to all 4 sides of safe area
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 0.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
// constrain "content" view to all 4 sides of scroll view's content layout guide
contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// activate these constraints
imageViewTopConstraint,
imageViewLeadingConstraint,
imageViewTrailingConstraint,
imageViewBottomConstraint,
imageViewWidthConstraint,
imageViewHeightConstraint,
// "aim" view: 200x200, centered in scroll view frame
aimView.widthAnchor.constraint(equalToConstant: 200.0),
aimView.heightAnchor.constraint(equalTo: aimView.widthAnchor),
aimView.centerXAnchor.constraint(equalTo: frameG.centerXAnchor),
aimView.centerYAnchor.constraint(equalTo: frameG.centerYAnchor),
])
// set the image
imageView.image = img
// disable interaction for "aim" view
aimView.isUserInteractionEnabled = false
// aim view translucent background color
aimView.backgroundColor = UIColor.green.withAlphaComponent(0.25)
// probably don't want scroll bouncing
scrollView.bounces = false
// set the scaling callback closures
scrollView.scaleStartCallback = { [weak self] in
guard let self = self else {
return
}
self.didStartScale()
}
scrollView.scaleChangeCallback = { [weak self] v in
guard let self = self else {
return
}
self.didChangeScale(v)
}
contentView.backgroundColor = .yellow
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// set constraint constants here, after all view have been initialized
let aimSize: CGSize = aimView.frame.size
imageViewWidthConstraint.constant = aimSize.width * imageViewWidthFactor
imageViewHeightConstraint.constant = aimSize.height * imageViewHeightFactor
let w = (scrollView.frame.width - aimSize.width) * 0.5 + aimSize.width
let h = (scrollView.frame.height - aimSize.height) * 0.5 + aimSize.height
imageViewTopConstraint.constant = h
imageViewLeadingConstraint.constant = w
imageViewTrailingConstraint.constant = -w
imageViewBottomConstraint.constant = -h
DispatchQueue.main.async {
// center the content in the scroll view
let xOffset = aimSize.width - ((aimSize.width - self.imageView.frame.width) * 0.5)
let yOffset = aimSize.height - ((aimSize.height - self.imageView.frame.height) * 0.5)
self.scrollView.contentOffset = CGPoint(x: xOffset, y: yOffset)
}
}
private var startContentOffset: CGPoint = .zero
private var startSize: CGSize = .zero
func didStartScale() -> Void {
startContentOffset = scrollView.contentOffset
startSize = imageView.frame.size
}
func didChangeScale(_ scale: CGFloat) -> Void {
// all sizing is based on the "aim" view
let aimSize: CGSize = aimView.frame.size
// starting scroll offset
var cOffset = startContentOffset
// starting image view width and height
let w = startSize.width
let h = startSize.height
// new image view width and height
let newW = aimSize.width * scale * imageViewWidthFactor
let newH = aimSize.height * scale * imageViewHeightFactor
// change image view width based on pinch scaling
imageViewWidthConstraint.constant = newW
imageViewHeightConstraint.constant = newH
// adjust content offset so image view zooms from its center
let xDiff = (newW - w) * 0.5
let yDiff = (newH - h) * 0.5
cOffset.x += xDiff
cOffset.y += yDiff
// update scroll offset
scrollView.contentOffset = cOffset
}
}
Give that a try. If it comes close to what you're going for, then you've got a place to start.
Edit
After playing around a bit more with scrollView.contentInset, this is a much simpler approach. It uses the standard UIScrollView with its zoom/pan functionality, and doesn't require any extra "zoom" calculations or constraint changes:
class AimInsetsViewController: UIViewController {
var scrollView: UIScrollView!
var imageView: UIImageView!
var aimView: UIView!
var imageViewTopConstraint: NSLayoutConstraint!
var imageViewLeadingConstraint: NSLayoutConstraint!
var imageViewTrailingConstraint: NSLayoutConstraint!
var imageViewBottomConstraint: NSLayoutConstraint!
var imageViewWidthConstraint: NSLayoutConstraint!
var imageViewHeightConstraint: NSLayoutConstraint!
var imageViewWidthFactor: CGFloat = 1.0
var imageViewHeightFactor: CGFloat = 1.0
override func viewDidLoad() {
super.viewDidLoad()
var imageName: String = ""
imageName = "apple"
// testing different sized images
//imageName = "apple228x346"
//imageName = "zoom640x360"
// make sure we can load the image
guard let img = UIImage(named: imageName) else {
fatalError("Could not load image!!!")
}
scrollView = UIScrollView()
imageView = UIImageView()
aimView = UIView()
[scrollView, imageView, aimView].forEach {
$0?.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(scrollView)
scrollView.addSubview(imageView)
scrollView.addSubview(aimView)
// init image view width constraint
imageViewWidthConstraint = imageView.widthAnchor.constraint(equalToConstant: 0.0)
imageViewHeightConstraint = imageView.heightAnchor.constraint(equalToConstant: 0.0)
// to handle non-1:1 ratio images
if img.size.width > img.size.height {
imageViewHeightFactor = img.size.height / img.size.width
} else {
imageViewWidthFactor = img.size.width / img.size.height
}
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain scroll view to all 4 sides of safe area
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 0.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
// constrain "content" view to all 4 sides of scroll view's content layout guide
imageView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
imageView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
imageView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
imageView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
imageViewWidthConstraint,
imageViewHeightConstraint,
// "aim" view: 200x200, centered in scroll view frame
aimView.widthAnchor.constraint(equalToConstant: 200.0),
aimView.heightAnchor.constraint(equalTo: aimView.widthAnchor),
aimView.centerXAnchor.constraint(equalTo: frameG.centerXAnchor),
aimView.centerYAnchor.constraint(equalTo: frameG.centerYAnchor),
])
// set the image
imageView.image = img
// disable interaction for "aim" view
aimView.isUserInteractionEnabled = false
// aim view translucent background color
aimView.backgroundColor = UIColor.green.withAlphaComponent(0.25)
// probably don't want scroll bouncing
scrollView.bounces = false
// delegate
scrollView.delegate = self
// set max zoom scale
scrollView.maximumZoomScale = 10.0
// set min zoom scale to less than 1.0
// if you want to allow image view smaller than aim view
scrollView.minimumZoomScale = 1.0
// scroll view background
scrollView.backgroundColor = .yellow
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// set constraint constants, scroll view insets and initial content offset here,
// after all view have been initialized
let aimSize: CGSize = aimView.frame.size
// aspect-fit image view to aim view
imageViewWidthConstraint.constant = aimSize.width * imageViewWidthFactor
imageViewHeightConstraint.constant = aimSize.height * imageViewHeightFactor
// set content insets
let f = aimView.frame
scrollView.contentInset = .init(top: f.origin.y + f.height,
left: f.origin.x + f.width,
bottom: f.origin.y + f.height,
right: f.origin.x + f.width)
// center image view in aim view
var c = scrollView.contentOffset
c.x -= (aimSize.width - imageViewWidthConstraint.constant) * 0.5
c.y -= (aimSize.height - imageViewHeightConstraint.constant) * 0.5
scrollView.contentOffset = c
}
}
extension AimInsetsViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
}
I think that will be much closer to what you're going for.
The easiest way to achieve this is by using a PDFView.
Code:
import PDFKit
let pdfView = PDFView(frame: self.view.bounds)
pdfView.displayDirection = .vertical
pdfView.displayMode = .singlePage
pdfView.backgroundColor = UIColor.white
if let image = UIImage(named: "sample"),
let pdfPage = PDFPage(image: image) {
let pdfDoc = PDFDocument()
pdfDoc.insert(pdfPage, at: 0)
pdfView.document = pdfDoc
pdfView.autoScales = true
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
}
self.view.addSubview(pdfView)
Result:
First I added padding after zoom (scrollView.contentInset)
var horizontalPadding: CGFloat { view.bounds.width / 4 }
var verticalPadding: CGFloat { view.bounds.height / 4 }
func setPadding() {
let imageViewSize = imageView.frame.size
let scrollViewSize = scrollView.bounds.size
let verticalPadding = imageViewSize.height < scrollViewSize.height
? (scrollViewSize.height - imageViewSize.height) / 2
: self.verticalPadding
let horizontalPadding = imageViewSize.width < scrollViewSize.width
? (scrollViewSize.width - imageViewSize.width) / 2
: self.horizontalPadding
let toAimViewWidthSpacing = aimView.frame.origin.x
let toAimViewHeightSpacing = aimView.frame.origin.y
scrollView.contentInset = UIEdgeInsets(
top: verticalPadding + toAimViewHeightSpacing ,
left: horizontalPadding + toAimViewWidthSpacing ,
bottom: verticalPadding + toAimViewHeightSpacing ,
right: horizontalPadding + toAimViewWidthSpacing)
}
Secondly, added delegate methods scrollViewDidZoom and scrolViewDidEndZooming
func scrollViewDidZoom(_ scrollView: UIScrollView) {
setPadding()
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
scrollView.contentSize = CGSize(
width: imageView.frame.width,
height: imageView.frame.height)
}
And finaly add a method to center the image which added to the viewDidLayoutSubviews()
override func viewDidLayoutSubviews() {
centerImageInScrollView()
}
func centerImageInScrollView() {
scrollView.contentSize = CGSize( width: imageView.frame.width, height: imageView.frame.height)
let newContentOffsetX = (scrollView.contentSize.width - scrollView.frame.size.width) / 2
let newContentOffsetY = (scrollView.contentSize.height - scrollView.frame.size.height) / 2
scrollView.setContentOffset(CGPoint(x: newContentOffsetX, y: newContentOffsetY), animated: true)
}
The entire code is here!
how it looks

Swift: ScrollView not scrolling (how to set the constraints)?

I am trying to set up a scroll view with auto layout in my storyboard and populate it with a couple of buttons in code, but the scroll view doesn't scroll and I don't understand how to set up the constraints to make scrolling available?
Especially how to set the constraints to the content view (what's that?).
In storyboard the scroll view is placed at the bottom of the screen (20 pix to the safe area) and from leading to trailing. It has a size of 375x80.
Now in code this is how the scroll view is populated with buttons and how it is set up:
override func viewDidLoad() {
super.viewDidLoad()
var xCoord: CGFloat = 5
var yCoord: CGFloat = 5
let buttonWidth: CGFloat = 70
let buttonHeight: CGFloat = 70
let gapBetweenButtons: CGFloat = 5
var itemCount = 0
for i in 0..<CIFilterNames.count {
itemCount = 1
// Button properties
let filterButton = UIButton(type: .custom)
filterButton.frame = CGRect(x: xCoord, y: yCoord, width: buttonWidth, height: buttonHeight)
filterButton.tag = itemCount
filterButton.addTarget(self, action: #selector(ViewController.filterButtonTapped(sender:)), for: .touchUpInside)
filterButton.layer.cornerRadius = 6
filterButton.clipsToBounds = true
//Code for filters will be added here
let ciContext = CIContext(options: nil)
let coreImage = CIImage(image: originalImage.image!)
let filter = CIFilter(name: "\(CIFilterNames[i])")
filter!.setDefaults()
filter?.setValue(coreImage, forKey: kCIInputImageKey)
let filteredImageDate = filter!.value(forKey: kCIOutputImageKey) as! CIImage
let filteredImageRef = ciContext.createCGImage(filteredImageDate, from: filteredImageDate.extent)
let imageForButton = UIImage(cgImage: filteredImageRef!)
// Asign filtered image to the button
filterButton.setBackgroundImage(imageForButton, for: .normal)
// Add buttons in the scrollView
xCoord += buttonWidth + gapBetweenButtons
filterScrollView.addSubview(filterButton)
}
filterScrollView.contentSize = CGSize(width: buttonWidth * CGFloat(itemCount + 2), height: yCoord)
filterScrollView.isScrollEnabled = true
}
Depending on the device size 4 or 5 buttons are shown, but not more and scrolling is not possible.
What can be done to make scrolling possible?
Reason is in itemCount , you're settings it to itemCount + 2 will be 3 as itemCount is 1 from the for loop so , set it to equal array size
filterScrollView.contentSize = CGSize(width: buttonWidth * CIFilterNames.count , height: yCoord)
You will be much better off using auto-layout over calculating sizes - gives much more flexibility for future changes.
Here is an example you can run in a Playground page. I used the "count" as the button labels, since I don't have your images. If you add images, you can un-comment the appropriate lines and remove the .setTitle and .backgroundColor lines:
import UIKit
import PlaygroundSupport
import CoreImage
class TestViewController : UIViewController {
var CIFilterNames = [
"CIPhotoEffectChrome",
"CIPhotoEffectFade",
"CIPhotoEffectInstant",
"CIPhotoEffectNoir",
"CIPhotoEffectProcess",
"CIPhotoEffectTonal",
"CIPhotoEffectTransfer",
"CISepiaTone"
]
let buttonWidth: CGFloat = 70
let buttonHeight: CGFloat = 70
let gapBetweenButtons: CGFloat = 5
override func viewDidLoad() {
super.viewDidLoad()
// create a UIScrollView
let filterScrollView = UIScrollView()
// we will set the auto-layout constraints
filterScrollView.translatesAutoresizingMaskIntoConstraints = false
// set background color so we can see the scrollView when the images are scrolled
filterScrollView.backgroundColor = .orange
// add the scrollView to the view
view.addSubview(filterScrollView)
// pin scrollView 20-pts from bottom/leading/trailing
filterScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0).isActive = true
filterScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0).isActive = true
filterScrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20.0).isActive = true
// scrollView height is 80
filterScrollView.heightAnchor.constraint(equalToConstant: 80).isActive = true
// create a UIStackView
let stackView = UIStackView()
stackView.spacing = gapBetweenButtons
// we will set the auto-layout constraints
stackView.translatesAutoresizingMaskIntoConstraints = false
// add the stackView to the scrollView
filterScrollView.addSubview(stackView)
// with auto-layout, scroll views use the content's constraints to
// determine the contentSize,
// so pin the stackView to top/bottom/leading/trailing of the scrollView
// with padding to match the gapBetweenButtons
stackView.leadingAnchor.constraint(equalTo: filterScrollView.leadingAnchor, constant: gapBetweenButtons).isActive = true
stackView.topAnchor.constraint(equalTo: filterScrollView.topAnchor, constant: gapBetweenButtons).isActive = true
stackView.trailingAnchor.constraint(equalTo: filterScrollView.trailingAnchor, constant: -gapBetweenButtons).isActive = true
stackView.bottomAnchor.constraint(equalTo: filterScrollView.bottomAnchor, constant: -gapBetweenButtons).isActive = true
// loop through the images
for i in 0..<CIFilterNames.count {
// create a new UIButton
let filterButton = UIButton(type: .custom)
filterButton.tag = i
// we will set the auto-layout constraints, and allow the stackView
// to handle the placement
filterButton.translatesAutoresizingMaskIntoConstraints = false
// set the width and height constraints
filterButton.widthAnchor.constraint(equalToConstant: buttonWidth).isActive = true
filterButton.heightAnchor.constraint(equalToConstant: buttonHeight).isActive = true
filterButton.layer.cornerRadius = 6
filterButton.clipsToBounds = true
filterButton.addTarget(self, action: #selector(filterButtonTapped(_:)), for: .touchUpInside)
// this example doesn't have your images, so
// set button title and background color
filterButton.setTitle("\(i)", for: .normal)
filterButton.backgroundColor = .blue
// //Code for filters will be added here
//
// let ciContext = CIContext(options: nil)
// let coreImage = CIImage(image: originalImage.image!)
// let filter = CIFilter(name: "\(CIFilterNames[i])")
// filter!.setDefaults()
// filter?.setValue(coreImage, forKey: kCIInputImageKey)
// let filteredImageDate = filter!.value(forKey: kCIOutputImageKey) as! CIImage
// let filteredImageRef = ciContext.createCGImage(filteredImageDate, from: filteredImageDate.extent)
// let imageForButton = UIImage(cgImage: filteredImageRef!)
//
// // Asign filtered image to the button
// filterButton.setBackgroundImage(imageForButton, for: .normal)
// add the image view to the stackView
stackView.addArrangedSubview(filterButton)
}
}
func filterButtonTapped(_ sender: Any?) -> Void {
if let b = sender as? UIButton {
print("Tapped:", b.tag)
}
}
}
let vc = TestViewController()
vc.view.backgroundColor = .red
PlaygroundPage.current.liveView = vc

Resources