How to update different views using SwiftUI? - rotation

I am aiming to make a program in which I am using using SwiftUI buttons to update by SCNView in SceneKit. I have a cylinder as a SCNCylinder in my SCNView inside a frame in SwiftUI. I want my cylinder to rotate about 180° after I press the button. In my current code I have used #State and #Binding to update the view. But somehow the cylinder rotates as soon as I run the code, not waiting for me to touch the button. Not sure why this happens
Here is my code :
struct ContentView: View {
#State var rotationAngle: Float = 180
var body: some View {
VStack{
Button(action: {
// What to perform
self.rotationAngle = 180
}) {
// How the button looks like
Text("180°")
.frame(width: 100, height: 100)
.position(x: 225, y: 500)
}
SceneKitView(angle: self.$rotationAngle)
.frame(width: 300, height: 300)
.position(x: 225, y: 0)
}
}
}
struct SceneKitView: UIViewRepresentable {
#Binding var angle: Float
func degreesToRadians(_ degrees: Float) -> CGFloat {
return CGFloat(degrees * .pi / 180)
}
func makeUIView(context: UIViewRepresentableContext<SceneKitView>) -> SCNView {
let sceneView = SCNView()
sceneView.scene = SCNScene()
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
sceneView.backgroundColor = UIColor.white
sceneView.frame = CGRect(x: 0, y: 10, width: 0, height: 1)
return sceneView
}
func updateUIView(_ sceneView: SCNView, context: UIViewRepresentableContext<SceneKitView>) {
let cylinder = SCNCylinder(radius: 0.02, height: 2.0)
let cylindernode = SCNNode(geometry: cylinder)
cylindernode.position = SCNVector3(x: 0, y: 0, z: 0)
cylinder.firstMaterial?.diffuse.contents = UIColor.green
cylindernode.pivot = SCNMatrix4MakeTranslation(0, -1, 0)
let rotation = SCNAction.rotate(by: self.degreesToRadians(self.angle), around: SCNVector3(1, 0, 0), duration: 5)
sceneView.scene?.rootNode.addChildNode(cylindernode)
cylindernode.runAction(rotation)
}
typealias UIViewType = SCNView
}
I want the cylinder to rotate after I press the button. Please help me with this problem.

just set startingAngle to 0
#State var rotationAngle: Float = 0

Related

SwiftUI Drag gesture is not ending

