How to keep specific area of content view within a visible frame while zooming in scrollview? - ios

I have cursor inside scroll content view. I am maintaining relative cursor size when zoom in but its going out of frame. I don't want to keep in center but I have to make sure it's always visible.
Before Zoom:
After Zoom:

I'm assuming you also don't want the user to be able to scroll the "selected rectangle) out of view...
One approach is to calculate the min and max content offsets in scrollViewDidScroll to make sure the "focus rect" is fully visible:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let fv = focusView else { return }
// get min and max scroll offsets
let mnx = fv.frame.minX * scrollView.zoomScale
let mny = fv.frame.minY * scrollView.zoomScale
let mxx = (fv.frame.maxX * scrollView.zoomScale) - scrollView.frame.width
let mxy = (fv.frame.maxY * scrollView.zoomScale) - scrollView.frame.height
let newX = max(min(scrollView.contentOffset.x, mnx), mxx)
let newY = max(min(scrollView.contentOffset.y, mny), mxy)
// update scroll offset if needed
scrollView.contentOffset = CGPoint(x: newX, y: newY)
}
Here's a quick example, using 6 subviews. For your "checkerboard grid" you would probably track a "focus rect" instead of a "focus view", but the same principle applies:
class RestrictZoomViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemYellow
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .systemTeal
return v
}()
var focusView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .lightGray
scrollView.addSubview(contentView)
view.addSubview(scrollView)
[contentView, scrollView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
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: -20.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),
contentView.widthAnchor.constraint(equalTo: frameG.widthAnchor),
contentView.heightAnchor.constraint(equalTo: frameG.heightAnchor),
])
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.orange, .purple, .brown,
]
colors.forEach { c in
let v = UIView()
v.backgroundColor = c
v.layer.borderColor = UIColor.white.cgColor
let t = UITapGestureRecognizer(target: self, action: #selector(tapHandler(_:)))
v.addGestureRecognizer(t)
contentView.addSubview(v)
}
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 5.0
scrollView.bouncesZoom = false
scrollView.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// only want to do this once
if let firstView = contentView.subviews.first,
firstView.frame.width == 0 {
var x:CGFloat = 40
let y: CGFloat = 160
var j = 0
for _ in 0..<(contentView.subviews.count / 2) {
contentView.subviews[j].frame = CGRect(x: x, y: y, width: 60, height: 60)
j += 1
contentView.subviews[j].frame = CGRect(x: x, y: y + 100, width: 60, height: 60)
j += 1
x += 100
}
}
}
#objc func tapHandler(_ g: UITapGestureRecognizer) {
guard let v = g.view else { return }
if let fv = focusView {
fv.layer.borderWidth = 0
}
// "highlight" tapped view
v.layer.borderWidth = 1
// set it as focusView
focusView = v
// adjust scroll offset if new focusView is not fully visible
scrollViewDidScroll(scrollView)
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return contentView
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let fv = focusView else { return }
// get min and max scroll offsets
let mnx = fv.frame.minX * scrollView.zoomScale
let mny = fv.frame.minY * scrollView.zoomScale
let mxx = (fv.frame.maxX * scrollView.zoomScale) - scrollView.frame.width
let mxy = (fv.frame.maxY * scrollView.zoomScale) - scrollView.frame.height
let newX = max(min(scrollView.contentOffset.x, mnx), mxx)
let newY = max(min(scrollView.contentOffset.y, mny), mxy)
// update scroll offset if needed
scrollView.contentOffset = CGPoint(x: newX, y: newY)
}
}

Related

How to fade out end of last line in multiline label?

