Move UIImageView Independently from its Mask in Swift - ios

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.

Related

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

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

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:

CALayer in UIScrollView has a wrong size

I'm trying to zoom UIImageView and UIView, which are inside the UIScrollView, but it doesn't work.
I have an UIImage and CALayer, both of them have an equal size (2048×2155). These components wrapped into UIImageView (image view) and UIView (canvas) and displayed inside UIScrollView.
The image view and canvas have the same content scale factor, which equals 2.0 (UIScreen.main.scale), but the canvas has the wrong size (see picture below).
CALayer sublayers were built by using CAShapeLayer and UIBezierPath.
The UIScrollViewDelegate looks next:
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
canvas
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
self.scrollView.centerContentView()
}
The center content view looks next (method inside custom UIScrollView):
func centerContentView() {
let boundsSize = bounds.size
var centerFrame = canvas.frame
if centerFrame.size.width < boundsSize.width {
centerFrame.origin.x = (boundsSize.width - centerFrame.size.width) / 2.0
} else {
centerFrame.origin.x = 0
}
if centerFrame.size.height < boundsSize.height {
centerFrame.origin.y = (boundsSize.height - centerFrame.size.height) / 2.0
} else {
centerFrame.origin.y = 0
}
imageView.frame = centerFrame
canvas.frame = centerFrame
}
All my efforts have failed. I tried to transform the CALayer with canvas but to no avail. I will be grateful for any suggestions or help. Thank you.
The problem was fixed by using the next lines:
let scale = UIScreen.main.bounds.width / imageSize.height
let scaleTransformation = CATransform3DMakeScale(scale, scale, 1)
for layer in layers {
layer.transform = scaleTransformation
}
where the imageSize equals to the 2048×2155 and everything works fine.
Thank you.

How to wait for subview layout settle down after the initial viewDidLoad cycle?

I have a UIViewController, I made an zoomableImageView by embedding a UIImageView inside a UIScrollView.
class ZoomableImageView: UIScrollView {
// public so that delegate can access
public let imageView: UIImageView = {
let _imageView = UIImageView()
_imageView.translatesAutoresizingMaskIntoConstraints = false
return _imageView
} ()
// this method will be called multiple times to display different images
public func setImage(image: UIImage) {
imageView.image = image
imageView.frame = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
self.contentSize = image.size
// gw: not working here, too early
setZoomScale()
}
func setZoomScale() {
let imageViewSize = imageView.bounds.size
let scrollViewSize = self.bounds.size
let widthScale = scrollViewSize.width / imageViewSize.width
let heightScale = scrollViewSize.height / imageViewSize.height
print("gw: imageViewSize: \(imageViewSize), scrollViewSize: \(scrollViewSize)")
self.minimumZoomScale = min(widthScale, heightScale)
self.maximumZoomScale = 1.2 // allow maxmum 120% of original image size
// set initial zoom to fit the longer side (longer side ==> smaller scale)
zoomScale = minimumZoomScale
}
}
Each time I change the UIImage of the image view, I want to wait for the UIImageView's bound size to settle down, before I can use it to calculate a scale factor for zooming in UIScrollView.
Question: What is the appropriate place to put setZoomScale()? I put it right before exiting the setImage method, but the imageView.bounds.size is not correct in my print statement. Note that it needs to be triggered each time the image changes, not just the initial view loading stage.
I also tried to put setZoomScale in ViewController's viewWillLayoutSubviews, but I have addtional question here: is viewWillLayoutSubviews only called once at view initialization stage? Can I force trigger it using setNeedsLayout? (which I tried, but not re-triggering viewWillLayoutSubviews)
As any change to UI elements dispatch the operation to the main dispatch queue, you can put your code in DispathQueue.main.async{} to make sure it run after the UI change is done.
public func setImage(image: UIImage) {
imageView.image = image
imageView.frame = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
self.contentSize = image.size
// gw: not working here, too early
DispatchQueue.main.async{
self.setZoomScale()
}
}

Swift - Add UIImageView as subview of UIWebView scrollView and scaling