I was trying to create a custom View with rotation effects in SwiftUI. Instead of using rotation gestures, I was trying to use the Drag Gesture to rotate the Parent by adding a separate view to the parent. Dragging the child (The Blue Dot) would rotate its parent (ZStack). But I've faced difficulty while dragging the blue dot. The drag gesture is not ending. Can you guys help me to find out what I did wrong?
import SwiftUI
struct EditingHolder: View {
/* For Rotation Circle Drag */
#State private var currDragRect : CGSize = .zero
#State private var prevDragRect : CGSize = .zero
var rectRadius : CGFloat {
let sum : CGFloat = 2*pow(150, 2)
return sqrt(sum/4)
}
var dragRadius : CGFloat {
let height = prevDragRect.height + currDragRect.height
let width = prevDragRect.width + currDragRect.width
let sum = pow(height, 2) + pow(width, 2)
return sqrt(sum/4)
}
var rotateAngle : CGFloat{
let angle = asin(dragRadius/rectRadius)
print("🍄 Angle Produced = ", angle)
return angle
}
var body: some View {
/* **** Gestures **** */
let rotateDrag = DragGesture()
.onChanged({ value in
print("🏵 Rotate Circle Drag Started ...")
currDragRect = value.translation
}).onEnded({ _ in
print("🏵 Rotate Circle Drag Ended ✅")
prevDragRect.height += currDragRect.height
prevDragRect.width += currDragRect.width
currDragRect = .zero
})
//************* Views *******************
GeometryReader { geo in
ZStack(alignment: .center) {
Rectangle()
.padding()
.foregroundColor(Color.yellow)
///Rotate Circle `top`
Circle()
.foregroundColor(Color.blue)
.frame(width: 20, height: 20)
.position(x: 150 - 3, y: 3)
.gesture(rotateDrag)
}.frame(width:150, height: 150, alignment: .center)
.border(.green, width: 3)
.position(x: geo.size.width/2, y: geo.size.height/2)
.rotationEffect(.radians(rotateAngle))
}
//************* Views *******************
}
}
struct EditingHolder_Previews: PreviewProvider {
static var previews: some View {
EditingHolder()
}
}
To provide high priority to a specific gesture there is a modifier called .highPriorityGesture(). Have a look here for a better explanation How to use gestures in SwiftUI.
I've updated the angle calculations,
/* **** Gestures **** */
let rotateDrag = DragGesture()
.onChanged({ value in
print("🏵 Rotate Circle Drag Started ...")
let difY = center.y - value.location.y
let difX = center.x - value.location.x
//Initial Angle when the drag started..
if deltaAngle == 0{
deltaAngle = atan2(difY, difX)
}else{
angle = atan2(difY, difX) - deltaAngle
}
}).onEnded({ _ in
print("🏵 Rotate Circle Drag Ended ✅")
withAnimation {
angle = 0
deltaAngle = 0
}
})
//************* Views *******************
Now to add .highPriorityGesture(rotateDrag) to ZStack.
.onTapGesture() is added to get the center for angle calculation. Tap on the view and then rotate by dragging the blue dot.
Here is the final implementation,
struct EditingHolder: View {
/* For Rotation Circle Drag */
#State private var center : CGPoint = .zero
#State private var angle : CGFloat = 0
#State private var deltaAngle : CGFloat = 0
var body: some View {
/* **** Gestures **** */
let rotateDrag = DragGesture()
.onChanged({ value in
print("🏵 Rotate Circle Drag Started ...")
let difY = center.y - value.location.y
let difX = center.x - value.location.x
//Initial Angle when the drag started..
if deltaAngle == 0{
deltaAngle = atan2(difY, difX)
}else{
angle = atan2(difY, difX) - deltaAngle
}
}).onEnded({ _ in
print("🏵 Rotate Circle Drag Ended ✅")
withAnimation {
angle = 0
deltaAngle = 0
}
})
//************* Views *******************
GeometryReader { geo in
ZStack(alignment: .center) {
Rectangle()
.padding()
.foregroundColor(Color.yellow)
///Rotate Circle `top`
Circle()
.foregroundColor(Color.blue)
.frame(width: 20, height: 20)
.position(x: 150 - 3, y: 3)
.gesture(rotateDrag)
}.frame(width:150, height: 150, alignment: .center)
.border(.green, width: 3)
.position(x: geo.size.width/2, y: geo.size.height/2)
.rotationEffect(Angle(radians: angle))
/* You have make the gesture a high priority */
.highPriorityGesture(rotateDrag)
.onTapGesture {
print("☘️ Center assigned..")
center = CGPoint(x: geo.frame(in: .global).size.width/2, y: geo.frame(in: .global).size.height/2)
}
}
//************* Views *******************
}
}
struct EditingHolder_Previews: PreviewProvider {
static var previews: some View {
EditingHolder()
}
}
The sequence of drag gesture and rotation is important, otherwise SwiftUI looses context of the dragged view (which is changing by drag).
Also you don't need GeometryReader. Here is an example that works in regards to the dragging, the angle calculation needs some more work.
struct ContentView: View {
/* For Rotation Circle Drag */
#State private var currDragRect : CGSize = .zero
#State private var prevDragRect : CGSize = .zero
let rectRadius : CGFloat = 75
var dragRadius : CGFloat {
let height = prevDragRect.height + currDragRect.height
let width = prevDragRect.width + currDragRect.width
let sum = pow(height, 2) + pow(width, 2)
return sqrt(sum/4)
}
var rotateAngle : CGFloat{
let x = min(1, max(-1, dragRadius/rectRadius)) // asin only defined in -1...1
let angle = asin(x)
print("🍄 Angle Produced = ", angle)
return angle
}
var body: some View {
/* **** Gestures **** */
let rotateDrag = DragGesture()
.onChanged { value in
print("🏵 Rotate Circle Drag Started ...")
currDragRect = value.translation
}
.onEnded { _ in
print("🏵 Rotate Circle Drag Ended ✅")
prevDragRect.height += currDragRect.height
prevDragRect.width += currDragRect.width
currDragRect = .zero
}
//************* Views *******************
ZStack(alignment: .topTrailing) {
Rectangle()
.padding()
.foregroundColor(Color.yellow)
.border(.green, width: 3)
///Rotate Circle `top`
Circle()
.foregroundColor(Color.blue)
.frame(width: 20, height: 20)
.offset(x: 8, y: -8)
}
.rotationEffect(.radians(rotateAngle)) // rotation here
.gesture(rotateDrag) // drag here
.frame(width:150, height: 150, alignment: .center)
//************* Views *******************
}
}

