Move UIView within parent view using pangesture - ios

I want to move a view with in the parent view I am using UIPanGestureRecognizer to move view. I am using below code.
-(void)move:(UIPanGestureRecognizer *)recognizer {
CGPoint touchLocation = [recognizer locationInView:self.customeView];
self.view.center = touchLocation;
}
I want to use locationInView to move view instead of translationInView. Can anyone help me how to move the view with in the parentview using locationInView?

You are on the right track with the UIPanGestureRecognizer. You are likely missing the began and end gesture state handling. Here is a full solution I just put together in sample project (Swift) with a panView as a subclass of the view controller's root view.
class ViewController: UIViewController {
#IBOutlet weak var panView: UIView!
// records the view's center for use as an offset while dragging
var viewCenter: CGPoint!
override func viewDidLoad() {
super.viewDidLoad()
panView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.dragView)))
}
func dragView(gesture: UIPanGestureRecognizer) {
let target = gesture.view!
switch gesture.state {
case .began, .ended:
viewCenter = target.center
case .changed:
let translation = gesture.translation(in: self.view)
target.center = CGPoint(x: viewCenter!.x + translation.x, y: viewCenter!.y + translation.y)
default: break
}
}
}

You can try this
#interface ViewController ()
#property (weak, nonatomic) IBOutlet UIView *panView;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//self.panView = self.panView -> self.view . self.panView is direct child of self.view
UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(move:)];
[self.panView addGestureRecognizer:panRecognizer];
// Do any additional setup after loading the view, typically from a nib.
}
-(void)move:(UIPanGestureRecognizer *)recognizer {
CGPoint centerPoint = [recognizer locationInView:self.view];
self.panView.center = centerPoint;
}
}

Related

Interactive ViewController transition triggered by pinch and pan gesture recognisers simultaneously

I have two viewControllers:
ViewController1
A complex stack of sub viewcontrollers with somewhere in the middle an imageView
ViewController2
A scrollView with an imageView embedded in it
What I'm trying to achieve is a transition between the two viewControllers which gets triggered by pinching the imageView from viewController 1 causing it to zoom in and switch over to viewController 2. When the transition has ended, the imageView should be zoomed in as far as it's been zoomed during the pinch gesture triggered transition.
At the same time I want to support panning the image while performing the zoom transition so that just like with the zoom, the image in the end state will be transformed to the place it's been panned to.
So far I've tried the Hero transitions pod and a custom viewController transitions I wrote myself. The problem with the hero transitions is that the image doesn't properly get snapped to the end state in the second viewController. The problem I had with the custom viewController transition is that I couldn't get both zooming and panning to work at the same time.
Does anyone have an idea of how to implement this in Swift? Help is much appreciated.
The question can be divided in to two:
How to implement pinch zoom and dragging using pan gesture on an imageView
How to present a view controller with one of its subviews (imageView in vc2) positioned same as a subview (imageView in vc1) in the presenting view controller
Pinch gesture zoom: Pinch zooming is easier to implement using UIScrollView as it supports it out of the box with out a need to add the gesture recogniser. Create a scrollView and add the view you'd like to zoom with pinch as its subview (scrollView.addSubview(imageView)). Don't forget to add the scrollView itself as well (view.addSubview(scrollView)).
Configure the scrollView's min and max zoom scales: scrollView.minimumZoomScale, scrollView.maximumZoomScale. Set a delegate for scrollView.delegate and implement UIScrollViewDelegate:
func viewForZooming(in scrollView: UIScrollView) -> UIView?
Which should return your imageView in this case and,
Also conform to UIGestureRecognizerDelegate and implement:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
Which should return true. This is the key that allows us have pan gesture recogniser work with the internal pinch gesture recogniser.
Pan gesture dragging: Simply create a pan gesture recogniser with a target and add it to your scroll view scrollView.addGestureRecognizer(pan).
Handling gestures: Pinch zoom is working nicely by this stage except you'd like to present the second view controller when pinching ends. Implement one more UIScrollViewDelegate method to be notified when zooming ends:
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat)
And call your method that presents the detail view controller presentDetail(), we'll implement it in a bit.
Next step is to handle the pan gesture, I'll let the code explain itself:
// NOTE: Do NOT set from anywhere else than pan handler.
private var initialTouchPositionY: CGFloat = 0
private var initialTouchPositionX: CGFloat = 0
#objc func panned(_ pan: UIPanGestureRecognizer) {
let y = pan.location(in: scrollView).y
let x = pan.location(in: scrollView).x
switch pan.state {
case .began:
initialTouchPositionY = pan.location(in: imageView).y
initialTouchPositionX = pan.location(in: imageView).x
case .changed:
let offsetY = y - initialTouchPositionY
let offsetX = x - initialTouchPositionX
imageView.frame.origin = CGPoint(x: offsetX, y: offsetY)
case .ended:
presentDetail()
default: break
}
}
The implementation moves imageView around following the pan location and calls presentDetail() when gesture ends.
Before we implement presentDetail(), head to the detail view controller and add properties to hold imageViewFrame and the image itself. Now in vc1, we implement presentDetail() as such:
private func presentDetail() {
let frame = view.convert(imageView.frame, from: scrollView)
let detail = DetailViewController()
detail.imageViewFrame = frame
detail.image = imageView.image
// Note that we do not need the animation.
present(detail, animated: false, completion: nil)
}
In your DetailViewController, make sure to set the imageViewFrame and the image in e.g. viewDidLoad and you'll be set.
Complete working example:
class ViewController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
let imageView: UIImageView = UIImageView()
let scrollView: UIScrollView = UIScrollView()
lazy var pan: UIPanGestureRecognizer = {
return UIPanGestureRecognizer(target: self, action: #selector(panned(_:)))
}()
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = // set your image
scrollView.delegate = self
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 10.0
scrollView.addSubview(imageView)
view.addSubview(scrollView)
scrollView.frame = view.frame
let w = view.bounds.width - 30 // padding of 15 on each side
imageView.frame = CGRect(x: 0, y: 0, width: w, height: w)
imageView.center = scrollView.center
scrollView.addGestureRecognizer(pan)
}
// NOTE: Do NOT set from anywhere else than pan handler.
private var initialTouchPositionY: CGFloat = 0
private var initialTouchPositionX: CGFloat = 0
#objc func panned(_ pan: UIPanGestureRecognizer) {
let y = pan.location(in: scrollView).y
let x = pan.location(in: scrollView).x
switch pan.state {
case .began:
initialTouchPositionY = pan.location(in: imageView).y
initialTouchPositionX = pan.location(in: imageView).x
case .changed:
let offsetY = y - initialTouchPositionY
let offsetX = x - initialTouchPositionX
imageView.frame.origin = CGPoint(x: offsetX, y: offsetY)
case .ended:
presentDetail()
default: break
}
}
// MARK: UIScrollViewDelegate
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
presentDetail()
}
// MARK: UIGestureRecognizerDelegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
// MARK: Private
private func presentDetail() {
let frame = view.convert(imageView.frame, from: scrollView)
let detail = DetailViewController()
detail.imageViewFrame = frame
detail.image = imageView.image
present(detail, animated: false, completion: nil)
}
}
class DetailViewController: UIViewController {
let imageView: UIImageView = UIImageView()
var imageViewFrame: CGRect!
var image: UIImage?
override func viewDidLoad() {
super.viewDidLoad()
imageView.frame = imageViewFrame
imageView.image = image
view.addSubview(imageView)
view.addSubview(backButton)
}
lazy var backButton: UIButton = {
let button: UIButton = UIButton(frame: CGRect(x: 10, y: 30, width: 60, height: 30))
button.addTarget(self, action: #selector(back(_:)), for: .touchUpInside)
button.setTitle("back", for: .normal)
return button
}()
#objc func back(_ sender: UIButton) {
dismiss(animated: false, completion: nil)
}
}
seems like UIView.animate(withDuration: animations: completion:) should help you; for example, in animations block you can set new image frame, and in completion: - present second view controller (without animation);

