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

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

Related

How to identify selected SwiftUI list item

In a macOS SwiftUI app, I have a List of items with context menus. When a menu selection is made, the app needs to act on the correct list item. (The context menu can apply to any item, not just the selected one.)
I have a solution that works fairly well, but it has a strange bug. When you right click (or Command+click) on an item, the app sets a variable indicating which item was clicked, and also sets a flag. The flag triggers a sheet requesting confirmation of the action. The problem is that the first time you select a menu item, the sheet doesn’t use the saved item as it should. You can see because the item’s name is not in the “Ok to delete” prompt. If you close that first sheet and select another item, it works correctly, and it works for for every subsequent item from then on, even the first one you tried. It doesn’t matter which item you try first, or whether you select the item first, or anything.
import SwiftUI
struct ContentView: View {
#State private var actionTarget = Value(name: "")
#State private var isDeleting = false
#State private var selection = Value(name: "")
struct Value: Identifiable, Hashable {
let id = UUID()
var name: String
}
let values = [Value(name: "One"), Value(name: "Two"), Value(name: "Three")]
var body: some View {
List(values, selection: $selection) { value in
Text (value.name)
.tag(value)
.contextMenu(ContextMenu {
Button {
actionTarget = value
isDeleting = true
} label: { Text("Delete \(value.name)") }
})
}
.sheet(isPresented: $isDeleting) {
Text("Ok to delete \"\(actionTarget.name)?\"")
.frame(width: 300)
.padding()
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { isDeleting = false }
}
ToolbarItem(placement: .destructiveAction) {
Button {
//TODO: Delete
isDeleting = false
} label: { Text("Delete") }
}
}
}
}
}
This is a bug in SwiftUI.
You can work around it by using a different version of the sheet modifier, the one that takes a Binding<Item?>. That also has the advantage that it leads you to a better data model. In your model as posted, you have separate isDeleting and actionTarget variables which can be out of sync. Instead, use a single optional variable holding the Value to be deleted, or nil if there is no deletion to be confirmed.
struct ContentView: View {
#State private var deleteRequest: Value? = nil
#State private var selection: Value? = nil
struct Value: Identifiable, Hashable {
let id = UUID()
var name: String
}
let values = [Value(name: "One"), Value(name: "Two"), Value(name: "Three")]
var body: some View {
List(values, selection: $selection) { value in
Text(value.name)
.tag(value)
.contextMenu(ContextMenu {
Button {
deleteRequest = value
} label: { Text("Delete \(value.name)") }
})
}
.sheet(
item: $deleteRequest,
onDismiss: { deleteRequest = nil }
) { item in
Text("Ok to delete \"\(item.name)?\"")
.frame(width: 300)
.padding()
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
deleteRequest = nil
}
}
ToolbarItem(placement: .destructiveAction) {
Button {
print("TODO: delete \(item)")
deleteRequest = nil
} label: { Text("Delete") }
}
}
}
}
}
But the use of a toolbar inside the sheet doesn't look like a normal macOS confirmation sheet. Instead, you should use confirmationDialog.
struct ContentView: View {
#State private var deleteRequest: Value? = nil
#State private var selection: Value? = nil
struct Value: Identifiable, Hashable {
let id = UUID()
var name: String
}
let values = [Value(name: "One"), Value(name: "Two"), Value(name: "Three")]
var body: some View {
List(values, selection: $selection) { value in
Text(value.name)
.tag(value)
.contextMenu(ContextMenu {
Button {
deleteRequest = value
} label: { Text("Delete \(value.name)") }
})
}
.confirmationDialog(
"OK to delete \(deleteRequest?.name ?? "(nil)")?",
isPresented: .constant(deleteRequest != nil),
presenting: deleteRequest,
actions: { item in
Button("Cancel", role: .cancel) { deleteRequest = nil }
Button("Delete", role: .destructive) {
print("TODO: delete \(item)")
deleteRequest = nil
}
}
)
}
}

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

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 SceneDelegate - contentView Missing argument for parameter 'index' in call

