BLACK FRIDAY SALE: Save big on all my Swift books and bundles! >>

SOLVED: Initialize @State variable with a function when the view is generated

Forums > 100 Days of SwiftUI

Hello,

I have a problem with a challenge from a day no 35 of "100 Days of SwiftUI" course. Basically I'm somehow unable to initialize @State variable in my code and the whole view crashes (index out of range for Text(tasks[questionNumber].task) because the array is uninitialized). First, let me explain what my code should do.

When the view changes from "Start View" to "Game View" (so, when the user selects options and clicks the button) Game View should generate questions before the view even shows up (I use .onAppear for that, so questions can be properly displayed). However, it just doesn't work - when I look at my code in the debugger I can see that questions are never generated.

Quick things about the code: questions are kept as a class of objects and my code tries to initialize that class. Some data shared between Views is kept in a file with @Published variables. Here is my code (functions GenerateTasks and OnAppear are at the end). I also paste some other code that can be helpful to understand the program. Can you please explain me, how can I initialize @State private var tasks with tasks, so that it can finally work? I'm stuck with this problem for a day now and can't find any solution.

Game View:

import SwiftUI

// This struct decides what should be displayed

struct GameView: View {

    @ObservedObject var data: StartViewData

    @State private var tasks = [Task]()
    @State private var questionNumber = 0
    @State private var gameInProgress = false
    @State private var answer = ""

    var buttonWidth: CGFloat = 60
    var buttonHeight: CGFloat = 25
    var startButtonWidth: CGFloat = 150
    var spacing: CGFloat = 15

    var body: some View {

        ZStack {

            LinearGradient(gradient: /*@START_MENU_TOKEN@*/Gradient(colors: [Color.red, Color.blue])/*@END_MENU_TOKEN@*/, startPoint: .top, endPoint: .bottom)
                .edgesIgnoringSafeArea(.all)

            VStack {
                HStack {
                    Text(tasks[questionNumber].task)
                        .foregroundColor(.blue)
                        .bold()
                    Text(answer.isEmpty ? "?" : answer)
                        .foregroundColor(.pink)

                }
            }

            < I deleted some code from here (not important)>

        }
        .onAppear(perform: generateTasks)
        Spacer()

    }

    func generateTasks() {

        tasks = TaskSet(limit: data.multiplicationRange, numberOfQuestions: data.questionsToGenerate).tasks

    }
}

Task set:

import Foundation

struct Task {
    var task: String
    var answer: String
}

Tasks class:

import Foundation

struct TaskSet {

    var tasks = [Task]()

    init(limit: Int, numberOfQuestions: Int) {
        tasks = generateTasks(range: limit, numberOfQuestions: numberOfQuestions)
    }

    func generateTasks(range: Int, numberOfQuestions: Int) -> [Task] {

        var temp = [Task]()

        for i in 1...range {
            for j in 1...12 {
                if j >= i {
                    let result = String(i * j)
                    temp.append(Task(task: "\(i) x \(j) =", answer: result))
                    if (i != j) {
                        temp.append(Task(task: "\(j) x \(i)", answer: result))
                    }
                }
            }
        }

        temp.shuffle()
        return Array(temp[0..<numberOfQuestions])

    }
}

File with data:

import Foundation

class StartViewData: ObservableObject {

    @Published var questionsToGenerate = 5
    @Published var multiplicationRange = 5
    @Published var counter = 0
    @Published var animateColor = false
    @Published var startButtonClicked = false
    @Published var startGame = true
    @Published var question = [String: Int]()

}

I will be very thankful for any help or tips!

1      

First thing to note, is that you call it Tasks class but it is defined as a struct. There is a big difference between class (reference semantics) and struct (value semantics). It is important to know the difference.

Second, you mention 2 views, but have only shared one view. The idea here is, the user is shown StartView when the app launches, where they will setup the game... correct? then upon hitting the "start" button, they are taken to GameView.

This requires passing data from one view to the other.

Accordingly, you should be initializing the data that GameView will be using before navigating to GameView and then pass it on.

For that, it is easiest to use @Binding in GameView. You should be using a navigation view or tabView to make things easy for you.

For your data, you certainly can use a class. But you could also consider using global constants. It is better to generate the tasks outside of your views. So your Model deals with it.

In order to provide more clarity on next steps for you, you will need to share your Start View code.

edit:

Otherwise you should access the data from your observed object. That's how you gain access to the published properties. Which means your StartViewData should be setting up your game data. It will be accessible via dot syntax data.generateData() or whichever way you decide to setup the game.

2      

Thanks @MarcusKay. You're right - these are structs. I come from the world of C languages and Java, so I accidentaly used that word. Sorry for that. You're of course correct about Views logic. I'm posting more code (and can share more if needed):