Note, that it must work with different number of lines in UILabel - 1,2,3 etc.
I've already found solution for 1 line label, where you mask UILabel's layer with CAGradientLayer, but it doesn't work for multiline labels, as it masks the whole layer and fades out all lines.
I tried to make another CALayer with position calculated to be in the position of last line with desired width and used CAGradientLayer as mask and add this layer as sublayer of UILabel, it worked for static objects, but i use this UILabel in UITableViewCell and when it's tapped - it changes color to gray and i can see my layer, because it uses background color of UILabel when view layout its subviews, and also something wrong with x position calculation:
extension UILabel {
func fadeOutLastLineEnd() { //Call in layoutSubviews
guard bounds.width > 0 else { return }
lineBreakMode = .byCharWrapping
let tmpLayer = CALayer()
let gradientWidth: CGFloat = 32
let numberOfLines = CGFloat(numberOfLines)
tmpLayer.backgroundColor = UIColor.white.cgColor
tmpLayer.frame = CGRect(x: layer.frame.width - gradientWidth,
y: layer.frame.height / numberOfLines,
width: gradientWidth,
height: layer.frame.height / numberOfLines)
let tmpGrLayer = CAGradientLayer()
tmpGrLayer.colors = [UIColor.white.cgColor, UIColor.clear.cgColor]
tmpGrLayer.startPoint = CGPoint(x: 1, y: 0)
tmpGrLayer.endPoint = CGPoint(x: 0, y: 0)
tmpGrLayer.frame = tmpLayer.bounds
tmpLayer.mask = tmpGrLayer
layer.addSublayer(tmpLayer)
}
}
So, i need :
which can be multiline
end of last line needs to be faded out (gradient?)
works in UITableViewCell, when the whole object changes color
There are various ways to do this -- here's one approach.
We can mask a view by setting the layer.mask. The opaque areas of the mask will show-through, and the transparent areas will not.
So, what we need is a custom layer subclass that will look like this:
This is an example that I'll call InvertedGradientLayer:
class InvertedGradientLayer: CALayer {
public var lineHeight: CGFloat = 0
public var gradWidth: CGFloat = 0
override func draw(in inContext: CGContext) {
// fill all but the bottom "line height" with opaque color
inContext.setFillColor(UIColor.gray.cgColor)
var r = self.bounds
r.size.height -= lineHeight
inContext.fill(r)
// can be any color, we're going from Opaque to Clear
let colors = [UIColor.gray.cgColor, UIColor.gray.withAlphaComponent(0.0).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colorLocations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations)!
// start the gradient "grad width" from right edge
let startPoint = CGPoint(x: bounds.maxX - gradWidth, y: 0.5)
// end the gradient at the right edge, but
// probably want to leave the farthest-right 1 or 2 points
// completely transparent
let endPoint = CGPoint(x: bounds.maxX - 2.0, y: 0.5)
// gradient rect starts at the bottom of the opaque rect
r.origin.y = r.size.height - 1
// gradient rect height can extend below the bounds, becuase it will be clipped
r.size.height = bounds.height
inContext.addRect(r)
inContext.clip()
inContext.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsBeforeStartLocation)
}
}
Next, we'll make a UILabel subclass that implements that InvertedGradientLayer as a layer mask:
class CornerFadeLabel: UILabel {
let ivgLayer = InvertedGradientLayer()
override func layoutSubviews() {
super.layoutSubviews()
guard let f = self.font, let t = self.text else { return }
// we only want to fade-out the last line if
// it would be clipped
let constraintRect = CGSize(width: bounds.width, height: .greatestFiniteMagnitude)
let boundingBox = t.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font : f], context: nil)
if boundingBox.height <= bounds.height {
layer.mask = nil
return
}
layer.mask = ivgLayer
ivgLayer.lineHeight = f.lineHeight
ivgLayer.gradWidth = 60.0
ivgLayer.frame = bounds
ivgLayer.setNeedsDisplay()
}
}
and here is a sample view controller showing it in use:
class FadeVC: UIViewController {
let wordWrapFadeLabel: CornerFadeLabel = {
let v = CornerFadeLabel()
v.numberOfLines = 1
v.lineBreakMode = .byWordWrapping
return v
}()
let charWrapFadeLabel: CornerFadeLabel = {
let v = CornerFadeLabel()
v.numberOfLines = 1
v.lineBreakMode = .byCharWrapping
return v
}()
let normalLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 1
return v
}()
let numLinesLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
return v
}()
var numLines: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let sampleText = "This is some example text that will wrap onto multiple lines and fade-out the bottom-right corner instead of truncating or clipping a last line."
wordWrapFadeLabel.text = sampleText
charWrapFadeLabel.text = sampleText
normalLabel.text = sampleText
let stack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 8
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let bStack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.spacing = 8
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let btnUP: UIButton = {
let v = UIButton()
let cfg = UIImage.SymbolConfiguration(pointSize: 28.0, weight: .bold, scale: .large)
let img = UIImage(systemName: "chevron.up.circle.fill", withConfiguration: cfg)
v.setImage(img, for: [])
v.tintColor = .systemGreen
v.widthAnchor.constraint(equalTo: v.heightAnchor).isActive = true
v.addTarget(self, action: #selector(btnUpTapped), for: .touchUpInside)
return v
}()
let btnDown: UIButton = {
let v = UIButton()
let cfg = UIImage.SymbolConfiguration(pointSize: 28.0, weight: .bold, scale: .large)
let img = UIImage(systemName: "chevron.down.circle.fill", withConfiguration: cfg)
v.setImage(img, for: [])
v.tintColor = .systemGreen
v.widthAnchor.constraint(equalTo: v.heightAnchor).isActive = true
v.addTarget(self, action: #selector(btnDownTapped), for: .touchUpInside)
return v
}()
bStack.addArrangedSubview(btnUP)
bStack.addArrangedSubview(numLinesLabel)
bStack.addArrangedSubview(btnDown)
let v1 = UILabel()
v1.text = "Word-wrapping"
v1.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
let v2 = UILabel()
v2.text = "Character-wrapping"
v2.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
let v3 = UILabel()
v3.text = "Normal Label (Truncate Tail)"
v3.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
stack.addArrangedSubview(bStack)
stack.addArrangedSubview(v1)
stack.addArrangedSubview(wordWrapFadeLabel)
stack.addArrangedSubview(v2)
stack.addArrangedSubview(charWrapFadeLabel)
stack.addArrangedSubview(v3)
stack.addArrangedSubview(normalLabel)
stack.setCustomSpacing(20, after: bStack)
stack.setCustomSpacing(20, after: wordWrapFadeLabel)
stack.setCustomSpacing(20, after: charWrapFadeLabel)
view.addSubview(stack)
// dashed border views so we can see the lable frames
let wordBorderView = DashedView()
let charBorderView = DashedView()
let normalBorderView = DashedView()
wordBorderView.translatesAutoresizingMaskIntoConstraints = false
charBorderView.translatesAutoresizingMaskIntoConstraints = false
normalBorderView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(wordBorderView)
view.addSubview(charBorderView)
view.addSubview(normalBorderView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
wordBorderView.topAnchor.constraint(equalTo: wordWrapFadeLabel.topAnchor, constant: 0.0),
wordBorderView.leadingAnchor.constraint(equalTo: wordWrapFadeLabel.leadingAnchor, constant: 0.0),
wordBorderView.trailingAnchor.constraint(equalTo: wordWrapFadeLabel.trailingAnchor, constant: 0.0),
wordBorderView.bottomAnchor.constraint(equalTo: wordWrapFadeLabel.bottomAnchor, constant: 0.0),
charBorderView.topAnchor.constraint(equalTo: charWrapFadeLabel.topAnchor, constant: 0.0),
charBorderView.leadingAnchor.constraint(equalTo: charWrapFadeLabel.leadingAnchor, constant: 0.0),
charBorderView.trailingAnchor.constraint(equalTo: charWrapFadeLabel.trailingAnchor, constant: 0.0),
charBorderView.bottomAnchor.constraint(equalTo: charWrapFadeLabel.bottomAnchor, constant: 0.0),
normalBorderView.topAnchor.constraint(equalTo: normalLabel.topAnchor, constant: 0.0),
normalBorderView.leadingAnchor.constraint(equalTo: normalLabel.leadingAnchor, constant: 0.0),
normalBorderView.trailingAnchor.constraint(equalTo: normalLabel.trailingAnchor, constant: 0.0),
normalBorderView.bottomAnchor.constraint(equalTo: normalLabel.bottomAnchor, constant: 0.0),
])
// set initial number of lines to 1
btnUpTapped()
}
#objc func btnUpTapped() {
numLines += 1
numLinesLabel.text = "Num Lines: \(numLines)"
wordWrapFadeLabel.numberOfLines = numLines
charWrapFadeLabel.numberOfLines = numLines
normalLabel.numberOfLines = numLines
}
#objc func btnDownTapped() {
if numLines == 1 { return }
numLines -= 1
numLinesLabel.text = "Num Lines: \(numLines)"
wordWrapFadeLabel.numberOfLines = numLines
charWrapFadeLabel.numberOfLines = numLines
normalLabel.numberOfLines = numLines
}
}
When running, it looks like this:
The red dashed borders are there just so we can see the frames of the labels. Tapping the up/down arrows will increment/decrement the max number of lines to show in each label.
You should create a CATextLayer with the same text properties as your UILabel.
Fill it with the end of your text you wish to fade.
Then calculate the position of this text segment in your UILabel.
Finally overlay the two.
Here are some aspect explained.

