Swift: restoring UIView center after moving it - ios

I'm using gestures to zoom and move a UIImageView (like Instagram zoom, for example). When the gesture ends, I want to restore UIImageView initial position, but I cannot get a copy of the initial center because it's a reference type. Using:
let prevCenter = myImage.center
is of course useless. How can I copy it?

On zoom and move apply transform to view. On the end just set .identity value.
Edit
Example:
#IBOutlet weak var butt: UIButton!
var offsetTransform: CGAffineTransform = .identity
var zoomTransform: CGAffineTransform = .identity
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let pan = UIPanGestureRecognizer(target: self, action: #selector(onPan(pan:)))
pan.minimumNumberOfTouches = 2
pan.delegate = self
view.addGestureRecognizer(pan)
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(onPinch(pinch:)))
pinch.delegate = self
view.addGestureRecognizer(pinch)
}
#objc func onPinch(pinch: UIPinchGestureRecognizer) {
let s = pinch.scale
zoomTransform = CGAffineTransform(scaleX: s, y: s)
butt.transform = offsetTransform.concatenating(zoomTransform)
if (pinch.state == .ended) {
finish()
}
}
#objc func onPan(pan: UIPanGestureRecognizer) {
let t = pan.translation(in: view)
offsetTransform = CGAffineTransform(translationX: t.x, y: t.y)
}
func updatePos() {
butt.transform = offsetTransform.concatenating(zoomTransform)
}
func finish() {
butt.transform = .identity
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}

Are you sure that you are not updating the prevCenter somewhere else?
center is struct so you shouldn't have problems copying it.
Just make sure to take it (the center) when the view is in original state, before starting to zoom it and after it is drawn with correct frame
let imageView = UIImageView(frame: CGRect(x: 10, y: 10, width: 20, height: 20))
let prevCenter = view.center // 20, 20
imageView.center = CGPoint(x: 50, y: 50)
// imageView.center - 50, 50
// prevCenter - 20, 20
imageView.frame = CGRect(x: 40, y: 40, width: 10, height: 10)
// imageView.center - 45, 45
// prevCenter - 20, 20

Related

How do you pass a gesture between subviews in UIKit?

I've got a ViewController with three subviews. I'm trying to get them to detect touches in their bounds from a starting point outside their bounds without the user lifting their finger (ie the user dragging into the view). I thought hitTest would do this but it only works for separate taps. I assume this is probably passing a gesture through instead but I've not found out how to implement this.
class SuperViewController: UIViewController {
var view01 = UIView(frame: CGRect(x: 0, y: 0, width: 1000,
height: 800))
var view02 = UIView(frame: CGRect(x: 0, y: 0, width: 600,
height: 400))
let view03 = UIView(frame: CGRect(x: 0, y: 0, width: 300,
height: 200))
override func viewDidLoad() {
super.viewDidLoad()
self.view = TestView()
view01.backgroundColor = .orange
view02.backgroundColor = .blue
view03.backgroundColor = .green
self.view.addSubview(view01)
self.view.addSubview(view02)
self.view.addSubview(view03)
}
}
Which produces this
And then I've subclassed UIView for the SuperViewController's view.
class TestView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard self.isUserInteractionEnabled, !isHidden, alpha > 0.01 else {return nil}
if self.point(inside: point, with: event) {
for subview in subviews.reversed() {
let hitView = subview.hitTest(point, with: event)
if hitView != nil {
hitView?.backgroundColor = .red
return hitView
}
}
return self
}
return nil
}
}
So each one turns red when the user taps. But ideally I want them to each respond with one drag from the top left corner of the screen to the other.
You can accomplish this with a UIPanGestureRecognizer.
Here's an example below:
class ViewController: UIViewController {
var view01 = UIView(frame: CGRect(x: 0, y: 0, width: 1000,
height: 800))
var view02 = UIView(frame: CGRect(x: 0, y: 0, width: 600,
height: 400))
let view03 = UIView(frame: CGRect(x: 0, y: 0, width: 300,
height: 200))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view01.backgroundColor = .orange
view02.backgroundColor = .blue
view03.backgroundColor = .green
self.view.addSubview(view01)
self.view.addSubview(view02)
self.view.addSubview(view03)
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
self.view.addGestureRecognizer(gestureRecognizer)
}
#objc
private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let view = gestureRecognizer.view else {
return
}
let translation = gestureRecognizer.translation(in: view)
for subview in view.subviews.reversed() {
if let hitView = subview.hitTest(translation, with: nil) {
hitView.backgroundColor = .red
return
}
}
}
}

Tapping a UIImage while it's being animated

I've been trying to be able to tap a UIImage as it animates to the top of my screen and print("Image Tapped"), yet to no success.
override func viewDidLoad() {
super.viewDidLoad()
redBalloon.image = UIImage(named: "redBalloon")
redBalloon.contentMode = .scaleAspectFit
redBalloon.frame = CGRect(x: Int(xOrigin), y: 667, width: Int(redBalloon.frame.size.width), height: Int(redBalloon.frame.size.height))
UIView.animate(withDuration: 5, delay: 0, options: UIImageView.AnimationOptions.allowUserInteraction, animations: {
self.redBalloon.frame = CGRect(x: Int(self.xEnding), y: -192, width: 166, height: 192)
}, completion: {(finished:Bool) in
self.endGame()
})
let imageTap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
redBalloon.isUserInteractionEnabled = true
redBalloon.addGestureRecognizer(imageTap)
}
#objc func imageTapped(_ sender: UITapGestureRecognizer) {
// do something when image tapped
print("image tapped")
}
The problem is that the image view is not in the spot where you see it during animation (it's at the endpoint of the animation). So you are not tapping on the image view at the point where it is, and thus the tap is not detected.
Therefore, either you must hit-test the presentation layer or, if you don't want to do that, you must use a UIViewPropertyAnimator instead of calling UIView.animate.
As an example of the first approach, I'll subclass UIImageView. Make your UIImageView an instance of this subclass:
class TouchableImageView : UIImageView {
override func hitTest(_ point: CGPoint, with e: UIEvent?) -> UIView? {
let pres = self.layer.presentation()!
let suppt = self.convert(point, to: self.superview!)
let prespt = self.superview!.layer.convert(suppt, to: pres)
return super.hitTest(prespt, with: e)
}
}
However, personally I think it's a lot simpler to use UIViewPropertyAnimator. In that case, do not make your UIImageView a TouchableImageView! You don't want to do extra hit-test munging. Just let the property animator do all the work:
redBalloon.image = UIImage(named: "redBalloon")
redBalloon.contentMode = .scaleAspectFit
redBalloon.frame = CGRect(x: Int(xOrigin), y: 667, width: Int(redBalloon.frame.size.width), height: Int(redBalloon.frame.size.height))
let anim = UIViewPropertyAnimator(duration: 5, timingParameters: UICubicTimingParameters(animationCurve: .easeInOut))
anim.addAnimations {
self.redBalloon.frame = CGRect(x: Int(xEnding), y: -192, width: 166, height: 192)
}
anim.addCompletion { _ in
self.endGame()
}
let imageTap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
redBalloon.isUserInteractionEnabled = true
redBalloon.addGestureRecognizer(imageTap)
anim.startAnimation()

Swift: Changing (translate) a UIView position through a pan gesture in its superview window

Introduction
Context:
In my main ViewController I have a scrollView with a few objects inside (which are UIViews). When one of the UIViews are tapped/selected I animate forward a UITextView in a UIView to go with the selected object. (only one UIView can appear at a time)
This UIView that appears on object selection is separated into a separate class called AdjunctiveTextView.
Issue/goal:
(the example code provided below will clear make this clear, I've also commented where the issue lies in the code)
When an object has been tapped and has an adjacent UIView with a text I want to have that adjacent UIView to follow with the scrollView.
I'm using a UIPanGestureRecognizer to attempt to do this. But I can't figure out how to make it work when the user drags in the scrollview. It only work if the user drags on the actual adjunctiveTextView.
Everything works as expected except that the adjunctiveTextView does not change its position during the panGesture.
I would like (if possible) to have the AdjunctiveTextView as a separate class. My ViewController file is getting rather big.
Question:
Why doesn't the UIPanGestureRecognizer work as expected? What is needed in order for it to translate the backView correctly?
Code
My attempt: (as shown below)
My attempt simply makes the backView itself "dragable" around through the panGesture. Nothing happens to it when I scroll the scrollView.
(I have only included relevant portions of my code)
class ViewController: UIViewController {
let adjunctiveTextView = AdjunctiveTextView()
// this is a delegate method which gets called when an object is tapped in the scrollView
func scrollViewObjectIsTapped(_ objectScrollView: ObjectScrollView, object: AvailableObject) {
** adjunctiveTextView.scrollView = scrollView // **Edited! (scrollView is the name of the scrollView in this class too)
adjunctiveTextView.showView(passInObject: AvailableObject)
}
}
class AdjunctiveTextView: NSObject {
lazy var backView: UIView = {
//backView setup
}
lazy var textView: UITextView = {
//textView setup
}
//additional init and setup
** weak var scrollView : UIScrollView! // **Edited!
func showView(passInObject: AvailableObject) {
if let window = UIApplication.shared.keyWindow {
// the issue must either be here in the PanGesture setup
let panG = UIPanGestureRecognizer(target: self, action: #selector(translateView(sender:)))
panG.cancelsTouchesInView = false
// window.addGestureRecognizer(panG)
** scrollView.addGestureRecognizer(panG) // **Edited!
window.addSubview(backView)
textView.text = passInObject.information
backView.frame = CGRect(x: passInObject.frame.minX, y: passInObject.minY, width: window.frame.width - passInObject.maxX - 6, height: textView.bounds.height + 5)
backView.alpha = 0
//it animates a change of the backViews x position and alpha.
UIView.animate(withDuration: 0.42, delay: 0, options: .curveEaseInOut, animations: {
self.backView.alpha = 1
self.backView.frame = CGRect(x: passInObject.frame.minX + passInObject.frame.width, y: passInObject.minY, width: window.frame.width - passInObject.maxX - 6, height: textView.bounds.height + 5)
}, completion: nil)
}
}
// or the issue is here in the handle function for the PanGesture.
#objc private func translateView(sender: UIPanGestureRecognizer) {
if let window = UIApplication.shared.keyWindow {
let translation = sender.translation(in: window) //Have tried setting this to scrollView also
switch sender.state {
case .began, .changed:
backView.center = CGPoint(x: backView.center.x, y: backView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: window) //Have tried setting this to sccrollView also
break
case .ended:
break
default:
break
}
}
}
}
Thanks for reading my question.
I just add a weak reference to your scrollView and then add the pan gesture to scrollView. It works as you want. You may consider add another pan gesture to the back view if you want your original behavior.
class AdjunctiveTextView: NSObject {
lazy var backView: UIView = {
//backView setup
return UIView.init()
}()
lazy var textView: UITextView = {
//textView setup
return UITextView.init(frame: CGRect.init(x: 0, y: 0, width: 300, height: 100))
}()
weak var scrollView: UIScrollView!
//additional init and setup
func showView(passInObject: AvailableObject) {
if let window = UIApplication.shared.keyWindow {
// the issue must either be here in the PanGesture setup
let panG = UIPanGestureRecognizer(target: self, action: #selector(translateView(sender:)))
panG.cancelsTouchesInView = false
// passInObject.addGestureRecognizer(panG)
scrollView.addGestureRecognizer(panG)
window.addSubview(backView)
textView.text = passInObject.information
textView.backgroundColor = UIColor.blue
backView.addSubview(textView)
backView.frame = CGRect(x: passInObject.frame.minX, y: passInObject.frame.minY, width: window.frame.width - passInObject.frame.maxX - 6, height: textView.bounds.height + 5)
backView.alpha = 0
//it animates a change of the backViews x position and alpha.
UIView.animate(withDuration: 0.42, delay: 0, options: .curveEaseInOut, animations: {
self.backView.alpha = 1
self.backView.frame = CGRect(x: passInObject.frame.minX + passInObject.frame.width , y: passInObject.frame.minY , width: window.frame.width - passInObject.frame.maxX - 6, height: self.textView.bounds.height + 5)
self.backView.backgroundColor = UIColor.red
}, completion: nil)
}
}
// or the issue is here in the handle function for the PanGesture.
#objc private func translateView(sender: UIPanGestureRecognizer) {
if let window = UIApplication.shared.keyWindow {
let translation = sender.translation(in: window)
switch sender.state {
case .began, .changed:
backView.center = CGPoint(x: backView.center.x, y: backView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: window)
break
case .ended:
break
default:
break
}
}
}
}
class ObjectScrollView: UIScrollView{
}
class AvailableObject: UIView{
var information: String!
}
class MySCNViewController: UIViewController {
#IBOutlet weak var oScrollView: ObjectScrollView!
// this is a delegate method which gets called when an object is tapped in the scrollView
func scrollViewObjectIsTapped(_ objectScrollView: ObjectScrollView, object: AvailableObject) {
adjunctiveTextView.showView(passInObject: object)
}
let adjunctiveTextView = AdjunctiveTextView()
let ao = AvailableObject.init(frame: CGRect.init(x: 0, y: 0, width: 200, height: 200))
override func viewDidLoad() {
super.viewDidLoad()
ao.information = "test"
adjunctiveTextView.scrollView = oScrollView
ao.backgroundColor = UIColor.yellow
}
#IBAction func tap(_ sender: Any?){
scrollViewObjectIsTapped(oScrollView, object: ao)}
}

How to add a UIPanGestureRecognizer to an shape drawn on CAShapLayer - swift?

I have an imageView that I have drawn a blue circle in its layer. I would like a user to be able to tap, hold and drag this blue circle anywhere within the UIImageView. I am unsure how to attach this shape to a UIPanGestureRecognizer. My effort so far is below:
class DrawCircleViewController: UIViewController {
#IBOutlet weak var imgView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// DRAW A FILLED IN BLUE CIRCLE
drawBlueCircle()
// ADD GESTURE RECOGNIZER
let panRecgonizer = UIPanGestureRecognizer.init(target: ???, action: <#T##Selector?#>)
}
func drawBlueCircle(){
let fourDotLayer = CAShapeLayer()
fourDotLayer.path = UIBezierPath.init(roundedRect: CGRect.init(x: 60, y: 60, width: 30, height: 30), cornerRadius: 50).cgPath
fourDotLayer.fillColor = UIColor.blue.cgColor
self.imgView.layer.addSublayer(fourDotLayer)
}
}
use this code to move the view
#objc func handlePanRecgonizer(_ gestureRecognizer: UIPanGestureRecognizer){
if panRecgonizer.state == .began || panRecgonizer.state == .changed {
let translation = panRecgonizer.translation(in: self.view)
panRecgonizer.view!.center = CGPoint(x: panRecgonizer.view!.center.x + translation.x, y: panRecgonizer.view!.center.y + translation.y)
panRecgonizer.setTranslation(CGPoint.zero, in: self.view)
}
}
if you want to add UIPanGestureRecognizer programmatically:
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanRecgonizer))
self.someDraggableView.addGestureRecognizer(gestureRecognizer)
As you might have noticed you can't add GestureRecognizers to CALayers. They can only be added to UIView types.
The solution is to add a subview to your imageview and draw the blue circle in it.
var drawingView: UIView?
override func viewDidLoad() {
super.viewDidLoad()
drawingView.frame = imgView.bounds
imgView.addSubview(drawingView)
// DRAW A FILLED IN BLUE CIRCLE
drawBlueCircle()
// ADD GESTURE RECOGNIZER
let panRecgonizer = UIPanGestureRecognizer(target: drawingView, action: #selector())
drawingView.addGestureRecognizer(panRecgonizer)
}
func drawBlueCircle(){
let fourDotLayer = CAShapeLayer()
fourDotLayer.path = UIBezierPath.init(roundedRect: CGRect.init(x: 60, y: 60, width: 30, height: 30), cornerRadius: 50).cgPath
fourDotLayer.fillColor = UIColor.blue.cgColor
drawingView?.layer.addSublayer(fourDotLayer)
}

UIImages or UIViews are not colliding each other in iOS Swift?

I am trying to collide two UIViews each other for using pan gesture. Now i can able to drag one view but when dragging view to next view its collide each other. It goes behind other view.
Here is the screenshot result:
Here is the code I used to drag, pan and collide of physics property.
import UIKit
class ViewController: UIViewController {
var greenSquare: UIView?
var redSquare: UIView?
var animator: UIDynamicAnimator?
var snap: UISnapBehavior!
var panGesture = UIPanGestureRecognizer()
#IBAction func pan(_ sender: UIPanGestureRecognizer) {
let tapPoint: CGPoint = sender.location(in: view)
if (snap != nil) {
animator?.removeBehavior(snap)
}
snap = UISnapBehavior(item: greenSquare!, snapTo: tapPoint)
animator?.addBehavior(snap)
}
override func viewDidLoad() {
super.viewDidLoad()
var dimen = CGRect(x: 25, y: 25, width: 120, height: 120)
greenSquare = UIView(frame: dimen)
greenSquare?.backgroundColor = UIColor.green
dimen = CGRect(x: 130, y: 25, width: 100, height: 100)
redSquare = UIView(frame: dimen)
redSquare?.backgroundColor = UIColor.red
self.view.addSubview(greenSquare!)
self.view.addSubview(redSquare!)
animator = UIDynamicAnimator(referenceView: self.view)
let gravity = UIGravityBehavior(items: [greenSquare!, redSquare!])
let direction = CGVector(dx: 0.0, dy: 1.0)
gravity.gravityDirection = direction
let boundries = UICollisionBehavior(items: [greenSquare!, redSquare!])
boundries.translatesReferenceBoundsIntoBoundary = true
let bounce = UIDynamicItemBehavior(items: [greenSquare!, redSquare!])
bounce.elasticity = 0.5
animator?.addBehavior(bounce)
animator?.addBehavior(boundries)
animator?.addBehavior(gravity)
panGesture = UIPanGestureRecognizer(target: self, action:#selector(draggedView(sender:)))
greenSquare?.isUserInteractionEnabled = true
greenSquare?.addGestureRecognizer(panGesture)
}
#objc func draggedView(sender:UIPanGestureRecognizer){
let translation = sender.translation(in: self.view)
greenSquare?.center = CGPoint(x: (greenSquare?.center.x)! + translation.x, y: (greenSquare?.center.y)! + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
}//class`
Anyone can help me out this.
The issue is that the animator only recognizes the collision at the origin of the greenSquare. In this case it is at the top of the screen. You can update the collision location on the animator after you move the greenSquare. Add animator?.updateItem(usingCurrentState: greenSquare!)
to the end of the draggedView method. You do not need to use UISnapBehavior for this so you can remove the IBAction func pan method at the top of your code. The updateItem(usingCurrentState:) call will reset where the collision barrier is when you are moving the view manually and it is not being done by the physics engine.

Resources