SwiftUI Animation - Issue with Tabview

I am having an issue with animations in a tabview. I have a tabview with 2 views.
The first view has a shape with an animation. The second view is a simple text.
When I launch the application, View1 appears and the animation is correct. When I swipe to View2 and come back to View1, the animation no longer appear as intended and is somewhat random. Anyone might know what the issue might be ? Thank you.
ContentView
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
View1()
View2()
} //: TAB
.tabViewStyle(PageTabViewStyle())
.padding(.vertical, 20)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
View1
import SwiftUI
struct FollowEffect: GeometryEffect {
var pct: CGFloat = 0
let path: Path
var rotate = true
var animatableData: CGFloat {
get { return pct }
set { pct = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
if !rotate {
let pt = percentPoint(pct)
return ProjectionTransform(CGAffineTransform(translationX: pt.x, y: pt.y))
} else {
// Calculate rotation angle, by calculating an imaginary line between two points
// in the path: the current position (1) and a point very close behind in the path (2).
let pt1 = percentPoint(pct)
let pt2 = percentPoint(pct - 0.01)
let a = pt2.x - pt1.x
let b = pt2.y - pt1.y
let angle = a < 0 ? atan(Double(b / a)) : atan(Double(b / a)) - Double.pi
let transform = CGAffineTransform(translationX: pt1.x, y: pt1.y).rotated(by: CGFloat(angle))
return ProjectionTransform(transform)
}
}
func percentPoint(_ percent: CGFloat) -> CGPoint {
let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
let f = pct > 0.999 ? CGFloat(1-0.001) : pct
let t = pct > 0.999 ? CGFloat(1) : pct + 0.001
let tp = path.trimmedPath(from: f, to: t)
return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY)
}
}
struct Solar2Grid: Shape {
func path(in rect: CGRect) -> Path {
return Solar2Grid.createArcPath(in: rect)
}
static func createArcPath(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.width, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height - 20))
path.addArc(center: CGPoint(x: rect.width - 20, y: rect.height - 20), radius: CGFloat(20), startAngle: .degrees(0), endAngle: .degrees(90), clockwise: false)
path.addLine(to: CGPoint(x: 0, y: rect.height))
return path
}
}
struct AnimRecView: View {
#State var flag: Bool = false
var body: some View {
ZStack {
Solar2Grid()
.stroke(Color.purple, style: StrokeStyle( lineWidth: 2, dash: [3]))
Circle()
.foregroundColor(Color.red)
.blur(radius: 3.0)
.frame(width: 8, height: 8).offset(x: -40, y: -40)
.modifier(FollowEffect(pct: self.flag ? 1 :0, path: Solar2Grid.createArcPath(in: CGRect(x: 0, y: 0, width: 80, height: 80)), rotate: false))
.onAppear {
withAnimation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false)) {
self.flag.toggle()
}
}
}
}
}
struct View1: View {
#State var flag: Bool = false
var body: some View {
VStack() {
Text("View1")
Spacer()
HStack() {
AnimRecView()
}
.frame(width: 80, height: 80, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
Spacer()
}
.frame(minWidth: /*#START_MENU_TOKEN#*/0/*#END_MENU_TOKEN#*/, maxWidth: /*#START_MENU_TOKEN#*/.infinity/*#END_MENU_TOKEN#*/, minHeight: /*#START_MENU_TOKEN#*/0/*#END_MENU_TOKEN#*/, maxHeight: /*#START_MENU_TOKEN#*/.infinity/*#END_MENU_TOKEN#*/, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.background(LinearGradient(gradient: Gradient(colors: [Color.blue, Color.black]), startPoint: .top, endPoint: .bottom))
.cornerRadius(20)
.padding(.horizontal, 20)
}
}
struct View1_Previews: PreviewProvider {
static var previews: some View {
View1()
}
}
View2
import SwiftUI
struct View2: View {
var body: some View {
Text("View2")
}
}
struct View2_Previews: PreviewProvider {
static var previews: some View {
View2()
}
}
The problem is that .onAppear() is only called once, so the next time the view is shown, the animation doesn't know what to do. The fix is to put an explicit animation on the Circle() itself. Then, when the view comes back on screen, it has the appropriate animation. Like this:
struct AnimRecView: View {
#State var flag: Bool = false
var body: some View {
ZStack {
Solar2Grid()
.stroke(Color.purple, style: StrokeStyle( lineWidth: 2, dash: [3]))
Circle()
.foregroundColor(Color.red)
.blur(radius: 3.0)
.frame(width: 8, height: 8).offset(x: -40, y: -40)
.modifier(FollowEffect(pct: self.flag ? 1 : 0, path: Solar2Grid.createArcPath(in: CGRect(x: 0, y: 0, width: 80, height: 80)), rotate: false))
// Put the explicit animation here
.animation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false), value: flag)
.onAppear {
self.flag = true
}
}
}
}

