Higherous
Higherous

Reputation: 102

Passing calculated variable to another View

I am trying to create my own grid, which resizing to every element. It's okay with that. Here is a code

GeometryReader { geo in
            let columnCount = Int((geo.size.width / 250).rounded(.down))
            let tr = CDNresponse.data?.first?.translations ?? []
            let rowsCount = (CGFloat(tr.count) / CGFloat(columnCount)).rounded(.up)
            LazyVStack {
                ForEach(0..<Int(rowsCount), id: \.self) { row in // create number of rows
                    HStack {
                        ForEach(0..<columnCount, id: \.self) { column in // create columns
                            let index = row * columnCount + column
                            if index < (tr.count) {
                                VStack {
                                    MovieCellView(index: index, tr: tr, qualities: $qualities)
                                }
                            }
                        }
                    }
                }
            }
        }

But since I don't know the exact number of elements and their indices, I need to calculate them in view.

let index = row * columnCount + column

And that's the problem - if I pass them as usual (MovieCellView(index: index ... )) when the index changing, the new value is not passed to view. I cannot use @State and @Binding, as I cannot declare it directly in View Builder and can't declare it on struct because I don't know the count. How to pass data correctly?

Code of MovieCellView:

struct MovieCellView: View {
    @State var index: Int
    @State var tr: [String]
    @State var showError: Bool = false
    @State var detailed: Bool = false
    
    @Binding var qualities: [String : [Int : URL]]
    
    var body: some View {
        ...
    }
    
}

The most simple example Just added Text("\(index)") in VStack with MovieCellView and Text("\(index)") in MovieCellView body. Index in VStack always changing, but not in MovieCellView. Result It is necessary to clarify, my application is on a macOS and the window is resized

Upvotes: 1

Views: 291

Answers (3)

Higherous
Higherous

Reputation: 102

Since none of the answers worked, and only one worked half, I solved the problem myself. You need to generate a Binding, and then just pass it

var videos: some View {
        GeometryReader { geo in
            let columnCount = Int((geo.size.width / 250).rounded(.down))
            let rowsCount = (CGFloat((CDNresponse.data?.first?.translations ?? []).count) / CGFloat(columnCount)).rounded(.up)
            LazyVStack {
                ForEach(0..<Int(rowsCount), id: \.self) { row in // create number of rows
                    HStack {
                        ForEach(0..<columnCount, id: \.self) { column in // create 3 columns
                            let index = row * columnCount + column
                            if index < ((CDNresponse.data?.first?.translations ?? []).count) {
                                MovieCellView(translation: trBinding(for: index), qualities: qualityBinding(for: (CDNresponse.data?.first?.translations ?? [])[index]))
                            }
                        }
                    }
                }
            }
        }
    }
    private func qualityBinding(for key: String) -> Binding<[Int : URL]> {
        return .init(
            get: { self.qualities[key, default: [:]] },
            set: { self.qualities[key] = $0 })
    }
    private func trBinding(for key: Int) -> Binding<String> {
        return .init(
            get: { (self.CDNresponse.data?.first?.translations ?? [])[key] },
            set: { _ in return })
    }



struct MovieCellView: View {
    @State var showError: Bool = false
    @State var detailed: Bool = false
    @Binding var translation: String
    
    @Binding var qualities: [Int : URL]
    
    var body: some View {
        ...
    }
    
}

Upvotes: 0

This is my solution (test code) to your issue of passing calculated variable to another View. I use an ObservableObject to store the information needed to achieve what you are after.

import SwiftUI

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class MovieCellModel: ObservableObject {
    @Published var columnCount: Int = 0
    @Published var rowCount: Int = 0
    
    // this could be in the view 
    func index(row: Int, column: Int) -> Int {
        return row * columnCount + column
    }
}

struct ContentView: View {
    @StateObject var mcModel = MovieCellModel()
    @State var qualities: [String : [Int : URL]] = ["":[1: URL(string: "https://example.com")!]] // for testing
    let tr = CDNresponse.data?.first?.translations ?? [] // for testing
    
    var body: some View {
        VStack {
            GeometryReader { geo in
                LazyVStack {
                    ForEach(0..<Int(mcModel.rowCount), id: \.self) { row in // create number of rows
                        HStack {
                            ForEach(0..<mcModel.columnCount, id: \.self) { column in // create 3 columns
                                if mcModel.index(row: row, column: column) < (tr.count) {
                                    VStack {
                                        MovieCellView(index: mcModel.index(row: row, column: column), tr: tr, qualities: $qualities) 
                                    }
                                }
                            }
                        }
                    }
                }.onAppear {
                    mcModel.columnCount = Int((geo.size.width / 250).rounded(.down))
                    mcModel.rowCount = Int((CGFloat(tr.count) / CGFloat(mcModel.columnCount)).rounded(.up))
                }
            }
        }
        
    }
}

struct MovieCellView: View {
    @State var index: Int

    @State var tr: [String]
    @Binding var qualities: [String : [Int : URL]]
    
    @State var showError: Bool = false
    @State var detailed: Bool = false

    var body: some View {
        Text("\(index)").foregroundColor(.red)
    }
}

Upvotes: 1

David Pasztor
David Pasztor

Reputation: 54706

The only variable that changes outside of a ForEach in your index calculation is columnCount. row and column are simply indices from the loops. So you can declare columnCount as a @State variable on the parent view, hence when it changes, the parent view will update and hence all of the cells will also update with the new columnCount.

@State private var columnCount: CGFloat = 0

var body: some View {
    GeometryReader { geo in
        columnCount = Int((geo.size.width / 250).rounded(.down))
        let tr = CDNresponse.data?.first?.translations ?? []
        let rowsCount = (CGFloat(tr.count) / CGFloat(columnCount)).rounded(.up)
        LazyVStack {
            ForEach(0..<Int(rowsCount), id: \.self) { row in // create number of rows
                HStack {
                    ForEach(0..<columnCount, id: \.self) { column in // create 3 columns
                        let index = row * columnCount + column
                        if index < (tr.count) {
                            VStack {
                                MovieCellView(index: index, tr: tr, qualities: $qualities)
                            }
                        }
                    }
                }
            }
        }
    }
}

Upvotes: 0

Related Questions