Calling 'scrollView.zoom' Does Not Zoom - ios

For some reason scrollView.zoom does not zoom in on an imageView.
I have an imageView inside a scrollView.
in ViewDidLoad:
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 2.0
scrollView.delegate = self
viewForZooming:
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
Now, I'm calling below after both scrollView and imageView are initialized.
var scale: CGFloat = 2
let location = CGPoint(x: imageView.frame.width/2, y: imageView.frame.height/2)
scrollView.zoom(to: zoomRectForScale(scale, center: location), animated: true)
Nothing is happening after scrollView.zoom is called. I tried doing
view.setNeedsDisplay()
view.layoutIfNeeded()
Still nothing happens, imageView is not zooming in.
UPDATE:
As requested, here is the code for scrollView and imageView initialization:
func createscrollView(image: UIImage?){
if image == nil{
let img = UIImage(named: "demo image.jpg")
let imgCorrected = scaleAndOrient(image: img!)
//created user selecged images
userSelectedImage_UI = img
userSelectedImage_UI_Corrected = imgCorrected
}
// create image scroll view
scrollView = UIScrollView(frame: CGRect(x: 0, y: 0,width: 100, height: 100))
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
scrollView.bouncesZoom = false
scrollView.bounces = false
scrollView.contentMode = .scaleAspectFill
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0),
scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -150),
scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0)
])
// add image view to scrollview
imageView = UIImageView(frame: CGRect(x: 0, y: 0,width: 100, height: 100))
scrollView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 0),
imageView.leftAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leftAnchor, constant: 0),
imageView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: 0),
imageView.rightAnchor.constraint(equalTo: scrollView.contentLayoutGuide.rightAnchor, constant: 0),
imageView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 1),
imageView.heightAnchor.constraint(equalTo: scrollView.heightAnchor, multiplier: 1)
])
imageView.contentMode = .scaleAspectFit
if image == nil{
imageView.image = userSelectedImage_UI_Corrected
} else {
imageView.image = scaleAndOrient(image: image!)
}
}

Based on comments...
It sounds like you are creating / setting up your scrollView and its content imageView from viewDidLoad() and then immediately trying to "zoom" it.
If so, that will be problematic, as auto-layout hasn't finished laying out the UI elements.
You can call it from:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
var scale: CGFloat = 2
let location = CGPoint(x: imageView.frame.width/2, y: imageView.frame.height/2)
scrollView.zoom(to: zoomRectForScale(scale, center: location), animated: true)
}

I have a feeling the problem is in your zoomRectForScale function, though it isn't posted. It should be something like this:
func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect {
var zoomRect = CGRect.zero
zoomRect.size.height = imageView.frame.size.height / scale
zoomRect.size.width = imageView.frame.size.width / scale
let newCenter = scrollView.convert(center, from: imageView)
zoomRect.origin.x = newCenter.x - (zoomRect.size.width / 2.0)
zoomRect.origin.y = newCenter.y - (zoomRect.size.height / 2.0)
return zoomRect
}
Additionally, I've verified that double-tapping to zoom works to the correct location using:
var gestureRecognizer: UITapGestureRecognizer!
// Sets up the gesture recognizer that receives double taps to auto-zoom
func setupGestureRecognizer() {
gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap))
gestureRecognizer.numberOfTapsRequired = 2
scrollView.addGestureRecognizer(gestureRecognizer)
}
// Handles a double tap by either resetting the zoom or zooming to where was tapped
#IBAction func handleDoubleTap() {
if scrollView.zoomScale == 1 {
scrollView.zoom(to: zoomRectForScale(scrollView.maximumZoomScale, center: gestureRecognizer.location(in: gestureRecognizer.view)), animated: true)
} else {
scrollView.setZoomScale(1, animated: true)
}
}
Don't forget to call this in viewDidLoad:
setupGestureRecognizer()
If this doesn't solve your issue, please adjust your question with missing code functions and variable declarations.

Related

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

how to create the transition with animations in the link