One of the views goes under the other view when doing transform animation with CATransform3DIdentity, even if I use autoreverses

I have reproduced the animation example so it is possible to just copy paste this to see the effect. What I would like is to do the animation on the redview, but I would want it to continue to appear above the green view after the animation, but it seems to go under it after the animation even if I set autoreverses = true. I tried putting redview.transform = .identity in the completion block but it didn't help.
import UIKit
class AnimationTest: UIViewController {
let greenView: UIView = {
let view = UIView()
view.backgroundColor = .green
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let redView: UIView = {
let view = UIView()
view.backgroundColor = .red
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(greenView)
greenView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 150).isActive = true
greenView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50).isActive = true
greenView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50).isActive = true
greenView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -150).isActive = true
view.addSubview(redView)
redView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100).isActive = true
redView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40).isActive = true
redView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40).isActive = true
redView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -100).isActive = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
redView.addGestureRecognizer(tapGesture)
}
#objc func handleTap(sender: UITapGestureRecognizer) {
var transform = CATransform3DIdentity
let angle: CGFloat = 0.1
transform.m34 = -1.0 / 500.0 // [500]: Smaller -> Closer to the 'camera', more distorted
transform = CATransform3DRotate(transform, angle, 0, 1, 0)
let duration = 0.1
let translationAnimation = CABasicAnimation(keyPath: "transform")
translationAnimation.toValue = transform
translationAnimation.duration = duration
translationAnimation.fillMode = .forwards
translationAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
translationAnimation.isRemovedOnCompletion = false
translationAnimation.autoreverses = true
CATransaction.setCompletionBlock {
}
redView.layer.add(translationAnimation, forKey: "translation")
CATransaction.commit()
}
}
EDIT:
Also I have another scenario where I add the view into the keywindow because I want it to appear above tab bars. But now the animation goes into the keywindow. How can I make the same animation without going into the keywindow.
import UIKit
class AnimationTest: UIViewController {
let redView: UIView = {
let view = UIView()
view.backgroundColor = .red
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
}
override func viewWillAppear(_ animated: Bool) {
guard let window = UIApplication.shared.keyWindow else { return }
let y = 16 + 10 + 30 + window.safeAreaInsets.top
let width = UIScreen.main.bounds.width - 8 - 8
let height = UIScreen.main.bounds.height - window.safeAreaInsets.top - window.safeAreaInsets.bottom - 16 - 10 - 30 - 4 - 50
redView.frame = CGRect(x: 8, y: y, width: width, height: height)
window.addSubview(redView)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
redView.addGestureRecognizer(tapGesture)
}
#objc func handleTap(sender: UITapGestureRecognizer) {
var transform = CATransform3DIdentity
let angle: CGFloat = 0.1
transform.m34 = -1.0 / 500.0 // [500]: Smaller -> Closer to the 'camera', more distorted
transform = CATransform3DRotate(transform, angle, 0, 1, 0)
let duration = 0.1
let translationAnimation = CABasicAnimation(keyPath: "transform")
translationAnimation.fromValue = CATransform3DIdentity
translationAnimation.toValue = transform
translationAnimation.duration = duration
translationAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
translationAnimation.autoreverses = true
CATransaction.setCompletionBlock {
}
redView.layer.add(translationAnimation, forKey: "translation") // Key doesn't matter, just call it translation.
CATransaction.commit()
}
}
You need to:
Set the fromValue to CATransform3DIdentity
Remove translationAnimation.isRemovedOnCompletion = false
I also cleaned up some of your other code. You don't need the setCompletionBlock or CATransaction at all. You also don't need fillMode.
#objc func handleTap(sender: UITapGestureRecognizer) {
var transform = CATransform3DIdentity
let angle: CGFloat = 0.1
transform.m34 = -1.0 / 500.0 // [500]: Smaller -> Closer to the 'camera', more distorted
transform = CATransform3DRotate(transform, angle, 0, 1, 0)
let duration = 0.5
let translationAnimation = CABasicAnimation(keyPath: "transform")
translationAnimation.fromValue = CATransform3DIdentity /// here!
translationAnimation.toValue = transform
translationAnimation.duration = duration
translationAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
translationAnimation.autoreverses = true
redView.layer.add(translationAnimation, forKey: "transformKey")
}
Before
After
Edit: Red view sinks under window
You will need to adjust the layer's z position to be higher.
override func viewWillAppear(_ animated: Bool) {
guard let window = UIApplication.shared.keyWindow else { return }
let y = 16 + 10 + 30 + window.safeAreaInsets.top
let width = UIScreen.main.bounds.width - 8 - 8
let height = UIScreen.main.bounds.height - window.safeAreaInsets.top - window.safeAreaInsets.bottom - 16 - 10 - 30 - 4 - 50
redView.frame = CGRect(x: 8, y: y, width: width, height: height)
window.addSubview(redView)
redView.layer.zPosition = 100 /// here!
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
redView.addGestureRecognizer(tapGesture)
}
0 (Default)
10
100
Half is completely obscured
Part of the view sinks
Completely fine

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

