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

Konrad
  • 17,740
  • 16
  • 106
  • 167

1 Answers1

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

Raja Kishan
  • 16,767
  • 2
  • 26
  • 52