I have a UIWebView and I have successfully added a UIImage view to the UIWebView’s scrollView like so:
let localUrl = String(format:"%#/%#", PDFFilePath, fileNameGroup)
let url = NSURL.fileURLWithPath(localUrl)
panRecognizer = UITapGestureRecognizer(target: self, action: #selector(panDetected))
pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(pinchDetected))
panRecognizer.delegate = self
pinchRecognizer.delegate = self
webview = UIWebView()
webview.frame = self.view.bounds
webview.scrollView.frame = webview.frame
webview.userInteractionEnabled = true
webview.scalesPageToFit = true
webview.becomeFirstResponder()
webview.delegate = self
webview.scrollView.delegate = self
self.view.addSubview(webview)
webview.loadRequest(NSURLRequest(URL:url))
webview.gestureRecognizers = [pinchRecognizer, panRecognizer]
let stampView:StampAnnotation = StampAnnotation(imageIcon: UIImage(named: "approved.png"), location: CGPointMake(currentPoint.x, currentPoint.y))
self.webview.scrollView.addSubview(stampView)
My UIWebView scrollView is scalable. Now I am looking for away to have my UIImageView (StampAnnotation is a class and UIImageView is its subclass) scale when the scrollView scales. So if the user zooms in on the scrollView, the UIImageView will get bigger and stay in a fixed position and if the user zooms out, the UIImageView will get smaller while the scrollView gets smaller while staying in a fixed position.
I really hope that makes sense. I have tried the following:
func pinchDetected(recognizer:UIPinchGestureRecognizer)
{
for views in webview.scrollView.subviews
{
if(views.isKindOfClass(UIImageView))
{
views.transform = CGAffineTransformScale(views.transform, recognizer.scale, recognizer.scale)
recognizer.scale = 1
}
}
if(appDelegate.annotationSelected == 0)
{
webview.scalesPageToFit = true
}
else
{
webview.scalesPageToFit = false
}
}
but this does nothing, if I remove this line:
recognizer.scale = 1
it scales way too big too fast. My question is, how do I get my UIImageView to scale when the UIWebview’s scrollView scrolls?
Any help would be appreciated.
This solved my problem.
func scrollViewDidZoom(scrollView: UIScrollView) {
for views in webview.scrollView.subviews
{
if(views.isKindOfClass(UIImageView))
{
views.transform = CGAffineTransformMakeScale(scrollView.zoomScale, scrollView.zoomScale)
}
}
}
No it does not stay in a fixed position on the page, but I think that is a constraints issue?
You were close...
1) Add a property to hold onto an external reference for your stampViewFrame:
var stampViewFrame = CGRect(x: 100, y: 100, width: 100, height: 100)
2) Replace your scrollViewDidZoom() with this:
func scrollViewDidZoom(scrollView: UIScrollView) {
for views in webView.scrollView.subviews
{
if(views.isKindOfClass(UIImageView))
{
views.frame = CGRect(x: stampViewFrame.origin.x * scrollView.zoomScale, y: stampViewFrame.origin.y * scrollView.zoomScale, width: stampViewFrame.width * scrollView.zoomScale, height: stampViewFrame.height * scrollView.zoomScale)
}
}
}
3) Finally, because the zoom scale resets to 1 at the begining of each new zooming action, you need to adjust the value of your stampViewFrame property:
func scrollViewDidEndZooming(scrollView: UIScrollView, withView view: UIView?, atScale scale: CGFloat) {
stampViewFrame = CGRect(x: stampViewFrame.origin.x * scale, y: stampViewFrame.origin.y * scale, width: stampViewFrame.width * scale, height: stampViewFrame.height * scale)
}
I also tried to answer your other question about layout during orientation change, but I now have a much better understanding of what you are trying to do. If you want your stampView to always be on in the same place relative to the web content, you have to get into HTML/JS because the webpage lays itself out dynamically. A much much more simple (and hopefully close enough) solution would be to add the following:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
webView.frame = view.bounds
stampView.frame = stampViewFrame
}
Use the scroll delegate method of scrollViewDidZoom :
func scrollViewDidZoom(scrollView: UIScrollView){
//Change the subview of scroll frame as per the scroll frame scale
//rect = initial position & size of the image.<class instance>
stampView.frame = CGRectMake((CGRectGetMaxX(rect)-rect.size.width)*webView.scrollView.zoomScale, (CGRectGetMaxY(rect)-rect.size.height)*webView.scrollView.zoomScale, rect.width*webView.scrollView.zoomScale,rect.height*webView.scrollView.zoomScale)
}

Resources