Table selection issue - swift

I'm having this weird issue with the new Table introduced in Xcode 13, where I cannot programmatically select the first row. (But other rows work fine)
Reproduction:
Make sure that macOS Montery and Xcode 13 Beta 2 are installed
Paste the following code in a new blank SwiftUI project, with its deployment target set to 12.0 (macOS Montery):
struct ContentView: View {
#State var data: [TestStruct] = []
#State var selection = Set<UUID>()
var body: some View {
Table(data, selection: $selection) {
TableColumn("column", value: \.text)
}
.toolbar {
Button(action: {
data.append(TestStruct(text: "Test"))
selection = Set([data.last!.id])
}) {
Text("Add")
}
}
}
}
struct TestStruct: Identifiable {
var text: String
var id = UUID()
}
Run the app, and press the "Add" button
Observe that no selection takes place, but when you press it again, a selection should appear
How would you fix this issue? Or is this a bug on Apple's part?

try something like this:
struct ContentView: View {
#State var data: [TestStruct] = []
// #State var selection = Set<UUID>() // <--- for multiple selections
#State var selection: UUID? = nil // <--- if you want just 1 selection
var body: some View {
Table(data, selection: $selection) {
TableColumn("column", value: \.text)
}
.toolbar {
Button(action: {
data.append(TestStruct(text: "Test"))
if let last = data.last {
DispatchQueue.main.async {
// selection.insert(last.id)
selection = last.id
}
}
}) {
Text("Add")
}
}
}
}

Related

displaying alert on picker select SwiftUI

i want to display an alert box when a user selects something in a form picker the user then have to confirm their choice before the value changes.
right now my code looks like this(just from some tutorial):
NavigationView {
Form {
Section {
Picker("Strength", selection: $selectedStrength) {
ForEach(strengths, id: \.self) {
Text($0)
}
}
}
}
}
I have tried using onChange() but ideally the check should be before the value changes.
If you want to ask the user to confirm before the selection changes you would need to implement a custom binding with a second var. With this you would be able to cancel the selection if necessary.
struct Test: View{
#State private var selectedStrength: String = ""
#State private var askForStrength: String = ""
#State private var ask: Bool = false
let strengths = ["1","2","3"]
var body: some View{
NavigationView {
Form {
Section {
Picker("Strength", selection: Binding(get: {selectedStrength}, set: {
//assign the selection to the temp var
askForStrength = $0
// show the Alert
ask = true
})) {
ForEach(strengths, id: \.self) {
Text($0)
}
}
}
}
}.alert(isPresented: $ask) {
// Here ask the user if selection is correct and apply the temp var to the selection
Alert(title: Text("select?"), message: Text("Do you want to select \(askForStrength)"), primaryButton: .default(Text("select"), action: {selectedStrength = askForStrength}), secondaryButton: .cancel())
}
}
}
You can do it this way; having an alert after the value is clicked and a temp variable for storing pre-selected data. Code is below the image:
import SwiftUI
struct ContentView: View {
let animals = ["dog", "cat", "pig"]
#State var selected = ""
#State var finalResult = ""
#State var alert = false
var body: some View {
NavigationView {
Form {
Section {
Picker("Animals", selection: $selected) {
ForEach(animals, id: \.self) {
Text($0)
}
}
.onChange(of: selected) { _ in
alert.toggle()
}
Text("You have confirmed to select this: \(finalResult)")
}
.alert("Confirm selection?", isPresented: $alert) {
Button("Confirm", role: .destructive) {
finalResult = selected
}
}
}
}
}
}

SwiftUI Navigation: How to switch detail view to a different item?

I'm struggling implementing the following navigation behavior:
From a list the user can select an item which triggers a detail view for this item. On this detail view there is an "Add" button in the navigation bar which opens a modal sheet for adding an other item.
Up to this point, everything works as expected.
But after adding the item, I want the detail view to show the new item. I tried to set the list selection to the id of the new item. This triggers the detail view to disappear, the list selects the new item and show the details for a very short time, then the detail view disappears again and the list is shown.
I've tried adding a bridged binding and let the list view not set the selection to nil, this solves the issue at first, but then the "Back" button isn't working anymore.
Please note: I want the "Add" button on the detail view and not on the list view as you would expect it.
Here's the full code to test:
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject private var state = AppState.shared
var body: some View {
NavigationView {
List(state.items) {item in
NavigationLink(destination: DetailView(item: item), tag: item.id, selection: self.$state.selectedId) {
Text(item.title)
}
}
.navigationBarTitle("Items")
}
}
}
struct DetailView: View {
var item: Item
#State private var showForm = false
var body: some View {
Text(item.title)
.navigationBarItems(trailing: Button("Add") {
self.showForm = true
})
.sheet(isPresented: $showForm, content: { FormView() })
}
}
struct FormView: View {
#Environment(\.presentationMode) private var presentationMode
private var state = AppState.shared
var body: some View {
Button("Add") {
let id = self.state.items.count + 1
self.state.items.append(Item(id: id, title: "Item \(id)"))
self.presentationMode.wrappedValue.dismiss()
self.state.selectedId = id
}
}
}
class AppState: ObservableObject {
static var shared = AppState()
#Published var items: [Item] = [Item(id: 1, title: "Item 1")]
#Published var selectedId: Int?
}
struct Item: Identifiable {
var id: Int
var title: String
}
In your scenario it is needed to make navigation link destination independent, so it want be reactivated/invalidated when destination changed.
Here is possible approach. Tested with Xcode 11.7 / iOS 13.7
Updated code only:
struct ContentView: View {
#ObservedObject private var state = AppState.shared
#State private var isActive = false
var body: some View {
NavigationView {
List(state.items) {item in
HStack {
Button(item.title) {
self.state.selectedId = item.id
self.isActive = true
}
Spacer()
Image(systemName: "chevron.right").opacity(0.5)
}
}
.navigationBarTitle("Items")
.background(NavigationLink(destination: DetailView(), isActive: $isActive) { EmptyView() })
}
}
}
struct DetailView: View {
#ObservedObject private var state = AppState.shared
#State private var showForm = false
#State private var fix = UUID() // << fix for known issue with bar button misaligned after sheet
var body: some View {
Text(state.selectedId != nil ? state.items[state.selectedId! - 1].title : "")
.navigationBarItems(trailing: Button("Add") {
self.showForm = true
}.id(fix))
.sheet(isPresented: $showForm, onDismiss: { self.fix = UUID() }, content: { FormView() })
}
}

