3

I would like to create a starry background view in SwiftUI that has its stars located randomly using Double.random(), but does not reinitialise them and move them when the parent view reloads its var body.

struct ContentView: View {
    @State private var showButton = true

    var body: some View {
        ZStack {
            BackgroundView()
            if showButton {
                Button("Tap me"){
                    self.showButton = false
                }
            }
        }
    }
}

I define my background view as such.

struct BackgroundView: View {
    var body: some View {
        ZStack {
            GeometryReader { geometry in
                Color.black
                ForEach(0..<self.getStarAmount(using: geometry), id: \.self){ _ in
                    Star(using: geometry)
                }
                LinearGradient(gradient: Gradient(colors: [.purple, .clear]), startPoint: .bottom, endPoint: .top)
                    .opacity(0.7)
            }
        }
    }

    func getStarAmount(using geometry: GeometryProxy) -> Int {
        return Int(geometry.size.width*geometry.size.height/100)
    }
}

A Star is defined as

struct Star: View {
    let pos: CGPoint
    @State private var opacity = Double.random(in: 0.05..<0.4)

    init(using geometry: GeometryProxy) {
        self.pos = CGPoint(x: Double.random(in: 0..<Double(geometry.size.width)), y: Double.random(in: 0..<Double(geometry.size.height)))
    }



    var body: some View {
        Circle()
            .foregroundColor(.white)
            .frame(width: 2, height: 2)
            .scaleEffect(CGFloat(Double.random(in: 0.25...1)))
            .position(pos)
            .opacity(self.opacity)
            .onAppear(){
                withAnimation(Animation.linear(duration: 2).delay(Double.random(in: 0..<6)).repeatForever()){
                    self.opacity = self.opacity+0.5
                }
            }
    }
}

As one can see, a Star heavily relies on random values, for both its animation (to create a 'random' twinkling effect) as well as its position. When the parent view of the BackgroundView, ContentView in this example, gets redrawn however, all Stars get reinitialised, their position values change and they move across the screen. How can this best be prevented?

I have tried several approaches to prevent the positions from being reinitialised. I can create a struct StarCollection as a static let of BackgroundView, but this is quite cumbersome. What is the best way to go about having a View dependent on random values (positions), only determine those positions once?


Furthermore, the rendering is quite slow. I have attempted to call .drawingGroup() on the ForEach, but this then seems to interfere with the animation's opacity interpolation. Is there any viable way to speed up the creation / re-rendering of a view with many Circle() elements?

5
  • why not making a picture/snapshot of your star-drawing and set this as background? Commented Oct 29, 2019 at 15:02
  • @Chris I would like to have the background be scalable to any screen size as well as the stars twinkling, thus animating. Furthmore I would like to retain control over the stars so that I may add animations later. This would not be possible with a static picture. Commented Oct 29, 2019 at 15:05
  • 1
    I'm not familiar with SwiftUI, but this is a good example of separating the model from the view (in the Model/View/Controller pattern). Instead of creating the starry background in a View, create a separate data model (e.g., a Background class), in which the information needed to generate the background (such as star positions) is stored and changed. This data model will be usable application-wide, and does not rely on any SwiftUI components. Commented Oct 29, 2019 at 15:11
  • @PeterO. Thanks for your reply! I will attempt to store the data seperately, but the positions are also dependent on the view (size), which makes it difficult. As for the MVC model, SwiftUI uses a whole new take and actually incorporates it. Commented Oct 29, 2019 at 15:13
  • @Isaiah - not sure if you solved this but wouldn't the solution be to also store the view size in your view model (or data object) so that the positions can be correctly calculated or at least scaled to the correct view position. I have similar issues with things like Lists where the scroll position gets reset if you switch between screens. Kind of begs the question as to how useful SwiftUI is currently. Commented Mar 14, 2020 at 6:59

1 Answer 1

2

The slowness coming out from the overcomplicated animations setting in onAppear, you only need the self.opacity state change to initiate the animation, so please move animation out and add to the shape directly.

   Circle()
        .foregroundColor(.white)
        .frame(width: 2, height: 2)
        .scaleEffect(CGFloat(Double.random(in: 0.25...1)))
        .position(pos)
        .opacity(self.opacity)
        .animation(Animation.linear(duration: 0.2).delay(Double.random(in: 0..<6)).repeatForever())
        .onAppear(){
           // withAnimation{ //(Animation.linear(duration: 2).delay(Double.random(in: 0..<6)).repeatForever()){
                self.opacity = self.opacity+0.5
          // }
        }
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you; so this would then load earlier & prevent 'live' lags?
You may try and let us know the result. you can change duration from 0.2 to 2. I use that for the test

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.