SwiftUI Animation Circle with Colors

My problem is simple I think but I can't figure how solve it.
I've this :
struct ArcSelectionView: View {
#Binding var isShowing: Bool
#Binding var curColor: Color
#Binding var colorToPress: Color
#Binding var score: Int
#State var colors = [Color.blue, Color.red, Color.green, Color.yellow]
var body: some View {
ZStack {
ForEach(1 ..< 5, id: \.self) { item in
Circle()
.trim(from: self.isShowing ? CGFloat((Double(item) * 0.25) - 0.25) : CGFloat(Double(item) * 0.25),
to: CGFloat(Double(item) * 0.25))
.stroke(self.colors[item - 1], lineWidth: 50)
.frame(width: 300, height: 300)
.onTapGesture {
if colors[item - 1] == colorToPress {
score += 1
}
isShowing.toggle()
colorToPress = colors.randomElement() ?? Color.offWhite
colors.shuffle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
self.isShowing.toggle()
}
}
}
}
.opacity(self.isShowing ? 1 : 0)
.rotationEffect(.degrees(self.isShowing ? 0 : 180))
.animation(.linear(duration: 0.35))
}
}
If I didn't shuffle colors in the .onTapGesture, everything is ok. But If I do, I've a strange plain Circle that appears in the middle and disappear after. It's ugly. Ugly Circle
Thank you for your help !
The issue is with the animation of the Circles. The better solution is to use arc shapes. Here is a working solution:
struct ArcSelectionView: View {
#Binding var curColor: Color
#Binding var colorToPress: Color
#Binding var score: Int
#State private var colors = [Color.blue, Color.red, Color.green, Color.yellow]
#State private var pct: CGFloat = 0.25
#State private var originalPCT: CGFloat = 0.25
let duration: Double = 0.35
var body: some View {
ZStack {
CircleView(wedge: originalPCT)
// I am not sure why, but at there is a difference of 10 in the sizes of the
// circle and the modifier. This corrects for it so the touch is accurate.
.frame(width: 310, height: 310)
PercentageArc(Color.clear, colors: colors, pct: pct) {
// With this solution you must have the callback sent to
// the main thread. This was unnecessary with AnimatbleModifier.
DispatchQueue.main.async {
pct = originalPCT
}
}
.animation(.linear(duration: duration), value: pct)
.frame(width: 300, height: 300)
// This forces the view to ignore taps.
.allowsHitTesting(false)
}
.onAppear {
pct = 1.0 / CGFloat(colors.count)
originalPCT = pct
}
}
func CircleView(wedge: CGFloat) -> some View {
ZStack {
// Array(zip()) is a cleaner and safe way of using indices AND you
// have the original object to use as well.
ForEach(Array(zip(colors, colors.indices)), id: \.0) { color, index in
Circle()
.trim(from: CGFloat((Double(index) * wedge)),
to: CGFloat(Double(index + 1) * wedge))
// The color of the stroke should match your background color.
// Clear won't work.
.stroke(.white, lineWidth: 50)
.onTapGesture {
if color == colorToPress {
score += 1
print("score!")
}
pct = 0
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
colorToPress = colors.randomElement() ?? .white
colors.shuffle()
}
}
}
}
}
}
struct PercentageArc<Content>: View, Animatable where Content: View {
private var content: Content
private var colors: [Color]
private var pct: CGFloat
private var target: CGFloat
private var onEnded: () -> ()
init(_ content: Content, colors: [Color], pct: CGFloat, onEnded: #escaping () -> () = {}) {
self.content = content
self.colors = colors
self.pct = pct
self.target = pct
self.onEnded = onEnded
}
var animatableData: CGFloat {
get { pct }
set { pct = newValue
// newValue here is interpolating by engine, so changing
// from previous to initially set, so when they got equal
// animation ended
if newValue == target {
onEnded()
}
}
}
var body: some View {
content
.overlay(
ForEach(Array(zip(colors, colors.indices)), id: \.0) { color, index in
ArcPortionShape(pct: pct, startAngle: .degrees(1.0 / CGFloat(colors.count) * CGFloat(index) * 360.0))
.foregroundColor(color)
}
)
}
struct ArcPortionShape: InsettableShape {
let pct: CGFloat
let startAngle: Angle
var insetAmount = 0.0
init(pct: CGFloat, startAngle: Angle) {
self.pct = pct
self.startAngle = startAngle
}
var portion: CGFloat {
pct * 360.0
}
var endAngle: Angle {
.degrees(startAngle.degrees + portion)
}
func path(in rect: CGRect) -> Path {
var p = Path()
p.addArc(center: CGPoint(x: rect.width / 2.0, y:rect.height / 2.0),
radius: rect.height / 2.0 + 5.0,
startAngle: startAngle,
endAngle: endAngle,
clockwise: false)
return p.strokedPath(.init(lineWidth: 50))
}
func inset(by amount: CGFloat) -> some InsettableShape {
var arc = self
arc.insetAmount += amount
return arc
}
}
}
Originally, I made this with an AnimatableModifier, but it is deprecated, and the solution using it fails if it is placed in ANY stack or NavigationView. I can see why AnimatableModifier is deprecated.
This solution draws inspiration from this answer from Asperi, for the callback idea, though the solution will not work in iOS 15.2.

