Passing calculated variable to another View - swift

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

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

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

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

Related

SwiftUI iterating through #State or #Published dictionary with ForEach

Here is a minimum reproducible code of my problem. I have a dictionary of categories and against each category I have different item array. I want to pass the item array from dictionary, as binding to ListRow so that I can observer the change in my ContentView. Xcode shows me this error which is very clear
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Item' conform to 'Identifiable.
The solution shows in this question Iterating through set with ForEach in SwiftUI not using any #State or #Published variable. They are just using it for showing the data. Any work around for this issue ??
struct Item {
var id = UUID().uuidString
var name: String
}
struct ListRow {
#Binding var item: Item
var body: some View {
TextField("Place Holder", text: $item.name)
}
}
struct ContentView: View {
var categories = ["Bakery","Fruits & Vagetables", "Meat & poultry", "Dairy & Eggs", "Pantry", "Household"]
#State private var testDictionary: [String: [Item]] = [:]
var body: some View {
VStack(alignment: .leading) {
ForEach(categories, id: \.self) { category in
Text(category)
.font(.system(size: 30))
ForEach(testDictionary[category]) { item in
ListRow(item: item)
}
}
}.onAppear(
addDummyDateIntoDictonary()
)
}
func addDummyDateIntoDictonary() {
for category in categories {
testDictionary[category] = [Item(name: category + "1"), Item(name: category + "2")]
}
}
}
One problem is that you didn't make ListRow conform to View.
// add this
// ╭─┴──╮
struct ListRow: View {
#Binding var item: Item
var body: some View {
TextField("Place Holder", text: $item.name)
}
}
Now let's address your main problem.
A Binding is two-way: SwiftUI can use it to get a value, and SwiftUI can use it to modify a value. In your case, you need a Binding that updates an Item stored somewhere in testDictionary.
You can create such a Binding “by hand” using Binding.init(get:set:) inside the inner ForEach.
struct ContentView: View {
var categories = ["Bakery","Fruits & Vagetables", "Meat & poultry", "Dairy & Eggs", "Pantry", "Household"]
#State private var testDictionary: [String: [Item]] = [:]
var body: some View {
VStack(alignment: .leading) {
ForEach(categories, id: \.self) { category in
Text(category)
.font(.system(size: 30))
let items = testDictionary[category] ?? []
ForEach(items, id: \.id) { item in
let itemBinding = Binding<Item>(
get: { item },
set: {
if
let items = testDictionary[category],
let i = items.firstIndex(where: { $0.id == item.id })
{
testDictionary[category]?[i] = $0
}
}
)
ListRow(item: itemBinding)
}
}
}.onAppear {
addDummyDateIntoDictonary()
}
}
func addDummyDateIntoDictonary() {
for category in categories {
testDictionary[category] = [Item(name: category + "1"), Item(name: category + "2")]
}
}
}

Dynamically sized #State var

I'm loading data into a struct from JSON. With this data, a new structure (itemStructures) is created and filled for use in a SwiftUI View. In this View I have a #State var which I manually initialise to have enough space to hold all items. This state var holds all parameters which are nested within the items, hence the nested array.
As long as this #State var has enough empty spaces everything works fine. But my question is, how do I modify this #State programmatically for when the number of items increases er decreases with the loading of a new JSON? I could make it really large but I'd rather have it the exact size after each load.
//Structs used in this example
struct MainViewState {
var itemStructures: [ItemStructure]
}
struct ItemStructure: Identifiable, Hashable {
var id: String {name}
var name: String
var parameters: [Parameter]
}
struct Parameter: Identifiable, Hashable {
var id: String {name}
var name: String
var value: Double
var range: [Double]
}
struct ContentView: View {
//In this model json is loaded, this seemed out of scope for this question to include this
#ObservedObject var viewModel: MainViewModel
//This is the #State var which should be dynamically allocated according to the content size of "itemStructures"
//For now 3 items with 10 parameters each are enough
#State var parametersPerItem: [[Float]] = [
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0]
]
init(viewModel: MainViewModel) {
self.viewModel = viewModel
}
var body: some View {
let itemStructures = viewModel.mainState.itemStructures
ForEach( Array(itemStructures.enumerated()), id: \.element ) { index, item in
Text(item.name)
ForEach( Array(item.parameters.enumerated()), id: \.element ) { i, parameter in
Text(parameter.name)
SliderView(
label: parameter.name,
value: Binding(
get: { self.parametersPerItem[index][i] },
set: { (newVal) in
self.parametersPerItem[index][i] = newVal
//Function to send slider values and ranges to real time processing
//doStuffWithRangesAndValues()
}
),
range: parameter.range,
showsLabel: false
).onAppear {
//Set initial value slider
parametersPerItem[index][i] = Float(parameter.value)
}
}
}
}
}
struct SliderView: View {
var label: String
#Binding var value: Float
var range: [Double]
var showsLabel: Bool
init(label: String, value: Binding<Float>, range: [Double], showsLabel: Bool = true) {
self.label = label
_value = value
self.range = range
self.showsLabel = showsLabel
}
var body: some View {
GeometryReader { geometry in
ZStack{
if showsLabel { Text(label) }
HStack {
Slider(value: $value)
.foregroundColor(.accentColor)
.frame(width: geometry.size.width * 0.8)
//In the real app range calculations are done here
let valueInRange = value
Text("\(valueInRange, specifier: range[1] >= 1000 ? "%.0f" : "%.2f")")
.foregroundColor(.white)
.font(.subheadline)
.frame(width: geometry.size.width * 0.2)
}
}
}
.frame(height: 40.0)
}
}
If you are looking for a solution where you want to initialise the array after the json has been loaded you could add a computed property in an extension to the main/root json model and use it to give the #State property an initial value.
extension MainViewState {
var parametersPerItem: [[Float]] {
var array: [[Float]] = []
if let max = itemStructures.map(\.parameters.count).max(by: { $0 < $1 }) {
for _ in itemStructures {
array.append(Array(repeating: 0.0, count: max))
}
}
return array
}
}