UIScrollView draw ruler using drawRect

I am trying to draw a ruler on top of UIScrollView. The way I do it is by adding a custom view called RulerView. I add this rulerView to superview of scrollView setting its frame to be same as frame of scrollView. I then do custom drawing to draw lines as scrollView scrolls. But the drawing is not smooth, it stutters as I scroll and the end or begin line suddenly appears/disappears. What's wrong in my drawRect?
class RulerView: UIView {
public var contentOffset = CGFloat(0) {
didSet {
self.setNeedsDisplay()
}
}
public var contentSize = CGFloat(0)
let smallLineHeight = CGFloat(4)
let bigLineHeight = CGFloat(10)
override open func layoutSubviews() {
super.layoutSubviews()
self.backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect) {
UIColor.white.set()
let contentWidth = max(rect.width, contentSize)
let lineGap:CGFloat = 5
let totalNumberOfLines = Int(contentWidth/lineGap)
let startIndex = Int(contentOffset/lineGap)
let endIndex = Int((contentOffset + rect.width)/lineGap)
let beginOffset = contentOffset - CGFloat(startIndex)*lineGap
if let context = UIGraphicsGetCurrentContext() {
for i in startIndex...endIndex {
let path = UIBezierPath()
path.move(to: CGPoint(x: beginOffset + CGFloat(i - startIndex)*lineGap , y:0))
path.addLine(to: CGPoint(x: beginOffset + CGFloat(i - startIndex)*lineGap, y: i % 5 == 0 ? bigLineHeight : smallLineHeight))
path.lineWidth = 0.5
path.stroke()
}
}
}
And in the scrollview delegate, I set this:
//MARK:- UIScrollViewDelegate
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.x
rulerView.contentSize = scrollView.contentSize.width
rulerView.contentOffset = offset
}
Your override func draw(_ rect: CGRect) is very "heavy." I think you'll get much better performance by using a shape layer for your "tick marks" and letting UIKit handle the drawing.
Edit - as per comments
Added numbering to the tick marks using CATextLayer as sublayers.
Here's a sample RulerView (using your tick mark dimensions and spacing):
class RulerView: UIView {
public var contentOffset: CGFloat = 0 {
didSet {
layer.bounds.origin.x = contentOffset
}
}
public var contentSize = CGFloat(0) {
didSet {
updateRuler()
}
}
let smallLineHeight: CGFloat = 4
let bigLineHeight: CGFloat = 10
let lineGap:CGFloat = 5
// numbers under the tick marks
// with 12-pt system font .light
// 40-pt width will fit up to 5 digits
let numbersWidth: CGFloat = 40
let numbersFontSize: CGFloat = 12
var shapeLayer: CAShapeLayer!
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
shapeLayer = self.layer as? CAShapeLayer
// these properties don't change
backgroundColor = .clear
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.lineWidth = 0.5
shapeLayer.masksToBounds = true
}
func updateRuler() -> Void {
// size is set by .fontSize, so ofSize here is ignored
let numbersFont = UIFont.systemFont(ofSize: 1, weight: .light)
let pth = UIBezierPath()
var x: CGFloat = 0
var i = 0
while x < contentSize {
pth.move(to: CGPoint(x: x, y: 0))
pth.addLine(to: CGPoint(x: x, y: i % 5 == 0 ? bigLineHeight : smallLineHeight))
// number every 10 ticks - change as desired
if i % 10 == 0 {
let layer = CATextLayer()
layer.contentsScale = UIScreen.main.scale
layer.font = numbersFont
layer.fontSize = numbersFontSize
layer.alignmentMode = .center
layer.foregroundColor = UIColor.white.cgColor
// if we want to number by tick count
layer.string = "\(i)"
// if we want to number by point count
//layer.string = "\(i * Int(lineGap))"
layer.frame = CGRect(x: x - (numbersWidth * 0.5), y: bigLineHeight, width: numbersWidth, height: numbersFontSize)
shapeLayer.addSublayer(layer)
}
x += lineGap
i += 1
}
shapeLayer.path = pth.cgPath
}
}
and here's a sample controller class to demonstrate:
class RulerViewController: UIViewController, UIScrollViewDelegate {
var rulerView: RulerView = RulerView()
var scrollView: UIScrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
[scrollView, rulerView].forEach {
view.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
}
// sample scroll content will be a horizontal stack view
// with 30 labels
// spaced 20-pts apart
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.spacing = 20
for i in 1...30 {
let v = UILabel()
v.textAlignment = .center
v.backgroundColor = .yellow
v.text = "Label \(i)"
stack.addArrangedSubview(v)
}
scrollView.addSubview(stack)
let g = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// scroll view 20-pts Top / Leading / Trailing
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// scroll view Height: 60-pts
scrollView.heightAnchor.constraint(equalToConstant: 60.0),
// stack view 20-pts Top, 0-pts Leading / Trailing / Bottom (to scroll view's content layout guide)
stack.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 20.0),
stack.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
stack.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
stack.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// ruler view 4-pts from scroll view Bottom
rulerView.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 4.0),
rulerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
// ruler view 0-pts from scroll view Leading / Trailing (equal width and horizontal position of scroll view)
rulerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
// ruler view Height: 24-pts (make sure it's enough to accomodate ruler view's bigLineHeight plus numbering height)
rulerView.heightAnchor.constraint(equalToConstant: 24.0),
])
scrollView.delegate = self
// so we can see the sroll view frame
scrollView.backgroundColor = .red
// if we want to see the rulerView's frame
//rulerView.backgroundColor = .brown
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// this is when we know the scroll view's content size
rulerView.contentSize = scrollView.contentSize.width
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
// update rulerView's x-offset
rulerView.contentOffset = scrollView.contentOffset.x
}
}
Output:
the tick marks (and numbers) will, of course, scroll left-right synched with the scroll view.

