Передача вычисляемой переменной в другое представление

#swift #swiftui #swiftui-view

Вопрос:

Я пытаюсь создать свою собственную сетку, размер которой изменяется для каждого элемента. С этим все в порядке. Вот код

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

Но поскольку я не знаю точного количества элементов и их индексов, мне нужно рассчитать их с учетом.

 let index = row * columnCount   column
 

И в этом проблема — если я передам их как обычно ( MovieCellView(index: index ... ) ) при изменении индекса, новое значение не будет передано для просмотра.
Я не могу использовать @State и @Binding, так как я не могу объявить его непосредственно в конструкторе представлений и не могу объявить его в структуре, потому что я не знаю количество. Как правильно передавать данные?

Кодекс 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 {
        ...
    }
    
}
 

Самый простой пример
Просто добавлено Text("(index)") в VStack с MovieCellView и Text("(index)") в MovieCellView теле. Индекс в VStack всегда меняется, но не в MovieCellView.
Результат
Необходимо уточнить, что мое приложение находится на macOS, и размер окна изменен

Ответ №1:

Это мое решение (тестовый код) для вашей проблемы передачи вычисляемой переменной в другое представление. Я использую ObservableОбъект для хранения информации, необходимой для достижения того, что вам нужно.

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

Комментарии:

1. У меня один и тот же индекс для каждой ячейки, использующей это решение

2. да, извините, я этого не видел. Я обновил свой ответ.

3. Это избавляет меня от главного преимущества моей сетки — замены элементов и динамического изменения размера.

4. означает ли это, что это не работает для вас?

5. Да, потому что количество столбцов и строк не меняются

Ответ №2:

Единственная переменная, которая изменяется за пределами a ForEach в вашем index расчете, — это columnCount . row и column являются просто индексами из циклов. Таким образом, вы можете объявить columnCount как @State переменную в родительском представлении, поэтому, когда оно изменится, родительское представление обновится, и, следовательно, все ячейки также обновятся с новым 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)
                            }
                        }
                    }
                }
            }
        }
    }
}
 

Комментарии:

1. Переменные состояния не могут быть изменены в представлении, поэтому это не работает

2. @Higherous вы действительно пробовали этот код? Переменные состояния предназначены для изменения из тела представления, что является их основной целью. Они не могут быть изменены извне представления, но они могут быть изменены изнутри него.

3. Ты действительно уверен? Просто попробуйте минимальный легкий код с тем же использованием struct ContentView: View { @State var myVar: Int = 0 var body: some View { myVar = 5 Text("Hello, World!") .padding() } } Переменные состояния могут быть изменены только с помощью таких функций, как onAppear

4. @Higherous это совершенно неправильно, не знаю, откуда у тебя эта идея. Я полностью уверен, что изменение State переменных внутри тела представления безопасно.

5. Это невозможно. Я сбросил код выше, попробуйте сами Вставить pastebin.com/qiQ1Y79z

Ответ №3:

Поскольку ни один из ответов не сработал, и только один сработал наполовину, я решил проблему сам. Вам нужно сгенерировать привязку, а затем просто передать ее

 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 {
        ...
    }
    
}