how can I detect if singleTap was tapped or annotation on mapView of mapbox?

Requirment on iOS for mapBox map. (I am not talking about MKMapView)
how can we detect if singleTap was tapped or annotation on mapView? I need that singleTap will be handled only on empty area of map (without pins), and didSelectAnnotation called when i tap on pin.
But I found on android we have method like this
mapboxMap.setOnMapClickListener(new MapboxMap.OnMapClickListener() {
public void onMapClick(#NonNull LatLng point) {
Toast.makeText(getActivity(),"on Tap "+point.getLatitude(),Toast.LENGTH_LONG).show();
}
});
and along with that
mapboxMap.setInfoWindowAdapter(new MapboxMap.InfoWindowAdapter() { ... }) will display the annotation.
Don't we have same kind of concept in iOS ?
The actual problem is in iOS is, when I add singleTapGesture on mapView of Mapbox
UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
[self.mapView addGestureRecognizer:singleTapGesture];
the delegate method of mapView of mapbox will not call.
- (nullable UIView <MGLCalloutView> *)mapView:(MGLMapView *)mapView calloutViewForAnnotation:(id <MGLAnnotation>)annotation;
to make sure the delegate method must call, then I dont have to use singleTapGesture
Here the situation is either this or that, but as per me need I needed both.
Looking forward of any solution.
Thanks,
Subclass MGLMapView and delegate its' touchesEnded.
protocol MapViewTapDelegate: class {
func mapViewTapped(withTouchLocation touchLocation: CLLocationCoordinate2D)
}
class MapView: MGLMapView {
weak var tapDelegate: MapViewTapDelegate?
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
guard let touch = touches.first else { return }
let touchLocation = convert(touch.location(in: self), toCoordinateFrom: nil)
tapDelegate?.mapViewTapped(withTouchLocation: touchLocation)
}
}
As touchesEnded isn't triggered when didSelect annotation is called and vice versa, this is what you need.
class ViewController: UIViewController {
#IBOutlet weak var mapView: MapView! {
didSet {
mapView.delegate = self; mapView.tapDelegate = self
}
}
}
extension ViewController: MGLMapViewDelegate {
func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
print("didSelect annotationWithCoordinate: \(annotation.coordinate)")
}
}
extension ViewController: MapViewTapDelegate {
func mapViewTapped(withTouchLocation touchLocation: CLLocationCoordinate2D) {
print("mapViewTapped touchLocation: \(touchLocation)")
}
}
I think that the method
-(void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id<MGLAnnotation>)annotation will solve your problem for selecting annotation.
I made another workaround here
I cloned the release-ios-v3.3.0 and created package using Building the SDK and added one delegate method in
MGLMapvViewDelegate.h
as per my need something like that.
-(void)mapView:(MGLMapView *)mapView tapOnNonAnnotationAreaWithPoints:(CGPoint)points
and in MGLMapView.mm I updated the code like that,
else{
if(self.selectedAnnotation)
[self deselectAnnotation:self.selectedAnnotation animated:YES];
else if([self.delegate respondsToSelector:#selector(mapView:tapOnNonAnnotationAreaWithPoints:)])
[self.delegate mapView:self tapOnNonAnnotationAreaWithPoints:tapPoint];
}
which is in -(void)handleSingleTapGesture:(UITapGestureRecognizer *)singleTap
method.
Its working for me, as I am able to detect single tap on non annotation area. later I convert the passed points to geo coordinates, so that I work with newly tapped coordinate.
Note
Newly Created library size is about 48.7 MB where as downloaded library was 38.3 MB.
Style is not working properly, you can see in attached pic. (Tried with different Style URL but none of it is same as previous)
I feel lack in App performance, Zooming and Panning is not smooth as the dynamic library gave by Mapbox guys.
I am still exploring. lemme know if you wanna me to check/explore.
Please accept this answer, if it is helpful.
Thank you,
Happy Coding.
Try swift 3
//determined is Point inner to realm polygons
func determinedInPoligon(point:CLLocationCoordinate2D, poligon:[CLLocationCoordinate2D]) -> Bool {
var result:Bool = false
var j = poligon.count - 1
for i in 0 ..< poligon.count {
if (poligon[i].longitude < point.longitude && poligon[j].longitude >= point.longitude || poligon[j].longitude < point.longitude && poligon[i].longitude >= point.longitude) &&
(poligon[i].latitude + (point.longitude - poligon[i].longitude) / (poligon[j].longitude - poligon[i].longitude) * (poligon[j].latitude - poligon[i].latitude) < point.latitude) {
result = !result;
}
j = i;
}
return result
}
func squareFrom(location: CGPoint, radius: Double) -> CGRect {//return rect with CGPoint center and radius
let length = radius
return CGRect(x: Double(location.x - CGFloat(length / 2)), y:
Double(location.y - CGFloat(length / 2)), width: length, height: length)
}
function handle
func handleTap(_ gesture: UITapGestureRecognizer) {
// Get the CGPoint where the user tapped.
let spot = gesture.location(in: mapView)
let my_features = mapView.visibleFeatures(at: spot)
let strZoomValue = mapView.zoomLevel > 15 ? "6" : "4"
//the feature structur object value not equal annotation object
for value in my_features.enumerated() {// feature items
if value.element is MGLPointAnnotation {
for annot in (mapView.annotations?.enumerated())! { // annotation items
if annot.element is MGLPointAnnotation {
//rounded lat and lng value
var arr_cllocation = [CLLocationCoordinate2D]()
for cllocation in [(annot.element as! MGLPointAnnotation).coordinate, (value.element as! MGLPointAnnotation).coordinate] {
let strLat = String(format: "%."+strZoomValue+"f", cllocation.latitude)
let strLon = String(format: "%."+strZoomValue+"f", cllocation.longitude)
arr_cllocation.append(
CLLocationCoordinate2D(latitude: CLLocationDegrees(strLat)!, longitude: CLLocationDegrees(strLon)!) )
}
if arr_cllocation.count == 2 &&
memcmp(&arr_cllocation[0], &arr_cllocation[1], MemoryLayout<CLLocationCoordinate2D>.size) == 0 {// 0 if equal object
// to do custom popup view
let instViewPopupLineClass = UINib(nibName: "ViewPopupBase", bundle: nil).instantiate(withOwner: self, options: nil).first as! UIView
for objectInst in instViewPopupLineClass.subviews.enumerated() {
if objectInst.element is UILabel {
let asdasdas = (annot.element as! MGLPointAnnotation).subtitle
(objectInst.element as! UILabel).text = asdasdas
MyCustomPopup(customView: instViewPopupLineClass, positionXY: spot)
break
}
}
}
}
} //for end
}
}
}
OR not accurate method, but a worker ;)
func handleTap(_ gesture: UITapGestureRecognizer) {// Get the CGPoint where the user tapped.
let spot = gesture.location(in: mapView)
let cllcordinatTap = mapView.convert(spot, toCoordinateFrom: mapView)
//for determined zoom scala
let deltScalaMap = abs(self.mapView.maximumZoomLevel - self.mapView.zoomLevel)
//The large is zoom maps, then smal size is tap location, and vice versa.
let checkScalaMap = deltScalaMap == 0 ? 1 : deltScalaMap
let _rect = squareFrom(location: CGPoint(x: cllcordinatTap.latitude, y: cllcordinatTap.longitude), radius: 0.00005 * checkScalaMap)
for annot in (mapView.annotations?.enumerated())! {
if annot.element is MGLPointAnnotation {
let _cordinatCurrentAnnotation = (annot.element as! MGLPointAnnotation).coordinate
if determinedInPoligon(point: _cordinatCurrentAnnotation, poligon:
[CLLocationCoordinate2D(latitude: CLLocationDegrees(_rect.minX), longitude: CLLocationDegrees(_rect.minY)),
CLLocationCoordinate2D(latitude: CLLocationDegrees(_rect.maxX), longitude: CLLocationDegrees(_rect.minY)),
CLLocationCoordinate2D(latitude: CLLocationDegrees(_rect.maxX), longitude: CLLocationDegrees(_rect.maxY)),
CLLocationCoordinate2D(latitude: CLLocationDegrees(_rect.minX), longitude: CLLocationDegrees(_rect.maxY))
]) {
// to do, if tap MGLPointAnnotation, annot.element
}
}
}
}
Here I got some workaround which helped to work with my requirement.
As per my need I am able to get single tap detection on both mapbox annotation marker and as well as on empty area of map
I created category for MGLMapView, (MGLMapView+EDCMapboxView) and overrides the touch methods
-touchesBegan:withEvent:
-touchesMoved:withEvent:
-touchesEnded:withEvent:
-touchesCancelled:withEvent:
MGLMapView+EDCMapboxView.h
#protocol EDCMapboxViewDelegate <NSObject>
#optional
- (void)mapboxViewDidCreateNewTicket:(MGLMapView*)mapView;
#end
#interface MGLMapView (EDCMapboxView)
#property (assign, nonatomic) BOOL shouldCreateNewTicket;
#property (weak, nonatomic) id <EDCMapboxViewDelegate> mapboxDelegate;
#end
MGLMapView+EDCMapboxView.m
#implementation MGLMapView (EDCMapboxView)
#dynamic mapboxDelegate;
#pragma mark -- Accessor
- (BOOL)shouldCreateNewTicket {
return [objc_getAssociatedObject(self, #selector(shouldCreateNewTicket)) boolValue];
}
- (void)setShouldCreateNewTicket:(BOOL)flag {
objc_setAssociatedObject(self, #selector(shouldCreateNewTicket), #(flag), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(id<EDCMapboxViewDelegate>)mapboxDelegate{
return objc_getAssociatedObject(self, #selector(mapboxDelegate));
}
- (void)setMapboxDelegate:(id<EDCMapboxViewDelegate>)mapboxDelegate{
objc_setAssociatedObject(self, #selector(mapboxDelegate), mapboxDelegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#pragma mark -- Overrided method for UIResponder
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{
NSLog(#"touchesBegan");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{
NSLog(#"touchesMoved");
self.shouldCreateNewTicket = NO;
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{
NSLog(#"touchesEnded");
}
- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{
NSLog(#"touchesCancelled");
[self createNewTicket];
}
- (void)createNewTicket{
if(self.shouldCreateNewTicket){
NSLog(#"Allowed to Create New ticket");
// Tells that tap is on empty area.
if([self.mapboxDelegate respondsToSelector:#selector(mapboxViewDidCreateNewTicket:)]){
[self.mapboxDelegate mapboxViewDidCreateNewTicket:self];
}
}
else{
NSLog(#"Not allowed to create New ticket");
// Tells tap is on annotation marker.
self.shouldCreateNewTicket = YES;
}
}
EDCMapboxViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.mapView.shouldCreateNewTicket = YES;
.....
.......
........
}
- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id <MGLAnnotation>)annotation {
NSLog(#"annotationCanShowCallout");
// Tells that annotation is tapped, then do not allow to create ticket.
self.mapView.shouldCreateNewTicket = NO;
return YES;
}
- (void)mapboxViewDidCreateNewTicket:(MGLMapView*)mapView{
// Tells that tap is on empty area and not on marker, then allow to create ticket.
}
It worked for me, hope it will help you guys as well.
Thanks.
Here is how I solved it, you should get the concept.
func onMapSingleTapped(recognizer: UITapGestureRecognizer)
{
let viewLocation: CGPoint = recognizer.locationInView(map)
// check if any annotations are hit
if(map.annotations != nil)
{
for annotation in map.annotations!
{
if(annotation.isKindOfClass(MapCheckInAnnotation))
{
let pin: MapCheckInAnnotation = annotation as! MapCheckInAnnotation
if let pinView = pin.view
{
print("pinview \(pinView.frame.origin)")
// check if hit pin instead of just map
if(viewLocation.x >= pinView.frame.origin.x && viewLocation.x < pinView.frame.origin.x + pinView.frame.width)
{
if(viewLocation.y >= pinView.frame.origin.y && viewLocation.y < pinView.frame.origin.y + pinView.frame.height)
{
mapView(map, didSelectAnnotationView: pinView)
return
}
}
}
}
}
}
// nope, no annotations were clicked
let mapLocation: CLLocationCoordinate2D = map.convertPoint(viewLocation, toCoordinateFromView: map)
print("onMapSingleTapped \(mapLocation)")
}
I tried sample project for your question and it works well.
.h
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#interface ViewController : UIViewController<MKMapViewDelegate>
#property (strong, nonatomic) IBOutlet MKMapView *mapTapAnnotation;
#end
.m
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
#synthesize mapTapAnnotation;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self setMapViewWithAnnnoationPin];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
-(void)setMapViewWithAnnnoationPin
{
mapTapAnnotation.showsUserLocation = YES;
mapTapAnnotation.mapType = MKMapTypeHybrid;
CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(12.900988, 80.227930);
MKCoordinateSpan span = MKCoordinateSpanMake(0.005, 0.005);
MKCoordinateRegion region = {coord, span};
MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
[annotation setCoordinate:coord];
[annotation setTitle:#"Single Tap"]; //You can set the subtitle too
[mapTapAnnotation setRegion:region];
[mapTapAnnotation addAnnotation:annotation];
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(detectSingleTap)];
tapGesture.numberOfTapsRequired = 1;
[mapTapAnnotation addGestureRecognizer:tapGesture];
}
-(void)detectSingleTap
{
NSLog(#"Finded the single tap on map view");
}
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
NSLog(#"The single tap annotation pin point is - %ld",(long)view.tag);
}
The printed output results are
Finded the single tap on map view
The single tap annotation pin point is - 0
As I haven't been successful with the solutions above I implemented a small delay after the always recognized map tap event. If an annotation is selected in this small time frame, then nothing is called any further. Otherwise only the map view was tapped and no annotation.
Works pretty well in my case.
MapView part
protocol MapDelegate: class {
func didTapMapView()
}
class AppleMapView: MKMapView {
weak var mapDelegate: MapDelegate?
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
if touches.first != nil {
mapDelegate?.didTapMapView()
}
}
........
}
MapViewController part
// property to store last tap at annotation
var lastAnnotationSelectionTime = Date()
// this method is called on every tap
func didTapMapView() {
let overlaySelectionDelayTime: TimeInterval = 0.2
DispatchQueue.main.asyncAfter(deadline: .now() + overlaySelectionDelayTime, execute: { [weak self] in
guard let self = self else { return }
if self.lastAnnotationSelectionTime.distance(to: Date()) > overlaySelectionDelayTime {
// only called if no annotation was tapped meanwhile
[ HANDLE TAP ON MAP HERE ]
}
})
}
// MARK: Marker Selection
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
lastAnnotationSelectionTime = Date() // setten if annotation is tapped
[ HANDLE TAP ON ANNOTATION HERE ]
}

How to disable scrolling while using UIPanGestureRecognizer on a UIView

In my app I have a draggable UIView contained in a UIScrollView so that I can zoom it. This moving UIView has a UIPanGestureRecognizer but if I zoom in first and then I try to move the UIView, that does not happen and instead the scrolling is performed as if the UIPanGestureRecognizer is not detected.
This is my associated method to the UIPanGestureRecognizer:
func handlePan(recognizer: UIPanGestureRecognizer) {
if (self.panRec.state == UIGestureRecognizerState.Began) {
self.scrollViewContainer.scrollEnabled = false
}
else if (self.panRec.state == UIGestureRecognizerState.Ended) {
self.scrollViewContainer.scrollEnabled = true
}
let translation = recognizer.translationInView(self)
if let view = recognizer.view {
self.center = CGPoint(x:view.center.x + translation.x,
y:view.center.y + translation.y)
}
recognizer.setTranslation(CGPointZero, inView: self)
self.updatePinsPosition()
}
As you can see I try to disable scrolling when the the user taps in the draggable UIView and reactivate it at the end but it doesn't work. Where am I making a mistake? What am I missing?
UPDATE
So explain myself better, I have a UIScrollView which contains a UIView and inside this last there are some other UIViews acting as pinch, the azure ones. I decided to use a further UIView as container so that the sub views are zoomed all together by scrolling. So, my zoom code is the following:
class PhotoViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var containerView: ContainerController!
override func viewDidLoad() {
...
self.scrollView.delegate = self
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.flashScrollIndicators()
self.scrollView.minimumZoomScale = 1.0
self.scrollView.maximumZoomScale = 10.0
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return self.containerView
}
}
You have to override this method of Scrollview Delegate in your ViewController for both zooming and panning
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
scale *= [[[self.scrollView window] screen] scale];
[view setContentScaleFactor:scale]; // View which you want to Zoom & pan.
for (UIView *subview in view.subviews) {
[subview setContentScaleFactor:scale]; // all the Container Views
}
}
Hope it will help you to solve your problem.

SpriteKit: add UIGestureRecognizer and detect which node was swiped

In which method do I add a UIGestureRecognizer to my SKScene. And how do I detect which node was swiped? This doesn't seem to work:
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
...
UISwipeGestureRecognizer *recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipe:)];
recognizer.direction = UISwipeGestureRecognizerDirectionUp;
[[self view] addGestureRecognizer:recognizer];
}
return self;
}
You add the UISwipeGestureRecognizer in this method:
- (void)didMoveToView:(SKView *)view
{
UISwipeGestureRecognizer *recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipe:)];
recognizer.direction = UISwipeGestureRecognizerDirectionUp;
[[self view] addGestureRecognizer:recognizer];
}
And that's how you detect which SKNode was swiped:
- (void)handleSwipe:(UISwipeGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint touchLocation = [sender locationInView:sender.view];
touchLocation = [self convertPointFromView:touchLocation];
SKSpriteNode *touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];
NSLog(#"%#", touchedNode);
}
}
Swift 3 version inspired by S.E.'s answer:
func didSceneViewPan(_ sender: UIPanGestureRecognizer) {
if sender.state == .began {
let touchPoint = sender.location(in: sender.view)
let touchLocation = convertPoint(fromView: touchPoint)
if isPointInNode(point: touchLocation, node: testNode) {
print("yes, node touched!")
} else {
print("no bueno :(")
}
}
}
fileprivate func isPointInNode(point: CGPoint, node: SKNode) -> Bool {
// Get all nodes intersecting <point>
let nodes = self.nodes(at: point)
// Return true on first one that matches <node>
for n in nodes {
if n == node {
return true
}
}
// If here, <point> not inside <node> so return false
return false
}
For 2017...
Say you have a scene with various sprites.
Tap on one of them. You need to know which one was tapped ...
class YourScene: SKScene {
override func didMove(to view: SKView) {
super.didMove(to: view)
// your setup for the scene - example, add objects etc
setupTapDetection()
}
func setupTapDetection() {
let t = UITapGestureRecognizer(target: self, action: #selector(tapped(_:)))
view?.addGestureRecognizer(t)
}
#objc func tapped(_ tap: UITapGestureRecognizer) {
// critical...
if tap.state != .ended { return }
// Note: you actually do NOT "invert the Y axis",
// these two calls in fact take care of that properly.
let viewPoint = tap.location(in: tap.view)
let scenePoint = convertPoint(fromView: viewPoint)
// technically there could be more than one node...
let nn = nodes(at: scenePoint)
if nn.count == 0 {
print("you very like tapped 'nowhere'")
return
}
if nn.count != 1 {
// in almost all games or physics scenes,
// it is not possible this would happen
print("something odd happened - overlapping nodes?")
return
}
// you must have a custom class for your sprites
// Say it is class YourSprites: SKSpriteNode
if let dotSpriteTapped = nn.first! as? YourSprites {
// we are done. dotSpriteTapped is the YourSprite,
// which was tapped. now do whatever you want.
let nm = String(describing: dotSpriteTapped.name)
print(" \(nm)")
// you can now access your own properties:
let whichTank = dotSpriteTapped.someID
print(" \(whichTank)")
let tankPower = dotSpriteTapped.sheildStrength
print(" \(tankPower)")
}
}
Enjoy
You can check of the nodes at a location include a node you're looking for using this:
nodes(at: touchLocation).contains(nodeYouAreLookingFor)

Navigation pop view when swipe right like Instagram iPhone app.How i achieve this?

I want to pop a view when swipe right on screen or it's work like back button of navigation bar.
I am using:
self.navigationController.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
This single line of code for pop navigation view and it's a work for me but when i swipe form middle of screen this will not work like Instagram iPhone app.
Here i give a one screen of Instagram app in that you can see the Example of swipe right pop navigation view:
Apple's automatic implementation of the "swipe right to pop VC" only works for the left ~20 points of the screen. This way, they make sure they don't mess with your app's functionalities. Imagine you have a UIScrollView on screen, and you can't swipe right because it keeps poping VCs out. This wouldn't be nice.
Apple says here :
interactivePopGestureRecognizer
The gesture recognizer responsible for popping the top view controller off the navigation stack. (read-only)
#property(nonatomic, readonly) UIGestureRecognizer *interactivePopGestureRecognizer
The navigation controller installs this gesture recognizer on its view
and uses it to pop the topmost view controller off the navigation
stack. You can use this property to retrieve the gesture recognizer
and tie it to the behavior of other gesture recognizers in your user
interface. When tying your gesture recognizers together, make sure
they recognize their gestures simultaneously to ensure that your
gesture recognizers are given a chance to handle the event.
So you will have to implement your own UIGestureRecognizer, and tie its behavior to the interactivePopGestureRecognizer of your UIViewController.
Edit :
Here is a solution I built. You can implement your own transition conforming to the UIViewControllerAnimatedTransitioning delegate. This solution works, but has not been thoroughly tested.
You will get an interactive sliding transition to pop your ViewControllers. You can slide to right from anywhere in the view.
Known issue : if you start the pan and stop before half the width of the view, the transition is canceled (expected behavior). During this process, the views reset to their original frames. Their is a visual glitch during this animation.
The classes of the example are the following :
UINavigationController > ViewController > SecondViewController
CustomPopTransition.h :
#import <Foundation/Foundation.h>
#interface CustomPopTransition : NSObject <UIViewControllerAnimatedTransitioning>
#end
CustomPopTransition.m :
#import "CustomPopTransition.h"
#import "SecondViewController.h"
#import "ViewController.h"
#implementation CustomPopTransition
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.3;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
SecondViewController *fromViewController = (SecondViewController*)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
ViewController *toViewController = (ViewController*)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toViewController.view];
[containerView bringSubviewToFront:fromViewController.view];
// Setup the initial view states
toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
[UIView animateWithDuration:0.3 animations:^{
fromViewController.view.frame = CGRectMake(toViewController.view.frame.size.width, fromViewController.view.frame.origin.y, fromViewController.view.frame.size.width, fromViewController.view.frame.size.height);
} completion:^(BOOL finished) {
// Declare that we've finished
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}];
}
#end
SecondViewController.h :
#import <UIKit/UIKit.h>
#interface SecondViewController : UIViewController <UINavigationControllerDelegate>
#end
SecondViewController.m :
#import "SecondViewController.h"
#import "ViewController.h"
#import "CustomPopTransition.h"
#interface SecondViewController ()
#property (nonatomic, strong) UIPercentDrivenInteractiveTransition *interactivePopTransition;
#end
#implementation SecondViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.navigationController.delegate = self;
UIPanGestureRecognizer *popRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePopRecognizer:)];
[self.view addGestureRecognizer:popRecognizer];
}
-(void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// Stop being the navigation controller's delegate
if (self.navigationController.delegate == self) {
self.navigationController.delegate = nil;
}
}
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
// Check if we're transitioning from this view controller to a DSLSecondViewController
if (fromVC == self && [toVC isKindOfClass:[ViewController class]]) {
return [[CustomPopTransition alloc] init];
}
else {
return nil;
}
}
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {
// Check if this is for our custom transition
if ([animationController isKindOfClass:[CustomPopTransition class]]) {
return self.interactivePopTransition;
}
else {
return nil;
}
}
- (void)handlePopRecognizer:(UIPanGestureRecognizer*)recognizer {
// Calculate how far the user has dragged across the view
CGFloat progress = [recognizer translationInView:self.view].x / (self.view.bounds.size.width * 1.0);
progress = MIN(1.0, MAX(0.0, progress));
if (recognizer.state == UIGestureRecognizerStateBegan) {
NSLog(#"began");
// Create a interactive transition and pop the view controller
self.interactivePopTransition = [[UIPercentDrivenInteractiveTransition alloc] init];
[self.navigationController popViewControllerAnimated:YES];
}
else if (recognizer.state == UIGestureRecognizerStateChanged) {
NSLog(#"changed");
// Update the interactive transition's progress
[self.interactivePopTransition updateInteractiveTransition:progress];
}
else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled) {
NSLog(#"ended/cancelled");
// Finish or cancel the interactive transition
if (progress > 0.5) {
[self.interactivePopTransition finishInteractiveTransition];
}
else {
[self.interactivePopTransition cancelInteractiveTransition];
}
self.interactivePopTransition = nil;
}
}
#end
Create a pan gesture recogniser and move the interactive pop gesture recogniser's targets across.
Add your recogniser to the pushed view controller's viewDidLoad and voila!
let popGestureRecognizer = self.navigationController!.interactivePopGestureRecognizer!
if let targets = popGestureRecognizer.value(forKey: "targets") as? NSMutableArray {
let gestureRecognizer = UIPanGestureRecognizer()
gestureRecognizer.setValue(targets, forKey: "targets")
self.view.addGestureRecognizer(gestureRecognizer)
}
Here's a Swift version of Spynet's answer, with a few modifications. Firstly, I've defined a linear curve for the UIView animation. Secondly, I've added a semi-transparent black background to the view underneath for a better effect. Thirdly, I've subclassed a UINavigationController. This allows the transition to be applied to any "Pop" transition within the UINavigationController. Here's the code:
CustomPopTransition.swift
import UIKit
class CustomPopTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
else {
return
}
let containerView = transitionContext.containerView
containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)
// Setup the initial view states
toViewController.view.frame = CGRect(x: -100, y: toViewController.view.frame.origin.y, width: fromViewController.view.frame.size.width, height: fromViewController.view.frame.size.height)
let dimmingView = UIView(frame: CGRect(x: 0,y: 0, width: toViewController.view.frame.width, height: toViewController.view.frame.height))
dimmingView.backgroundColor = UIColor.black
dimmingView.alpha = 0.5
toViewController.view.addSubview(dimmingView)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
options: UIView.AnimationOptions.curveLinear,
animations: {
dimmingView.alpha = 0
toViewController.view.frame = transitionContext.finalFrame(for: toViewController)
fromViewController.view.frame = CGRect(x: toViewController.view.frame.size.width, y: fromViewController.view.frame.origin.y, width: fromViewController.view.frame.size.width, height: fromViewController.view.frame.size.height)
},
completion: { finished in
dimmingView.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
)
}
}
PoppingNavigationController.swift
import UIKit
class PoppingNavigationController : UINavigationController, UINavigationControllerDelegate {
var interactivePopTransition: UIPercentDrivenInteractiveTransition!
override func viewDidLoad() {
self.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
addPanGesture(viewController: viewController)
}
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if (operation == .pop) {
return CustomPopTransition()
}
else {
return nil
}
}
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
if animationController.isKind(of: CustomPopTransition.self) {
return interactivePopTransition
}
else {
return nil
}
}
func addPanGesture(viewController: UIViewController) {
let popRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanRecognizer(recognizer:)))
viewController.view.addGestureRecognizer(popRecognizer)
}
#objc
func handlePanRecognizer(recognizer: UIPanGestureRecognizer) {
// Calculate how far the user has dragged across the view
var progress = recognizer.translation(in: self.view).x / self.view.bounds.size.width
progress = min(1, max(0, progress))
if (recognizer.state == .began) {
// Create a interactive transition and pop the view controller
self.interactivePopTransition = UIPercentDrivenInteractiveTransition()
self.popViewController(animated: true)
}
else if (recognizer.state == .changed) {
// Update the interactive transition's progress
interactivePopTransition.update(progress)
}
else if (recognizer.state == .ended || recognizer.state == .cancelled) {
// Finish or cancel the interactive transition
if (progress > 0.5) {
interactivePopTransition.finish()
}
else {
interactivePopTransition.cancel()
}
interactivePopTransition = nil
}
}
}
Example of the result:
There really is no need to roll your own solution for this, sub-classing UINavigationController and referencing the built-in gesture works just fine as explained here.
The same solution in Swift:
public final class MyNavigationController: UINavigationController {
public override func viewDidLoad() {
super.viewDidLoad()
self.view.addGestureRecognizer(self.fullScreenPanGestureRecognizer)
}
private lazy var fullScreenPanGestureRecognizer: UIPanGestureRecognizer = {
let gestureRecognizer = UIPanGestureRecognizer()
if let cachedInteractionController = self.value(forKey: "_cachedInteractionController") as? NSObject {
let string = "handleNavigationTransition:"
let selector = Selector(string)
if cachedInteractionController.responds(to: selector) {
gestureRecognizer.addTarget(cachedInteractionController, action: selector)
}
}
return gestureRecognizer
}()
}
If you do this, also implement the following UINavigationControllerDelegate function to avoid strange behaviour at the root view controller:
public func navigationController(_: UINavigationController,
didShow _: UIViewController, animated _: Bool) {
self.fullScreenPanGestureRecognizer.isEnabled = self.viewControllers.count > 1
}
Subclassing the UINavigationController you can add a UISwipeGestureRecognizer to trigger the pop action:
.h file:
#import <UIKit/UIKit.h>
#interface CNavigationController : UINavigationController
#end
.m file:
#import "CNavigationController.h"
#interface CNavigationController ()<UIGestureRecognizerDelegate, UINavigationControllerDelegate>
#property (nonatomic, retain) UISwipeGestureRecognizer *swipeGesture;
#end
#implementation CNavigationController
#pragma mark - View cycles
- (void)viewDidLoad {
[super viewDidLoad];
__weak CNavigationController *weakSelf = self;
self.delegate = weakSelf;
self.swipeGesture = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(gestureFired:)];
[self.view addGestureRecognizer:self.swipeGesture]; }
#pragma mark - gesture method
-(void)gestureFired:(UISwipeGestureRecognizer *)gesture {
if (gesture.direction == UISwipeGestureRecognizerDirectionRight)
{
[self popViewControllerAnimated:YES];
} }
#pragma mark - UINavigation Controller delegate
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
self.swipeGesture.enabled = NO;
[super pushViewController:viewController animated:animated]; }
#pragma mark UINavigationControllerDelegate
- (void)navigationController:(UINavigationController *)navigationController
didShowViewController:(UIViewController *)viewController
animated:(BOOL)animate {
self.swipeGesture.enabled = YES; }
#end

Resources