SwiftUI out of index when deleting an array element in ForEach

I looked through different questions here, but unfortunately I couldn't find an answer. This is my code:
SceneDelegate.swift
...
let contentView = ContentView(elementHolder: ElementHolder(elements: ["abc", "cde", "efg"]))
...
window.rootViewController = UIHostingController(rootView: contentView)
ContentView.swift
class ElementHolder: ObservableObject {
#Published var elements: [String]
init(elements: [String]) {
self.elements = elements
}
}
struct ContentView: View {
#ObservedObject var elementHolder: ElementHolder
var body: some View {
VStack {
ForEach(self.elementHolder.elements.indices, id: \.self) { index in
SecondView(elementHolder: self.elementHolder, index: index)
}
}
}
}
struct SecondView: View {
#ObservedObject var elementHolder: ElementHolder
var index: Int
var body: some View {
HStack {
TextField("...", text: self.$elementHolder.elements[self.index])
Button(action: {
self.elementHolder.elements.remove(at: self.index)
}) {
Text("delete")
}
}
}
}
When pressing on the delete button, the app is crashing with a Index out of bounds error.
There are two strange things, the app is running when
1) you remove the VStack and just put the ForEach into the body of the ContentView.swift or
2) you put the code of the SecondView directly to the ForEach
Just one thing: I really need to have the ObservableObject, this code is just a simplification of another code.
UPDATE
I updated my code and changed Text to a TextField, because I cannot pass just a string, I need a connection in both directions.
The issue arises from the order in which updates are performed when clicking the delete button.
On button press, the following will happen:
The elements property of the element holder is changed
This sends a notification through the objectWillChange publisher that is part of the ElementHolder and that is declared by the ObservableObject protocol.
The views, that are subscribed to this publisher receive a message and will update their content.
The SecondView receives the notification and updates its view by executing the body getter.
The ContentView receives the notification and updates its view by executing the body getter.
To have the code not crash, 3.1 would have to be executed after 3.2. Though it is (to my knowledge) not possible to control this order.
The most elegant solution would be to create an onDelete closure in the SecondView, which would be passed as an argument.
This would also solve the architectural anti-pattern that the element view has access to all elements, not only the one it is showing.
Integrating all of this would result in the following code:
struct ContentView: View {
#ObservedObject var elementHolder: ElementHolder
var body: some View {
VStack {
ForEach(self.elementHolder.elements.indices, id: \.self) { index in
SecondView(
element: self.elementHolder.elements[index],
onDelete: {self.elementHolder.elements.remove(at: index)}
)
}
}
}
}
struct SecondView: View {
var element: String
var onDelete: () -> ()
var body: some View {
HStack {
Text(element)
Button(action: onDelete) {
Text("delete")
}
}
}
}
With this, it would even be possible to remove ElementHolder and just have a #State var elements: [String] variable.
Here is possible solution - make body of SecondView undependable of ObservableObject.
Tested with Xcode 11.4 / iOS 13.4 - no crash
struct SecondView: View {
#ObservedObject var elementHolder: ElementHolder
var index: Int
let value: String
init(elementHolder: ElementHolder, index: Int) {
self.elementHolder = elementHolder
self.index = index
self.value = elementHolder.elements[index]
}
var body: some View {
HStack {
Text(value) // not refreshed on delete
Button(action: {
self.elementHolder.elements.remove(at: self.index)
}) {
Text("delete")
}
}
}
}
Another possible solution is do not observe ElementHolder in SecondView... for presenting and deleting it is not needed - also no crash
struct SecondView: View {
var elementHolder: ElementHolder // just reference
var index: Int
var body: some View {
HStack {
Text(self.elementHolder.elements[self.index])
Button(action: {
self.elementHolder.elements.remove(at: self.index)
}) {
Text("delete")
}
}
}
}
Update: variant of SecondView for text field (only changed is text field itself)
struct SecondViewA: View {
var elementHolder: ElementHolder
var index: Int
var body: some View {
HStack {
TextField("", text: Binding(get: { self.elementHolder.elements[self.index] },
set: { self.elementHolder.elements[self.index] = $0 } ))
Button(action: {
self.elementHolder.elements.remove(at: self.index)
}) {
Text("delete")
}
}
}
}

