Reputation: 102
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.
It is necessary to clarify, my application is on a macOS and the window is resized
Upvotes: 1
Views: 291
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
Reputation: 36304
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
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