SwiftUI Confetti Animation

Note: I solved the issue. If you are interested in the animation, I created a package with SPM and is available on https://github.com/simibac/ConfettiSwiftUI
So I am trying to create a confetti animation in SwiftUI. This is what I have so far:
This works all as expected and is done with the following code:
struct Movement{
var x: CGFloat
var y: CGFloat
var z: CGFloat
var opacity: Double
}
struct FancyButtonViewModel: View {
#State var animate = [false]
#State var finishedAnimationCouter = 0
#State var counter = 0
var body: some View {
VStack{
ZStack{
ForEach(finishedAnimationCouter...counter, id:\.self){ i in
ConfettiContainer(animate:$animate[i], finishedAnimationCouter:$finishedAnimationCouter, num:1)
}
}
Button("Confetti"){
animate[counter].toggle()
animate.append(false)
counter+=1
}
}
}
}
struct ConfettiContainer: View {
#Binding var animate:Bool
#Binding var finishedAnimationCouter:Int
var num:Int
var body: some View{
ZStack{
ForEach(0...num-1, id:\.self){ _ in
Confetti(animate: $animate, finishedAnimationCouter:$finishedAnimationCouter)
}
}
.onChange(of: animate){_ in
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) {
finishedAnimationCouter+=1
}
}
}
}
struct Confetti: View{
#Binding var animate:Bool
#Binding var finishedAnimationCouter:Int
#State var movement = Movement(x: 0, y: 0, z: 1, opacity: 0)
var body: some View{
Text("❤️")
.frame(width: 50, height: 50, alignment: .center)
.offset(x: movement.x, y: movement.y)
.scaleEffect(movement.z)
.opacity(movement.opacity)
.onChange(of: animate) { _ in
withAnimation(Animation.easeOut(duration: 0.4)) {
movement.opacity = 1
movement.x = CGFloat.random(in: -150...150)
movement.y = -300 * CGFloat.random(in: 0.7...1)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
withAnimation(Animation.easeIn(duration: 3)) {
movement.y = 200
movement.opacity = 0.0
}
}
}
}
}
Now I want to replace the Heart Emoji with an animated confetti view. Thus, I created the following view:
struct ConfettiView: View {
#State var animate = false
#State var xSpeed = Double.random(in: 0.7...2)
#State var zSpeed = Double.random(in: 1...2)
#State var anchor = CGFloat.random(in: 0...1).rounded()
var body: some View {
Rectangle()
.frame(width: 20, height: 20, alignment: .center)
.onAppear(perform: { animate = true })
.rotation3DEffect(.degrees(animate ? 360:0), axis: (x: 1, y: 0, z: 0))
.animation(Animation.linear(duration: xSpeed).repeatForever(autoreverses: false))
.rotation3DEffect(.degrees(animate ? 360:0), axis: (x: 0, y: 0, z: 1), anchor: UnitPoint(x: anchor, y: anchor))
.animation(Animation.linear(duration: zSpeed).repeatForever(autoreverses: false))
}
}
This works as expected as well. However, if I replace the Text("❤️") with. ConfettiView() I get some unexpected animation as shown below. (I reduced the number of confettis to 1 so the animation can be observed better). What does the animation break?
Your animations overlaps, to solve you need to join inner one with state value.
Here is fixed variant. Tested with Xcode 12.1 / iOS 14.1
struct ConfettiView: View {
#State var animate = false
#State var xSpeed = Double.random(in: 0.7...2)
#State var zSpeed = Double.random(in: 1...2)
#State var anchor = CGFloat.random(in: 0...1).rounded()
var body: some View {
Rectangle()
.frame(width: 20, height: 20, alignment: .center)
.onAppear(perform: { animate = true })
.rotation3DEffect(.degrees(animate ? 360:0), axis: (x: 1, y: 0, z: 0))
.animation(Animation.linear(duration: xSpeed).repeatForever(autoreverses: false), value: animate)
.rotation3DEffect(.degrees(animate ? 360:0), axis: (x: 0, y: 0, z: 1), anchor: UnitPoint(x: anchor, y: anchor))
.animation(Animation.linear(duration: zSpeed).repeatForever(autoreverses: false), value: animate)
}
}
Thanks for the code above, very useful for my project.
However, instead of clicking, I have embedded the animation generation within a timer:
func startConfettiTimer() {
confettiTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: { _ in
confettiAnimate[confettiCounter].toggle()
confettiAnimate.append(false)
confettiCounter+=1
})
_ = confettiTimer
}
func stopConfettiTimer() {
confettiTimer?.invalidate()
}
Full code here:
Github