I have to create a transition animation like in the link below.. please help if anyone knows how to do this
https://dribbble.com/shots/6488787-Music-App-UI-Pages
I tried to zoom image.. and i need this view in every view controller in the app so i tried to do it as a base view controller
I tried to add the bar in the right side of the screen. but now struggling to do the effect as the design shows
import UIKit
class BaseViewController: UIViewController {
var location = CGPoint(x: 0, y: 0)
var buttonImage = UIImageView()
var strechyImage = UIImageView()
var boxviewImage = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
setUpView()
}
override func touchesMoved(_ touches: Set<UITouch>?, with event: UIEvent?) {
let touch : UITouch! = touches?.first
location = touch.location(in: self.view)
strechyImage.center = location
}
func setUpView() {
let viewHeight = UIScreen.main.bounds.height
//FloatButton.setImage(UIImage(named: "Icon ionic-md-sad"), for: .normal)
boxviewImage.image = UIImage(named: "nfc box")
boxviewImage.frame = CGRect(x: 0, y: 0, width: 5, height: 200)
let currentWindow: UIWindow? = UIApplication.shared.keyWindow
currentWindow?.addSubview(boxviewImage)
//view.addSubview(boxviewImage)
strechyImage.image = UIImage(named: "nfc curve")
strechyImage.frame = CGRect(x: 0, y: 0, width: 100, height: 200)
view.addSubview(strechyImage)
buttonImage.image = UIImage(named: "nfc floating")
buttonImage.frame = CGRect(x: 0, y: 0, width: 100, height: 200)
strechyImage.addSubview(buttonImage)
//view.bringSubviewToFront(buttonImage)
buttonImage.translatesAutoresizingMaskIntoConstraints = false
strechyImage.translatesAutoresizingMaskIntoConstraints = false
boxviewImage.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
strechyImage.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 1),
strechyImage.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.142),
strechyImage.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.3),
strechyImage.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -(viewHeight / 20)),
buttonImage.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.1),
buttonImage.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.055),
buttonImage.centerXAnchor.constraint(equalToSystemSpacingAfter: strechyImage.centerXAnchor, multiplier: -0.9),
buttonImage.centerYAnchor.constraint(equalToSystemSpacingBelow: strechyImage.centerYAnchor, multiplier: 0),
boxviewImage.trailingAnchor.constraint(equalTo: currentWindow!.safeAreaLayoutGuide.trailingAnchor, constant: 2.2),
boxviewImage.widthAnchor.constraint(equalTo: currentWindow!.widthAnchor, multiplier: 0.0255),
boxviewImage.heightAnchor.constraint(equalToConstant: viewHeight),
])
}
}
Need to create this transition as close as possible to the design in the link

Image view moves to aside when scrolling vertically in scroll view

I've got a view controller that has a UIView (featuredStoryView) and inside that view, I've added a scroll view (scrollView) and inside the scroll view, I've added an ImageView (bookCover), one on top of the other. Here is the code for all of those elements:
func setupFeaturedStoryView() {
featuredStoryView.backgroundColor = .white
featuredStoryView.layer.cornerRadius = 12
// Let the user tap on the featured story image view
featuredStoryView.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(featuredStoryViewTapped))
featuredStoryView.addGestureRecognizer(tapGesture)
view.addSubview(featuredStoryView)
addFeaturedStoryViewConstraints()
}
func setupScrollView() {
scrollView.backgroundColor = UIColor(red: 10/255, green: 10/255, blue: 20/255, alpha: 0.5)
scrollView.layer.cornerRadius = 15
featuredStoryView.addSubview(scrollView)
// Add the constraints to the scroll view
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraint(equalTo: featuredStoryView.topAnchor).isActive = true
scrollView.leadingAnchor.constraint(equalTo: featuredStoryView.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: featuredStoryView.trailingAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: featuredStoryView.bottomAnchor).isActive = true
}
func setupBookCover() {
bookCover.backgroundColor = .yellow
bookCover.layer.cornerRadius = 15
bookCover.contentMode = .scaleAspectFill
scrollView.addSubview(bookCover)
addBookCoverConstraints()
}
// Add the constraints to the featured story view
func addFeaturedStoryViewConstraints() {
featuredStoryView.translatesAutoresizingMaskIntoConstraints = false
featuredStoryView.topAnchor.constraint(equalTo: view.centerYAnchor, constant: -130).isActive = true
featuredStoryView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100).isActive = true
featuredStoryView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -100).isActive = true
featuredStoryView.bottomAnchor.constraint(equalTo: view.centerYAnchor, constant: 130).isActive = true
}
// Add constraints to the book cover
func addBookCoverConstraints() {
bookCover.translatesAutoresizingMaskIntoConstraints = false
bookCover.topAnchor.constraint(equalTo: featuredStoryView.topAnchor).isActive = true
bookCover.leadingAnchor.constraint(equalTo: featuredStoryView.leadingAnchor).isActive = true
bookCover.trailingAnchor.constraint(equalTo: featuredStoryView.trailingAnchor).isActive = true
bookCover.bottomAnchor.constraint(equalTo: featuredStoryView.bottomAnchor).isActive = true
}
When you tap on the featuredStoryView, the following code snippet runs:
#objc func featuredStoryViewTapped() {
scrollView.contentSize.height = 1500
let animator = UIViewPropertyAnimator(duration: 0.6, dampingRatio: 0.8) {
self.featuredStoryView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)
self.scrollView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)
self.view.layoutIfNeeded()
self.bookCover.frame = CGRect(x: self.featuredStoryView.frame.midX - (bookCoverWidth / 2), y: 90, width: bookCoverWidth, height: bookCoverHeight)
self.view.layoutIfNeeded()
}
animator.startAnimation()
}
Now take a look at this Gif:
The problem here is, when I scroll, the yellow imageView(bookCover) moves to the top left corner, why does this happen? It should just go up with the scroll view. Is there any way to fix this?
You can not set frame directly like self.featuredStoryView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height) and contentSize like scrollView.contentSize.height = 1500 in auto layout. So try to set constraints as per your requirement.

