SwiftUI: Setting Picker selection to last in dynamic array - swiftui

I have a Picker in a detail view that changes the subviews accordingly. I can set it to the first in the array (Index 0), but for the user it would be more logical if it defaulted to the last.
struct ViewDetail: View {
#State public var selectedYearIndex = 0
.
.
.
Picker(selection: $selectedYearIndex, label: PickerLabel()) {
Group {
ForEach(0 ..< yearArray.count)) {
Text(self.yearArray[$0].description).tag($0)
}
}
}
.
.
.
}
Can anyone suggest a way to default the Picker Selection to the last index in an array of variable size?
Thoughts?

I was able to resolve this by adding the following to the view:
.onAppear {
if self.selectedYearIndex == 0 {
self.selectedYearIndex = (yearArray.count - 1)
}
}
This work consistently, and did not affect the functionality of the picker.

Related

SwiftUI Dynamically Create Enum or

I have a custom picker where the contents of the picker are created from a downloaded JSON file. In an effort to provide better accessibility for challenged users, I am trying to use the .accessibilityFocused modifier to focus VO on a user's previous selection. This means that when the user first views the picker it should start at the top, but if they go back in to change their selection, it should auto-focus on their previous selection instead of having them start over from the top of the list.
The issue is that to use .accessibilityFocused you really need to do so via an enum so you can call something like .accessibilityFocused($pickerAccessFocus, equals: .enumValue). As I am using a ForEach loop to create the picker based off of the array that is created when the JSON is parsed and then stored in the struct, I haven't figured out how to create the various enum cases based off of that array.
So, to recap:
Inside the ForEach loop, I need to have a .accessibilityFocused modifier identifying each of the picker options
The onAppear needs to say something along the lines of...
if salutation == salutation.salutation {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
pickerAccessFocus = .ENUMCASE
}
} else {
pickerAccessFocus = Optional.none
}
Though apparently onAppear does not like to have a ForEach statement, so I'm not sure how to solve that issue either.
I know there's a lot of code in the example I provided, but I tried to combine it all into as simple as example as possible. Hoping it makes sense.
If I'm thinking about it wrong, and there's a better solution, I'm all ears.
import SwiftUI
struct Picker: View {
#AccessibilityFocusState var pickerAccessFocus: PickerAccessFocus?
#State private var salutation = ""
var salutationList: [SalutationOptions] = [] // SalutationOptions is the struct from the parsed JSON
enum PickerAccessFocus: Hashable {
case ? // These cases need to be dynamically created as the same values the ForEach loop uses
}
static func nameSalutationPicker(name: String) -> LocalizedStringKey { LocalizedStringKey(String("NAME_SALUTATION_PICKER_\(name)")) }
var body: some View {
List {
Section {
ForEach(salutationList, id: \.id) { salutation in
HStack {
Text(nameSalutationPicker(name: salutation.salutation))
} // End HStack
.contentShape(Rectangle())
.accessibilityFocused(salutation == salutation.salutation ? ($pickerAccessFocus, equals: .ENUMCASE) : ($pickerAccessFocus, equals: Optional.none))
} // End ForEach
} // End Section
} // End List
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
pickerAccessFocus = .ENUMCASE
}
}
}
}
You're making it much more complicated than necessary. You don't need an enum, you can use any value, even your array values directly.
struct ContentView: View {
#AccessibilityFocusState var pickerAccessFocus: SalutationOptions?
#State private var salutation = ""
var salutationList: [SalutationOptions] = []
var body: some View {
List {
Section {
ForEach(salutationList, id: \.id) { salutation in
HStack {
Text(salutation.salutation)
} // End HStack
.contentShape(Rectangle())
.accessibilityFocused($pickerAccessFocus, equals: salutation)
} // End ForEach
} // End Section
} // End List
}
}

SwiftUI on macOS - using Picker for each row in a Table Column

