I'm building an app to edit PDFs on iPad.
I'm trying to implement the dragging of the annotations with a panGesture recognizer that I added to the superview of the PDFView. The problem is that the new rect bounds of the annotation gets assigned but the changes doesn't reflect on screen.
Here is my code:
#objc func handlePanGesture(panGesture: UIPanGestureRecognizer) {
let touchLocation = panGesture.location(in: pdfView)
guard let page = pdfView.page(for: touchLocation, nearest: true) else {
return
}
let locationOnPage = pdfView.convert(touchLocation, to: page)
switch panGesture.state {
case .began:
guard let annotation = page.annotation(at: locationOnPage) else {
return
}
currentlySelectedAnnotation = annotation
case .changed:
guard let annotation = currentlySelectedAnnotation else {
return
}
let initialBounds = annotation.bounds
annotation.bounds = CGRect(origin: locationOnPage,
size: initialBounds.size)
print("move to \(locationOnPage)")
case .ended, .cancelled, .failed:
break
default:
break
}
}
Hope you can help me.
Well, Since nobody replied. I think there is a bug in the framework, so I'll post what worked for me, after some time of trial and error.
let initialBounds = annotation.bounds
annotation.bounds = CGRect(
origin: locationOnPage,
size: initialBounds.size)
page.removeAnnotation(annotation)
page.addAnnotation(annotation)
It's not elegant, but it does the job
Using a Bezierpath the whole bezierpath moves on bounds change.
PDF's builtin line type does not get moved with changing bounds though, so have to set startPoint and endPoint at every change.
I added a line to your code so that when dragging, it puts the annotation center where your finger is dragging
case .changed:
guard let annotation = currentlySelectedAnnotation else {
return
}
let initialBounds = annotation.bounds
// Set the center of the annotation to the spot of our finger
annotation.bounds = CGRect(x: locationOnPage.x - (initialBounds.width / 2), y: locationOnPage.y - (initialBounds.height / 2), width: initialBounds.width, height: initialBounds.height)
print("move to \(locationOnPage)")
case .ended, .cancelled, .failed:
currentlySelectedAnnotation = nil
default:
break
}
}
Related
Hi all I am trying to get the compass to disappear off my map and have hit a brick wall. Below is my map initialization function and I'm not sure what the variable is. I have tried mapView.compassEnabled == false, mapView.compassView = nil and a few other things that I have found to no avail.
I appreciate any help!
#objc func initMap() {
if mapView != nil {
print("Attempting to init map that is already initialized, returning")
return
}
mapView = NavigationMapView(frame: UIApplication.shared.delegate!.window!!.bounds, // TODO: Set frame from react
styleURL: NSURL(string: "mapbox://styles/paway/ckcp7w04x03by1iqi1t26uilu") as URL?)
guard let mapView = self.mapView else { return }
mapView.zoomLevel = 2
mapView.delegate = self
mapView.locationManager.setDistanceFilter?(6)
PWLocationManager.shared.delegate = self
mapView.setCenter(CLLocationCoordinate2D(latitude: 37.0902, longitude: -95.7129), animated: false)
mapView.navigationMapViewDelegate = self
switch cameraMode {
case "overview":
mapCameraState = .overviewMode
mapView.userTrackingMode = .followWithHeading
case "followHeading":
mapCameraState = .followMode
mapView.userTrackingMode = .followWithHeading
default:
mapView.userTrackingMode = .none
}
mapView.isPitchEnabled = true
mapView.showsUserLocation = true
self.addSubview(mapView)
addMapTapRecognizer()
addObservers()
}
Updated answer**
This only works if you have a component that is on top of your map. IF you have just a plain map I would try using negative margins to push the compass out of view.
I found the answer here https://stackoverflow.com/a/57560116/13639051
I added :
mapView.compassViewPosition = .bottomLeft
mapView.compassViewMargins = CGPoint(x: 0, y: 0)
which hides the compass behind a bottom toast that is consistently across the project.
I have a markerView which you can grab and move around. I added a panGesture to it.
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handleDraggedView(_:)))
fanMenu.addGestureRecognizer(panGesture)
To drag the view around and update the position of it:
#objc func handleDraggedView(_ sender: UIPanGestureRecognizer){
if let marker = documentationMarkers.first(where: {$0.documentationId == documentationPressed.id}) {
if(fanMenu.isOpen && marker.documentationType == "DEFAULT") {
self.viewForTilingView.bringSubview(toFront: marker)
let translation = sender.translation(in: self.viewForTilingView)
marker.center = CGPoint(x: marker.center.x + translation.x, y: marker.center.y + translation.y)
let point = viewForTilingView.convert(marker.center, to: scrollView)
fanMenu.center = CGPoint(x: point.x,
y: point.y)
sender.setTranslation(CGPoint.zero, in: self.viewForTilingView.superview)
print("Startpoint-X: \(point.x)", "Startpoint-Y: \(point.y)")
if(sender.state == .ended) {
print("Endpoint-X: \(point.x)", "Endpoint-Y: \(point.y)")
if let documentation = documentations.first(where: {$0.id == marker.documentationId}) {
let levelZoom : CGFloat = pow(2.0, CGFloat(viewForTilingView.tileProvider!.levelsOfDetail - 1))
let positionX = Int(marker.center.x * levelZoom)
let positionY = Int(marker.center.y * levelZoom)
_ = updatePosition(documentation: documentation, positionX: positionX, positionY: positionY)
documentationAdded(doc: documentation, update: true)
}
}
}
}
}
my start and endpoints are correct. When I long press my marker it prints the correct startpoints and when I release it somewhere else it gives me the correct endpoints.
I added some buttons to my marker view. Two of them are a cancel and confirm Button.
In a switch case statement it checks which button is selected. If the conform button is selected and you moved the marker around it updates the position ( = do nothing cause it updates it anyways) but when the cancel button is selected it should not update the postion and reset it to the old one.
func contextMenuButtonClicked(buttonId: String) {
let doc = documentationPressed
switch buttonId {
case "main":
break
case "cancelButton":
//update to the old position
My question is how can I get the old position of my marker view and place it to the start position when the user presses the cancel button.
You can store the marker position by switching between pan states.
For e.g.
//declare a variable inside your class- var olPostion = CGPoint()
if sender.state == .began {
oldPosition = marker.center
}
and on cancelButtonPress, reset the button position to old position
marker.center = oldPosition
NOTE: If you want to reset the button position the position it was
when the view controller loaded, assign the value od oldPosition in
viewDidLoad()
I have function which creates a drag line to connect 2 buttons to each other. This works fine but if some buttons overlap each other, it will select both if I drag over where they overlap. I only want to connect the top button.
I think the issue is with the sender.location selecting layers on top and below. Is there a way to tell the sender.location to only select the top view? Thanks for any input and direction
func addPanReconiser(view: UIView){
let pan = UIPanGestureRecognizer(target: self, action: #selector(DesignViewController.panGestureCalled(_:)))
view.addGestureRecognizer(pan)
}
#objc func panGestureCalled(_ sender: UIPanGestureRecognizer) {
let currentPanPoint = sender.location(in: self.view)
switch sender.state {
case .began:
panGestureStartPoint = currentPanPoint
self.view.layer.addSublayer(lineShape)
case .changed:
let linePath = UIBezierPath()
linePath.move(to: panGestureStartPoint)
linePath.addLine(to: currentPanPoint)
lineShape.path = linePath.cgPath
lineShape.path = CGPath.barbell(from: panGestureStartPoint, to: currentPanPoint, barThickness: 2.0, bellRadius: 6.0)
for button in buttonArray {
let point = sender.location(in: button)
if button.layer.contains(point) {
button.layer.borderWidth = 4
button.layer.borderColor = UIColor.blue.cgColor
} else {
button.layer.borderWidth = 0
button.layer.borderColor = UIColor.clear.cgColor
}
}
case .ended:
for button in buttonArray {
let point = sender.location(in: button)
if button.layer.contains(point){
//DO my Action here
lineShape.path = nil
lineShape.removeFromSuperlayer()
}
}
default: break
}
}
}
Note: some of the lines of codes are from custom extensions. I kept them in as they were self explanatory.
Thanks for the help
There is a way to walk around. It seems like you simply want your gesture end up at one button above all the others, thus by adding a var outside the loop and each time a button picked, comparing with the var of its level at z.
case .ended:
var pickedButton: UIButton?
for button in buttonArray {
let point = sender.location(in: button)
if button.layer.contains(point){
if pickedButton == nil {
pickedButton = button
} else {
if let parent = button.superView, parent.subviews.firstIndex(of: button) > parent.subviews.firstIndex(of: pickedButton!) {
pickedButton = button
}
}
}
}
//DO my Action with pickedButton here
lineShape.path = nil
lineShape.removeFromSuperlayer()
A UIView has a property called subViews where elements with higher indexes are in front of the ones with lower indexes. For instance, subView at index 1 is in front of subView with index 0.
That being said, to get the button that's on top, you should sort your buttonArray the same way subViews property of UIView is organized. Assuming that your buttons are all siblings of the same UIView (this might not be necessarily the case, but you can tweak them so you get them sorted correctly):
var buttonArray = view.subviews.compactMap { $0 as? UIButton }
Thus, keeping your buttonArray sorted that way, the button you want is the one that contains let point = sender.location(in: button) with higher index in the array.
I am working on an AR based iOS app using ARKit(SceneKit). I used the Apple sample code https://developer.apple.com/documentation/arkit/handling_3d_interaction_and_ui_controls_in_augmented_reality as base for this. Using this i am able to move or rotate the whole Virtual Object.
But i want to select and move/rotate a Child Node in Virtual object using user finger, similar to how we move/rotate the whole Virtual Object itself.
I tried the below two links but it is only moving the child node in particular axis but not freely moving anywhere as the user moves the finger.
ARKit - Drag a node along a specific axis (not on a plane)
Dragging SCNNode in ARKit Using SceneKit
Also i tried replacing the Virtual Object which is a SCNReferenceNode with SCNode so that whatever functionality present for existing Virtual Object applies to Child Node as well, it is not working.
Can anyone please help me on how to freely move/rotate not only the Virtual Object but also the child node of a Virtual Object?
Please find the code i am currently using below,
let tapPoint: CGPoint = gesture.location(in: sceneView)
let result = sceneView.hitTest(tapPoint, options: nil)
if result.count == 0 {
return
}
let scnHitResult: SCNHitTestResult? = result.first
movedObject = scnHitResult?.node //.parent?.parent
let hitResults = self.sceneView.hitTest(tapPoint, types: .existingPlane)
if !hitResults.isEmpty{
guard let hitResult = hitResults.last else { return }
movedObject?.position = SCNVector3Make(hitResult.worldTransform.columns.3.x, hitResult.worldTransform.columns.3.y, hitResult.worldTransform.columns.3.z)
}
To move an object:
Perform a hitTest to check where you have touched, and detect which plane you touched and get a position. Move your SCNNode to that position by changing the node.position value with an SCNVector3.
Code:
#objc func panDetected(recognizer: UIPanGestureRecognizer){
let hitResult = self.arSceneView.hitTest(loc, types: .existingPlane)
if !hitResult.isEmpty{
guard let hitResult = hitResult.last else { return }
self.yourNode.position = SCNVector3Make(hitResult.worldTransform.columns.3.x, hitResult.worldTransform.columns.3.y, hitResult.worldTransform.columns.3.z)
}
The above code is enough to move your node over a detected plane, anywhere you touch, and not just in a single axis.
Rotating a node according to your gesture is a very difficult task and I have worked on a solution for quite sometime, never reaching a perfect output.
But, I have come across this repository in GitHub which allows you to do just that with a very impressive result.
https://github.com/Xartec/ScreenSpaceRotationAndPan
The Swift version of the code you require to rotate your node using your gesture would be :
var previousLoc: CGPoint?
var touchCount: Int?
#objc func panDetected(recognizer: UIPanGestureRecognizer){
let loc = recognizer.location(in: self.view)
var delta = recognizer.translation(in: self.view)
if recognizer.state == .began {
previousLoc = loc
touchCount = recognizer.numberOfTouches
}
else if gestureRecognizer.state == .changed {
delta = CGPoint.init(x: 2 * (loc.x - previousLoc.x), y: 2 * (loc.y - previousLoc.y))
previousLoc = loc
if touchCount != recognizer.numberOfTouches {
return
}
var rotMatrix: SCNMatrix4!
let rotX = SCNMatrix4Rotate(SCNMatrix4Identity, Float((1.0/100) * delta.y), 1, 0, 0)
let rotY = SCNMatrix4Rotate(SCNMatrix4Identity, Float((1.0 / 100) * delta.x), 0, 1, 0)
rotMatrix = SCNMatrix4Mult(rotX, rotY)
let transMatrix = SCNMatrix4MakeTranslation(yourNode.position.x, yourNode.position.y, yourNode.position.z)
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, SCNMatrix4Invert(transMatrix))
let parentNoderanslationMatrix = SCNMatrix4MakeTranslation((self.yourNode.parent?.worldPosition.x)!, (self.yourNode.parent?.worldPosition.y)!, (self.yourNode.parent?.worldPosition.z)!)
let parentNodeMatWOTrans = SCNMatrix4Mult((self.yourNode.parent?.worldTransform)!, SCNMatrix4Invert(parentNoderanslationMatrix))
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, parentNodeMatWOTrans)
let camorbitNodeTransMat = SCNMatrix4MakeTranslation((self.arSceneView.pointOfView?.worldPosition.x)!, (self.arSceneView.pointOfView?.worldPosition.y)!, (self.arSceneView.pointOfView?.worldPosition.z)!)
let camorbitNodeMatWOTrans = SCNMatrix4Mult((self.arSceneView.pointOfView?.worldTransform)!, SCNMatrix4Invert(camorbitNodeTransMat))
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, SCNMatrix4Invert(camorbitNodeMatWOTrans))
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, rotMatrix)
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, camorbitNodeMatWOTrans)
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, SCNMatrix4Invert(parentNodeMatWOTrans))
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, transMatrix)
}
}
I'm trying to add a node (a sphere) to a body model but it doesn't work properly after I rotate the model through a pan gesture.
Here's how I'm adding the node (using a long tap gesture):
func addSphere(sender: UILongPressGestureRecognizer) {
switch sender.state {
case .Began:
let location = sender.locationInView(bodyView)
let hitResults = bodyView.hitTest(location, options: nil)
if hitResults.count > 0 {
let result = hitResults.first!
let secondSphereGeometry = SCNSphere(radius: 0.015)
secondSphereGeometry.firstMaterial?.diffuse.contents = UIColor.redColor()
let secondSphereNode = SCNNode(geometry: secondSphereGeometry)
let vpWithZ = SCNVector3(x: Float(result.worldCoordinates.x), y: Float(result.worldCoordinates.y), z: Float( result.worldCoordinates.z))
secondSphereNode.position = vpWithZ
bodyView.scene!.rootNode.addChildNode(secondSphereNode)
}
break
default:
break
}
}
Here is how I rotate the view:
func rotateGesture(sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(sender.view)
var newZAngle = (Float)(translation.x)*(Float)(M_PI)/180.0
newZAngle += currentZAngle
bodyView.scene!.rootNode.transform = SCNMatrix4MakeRotation(newZAngle, 0, 0, 1)
if sender.state == .Ended {
currentZAngle = newZAngle
}
}
And to load the 3D model I just do:
bodyView.scene = SCNScene(named: "male_body.dae") // bodyView is a SCNView in the storyboard
I found something related to the worldTransform property and also the function convertPosition:toNode: but couldn't find an example that works well.
The problem is that, if I rotate the model, the sphere are not positioned properly. They're always positioned as if the model was in its initial state.
If I turn the body and add long tap his arm (on the side), the sphere is added somewhere floating in front of the body, as you can see above.
I don't know how to fix this. Appreciate if someone can help me. Thanks!