SwiftUI: How to update element in ForEach without necessity to update all elements?

Imagine that you have some parent view that generate some number of child views:
struct CustomParent: View {
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index)
}
}
}
}
struct CustomChild: View {
#State var index: Int
#State private var text: String = ""
var body: some View {
Button(action: {
// Here should be some update of background/text/opacity or whatever.
// So how can I update background/text/opacity or whatever for button with index for example 3 from button with index for example 1?
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Question is included in the code as comment.
Thanks!
UPDATE:
First of all really thanks for all of your answers, but now imagine that you use mentioned advanced approach.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
If I use let _ = Self._printChanges() method in CustomChildView, to catch UI updates/changes, it'll print that every element in ForEach was updated/changed on button action.
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
let _ = Self._printChanges() // This have been added to code
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}
And now imagine that you have for example 1000 custom elements which have some background, opacity, shadow, texts, fonts and so on. Now I change text in any of the elements.
Based on log from let _ = Self._printChanges() method, it goes through all elements, and all elements are updated/changed what can cause delay.
Q1: Why did update/change all elements, if I change text in only one element?
Q2: How can I prevent update/change all elements, if I change only one?
Q3: How to update element in ForEach without necessity to update all elements?
Simpler Approach:
Although child views cannot access things that the host views have, it's possible to declare the child states in the host view and pass that state as a binding variable to the child view. In the code below, I have passed the childTexts variable to the child view, and (for your convenience) initialized the text so that it binds to the original element in the array (so that your onAppear works properly). Every change performed on the text and childTexts variable inside the child view reflects on the host view.
I strongly suggest not to do this though, as more elegant approaches exist.
struct CustomParent: View {
#State var childTexts = [String](repeating: "", count: 10)
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, childTexts: $childTexts)
}
}
}
}
struct CustomChild: View {
let index: Int
#Binding private var text: String
#Binding private var childTexts: [String]
init(index: Int, childTexts: Binding<[String]>) {
self.index = index
self._childTexts = childTexts
self._text = childTexts[index]
}
var body: some View {
Button(action: {
//button behaviors goes here
//for example
childTexts[index + 1] = "A"
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Advanced Approach:
By using the Combine framework, all your logics can be moved into an ObservableObject view model. This is much better as the button logic is no longer inside the view. In simplest terms, the #Published variable in the ObservableObject will publish a change when it senses its own mutation, while the #StateObjectand the #ObservedObject will listen and recalculate the view for you.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}

How to get data from the Model and update it

I have a data model that I want to use its data in various views. So firstly I have problem polling that information from my data model and secondly I have problem to update the data.
Here in one example of my data model with three views.
Data Model
// A basic resocrd of each exercise in a workout
class WorkoutItem:ObservableObject, Identifiable{
var id:Int = 0
var name: String = "An Exercise"
var sets:Int = 3
var reps:Int = 10
func addSet() {
sets += 1
}
init(){
}
}
// The model for holding a workout
class WorkoutModel:ObservableObject{
#Published var workout:[WorkoutItem] = []
var lastID:Int = -1
/// Creates a newID based on the last known ID
private func newId()->Int{
lastID += 1
return lastID
}
func add(name:String, sets:Int, reps:Int){
let newExercise = WorkoutItem()
workout += [newExercise]
}
init() {
add(name: "Psuh Pp", sets: 1, reps: 8)
add(name: "Pull UPs", sets: 1, reps: 10)
}
}
View 1
struct ContentView: View {
#ObservedObject var myWorkout: WorkoutModel
var body: some View {
List(self.myWorkout.workout) { item in
VStack(alignment: .leading) {
VStack(alignment: .leading) {
Text(item.name).font(.headline)
ExerciseEntryView(exItem: item)
}
}
}
}
}
View 2
struct ExerciseEntryView: View {
var exItem: WorkoutItem
var body: some View {
VStack {
VStack(alignment: .leading){
ForEach(0 ..< exItem.sets, id: \.self){ row in
ExerciseEntryView_Row(setNumber: row)
}
}
Button(action: {
self.exItem.addSet()
}) {
Text("Add Set").foregroundColor(Color.red)
}
}
}
}
View 3
struct ExerciseEntryView_Row: View {
var setNumber: Int
var body: some View {
HStack{
Text("Set Number \(setNumber)")
}
}
}
Firstly when running the code, as you can see in the below image the title is still the default value ('An Exercise') while it should Pull up and Push up. Also when I press on add set the set gets updated in the terminal but it does not update the view.
Any idea why this is not functioning?
Thanks in advance.
First you can make your WorkoutItem a struct:
struct WorkoutItem: Identifiable {
var id: Int = 0
var name: String = "An Exercise"
var sets: Int = 3
var reps: Int = 10
mutating func addSet() {
sets += 1
}
}
Then in the WorkoutModel you weren't using setting any properties for the new exercise:
class WorkoutModel: ObservableObject {
...
func add(name: String, sets: Int, reps: Int) {
var newExercise = WorkoutItem()
newExercise.name = name // <- set properties
newExercise.sets = sets
newExercise.reps = reps
workout += [newExercise]
}
init() {
add(name: "Push Pp", sets: 1, reps: 8)
add(name: "Pull UPs", sets: 1, reps: 10)
}
}

How to subclass the #State property wrapper in SwiftUI

I have a #State variable that I that I want to add a certain constraint to, like this simplified example:
#State private var positiveInt = 0 {
didSet {
if positiveInt < 0 {
positiveInt = 0
}
}
}
However this doesn't look so nice (it seems to be working though) but what I really want to do is to subclass or extend the property wrapper #State somehow so I can add this constraint in it's setter. But I don't know how to do that. Is it even possible?
You can't subclass #State since #State is a Struct. You are trying to manipulate your model, so you shouldn't put this logic in your view. You should at least rely on your view model this way:
class ContentViewModel: ObservableObject {
#Published var positiveInt = 0 {
didSet {
if positiveInt < 0 {
positiveInt = 0
}
}
}
}
struct ContentView: View {
#ObservedObject var contentViewModel = ContentViewModel()
var body: some View {
VStack {
Text("\(contentViewModel.positiveInt)")
Button(action: {
self.contentViewModel.positiveInt = -98
}, label: {
Text("TAP ME!")
})
}
}
}
But since SwiftuUI is not an event-driven framework (it's all about data, model, binding and so forth) we should get used not to react to events, but instead design our view to be "always consistent with the model". In your example and in my answer here above we are reacting to the integer changing overriding its value and forcing the view to be created again. A better solution might be something like:
class ContentViewModel: ObservableObject {
#Published var number = 0
}
struct ContentView: View {
#ObservedObject var contentViewModel = ContentViewModel()
private var positiveInt: Int {
contentViewModel.number < 0 ? 0 : contentViewModel.number
}
var body: some View {
VStack {
Text("\(positiveInt)")
Button(action: {
self.contentViewModel.number = -98
}, label: {
Text("TAP ME!")
})
}
}
}
Or even simpler (since basically there's no more logic):
struct ContentView: View {
#State private var number = 0
private var positiveInt: Int {
number < 0 ? 0 : number
}
var body: some View {
VStack {
Text("\(positiveInt)")
Button(action: {
self.number = -98
}, label: {
Text("TAP ME!")
})
}
}
}
You can't apply multiple propertyWrappers, but you can use 2 separate wrapped values. Start with creating one that clamps values to a Range:
#propertyWrapper
struct Clamping<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>
init(wrappedValue value: Value, _ range: ClosedRange<Value>) {
precondition(range.contains(value))
self.value = value
self.range = range
}
var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
}
Next, create an ObservableObject as your backing store:
class Model: ObservableObject {
#Published
var positiveValue: Int = 0
#Clamping(0...(.max))
var clampedValue: Int = 0 {
didSet { positiveValue = clampedValue }
}
}
Now you can use this in your content view:
#ObservedObject var model: Model = .init()
var body: some View {
Text("\(self.model.positiveValue)")
.padding()
.onTapGesture {
self.model.clampedValue += 1
}
}