Saving a list using Codable or userDefaults - swift

Can someone help me to save the list in this code using Codable or another methods. I am not able to use the UserDefaults in the code. Can anyone help me how to use save the lists so that when ever, I re-open my app, the list is still there. Thanks.
import SwiftUI
struct MainView: View {
#State var br = Double()
#State var loadpay = Double()
#State var gp : Double = 0
#State var count: Int = 1
#State var listcheck = Bool()
#StateObject var taskStore = TaskStore()
#State var name = String()
var userCasual = UserDefaults.standard.value(forKey: "userCasual") as? String ?? ""
func addNewToDo() {
taskStore.tasks.append(Task(id: String(taskStore.tasks.count + 1), toDoItem: "load \(count)", amount: Double(gp)))
}
func stepcount() {
count += 1
}
var body: some View {
VStack {
TextField("Name", text: $name)
HStack {
Button(action: { gp += loadpay }) {
Text("Add Load")
}
Button(action: {
addNewToDo()
}) {
Text("Check")
}
}
Form {
ForEach(self.taskStore.tasks) {
task in
Text(task.toDoItem)
}
}
}
Button(action: {
UserDefaults.standard.set(name, forKey: "userCasual")})
{Text("Save")}
}
}
struct Task : Identifiable {
var id = String()
var toDoItem = String()
var amount : Double = 0
}
class TaskStore : ObservableObject {
#Published var tasks = [Task]()
}

In Task adopt Codable
struct Task : Codable, Identifiable {
var id = ""
var toDoItem = ""
var amount = 0.0
}
In TaskStore add two methods to load and save the tasks and an init method
class TaskStore : ObservableObject {
#Published var tasks = [Task]()
init() {
load()
}
func load() {
guard let data = UserDefaults.standard.data(forKey: "tasks"),
let savedTasks = try? JSONDecoder().decode([Task].self, from: data) else { tasks = []; return }
tasks = savedTasks
}
func save() {
do {
let data = try JSONEncoder().encode(tasks)
UserDefaults.standard.set(data, forKey: "tasks")
} catch {
print(error)
}
}
}
In the view call taskStore.save() to save the data.
However: For large data sets UserDefaults is the wrong place. Save the data in the Documents folder or use Core Data.
Side note: Never use value(forKey:) in UserDefaults, in your example there is string(forKey:)

You should take a look at the #AppStorage property wrapper. Here is a great article written by Paul Hudson who is a great resource when you're learning iOS.
UserDefaults isn't the best way to store persistent information though. Once you get a bit more comfortable with Swift and SwiftUI, you should look into CoreData for storing your data across sessions.

Related

SwiftUI+Combine - Dynamicaly subscribing to a dict of publishers

In my project i hold a large dict of items that are updated via grpc stream. Inside the app there are several places i am rendering these items to UI and i would like to propagate the realtime updates.
Simplified code:
struct Item: Identifiable {
var id:String = UUID().uuidString
var name:String
var someKey:String
init(name:String){
self.name=name
}
}
class DataRepository {
public var serverSymbols: [String: CurrentValueSubject<Item, Never>] = [:]
// method that populates the dict
func getServerSymbols(serverID:Int){
someService.fetchServerSymbols(serverID: serverID){ response in
response.data.forEach { (name,sym) in
self.serverSymbols[name] = CurrentValueSubject(Item(sym))
}
}
}
// background stream that updates the values
func serverStream(symbols:[String] = []){
someService.initStream(){ update in
DispatchQueue.main.async {
self.serverSymbols[data.id]?.value.someKey = data.someKey
}
}
}
}
ViewModel:
class SampleViewModel: ObservableObject {
#Injected var repo:DataRepository // injection via Resolver
// hardcoded value here for simplicity (otherwise dynamically added/removed by user)
#Published private(set) var favorites:[String] = ["item1","item2"]
func getItem(item:String) -> Item {
guard let item = repo.serverSymbols[item] else { return Item(name:"N/A")}
return ItemPublisher(item: item).data
}
}
class ItemPublisher: ObservableObject {
#Published var data:Item = Item(name:"")
private var cancellables = Set<AnyCancellable>()
init(item:CurrentValueSubject<Item, Never>){
item
.receive(on: DispatchQueue.main)
.assignNoRetain(to: \.data, on: self)
.store(in: &cancellables)
}
}
Main View with subviews:
struct FavoritesView: View {
#ObservedObject var viewModel: QuotesViewModel = Resolver.resolve()
var body: some View {
VStack {
ForEach(viewModel.favorites, id: \.self) { item in
FavoriteCardView(item: viewModel.getItem(item: item))
}
}
}
}
struct FavoriteCardView: View {
var item:Item
var body: some View {
VStack {
Text(item.name)
Text(item.someKey) // dynamic value that should receive the updates
}
}
}
I must've clearly missed something or it's a completely wrong approach, however my Item cards do not receive any updates (i verified the backend stream is active and serverSymbols dict is getting updated). Any advice would be appreciated!
I've realised i've made a mistake - in order to receive the updates i need to pass down the ItemPublisher itself. (i was incorrectly returning ItemPublisher.data from my viewModel's method)
I've refactored the code and make the ItemPublisher provide the data directly from my repository using the item key, so now each card is subscribing individualy using the publisher.
Final working code now:
class SampleViewModel: ObservableObject {
// hardcoded value here for simplicity (otherwise dynamically added/removed by user)
#Published private(set) var favorites:[String] = ["item1","item2"]
}
MainView and CardView:
struct FavoritesView: View {
#ObservedObject var viewModel: QuotesViewModel = Resolver.resolve()
var body: some View {
VStack {
ForEach(viewModel.favorites, id: \.self) { item in
FavoriteCardView(item)
}
}
}
}
struct FavoriteCardView: View {
var itemName:String
#ObservedObject var item:ItemPublisher
init(_ itemName:String){
self.itemName = itemName
self.item = ItemPublisher(item:item)
}
var body: some View {
let itemData = item.data
VStack {
Text(itemData.name)
Text(itemData.someKey)
}
}
}
and lastly, modified ItemPublisher:
class ItemPublisher: ObservableObject {
#Injected var repo:DataRepository
#Published var data:Item = Item(name:"")
private var cancellables = Set<AnyCancellable>()
init(item:String){
self.data = Item(name:item)
if let item = repo.serverSymbols[item] {
self.data = item.value
item.receive(on: DispatchQueue.main)
.assignNoRetain(to: \.data, on: self)
.store(in: &cancellables)
}
}
}

How we can notify ObservableObject about changes of its initializers?

I have a ObservableObject-Class which inside this class, I got a published var with name of persones! I do initialize it with some data called: allData.
Then I try to update my allData with action of a Button, and this action apply the wanted update to my allData, but my published var has no idea, that this data got updated!
How we can make published see the new updated allData?
struct PersonData: Identifiable {
let id = UUID()
var name: String
}
var allData = [PersonData(name: "Bob"), PersonData(name: "Nik"), PersonData(name: "Tak"), PersonData(name: "Sed"), PersonData(name: "Ted")]
class PersonDataModel: ObservableObject {
#Published var persones: [PersonData] = allData
}
struct ContentView: View {
#StateObject var personDataModel = PersonDataModel()
var body: some View {
VStack
{
Button("update allData") { allData = [PersonData(name: "Bob")] }
HStack
{
ForEach(personDataModel.persones) { person in Text(person.name) }
}
}
.font(Font.title)
}
}
PS: I donĀ“t want use .onChange or other things for this, I would like this happens internally in my class.
Also I know I can use down code for this work, but that is not the answer
personDataModel.persones = [PersonData(name: "Bob")]
Having a top-level property (outside of any class or struct) is probably not a good idea. I don't see the whole picture, but it looks like your app needs a global state (e.g., a #StateObject initialised on the App level). Consider this answer:
Add EnvironmentObject in SwiftUI 2.0
If you really need to observe your array, you need to make it observable.
One option is to use CurrentValueSubject from the Combine framework:
var persons = ["Bob", "Nik", "Tak", "Sed", "Ted"].map(PersonData.init)
var allData = CurrentValueSubject<[PersonData], Never>(persons)
class PersonDataModel: ObservableObject {
#Published var persones: [PersonData] = allData.value
private var cancellables = Set<AnyCancellable>()
init() {
allData
.sink { [weak self] in
self?.persones = $0
}
.store(in: &cancellables)
}
}
struct ContentView: View {
#StateObject var personDataModel = PersonDataModel()
var body: some View {
VStack {
Button("update allData") {
allData.send([PersonData(name: "Bob")])
}
HStack {
ForEach(personDataModel.persones) { person in
Text(person.name)
}
}
}
.font(Font.title)
}
}
The allData is copied into persones at initialization time, so changing it afterwards does nothing to personDataModel. After StateObject created you have to work with it, like
Button("update allData") {
self.personDataModel.persones = [PersonData(name: "Bob")]
}
I think you're doing something wrong.
if you want to update all your views, you have to pass the same object with #EnviromentObject.
I don't know your storage method (JSON, CORE DATA, iCloud) but the correct approach is to update directly the model
class PersonDataModel: ObservableObject
{
#Published var persones: [PersonData] = loadFromJSON //one func that is loading your object stored as JSON file
func updateAllData() {
storeToJSON(persones) //one func that is storing your object as JSON file
}
}
struct ContentView: View {
#StateObject var personDataModel = PersonDataModel()
var body: some View {
VStack
{
Button("update allData") {
self.personDataModel.persones = [PersonData(name: "Bob")]
}
HStack
{
ForEach(personDataModel.persones) { person in Text(person.name) }
}
}
.font(Font.title)
.onChange($personDataModel.persones) {
persones.updateAllData()
}
}
}

Storing a selected value from picker to use in other views - SwiftUI

I was very kindly helped to allow my picker to select a value from my Firestore database. What I would like to do is once that value is selected in my picker I would like to be able to show that value in other views. I have tried setting this up using UserDefaults but I'm not sure that's the way to go? If you could suggest a better method I'd be more than grateful. My code currently is below.
The value in the below code returns Unknown School each time but without the user defaults works flawlessly in fetching the data.
Thanks in advance.
import SwiftUI
import Firebase
struct SchoolDetailsView: View {
#ObservedObject var schoolData = getSchoolData()
#State private var selectedSchool = UserDefaults.standard.string(forKey: "") // `schoolName.id` is of type String
var body: some View {
VStack {
Form {
Section {
Picker(selection: $selectedSchool, label: Text("School Name")) {
ForEach(schoolData.datas, id: \.id) {
Text($0.name)
}
}
Text("Selected School: \(selectedSchool ?? "Unknown School")")
}
}.navigationBarTitle("School Selection")
}
}
struct SchoolPicker_Previews: PreviewProvider {
static var previews: some View {
SchoolDetailsView()
}
}
class getSchoolData : ObservableObject{
#Published var datas = [schoolName]()
init() {
let db = Firestore.firestore()
db.collection("School Name").addSnapshotListener { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in snap!.documentChanges{
let id = i.document.documentID
let name = i.document.get("Name") as! String
self.datas.append(schoolName(id: id, name: name))
}
}
}
}
struct schoolName : Identifiable {
var id : String
var name : String
}
}
First, the UserDefaults key for your variable can't be empty:
#State private var selectedSchool: String = UserDefaults.standard.string(forKey: "selectedSchool") ?? "Unknown School"
Then you can use onReceive to update the variable:
.onReceive(Just(selectedSchool)) {
UserDefaults.standard.set($0, forKey: "selectedSchool")
}
Full code:
import Combine
import Firebase
import SwiftUI
struct SchoolDetailsView: View {
#ObservedObject var schoolData = getSchoolData()
#State private var selectedSchool = UserDefaults.standard.string(forKey: "selectedSchool")
var body: some View {
VStack {
Form {
Section {
Picker(selection: $selectedSchool, label: Text("School Name")) {
ForEach(schoolData.datas, id: \.id) {
Text($0.name)
}
}
.onReceive(Just(selectedSchool)) {
UserDefaults.standard.set($0, forKey: "selectedSchool")
}
Text("Selected School: \(selectedSchool)")
}
}.navigationBarTitle("School Selection")
}
}
}
Note that in SwiftUI 2 / iOS 14 you can use #AppStorage instead.

String contents not being changed

This is my main view where I create an object of getDepthData() that holds a string variable that I want to update when the user click the button below. But it never gets changed after clicking the button
import SwiftUI
struct InDepthView: View {
#State var showList = false
#State var pickerSelectedItem = 1
#ObservedObject var data = getDepthData()
var body: some View {
VStack(alignment: .leading) {
Button(action: {
self.data.whichCountry = "usa"
print(" indepthview "+self.data.whichCountry)
}) {
Text("change value")
}
}
}
}
Here is my class where I hold a string variable to keep track of the country they are viewing. But when every I try to modify the whichCountry variable it doesn't get changed
class getDepthData: ObservableObject {
#Published var data : Specific!
#Published var countries : HistoricalSpecific!
#State var whichCountry: String = "italy"
init() {
updateData()
}
func updateData() {
let url = "https://corona.lmao.ninja/v2/countries/"
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: url+"\(self.whichCountry)")!) { (data, _, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
let json = try! JSONDecoder().decode(Specific.self, from: data!)
DispatchQueue.main.async {
self.data = json
}
}.resume()
}
}
Any help would be greatly appreciated!
You need to define the whichCountry variable as #Published to apply changes on it
#Published var whichCountry: String = "italy"
You need to mark whichCountry as a #Published variable so SwiftUI publishes a event when this property have been changed. This causes the body property to reload
#Published var whichCountry: String = "italy"
By the way it is a convention to write the first letter of your class capitalized:
class GetDepthData: ObservableObject { }
As the others have mentioned, you need to define the whichCountry variable as #Published to apply changes to it. In addition you probably want to update your data because whichCountry has changed. So try this:
#Published var whichCountry: String = "italy" {
didSet {
self.updateData()
}
}

Convert a #State into a Publisher

I want to use a #State variable both for the UI and for computing a value.
For example, let's say I have a TextField bound to #State var userInputURL: String = "https://". How would I take that userInputURL and connect it to a publisher so I can map it into a URL.
Pseudo code:
$userInputURL.publisher()
.compactMap({ URL(string: $0) })
.flatMap({ URLSession(configuration: .ephemeral).dataTaskPublisher(for: $0).assertNoFailure() })
.eraseToAnyPublisher()
You can't convert #state to publisher, but you can use ObservableObject instead.
import SwiftUI
final class SearchStore: ObservableObject {
#Published var query: String = ""
func fetch() {
$query
.map { URL(string: $0) }
.flatMap { URLSession.shared.dataTaskPublisher(for: $0) }
.sink { print($0) }
}
}
struct ContentView: View {
#StateObject var store = SearchStore()
var body: some View {
VStack {
TextField("type something...", text: $store.query)
Button("search") {
self.store.fetch()
}
}
}
}
You can also use onChange(of:) to respond to #State changes.
struct MyView: View {
#State var userInputURL: String = "https://"
var body: some View {
VStack {
TextField("search here", text: $userInputURL)
}
.onChange(of: userInputURL) { _ in
self.fetch()
}
}
func fetch() {
print("changed", userInputURL)
// ...
}
}
Output:
changed https://t
changed https://ts
changed https://tsr
changed https://tsrs
changed https://tsrst
The latest beta has changed how variables are published so I don't think that you even want to try. Making ObservableObject classes is pretty easy but you then want to add a publisher for your own use:
class ObservableString: Combine.ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<String, Never>()
var string: String {
willSet { objectWillChange.send() }
didSet { publisher.send(string) }
}
init(_ string: String = "") { self.string = string }
}
Instead of #State variables you use #ObservableObject and remember to access the property string directly rather than use the magic that #State uses.
After iOS 14.0, you can access to Publisher.
struct MyView: View {
#State var text: String?
var body: some View {
Text(text ?? "")
.onReceive($text.wrappedValue.publisher) { _ in
let publisher1: Optional<String>.Publisher = $text.wrappedValue.publisher
// ... or
let publisher2: Optional<String>.Publisher = _text.wrappedValue.publisher
}
}
}