How to change value of existence key in a dictionary with textfield - ios

I have dictionary that saves a questions as keys and answers as values ,I want change the answers using TextField each question to different answer and save them using UserDefaults .
I tried different ways but I had some issues , like all the values of the dictionary
would be saved to the same sting , or..it would show an error index out of range when I try to access dictionary.values.sorted()[Int].
1- created questions array
struct Questions :ObservableObject{
#Published var questions = [ “Q1” , “Q2” , “Q3” ]
}
2- here's the list of questions(I want the answers to be shown below the question)
struct FavoritesQuestionsListView:View {
#ObservedObject var questions = Questions()
#ObservedObject var dictionary = Dictionary()
#State var isPresented: Bool = false
var body: some View {
VStack{
List(0..<self.questions.count , id:\.self){ currentQuestion in
VStack{
Text(questions[currentQuestions])
// I want to show here the saved answer to each questions but here’s what I tried…..
//Text(saveAnswers()) -> Error :index out of range.
//Text(dictionary.dictionary[questions.questions[currentQuestion] ?? "")
//only show the last answer I answered, and when I used print I found that all questions
//are saved to the same answer.
}
}.sheet(isPresented: self.$isPresented, content: {
QuestionsSheet( currentQuestion: currentQuestion , dictionary :dictionary ,isPresented:
self.$isPresented ,questions :questions)
})
.onTapGesture{}
self.isPresented.toggle()
}
func saveAnswers()->String{
let values = Array(dictionary.dictionary.values.sorted())
return values[currentQuestion]
}
}
}
3- the question sheet
struct QuestionsSheet:View {
#ObservedObject var questions: Questions
#ObservedObject var dictionary: Dictionary
#State var currentQuestion: Int = 0
#Binding var isPresented: Bool
#State var answer: String = ""
var body: some View {
VStack{
Text("\(currentQuestion + 1)/ \(questions.questions.count)")
Text(self.questions.questions[currentQuestion])
TextField("Enter your Answer..", text: self.$answer)
Button {
self.nextQuestion()
self.saveAnswers()
} label: {
Text("Next")
}
}
}
func nextQuestion(){
if self.question <= (self.favoritesQuestionsData.favoritesQuestionsJson[category].favoritesQuestions.count - 1) {
self.question += 1
}
}
func saveAnswers() {
//HERE’S My PROBLEM
//All the answers of the answers are saved to the same value
dictionary.dictionary = [questions.questions[currentQuestion] : answer]
//using updateValue to be able to change the answer in the future
dictionary.dictionary.updateValue(answer, forKey:questions.questions[currentQuestion])
}
}
4- finally the dictionary..
//it works successfully
class Dictionary: ObservableObject {
#Published var dictionary = UserDefaults.standard.object(forKey: “answer”) as?
[String:String] ?? [:]{
didSet{
UserDefaults.standard.set(dictionary, forKey: “answer”)
}
}
}

Related

