SwiftUI PickerView with working CallBack HOW? - swiftui

Ok -
I want a picker view to pick one operator: "=","<",">"
This operator will be sent as a binding:
#Binding var op:String
My Picker:
Picker(selection: binding, label: Text("Query Type")) {
ForEach(0..<self.operators.count) { index in
Text(self.operators[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
.padding()
Now My Binding with CallBack:
let binding = Binding<Int>(
get: {
return self.pickerSelection
},
set: {
//pickerSelection = $0
print("SETTTING: \($0)")
self.op = self.operators[self.pickerSelection]
self.queryCallback()
})
So, I can set the pickers perfectly. BUT, when I go back to edit my data, the picker never can choose the existing bound operator, say "<"
I put in the init an:
pickerSelection = operators.firstIndex(opValue)
However this will just start an infinite loop as pickerSelection is a #State variable
Anyone have a solution?

This is a method that works. It uses Combine to make an observable one can use to trigger the needed events. Also I see how useful Combine is with SwiftUI
https://stackoverflow.com/a/57519105/810300

Related

Cannot use instance member '' within property initializer OnAppear does not working as well

I am trying to create a LazyVGrid based on user selection in another view. As follows, the peoplelist and selectedpersonID are coming from other view.
I understand that I cannot use the "selectedpersons" during initializing of this view. I looked here(Cannot use instance member 'service' within property initializer; property initializers run before 'self' is available) to use onAppear() of the LazyVGrid.
It goes well during compiling and works ok if you select 1 person.
Once I selected 2 persons, I got a Fatal error that Index out of range at row.
struct Someview: View {
#ObservedObject var peoplelist : PersonList
let selectedpersonID : Set<UUID>?
#State private var days : [String] = Array(repeating: "0", count: selectedpersons.count * 5) //got first error here, during compiling
var body: some View {
VStack{
LazyVGrid(columns: columns) {
Text("")
ForEach(1..<6) { i in
Text("\(i)").bold()
}
ForEach(0..< selectedpersons.count , id: \.self) { row in
Text(selectedpersons[row].Name)
ForEach(0..<5) { col in
TextField("", text: $days[row * 5 + col])
}
}
}
.onAppear(){
days = Array(repeating: "0", count: selectedpersons.count * 5)}//no problem during compiling, but will have error when more than 1 person are selected.
.padding()
}
}
var selectedpersons: [Persons] {
return peoplelist.persons.filter {selectedpersonID!.contains($0.id)}
}
}
It seems to me that this OnAppear() is still slower than the content inside the LazyVGrid? So, the days is not changed quick enough for building the content insider the LazyVGrid?
Or did I make an error of the index in the array of days?
It's crashing because ForEach isn't a for loop its a View that needs to be supplied Identifiable data. If you're using indices, id: \self or data[index] then something has gone wrong. There are examples of how to use it correctly in the documentation.
Also onAppear is for performing a side-effect action when the UIView that SwiftUI manages appears, it isn't the correct place to set view data, the data should be already in the correct place when the View struct is init. Making custom Views is a good way to solve this.

ForEach: ID parameter and closure return type

So, I'm going through the SwiftUI documentation to get familiar. I was working on a grid sample app. It has the following code:
ForEach(allColors, id: \.description) { color in
Button {
selectedColor = color
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(color)
}
.buttonStyle(.plain)
}
It didn't occur to me first that ForEach is actually a struct, I thought it's a variation of the for in loop at first so I'm quite new at this. Then I checked the documentation.
When I read the documentation and some google articles for the ForEach struct, I didn't understand two points in the code:
So we are initializing the foreach struct with an array of colors. For the the ID why did they use .\description instead of .self?
Second is using color in. Since foreach is a struct and the paranthesis is the initializtion parameters this looks like the return type of a closure but why would we return individual colors to foreach? I thought the return is a collection of views or controls like button and label. This is like var anInteger: Int = 1 for example. What type does ForEach accept as a result of the closure? Or am I reading this all wrong?
So we are initializing the foreach struct with an array of colors. For the the ID why did they use .\description instead of .self?
It depends on the type of allColors. What you should have in mind that id here is expected to be stable. The documentation states:
It’s important that the id of a data element doesn’t change unless you replace the data element with a new data element that has a new identity. If the id of a data element changes, the content view generated from that data element loses any current state and animations.
So for example if colors are reference types (which are identifiable) and you swap one object with an identical one (in terms of field values), the identity will change, whereas description wouldn't (for the purposes of this example - just assuming intentions of code I have no access to).
Edit: Also note that in this specific example allColors appears to be a list of Color, which is not identifiable. So that's the reason behind the custom id keyPath.
Regarding your second point, note that the trailing closure is also an initialization parameter. To see this clearly we could use the "non-sugared" version:
ForEach(allColors, id: \.description, content: { color in
Button {
selectedColor = color
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(color)
}
.buttonStyle(.plain)
})
where content is a closure (an anonymous function) that gets passed an element of the collection and returns some View.
So the idea is something like this: "Give me an collection of identifiable elements and I will call a function for each of these elements expecting from you to return me some View".
I hope that this makes (some) sense.
Additional remarks regarding some of the comments:
It appears to me that the main source of confusion is the closure itself. So let's try something else. Let's write the same code without a closure:
ForEach's init has this signature:
init(_ data: Data, id: KeyPath<Data.Element, ID>, content: #escaping (Data.Element) -> Content)
Now, the content translates to:
A function with one parameter of type Data.Element, which in our case is inferred from the data so it is a Color. The function's return type is Content which is a view builder that produces some View
so our final code, which is equivalent to the first one, could look like this:
struct MyView: View {
let allColors: [Color] = [.red, .green, .blue]
#State private var selectedColor: Color?
var body: some View {
List {
ForEach(allColors, id: \.description, content: colorView)
}
}
#ViewBuilder
func colorView(color: Color) -> some View {
Button {
selectedColor = color
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(color)
}
.buttonStyle(.plain)
}
}
I hope that this could help to clarify things a little bit better.

Changing swipeActions dynamically in SwiftUI

I am trying to change the swipeAction from "Paid" to "UnPaid" based on payment status and somehow seems to be failing. Error: "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
Appreciate any help
struct ContentView: View {
var data: [Data] = [data1, data2, data3, data4]
#State var swipeLabel = true
var body: some View {
let grouped = groupByDate(data)
List {
ForEach(Array(grouped.keys).sorted(by: >), id: \.self) { date in
let studentsDateWise = grouped[date]!
Section(header:Text(date, style: .date)) {
ForEach(studentsDateWise, id:\.self) { item in
HStack {
Text(item.name)
padding()
Text(item.date, style: .time)
if(item.paymentStatus == false) {
Image(systemName: "person.fill.questionmark")
.foregroundColor(Color.red)
} else {
Image(systemName: "banknote")
.foregroundColor(Color.green)
}
} // HStack ends here
.swipeActions() {
if(item.paymentStatus) {
Button("Paid"){}
} else {
Button("UnPaid"){}
}
}
} // ForEach ends here...
} // section ends here
} // ForEach ends here
} // List ends here
} // var ends here
}
The body func shouldn't do any grouping or sorting. You need to prepare your data first into properties and read from those in body, e.g. in an onAppear block. Also if your Data is a struct you can't use id: \.self you need to either specify a unique identifier property on the data id:\.myUniqueID or implement the Indentifiable protocol by either having an id property or an id getter that computes a unique identifier from other properties.
I would suggest separating all this code into small Views with a small body that only uses one or a two properties. Work from bottom up. Then eventually with one View works on an array of dates and another on an array of items that contains the small Views made earlier.
You should probably also learn that if and foreach in body are not like normal code, those are converted into special Views. Worth watching Apple's video Demystify SwiftUI to learn about structural identity.

ForEach - Index out of range?

Why running this code shows "Fatal error: Index out of range"?
import SwiftUI
struct MyData {
var numbers = [Int](repeating: 0, count: 5)
}
#main
struct TrySwiftApp: App {
#State var myData = MyData()
var body: some Scene {
WindowGroup {
ChildView(myData: myData)
.frame(width: 100, height: 100)
.onAppear {
myData.numbers.removeFirst() // change myData
}
}
}
}
struct ChildView: View {
let myData: MyData // a constant
var body: some View {
ForEach(myData.numbers.indices) {
Text("\(myData.numbers[$0])") // Thread 1: Fatal error: Index out of range
}
}
}
After checking other questions,
I know I can fix it by following ways
// fix 1: add id
ForEach(myData.numbers.indices, id: \.self) {
//...
}
or
// Edited:
//
// This is not a fix, see George's reply
//
// fix 2: make ChildView conforms to Equatable
struct ChildView: View, Equatable {
static func == (lhs: ChildView, rhs: ChildView) -> Bool {
rhs.myData.numbers == rhs.myData.numbers
}
...
My Questions:
How a constant value (defined by let) got out of sync?
What ForEach really did?
Let me give you a simple example to show you what happened:
struct ContentView: View {
#State private var lowerBound: Int = 0
var body: some View {
ForEach(lowerBound..<11) { index in
Text(String(describing: index))
}
Button("update") { lowerBound = 5 }.padding()
}
}
if you look at the upper code you would see that I am initializing a ForEach JUST with a Range like this: lowerBound..<11 which it means this 0..<11, when you do this you are telling SwiftUI, hey this is my range and it will not change! It is a constant Range! and SwiftUI says ok! if you are not going update upper or lower bound you can use ForEach without showing or given id! But if you see my code again! I am updating lowerBound of ForEach and with this action I am breaking my agreement about constant Range! So SwiftUI comes and tell us if you are going update my ForEach range in count or any thing then you have to use an id then you can update the given range! And the reason is because if we have 2 same item with same value, SwiftUI would have issue to know which one you say! with using an id we are solving the identification issue for SwiftUI! About id you can use it like this: id:\.self or like this id:\.customID if your struct conform to Hash-able protocol, or in last case you can stop using id if you confrom your struct to identifiable protocol! then ForEach would magically sink itself with that.
Now see the edited code, it will build and run because we solved the issue of identification:
struct ContentView: View {
#State private var lowerBound: Int = 0
var body: some View {
ForEach(lowerBound..<11, id:\.self) { index in
Text(String(describing: index))
}
Button("update") { lowerBound = 5 }.padding()
}
}
Things go wrong when you do myData.numbers.removeFirst(), because now myData.numbers.indices has changed and so the range in the ForEach showing Text causes problems.
You should see the following warning (at least I do in Xcode 13b5) hinting this could cause issues:
Non-constant range: not an integer range
The reason it is not constant is because MyData's numbers property is a var, not let, meaning it can change / not constant - and you do change this. However the warning only shows because you aren't directly using a range literal in the ForEach initializer, so it assumes it's not constant because it doesn't know.
As you say, you have some fixes. Solution 1 where you provide id: \.self works because now it uses a different initializer. Definition for the initializer you are using:
#available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ForEach where Data == Range<Int>, ID == Int, Content : View {
/// Creates an instance that computes views on demand over a given constant
/// range.
///
/// The instance only reads the initial value of the provided `data` and
/// doesn't need to identify views across updates. To compute views on
/// demand over a dynamic range, use ``ForEach/init(_:id:content:)``.
///
/// - Parameters:
/// - data: A constant range.
/// - content: The view builder that creates views dynamically.
public init(_ data: Range<Int>, #ViewBuilder content: #escaping (Int) -> Content)
}
Stating:
The instance only reads the initial value of the provided data and doesn't need to identify views across updates. To compute views on demand over a dynamic range, use ForEach/init(_:id:content:).
So that's why your solution 1 worked. You switched to the initializer which didn't assume the data was constant and would never change.
Your solution 2 isn't really a "solution". It just doesn't update the view at all, because myData.numbers changes so early that it is always equal, so the view never updates. You can see the view still has 5 lines of Text, rather than 4.
If you still have issues with accessing the elements in this ForEach and get out-of-bounds errors, this answer may help.

Is it possible to set a character limit on a TextField using SwiftUI?

[RESOLVED]
I am using a codable struct which stores the object values retrieved from an API call so I have amended my TextField using Cenk Belgin's example, I've also removed extra bits I've added in so if anyone else is trying to do the same thing then they won't have pieces of code from my app that aren't required.
TextField("Product Code", text: $item.ProdCode)
.onReceive(item.ProdCode.publisher.collect()) {
self.item.ProdCode = String($0.prefix(5))
}
Here is one way, not sure if it was mentioned in the other examples you gave:
#State var text = ""
var body: some View {
TextField("text", text: $text)
.onReceive(text.publisher.collect()) {
self.text = String($0.prefix(5))
}
}
The text.publisher will publish each character as it is typed. Collect them into an array and then just take the prefix.
From iOS 14 you can add onChange modifier to the TextField and use it like so :
TextField("Some Placeholder", text: self.$someValue)
.onChange(of: self.someValue, perform: { value in
if value.count > 10 {
self.someValue = String(value.prefix(10))
}
})
Works fine for me.
You can also do it in the Textfield binding directly:
TextField("Text", text: Binding(get: {item.ProCode}, set: {item.ProCode = $0.prefix(5).trimmingCharacters(in: .whitespacesAndNewlines)}))