I have data in a dictionary which is [String: String]. What I want to provide is an interface to the user to edit the values in the dictionary, while the keys remain fixed. I can see how to display the values, but putting them into a TextField is what I want, and haven't been able to find how to do.
Here is the code:
struct dictionaryEditor: View {
#Binding var entries: [String: String]
var body: some View {
List {
ForEach(entries.sorted(by: <), id: \.key) { key, value in
HStack {
Text(key)
TextField("", text: $entries[key])
}
}
}
}
}
This doesn't compile, with no fewer than three errors on the TextField line:
Cannot convert value of type 'Slice<Binding<[String : String]>>' to expected argument type 'Binding'
Cannot convert value of type 'String' to expected argument type 'Range<Binding<[String : String]>.Index>'
Referencing subscript 'subscript(_:)' on 'Binding' requires that '[String : String]' conform to 'MutableCollection'
So obviously I am doing things incorrectly, but I am lost trying to find what the correct way would be, and haven't been able to find an answer in an internet search. Can anyone point me in the right direction?
you could try this simple approach:
struct dictionaryEditor: View {
#Binding var entries: [String: String]
var body: some View {
List {
ForEach(entries.keys.sorted(by: <), id: \.self) { key in
HStack {
Text(key)
TextField("", text: Binding(
get: { entries[key]! },
set: { entries[key] = $0 }
))
}
}
}
}
}
struct ContentView: View {
#State var entries: [String: String] = ["key1":"val1", "key2":"val2", "key3":"val3", "key4":"val4"]
var body: some View {
dictionaryEditor(entries: $entries)
Button(action: { print("----> entries: \(entries)") }) {
Text("print entries")
}
}
}
The problem is that entries[key] returns an optional String value while the text parameter of TextField expects a Binding of non optional String.
You can create an optional binding extension and then you can use it safely:
extension Binding where Value == String? {
var optionalBind: Binding<String> {
.init(
get: {
wrappedValue ?? ""
}, set: {
wrappedValue = $0
}
)
}
}
Then you can just add the optionalBind to your code:
struct dictionaryEditor: View {
#Binding var entries: [String: String]
var body: some View {
List {
ForEach(entries.sorted(by: <), id: \.key) { key, value in
HStack {
Text(key)
TextField("", text: $entries[key].optionalBind) // <--
}
}
}
}
}
Related
I have a Data class as follows:
class Data: Hashable, Equatable {
var id: Int = 0
var name: String = ""
var date: Date = Date()
var paymentStatus: Bool = false
static func == (lhs: Data, rhs: Data) -> Bool {
return lhs.id ==
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
var data1: Data {
let data = Data()
data.id = 1
data.name = "John"
data.date = Calendar.current.date(byAdding: DateComponents(day: -6), to: Date())!
data.paymentStatus = true
return data
}
var data2: Data {
let data = Data()
data.id = 2
data.name = "Peter"
data.date = Date()
data.paymentStatus = false
return data
}
I’m trying to display the data in sections as follows:
struct ContentView: View {
var data: [Data] = [data1, data2]
var body: some View {
List {
ForEach(groupByDate(data), id: \.self) { studentsInMonth in
Section(header:Text(Date(), style: .date)) {
ForEach(data, 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)
}
}
} // ForEach ends here...
} // section ends here
} // ForEach ends here
} // List ends here
}
}
func groupByDate(_ data: [Data]) -> [Date: [Data]] {
let empty: [Date: [Data]] = [:]
return data.reduce(into: empty) { acc, cur in
let components = Calendar.current.dateComponents([.year, .month], from: cur.date)
let date = Calendar.current.date(from: components)!
let existing = acc[date] ?? []
acc[date] = existing + [cur]
}
}
Not sure what mistake I’m making, but its throwing two errors:
Cannot convert value of type ‘[Date:[Data]]’ to expected argument type
Generic parameter ‘C’ could not be inferred
Appreciate any help
Right now, you're trying to iterate through a Dictionary using ForEach, which doesn't work -- ForEach expects a RandomAccessCollection, which is most often, an Array.
Instead, you can iterate over just the keys of the Dictionary.
struct ContentView: View {
var data: [Data] = [data1, data2]
var body: some View {
let grouped = groupByDate(data)
List {
ForEach(Array(grouped.keys), id: \.self) { key in
let studentsInMonth = grouped[key]!
Section(header:Text(key, style: .date)) {
ForEach(studentsInMonth, 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)
}
}
} // ForEach ends here...
} // section ends here
} // ForEach ends here
} // List ends here
}
}
Although the above works, I'd consider refactoring your groupByDate function so that it returns an Array directly instead of having to go through the above transformations.
Also, unrelated to your question, but right now, in your header, you're displaying a date -- you get just the year and month, but then display it as a full date, so the header says "January 1, 2022" for all the January dates. You probably want to refactor this to just show the month and year.
This question already has answers here:
SwiftUI iterating through dictionary with ForEach
(10 answers)
Closed 1 year ago.
I am trying to make a list with a dictionary. My dictionary is located in a model. My dictionary is [String : String]. I tried to sort it hopefully it is sorted by alphabetical. I couldn't figure it out why it does not work
var fw: Deck
var body: some View {
let sortedDict = fw.dictItems.sorted(by: < )
let keys = sortedDict.map {$0.key}
let values = sortedDict.map {$0.value}
return List{
ForEach(keys.indices) { index in
HStack {
Text(keys[index])
Text("\(values[index])")
}
}
}
}
}
Here is a way for you:
struct ContentView: View {
#State var entries: [String: String] = ["key1":"val1", "key2":"val2", "key3":"val3", "key4":"val4"]
var body: some View {
List {
ForEach(entries.keys.sorted(by: <), id: \.self) { key in
HStack {
Text(key)
Text(entries[key]!)
}
}
}
}
}
I am creating a Form in SwiftUi with a section that is including a flexible number of instruction.
Next to the last instruction TextField, I am showing a "+"-Button that is extending the instructions array with a new member:
var body: some View {
NavigationView {
Form {
...
Section(header: Text("Instructions")) {
InstructionsSectionView(instructions: $recipeViewModel.recipe.instructions)
}
...
struct InstructionsSectionView: View {
#Binding var instructions: [String]
var body: some View {
ForEach(instructions.indices, id: \.self) { index in
HStack {
TextField("Instruction", text: $instructions[index])
if(index == instructions.count-1) {
addInstructionButton
}
}
}
}
var addInstructionButton: some View {
Button(action: {
instructions.append("")
}) {
Image(systemName: "plus.circle.fill")
}
}
}
Now the problem is, that the button click-area is not limited to the picture but to the whole last row. Precisely the part just around the textField, meaning if I click in it, I can edit the text, but if I click on the border somewhere, a new entry is added.
I assume that this is specific to Form {} (or also List{}), since it does not happen if I use a Button next to a text field in a "normal" set-up.
Is there something wrong with my code? Is this an expected behaviour?
I am not sure why border is getting tappable, but as a workaround I used plainButtonStyle and that seems to fix this issue, and keeps functionality intact .
struct TestView: View {
#State private var endAmount: CGFloat = 0
#State private var recipeViewModel = ["abc","Deef"]
var body: some View {
NavigationView {
Form {
Section(header: Text("Instructions")) {
InstructionsSectionView(instructions: $recipeViewModel)
}
}
}
}
}
struct InstructionsSectionView: View {
#Binding var instructions: [String]
var body: some View {
ForEach(instructions.indices, id: \.self) { index in
HStack {
TextField("Instruction", text: $instructions[index])
Spacer()
if(index == instructions.count-1) {
addInstructionButton
.buttonStyle(PlainButtonStyle())
.foregroundColor(.blue)
}
}
}
}
var addInstructionButton: some View {
Button(action: {
instructions.append("")
}) {
Image(systemName: "plus.circle.fill")
}
}
}
Scenario: Attempting to broadcast a variable value via an ObservableObject.
Problem: I'm only getting the default value; not the assigned value.
Here's the origin.
Button #1 starts a function to get data.
Button #2 retrieves the ObservedObject's revised value
I removed some of the vestigial code to make the presentation simpler:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
let fontCustom = Font.custom("Noteworthy", size: 23.0)
var body: some View {
ZStack {
// ...
// ...
HStack {
Button(
action: {
NetworkManager().getCalculatorIDs()
},
label: {
Text("1")
}
)
Button(
action: {
self.calculator.calculate("2");
print(self.networkManager.calculationID) // stop and check.
},
label: { Text("2") }
)
// ...
// ...
}
}
So I tap Button #1 then tap Button #2 to check if the ObservedObject has the generated id value.
I'm expecting an alphanumeric id value in the print().
Instead, I got the original value:
Royal Turkey
(lldb)
Here's the ObservableObject:
struct CalculationIdentifier: Decodable {
let id: String
let tokens: [String]
}
class NetworkManager: ObservableObject {
#Published var calculationID = "Royal Turkey"
#Published var isAlert = false
#Published var name = "Ric Lee"
let calculations = "https://calculator-frontend-challenge.herokuapp.com/Calculations"
func getCalculatorIDs() {
let urlRequest = URLRequest(url: URL(string: calculations)!)
let configuration = URLSessionConfiguration.ephemeral
let task = URLSession(configuration: configuration).dataTask(with: urlRequest) { data, _, error in
DispatchQueue.main.async {
do {
let result = try JSONDecoder().decode([CalculationIdentifier].self, from: data!)
if !result.isEmpty {
self.calculationID = (result[0] as CalculationIdentifier).id
print("Inside do{}. result = \(result)")
self.isAlert = true
} else {
print(#function, "Line:", #line, ": No Result")
}
} catch {
print(error)
}
}
}
task.resume()
}
}
BTW: Here's the local console output, the string value of 'id' should have been passed to the host as an ObservedObject value:
Inside do{}. result = [RicCalculator2.CalculationIdentifier(id: "d3dd3b1e-d9f6-4593-8c85-b8fd3d018383", tokens: [])]
So I do have a bona fide id value to send.
Why only the original value?
What am I missing?
...do I need to do a 'send' or something?
This
A. #ObservedObject var networkManager = NetworkManager()
and this
B. NetworkManager().getCalculatorIDs()
in your code are different objects, ie. you create one object as member, then other object on the stack, which does something, and then ask first object to return something - naturally if returns what it has on initialise.
Probably you assumed in case B
self.networkManager.getCalculatorIDs()
Typically I would use presentTextInputControllerWithSuggestions() to show the TextInput field. But this isn't available in swiftUI because it is a function of WKInterfaceController. Do I have to use the WKInterfaceController for this?
I couldn't find anything in the documentation.
You can use extension for View in SwiftUI:
extension View {
typealias StringCompletion = (String) -> Void
func presentInputController(withSuggestions suggestions: [String], completion: #escaping StringCompletion) {
WKExtension.shared()
.visibleInterfaceController?
.presentTextInputController(withSuggestions: suggestions,
allowedInputMode: .plain) { result in
guard let result = result as? [String], let firstElement = result.first else {
completion("")
return
}
completion(firstElement)
}
}
}
Example:
struct ContentView: View {
var body: some View {
Button(action: {
presentInputController()
}, label: {
Text("Press this button")
})
}
private func presentInputController() {
presentInputController(withSuggestions: []) { result in
// handle result from input controller
}
}
}
This would be done through a TextField in SwiftUI.