I am displaying some items in a Table on macOS (new in SwiftUI 3.0). I can get the table to work fine when displaying Text views in each column, but I really want to show a Picker view in one of the columns. But I'm not sure how I would 'bind' the selection of the picker to an item in the array that I use to drive the list of items.
Here's the code I'm trying:
struct TestSwiftUIView: View {
#State var mappingFields: [ImportMappingField]
var body: some View {
VStack {
Table(mappingFields) {
TableColumn("Imported Field") { item in
Text("\(item.columnName)")
}.width(160)
TableColumn("Mapped Field") { item in
Text("\(item.fieldName.typeNameAndDescription.description)")
}.width(150)
TableColumn("Type") { item in
Picker("", selection: $item.selectedCustomFieldType){ // error here
ForEach(ImportMappingType.allCases, id:\.self ) { i in
Text(i)
}
}
}.width(100)
}
}
}
}
In selection: $item.selectedCustomFieldType I get an error that says "Cannot find '$item' in scope".
So I want to bind each picker to an element in each 'item', but it doesn't let me do that.
Is there a good solution for this problem?
try using this approach to give you the item binding you need:
Table($mappingFields) { // <-- here
TableColumn("Type") { $item in // <-- here
Picker("", selection: $item.selectedCustomFieldType){
...
}
similarly for the other TableColumn. You may have to make ImportMappingField conform to Identifiable

SwiftUI - Are views kept alive while navigating?

I have a bug when using NavigationLinks and an ObservableObject. I don't quite understand why because I don't understand what is happening to the views and data as I am navigating. This is some pseudo-code to illustrate the problem:
class Settings: ObservableObject {
#Published var data: [Int] = [0, 1, 2, 3, 4, 5]
}
struct ContentView: View {
#State var new_view: Bool = false
#ObservedObject var content_view_settings = Settings()
var body: some View {
NavigationView {
VStack {
Button(action: {
DeleteLastItem()
}) {
Text("Delete last item")
}
Button(action: {
self.new_view = true
}) {
Text("New View")
}
NavigationLink(destination: NewView(new_view_settings: content_view_settings), isActive: $new_view) {
EmptyView()
}
}
}
}
}
struct NewView: View {
#ObservedObject var new_view_settings: Settings
#State var index = -1
var body: some View {
VStack {
Button(action: {
self.index = self.new_view_settings.count - 1
}) {
Text("change index")
}
if self.index > -1 {
Text("\(self.new_view_settings.data[index])")
}
}
}
}
The description of the problem is this:
I have a view with an ObservedObject that I pass to a subsequent view upon navigating. This sub-view accesses the last element of the array, but it only does that once the index variable is validated through a button click. The text is then rendered only after the index is validated.
Now, suppose I validate the index so it would equal 5 in this example. Then I navigate back to the original view. If I delete the last element, the index 5 is no longer valid. As soon as I delete that last element I get an invalid index error and the simulator crashes.
But let's say I navigate backward and do not delete the last element. Then when I navigate forward, the index variable is reset.
Since I get the crash, this means the view is still alive and being updated or something but when I navigate to it once again the view is reloaded. Does this mean the view is alive until it gets initialized again? This is contrived code but it is essentially the issue I am having. I thought the original code would be a bit harder to understand.
Does this mean the view is alive until it gets initialized again?
Yes, the view may be alive even after you navigate back to the parent view.
To better understand what's happening run the same code on the iPad simulator (preferably in the horizontal mode). You'll notice that the NavigationView is split in two parts: master and detail - this way you can see both parent and child view at once.
Now, if you perform the same experiment from your question, you'll see the child view remains present even if you navigate back. The same happens on iOS.
One way to prevent this can be to check if indices are present in the array:
struct NewView: View {
#ObservedObject var new_view_settings: Settings
#State var index = -1
var body: some View {
VStack {
Button(action: {
//self.index = self.new_view_settings.count - 1
}) {
Text("change index")
}
// check if `index` is in array
if self.index > -1 && self.index < self.new_view_settings.data.count {
Text("\(self.new_view_settings.data[index])")
}
}
}
}
Note: in general I don't recommend dealing with indices in SwiftUI views - there usually is a better way to pass data. Dealing with indices is risky.

Why doesn't my swift ui view update after I change a State var?

I'm new to Swift development, and I'm trying to make a View, where you can click an item and it gets bigger, while the old big item gets smaller. I'm using an #State var called chosen to know which Element should be big at the moment. The items itself are Views with a Button on top. The idea is, that I click the button and the button will change the chosen variable, which is working. But it seems that my view doesn't redraw itself and everything stays as is. The simplified pseudocode looks like this:
struct MyView: View {
#State var chosen = 0
var body: some View {
VStack(){
ForEach(0 ..< 4) { number in
if self.chosen == number {
DifferentView()
.frame(big)
.clipShape(big)
}else{
ZStack{
DifferentView()
.frame(small)
.clipShape(small)
Button(action: {self.chosen = number}){Rectangle()}
}
}
}
}
}
You're using this overload of ForEach.init(_:content:), which accepts a constant range. While your range doesn't change, it also appears to be that this ForEach variant doesn't update the content (it was surprising to me).
You need to use the following overload: ForEach.init(_:id:content:) - supplying id with a keypath:
ForEach(0 ..< 4, id: \.self) { number in
// ...
}
But because there is a conditional, it trips up SwiftUI (hard to know why). The way to avoid it is to wrap it in something, like a Group or a ZStack, or even a function that generates the inner view:
ForEach(0 ..< 4, id: \.self) { number in
Group {
self.chosen == number {
// ...
} else {
// ...
}
}
}
Or, like so:
ForEach(0 ..< 4, id: \.self) { number in
self.inner(for: number)
}
#ViewBuilder
func inner(for number: Int) -> some View {
self.chosen == number {
// ...
} else {
// ...
}
}

View is not rerendered in Nested ForEach loop

I have the following component that renders a grid of semi transparent characters:
var body: some View {
VStack{
Text("\(self.settings.numRows) x \(self.settings.numColumns)")
ForEach(0..<self.settings.numRows){ i in
Spacer()
HStack{
ForEach(0..<self.settings.numColumns){ j in
Spacer()
// why do I get an error when I try to multiply i * j
self.getSymbol(index:j)
Spacer()
}
}
Spacer()
}
}
}
settings is an EnvironmentObject
Whenever settings is updated the Text in the outermost VStack is correctly updated. However, the rest of the view is not updated (Grid has same dimenstions as before). Why is this?
Second question:
Why is it not possible to access the i in the inner ForEach-loop and pass it as a argument to the function?
I get an error at the outer ForEach-loop:
Generic parameter 'Data' could not be inferred
TL;DR
Your ForEach needs id: \.self added after your range.
Explanation
ForEach has several initializers. You are using
init(_ data: Range<Int>, #ViewBuilder content: #escaping (Int) -> Content)
where data must be a constant.
If your range may change (e.g. you are adding or removing items from an array, which will change the upper bound), then you need to use
init(_ data: Data, id: KeyPath<Data.Element, ID>, content: #escaping (Data.Element) -> Content)
You supply a keypath to the id parameter, which uniquely identifies each element that ForEach loops over. In the case of a Range<Int>, the element you are looping over is an Int specifying the array index, which is unique. Therefore you can simply use the \.self keypath to have the ForEach identify each index element by its own value.
Here is what it looks like in practice:
struct ContentView: View {
#State var array = [1, 2, 3]
var body: some View {
VStack {
Button("Add") {
self.array.append(self.array.last! + 1)
}
// this is the key part v--------v
ForEach(0..<array.count, id: \.self) { index in
Text("\(index): \(self.array[index])")
//Note: If you want more than one views here, you need a VStack or some container, or will throw errors
}
}
}
}
If you run that, you'll see that as you press the button to add items to the array, they will appear in the VStack automatically. If you remove "id: \.self", you'll see your original error:
`ForEach(_:content:)` should only be used for *constant* data.
Instead conform data to `Identifiable` or use `ForEach(_:id:content:)`
and provide an explicit `id`!"
ForEach should only be used for constant data. So it is only evaluated once by definition. Try wrapping it in a List and you will see errors being logged like:
ForEach, Int, TupleView<(Spacer, HStack, Int, TupleView<(Spacer, Text, Spacer)>>>, Spacer)>> count (7) != its initial count (0). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
I was surprised by this as well, and unable to find any official documentation about this limitation.
As for why it is not possible for you to access the i in the inner ForEach-loop, I think you probably have a misleading compiler error on your hands, related to something else in the code that is missing in your snippets. It did compile for me after completing the missing parts with a best guess (Xcode 11.1, Mac OS 10.14.4).
Here is what I came up with to make your ForEach go over something Identifiable:
struct SettingsElement: Identifiable {
var id: Int { value }
let value: Int
init(_ i: Int) { value = i }
}
class Settings: ObservableObject {
#Published var rows = [SettingsElement(0),SettingsElement(1),SettingsElement(2)]
#Published var columns = [SettingsElement(0),SettingsElement(1),SettingsElement(2)]
}
struct ContentView: View {
#EnvironmentObject var settings: Settings
func getSymbol(index: Int) -> Text { Text("\(index)") }
var body: some View {
VStack{
Text("\(self.settings.rows.count) x \(self.settings.columns.count)")
ForEach(self.settings.rows) { i in
VStack {
HStack {
ForEach(self.settings.columns) { j in
Text("\(i.value) \(j.value)")
}
}
}
}
}
}
}