0

Background

In this learning app I would like to deliver functionality that moves a point to a random location until interrupted.

App

The interface is very simple and contains of two buttons that Start/Stop the animation or Reset/Center position.

Initial state

Code

The full application code is provided below.

//
//  ContentView.swift
//  BouncingBall
//
//  Sample script providing endless animation of bouncing ball.
//
//  Created by Konrad on 24/07/2021.
//

import SwiftUI

// Simple UI button
struct SimpleButton: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding(.horizontal)
            .background(Color(.lightGray))
            .clipShape(Capsule())
    }
}

func randomiseXY(xMax: CGFloat, yMax: CGFloat) -> (CGFloat, CGFloat) {
    let randX = CGFloat.random(in: 0...xMax)
    let randY = CGFloat.random(in: 0...yMax)
    return (randX, randY)
}

func makeCircleView(_ geometry: GeometryProxy, randomPosition: Bool) -> some View {
    // Original idea: https://stackoverflow.com/a/57577752/1655567

    // Define initial position in the centre
    var posX: CGFloat
    var posY: CGFloat

    if randomPosition {
        (posX, posY) = randomiseXY(xMax: geometry.size.width, yMax: geometry.size.height)
    } else {
        posX = (geometry.size.width - geometry.size.width / 7) / 2
        posY = (geometry.size.height - geometry.size.height / 7) / 2
    }

    let circleWithLabels =
        VStack {
            HStack {
                Text("X: \(posX)")
                Text("Y: \(posY)")
            }
            Circle()
                .path(in: CGRect(x: posX, y: posY,
                                 width: CGFloat(geometry.size.width / 7),
                                 height: CGFloat(geometry.size.height / 7))
                )
        }

    return circleWithLabels
}

struct ContentView: View {

    @State private var currentlyRunning: Bool = false

    var body: some View {
        VStack {
            HStack {
                if currentlyRunning {
                    Button("Stop") {
                        self.currentlyRunning = false
                    }
                    .buttonStyle(SimpleButton())
                } else {
                    Button("Start") {
                        self.currentlyRunning.toggle()
                    }
                    .buttonStyle(SimpleButton())
                }
                Button("Reset") {
                    self.currentlyRunning = false
                }
                .buttonStyle(SimpleButton())
            }
            GeometryReader { geometry in
                while currentlyRunning {
                    makeCircleView(geometry, randomPosition: currentlyRunning)
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Problem

The offending bit relates to my attempt of trying to use the ViewBuilder within the flow control:

 GeometryReader { geometry in
        while currentlyRunning {
            makeCircleView(geometry, randomPosition: currentlyRunning)
        }
    }

This results in the following error:

Closure containing control flow statement cannot be used with result builder 'ViewBuilder'

Question

How can I work around it so I can force the partial re-creation of the View, until the condition changes?

Side questions

  • As I'm starting with / any broader pointers on good coding practice in Swift are most welcome.

With the offending bit removed as follows:

    GeometryReader { geometry in
        //while currentlyRunning {
            makeCircleView(geometry, randomPosition: currentlyRunning)
        //}
    }

I can keep on manually refreshing the view; however the dot gets cantered every so often due to the randomPosition taking the false value.

Half-working animation

1 Answer 1

1

Use timer. Generate a new random location at every second or any interval.

struct ContentView: View {
    
    @State private var currentlyRunning: Bool = false
    
    // Define initial position in the centre
    @State var posX: CGFloat = 0.0
    @State var posY: CGFloat = 0.0
    
    @State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    
    var body: some View {
        VStack {
            HStack {
                if currentlyRunning {
                    Button("Stop") {
                        self.currentlyRunning = false
                        stopTimer()
                    }
                    .buttonStyle(SimpleButton())
                } else {
                    Button("Start") {
                        self.currentlyRunning.toggle()
                        startTimer()
                    }
                    .buttonStyle(SimpleButton())
                }
                Button("Reset") {
                    self.currentlyRunning = false
                }
                .buttonStyle(SimpleButton())
            }
            GeometryReader { geometry in
                makeCircleView(geometry, randomPosition: currentlyRunning)
            }
        }
    }
    
    func randomiseXY(xMax: CGFloat, yMax: CGFloat) -> (CGFloat, CGFloat) {
        let randX = CGFloat.random(in: 0...xMax)
        let randY = CGFloat.random(in: 0...yMax)
        return (randX, randY)
    }
    
    func stopTimer() {
        self.timer.upstream.connect().cancel()
    }
    
    func startTimer() {
        self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    }
    func makeCircleView(_ geometry: GeometryProxy, randomPosition: Bool) -> some View {
        let circleWithLabels =
            VStack {
                HStack {
                    Text("X: \(posX)")
                    Text("Y: \(posY)")
                }
                Circle()
                    .path(in: CGRect(x: posX, y: posY,
                                     width: CGFloat(geometry.size.width / 7),
                                     height: CGFloat(geometry.size.height / 7))
                    )
            } .onReceive(timer) { _ in
                if self.currentlyRunning {
                    (posX, posY) = randomiseXY(xMax: geometry.size.width, yMax: geometry.size.height)
                }
            }
        
        return circleWithLabels
    }
}

And try to avoid global function. Also, use the view model for other calculations.

enter image description here

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.