Add imageview to a scrollview page control

My code below is a basic scrollview using page control which uses basic colors on each screen. I want to display an imageview over the uiview which displays the basic colors. I want to call var pics from the asset file in Xcode.
var colors:[UIColor] = [UIColor.red, UIColor.blue, UIColor.green, UIColor.yellow]
var pics = ["a","b","c","d"]
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(scrollView)
for index in 0..<4 {
frame.origin.x = self.scrollView.frame.size.width * CGFloat(index)
frame.size = self.scrollView.frame.size
let subView = UIView(frame: frame)
subView.backgroundColor = colors[index]
self.scrollView .addSubview(subView)
}
func configurePageControl() {
self.pageControl.numberOfPages = colors.count
}
Here I suppose that you have 4 images attached in your project named 0,1,2,3 png , try this
import UIKit
class ViewController: UIViewController , UIScrollViewDelegate {
let scrollView = UIScrollView()
let pageCon = UIPageControl()
override func viewDidLoad() {
super.viewDidLoad()
let viewsCount = 4
var prevView = self.view!
scrollView.delegate = self
scrollView.isPagingEnabled = true
pageCon.numberOfPages = viewsCount
pageCon.currentPage = 0
pageCon.tintColor = .green
pageCon.currentPageIndicatorTintColor = .orange
pageCon.backgroundColor = .blue
pageCon.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
view.insertSubview(pageCon, aboveSubview: scrollView)
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant:20),
scrollView.heightAnchor.constraint(equalToConstant: 400),
pageCon.centerXAnchor.constraint(equalTo: view.centerXAnchor),
pageCon.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant:-20),
])
for i in 0..<viewsCount {
let imageV = UIImageView()
imageV.image = UIImage(named: "\(i).png")
imageV.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(imageV)
if prevView == self.view {
imageV.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
}
else {
imageV.leadingAnchor.constraint(equalTo: prevView.trailingAnchor).isActive = true
}
NSLayoutConstraint.activate([
imageV.topAnchor.constraint(equalTo: scrollView.topAnchor),
imageV.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
imageV.widthAnchor.constraint(equalToConstant: self.view.frame.width),
imageV.heightAnchor.constraint(equalToConstant: 400)
])
if i == viewsCount - 1 {
imageV.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
}
prevView = imageV
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
pageCon.currentPage = Int(scrollView.contentOffset.x / self.view.frame.width)
}
}

Resources