Switching between an array of views in swiftUI

Viewed 2763

Swift 5.x iOS 13.x

I have an array of dice views I created and I want to switch between them. I just show two views here to keep this short. I created an array of the views but I can't quite figure out how to address it in SwiftUI? or indeed what sort of object a view is since it isn't a view or some view it seems.

import SwiftUI
import GameplayKit

let dotSize:CGFloat = 24
let diceSize:CGFloat = 128
let fieldSide:CGFloat = 32

let d6 = GKRandomDistribution.d2()

var diceViews:[Any] = [oneDotDice(),twoDotDice()]

struct ContentView: View {
  @State var showInt:Int = 1
  var body: some View {

VStack {
  if showInt == 1 {
    oneDotDice()
      .transition(AnyTransition.opacity)
      .animation(.default)
      .modifier(changeDice(showInt: self.$showInt))
  }
  if showInt == 2 {
    twoDotDice()
      .transition(AnyTransition.opacity)
      .animation(.default)
      .modifier(changeDice(showInt: self.$showInt))
  }
 }
}

struct oneDotDice: View {
  var body: some View {
  Rectangle()
    .fill(Color.clear)
    .frame(width: diceSize, height: diceSize, alignment: .center)
    .border(Color.black)
   .background(Text(1))
  }
}

struct twoDotDice: View {
  var body: some View {
  Rectangle()
    .fill(Color.clear)
    .frame(width: diceSize, height: diceSize, alignment: .center)
    .border(Color.black)
   .background(Text(2))
  }
}

struct changeDice: ViewModifier {
  @Binding var showInt:Int
  func body(content: Content) -> some View {
    content
      .onTapGesture {
        self.showInt = d6.nextInt()
        print("Int ",self.showInt)
      }
    }
  }

How can make this code better using my array of views the in main loop? I cannot even use a switch statement it seems in SwiftUI code. I need to use half a dozen nested if/else statements...

2 Answers

View is just a struct. You can store it in an array or other attribute type and then use it in another Views. AnyView is the good solution if you really want to mix different types of View in one collection. To dynamically show that collection I like to use ForeEach. You can iterate not the exact Views but indices of an array. Therefore SwiftUI will make a key of this View from an array index of it. But it have a cost. You can't dynamically change the content of that array. it must be constant at all parent View lifetime. To be honest, you actually can iterate the exact View through the ForeEach but you need to make them Identyfiable (they must have an public var id = UUID() attribute).

import SwiftUI

let dotSize:CGFloat = 24
let diceSize:CGFloat = 128
let fieldSide:CGFloat = 32


struct ContentView: View {
  var diceViews:[AnyView] = [AnyView(oneDotDice()), AnyView(twoDotDice())]
  @State var showInt: Int = 1
  var body: some View {
    ZStack{
        ForEach(diceViews.indices){ind in
            ZStack{
                if ind == self.showInt{
                    self.diceViews[ind]
                        .transition(.scale(scale: 0, anchor: .center))
                    .onTapGesture {
                        let newShow = Int.random(in: 0...1)
                        print("new show id \(newShow)")
                          withAnimation(.linear(duration: 0.3)){
                            self.showInt = newShow
                          }
                    }
                }
            }

        }
    }
  }
}
struct oneDotDice: View {
  var text: String = "1"
  var body: some View {
  Rectangle()
    .fill(Color.clear)
    .frame(width: diceSize, height: diceSize)
    .border(Color.black)
   .background(Text(text))
  }
}

struct twoDotDice: View {
  var text: String = "2"
  var body: some View {
  Rectangle()
    .fill(Color.clear)
    .frame(width: diceSize, height: diceSize)
    .border(Color.black)
   .background(Text(text))
  }
}


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

By the way, I change your random logic, not sure how it was working, but I think its not really important for your question.

I suggest you to remove oneDotDice and twoDotDice, to have a single DiceFace that can represent all the possible values. Also, no need for a custom modifier, no need for an array of Views (by the way, if you want something like that you could use AnyView, or a ForEach).

You can copy paste the following example in a Playground to check:

import SwiftUI
import GameplayKit
import PlaygroundSupport

let dotSize: CGFloat = 24
let diceSize: CGFloat = 128
let fieldSide: CGFloat = 32

let d6 = GKRandomDistribution(forDieWithSideCount: 6)

struct ContentView: View {
    @State var currentValue: Int = 1

    var body: some View {
        DiceFace(dotCount: currentValue)
            .transition(AnyTransition.opacity)
            .animation(.default)
            .onTapGesture(perform: self.roll)
    }

    func roll() {
        self.currentValue = d6.nextInt()
        print("Int: \(self.currentValue)")
    }

    // Your `oneDotDice` and `twoDotDice` can be represented with this View, passing simply a different `dotCount`
    // Also, in Swift types go with an uppercase, and variables with a lowercase.
    struct DiceFace: View {
        let dotCount: Int

        var body: some View {
            Rectangle()
                .fill(Color.clear)
                .frame(width: diceSize, height: diceSize, alignment: .center)
                .border(Color.black)
                .background(Text(String(dotCount)))
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())
PlaygroundPage.current.needsIndefiniteExecution = true
Related