Detect tap for UILabel with Autoshrink to minimum font size enabled - uilabel

I am using the following UITapGestureRecognizer that I modified so others could copy and paste to see the same output as me.
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
//let mutableAttribString = NSMutableAttributedString(attributedString: label.attributedText!)
//mutableAttribString.addAttributes([NSAttributedString.Key.font: label.font!], range: NSRange(location: 0, length: label.attributedText!.length))
let textStorage = NSTextStorage(attributedString: label.attributedText!)
//let textStorage = NSTextStorage(attributedString: mutableAttribString)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
print("LabelSize=",labelSize)
textContainer.size = labelSize
print("TextContainerSize=",textContainer.size)
// Find the tapped character location and compare it to the specified range
let locationOfTouchInLabel = self.location(in: label)
print("LocationOfTouchInLabel=",locationOfTouchInLabel)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
print("TextBoundingBox=",textBoundingBox)
//let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
//(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
print("textContainerOffset",textContainerOffset)
//let locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
// locationOfTouchInLabel.y - textContainerOffset.y);
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
print("LocationOfTouchInTextContainer",locationOfTouchInTextContainer)
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
print("IndexOfCharacter=",indexOfCharacter)
print("TargetRange=",targetRange)
return NSLocationInRange(indexOfCharacter, targetRange)
}
}
I set the label like so to detect clicks like other answers have said to do:
formulaLabel.addGestureRecognizer(UITapGestureRecognizer(target:self, action: #selector(tapLabel(gesture:))))
It is set to attributed.
In case it matters I have the following code that performs an animation with the text.
func doFormulaAnimation(){
//animTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true, block: {_ in
//Draw correct formula based on mode.
var formula = ""
var highlight_ranges:[NSRange] = []
if(stateController?.tdfvariables.ebrtMode == true){
formulaLabel.numberOfLines = 1
formula = "BED = N * d * [ RBE + ( d / (α/β) ) ]"
self.formulaLabel.attributedText = NSMutableAttributedString(string: formula, attributes: [ NSAttributedString.Key.foregroundColor: UIColor.label ])
if(self.dosePerFractionSelected==true){
highlight_ranges.append(NSRange(location: 10, length: 1))
highlight_ranges.append(NSRange(location: 24, length: 1))
}
if(self.alphaBetaSelected==true){
highlight_ranges.append(NSRange(location: 29, length: 3))
}
if(self.totalDoseSelected==true){
highlight_ranges.append(NSRange(location: 6, length: 5))
}
if(self.numFractionsSelected==true){
highlight_ranges.append(NSRange(location: 6, length: 1))
}
}
if(stateController?.tdfvariables.ldrMode == true){
formulaLabel.numberOfLines = 3
formula = "BED = R * T * [RBE + (( g * R * T) / (α/β) ) ]\ng = ( 2 / μT ) * ( 1 - ( ( 1 - exp(-μT) / μT ) )\nμ = 0.693 / Thalf repair"
}
if(stateController?.tdfvariables.permMode == true){
formulaLabel.numberOfLines = 2
formula = "BED = ( R0 / L ) * [ RBE + ( R0 / ( ( u + L) * (α/β) ) ) ]\n"
}
UIView.transition(with: self.formulaLabel, duration: 0.5, options: .transitionCrossDissolve, animations: {
let colorAttribute = [ NSAttributedString.Key.foregroundColor: UIColor.label ]
let attributedTextFormula = NSMutableAttributedString(string: formula, attributes: colorAttribute)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 6
paragraphStyle.lineBreakMode = .byTruncatingTail
//paragraphStyle.alignment = .center
attributedTextFormula.addAttribute(
.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: attributedTextFormula.length
))
self.formulaLabel.attributedText = attributedTextFormula
//
}, completion: { finished in
print("finished first transition")
UIView.transition(with: self.formulaLabel, duration: 0.5, options: .transitionCrossDissolve, animations: {
let colorAttribute = [ NSAttributedString.Key.foregroundColor: UIColor.label ]
let attributedTextFormula = NSMutableAttributedString(string: formula, attributes: colorAttribute)
for range in highlight_ranges {
attributedTextFormula.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.red, range: range)
}
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 6
paragraphStyle.lineBreakMode = .byTruncatingTail
//paragraphStyle.alignment = .center
attributedTextFormula.addAttribute(
.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: attributedTextFormula.length
))
self.formulaLabel.attributedText = attributedTextFormula
//self.formulaLabel.addInterlineSpacing(spacingValue: 1)
}, completion: { finished in
print("finished second transition")
})
})
}
The code above demonstrates me creating the NSMutableString accordingly and setting the paragraph style and line spacing accordingly (I have a hunch this may be the issue but not sure how to test that) etc...
Finally I have my tap label function
#IBAction func tapLabel(gesture: UITapGestureRecognizer) {
guard let text = formulaLabel.attributedText?.string else {
return
}
let AB_Range = text.range(of: "(α/β)")
//let AB_Range = NSRange(location: 29, length: 3)
if gesture.didTapAttributedTextInLabel(label: formulaLabel, inRange: NSRange(AB_Range!, in: text)) {
print("Tapped a/b")
} else {
print("Tapped none")
}
}
Currently it does not tap on the correct location but is off by a very large degree. Upon debugging it becomes obvious why it's not working but I am not sure how to fix the code or where it went wrong.
When i click on the A/B like I'm supposed to I get this as the debug info.
LabelSize= (315.0, 43.0)
TextContainerSize= (315.0, 43.0)
LocationOfTouchInLabel= (266.0, 28.0)
TextBoundingBox= (0.0, 0.0, 185.373046875, 13.8)
textContainerOffset (64.8134765625, 14.6)
LocationOfTouchInTextContainer (201.1865234375, 13.4)
IndexOfCharacter= 36
TargetRange= {28, 5}
Tapped none
The part where it calculates the textbounding box or textcontaineroffset or the locationoftouchintextcontainer are most likely the error points but I'm not sure which it is since this is hard to understand.
I can tell it's not working correctly because in my string it's no where near the end and it always says indexofcharacter=36 no matter where I click past it, which means it's not calculating something correctly with one of the variables I mentioned above.
Any help is appreciated if they know why this isn't working.
To give an idea how the text looks right now I'll show you below. My end goal is to make it so I can click on parts in the formula to get values to appear, so clickable links you could say.
UPDATE::
I forgot to mention that when I updated the UITapGestureRecognizer and add these lines:
let mutableAttribString = NSMutableAttributedString(attributedString: label.attributedText!)
mutableAttribString.addAttributes([NSAttributedString.Key.font: label.font!], range: NSRange(location: 0, length: label.attributedText!.length))
and set it to the text storage, let textStorage = NSTextStorage(attributedString: mutableAttribString) I do seem to see some improvements in the numbers but it's still way off.
LabelSize= (315.0, 43.0)
TextContainerSize= (315.0, 43.0)
LocationOfTouchInLabel= (269.0, 25.5)
TextBoundingBox= (7.505859375, 0.0, 299.98828125, 42.9609375)
textContainerOffset (0.0, 0.01953125)
LocationOfTouchInTextContainer (269.0, 25.48046875)
IndexOfCharacter= 17
TargetRange= {28, 5}
Tapped none
You will notice the textboundingbox more closely resembles the labelsize at least. I read somewhere that an issue could be if you are using different fonts so this is why I tried adding that in to be sure it's working correctly.
Also interestingly, adding this to the UITapGestureRecognizer seems to help also:
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 6
paragraphStyle.lineBreakMode = .byTruncatingTail
//paragraphStyle.alignment = .center
mutableAttribString.addAttribute(
.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: mutableAttribString.length
))
I noticed the textbounding box shows 0,0 correctly when I do this instead of 7.50
LabelSize= (315.0, 43.0)
TextContainerSize= (315.0, 43.0)
LocationOfTouchInLabel= (262.5, 31.0)
TextBoundingBox= (0.0, 0.0, 299.98828125, 42.9609375)
textContainerOffset (7.505859375, 0.01953125)
LocationOfTouchInTextContainer (254.994140625, 30.98046875)
IndexOfCharacter= 17
TargetRange= {28, 5}
Tapped none
**MAJOR UPDATE*****
Well great news! It turns out that the font fix was part of the fix, but because I had autoshrink enabled with a minimum font size and maximum font size it was never calculating correctly! When I set to a fixed font size everything works!
Question is now how to make it work with autoshrink enabled? Note that you will have to set size to something very large to prove this works, like I set mine to 55 so it autoshrinks accordingly and then you see the error, if the default size is smaller then it seems to work fine, which tells me the error has something to do with the size being initially very large maybe?

