I have a short line (MKPolyline) and a custom annotation class (MKPointAnnotaion). Right now I have the point annotation located at the midpoint of the polyline. However, I would like the callout to be displayed whenever any point on the polyline is touched, similar to how routing performs in Maps. Since polylines don't seem to have a touchable attribute like annotations, is there a way to have the annotation encompass the entire line?
I do not care about the annotations marker (it is actually a nuisance and would prefer to hide it), only the callout and associated class info.
If at all possible could someone provide a brief example code with the answer?
EDIT: Other posts seem to be from 5+ years ago with links that do not work anymore
A couple of thoughts:
I think your “add annotation” approach for where you want this callout to start from is going to be the easiest approach. If you start contemplating making an annotation view subclass that shows the path, I think that’s going to get pretty hairy pretty quickly (handling scaling changes, rotations, 3D, etc.). Overlays give you a bunch of behaviors for free, so I think you’ll want to stick with that. And annotations give you callout behaviors for free, too, so I think you’ll want to stay with that too.
If you don’t want your annotation view to show a pin/marker, just don’t subclass from MKPinAnnotationView or MKMarkerAnnotationView, but rather just use MKAnnotationView.
The tricky step is how to detect taps on the MKPolylineRenderer. One idea is to create a path that traces the outline of the path of the MKPolyline.
extension MKPolyline {
func contains(point: CGPoint, strokeWidth: CGFloat = 44, in mapView: MKMapView) -> Bool {
guard pointCount > 1 else { return false }
let path = UIBezierPath()
path.move(to: mapView.convert(from: points()[0]))
for i in 1 ..< pointCount {
path.addLine(to: mapView.convert(from: points()[i]))
}
let outline = path.cgPath.copy(strokingWithWidth: strokeWidth, lineCap: .round, lineJoin: .round, miterLimit: 0)
return outline.contains(point)
}
}
where
extension MKMapView {
func convert(from mapPoint: MKMapPoint) -> CGPoint {
return convert(mapPoint.coordinate, toPointTo: self)
}
}
You can then create a gesture recognizer that detects a tap, checks to see if it’s within this path that outlines the MKPolyline, or whatever. But these are the basic pieces of the solution.
Obviously, the answers here outline different, apparently looking at the distance to the paths. That conceivably would work, too.
Related
I have a view with many CAShapeLayer objects (there can be other CALayer objects as well) and I want to modify the CAShapeLayer object that the user clicks on.
I was experimenting with the below two methods but none of them works. Any tips would be great.
Approach one:
private func modifyDrawing(at point: NSPoint) {
for layer in view.layer!.sublayers! {
let s = layer.hitTest(point)
if s != nil && s is CAShapeLayer {
selectedShape = s as? CAShapeLayer
}
}
// modify some properties
selectedShape?.shadowRadius = 20
selectedShape?.shadowOpacity = 1
selectedShape?.shadowColor = CGColor.black
}
Approach two:
private func modifyDrawing(at point: NSPoint) {
let drawingsAtMouseClick: [CAShapeLayer] = view.layer!.sublayers!.compactMap{ $0 as? CAShapeLayer }
if drawingsAtMouseClick.isEmpty {
return
}
for drawing in drawingsAtMouseClick {
if drawing.contains(point) {
selectedShape = drawing
break
}
}
// modify some properties
selectedShape?.shadowRadius = 20
selectedShape?.shadowOpacity = 1
selectedShape?.shadowColor = CGColor.black
}
The point parameter passed to these functions is the NSEvent.locationInWindow. Not sure whether I should convert this with respect to the CAShapeLayer or something.
P.S.: This isn't production code so please kindly ignore Swift best practices, etc.
The CALayer.hitTest(_:) method will tell you the layer that was hit, including in a layer's sublayers.
You shouldn't need to check every sublayer. You should be able to ask your view's layer what layer was hit by asking the top-level layer.
A view's layer's coordinates are generally the same as the view's bounds. It's anchored at 0,0 in the parent layer, and the sublayers use that coordinate space. Thus, you should convert your point to view/layer coordinates before hit testing.
(I always have to go check to see which coordinate systems are flipped from the other between iOS and Mac OS and views and layers. You might need to flip coordinates. I leave that research up to you.)
Edit:
I seem to remember that CALayer.hitTest(_:) just checks that the layer's frame contains the point, not that it actually contains an opaque pixel at that position. It's more complex if you want to check to see if the point contains an opaque pixel.
I am creating a SpriteKit game and want to be able to take advantage of textAlerts - like all the games I grew up playing. I also want them to be flexible so I can subclass them and add them throughout my game - I am not sure how to do this and so would be very grateful of any advice.
There are a number of ways that I have started to look into:
Use another SKScene
The idea here would be to add another scene immediately on top of our current one. This would have a faded out background and would be configured with the text to be displayed. This seems like the wrong approach
Use an SKNode
I could create a custom node and initialise it with my specific text. I would then disable movement and add the node at the bottom of the screen. I would then customise the actions that occur when it is tapped.
Use an SKLabel
SKLabels are designed to show text so these seem like a promising place to look. The issue is that I want to add an image into the popup view (this a headshot of the person you are talking to) and so it doesn't feel like I should be able to inject an image in.
Use something else
I don't know what this might be. Is what I am trying to do easy or harder than I think?
My problem is that I come from a swift background so am struggling to convert to a SpriteKit mindset. In Swift I would put together a custom UIView with all the UIKit components and add it to the screen. In SpriteKit we don't have the same tools so I don't know the right combinations to put together what I want.
Ideally I want to be able to configure the text popup with an array of text so that a conversation can be had with the user tapping for each new line.
Edit 1: What I have tried so far
Based on #ElTomatos comment I have experimented with adding a SKSpriteNode and an SKLabelNode on top of each other using the following code:
guard let camera = camera else { return }
let dialogueBackground = SKSpriteNode(texture: nil, color: .red, size: CGSize(width: screenSize.width / camera.xScale, height: screenSize.height / 3 / camera.xScale))
dialogueBackground.anchorPoint = CGPoint(x: 0, y: 0)
dialogueBackground.zPosition = 50
dialogueBackground.position = CGPoint(x: -screenSize.width, y: -screenSize.height)
camera.addChild(dialogueBackground)
let label = SKLabelNode(text: "Hello world")
label.zPosition = 100
label.position = CGPoint(x: -screenSize.width, y: 100 - screenSize.height)
camera.addChild(label)
As you can see the view doesn't go to the edge of the screen. Is this something to do with the safe area? It's difficult to get the width and height as the camera doesn't have a CGSize we can use.
I have created a circle using Bezier path as well as i created tap events to check whether the layer has been tapped or not, it works fine until i increase the size of lineWidth of CAShapeLayer. By tapping on the lineWidth, sometimes it is detected and sometimes it is not detected.
I searched stackoverflow about the problem which i was having but i am unable to solve it. The problem remains the same, i am unable to detect certain taps on my layer(lineWidth area).
I just want to detect taps on the lineWidth of CAShapeLayer, i have been searching everywhere but couldn't find a proper solution for it. Most of the answers are in outdated languages. I would really appreciate if anyone could give an example to solve my issue. Swift 4.
https://i.imgur.com/194Aljn.png
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapDetected(tapRecognizer:)))
self.addGestureRecognizer(tapRecognizer)
#objc public func tapDetected(tapRecognizer:UITapGestureRecognizer) {
let tapLocation:CGPoint = tapRecognizer.location(in: self)
self.hitTest(tapLocation: CGPoint(x: tapLocation.x, y: tapLocation.y))
}
private func hitTest(tapLocation:CGPoint) {
if layer.path?.contains(tapLocation) == true {
print("Do something")
} else {
print("Nothing Found")
}
}
The problem is that the line stroke is not really part of the path - is it is just parts of its display. You can convert the path to be an larger path containing the stroke by using some CGPath methods:
let pathWithLineStroke = UIBezierPath.init(cgPath: path.cgPath.copy(strokingWithWidth: 2.0, lineCap: CGLineCap.butt, lineJoin: .bevel, miterLimit: 1.0));
Of course replace the the width, lineCap, lineJoin, and miterLimit with your actual values.
I'd recommend doing this earlier in your code, and then just drawing the path that already has the strokes built in, instead of setting those properies on the CALayer.
Hope that helps. Good luck.
My container view controller has a screen edge pan gesture to change the views. The code for panning the views looks as follows:
func changeView(recognizer: UIScreenEdgePanGestureRecognizer) {
println("INITIAL: \(recognizer.translationInView(view))")
if recognizer.state == .Began {
// Create and configure the view
println("BEGAN: \(recognizer.translationInView(view))")
}
if recognizer.state == .Changed {
println("CHANGED: \(recognizer.translationInView(view))")
let translation = recognizer.translationInView(view)
currentView.view.center.x += translation.x
pendingView.view.center.x += translation.x
recognizer.setTranslation(CGPointZero, inView: view)
}
if recognizer.state == .Ended {
if recognizer.view!.center.x > view.bounds.size.width {
// Animate the view to position
} else {
// Animate the view back to original
}
}
}
While this works, I'm still having an issue with the start of the panning. When a user swipes quickly, translation will have a value big enough to make the start of the pan looking "unsmoothly".
For example, with a quick swipe translation will start with a value of 100. The value is then added to the center.x of the views causing the undesired effect.
I noticed Safari has a screen edge gesture as well to change views and this effect doesn't occur no matter how quick the swipe is. Nor does this happen with a normal UIPanGestureRecognizer.
I've tried wrapping the "animation" in UIView.animateWithDuration(). It does look more smooth, but then it feels it's just lagging behind the actual gesture, unlike how it's done in Safari.
Can someone please tell me a better way to pan the views so it will look as smooth as in Safari?
EDIT:
I've added several lines to check the value of the translation and the problem is it jumps from 0 to some value causing the unwanted behavior. It doesn't matter where I put recognizer.setTranslation(CGPointZero, inView: view).
The output is:
INITIAL: (21.5, 0.0)
BEGAN: (21.5, 0.0)
INITIAL: (188.0, -3.0)
CHANGED: (188.0, -3.0)
After some more testing:
func changeView(recognizer: UIScreenEdgePanGestureRecognizer) {
println("INITIAL: \(recognizer.translationInView(view))")
recognizer.setTranslation(CGPointZero, inView: view)
}
INITIAL: (0.0, 0.0)
INITIAL: (130.5, -35.5)
FINAL:
Seems like creating and preparing the new view is causing some kind of minor lag in Began. The small amount of lag is enough to create a difference in translation of 100-200.
Probably have to preload the views somewhere else I guess.
This won't solve all your problems, since, as you have rightly said, a screen edge pan gesture recognizer is a little crusty in its behavior; but do note that you are omitting one valuable piece of data - the question of what recognizer.translationInView is in the .Began state. At that time, obviously, the finger has already moved considerably; for, if it had not, we would not have recognized this as a screen edge pan gesture! You will thus be much happier, I think, if you construct your tests like this:
switch recognizer.state {
case .Began:
// ... do initial setup
fallthrough // <-- TAKE NOTE
case .Changed:
// respond to changes
default:break
}
In that way, you will capture the missing datum and respond to it, and the jump will not be quite so bad.
I tried logging in both began and changed and my numbers (showing translationInView with no setTranslation back to zero) are this sort of thing:
began
changed
(-16.5, 0.0)
changed
(-41.5, 0.0)
changed
(-41.5, 0.0)
changed
(-58.5, 0.0)
(The first one, preceded by began, is the fallthrough execution of changed.) So yes, we do go from nothing to -41 very fast, but at least there is an intermediate value of -16.5 so it isn't quite so abrupt.
Also I should add that if there is a serious delay and jump it may well be that you have multiple conflicting gesture recognizers. If so, you can detect this fact by using delegate methods such as gestureRecognizer:shouldRequireFailureOfGestureRecognizer: - which will also let you prioritize between them and perhaps make the other g.r. give way sooner.
I would like my buttons to fade in when changing scene rather than just be their.
I am using sprite kit and UIView.animateWithDuration doesn't work.
How else could you do it using sprite kit in swift?
I'm assuming you are using UIButtons, and you really shouldn't, use only SKNodes (Sprites, Shapes, Labels, etc) and the methods of touch handle like:
touchBegan:
touchMoved:
TouchEnded:
If you want to build a button or a switch for generic use, you should try this control I made, the use its pretty straight forward:
You just initialize the type of Button/Switch you want (ColoredSprite, Textured or TextOnly)
let control = TWButton(normalColor: SKColor.blueColor(), highlightedColor: SKColor.redColor(), size: CGSize(width: 160, height: 80))`
And after initialize you add a closure to it (like addTargetForSelector on UIButton)
control.addClosureFor(.TouchUpInside, target: self, closure: { (scene, sender) -> () in
scene.testProperty = "Changed Property"
})
}
That’s it! There’s more info on the readme section on the GitHub page: https://github.com/txaidw/TWControls
Then you can use actions to fade your button
SKAction.fadeAlphaTo(alpha:, duration:)