This View switches Views (from Start View to Game View after startGame from StartViewData changes it's state):

import SwiftUI

// This struct decides what should be displayed

struct ContentView: View {

    @ObservedObject var start = StartViewData()

    var body: some View {
        if (!start.startGame) {
            StartView(data: start)
        } else {
            GameView(data: start)
        }
    }
}

Start View is generally pretty long and boring (because it generally handles interface and some actions), but here it is. I update StartViewData in this file, so that it can be passed to the Game View:

import SwiftUI
import Foundation

struct StartView: View {

    @State private var questionsToGenerate = 5
    @State private var animateColor = false

    @ObservedObject var data: StartViewData

    var buttonWidth: CGFloat = 60
    var buttonHeight: CGFloat = 35
    var startButtonWidth: CGFloat = 150
    var buttonCornerRadius: CGFloat = 16
    var opacityValue = 0.7
    var spacing: CGFloat = 15
    let animationDuration = 0.3

    var body: some View {

        ZStack {

            // TODO: Add cool gradient to it (blue to violet or sth)

            /*Image("AppBackground")
             .resizable()
             .scaledToFill()
             .blur(radius: 10.0)
             .offset(x: -30)
             .edgesIgnoringSafeArea(.all)*/

            LinearGradient(gradient: /*@START_MENU_TOKEN@*/Gradient(colors: [Color.red, Color.blue])/*@END_MENU_TOKEN@*/, startPoint: .top, endPoint: .bottom)
                .edgesIgnoringSafeArea(.all)

            VStack {

                Text("Multiplication")
                    .fontWeight(.black)
                    .appLogoStyle()

                Text("Practise")
                    .fontWeight(.black)
                    .appLogoStyle()

                Spacer()
                    .frame(height: 580)

            }

            VStack {

                Text("In what range\nwould you like to practise?")
                    .fontWeight(.light)
                    .foregroundColor(.white)
                    .multilineTextAlignment(.center)
                    .padding()

                HStack {

                    Text("Range: 1 - \(data.multiplicationRange)")
                        .padding()
                        .foregroundColor(.black)

                    Stepper(value: $data.multiplicationRange, in: 1...12) {
                    }
                    .padding()
                    .frame(width: 100, height: /*@START_MENU_TOKEN@*/100/*@END_MENU_TOKEN@*/, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                }
                .padding()
                .background(Color.white.opacity(opacityValue))
                .cornerRadius(buttonCornerRadius)

                Text("How many questions\nwould you like to get?")
                    .fontWeight(.light)
                    .foregroundColor(.white)
                    .multilineTextAlignment(.center)
                    .padding()

                VStack {

                    HStack(spacing: spacing) {

                        Button(action: {
                            questionButtonTapped(5)
                        }) {
                            Text("5")
                                .font(.title)
                                .frame(minWidth: buttonWidth, maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/, minHeight: buttonHeight, maxHeight: buttonHeight, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                        }
                        .frame(width: buttonWidth)
                        .buttonStyle()
                        .background(self.animateColor ? (data.questionsToGenerate == 5 ? Color.green.opacity(opacityValue) : Color.white.opacity(opacityValue)) : Color.white.opacity(opacityValue))
                        .cornerRadius(buttonCornerRadius)

                        Button(action: {
                            questionButtonTapped(10)
                        }) {
                            Text("10")
                                .font(.title)
                                .frame(minWidth: buttonWidth, maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/, minHeight: buttonHeight, maxHeight: buttonHeight, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                        }
                        .frame(width: buttonWidth)
                        .buttonStyle()
                        .background(self.animateColor ? (data.questionsToGenerate == 10 ? Color.green.opacity(opacityValue) : Color.white.opacity(opacityValue)) : Color.white.opacity(opacityValue))
                        .cornerRadius(buttonCornerRadius)

                    }

                    Spacer()
                        .frame(height: spacing)

                    HStack(spacing: spacing) {

                        Button(action: {
                            questionButtonTapped(20)
                        }) {
                            Text("20")
                                .font(.title)
                                .frame(minWidth: buttonWidth, maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/, minHeight: buttonHeight, maxHeight: buttonHeight, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                        }
                        .frame(width: buttonWidth)
                        .buttonStyle()
                        .background(self.animateColor ? (data.questionsToGenerate == 20 ? Color.green.opacity(opacityValue) : Color.white.opacity(opacityValue)) : Color.white.opacity(opacityValue))
                        .cornerRadius(buttonCornerRadius)

                        Button(action: {
                            questionButtonTapped(data.multiplicationRange * 12)
                        }) {
                            Text("All")
                                .font(.title)
                                .frame(minWidth: buttonWidth, maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/, minHeight: buttonHeight, maxHeight: buttonHeight, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                        }
                        .frame(width: buttonWidth)
                        .buttonStyle()
                        .background(self.animateColor ? (data.questionsToGenerate == data.multiplicationRange * 12 ? Color.green.opacity(opacityValue) : Color.white.opacity(opacityValue)) : Color.white.opacity(opacityValue))
                        .cornerRadius(buttonCornerRadius)

                    }

                    Button(action: {
                        startButtonTapped()
                    }) {
                        Text("Start the game!")
                            .font(.body)
                            .frame(minWidth: startButtonWidth, maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/, minHeight: buttonHeight, maxHeight: buttonHeight, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                    }
                    .frame(width: startButtonWidth)
                    .buttonStyle()
                    .background(self.animateColor ? (data.startButtonClicked ? Color.green.opacity(opacityValue) : Color.white.opacity(opacityValue)) : Color.white.opacity(opacityValue))
                    .cornerRadius(buttonCornerRadius)
                    .offset(y: 60)

                }
            }
        }
    }

    func questionButtonTapped(_ value: Int) {

        data.questionsToGenerate = value
        let delay = 0.3
        print(value)

        if (data.counter == 0) {
            withAnimation(Animation.easeInOut(duration: animationDuration)) {
                self.animateColor.toggle()
            }
        }

        if (data.counter > 0) {
            self.animateColor.toggle()
            DispatchQueue.main.asyncAfter(deadline: .now() + delay / 5) {
                withAnimation(Animation.easeInOut(duration: animationDuration)) {
                    self.animateColor.toggle()
                }
            }
        }

        data.counter += 1

    }

    func startButtonTapped() {

        let delay = 0.3
        data.startButtonClicked.toggle()

        withAnimation(Animation.easeInOut(duration: animationDuration)) {
            self.animateColor.toggle()
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
            data.startGame.toggle()
        }

    }
}

struct appLogo: ViewModifier {
    func body(content: Content) -> some View {
        content
            .foregroundColor(.white)
            .font(.largeTitle)
    }
}

extension View {
    func appLogoStyle() -> some View {
        self.modifier(appLogo())
    }
}

struct buttonOption: ViewModifier {
    func body(content: Content) -> some View {
        content
            .foregroundColor(.black)
            .padding(.all)
    }
}

extension View {
    func buttonStyle() -> some View {
        self.modifier(buttonOption())
    }
}

Could you please give me some hint about @Binding? Where should it be? Thanks for your help and sorry if my code isn't that great - I have recently started to learn Swift.

1      

Here's an example that uses 2 views, one as the start which sets up the data, then passes it to the next view.

struct StartView: View {
    @State private var dataToPass = [Task]()
    // rest of the code sets it up

    Button("Click To Start") {
        dataToPass = generateData() //just an example
        GameView(dataToUse: $dataToPass)
    }
}

and in GameView you just declare the binding:

struct GameView: View {
    @Binding var dataToUse: [Task]
    // rest of code
}

The point of my mentioning @Binding was to simply point you at an overall simpler way of doing things.

You have added a couple layers of unnecessary complication. You could have simply done the following:

struct TaskSet {

    var tasks = [Task]()

// remove init (not needed)
    //make func static so you can access it anywhere
    static func generateTasks(range: Int, numberOfQuestions: Int) -> [Task] {

        var temp = [Task]()

        for i in 1...range {
            for j in 1...12 {
                if j >= i {
                    let result = String(i * j)
                    temp.append(Task(task: "\(i) x \(j) =", answer: result))
                    if (i != j) {
                        temp.append(Task(task: "\(j) x \(i)", answer: result))
                    }
                }
            }
        }

        temp.shuffle()
        return Array(temp[0..<numberOfQuestions])

    }
}

and in GameView, instead of using

@State private var tasks = [Task]()

You use the following only (no need for ObservedObject:

var tasks: [Task]

Then once you have the game setup properly, you just call

GameView(tasks: TaskSet.generateTasks(range: yourNumber, numberOfQuestions: yourOtherNumber)

2      

Point being, there is no need for classes and observedObject. GameView is only receiving the data. That's all it needs.

StartView, is where you present the UI for the user to choose their settings, at which point, you use these details to generate the right questions. Again, what StartView really needs to know is the relevant data to be able to generate the questions, which will then be passed to GameView.

2      

Thank you so much @MarcusKay! Your answers were really helpful. I think that I understand now what should I do to make my program work. I will try to apply the necessary fixes in a few days (because right now I'm pretty overwhelmed with the university stuff). I will then post an update about my progress and whether everything went fine.

1      

I finally got it to work, thank you so much @MarcusKay. Now I clearly understand how passing data in that way works in Swift. I hope that I will finish the project without any more problems.

2      

Hacking with Swift is sponsored by RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.