So I finally figured out an acceptable answer.
The key is the code below:
let formulaLabelWidth = formulaLabel.bounds.size.width
var font_size:CGFloat = 36.0 //Change this to a higher number if you need to.
var stringSize = NSString(string: formula).size(withAttributes: [.font : self.formulaLabel.font.withSize(font_size)])
while(stringSize.width>formulaLabelWidth){
font_size = font_size - 1
stringSize = NSString(string: formula).size(withAttributes: [.font : self.formulaLabel.font.withSize(font_size)])
}
formulaLabel.font = formulaLabel.font.withSize(font_size)
What this code does is draws the string as if it had a certain font size giving you the boundaries in width and height. In my case I only care about the width thanks to a trick with how you set up the UILabel in interface builder.
For this strategy to work this MUST be set on your UILabel since it does all the hard work of finding the perfect size in which the text will fit.
This DOES work with Multiple Lines as well as I am using that for many of my formulas. The lines are separated with \n characters and is automatically accounted for since it just adds to the height of how it would draw with the .size(withAttributes) function.
They used to have a sizeForFont but it was deprecated so I looked into that function as a possible solution and indeed it does work with some clever thinking.
As for detecting a tap use the following modified UITapGestureRecognizer that I created.
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
let mutableAttribString = NSMutableAttributedString(attributedString: label.attributedText!)
mutableAttribString.addAttributes([NSAttributedString.Key.font: label.font!], range: NSRange(location: 0, length: label.attributedText!.length))
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 6
paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.alignment = .center
mutableAttribString.addAttributes([.paragraphStyle: paragraphStyle], range: NSMakeRange(0, mutableAttribString.string.count))
let textStorage = NSTextStorage(attributedString: mutableAttribString)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
let labelSize = label.bounds.size
textContainer.size = labelSize
// Find the tapped character location and compare it to the specified range
let locationOfTouchInLabel = self.location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
//let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
//(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
//let locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
// locationOfTouchInLabel.y - textContainerOffset.y);
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
print("IndexOfCharacter=",indexOfCharacter)
print("TargetRange=",targetRange)
return NSLocationInRange(indexOfCharacter, targetRange)
}
}
Feel free to remove the comments, but I left them there so you could see that it is selecting the correct index of the strings.
Also note that I had a paragraph setting in there with line spacing of 6 and alignment center so feel free to change those to your use case but do not change the line break mode! That is critical to the system automatically finding the optimal font size when it draws.
Also in this code is reference to the current labels font, this was the new part I added, because without it the text-storage calculations would be way off as pointed out in other answers on stack-overflow.
Put it together with the function for the tap gesture
#IBAction func tapLabel(gesture: UITapGestureRecognizer) {
guard let text = formulaLabel.attributedText?.string else {
return
}
let AB_Range = text.range(of: "(α/β)")
//let AB_Range = NSRange(location: 29, length: 3)
if gesture.didTapAttributedTextInLabel(label: formulaLabel, inRange: NSRange(AB_Range!, in: text)) {
print("Tapped a/b")
} else {
print("Tapped none")
}
}
and of course set the tap gesture to the UILabel in viewDidLoad or where appropriate...
formulaLabel.addGestureRecognizer(UITapGestureRecognizer(target:self, action: #selector(tapLabel(gesture:))))
And you have yourself a working
Multiline Friendly
Autoshrink Friendly
Clickable Friendly
UILabel!

Related

AGSMAPView callout not showing on touch point

I am new to ARCGIS. Any help will be appreciated.
I am showing callout on didtap delegate Like this
func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
isFromSearch = false
MBProgressHUD.showAdded(to: self.view, animated: true)
self.mapView.identifyLayers(atScreenPoint: screenPoint, tolerance: 12, returnPopupsOnly: false, maximumResultsPerLayer: 10) { (identifyLayerResults: [AGSIdentifyLayerResult]?, error: Error?) in
//check for errors and ensure identifyLayerResults is not nil
MBProgressHUD.hide(for: self.view, animated: true)
if let error = error {
print(error)
return
}
guard let identifyLayerResults = identifyLayerResults else { return }
// iterate the identify layer results
guard identifyLayerResults.count > 0 else {return}
guard identifyLayerResults[0].sublayerResults.count > 0 else {return}
guard identifyLayerResults[0].sublayerResults[0].geoElements.count > 0 else {return}
let result = identifyLayerResults[0].sublayerResults[0].geoElements[0].attributes
self.identifyLayerResult = identifyLayerResults[0]
var title: String? = nil
var subtitle: String? = nil
if ((result["SiteCode"] as? String) != nil) && ((result["SiteName"] as? String) != nil){
title = (result["SiteCode"] as? String)
subtitle = (result["SiteName"] as? String)
}
else {
title = (result["company"] as? String)
subtitle = (result["identifier"] as? String)
}
self.mapView.callout.title = title
self.mapView.callout.detail = subtitle
self.mapView.callout.show(at: mapPoint, screenOffset: .zero, rotateOffsetWithMap: false, animated: true)
}
}
Everything is Working fine first time . But User can also search for places using REST API
and then mapview is moves to that point and show callout
https://******/arcgis/rest/services/Google/MobileiOS3/MapServer/find?
It returns Site and I create ViewPoint using Latitude and Longitude and show callout with zoom out and zoom in animation Code is given below
let pointView = AGSViewpoint(latitude: center.latitude, longitude: center.longitude, scale: 12E7)
self.mapView.setViewpoint(pointView, duration: 2) { (value) in
let pointView1 = AGSViewpoint(latitude: center.latitude, longitude: center.longitude, scale: 12E4)
self.mapView.setViewpoint(pointView1, duration: 2) { (true) in
let wgs84 = AGSSpatialReference(wkid: 4236)
let point = AGSPoint(x: center.latitude, y: center.longitude, spatialReference: wgs84)
let marker = AGSPictureMarkerSymbol(image: UIImage(named: "BluePushpin.png")!)
marker.leaderOffsetX = 9
marker.leaderOffsetY = -16
let graphics = AGSGraphic(geometry: point, symbol: marker, attributes: nil)
self.mGraphicOverlay.graphics.add(graphics)
let cgPoint = CGPoint(x: self.mapView.center.x, y: self.mapView.center.y - (self.mapView.callout.frame.height + 33))
print(cgPoint)
self.mapView.callout.show(at: graphics.geometry as! AGSPoint, screenOffset: cgPoint, rotateOffsetWithMap: false, animated: true)
}
}
After that when I tap on map any point Callout always shows to top Left Corner While first time didtap delegate was working fine
When I debug code and print callout frame it always shows zero x and zero y
There are a few things going on here:
Firstly, I think you've got the wrong spatial reference. You're using 4236 but WGS84 is 4326.
Note, you can avoid this type of typo by just referencing AGSSpatialReference.wgs84(), so you could say this:
let point = AGSPoint(x: center.latitude, y: center.longitude, spatialReference: .wgs84())
But look closely at that: you're also using latitude as X and longitude as Y. It's unfortunately confusing, but when you create an x,y point you need to specify longitude,latitude, not latitude,longitude:
let point = AGSPoint(x: center.longitude, y: center.latitude, spatialReference: .wgs84())
It's a common mistake. We have a helper function to simplify working with lat/lon source data:
AGSPointMakeWGS84(center.latitude, center.longitude)
You're also doing a bit more work than you need to (especially in forcing spatial reference conversions) and are introducing a few potential areas where errors could creep in.
So, assuming you actually need to set the map scale twice (I guess you are zooming to the location, and then zooming in a bit to focus attention), you could try something like this, which seems much less prone to errors:
let centerPoint = AGSPointMakeWGS84(center.latitude, center.longitude)
self.mapView.setViewpoint(AGSViewpoint(center: centerPoint, scale: 12E7), duration: 2) { _ in
self.mapView.setViewpoint(AGSViewpoint(center: centerPoint, scale: 12E4), duration: 2) { _ in
let marker = AGSPictureMarkerSymbol(image: UIImage(named: "BluePushpin.png")!)
marker.leaderOffsetX = 9
marker.leaderOffsetY = -16
let graphics = AGSGraphic(geometry: centerPoint, symbol: marker, attributes: nil)
self.mGraphicOverlay.graphics.add(graphics)
let cgPoint = CGPoint(x: self.mapView.center.x, y: self.mapView.center.y - (self.mapView.callout.frame.height + 33))
print(cgPoint)
self.mapView.callout.show(at: centerPoint, screenOffset: cgPoint, rotateOffsetWithMap: false, animated: true)
}
}
I confess I'm not entirely sure what your callout screenOffset calculation is doing, but I've left that as is.
I might also suggest adding the graphic before you animate the view, or after the first animation and before the second one (i.e. you're looking at the right location but have yet to zoom in a bit), but that's up to you.
Also, could I suggest that you post questions like this over at the ArcGIS Runtime SDK for iOS forum? We do monitor this space if you use the right tags, but you'll generally get more eyes on your questions over there.

ARKit – Spatial Audio barely changes the volume over distance

I created a SCNNode and added an Audio to it.
It is a Mono audio. Everything is set up correctly.
It is working as Spatial Audio, that's not the problem.
The problem is that as i get closer or far away it barely changes the volume. I know it changes if i get very very far away, but it's nothing like Apple demonstrated here:
https://youtu.be/d9kb1LfNNU4?t=23
Some other games i see the audio volume really changing from one step distance.
With mine, with one step you can't even tell the volume changed. You need at least 4 steps.
Anyone has any clue why?
Code bellow:
SCNNode *audioNode = [[SCNNode alloc] init];
SCNAudioSource *audioSource = [[SCNAudioSource alloc] initWithFileNamed:audioFileName];
audioSource.loops = YES;
[audioSource load];
audioSource.volume = 0.05; // <-- i used different values. won't change much either
audioSource.positional = YES;
//audioSource.shouldStream = NO; // <-- makes no difference
[audioNode addAudioPlayer:[SCNAudioPlayer audioPlayerWithSource:audioSource]];
[audioNode runAction:[SCNAction playAudioSource:audioSource waitForCompletion:NO] completionHandler:nil];
[massNode addChildNode:audioNode];
Maybe scale of the nodes?
The whole scene is the size of around 4 feet.
When i add an object i usually scale it to 0.005 (otherwise it gets way too big).
But i also tried with one that was already in the right size from .scn file.
It shouldn't affect anything tho, since the result is a coffee table size scene and i can see the objects alright.
Updated.
Here's a working code for controlling sound's decay (works in iOS and macOS):
import AVFoundation
import ARKit
class ViewController: UIViewController, AVAudioMixing {
#IBOutlet var sceneView: SCNView!
// #IBOutlet var sceneView: ARSCNView!
func destination(forMixer mixer: AVAudioNode,
bus: AVAudioNodeBus) -> AVAudioMixingDestination? {
return nil
}
var volume: Float = 0.0
var pan: Float = 0.0
var sourceMode: AVAudio3DMixingSourceMode = .bypass
var pointSourceInHeadMode: AVAudio3DMixingPointSourceInHeadMode = .bypass
var renderingAlgorithm = AVAudio3DMixingRenderingAlgorithm.sphericalHead
var rate: Float = 1.2
var reverbBlend: Float = 40.0
var obstruction: Float = -100.0
var occlusion: Float = -100.0
var position = AVAudio3DPoint(x: 0, y: 0, z: 10)
let audioNode = SCNNode()
override func viewDidLoad() {
super.viewDidLoad()
let myScene = SCNScene()
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 0, 0)
myScene.rootNode.addChildNode(cameraNode)
// let sceneView = view as! SCNView
sceneView.scene = myScene
sceneView.backgroundColor = UIColor.orange
let myPath = Bundle.main.path(forResource: "Mono_Audio", ofType: "mp3")
let myURL = URL(fileURLWithPath: myPath!)
let mySource = SCNAudioSource(url: myURL)!
mySource.loops = true
mySource.isPositional = true // Positional Audio
mySource.shouldStream = false // FALSE for Positional Audio
mySource.volume = volume
mySource.reverbBlend = reverbBlend
mySource.rate = rate
mySource.load()
let player = SCNAudioPlayer(source: mySource)
let sphere: SCNGeometry = SCNSphere(radius: 0.1)
let sphereNode = SCNNode(geometry: sphere)
sphereNode.addChildNode(audioNode)
myScene.rootNode.addChildNode(sphereNode)
audioNode.addAudioPlayer(player)
sceneView.audioEnvironmentNode.distanceAttenuationParameters.maximumDistance = 2
sceneView.audioEnvironmentNode.distanceAttenuationParameters.referenceDistance = 0.1
sceneView.audioEnvironmentNode.renderingAlgorithm = .auto
// sceneView.audioEnvironmentNode.reverbParameters.enable = true
// sceneView.audioEnvironmentNode.reverbParameters.loadFactoryReverbPreset(.plate)
let hither = SCNAction.moveBy(x: 0, y: 0, z: 1, duration: 2)
let thither = SCNAction.moveBy(x: 0, y: 0, z: -1, duration: 2)
let sequence = SCNAction.sequence([hither, thither])
let loop = SCNAction.repeatForever(sequence)
sphereNode.runAction(loop)
}
}
And, yes, you're absolutely right – there are some obligatory settings.
But there are 7 of them:
use AVAudioMixing protocol with its stubs (properties and methods).
use MONO audio file.
use source.isPositional = true.
use source.shouldStream = false.
assign maximumDistance value to distanceAttenuationParameters property.
assign referenceDistance value to distanceAttenuationParameters property.
and location of mySource.load() is very important in your code.
P.S. If the aforementioned tips didn't help you, then use additional instance properties to make your sound even quieter using a graph, obstacles and orientation of implicit listener:
var rolloffFactor: Float { get set } // attenuation's graph, default = 1
var obstruction: Float { get set } // default = 0.0
var occlusion: Float { get set } // default = 0.0
var listenerAngularOrientation: AVAudio3DAngularOrientation { get set } //(0,0,0)
It definitely works if you'll write it in Objective-C.
In this example the distance of audioNode is 1 meter away from a listener.
If none of the above answers seem to work, try the following code:
sceneView.audioEnvironmentNode.reverbParameters.enable = true
And if even these seem to barely work, or if you wanna optimal performance, there is a property called level where you can set the level of how spatial the code can be.
sceneView.audioEnvironmentNode.reverbParameters.level = 40
(the level of the reverbParameters ranges between -40 to 40 parameters)

Swift/Objective c - rotate/spin CAShapeLayer on the spot

Im trying to animate/spin a circle around it self.
did is what i have tried,but it just rotating on the entire screen.. i dont need it to move, i just need to to spin on the spot.
let circleLayer: CAShapeLayer!\outside the method
vlet rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.fromValue = Float(0.0)
rotate.toValue = Float(2.0 * M_PI)
rotate.duration = 1.0
rotate.cumulative = true
rotate.repeatCount = 1
rotate.removedOnCompletion = false
rotate.fillMode = kCAFillModeForward
circleLayer.addAnimation(rotate, forKey: "transform.rotation.z")
any idea why it's moving and to spinning/rotating on the spot?
found the solution, i'll just rotate the view itself with diffrent animation :
UIView.animateWithDuration(0.95, delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: { () -> Void in
self.circleView.transform = CGAffineTransformMakeRotation(self.sumer)
}, completion: nil)
enjoy

SceneKit – Drawing a line between two points

I have two points (let's call them pointA and pointB) of type SCNVector3. I want to draw a line between them. Seems like it should be easy, but can't find a way to do it.
I see two options, both have issues:
Use a SCNCylinder with a small radius, with length |pointA-pointB| and then position it/rotate it.
Use a custom SCNGeometry but not sure how; would have to define two triangles to form a very thin rectangle perhaps?
It seems like there should be an easier way of doing this, but I can't seem to find one.
Edit: Using the triangle method gives me this for drawing a line between (0,0,0) and (10,10,10):
CGFloat delta = 0.1;
SCNVector3 positions[] = { SCNVector3Make(0,0,0),
SCNVector3Make(10, 10, 10),
SCNVector3Make(0+delta, 0+delta, 0+delta),
SCNVector3Make(10+delta, 10+delta, 10+delta)};
int indicies[] = {
0,2,1,
1,2,3
};
SCNGeometrySource *vertexSource = [SCNGeometrySource geometrySourceWithVertices:positions count:4];
NSData *indexData = [NSData dataWithBytes:indicies length:sizeof(indicies)];
SCNGeometryElement *element = [SCNGeometryElement geometryElementWithData:indexData primitiveType:SCNGeometryPrimitiveTypeTriangles primitiveCount:2 bytesPerIndex:sizeof(int)];
SCNGeometry *line = [SCNGeometry geometryWithSources:#[vertexSource] elements:#[element]];
SCNNode *lineNode = [SCNNode nodeWithGeometry:line];
[root addChildNode:lineNode];
But there are problems: due to the normals, you can only see this line from one side! It's invisible from the other side. Also, if "delta" is too small you can't see the line at all. As it is, it's technically a rectangle, rather than the line I was going for, which might result in small graphical glitches if I want to draw multiple joined up lines.
Here's a simple extension in Swift:
extension SCNGeometry {
class func lineFrom(vector vector1: SCNVector3, toVector vector2: SCNVector3) -> SCNGeometry {
let indices: [Int32] = [0, 1]
let source = SCNGeometrySource(vertices: [vector1, vector2])
let element = SCNGeometryElement(indices: indices, primitiveType: .Line)
return SCNGeometry(sources: [source], elements: [element])
}
}
There are lots of ways to do this.
As noted, your custom geometry approach has some disadvantages. You should be able to correct the problem of it being invisible from one side by giving its material the doubleSided property. You still may have issues with it being two-dimensional, though.
You could also modify your custom geometry to include more triangles, so you get a tube shape with three or more sides instead of a flat rectangle. Or just have two points in your geometry source, and use the SCNGeometryPrimitiveTypeLine geometry element type to have Scene Kit draw a line segment between them. (Though you won't get as much flexibility in rendering styles with line drawing as with shaded polygons.)
You can also use the SCNCylinder approach you mentioned (or any of the other built-in primitive shapes). Remember that geometries are defined in their own local (aka Model) coordinate space, which Scene Kit interprets relative to the coordinate space defined by a node. In other words, you can define a cylinder (or box or capsule or plane or whatever) that's 1.0 units wide in all dimensions, then use the rotation/scale/position or transform of the SCNNode containing that geometry to make it long, thin, and stretching between the two points you want. (Also note that since your line is going to be pretty thin, you can reduce the segmentCounts of whichever built-in geometry you're using, because that much detail won't be visible.)
Yet another option is the SCNShape class that lets you create an extruded 3D object from a 2D Bézier path. Working out the right transform to get a plane connecting two arbitrary points sounds like some fun math, but once you do it you could easily connect your points with any shape of line you choose.
New code for a line from (0, 0, 0) to (10, 10, 10) below.
I'm not sure if it could be improved further.
SCNVector3 positions[] = {
SCNVector3Make(0.0, 0.0, 0.0),
SCNVector3Make(10.0, 10.0, 10.0)
};
int indices[] = {0, 1};
SCNGeometrySource *vertexSource = [SCNGeometrySource geometrySourceWithVertices:positions
count:2];
NSData *indexData = [NSData dataWithBytes:indices
length:sizeof(indices)];
SCNGeometryElement *element = [SCNGeometryElement geometryElementWithData:indexData
primitiveType:SCNGeometryPrimitiveTypeLine
primitiveCount:1
bytesPerIndex:sizeof(int)];
SCNGeometry *line = [SCNGeometry geometryWithSources:#[vertexSource]
elements:#[element]];
SCNNode *lineNode = [SCNNode nodeWithGeometry:line];
[root addChildNode:lineNode];
Here's one solution
class func lineBetweenNodeA(nodeA: SCNNode, nodeB: SCNNode) -> SCNNode {
let positions: [Float32] = [nodeA.position.x, nodeA.position.y, nodeA.position.z, nodeB.position.x, nodeB.position.y, nodeB.position.z]
let positionData = NSData(bytes: positions, length: MemoryLayout<Float32>.size*positions.count)
let indices: [Int32] = [0, 1]
let indexData = NSData(bytes: indices, length: MemoryLayout<Int32>.size * indices.count)
let source = SCNGeometrySource(data: positionData as Data, semantic: SCNGeometrySource.Semantic.vertex, vectorCount: indices.count, usesFloatComponents: true, componentsPerVector: 3, bytesPerComponent: MemoryLayout<Float32>.size, dataOffset: 0, dataStride: MemoryLayout<Float32>.size * 3)
let element = SCNGeometryElement(data: indexData as Data, primitiveType: SCNGeometryPrimitiveType.line, primitiveCount: indices.count, bytesPerIndex: MemoryLayout<Int32>.size)
let line = SCNGeometry(sources: [source], elements: [element])
return SCNNode(geometry: line)
}
if you would like to update the line width or anything related to modifying properties of the drawn line, you'll want to use one of the openGL calls in SceneKit's rendering callback:
func renderer(aRenderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: NSTimeInterval) {
//Makes the lines thicker
glLineWidth(20)
}
Here is a swift5 version:
func lineBetweenNodes(positionA: SCNVector3, positionB: SCNVector3, inScene: SCNScene) -> SCNNode {
let vector = SCNVector3(positionA.x - positionB.x, positionA.y - positionB.y, positionA.z - positionB.z)
let distance = sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z)
let midPosition = SCNVector3 (x:(positionA.x + positionB.x) / 2, y:(positionA.y + positionB.y) / 2, z:(positionA.z + positionB.z) / 2)
let lineGeometry = SCNCylinder()
lineGeometry.radius = 0.05
lineGeometry.height = distance
lineGeometry.radialSegmentCount = 5
lineGeometry.firstMaterial!.diffuse.contents = GREEN
let lineNode = SCNNode(geometry: lineGeometry)
lineNode.position = midPosition
lineNode.look (at: positionB, up: inScene.rootNode.worldUp, localFront: lineNode.worldUp)
return lineNode
}
So inside your ViewController.cs define your vector points and call a Draw function, then on the last line there - it's just rotating it to look at point b.
var a = someVector3;
var b = someOtherVector3;
nfloat cLength = (nfloat)Vector3Helper.DistanceBetweenPoints(a, b);
var cyclinderLine = CreateGeometry.DrawCylinderBetweenPoints(a, b, cLength, 0.05f, 10);
ARView.Scene.RootNode.Add(cyclinderLine);
cyclinderLine.Look(b, ARView.Scene.RootNode.WorldUp, cyclinderLine.WorldUp);
Create a static CreateGeomery class and put this static method in there
public static SCNNode DrawCylinderBetweenPoints(SCNVector3 a,SCNVector3 b, nfloat length, nfloat radius, int radialSegments){
SCNNode cylinderNode;
SCNCylinder cylinder = new SCNCylinder();
cylinder.Radius = radius;
cylinder.Height = length;
cylinder.RadialSegmentCount = radialSegments;
cylinderNode = SCNNode.FromGeometry(cylinder);
cylinderNode.Position = Vector3Helper.GetMidpoint(a,b);
return cylinderNode;
}
you may also want these utility methods in a static helper class
public static double DistanceBetweenPoints(SCNVector3 a, SCNVector3 b)
{
SCNVector3 vector = new SCNVector3(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
return Math.Sqrt(vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z);
}
public static SCNVector3 GetMidpoint(SCNVector3 a, SCNVector3 b){
float x = (a.X + b.X) / 2;
float y = (a.Y + b.Y) / 2;
float z = (a.Z + b.Z) / 2;
return new SCNVector3(x, y, z);
}
For all my Xamarin c# homies out there.
Here's a solution using triangles that works independent of the direction of the line.
It's constructed using the cross product to get points perpendicular to the line. So you'll need a small SCNVector3 extension, but it'll probably come in handy in other cases, too.
private func makeRect(startPoint: SCNVector3, endPoint: SCNVector3, width: Float ) -> SCNGeometry {
let dir = (endPoint - startPoint).normalized()
let perp = dir.cross(SCNNode.localUp) * width / 2
let firstPoint = startPoint + perp
let secondPoint = startPoint - perp
let thirdPoint = endPoint + perp
let fourthPoint = endPoint - perp
let points = [firstPoint, secondPoint, thirdPoint, fourthPoint]
let indices: [UInt16] = [
1,0,2,
1,2,3
]
let geoSource = SCNGeometrySource(vertices: points)
let geoElement = SCNGeometryElement(indices: indices, primitiveType: .triangles)
let geo = SCNGeometry(sources: [geoSource], elements: [geoElement])
geo.firstMaterial?.diffuse.contents = UIColor.blue.cgColor
return geo
}
SCNVector3 extension:
import Foundation
import SceneKit
extension SCNVector3
{
/**
* Returns the length (magnitude) of the vector described by the SCNVector3
*/
func length() -> Float {
return sqrtf(x*x + y*y + z*z)
}
/**
* Normalizes the vector described by the SCNVector3 to length 1.0 and returns
* the result as a new SCNVector3.
*/
func normalized() -> SCNVector3 {
return self / length()
}
/**
* Calculates the cross product between two SCNVector3.
*/
func cross(_ vector: SCNVector3) -> SCNVector3 {
return SCNVector3(y * vector.z - z * vector.y, z * vector.x - x * vector.z, x * vector.y - y * vector.x)
}
}
Swift version
To generate a line in a form of cylinder with a certain position and an orientation, let's implement the SCNGeometry extension with a cylinderLine() class method inside. The toughest part here is a trigonometry (for defining cylinder's direction). Here it is:
import SceneKit
extension SCNGeometry {
class func cylinderLine(from: SCNVector3, to: SCNVector3,
segments: Int = 5) -> SCNNode {
let x1 = from.x; let x2 = to.x
let y1 = from.y; let y2 = to.y
let z1 = from.z; let z2 = to.z
let subExpr01 = Float((x2-x1) * (x2-x1))
let subExpr02 = Float((y2-y1) * (y2-y1))
let subExpr03 = Float((z2-z1) * (z2-z1))
let distance = CGFloat(sqrtf(subExpr01 + subExpr02 + subExpr03))
let cylinder = SCNCylinder(radius: 0.005, height: CGFloat(distance))
cylinder.radialSegmentCount = segments
cylinder.firstMaterial?.diffuse.contents = NSColor.systemYellow
let lineNode = SCNNode(geometry: cylinder)
lineNode.position = SCNVector3((x1+x2)/2, (y1+y2)/2, (z1+z2)/2)
lineNode.eulerAngles = SCNVector3(x: CGFloat.pi / 2,
y: acos((to.z-from.z)/CGFloat(distance)),
z: atan2((to.y-from.y), (to.x-from.x)))
return lineNode
}
}
The rest is easy.
class ViewController: NSViewController {
#IBOutlet var sceneView: SCNView!
let scene = SCNScene()
var startingPoint: SCNVector3!
var endingPoint: SCNVector3!
override func viewDidLoad() {
super.viewDidLoad()
sceneView.scene = scene
sceneView.backgroundColor = NSColor.black
sceneView.allowsCameraControl = true
self.startingPoint = SCNVector3Zero
self.endingPoint = SCNVector3(1,1,1)
self.lineInBetween()
}
func lineInBetween() {
self.addSphereDot(position: startingPoint)
self.addSphereDot(position: endingPoint)
self.addLine(start: startingPoint, end: endingPoint)
}
func addSphereDot(position: SCNVector3) {
let sphere = SCNSphere(radius: 0.03)
sphere.firstMaterial?.diffuse.contents = NSColor.red
let node = SCNNode(geometry: sphere)
node.position = position
scene.rootNode.addChildNode(node)
}
func addLine(start: SCNVector3, end: SCNVector3) {
let lineNode = SCNGeometry.cylinderLine(from: start, to: end)
scene.rootNode.addChildNode(lineNode)
}
}

How can I expand a MKMapRect by a fixed percentage?

I want to get a result MKMapRect that's 10-20% larger in all directions than the current visibleMapRect. If this were a CGRect I'd use CGRectInset with negative x and y values, providing me with an inverse inset (i.e. a larger rect). Unfortunately, MKMapInset doesn't support negative inset values so it's not quite that easy.
This might be easier if the the values for the map rect were recognizable units but the origin x and y values are on the order of 4.29445e+07, and the width/height is 2500-3000.
I'm about 10 seconds from writing a category to do this manually but I wanted to make sure I wasn't missing something first. Is there an easier way to expand MKMapRect?
In iOS7, rectForMapRect: and mapRectForRect: has been deprecated and now are part of the MKOverlayRenderer class. I'd rather recommend to use the MapView mapRectThatFits: edgePadding: methods. Here is a sample code :
MKMapRect visibleRect = self.mapView.visibleMapRect;
UIEdgeInsets insets = UIEdgeInsetsMake(50, 50, 50, 50);
MKMapRect biggerRect = [self.mapView mapRectThatFits:visibleRect edgePadding:insets];
latest Swift for 2017...
func updateMap() {
mkMap.removeAnnotations(mkMap.annotations)
mkMap.addAnnotations(yourAnnotationsArray)
var union = MKMapRectNull
for p in yourAnnotationsArray {
// make a small, say, 50meter square for each
let pReg = MKCoordinateRegionMakeWithDistance( pa.coordinate, 50, 50 )
// convert it to a MKMapRect
let r = mkMapRect(forMKCoordinateRegion: pReg)
// union all of those
union = MKMapRectUnion(union, r)
// probably want to turn on the "sign" for each
mkMap.selectAnnotation(pa, animated: false)
}
// expand the union, using the new #edgePadding call. T,L,B,R
let f = mkMap.mapRectThatFits(union, edgePadding: UIEdgeInsetsMake(70, 0, 10, 35))
// NOTE you want the TOP padding much bigger than the BOTTOM padding
// because the pins/signs are actually very tall
mkMap.setVisibleMapRect(f, animated: false)
}
What about converting the visibleMapRect to a CGRect with rectForMapRect:, getting a new CGRect with CGRectInset and then converting it back to a MKMapRect with mapRectForRect:?
Simple and clean solution for Xcode 10+, Swift 4.2
Just set the edge insets for the maps' margins like this:
self.mapView.layoutMargins = UIEdgeInsets(top: 8, right: 8, bottom: 8, left: 8)
self.mapView.showAnnotations(map.annotations, animated: true)
Please let us know if it works for you.
Refined and corrected user2285781's answer for Swift 4:
// reference: https://stackoverflow.com/a/15683034/347339
func MKMapRectForCoordinateRegion(region:MKCoordinateRegion) -> MKMapRect {
let topLeft = CLLocationCoordinate2D(latitude: region.center.latitude + (region.span.latitudeDelta/2), longitude: region.center.longitude - (region.span.longitudeDelta/2))
let bottomRight = CLLocationCoordinate2D(latitude: region.center.latitude - (region.span.latitudeDelta/2), longitude: region.center.longitude + (region.span.longitudeDelta/2))
let a = MKMapPointForCoordinate(topLeft)
let b = MKMapPointForCoordinate(bottomRight)
return MKMapRect(origin: MKMapPoint(x:min(a.x,b.x), y:min(a.y,b.y)), size: MKMapSize(width: abs(a.x-b.x), height: abs(a.y-b.y)))
}
// reference: https://stackoverflow.com/a/19307286/347339
// assuming coordinates that create a polyline as well as a destination annotation
func updateMap(coordinates: [CLLocationCoordinate2D], annotation: MKAnnotation) {
var union = MKMapRectNull
var coordinateArray = coordinates
coordinateArray.append(annotation.coordinate)
for coordinate in coordinateArray {
// make a small, say, 50meter square for each
let pReg = MKCoordinateRegionMakeWithDistance( coordinate, 50, 50 )
// convert it to a MKMapRect
let r = MKMapRectForCoordinateRegion(region: pReg)
// union all of those
union = MKMapRectUnion(union, r)
}
// expand the union, using the new #edgePadding call. T,L,B,R
let f = mapView.mapRectThatFits(union, edgePadding: UIEdgeInsetsMake(70, 35, 10, 35))
// NOTE you want the TOP padding much bigger than the BOTTOM padding
// because the pins/signs are actually very tall
mapView.setVisibleMapRect(f, animated: false)
}