I am trying to create a list using ForEach and NavigationLink of an array of data.
I believe my code (see the end of the post) is correct but my build fails due to
"Missing argument for parameter 'index' in call" and takes me to SceneDelegate.swift a place I haven't had to venture before.
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
I can get the code to run if I amend to;
let contentView = ContentView(habits: HabitsList(), index: 1)
but then all my links hold the same data, which makes sense since I am naming the index position.
I have tried, index: self.index (which is what I am using in my NavigationLink) and get a different error message - Cannot convert value of type '(Any) -> Int' to expected argument type 'Int'
Below are snippets of my code for reference;
struct HabitItem: Identifiable, Codable {
let id = UUID()
let name: String
let description: String
let amount: Int
}
class HabitsList: ObservableObject {
#Published var items = [HabitItem]()
}
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var index: Int
var body: some View {
NavigationView {
List {
ForEach(habits.items) { item in
NavigationLink(destination: HabitDetail(habits: self.habits, index: self.index)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}
}
}
}
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var habits: HabitsList
var index: Int
var body: some View {
NavigationView {
Form {
Text(self.habits.items[index].name)
}
}
}
}
You probably don't need to pass the whole ObservedObject to the HabitDetail.
Passing just a HabitItem should be enough:
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
let item: HabitItem
var body: some View {
// remove `NavigationView` form the detail view
Form {
Text(item.name)
}
}
}
Then you can modify your ContentView:
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var body: some View {
NavigationView {
List {
// for every item in habits create a `linkView`
ForEach(habits.items, id:\.id) { item in
self.linkView(item: item)
}
}
}
}
// extract to another function for clarity
func linkView(item: HabitItem) -> some View {
// pass just a `HabitItem` to the `HabitDetail`
NavigationLink(destination: HabitDetail(item: item)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}

SwiftUI TextField Simple Example not working

I tried to create a very simple TextField in SwiftUI but I cannot get it to work and I don't understand what I am doing wrong.
Xcode gives me an error message that says:
"Unable to infer complex closure return type; add explicit type to disambiguate."
I am not sure what to do. I found some other code examples for TextFields with SwiftUI on StackOverflow but keep getting the same error.
struct TextFieldExample : View {
#State var email: String = "Enter email address"
var body: some View {
VStack {
TextField($email)
Text("Your email is \(email)!")
}
}
}
struct ButtonTextField : View {
#State var text: String = ""
var body: some View {
HStack {
TextField($text,
placeholder: Text("type something here..."))
Button(action: {
// Closure will be called once user taps your button
print(self.$text)
}) {
Text("SEND")
}
}
}
}
Expected results = working TextField
Actual result = Error in Xcode
It seems the TextField view has been changed in a recent beta release. You should be able to create one using something like this:
struct MyView {
#State var myInput: String = ""
var body: some View {
TextField("placeholder text", text: $myInput)
}
}
In the recent beta release of Xcode TextField has been changed.
#State var email: String = ""
var body: some View {
TextField("Email", text: $email, onEditingChanged: { (isChanges) in
// On Editing Changed
}) {
// On Commit
}
.padding(.leading, 13).padding(.trailing, 13).padding(.top, UIScreen.main.bounds.size.height / 2)
.textContentType(.emailAddress)
.textFieldStyle(RoundedBorderTextFieldStyle.init())
}
First of all do you really need to combine these Views into a custom view? If yes than:
#State and BindableObject should be passed into the view to the property marked with #Binding keyword
Don't use the same name as some of the native classes have
struct MyTextField : View {
#Binding var email: String
var body: some View {
VStack {
Text("Your email is \(email)!")
TextField($email, placeholder: Text("Enter your email"))
}
}
}
Call it like this
#State private var email: String = ""
var body: some View {
MyTextField(email: $email)
}
TextField like SearchView - XCODE 11.3
struct SearchBarV: View {
#State var text: String = ""
var onEditingChanged: (Bool) -> Void = { _ in }
var onCommit: () -> Void = { }
var body: some View {
GeometryReader { metrics in
TextField("placeholder", text: self.$text, onEditingChanged: self.onEditingChanged, onCommit: self.onCommit)
.background(Color.gray.opacity(0.1))
.padding(EdgeInsets(top: 0.0, leading: 16.0, bottom: 0, trailing: 16.0))
.frame(width: metrics.size.width, height: 50)
.keyboardType(.emailAddress)
}
}
}
struct SearchBarV_Previews : PreviewProvider {
static var previews: some View {
SearchBarV()
}
}