Difficulty passing user input from TextField as a Double / Date [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 1 year ago.
Improve this question
I'm learning how to code using the tutorial provided by Apple. I'm at the stage where I learn to pass data with bindings.
I'm still learning so please expect two noob questions below, I've tried to find answers to this but couldn't find anything compelling, hence this post. Thanks in advance for your patience :)
Summarize the problem
The example provided in the tutorial makes use of TextField to allow user inputs.
I'm trying to do that so that people can enter a year (2008) for example, however, that doesn't work because the data I'm trying to pass it to is a Double, so I get an error message.
I've tried to use the date picker as well, but that leads me to deal with dates, which seems very complex to me at this stage.
Describe what you’ve tried
I've tried to make it so that the user input from the TextField is considered as a Double, but every way I tried it leads to more error messages. Nothing has worked so far.
When appropriate, show some code:
My data model:
struct Animal: Identifiable {
let id: UUID
var name: String
var birthyear: Int
}
init(id: UUID = UUID(), name: String, birthyear: Int){
self.id = id
self.name = name
self.birthyear = birthyear
}
extension Animal {
static var data: [Animal] {
[
Animal(name: "Félix", birthyear: 1999
]
}
}
extension Animal {
struct Data {
var name: String = ""
var birthyear: Double = 1999
}
var data: Data {
return Data (name: name, birthyear: Double(birthyear))
}
mutating func update(from data: Data) {
name = data.name
birthyear = Int(data.birthyear)
}
}
and the code where my TextField appears (the EditView):
import SwiftUI
struct EditView: View {
#Binding var animData: Animal.Data
var body: some View {
Form {
Section(header: Text("Animal Info")){
List {
TextField("Title", text: $animData.name)
HStack {
TextField("Birth Year", text: $animData.vaccinyear)
}
}
}
}
}
struct EditView_Previews: PreviewProvider {
static var previews: some View {
EditView(animData: .constant(Animal.data[0].data))
}
}
Now I have two questions:
If I want the data passed as a Double, so my code above would actually work, what should I do? It works with a slider, but it is a ridiculous way to go about it.
(the slider code that works)
var body: some View {
Form {
Section(header: Text("Animal Info")){
List {
TextField("Title", text: $animData.name)
HStack {
Slider(value: $animData.birthyear, in: 1999...2021, step: 1.0) {
Text("Length")
}
}
}
}
edit: solution from here: SwiftUI - How to create TextField that only accepts numbers
TextField("Birth Year", text: $animData.birthyear)
.keyboardType(.numberPad)
.onReceive(Just(animData.birthyear)) { newValue in
let filtered = newValue.filter { "0123456789".contains($0) }
if filtered != newValue {
self.animData.birthyear = filtered
}
}
If you're interested in a solution that still lets you keep an Int, this modification works by using an intermediary #State variable with the String value and then assigns the real Int value if it is valid.
struct ContentView : View {
#State var birthyear = 1999
#State var stringBirthyear = ""
var birthyearBinding : Binding<String> {
.init {
print("returning",birthyear)
return "\(birthyear)"
} set: { (newValue) in
if let intVal = Int(newValue) {
DispatchQueue.main.async {
print("Setting birthyear to",intVal)
self.birthyear = intVal
}
}
}
}
var body: some View {
Text("\(birthyear)")
TextField("Birth Year", text: $stringBirthyear)
.keyboardType(.numberPad)
.onReceive(Just(stringBirthyear)) { newValue in
let filtered = newValue.filter { "0123456789".contains($0) }
if filtered != newValue {
print(filtered,newValue)
stringBirthyear = filtered
}
if let intVal = Int(filtered) {
birthyear = intVal
}
}
.onAppear {
stringBirthyear = "\(birthyear)"
}
}
}

Views dependant on UserDefaults not updating on change

So I have a class that records the state of a toggle and a selection of a picker into UserDefaults
import Foundation
import Combine
class UserSettings: ObservableObject {
#Published var shouldSort: Bool {
didSet {
UserDefaults.standard.set(shouldSort, forKey: "shouldSort")
}
}
#Published var sortKey: String {
didSet {
UserDefaults.standard.set(sortKey, forKey: "sortKey")
}
}
public var sortKeys = ["alphabetical", "length", "newest", "oldest"]
init() {
self.shouldSort = UserDefaults.standard.object(forKey: "shouldSort") as? Bool ?? true
self.sortKey = UserDefaults.standard.object(forKey: "sortKey") as? String ?? "Name"
}
}
On my settings page I use the following code
#ObservedObject var userSettings = UserSettings()
...
Toggle(isOn: $userSettings.shouldSort, label: {Text("Sort Books")})
Picker(selection: $userSettings.sortKey, label: Text("Sort By"), content: {
ForEach(userSettings.sortKeys, id: \.self){ key in
Text(key)
}
})
This code changes the value just fine because if I close and open the app, the views update based on the data. I am reading the data with
#State var sorted = UserDefaults.standard.bool(forKey: "shouldSort")
#State var sort = UserDefaults.standard.string(forKey: "sortKey")
in my content view. (shouldSort calls a function to sort if true and sortKey determines how the data is sorted)
Am I reading the data wrong with the #State variable (can #State even detect changes in state of UserDefaults)?
Forget all what you learnt about UserDefaults in UIKit and say Hello to AppStorage in SwiftUI, use this Codes:
#AppStorage("shouldSort") var sorted: Bool = false
#AppStorage("sortKey") var sort: String = ""

Swift UI: Update view with random element from array

I'm trying to update a view in Swift and I can't figure out how to make it work. My app has questions, which are loaded from Core data. From there, a random question should be displayed at the top. After saving the answer (by pressing the Button with action: save), a new random question should be displayed.
struct RecordView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Question.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Question.question, ascending: false)])
var questions: FetchedResults<Question>
var currentQuestion: String { return questions.randomElement()!.question! }
#State private var newEntryText = ""
var body: some View {
VStack {
Section(header: Text(currentQuestion)){
TextField("New entry", text: self.$newEntryText)
.padding(100)
HStack {
SwiftSpeech.RecordButton().scaleEffect(0.8).swiftSpeechToggleRecordingOnTap(locale: Locale(identifier: "de"), animation: .spring(response: 0.3, dampingFraction: 0.5, blendDuration: 0))
.onRecognize(update: self.$newEntryText)
Button(action: save)
{
Image(systemName: "plus.circle.fill").foregroundColor(.green).imageScale(.large).scaleEffect(2.0)
}
}
}.automaticEnvironmentForSpeechRecognition()
}
}
func save() {
let newEntry = Entry(context: self.moc)
newEntry.text = self.newEntryText
newEntry.createdAt = Date()
do {
try self.moc.save()
}catch{
print(error)
}
self.newEntryText = ""
print(currentQuestion)
}
What I tried:
1) #State var currentQuestion: String = questions.randomElement()!.question!-> Cannot use instance member 'questions' within property initializer; property initializers run before 'self' is available. Here the problems seems to be that the questions array has to be loaded first.
2) var currentQuestion: String { return questions.randomElement()!.question! } -> Here the currentQuestion is recomputed every time it is accessed, but the View does not update. Same thing if I move the questions.randomElement()!.question! to the Text() component.
3) lazy var currentQuestion = questions.randomElement()!.question!-> Cannot use mutating getter on immutable value: 'self' is immutable (at the Text() component). The lazy part should have solved the problem I have at the 1) solution, but then I cannot use it at the Text() component.
... and some other minor variations. I'm a Swift/Swift UI Beginner, and I am running out of ideas how to update the displayed current question everytime the button is pressed. Does anyone has an idea for this?
Many thanks!
Try the following (scratchy)
#State var currentQuestion: String = "" // as state !!
var body: some View {
VStack {
Section(header: Text(currentQuestion)){
// ... other your code here
}.automaticEnvironmentForSpeechRecognition()
}.onAppear {
self.nextQuestion() // << here !!
}
}
...
func save() {
// ... other your code here
self.nextQuestion() // << here !!
}
private func nextQuestion() {
self.currentQuestion = questions.randomElement()?.question ?? ""
}

