Cannot Scroll Left or right side of UIImageView in UIScrollView - Swift - ios

I have a view inside a scrollview which contains imageview. Using the viewForZooming trying to zoom in and zoom out functionality for the image view. I can zoom in and zoom out the image. However, I have trouble in scrolling left side or right side of the image after zooming in.
My Main (Base) looks like,
[! [Main Base Hierarchy here] (https://i.stack.imgur.com/BCoPF.png)] (https://i.stack.imgur.com/BCoPF.png)
My Code,
#IBOutlet weak var scrollView: UIScrollView!
In ViewDidLoad(),
scrollView.maximumZoomScale = 4
scrollView.minimumZoomScale = 1
scrollView.contentSize = Img.frame.size
scrollView.delegate = self
My extension code,
extension CardsViewController: UIScrollViewDelegate{
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
let trailingSpace: CGFloat = 10
UIView.animate(withDuration: 1.0, delay: 0.0, options: [], animations: {
let screenSize = UIScreen.main.bounds.size
let x = screenSize.width - self.cardbgView.frame.size.width - trailingSpace
let y = self.cardbgView.frame.origin.y
self.cardbgView.frame =
CGRect(x: x, y: y, width: self.cardbgView.frame.size.width, height:self.cardbgView.frame.size.height)
})
return cardbgView
}
What I am trying to achieve here is to zoom in the image and scroll left side and right side as well as move the zoomed image all sides to view/read the image

Related

How do I make an image act like a map (zoom/pan/map markers)?

Swift 5 / Xcode 12.4
I've got a single png image that's downloaded into the Documents folder and then loaded at runtime (currently as UIImage). This image has to act as some type of map:
Pinch zoom
Pan
I want to place some type of map marker (e.g. a dot) in specific spots: The user can click on them (to open a popup with more information) and they move according to the zoom/pan but always stay the same size.
Not full screen but inside a specific area in my ViewController.
I already did the same thing in Android but all Java map libraries I found require tiles (I've only got a single big image), so I ended up using a "zoom/pan" library (also lets you set the maximum zoom) and created my own invisible image sublayer for the markers.
For iOS I've found the Goggle Maps SDK and the Apple MapKit so far but they both look like they load rl map data and you can't set the actual image - is this possible with either of them?
I haven't found a zoom/pan library for iOS yet (at least one that's not 5+ years old) either, so how do I best accomplish this? Write my own zoom/pan listeners and use some type of sublayer (that moves with the parent) for the map markers - is that the way to go/what UI objects do I have to use?
this will help with the pinch to zoom - https://stackoverflow.com/a/58558272/2481602
this will help you to achieve a pan - How do I pan the image inside a UIImageView?
As far as the imposed markers, that you will have to manually handle the transformations and apply the the anchor points of the marker image. the documentation here - https://developer.apple.com/documentation/coregraphics/cgaffinetransform and this supplies a loose explanation - https://medium.com/weeronline/swift-transforms-5981398b437d
it not being full screen just needs a view to hold the scrollView that will hold the map in the location on the screen that you want.
Not a full answer but hopefully this will all point you in the right direction
Use a UIScrollView for the pinch/zoom/pan. To add the markers add them to a container view atop the scroll view, and respond to scroll view changes (scrollViewDidEndZooming, scrollViewDidZoom, scrollViewDidEndDragging...) by updating the positions of the annotation views in the container - you'll need to use UIView's convert to convert between coordinate systems, setting the center of annotation views to the appropriate point converted from your scrollview to the container view. Container view should be same size as scrollview, not scrollview's content.
Or you could add the annotations into the scrollview's content but then you have to update the transforms of those views to counter-magnify them as you zoom in.
One approach...
Use "standard" scroll view zoom/pan functionality
Use image view as viewForZooming in the scroll view
add "marker views" to the scroll view as siblings of the image view (not subviews of the image view)
For the marker positions, calculate the percent location. So, for example, if your image is 1000x1000, and you want a marker at 100,200, that marker would have a "point percentage" of 0.1,0.2. When the image view is zoomed, change the frame origin of the marker by its location percentages.
Here is a complete example (done very quickly, so just to get you going)...
I used this 1600 x 1600 image, with marker locations:
A simple "marker view" class:
class MarkerView: UILabel {
var yPCT: CGFloat = 0
var xPCT: CGFloat = 0
}
And the controller class:
class ZoomWithMarkersViewController: UIViewController, UIScrollViewDelegate {
let imgView: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var points: [CGPoint] = [
CGPoint(x: 200, y: 200),
CGPoint(x: 800, y: 300),
CGPoint(x: 500, y: 700),
CGPoint(x: 1100, y: 900),
CGPoint(x: 300, y: 1200),
CGPoint(x: 1300, y: 1400),
]
var markers: [MarkerView] = []
override func viewDidLoad() {
super.viewDidLoad()
// make sure we have an image
guard let img = UIImage(named: "points1600x1600") else {
fatalError("Could not load image!!!!")
}
// set the image
imgView.image = img
// add the image view to the scroll view
scrollView.addSubview(imgView)
// add scroll view to view
view.addSubview(scrollView)
// respect safe area
let safeG = view.safeAreaLayoutGuide
// to save on typing
let contentG = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// scroll view inset 20-pts on each side
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
// square (1:1 ratio)
scrollView.heightAnchor.constraint(equalTo: scrollView.widthAnchor),
// center vertically
scrollView.centerYAnchor.constraint(equalTo: safeG.centerYAnchor),
// constrain all 4 sides of image view to scroll view's Content Layout Guide
imgView.topAnchor.constraint(equalTo: contentG.topAnchor),
imgView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
imgView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
// we will want zoom scale of 1 to show the "native size" of the image
imgView.widthAnchor.constraint(equalToConstant: img.size.width),
imgView.heightAnchor.constraint(equalToConstant: img.size.height),
])
// create marker views and
// add them as subviews of the scroll view
// add them to our array of marker views
var i: Int = 0
points.forEach { pt in
i += 1
let v = MarkerView()
v.textAlignment = .center
v.font = .systemFont(ofSize: 12.0)
v.text = "\(i)"
v.backgroundColor = UIColor.green.withAlphaComponent(0.5)
scrollView.addSubview(v)
markers.append(v)
v.yPCT = pt.y / img.size.height
v.xPCT = pt.x / img.size.width
v.frame = CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0))
}
// assign scroll view's delegate
scrollView.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print(#function)
guard let img = imgView.image else { return }
// max scale is 1.0 (original image size)
scrollView.maximumZoomScale = 1.0
// min scale fits the image in the scroll view frame
scrollView.minimumZoomScale = scrollView.frame.width / img.size.width
// start at min scale (so full image is visible)
scrollView.zoomScale = scrollView.minimumZoomScale
// just to make the markers "appear" nicely
markers.forEach { v in
v.center = CGPoint(x: scrollView.bounds.midX, y: scrollView.bounds.midY)
v.alpha = 0.0
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// animate the markers into position
UIView.animate(withDuration: 1.0, animations: {
self.markers.forEach { v in
v.alpha = 1.0
}
self.updateMarkers()
})
}
func updateMarkers() -> Void {
markers.forEach { v in
let x = imgView.frame.origin.x + v.xPCT * imgView.frame.width
let y = imgView.frame.origin.y + v.yPCT * imgView.frame.height
// for example:
// put bottom-left corner of marker at coordinates
v.frame.origin = CGPoint(x: x, y: y - v.frame.height)
// or
// put center of marker at coordinates
//v.center = CGPoint(x: x, y: y)
}
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateMarkers()
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imgView
}
}
I'm placing the markers so their bottom-left corner is at the marker-point.
It starts like this:
and looks like this after zooming-in on marker #3:

How to animate image change in ScrollView Swift

i have a scrollview that contains an array of image, I would like to animate it changing image from right to left. My scrollView:
#IBOutlet weak var scrollView: UIScrollView!
var imageArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.frame = view.frame
imageArray = [UIImage(named:"image3")!,UIImage(named:"image4")!,UIImage(named:"image1")!]
for i in 0..<imageArray.count{
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.image = imageArray[i]
let xPosition = self.view.frame.width * CGFloat(i)
imageView.frame = CGRect(x: xPosition, y: 0, width: self.scrollView.frame.width, height: 205)
scrollView.contentSize.width = scrollView.frame.width * CGFloat(i + 1)
scrollView.addSubview(imageView)
}
startAnimating()
}
and for animation I did it with :
func startAnimating() {
UIView.animateKeyframes(withDuration: 2, delay: 0, options: .repeat, animations: {
self.scrollView.center.x += self.view.bounds.width
}, completion: nil)
}
However, it is moving from left to right and not right to left, plus it's not changing images...How should i go about this? Any guidance is much appreciated! Thank you
You're moving the scrollview within it's parent view. The image views are children of the scrollview (they're inside the scrollview), so they're simply moving with it.
To scroll the content within your UIScrollView you should be using its contentOffset property:
var newOffset = scrollView.contentOffset
newOffset.x += scrollView.frame.size.width
UIView.animate(withDuration: 2, delay: 0, options, .repeat, animations: {
self.scrollView.contentOffset = newOffset
})
Also, you shouldn't be using animateKeyFrames for this. Use the animate(withDuration:delay:options:animations) method instead.
It's also worth thinking about whether a UIScrollView is the right choice here, and it really comes down to how many images are going to inside it.
If it's always going to be a small number of images, then a scrollview is a fine choice.
However, if there are ever going to be a larger number images to display, then UICollectionView would be a better choice as it reuses its subviews.

Swift: How to zoom an image as I scroll up in scrollview

I'm trying to zoom my image view as I scroll the scrollView past the top of the screen. Here's my code:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.y
if (offset <= 0) {
let ratio: CGFloat = -offset*1.0 / UIScreen.main.bounds.height
self.coverImageView.transform = CGAffineTransform(scaleX: 1.0 + ratio, y: 1.0 + ratio)
}
}
This zooms the image as I scroll up, but because I am also scrolling up, my view goes down, and reveals the white background behind the image as it expands. How do I prevent that from happening?
It sounds like a scroll view is not really suited to what you are trying to do. How about using a gesture recognizer instead? Something along these lines:
override func viewDidLoad() {
super.viewDidLoad()
coverImageView.isUserInteractionEnabled = true
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPan))
coverImageView.addGestureRecognizer(panGestureRecognizer)
}
func didPan(panGestureRecognizer: UIPanGestureRecognizer) {
let translation = panGestureRecognizer.translation(in: coverImageView)
if translation.y > 0 {
let zoomRatio = (translation.y * 0.1) + 1.0
coverImageView.transform = CGAffineTransform(scaleX: zoomRatio, y: zoomRatio)
}
}
You'll have to play around to get it to behave exactly how you want, but it should be enough to get you started.