List selection as Set<String> - how to use?

Am playing around with SwiftUI and am obviously not getting it.
Basic example which works and is just displaying the selected name.
struct ContentView: View {
let names = ["Joe", "Jim", "Paul"]
#State var selectedName = Set<String>()
var body: some View {
VStack {
List(names, id: \.self, selection: $selectedName) { name in
Text(name)
}
if !selectedName.isEmpty {
Text(selectedName.first!) // <-- this line
}
}
}
}
What I want is a textfield where that name can be changed. Tried many ways but getting another error every time.
TextField("Name", text: $selectedName)
Gives this error: Cannot convert value of type 'Binding<Set<String>>' to expected argument type 'Binding<String>'
TextField("Name", text: $selectedName.first!)
Cannot force unwrap value of non-optional type 'Binding<((String) throws -> Bool) throws -> String?>'
How would I do this?
You may make a binding by yourself:
TextField("Name", text: Binding<String>(get: {self.selectedName.first!}, set: { _ in}) )
Obviously you can't pass Binding<Set<String>> to Binding<String>. Here gives you an idea or solution to change selectedName variable using TextField:
I added a new variable which is Binding<String>. Then I change the selectedName inside the TextField's onCommit closure.
struct ContentView: View {
let names = ["Joe", "Jim", "Paul"]
#State var selectedName = Set<String>()
#State var textFieldName = ""
var body: some View {
VStack {
List(names, id: \.self, selection: $selectedName) { name in
Text(name)
}
if !selectedName.isEmpty {
Text(selectedName.first!)
}
Text(textFieldName)
TextField("Name", text: $textFieldName, onEditingChanged: { (Bool) in
//onEditing
}) {
//onCommit
self.selectedName.insert(self.textFieldName)
}
}
}
}
Ok, here is my alternate if I'd needed to edit some value of names having in one screen and list and edit field and make them all synchronised and not confuse each other.
Here is full testable module (tested on Xcode 11.2/iOS 13.2). As I tested it for iOS there are API requirement for put List into EditMode to process selection, so this included.
struct TestChangeSelectedItem: View {
#State var names = ["Joe", "Jim", "Paul"] // made modifiable
#State var selectedName: String? = nil // only one can be edited, so single selection
#State var editMode: EditMode = .active // Tested for iOS, so it is needed
var body: some View {
VStack {
List(selection: $selectedName) {
ForEach(names, id: \.self) { name in
Text(name)
}
}
.environment(\.editMode, $editMode) // Tested for iOS, so it is needed
if selectedName != nil {
Divider()
Text(selectedName!) // Left to see updates for selection
editor(for: selectedName!) // Separated to make more clear
}
}
}
private func editor(for selection: String) -> some View {
let index = names.firstIndex(of: selection)!
var editedValue = selection // local to avoid cycling in refresh
return HStack {
Text("New name:")
TextField("Name", text: Binding<String>(get: { editedValue }, set: { editedValue = $0}), onCommit: {
self.names[index] = editedValue
self.selectedName = editedValue
})
}
}
}
struct TestChangeSelectedItem_Previews: PreviewProvider {
static var previews: some View {
TestChangeSelectedItem()
}
}

Why use a sheet or a popover? Are there any alternatives?

I am having a lot of trouble with getting a .sheet() to work and I am beginning to wonder why I want to use one. What are the advantages of .sheet() and .popover() over e.g. changing the zIndex?
I have a search window in my app which popped up and down happily one or two betas ago. Now it pops up and down once. The code I am using works happily when isolated but not in my app. I tried using both the isPresented and item forms but neither were successful. The attached code works though if I have made any errors I would like to know. It is more complex than it needs to be as I was grasping at straws trying to find the problem.
struct ContentView : View {
#State var count = 0
#State var dismisses = 0
var body: some View {
VStack {
Spacer()
ShowButton(count: $count, dismisses: $dismisses)
Spacer()
Text("Shows: \(count), Dismisses:\(dismisses)")
}
}
}
struct ShowButton: View {
#State var show = false
#Binding var count: Int
#Binding var dismisses: Int
var body: some View {
Button(
action: { self.count += 1; self.show = true },
label: { Text("Show sheet") }
)
.sheet(
isPresented: $show,
onDismiss: { self.dismisses += 1 },
content: { Show(text: "Test #\(self.count)") }
)
}
}
struct Show: View {
let text: String
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body: some View {
VStack {
Text(text)
Button(
action: {
self.mode.value.dismiss()
},
label: { Text("Dismiss") }
)
}
}
}
This code works as expected, similar code in my app works once.
Are there any good alternatives to sheet()/popover()?