I am trying to implement a Loader that rotates in the center of the screen:
struct Loader : View {
#State var isRotated = false
var animation: Animation {
Animation.linear
.repeatForever(autoreverses: false)
}
var body: some View {
VStack {
Circle()
.trim(from: 0, to: 0.8)
.stroke(AngularGradient(gradient: .init(colors: [Color(#colorLiteral(red: 0.9069682956, green: 0.7839415669, blue: 0.9612388015, alpha: 1)),.white]), center: .center), style: StrokeStyle(lineWidth: 8, lineCap: .round))
.frame(width: 45, height: 45)
.rotationEffect(Angle.degrees(isRotated ? 360 : 0))
.animation(animation)
}
.onAppear{
self.isRotated.toggle()
}
}
}
I want the loader to rotate in the middle of the screen, but when I run my code the circle moves up the y axis with each rotation. Why is it doing this?
Related
Swift 5.2, iOS 13
I want to predict/calculate the centre point of a frame after it has been scaled so I can move a shape to it while scaling it. If I try to scale and center an view in an animation/dynamically it doesn't work, with the end result a combination of center points I suspect. So in the images below, the blue box starts in the top right hand corner, I scale and move it to the center. But as you can see from the green box, the scaling has messed up the point it needs to get to...
struct SwiftUIView2: View {
#State var relocate = Alignment.topTrailing
#State var zoom:CGFloat = 1.0
#State var tag:Bool = true
#State var tag2:Bool = false
#State var centerPoint: CGPoint = .zero
var body: some View {
ZStack {
ZStack(alignment: relocate) {
Rectangle()
.stroke(Color.blue, lineWidth: 2)
.frame(width: 32, height: 32, alignment: .center)
.onTapGesture {
withAnimation {
if self.tag {
self.zoom = 2.0
self.relocate = Alignment.center
} else {
self.relocate = Alignment.topTrailing
self.zoom = 1.0
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.tag.toggle()
}
}
.scaleEffect(zoom, anchor: .topLeading)
}.frame(width: 256, height: 256, alignment: relocate)
.border(Color.red)
if !tag {
ZStack {
Rectangle()
.stroke(Color.green, lineWidth: 2)
.frame(width: 32, height: 32, alignment: .center)
.onTapGesture {
self.tag.toggle()
}
}.frame(width: 256, height: 256, alignment: .center)
.scaleEffect(zoom, anchor: .center)
}
}
}
}
GeoReader I thought would be the answer, but I get garbage from it too. Spent more than a week trying custom alignments, position, everything I can think of. Eyeballed a solution with screen site percentages for now, but obviously it doesn't work too well with different sized screens.
Here is fix (tested with Xcode 11.4 / iOS 13.4)
}
.scaleEffect(zoom) // << here !! (remove topLeading anchor)
}.frame(width: 256, height: 256, alignment: relocate)
I have an app with a view where I show images taken with the device's camera (stored
with Core Data) and I want to create the effect of time lapse - say you take photos
of a flower over a period of time and want to show those images in a stream. I have
been able to do that - but I want to have some transition effects between the images
and have not been able to do so. I can apply rotation and a static scale but cannot
fade from 0 to 1 opacity, nor scale up from zero to full frame. I must be missing
something really simple. Here's the code using an array of photos instead of Core Data
for simplicity.
struct ContentView: View {
#State private var activeImageIndex = 0
#State private var startTimer = false
#State private var myAnimationBool = true
let timer = Timer.publish(every: 1.0, on: .main, in: .default).autoconnect()
let myShots = ["CoolEquipment", "FishAndChipsMedium", "FlatheadLake1", "GlacierBusMedium", "Hike", "HuckALaHuckMedium", "JohnAndRiverMedium", "MacDonaldLodgeLakesideMedium"
]
var body: some View {
GeometryReader { geo in
VStack {
Text("Sequence")
.foregroundColor(.blue)
.font(.system(size: 25))
Image(self.myShots[self.activeImageIndex])
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geo.size.width - 20, height: geo.size.width - 20, alignment: .center)
.cornerRadius(20)
.shadow(radius: 10, x: 10, y: 10)
//None of these do anything
//.animation(.easeInOut(duration: 0.5))
//.animation(.easeInOut)
//.transition(.opacity)
//.transition(self.myAnimationBool ? .opacity : .slide)
//.transition(.scale)
//.transition(AnyTransition.opacity.combined(with: .slide))
//this works but is static
//.scaleEffect(self.myAnimationBool ? 0.5 : 1.0)
//this works but again is static
//.rotationEffect(.degrees(self.myAnimationBool ? 90 : 0))
.onReceive(self.timer) { t in
print("in fixed timer")
if self.startTimer {
self.activeImageIndex = (self.activeImageIndex + 1) % self.myShots.count
}
}
HStack {
Button(action: {
self.startTimer.toggle()
}) {
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
.frame(width: 200, height: 40)
Text(self.startTimer ? "Stop" : "Start").font(.headline)
}
}
}
.padding()
}//top VStack
}.onDisappear{ self.startTimer = false }
}
}
Any guidance would be appreciated. Xcode 11.3 (11C29)
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
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()
}
I am attempting to use the GeometryReader to place a view. I used the GeometryReader to
modify a Rectangle() and it is placed as I expected. However, I want to programmatically
set the y value to line up with the top of the VStack frame. I thought I could read
the geometry height of the text field and use it, but I cannot assign the geometry
dimension of the textField to a variable, even though I use the same concept to move
the Rectangle.
Pictorially:
The simple code:
struct ContentView: View {
#State private var textHeight = 0.0
var body: some View {
VStack {
GeometryReader { geometry in
VStack {
Text("Title Text")
}
//self.textHeight = CGFloat(geometry.size.height)
}.border(Color.green)
GeometryReader { geometry in
Rectangle()
.path(in: CGRect(x: geometry.size.width - 50,
y: 0, width: geometry.size.width / 2.0,
height: geometry.size.height / 2))
.fill(Color.red)
}
}.frame(width: 150, height: 300).border(Color.black)
}
}
Xcode Beta 7, Catalina Beta 7
Any guidance would be appreciated.
The problem is that you're using a VStack with two GeometryReader inside. The VStack positions its children vertically, one above the other. The y=0 of your second GeometryReader is the first y after the first GeometryReader. I suggest you change the way you're designing your UI, for example you can get the same result this way:
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
VStack {
ZStack {
Rectangle()
.path(in: CGRect(x: geometry.size.width - 50, y: 0, width: geometry.size.width / 2.0, height: geometry.size.height / 4))
.fill(Color.red)
Text("Title Text")
.frame(width: geometry.size.width, height: geometry.size.height/2.0)
.border(Color.green)
}
Spacer()
}
}
.frame(width: 150, height: 300).border(Color.black)
}
}
The result is:
To push things up and down or to the left/right you can use the really useful Spacer view.
This is my advice, but if you really need to use two GeometryReader you have to do this way:
struct ContentView2: View {
#State private var textHeight = CGFloat(0)
private func firstContentView(geometry: GeometryProxy) -> some View {
textHeight = geometry.size.height
return Text("Title Text")
.position(x: geometry.size.width/2.0, y: geometry.size.height/2.0)
}
var body: some View {
VStack(spacing: 0) {
GeometryReader { geometry in
self.firstContentView(geometry: geometry)
}.border(Color.green)
GeometryReader { geometry in
Rectangle()
.path(in: CGRect(x: geometry.size.width - 50, y: -self.textHeight, width: geometry.size.width / 2.0, height: geometry.size.height / 2))
.fill(Color.red)
}
}.frame(width: 150, height: 300).border(Color.black)
}
}
But it's a pretty dirty solution.