Move UIImageView Independently from its Mask in Swift

I'm trying to mask a UIImageView in such a way that it would allow the user to drag the image around without moving its mask. The effect would be similar to how one can position an image within the Instagram app essentially allowing the user to define the crop region of the image.
Here's an animated gif to demonstrate what I'm after.
Here's how I'm currently masking the image and repositioning it on drag/pan events.
import UIKit
class ViewController: UIViewController {
var dragDelta = CGPoint()
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
attachMask()
// listen for pan/drag events //
let pan = UIPanGestureRecognizer(target:self, action:#selector(onPanGesture))
pan.maximumNumberOfTouches = 1
pan.minimumNumberOfTouches = 1
self.view.addGestureRecognizer(pan)
}
func onPanGesture(gesture:UIPanGestureRecognizer)
{
let point:CGPoint = gesture.locationInView(self.view)
if (gesture.state == .Began){
print("begin", point)
// capture our drag start position
dragDelta = CGPoint(x:point.x-imageView.frame.origin.x, y:point.y-imageView.frame.origin.y)
} else if (gesture.state == .Changed){
// update image position based on how far we've dragged from drag start
imageView.frame.origin.y = point.y - dragDelta.y
} else if (gesture.state == .Ended){
print("ended", point)
}
}
func attachMask()
{
let mask = CAShapeLayer()
mask.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 100, width: imageView.frame.size.width, height: 400), cornerRadius: 5).CGPath
mask.anchorPoint = CGPoint(x: 0, y: 0)
mask.fillColor = UIColor.redColor().CGColor
view.layer.addSublayer(mask)
imageView.layer.mask = mask;
}
}
This results in both the image and mask moving together as you see below.
Any suggestions on how to "lock" the mask so the image can be moved independently underneath it would be very much appreciated.
Moving a mask and frame separately from each other to reach this effect isn't the best way to go about doing this. Most apps that do this sort of effect do the following:
Add a UIScrollView to the root view (with panning/zooming enabled)
Add a UIImageView to the UIScrollView
Size the UIImageView such that it has a 1:1 ratio with the image
Set the contentSize of the UIScrollView to match that of the UIImageView
The user can now pan around and zoom into the UIImageView as needed.
Next, if you're, say, cropping the image:
Get the visible rectangle (taken from Getting the visible rect of an UIScrollView's content)
CGRect visibleRect = [scrollView convertRect:scrollView.bounds toView:zoomedSubview];
Use whatever cropping method you'd like on the UIImage to get the necessary content.
This is the smoothest way to handle this kind of interaction and the code stays pretty simple!
Just figured it out. Setting the CAShapeLayer's position property to the inverse of the UIImageView's position as it's dragged will lock the CAShapeLayer in its original position however CoreAnimation by default will attempt to animate it whenever its position is reassigned.
This can be disabled by wrapping both position settings within a CATransaction as shown below.
func onPanGesture(gesture:UIPanGestureRecognizer)
{
let point:CGPoint = gesture.locationInView(self.view)
if (gesture.state == .Began){
print("begin", point)
// capture our drag start position
dragDelta = CGPoint(x:point.x-imageView.frame.origin.x, y:point.y-imageView.frame.origin.y)
} else if (gesture.state == .Changed){
// update image & mask positions based on the distance dragged
// and wrap both assignments in a CATransaction transaction to disable animations
CATransaction.begin()
CATransaction.setDisableActions(true)
mask.position.y = dragDelta.y - point.y
imageView.frame.origin.y = point.y - dragDelta.y
CATransaction.commit()
} else if (gesture.state == .Ended){
print("ended", point)
}
}
UPDATE
Here's an implementation of what I believe AlexKoren is suggesting. This approach nests a UIImageView within a UIScrollView and uses the UIScrollView to mask the image.
class ViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
var imageView:UIImageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
let image = UIImage(named: "point-bonitas")
imageView.image = image
imageView.frame = CGRectMake(0, 0, image!.size.width, image!.size.height);
scrollView.delegate = self
scrollView.contentMode = UIViewContentMode.Center
scrollView.addSubview(imageView)
scrollView.contentSize = imageView.frame.size
let scale = scrollView.frame.size.width / scrollView.contentSize.width
scrollView.minimumZoomScale = scale
scrollView.maximumZoomScale = scale // set to 1 to allow zoom out to 100% of image size //
scrollView.zoomScale = scale
// center image vertically in scrollview //
let offsetY:CGFloat = (scrollView.contentSize.height - scrollView.frame.size.height) / 2;
scrollView.contentOffset = CGPointMake(0, offsetY);
}
func scrollViewDidZoom(scrollView: UIScrollView) {
print("zoomed")
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return imageView
}
}
The other, perhaps simpler way would be to put the image view in a scroll view and let the scroll view manage it for you. It handles everything.

Swift UIScrollView - strange padding

I need to make the flowers image flipping. Images must be with the same height, but the width to set automatically. I want them to scroll right and left
Here is my code:
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
var images = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
for i in 1...3 {
images.append(UIImage(named: "bild-0\(i).jpg")!)
}
var i: CGFloat = 0
var origin: CGFloat = 0
let height: CGFloat = scrollView.bounds.height
for image in images {
let imageView = UIImageView(frame: CGRectZero)
imageView.frame.size.height = height
imageView.image = image
imageView.sizeToFit()
imageView.frame.origin.x = origin
println(imageView.frame.size.width)
println(imageView.frame.origin.x)
println(imageView.frame.size.height)
println("asd")
origin = origin + imageView.frame.size.width
i++
scrollView.addSubview(imageView)
}
scrollView.contentSize.width = origin
scrollView.bounces = false
scrollView.pagingEnabled = false
}
}
Storyboard:
Problem (Padding from top! - Red color - is a background for UIScrollView):
Images are 765x510 300x510 and so on
UIScrollView height is 170
This is caused by scrolling insets:
Click your ViewController on Storyboard and go to file inspector, and you should see this dialog:
Untick the Adjust Scroll View Insets.

Resources