简体   繁体   中英

Ambiguous reference to member 'subscript' in VStack Swift UI 5

I try to run a function in a VStack statement but it don't work. When I run it in a button (with the action label) it work perfectly. How can I insert my func in a VStack?

I declare a QuizData class:

class QuizData: ObservableObject {
    var allQuizQuestion: [QuizView] = [QuizView]()

    let objectWillChange = PassthroughSubject<QuizData,Never>()

    var currentQuestion: Int = 0 {
        didSet {
            withAnimation() {
                objectWillChange.send(self)
            }
        }
    }
}

and I use it there :

struct Quiz: View {        
    var continent: Continent
    @EnvironmentObject var quizData: QuizData

    var body: some View {
        VStack
        {
            generateQuiz(continent: continent, quizData: self.quizData)
            quizData.allQuizQuestion[quizData.currentQuestion]
        }
        .navigationBarTitle (Text(continent.name), displayMode: .inline)
    }

}

The func generateQuiz is:

func generateQuiz(continent: Continent, quizData: QuizData) -> Void {

    var capital: [Capital]
    var alreadyUse: [Int]

    for country in CountryData {

        if country.continentId == continent.id
        {
            alreadyUse = [Int]()
            capital = [Capital]()

            capital.append(CapitalData[country.id])

            for _ in 1...3 {
                var index = Int.random(in: 1 ... CapitalData.count - 1)

                while alreadyUse.contains(index) {
                    index = Int.random(in: 1 ... CapitalData.count - 1)
                }

                capital.append(CapitalData[index])
            }

            capital.shuffle()
            quizData.allQuizQuestion.append(QuizView(country: country, question: QuestionData[country.id], capital: capital))
        }
    }
    quizData.allQuizQuestion.shuffle()
}

I need to generate quiz question before the view appear. How should I do this?

First, you can't call a function that doesn't return some View in a VStack closure because that closure is not a normal closure, but a @ViewBuilder closure:

@functionBuilder
struct ViewBuilder {
    // Build a value from an empty closure, resulting in an
    // empty view in this case:
    func buildBlock() -> EmptyView {
        return EmptyView()
    }

    // Build a single view from a closure that contains a single
    // view expression:
    func buildBlock<V: View>(_ view: V) -> some View {
        return view
    }

    // Build a combining TupleView from a closure that contains
    // two view expressions:
    func buildBlock<A: View, B: View>(_ viewA: A, viewB: B) -> some View {
        return TupleView((viewA, viewB))
    }

    // And so on, and so forth.
    ...
}

It's a Swift 5.1 feature that lets you do things like these:

VStack {
    Image(uiImage: image)
    Text(title)
    Text(subtitle)
}

With which you can easily create a view from several other views. For further information take a look at https://www.swiftbysundell.com/posts/the-swift-51-features-that-power-swiftuis-api

Now, if I get your issue (correct me if I'm wrong) you need to call a function before your view appears to generate some data. Honestly I'd prefer to pass that data to the view from the outside (creating the data before the view creation). But if you really need it you can do something like:

struct ContentView: View {

    private var values: [Int]! = nil

    init() {
        values = foo()
    }

    var body: some View {
        List(values, id: \.self) { val in
            Text("\(val)")
        }
    }

    func foo() -> [Int] {
        [0, 1, 2]
    }
}

#if DEBUG

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

#endif

Using the struct init and calling the function at the view creation.

EDIT: To answer your comment here below and since you are using an @EnvironmentObject you can do:

class ContentViewModel: ObservableObject {
    @Published var values: [Int]!

    init() {
        values = generateValues()
    }

    private func generateValues() -> [Int] {
        [0, 1, 2]
    }
}

struct ContentView: View {
    @EnvironmentObject var contentViewModel: ContentViewModel

    var body: some View {
        List(contentViewModel.values, id: \.self) { val in
            Text("\(val)")
        }
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
        .environmentObject(ContentViewModel()) //don't forget this
  }
}
#endif

And in your SceneDelegate :

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(
                rootView: ContentView()
                    .environmentObject(ContentViewModel()) //don't forget this
            )
            self.window = window
            window.makeKeyAndVisible()
        }
    }

This way you are creating a view model for your view and that view model will be accessible throughout your view hierarchy. Every time your view model will change your view will change too.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM