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

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

Related

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

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

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 = ""

SwiftUI: How can I catch changing value from observed object when I execute function

I have a problem with observed object in SwiftUI.
I can see changing values of observed object on the View struct.
However in class or function, even if I change text value of TextField(which is observable object) but "self.codeTwo.text still did not have changed.
here's my code sample (this is my ObservableObject)
class settingCodeTwo: ObservableObject {
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: settingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: settingCodeTwo.userDefaultTextKey)
}
}
deinit {
canc.cancel()
}
}
and the main problem is... "self.codeTwo.text" never changed!
class NetworkManager: ObservableObject {
#ObservedObject var codeTwo = settingCodeTwo()
#Published var posts = [Post]()
func fetchData() {
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "\(self.codeTwo.text)" //This one I want to use observable object
urlComponents.path = "/mob_json/mob_json.aspx"
urlComponents.queryItems = [
URLQueryItem(name: "nm_sp", value: "UP_MOB_CHECK_LOGIN"),
URLQueryItem(name: "param", value: "1000|1000|\(Gpass.hahaha)")
]
if let url = urlComponents.url {
print(url)
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.Table
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
and this is view, I can catch change of the value in this one
import SwiftUI
import Combine
struct SettingView: View {
#ObservedObject var codeTwo = settingCodeTwo()
var body: some View {
ZStack {
Rectangle().foregroundColor(Color.white).edgesIgnoringSafeArea(.all).background(Color.white)
VStack {
TextField("test", text: $codeTwo.text).textFieldStyle(BottomLineTextFieldStyle()).foregroundColor(.blue)
Text(codeTwo.text)
}
}
}
}
Help me please.
Non-SwiftUI Code
Use ObservedObject only for SwiftUI, your function / other non-SwiftUI code will not react to the changes.
Use a subscriber like Sink to observe changes to any publisher. (Every #Published variable has a publisher as a wrapped value, you can use it by prefixing with $ sign.
Reason for SwiftUI View not reacting to class property changes:
struct is a value type so when any of it's properties change then the value of the struct has changed
class is a reference type, when any of it's properties change, the underlying class instance is still the same.
If you assign a new class instance then you will notice that the view reacts to the change.
Approach:
Use a separate view and that accepts codeTwoText as #Binding that way when the codeTwoText changes the view would update to reflect the new value.
You can keep the model as a class so no changes there.
Example
class Model : ObservableObject {
#Published var name : String //Ensure the property is `Published`.
init(name: String) {
self.name = name
}
}
struct NameView : View {
#Binding var name : String
var body: some View {
return Text(name)
}
}
struct ContentView: View {
#ObservedObject var model : Model
var body: some View {
VStack {
Text("Hello, World!")
NameView(name: $model.name) //Passing the Binding to name
}
}
}
Testing
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let model = Model(name: "aaa")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
model.name = "bbb"
}
return ContentView(model: model)
}
}
It is used two different instances of SettingCodeTwo - one in NetworkNamager another in SettingsView, so they are not synchronised if created at same time.
Here is an approach to keep those two instances self-synchronised (it is possible because they use same storage - UserDefaults)
Tested with Xcode 11.4 / iOS 13.4
Modified code below (see also important comments inline)
extension UserDefaults {
#objc dynamic var textKey2: String { // helper keypath
return string(forKey: "textKey2") ?? ""
}
}
class SettingCodeTwo: ObservableObject { // use capitalised name for class !!!
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: SettingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
private var observer: NSKeyValueObservation!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: SettingCodeTwo.userDefaultTextKey)
}
observer = UserDefaults.standard.observe(\.textKey2, options: [.new]) { _, value in
if let newValue = value.newValue, self.text != newValue { // << avoid cycling on changed self
self.text = newValue
}
}
}
}
class NetworkManager: ObservableObject {
var codeTwo = SettingCodeTwo() // no #ObservedObject needed here
...

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

SwiftUI update Elements.count after Network Request

i want to update a Custom List with columns.
struct XyzView: View {
#ObservedObject var networkManager: NetworkManager = NetworkManager()
#State var maxrow : Int = 1
init() {
maxrow = self.maxRow()
}
func maxRow() -> Int {
let a = self.networkManager.products.count
return a
}
var body: some View {
Text("\(maxrow) Elements")
}
}
the problem is after network request the count is not updated correctly and reload the view.
DispatchQueue.main.async {
self.products = products
}
how can i update the view and set maxrow to the current products.count ?
thanks for help.
The maxRow you are using in the body is not getting update.
You should bind the view directly to the products instead of trying to duplicate it:
{
#ObservedObject var networkManager = NetworkManager()
var maxRow: Int { networkManager.products.count }
var body: some View {
Text("\(maxRow) Elements")
}
}
In Views it might work more efficient and better to use the .onAppear when loading data when displaying a View.
I remade your code to the following:
struct XyzView: View {
#ObservedObject var networkManager: NetworkManager = NetworkManager()
#State var maxrow : Int = 1
func maxRow() -> Int {
let a = self.networkManager.products.count
return a
}
var body: some View {
Text("\(maxrow) Elements")
.onAppear {
self.maxrow = self.maxRow()
}
}
}
This should work like you wanted.
Tip: If you making any changes in your NetworkManager model, make sure you make all variables #Published so the View updates itself.

Resources