ty1
ty1

Reputation: 364

How to extract state from an array of struct views? SwiftUI

I'm having an issue grabbing the state out of an array of SwiftUI structs.

I have a struct for cards that contain topText and bottomText. I have another struct that is a form for input components such as the card. I store these cards in an array, appending more and more as a + button is clicked. In the parent view I have a foreach over that array so they render dynamically.

I'm having an issue with trying to extract the state value from these structs. The cards contain state for the topText and bottomText. When a save button is clicked in the parent array (where the array of cards exists), it iterates over the array of cards, and prints their state through a get method. However, it is not printing the current state of those cards. It prints a blank string. I believe this is happening because when I append the struct, it's merely copying a dead struct, and not the real stateful struct.

Do I have my assumptions correct? Or is this a bug in swiftUI? Does anyone have any ideas on a proper way to do this? I just want to get the input out of the cards whenever the button is clicked. It needs to be dynamic though, as the user can click the + button however many times they want and continue making more cards.

Heres the code for the card stuct:

struct memeCard: View, Identifiable{
    @State private var topText = ""
    @State private var bottomText = ""

    let id = UUID()

    //  TODO: figure out how to make text wrap so it doesn't go on one line forever.

    var body: some View{
        VStack(spacing: 30){
            TextField("Top text...", text: $topText).multilineTextAlignment(.center)
            Divider()
            TextField("Bottom text...", text: $bottomText)
        }
        .frame(width: 300, height: 350, alignment: .center).multilineTextAlignment(.center)
        .background(Color.white)
        .cornerRadius(20)
        .shadow(color: Color.gray, radius: 4, x: 4, y: 4)
    }

    func getBottomText() -> String{
        return self.bottomText
    }

    func getTopText() -> String{
        return self.topText
    }
}

And here is the parent view:


struct CreateMemePage: View {
    @Environment(\.presentationMode) var presentation

    let realm = try! Realm()
    @State private var sectionInput = ""
    @State private var memeTitleInput = ""
    @State private var offsetValue: CGFloat = 0.0
    @State private var createCards: [memeCard] = [memeCard()]

    var body: some View {

NavigationView{
            VStack(spacing: 30){
                ScrollView{
                    TextField("Section...", text: $sectionInput)
                        .multilineTextAlignment(.center)
                        .frame(width: 350, height: 75, alignment: .center)
                        .background(Color.white)
                        .cornerRadius(15)
                        .shadow(color: Color.gray, radius: 4, x: 4, y: 4)

                    TextField("Meme Title...", text: $memeTitleInput)
                        .multilineTextAlignment(.center)
                        .frame(width: 350, height: 75, alignment: .center)
                        .background(Color.white)
                        .cornerRadius(15)
                        .shadow(color: Color.gray, radius: 4, x: 4, y: 4)


                    ForEach(0..<self.createCards.count, id: \.self){ item in
                        self.createCards[item].padding()
                    }

                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
                }

                Button(action: {
                    self.createCards.append(memeCard())
                }) {
                    Image(systemName: "plus")
                        .accentColor(Color.white)
                        .frame(width: 50, height: 50, alignment: .center)
                        .background(LinearGradient(gradient: Gradient(colors: [ .red, .purple]), startPoint: .top, endPoint: .trailing))
                        .cornerRadius(60)
                        .shadow(color: Color.gray, radius: 2, x: 3, y: 3)
                }
            }
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
            .keyboardSensible($offsetValue)

            .navigationBarTitle("Create meme")
            .navigationBarItems(trailing:
                Button(action: {

                 for item in createCards{
                     print(item.getBottomText())
                     print(item.getTopText())
              }
                }){
                    Text("Save")
                }
            )        }

    }

}

Upvotes: 1

Views: 1266

Answers (1)

Glenn Posadas
Glenn Posadas

Reputation: 13300

You need to know how to use ObservableObject, @ObservedObject, @State, and @Binding.

But for simplicity, let's take a look how we can make your memeCard functions work.

Creating a model

class Model: ObservableObject {
    var topText: String = ""
    var bottomText: String = ""

    init(top: String, bottom: String) {
        self.topText = top
        self.bottomText = bottom
    }
}

Using a model instance inside memeCard View.

struct memeCard: View, Identifiable{
    @ObservedObject var model = Model(top: "", bottom: "")

    let id = UUID()
    ...

Next, we bind the model instance to the textFields.

VStack(spacing: 30){
    TextField("Top text...", text: self.$model.topText).multilineTextAlignment(.center)
    Divider()
    TextField("Bottom text...", text: self.$model.bottomText)
}

And then we can extract the data from the model class using your existing top and bottom functions, like so:

func getBottomText() -> String{
    print("BOTTOM: \(self.model.bottomText)")
    return self.model.bottomText
}

func getTopText() -> String{
    print("TOP: \(self.model.topText)")
    return self.model.topText
}

Voila! It now should work. This answer from another question https://stackoverflow.com/a/57623488/3231194 should help you out, it's written comprehensively.

Upvotes: 1

Related Questions