in my code right now you can add a cylinder. What I would like to do is display a label that says height and the corresponding slider value. If the cylinder is too wide I would eventually like to to be displayed horizontally but when it gets too tall or the aspect ratio is even display it vertically like I have with the corresponding gif. The current code does not print a label on it. Please print a label on it similar to the one shown in the gif.
import UIKit
import SceneKit;import CoreData
class one: UIViewController {
var cylinderButton = UIButton()
var onOfSwitch = UISwitch()
var scnView = SCNView()
var cameraNode = SCNNode()
var selectedNode: SCNNode?
var sL = UISlider(), sW = UISlider(), sH = UISlider()
var sLLabel = UILabel(), sWLabel = UILabel(), sHLabel = UILabel()
var pinchGesture: UIPinchGestureRecognizer?
var DragLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
[ onOfSwitch, scnView,cylinderButton,sL,sW,sH,sLLabel,sWLabel,sHLabel,DragLabel].forEach {
view.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
$0.layer.borderWidth = 1
$0.backgroundColor = UIColor(
red: .random(in: 0.5...0.7),
green: .random(in: 0.0...1),
blue: .random(in: 0.3...0.5),
alpha: 1
)
if let button = $0 as? UIButton {
button.setTitleColor(.black, for: .normal)
}
}
[sL, sW, sH].forEach {
$0.minimumValue = 0.2
$0.maximumValue = 2.0
$0.value = 1.0
$0.addTarget(self, action: #selector(updateBoxSize), for: .valueChanged)
}
NSLayoutConstraint.activate([
scnView.topAnchor.constraint(equalTo: view.topAnchor),
scnView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.55),
scnView.widthAnchor.constraint(equalTo: view.widthAnchor),
scnView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
sL.topAnchor.constraint(equalTo: scnView.bottomAnchor),
sL.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.04),
sL.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 2/3),
sL.leadingAnchor.constraint(equalTo: view.leadingAnchor),
sLLabel.topAnchor.constraint(equalTo: scnView.bottomAnchor),
sLLabel.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.04),
sLLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1/5),
sLLabel.leadingAnchor.constraint(equalTo: sL.trailingAnchor),
sW.topAnchor.constraint(equalTo: sL.bottomAnchor),
sW.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.04),
sW.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 2/3),
sW.leadingAnchor.constraint(equalTo: view.leadingAnchor),
sWLabel.topAnchor.constraint(equalTo: sL.bottomAnchor),
sWLabel.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.04),
sWLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1/5),
sWLabel.leadingAnchor.constraint(equalTo: sW.trailingAnchor),
sH.topAnchor.constraint(equalTo: sW.bottomAnchor),
sH.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.04),
sH.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 2/3),
sH.leadingAnchor.constraint(equalTo: view.leadingAnchor),
sHLabel.topAnchor.constraint(equalTo: sW.bottomAnchor),
sHLabel.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.04),
sHLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1/5),
sHLabel.leadingAnchor.constraint(equalTo: sH.trailingAnchor),
cylinderButton.topAnchor.constraint(equalTo: sH.bottomAnchor),
cylinderButton.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.10),
cylinderButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1/3),
cylinderButton.leadingAnchor.constraint(equalTo: view.leadingAnchor),
DragLabel.topAnchor.constraint(equalTo: sH.bottomAnchor),
DragLabel.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.05),
DragLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1/3),
DragLabel.leadingAnchor.constraint(equalTo: cylinderButton.trailingAnchor),
onOfSwitch.topAnchor.constraint(equalTo: DragLabel.bottomAnchor),
onOfSwitch.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.05),
onOfSwitch.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1/3),
onOfSwitch.leadingAnchor.constraint(equalTo: cylinderButton.trailingAnchor),
])
DragLabel.text = "Drag to Rotate"
let scene = SCNScene()
scnView.scene = scene
scnView.backgroundColor = .gray
DragLabel.textAlignment = .center
setupCamera()
setupLight()
cylinderButton.setTitle("Circle", for: .normal)
cylinderButton.addTarget(self, action: #selector(addCylinder), for: .touchUpInside)
onOfSwitch.addTarget(self, action: #selector(toggleCameraControl), for: .valueChanged)
onOfSwitch.isOn = true
scnView.allowsCameraControl = true
sWLabel.text = "Width"
sHLabel.text = "Height"
sHLabel.textAlignment = .center
sWLabel.textAlignment = .center
sLLabel.textAlignment = .center
sL.value = 0.5
sH.value = 0.5
sW.value = 0.5
sL.minimumValue = 1
sL.maximumValue = 25
sH.minimumValue = 1
sH.maximumValue = 25
sW.minimumValue = 1
sW.maximumValue = 25
onOfSwitch.isOn = false
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)
//3
let rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(handleRotation(_:)))
scnView.addGestureRecognizer(rotationGesture)
}
@objc func copyM() {
guard let originalNode = selectedNode else { return }
let clonedNode = originalNode.clone()
clonedNode.position = SCNVector3(
originalNode.position.x + 1.0,
originalNode.position.y + 1.0,
originalNode.position.z
)
scnView.scene?.rootNode.addChildNode(clonedNode)
selectedNode = clonedNode
}
@objc func depthTextChanged(_ sender: UITextField) {
guard let currentView = selectedNode else { return }
guard let text = sender.text, let newDepth = Float(text) else { return }
if let box = currentView.geometry as? SCNBox {
box.length = CGFloat(newDepth)
applyTextAsTexture55(to: box, width: sW.value, height: sH.value, depth: newDepth)
} else if let cylinder = currentView.geometry as? SCNCylinder {
// Treat "depth" as diameter here
cylinder.radius = CGFloat(newDepth / 2)
applyTextAsTextureToCylinder(cylinder, width: newDepth, height: sH.value)
}
}
@objc func libraryBUttonFunction() {
print("Library Button Selected")
// Replace YourViewController with the name of the view controller you want to present
let destinationVC = Second()
// Customize the modal presentation style (optional)
destinationVC.modalPresentationStyle = .fullScreen // or .overFullScreen, .pageSheet, etc.
// Present the view controller modally
self.present(destinationVC, animated: true, completion: nil)
}
@objc func heightTextChanged(_ sender: UITextField) {
guard let currentView = selectedNode else { return }
guard let text = sender.text, let newHeight = Float(text) else { return }
if let box = currentView.geometry as? SCNBox {
box.height = CGFloat(newHeight)
applyTextAsTexture55(to: box, width: sW.value, height: newHeight, depth: sL.value)
} else if let cylinder = currentView.geometry as? SCNCylinder {
cylinder.height = CGFloat(newHeight)
applyTextAsTextureToCylinder(cylinder, width: sW.value, height: newHeight)
}
}
@objc func widthTextChanged(_ sender: UITextField) {
guard let currentView = selectedNode else { return }
guard let text = sender.text, let newWidth = Float(text) else { return }
if let box = currentView.geometry as? SCNBox {
box.width = CGFloat(newWidth)
applyTextAsTexture55(to: box, width: newWidth, height: sH.value, depth: sL.value)
} else if let cylinder = currentView.geometry as? SCNCylinder {
cylinder.radius = CGFloat(newWidth / 2)
applyTextAsTextureToCylinder(cylinder, width: newWidth, height: sH.value)
}
}
@objc func handleTap(_ gesture: UITapGestureRecognizer) {
let location = gesture.location(in: scnView)
let hitResults = scnView.hitTest(location, options: nil)
if let result = hitResults.first {
selectedNode = result.node
print("Selected a node: \(selectedNode!)")
} else {
selectedNode = nil
print("No node selected.")
}
}
func setupCamera() {
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 0, 10)
scnView.scene?.rootNode.addChildNode(cameraNode)
// Set the camera as the active point of view
scnView.pointOfView = cameraNode
}
func setupLight() {
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light?.type = .omni
lightNode.position = SCNVector3(0, 10, 10)
scnView.scene?.rootNode.addChildNode(lightNode)
let ambientLight = SCNNode()
ambientLight.light = SCNLight()
ambientLight.light?.type = .ambient
ambientLight.light?.color = UIColor.darkGray
scnView.scene?.rootNode.addChildNode(ambientLight)
}
@objc func toggleCameraControl() {
if onOfSwitch.isOn {
scnView.allowsCameraControl = true
// Remove pan and pinch gestures
if let pan = panGesture {
scnView.removeGestureRecognizer(pan)
panGesture = nil
}
if let pinch = pinchGesture {
scnView.removeGestureRecognizer(pinch)
pinchGesture = nil
}
} else {
scnView.allowsCameraControl = false
if panGesture == nil {
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
scnView.addGestureRecognizer(pan)
panGesture = pan
}
if pinchGesture == nil {
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
scnView.addGestureRecognizer(pinch)
pinchGesture = pinch
}
}
}
@objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
guard let camera = scnView.pointOfView else { return }
if gesture.state == .changed {
let scale = Float(gesture.scale)
var position = camera.position
position.z /= scale // Zoom in or out
position.z = max(min(position.z, 100), 1) // Clamp zoom
camera.position = position
gesture.scale = 1
}
}
@objc func addCylinder() {
let radius = CGFloat(sW.value / 2)
let height = CGFloat(sH.value * 3)
let cylinder = SCNCylinder(radius: radius, height: height)
applyTextAsTextureToCylinder(cylinder, width: sW.value, height: sH.value)
let cylinderNode = SCNNode(geometry: cylinder)
cylinderNode.position = SCNVector3(0, 0, 0)
scnView.scene?.rootNode.addChildNode(cylinderNode)
selectedNode = cylinderNode
}
var panGesture: UIPanGestureRecognizer?
@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
let location = gesture.location(in: scnView)
switch gesture.state {
case .began:
let hitResults = scnView.hitTest(location, options: nil)
if let hitNode = hitResults.first?.node,
let geometry = hitNode.geometry,
geometry is SCNBox || geometry is SCNCylinder {
selectedNode = hitNode
}
case .changed:
guard let selectedNode = selectedNode else { return }
let translation = gesture.translation(in: scnView)
let deltaX = Float(translation.x) * 0.01
let deltaY = Float(-translation.y) * 0.01
selectedNode.position.x += deltaX
selectedNode.position.y += deltaY
gesture.setTranslation(.zero, in: scnView)
default:
break
}
}
@objc func handleRotation(_ gesture: UIRotationGestureRecognizer) {
guard let node = selectedNode else { return }
if gesture.state == .changed {
node.eulerAngles.y += Float(gesture.rotation)
gesture.rotation = 0
}
}
func createCylinder(radius: CGFloat, height: CGFloat) -> SCNNode {
let cylinder = SCNCylinder(radius: radius, height: height)
cylinder.firstMaterial?.diffuse.contents = UIColor.lightGray
cylinder.firstMaterial?.isDoubleSided = true
cylinder.radialSegmentCount = 20 // Boxier look
let node = SCNNode(geometry: cylinder)
node.position = SCNVector3(0, 0, 0)
return node
}
func applyTextAsTextureToCylinder(_ cylinder: SCNCylinder, width: Float, height: Float) {
let sideImage = drawCylinderHeightLabel(height: height)
let topImage = drawCylinderTopWidthLabel(width: width)
let sideMaterial = SCNMaterial()
sideMaterial.diffuse.contents = sideImage
sideMaterial.isDoubleSided = true
let topMaterial = SCNMaterial()
topMaterial.diffuse.contents = topImage
topMaterial.isDoubleSided = true
let blankMaterial = SCNMaterial()
blankMaterial.diffuse.contents = UIColor.lightGray
cylinder.materials = [
sideMaterial, // side
topMaterial, // top
blankMaterial // bottom (optional)
]
}
func drawCylinderHeightLabel(height: Float) -> UIImage {
let size = CGSize(width: 256, height: 512)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }
UIColor.white.setFill()
context.fill(CGRect(origin: .zero, size: size))
let width = sW.value
let aspectRatio = width / height
let heightText = "height: \(sH.value)"
// New: Reasonable bounds for font size
let baseFontSize: CGFloat = 16
let minFontSize: CGFloat = 8
let maxFontSize: CGFloat = 32
var fontSize: CGFloat
if aspectRatio > 3 {
print("a")
// fontSize = max(minFontSize, baseFontSize * CGFloat((height / width)) * 8)
fontSize = max(minFontSize, baseFontSize * CGFloat(sW.value))
} else {
print("b")
fontSize = max(minFontSize, baseFontSize * CGFloat((height / width)) * 2.5)
}
fontSize = min(fontSize, maxFontSize)
let font = UIFont.systemFont(ofSize: fontSize)
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: UIColor.black
]
if aspectRatio > 3 {
print("1")
// Wide, flat cylinder – draw horizontal centered
let textSize = (heightText as NSString).size(withAttributes: attributes)
let rect = CGRect(
x: (size.width - textSize.width) / 2 ,
y: (size.height - textSize.height) / 2,
width: textSize.width,
height: textSize.height * 5
)
(heightText as NSString).draw(in: rect, withAttributes: attributes)
} else {
print("2")
// Taller cylinder – draw vertically rotated
context.saveGState()
context.translateBy(x: size.width / 2, y: size.height / 2)
context.rotate(by: -.pi / 2)
let rect = CGRect(x: -128, y: -20, width: 256, height: 40)
(heightText as NSString).draw(in: rect, withAttributes: attributes)
context.restoreGState()
}
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image ?? UIImage()
}
func drawCylinderTopWidthLabel(width: Float) -> UIImage {
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }
// Fill background
UIColor.white.setFill()
context.fill(CGRect(origin: .zero, size: size))
// Text attributes
let font = UIFont.systemFont(ofSize: 28, weight: .bold)
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: UIColor.black
]
let widthText = "width: \(Int(width * 1))"
let textSize = (widthText as NSString).size(withAttributes: attributes)
// Move to center, flip horizontally
context.translateBy(x: size.width / 2, y: size.height / 2)
context.scaleBy(x: -1.0, y: 1.0) // 🔁 Horizontal mirror
context.rotate(by: (3 * .pi) / 2) // 180 degrees
// Draw centered mirrored text
let rect = CGRect(
x: -textSize.width / 2,
y: -textSize.height / 2,
width: textSize.width,
height: textSize.height
)
(widthText as NSString).draw(in: rect, withAttributes: attributes)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image ?? UIImage()
}
func drawTopWithDepth(depth: Float) -> UIImage {
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }
UIColor.white.setFill()
context.fill(CGRect(origin: .zero, size: size))
let font = UIFont.systemFont(ofSize: 28)
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: UIColor.black
]
let depthText = "depth: \(Int(depth * 1))"
// Rotate and place on bottom right corner of top face
context.saveGState()
context.translateBy(x: size.width - 40, y: size.height - 20)
context.rotate(by: -.pi / 2)
let rect = CGRect(x: 0, y: 0, width: 200, height: 40)
(depthText as NSString).draw(in: rect, withAttributes: attributes)
context.restoreGState()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image ?? UIImage()
}
func applyTextAsTexture55(to box: SCNBox, width: Float, height: Float, depth: Float) {
let blankMaterial = SCNMaterial()
blankMaterial.diffuse.contents = UIColor.lightGray
blankMaterial.isDoubleSided = true
// Front face: width + height
let frontImage = drawFrontWithWidthAndHeight(width: width, height: height)
let frontMaterial = SCNMaterial()
frontMaterial.diffuse.contents = frontImage
frontMaterial.isDoubleSided = true
// Top face: depth
let topImage = drawTopWithDepth(depth: depth)
let topMaterial = SCNMaterial()
topMaterial.diffuse.contents = topImage
topMaterial.isDoubleSided = true
box.materials = [
frontMaterial, // front
blankMaterial, // right
blankMaterial, // back
blankMaterial, // left
topMaterial, // top
blankMaterial // bottom
]
}
func drawFrontWithWidthAndHeight(width: Float, height: Float) -> UIImage {
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }
// Background
UIColor.white.setFill()
context.fill(CGRect(origin: .zero, size: size))
// Font and style
let font = UIFont.systemFont(ofSize: 28)
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: UIColor.black
]
// 1. Draw width at top-left
let widthText = "width: \(Int(width * 10))"
let widthRect = CGRect(x: 10, y: 10, width: 200, height: 40)
(widthText as NSString).draw(in: widthRect, withAttributes: attributes)
// 2. Draw rotated height on right edge
let heightText = "height: \(Int(height * 10))"
context.saveGState()
// Translate to bottom-right corner, then rotate counter-clockwise
context.translateBy(x: size.width - 40, y: size.height - 20)
context.rotate(by: -.pi / 2)
let heightRect = CGRect(x: 0, y: 0, width: 200, height: 40)
(heightText as NSString).draw(in: heightRect, withAttributes: attributes)
context.restoreGState()
// Debug border (optional)
// UIColor.black.setStroke()
// context.stroke(CGRect(origin: .zero, size: size), width: 2)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image ?? UIImage()
}
@objc func updateBoxSize() {
guard let selectedNode = selectedNode else { return }
let width = sW.value
let height = sH.value
let depth = sL.value
if let box = selectedNode.geometry as? SCNBox {
box.width = CGFloat(width)
box.height = CGFloat(height)
box.length = CGFloat(depth)
applyTextAsTexture55(to: box, width: width, height: height, depth: depth)
} else if let cylinder = selectedNode.geometry as? SCNCylinder {
cylinder.radius = CGFloat(width / 2) // radius = width / 2 for diameter control
cylinder.height = CGFloat(height)
applyTextAsTextureToCylinder(cylinder, width: width, height: height)
}
}
}
When the aspectRatio
is very high (wide, flat cylinder), your code multiplies the font size by a large factor (180), and then draws the text inside a rect where the height is textSize.height * 20, leading to stretched, unreadable text.
Instead of scaling font size so dramatically when aspectRatio > 3
, you should
Keep a reasonable font size, maybe scale it mildly based on aspect ratio
Adjust the rect
height to match the text height (no huge multiplier)
For the horizontal case, align the text nicely in a centered rect without distorting its height
Updated/ Revised code is as below.
func drawCylinderHeightLabel(height: Float) -> UIImage {
let size = CGSize(width: 256, height: 512)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }
UIColor.white.setFill()
context.fill(CGRect(origin: .zero, size: size))
let width = sW.value
let aspectRatio = width / height
let heightText = "height: \(Int(height))"
// Reasonable font sizes
let baseFontSize: CGFloat = 28
let minFontSize: CGFloat = 14
let maxFontSize: CGFloat = 36
// Adjust font size based on aspect ratio, but within safe bounds
var fontSize: CGFloat
if aspectRatio > 3 {
fontSize = baseFontSize * (3 / aspectRatio) // decrease size as aspect ratio increases
} else {
fontSize = baseFontSize * (height / width) * 2.5
}
fontSize = min(max(fontSize, minFontSize), maxFontSize)
let font = UIFont.systemFont(ofSize: fontSize)
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: UIColor.black
]
if aspectRatio > 3 {
// Horizontal label
let textSize = (heightText as NSString).size(withAttributes: attributes)
let rect = CGRect(
x: (size.width - textSize.width) / 2,
y: (size.height - textSize.height) / 2,
width: textSize.width,
height: textSize.height
)
(heightText as NSString).draw(in: rect, withAttributes: attributes)
} else {
// Vertical rotated label
context.saveGState()
context.translateBy(x: size.width / 2, y: size.height / 2)
context.rotate(by: -.pi / 2)
let textSize = (heightText as NSString).size(withAttributes: attributes)
let rect = CGRect(
x: -textSize.width / 2,
y: -textSize.height / 2,
width: textSize.width,
height: textSize.height
)
(heightText as NSString).draw(in: rect, withAttributes: attributes)
context.restoreGState()
}
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image ?? UIImage()
}