How to update a SceneKit scene with SwiftUI?

I have a cylinder as SCNCylinder in a SCNScene in SceneKit and want to display it in a frame in SwiftUI. My goal is to rotate the cylinder by a angle of 180° or 90° (as the user chooses). To take the input (of the angle of rotation) i have used Text() and onTapGesture{ .... } property in SwiftUI. After I tap the text, the cylinder rotates but now I have two cylinders, one at the original position and one rotating at an desired angle. I am not sure why this happens. I want the same cylinder to rotate, not a identical copy of that doing it. I have connected the SwiftUI view and SceneKit view by using #State and #Binding.
Here is my code :
struct ContentView: View {
#State var rotationAngle = 0
var body: some View {
VStack{
Text("180°").onTapGesture {
self.rotationAngle = 180
}
Spacer()
Text("90°").onTapGesture {
self.rotationAngle = 90
}
SceneKitView(angle: $rotationAngle)
.position(x: 225.0, y: 200)
.frame(width: 300, height: 300, alignment: .center)
}
}
}
struct SceneKitView: UIViewRepresentable {
#Binding var angle: Int
func degreesToRadians(_ degrees: Float) -> CGFloat {
return CGFloat(degrees * .pi / 180)
}
func makeUIView(context: UIViewRepresentableContext<SceneKitView>) -> SCNView {
let sceneView = SCNView()
sceneView.scene = SCNScene()
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
sceneView.backgroundColor = UIColor.white
sceneView.frame = CGRect(x: 0, y: 10, width: 0, height: 1)
return sceneView
}
func updateUIView(_ sceneView: SCNView, context: UIViewRepresentableContext<SceneKitView>) {
let cylinder = SCNCylinder(radius: 0.02, height: 2.0)
let cylindernode = SCNNode(geometry: cylinder)
cylindernode.position = SCNVector3(x: 0, y: 0, z: 0)
cylinder.firstMaterial?.diffuse.contents = UIColor.green
cylindernode.pivot = SCNMatrix4MakeTranslation(0, -1, 0)
let inttofloat = Float(self.angle)
let rotation = SCNAction.rotate(by: self.degreesToRadians(inttofloat), around: SCNVector3(1, 0, 0), duration: 5)
cylindernode.runAction(rotation)
sceneView.scene?.rootNode.addChildNode(cylindernode)
}
typealias UIViewType = SCNView
}
I want to have a single cylinder rotation at a given angle.
The problem is, that updateUIView will be called several times. You can check this by adding a debug point there. Because of that your cylinder will be added several times. So you can solve this by many ways...one way would be to delete all nodes in your sceneview before starting your animation like so:
func updateUIView(_ sceneView: SCNView, context: UIViewRepresentableContext<SceneKitView>) {
sceneView.scene?.rootNode.enumerateChildNodes { (node, stop) in
node.removeFromParentNode()
}

Resources