How can I create reusable view controllers inside of UIScrollView?

I am trying to create a full page sized horizontally scrolling UIScrollView. On each page I am adding instances of the same UIViewController class. I would like to create some kind of reusable functionality to conserve both memory and processor use needed. Below is a basic implementation I have created with some toying around with how reusability might work although Im not quite sure. Thank you for any help you can offer.
Current UIScroll ViewController Model
let scrollView:UIScrollView = {
let scrollView = UIScrollView(frame: CGRect.zero)
scrollView.isPagingEnabled = true
scrollView.backgroundColor = UIColor.gray
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = true
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
scrollView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: self.view.bounds.size)
self.view.addSubview(scrollView)
scrollView.delegate = self
scrollView.contentSize = CGSize(width: 3 * self.view.frame.width, height: self.view.frame.height)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0),
scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0),
scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0),
scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0)
])
let viewController1 = UIViewController()
viewController1.view.backgroundColor = UIColor.red
let viewController2 = UIViewController()
viewController2.view.backgroundColor = UIColor.blue
let viewController3 = UIViewController()
viewController3.view.backgroundColor = UIColor.green
self.addChild(viewController1)
viewController1.view.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: self.view.bounds.size)
scrollView.addSubview(viewController1.view)
addViewControllerContraints(viewController: viewController1, index: 0)
viewController1.didMove(toParent: self)
self.addChild(viewController2)
viewController2.view.frame = CGRect(origin: CGPoint(x: self.view.bounds.width, y: 0), size: self.view.bounds.size)
scrollView.addSubview(viewController2.view)
addViewControllerContraints(viewController: viewController2, index: 1)
viewController2.didMove(toParent: self)
self.addChild(viewController3)
viewController3.view.frame = CGRect(origin: CGPoint(x: 2 * self.view.bounds.width, y: 0), size: self.view.bounds.size)
scrollView.addSubview(viewController3.view)
addViewControllerContraints(viewController: viewController3, index: 2)
viewController3.didMove(toParent: self)
}
func addViewControllerContraints( viewController: UIViewController, index:Int){
guard let view = viewController.view else{
print("View found nil")
return
}
view.translatesAutoresizingMaskIntoConstraints = false
let offset:CGFloat = UIScreen.main.bounds.width * CGFloat(index)
print("Offset: \(offset)")
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0),
view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: offset),
view.heightAnchor.constraint(equalToConstant: self.view.bounds.height),
view.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
}
Is there a good way to create some type of reuse functionality this is something I was playing around with based on This Answer although I realize that is primarily for UIPageViewControllers where allocation and deallocation of UIViewController's is handled for you.
Possible Reuse Functionality
var reuseableViewControllers:[UIViewController] = [UIViewController]()
private func unusedViewController() -> UIViewController {
let unusedViewControllers = reuseableViewControllers.filter { $0.parent == nil }
if let someUnusedViewController = unusedViewControllers.first {
return someUnusedViewController
} else {
let newViewController = UIViewController()
reuseableViewControllers.append(newViewController)
return newViewController
}
}

Resources