How to use Dictionary as #Binding var in SwiftUI

I will need to display a collapsed menu in SwiftUI, it is possible to pass one single bool value as binding var to subviews but got stuck when trying to pass that value from a dictionary.
see code below:
struct MenuView: View {
#EnvironmentObject var data: APIData
#State var menuCollapsed:[String: Bool] = [:]
#State var isMenuCollapsed = false;
// I am able to pass self.$isMenuCollapsed but self.$menuCollapsed[menuItem.name], why?
var body: some View {
if data.isMenuSynced {
List() {
ForEach((data.menuList?.content)!, id: \.name) { menuItem in
TopMenuRow(dataSource: menuItem, isCollapsed: self.$isMenuCollapsed)
.onTapGesture {
if menuItem.isExtendable() {
let isCollapsed = self.menuCollapsed[menuItem.name]
self.menuCollapsed.updateValue(!(isCollapsed ?? false), forKey: menuItem.name)
} else {
print("Go to link:\(menuItem.url)")
}
}
}
}
}else {
Text("Loading...")
}
}
}
in ChildMenu Row:
struct TopMenuRow: View {
var dataSource: MenuItemData
#Binding var isCollapsed: Bool
var body: some View {
ChildView(menuItemData)
if self.isCollapsed {
//display List of child data etc
}
}
}
}
If I use only one single bool as the binding var, the code is running ok, however, if I would like to use a dictionary to store each status of the array, it has the error of something else, see image blow:
if I use the line above, it's fine.
Any idea of how can I fix it?
Thanks
How to use dictionary as a storage of mutable values with State property wrapper?
As mentioned by Asperi, ForEach requires that source of data conforms to RandomAccessCollection. This requirements doesn't apply to State property wrapper!
Let see one of the possible approaches in the next snippet (copy - paste - run)
import SwiftUI
struct ContentView: View {
#State var dict = ["alfa":false, "beta":true, "gamma":false]
var body: some View {
List {
ForEach(Array(dict.keys), id: \.self) { (key) in
HStack {
Text(key)
Spacer()
Text(self.dict[key]?.description ?? "false").onTapGesture {
let v = self.dict[key] ?? false
self.dict[key] = !v
}.foregroundColor(self.dict[key] ?? false ? Color.red: Color.green)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
with the following result

Changing #State variable to value from UserDefaults on load not updating Picker

I'm trying to update a #State variable upon loading my app with a value stored in UserDefaults but my Picker is not updating. This is what I tried:
struct ContentView: View {
#State var selectedCanteen = 1
init() {
let previousSelectedCanteen = UserDefaults.standard.string(forKey: "selectedCanteen")
if let exist = previousSelectedCanteen {
self.selectedCanteen = Int(exist) ?? 1
}
}
var body: some View {
Picker(selection: $selectedCanteen, label: Text("Testing...")) {
Text("Stuff").tag(1)
Text("Stuff 2").tag(2)
}
}
}
I'm coming from react-native so might I might have missed some basic concepts in Swift/SwiftUI. Hope somebody can lead me in the right direction.
Try to use the following approach
#State var selectedCanteen: Int
init() {
let previousSelectedCanteen = UserDefaults.standard.string(forKey: "selectedCanteen")
var initialValue = 1
if let exist = previousSelectedCanteen {
initialValue = Int(exist) ?? 1
}
_selectedCanteen = State<Int>(initialValue: initialValue)
}

Resources