1

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

Higherous
  • 92
  • 1
  • 6

3 Answers3

1

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)
    }
}
0

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)
                            }
                        }
                    }
                }
            }
        }
    }
}
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • 1
    State variables cannot be modified on the view, so this does not work – Higherous Aug 09 '21 at 09:11
  • @Higherous did you actually try this code? State variables are designed to be modified from the body of the view, that is their main purpose. They cannot be modified from _outside_ the view, but they can be modified from inside it. – Dávid Pásztor Aug 09 '21 at 09:14
  • Are you really sure? Just try minimal lightweight code with the same usage `struct ContentView: View { @State var myVar: Int = 0 var body: some View { myVar = 5 Text("Hello, World!") .padding() } }` State variables can only be changed from functions like onAppear – Higherous Aug 09 '21 at 09:41
  • @Higherous that's completely wrong, not sure why you got this idea from. I am completely sure that changing `State` variables from inside the body of a view is safe. – Dávid Pásztor Aug 09 '21 at 09:42
  • This is impossible. I dropped the code above, try it yourself Pastebin https://pastebin.com/qiQ1Y79z – Higherous Aug 09 '21 at 09:43
0

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 {
        ...
    }
    
}
Higherous
  • 92
  • 1
  • 6