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()
Related
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
}
}
}
}
Language: Swift 3
Task: Allow users to crop their profile image on upload
Question: Initially I was going to try and figure out to save the cropped image and the full image to my server because I need both, then came up with the question... Is there a way to save the cropped position of the full image to my mysql database(php) then access it when I need to display the cropped image?
I'm using
self.imagePicker.allowsEditing = true
To allow the users to crop photo
let chooseAction = UIAlertAction(title: "Choose Image", style: .default, handler: { (alert: UIAlertAction!) -> Void in
if UIImagePickerController.isSourceTypeAvailable(.savedPhotosAlbum) {
self.imagePicker.delegate = self
self.imagePicker.sourceType = .savedPhotosAlbum
self.imagePicker.allowsEditing = true
self.present(self.imagePicker, animated: true, completion: nil) }
})
let takeAction = UIAlertAction(title: "Take Image", style: .default, handler: { (alert: UIAlertAction!) -> Void in
if UIImagePickerController.isSourceTypeAvailable(.camera) {
self.imagePicker.delegate = self
self.imagePicker.sourceType = .camera
self.imagePicker.allowsEditing = true
self.present(self.imagePicker, animated: true, completion: nil) }
})
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: { (alert: UIAlertAction!) -> Void in })
optionMenu.addAction(chooseAction)
optionMenu.addAction(takeAction)
optionMenu.addAction(cancelAction)
self.present(optionMenu, animated: true, completion: nil)
}
EDIT
I am saving the full image to server
I need the simplest way to get the cropped square position if possible then store the position in a variable (I will store this on my server)
On certain views when the image is displayed I only want to show the cropped part of the full image. I will retrieve the saved cropped position set on upload then only show that part of the image.
I decided to try this out since there are a few issues that can occur doing this. I made this example:
class ViewController: UIViewController {
private let scrollViewPanel: HitForwardingView = HitForwardingView()
private let scrollView: UIScrollView = UIScrollView()
private let imageView: UIImageView = UIImageView()
private var image: UIImage?
override func viewDidLoad() {
super.viewDidLoad()
scrollViewPanel.frame = view.bounds
scrollViewPanel.listenerView = scrollView
view.addSubview(scrollViewPanel)
scrollView.frame = CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200.0)
scrollView.center = CGPoint(x: scrollViewPanel.bounds.midX, y: scrollViewPanel.bounds.midY)
scrollViewPanel.addSubview(scrollView)
scrollView.clipsToBounds = false
scrollView.delegate = self
scrollView.minimumZoomScale = 0.2
scrollView.maximumZoomScale = 5.0
if let image = UIImage(named: "testImage") {
setupWithImage(image)
scrollToCenter()
}
// Just some masking to make things look nicer
scrollViewPanel.addSubview({
let view = UIView(frame: CGRect(x: 0.0, y: 0.0, width: scrollViewPanel.bounds.width, height: scrollView.frame.minY))
view.isUserInteractionEnabled = false
view.backgroundColor = UIColor.black.withAlphaComponent(0.6)
return view
}())
scrollViewPanel.addSubview({
let view = UIView(frame: CGRect(x: 0.0, y: scrollView.frame.minY, width: scrollView.frame.minX, height: scrollView.frame.height))
view.isUserInteractionEnabled = false
view.backgroundColor = UIColor.black.withAlphaComponent(0.6)
return view
}())
scrollViewPanel.addSubview({
let view = UIView(frame: CGRect(x: scrollView.frame.maxX, y: scrollView.frame.minY, width: scrollViewPanel.frame.width - scrollView.frame.maxX, height: scrollView.frame.height))
view.isUserInteractionEnabled = false
view.backgroundColor = UIColor.black.withAlphaComponent(0.6)
return view
}())
scrollViewPanel.addSubview({
let view = UIView(frame: CGRect(x: 0.0, y: scrollView.frame.maxY, width: scrollViewPanel.bounds.width, height: scrollViewPanel.frame.height - scrollView.frame.maxY))
view.isUserInteractionEnabled = false
view.backgroundColor = UIColor.black.withAlphaComponent(0.6)
return view
}())
// Add a trigger to show crop
view.addGestureRecognizer({
let recognizer = UITapGestureRecognizer(target: self, action: #selector(debugSnapshot))
recognizer.numberOfTapsRequired = 2
return recognizer
}())
}
#objc private func debugSnapshot() {
// Going to use relative coordinates depending on the original image. The result should be a frame normalized depending on original:
// x=0 is left-most
// x=1 is right-most
// y=0 is top-most
// y=1 is bottom-most
let convertedScrollViewFrame = scrollView.convert(scrollView.bounds, to: scrollViewPanel)
let convertedImageViewFrame = imageView.convert(imageView.bounds, to: scrollViewPanel)
var frame = convertedScrollViewFrame
frame.origin.x -= convertedImageViewFrame.origin.x
frame.origin.y -= convertedImageViewFrame.origin.y
frame.origin.x /= convertedImageViewFrame.width
frame.origin.y /= convertedImageViewFrame.height
frame.size.width /= convertedImageViewFrame.width
frame.size.height /= convertedImageViewFrame.height
print(frame)
// Do a reconstruction
let previewPanelView = UIView(frame: self.view.bounds)
previewPanelView.backgroundColor = UIColor.black.withAlphaComponent(0.8)
previewPanelView.addSubview({
let panel = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 300.0, height: 300.0))
panel.center = CGPoint(x: previewPanelView.bounds.midX, y: previewPanelView.bounds.midY)
panel.clipsToBounds = true
let imageView = UIImageView(image: self.imageView.image)
// Frame now relative to where we display it (panel)
imageView.frame = CGRect(x: -frame.origin.x * panel.bounds.width / frame.size.width,
y: -frame.origin.y * panel.bounds.height / frame.size.height,
width: panel.bounds.width / frame.size.width,
height: panel.bounds.height / frame.size.height)
panel.addSubview(imageView)
return panel
}())
view.addSubview(previewPanelView)
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
previewPanelView.removeFromSuperview()
}
}
private func setupWithImage(_ image: UIImage) {
imageView.image = image
imageView.frame = CGRect(x: 0.0, y: 0.0, width: image.size.width, height: image.size.height)
scrollView.addSubview(imageView)
scrollView.contentSize = imageView.frame.size
}
private func scrollToCenter() {
scrollView.zoomScale = 1.0
scrollView.contentOffset = CGPoint(x: scrollView.contentSize.width*0.5 - scrollView.bounds.size.width*0.5,
y: scrollView.contentSize.height*0.5 - scrollView.bounds.size.height*0.5)
}
}
extension ViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
}
private extension ViewController {
class HitForwardingView: UIView {
weak var listenerView: UIView?
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let original = super.hitTest(point, with: event)
if (original == self || original == nil) && self.bounds.contains(point) {
return listenerView
} else {
return original
}
}
}
}
To make it work you can simply create a new project and copy this code into your ViewController. You need a test image as well named "testImage". You can scroll your image around, zoom it in or out. If you double-tap a new overlay will appear which should show a cropped image.
Explanations:
The whole view controller is done in code just to make it easier to explain. In reality all the views would be done in storyboard with constraints.
Because we use a "small" scroll view we need to forward touch events to the scroll view even when you drag outside of it. To do so HitForwardingView is introduced. Basically it is collecting events and forwarding them to the scroll view.
Both "position" construction and deconstruction is demonstrated in debugSnapshot. To your backend you would send data from received frame in this method. You would then later use this same frame from backend to position image correctly to mimic cropped image.
I hope the rest is pretty much self-explanatory from the code. If there are any questions about it please go ahead.
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
I have this code. I'm using the GestureRecognizer to call another method but when testing it on my iPhone the TapGestureRecognizer sends nothing. I thought it was a problem with the handleSettingsDismiss so I changed it to print something to know if it was recognizing the tap. But it did nothing and I get nothing on console. I don't know what is wrong.
func showSettingsMenu() {
if let window = UIApplication.shared.keyWindow {
blackView.isUserInteractionEnabled = true
blackView.backgroundColor = UIColor(white: 0, alpha: 0.5)
blackView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleSettingsDismiss)))
}
#objc func handleSettingsDismiss(){
UIView.animate(withDuration: 0.5){
self.blackView.alpha = 0
if let window = UIApplication.shared.keyWindow{
self.collectionView.frame = CGRect(x: 0, y: window.frame.height, width: self.collectionView.frame.width, height: self.collectionView.frame.height)
}
}
}
I am trying to enable UIGestureTap on a custom view. I have a view controller, and in that view controller, when I press a button, a custom view pops up.
var transparentBackground = UIView()
#IBAction func UserViewImage(_ sender: UIButton) -> Void {
self.transparentBackground = UIView(frame: UIScreen.main.bounds)
self.transparentBackground.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
UIApplication.shared.keyWindow!.addSubview(self.transparentBackground)
self.opaqueView = self.setupOpaqueView()
self.transparentBackground.addSubview(opaqueView)
UIApplication.shared.keyWindow!.bringSubview(toFront: self.transparentBackground)
self.view.bringSubview(toFront: transparentBackground)
}
I want to be able to tap on the transparentBackground view and dismiss it. So I have a dismiss function called removeAnimate()
func removeAnimate()
{
UIView.animate(withDuration: 0.25, animations: {
self.transparentBackground.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
self.transparentBackground.alpha = 0.0;
}, completion:{(finished : Bool) in
if (finished)
{
self.transparentBackground.removeFromSuperview()
}
});
}
So, in viewdidload I enabled the UITapGesture:
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(removeAnimate))
self.transparentBackground.addGestureRecognizer(gestureRecognizer)
self.transparentBackground.isUserInteractionEnabled = true
I know the function removeAnimate works because I used it on a button in the transparentBackground view and it works perfectly. But when I tap on the transparentBackground view it does not dismiss and I am not sure what I am doing wrong
func setupOpaqueView() -> UIView{
let mainView = UIView(frame: CGRect(x: 16, y: 132, width: Int(UIScreen.main.bounds.width-32), height: 403))
mainView.backgroundColor = UIColor.clear
mainView.layer.cornerRadius = 6
self.imageView = UIImageView(frame: CGRect(x: 29, y: 18, width: 274, height: 350))
mainView.addSubview(OKbutton)
mainView.addSubview(self.imageView)
OKbutton.addTarget(self, action: #selector(ThirdWheelViewController.handleOKButtonTapped(_:)), for: .touchUpInside)
return mainView
}
This is an example and hope it helps you:
First of all create a variable:
var customView:UIView!
This is going to be our function for adding a custom view:
#IBAction func customAction(_ sender: AnyObject) {
self.customView = UIView.init(frame: CGRect.init(x: self.view.bounds.width / 2, y: self.view.bounds.height / 2, width: 100, height: 100))
self.customView.backgroundColor = UIColor.red
self.view.addSubview(self.customView)
let tap = UITapGestureRecognizer.init(target: self, action: #selector(self.removeFromSuperView))
tap.numberOfTapsRequired = 1
self.customView.addGestureRecognizer(tap)
}
And finally:
func removeFromSuperView() {
self.customView.alpha = 1.0
self.customView.transform = .identity
UIView.animate(withDuration: 0.3, animations: {
self.customView.alpha = 0.0
self.customView.transform = .init(scaleX: 1.5, y: 1.5)
}) { (finished) in
if !finished {
} else {
self.customView.removeFromSuperview()
}
}
}