Persistently store list items added by user in SwiftUI - swift

I am very new to swift (as in I started today) and I have code that allows the user to add items to a list:
private func onAdd() {
let alert = UIAlertController(title: "Enter a name for your plant", message: "Make sure it's descriptive!", preferredStyle: .alert)
alert.addTextField { (textField) in
textField.placeholder = "Enter here"
}
alert.addAction(UIAlertAction(title: "Done", style: .default) { _ in
let textField = alert.textFields![0] as UITextField
plantName2 = textField.text ?? "Name"
appendItem()
})
showAlert(alert: alert)
}
private func appendItem() {
items.append(Item(title: plantName2))
}
and
struct HomePage: View {
#State var plantName2: String = ""
#State private var items: [Item] = []
#State private var editMode = EditMode.inactive
private static var count = 0
var body: some View {
NavigationView {
List {
Section(header: Text("My Plants")) {
ForEach(items) { item in
NavigationLink(destination: PlantView(plantName3: item.title)) {
Text(item.title)
}
}
.onDelete(perform: onDelete)
.onMove(perform: onMove)
.onInsert(of: [String(kUTTypeURL)], perform: onInsert)
}
}
.listStyle(InsetGroupedListStyle()) // or GroupedListStyle
.navigationBarTitle("Plantify")
.navigationBarTitleTextColor(CustomColor.pastelGreen)
.navigationBarItems(leading: EditButton().accentColor(CustomColor.pastelGreen), trailing: addButton)
.environment(\.editMode, $editMode)
}
}
and I want the entries the user adds to be saved in persistent storage. I've looked at the docs for persistent storage and am a bit confused. Is it even possible with the code I have?
Thanks!

Basically you can save the data as String array in UserDefaults
func save() {
UserDefaults.standard.set(items.map(\.title), forKey: "items")
}
func load() {
let savedItems = UserDefaults.standard.stringArray(forKey: "items") ?? []
items = savedItems.map(Item.init)
}
and call load in .onAppear.
However if there is a huge amount of items consider a file in the Documents folder or Core Data

Related

Swift i want to check the data entered by user

I'm new to coding on Swift and I'm having a problem with a code. I want to verify the data entered by the user.
IngredientDetailView contains a form to add or modify an ingredient. In this view I want to control the data entered by the user. So the ingredients have a unique name.
My problem
When I modify the name of an existing ingredient with a name of another ingredient, my control does not work.
IngredientListView contains the list of existing ingredients that can be modified.
Thanks in advance for reading and helping.
struct IngredientDetailView: View {
#Environment(\.dismiss) private var dismiss
#State var ingredientItemData : Ingredient
#EnvironmentObject var ingredientsVM: IngredientModel
// To display the message that an ingredient already exists
#State private var showingAlertIngredientExist = false
let metrics: [String] = [
"Unité", "kg", "g", "l", "ml"
]
var body: some View {
List{
TextField("Ingredient name", text: $ingredientItemData.nom)
Text("Metric").bold()
Picker(selection: $ingredientItemData.metric, label: Text("Metric"), content: {
ForEach(metrics, id: \.self){ item in
Text(item)
}
}).pickerStyle(.segmented)
}
.toolbar{
ToolbarItem(placement: .navigationBarLeading){
Button("Cancel"){
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing){
Button("Ok"){
//If the id already exists
if ingredientsVM.ingredients.contains(where: {$0.id == ingredientItemData.id}){
// If the name you entered already exists
if ingredientsVM.ingredients.contains(where: {$0.nom.lowercased() == ingredientItemData.nom.lowercased()}){
// We get the index of the element we are working on
let i = ingredientsVM.ingredients.firstIndex(where: {$0.id == ingredientItemData.id})
// We check if the name has not changed if the ingredient has not been modified
if ingredientsVM.ingredients[i!].nom == ingredientItemData.nom {
dismiss()
}
else
{
// The name you want to modify already exists
showingAlertIngredientExist.toggle()
}
}
else {
// We save and save in the application
ingredientsVM.add(ing: ingredientItemData)
dismiss()
}
} else{
// No ID so we check if the name entered by the user already exists
if ingredientsVM.ingredients.contains(where: {$0.nom.lowercased() == ingredientItemData.nom.lowercased()}){
// The name you want to modify already exists
showingAlertIngredientExist.toggle()
}
else{
// We save and save in the application
ingredientsVM.add(ing: ingredientItemData)
dismiss()
}
}
}
.alert(isPresented: $showingAlertIngredientExist){
Alert(title: Text("Ingredient already present"), message: Text("An ingredient already exists with this name !"), dismissButton: .default(Text("Ok")))
}
}
}
.navigationBarBackButtonHidden()
.navigationBarTitleDisplayMode(.inline)
}
}
struct IngredientDetailView_Previews: PreviewProvider {
static var previews: some View {
IngredientDetailView(ingredientItemData: Ingredient())
.environmentObject(IngredientModel())
}
}
struct IngredientListView: View {
#Environment(\.dismiss) private var dismiss
#State private var sheetIsPresented = false
#EnvironmentObject var ingredientVM: IngredientModel
func getIndex(item: Ingredient) -> Int{
return ingredientVM.ingredients.firstIndex{(item1) -> Bool in
return item.id == item1.id
} ?? 0
}
var body: some View {
NavigationStack{
List{
ForEach(ingredientVM.ingredients){ item in
NavigationLink{
IngredientDetailView(ingredientItemData: item)
}label: {
Text(item.nom)
}
}
.onDelete{ indexSet in
// Temporaire. Fonction à mettre dans le model
ingredientVM.ingredients.remove(atOffsets: indexSet)
ingredientVM.saveData()
}
}.navigationTitle("IngredientListView")
.navigationBarTitleDisplayMode(.automatic)
.toolbar{
ToolbarItem(placement: .navigationBarLeading){
EditButton()
}
ToolbarItem(placement: .navigationBarTrailing){
Button{
sheetIsPresented.toggle()
}label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $sheetIsPresented){
NavigationStack{
IngredientDetailView(ingredientItemData: Ingredient())
}
.presentationDetents([.large,.medium,.fraction(0.75)])
}
}
}
}
struct IngredientListView_Previews: PreviewProvider {
static var previews: some View {
IngredientListView()
.environmentObject(IngredientModel())
}
}
class IngredientModel: ObservableObject{
#Published var ingredients : [Ingredient] = []
let saveKey = "Ingredients"
init(){
loadData()
}
func add(ing: Ingredient){
if ing.id == nil{
var newIngredient = ing
newIngredient.id = UUID().uuidString
ingredients.append(newIngredient)
}else{
if let index = ingredients.firstIndex(where: {$0.id == ing.id}){
ingredients[index] = ing
}
}
saveData()
}
func loadData(){
if let data = UserDefaults.standard.data(forKey: saveKey){
if let decoded = try? JSONDecoder().decode([Ingredient].self, from: data){
ingredients = decoded
return
}
}
}
func saveData(){
if let encoded = try? JSONEncoder().encode(ingredients) {
UserDefaults.standard.set(encoded, forKey: saveKey)
}
}
}
class Ingredient: Identifiable, Codable{
var id: String?
var nom = ""
var metric = ""
init (){
}
init(id: String, nom: String, metric: String){
self.id = id
self.nom = nom
self.metric = metric
}
}

Im trying to set displayName to users collection and display it in textfield

When registering a user through Apple sign in, a users collection is created in which there are userId, email and displayName fields.
When registering, the user cannot set a displayName, so the field is empty, in settings user can set displayName.
I have a test textfield and would like to keep it that way. (done and edit button next to the field, not in the bar)
#State var nameInEditMode = false
#State var name = "Example"
HStack {
if nameInEditMode {
TextField("New name", text: $name)
.padding(.leading, 5)
.onReceive(name.publisher.collect()) {
self.name = String($0.prefix(10))
}
} else {
Text(name)
}
Button(action: {
self.nameInEditMode.toggle()
}) {
Text(nameInEditMode ? "Done" : "Edit")
}
}
I tried to change displayName with:
#ObservedObject var viewModel = SetNameView()
#State var mode: Mode = .new
HStack {
if mode == .new {
TextField("New name", text: $viewModel.updatename.displayName)
.padding(.leading, 5)
.onReceive(viewModel.updatename.displayName.publisher.collect()) {
self.viewModel.updatename.displayName = String($0.prefix(10))
}
} else {
Text(viewModel.updatename.displayName)
}
Button(action: {
self.handleDoneTapped()
}) {
Text(mode == .new ? "Done" : "Edit")
}
}
func handleDoneTapped() {
self.viewModel.save()
}
}
Firestore parameters for updating data inside users collection:
class SetNameView: ObservableObject {
#Published var updatename: FBKeys
#Published var modified = false
private var cancellables = Set<AnyCancellable>()
init(updatename: FBKeys = FBKeys(displayName: "")) {
self.updatename = updatename
self.$updatename
.dropFirst()
.sink { [weak self] updatename in
self?.modified = true
}
.store(in: &self.cancellables)
}
private var db = Firestore.firestore()
private func addItem(_ updatename: FBKeys) {
do {
var addedItem = updatename
addedItem.displayName = Auth.auth().currentUser?.uid ?? ""
_ = try db.collection("users").addDocument(from: addedItem)
}
catch {
print(error)
}
}
private func updateItem(_ updatename: FBKeys) {
if let documentID = updatename.id {
do {
try db.collection("users").document(documentID).setData(from: updatename)
}
catch {
print(error)
}
}
}
public func updateOrAddItem() {
if let _ = updatename.id {
self.updateItem(self.updatename)
}
else {
addItem(updatename)
}
}
func save() {
self.updateOrAddItem()
}
}
Keys:
struct FBKeys: Codable, Hashable, Identifiable {
#DocumentID var id = UUID().uuidString
var displayName : String
var userId : String?
}
In general, when I try to substitute the viewModel, I can’t enter characters, but I can click on the edit button and already on clicking the button (with obviously impossible to enter any characters), a new document is still created in the users collection, nothing changes in the existing one user document.
Through the edit button (since it's the only one displayed) - I call the handleDoneTapped() func, which in turn calls the function inside SetNameView() -> func save() inside which has self.updateOrAddItem()
There are many problems, but the main one that haunts me is that a new document is being created. It is necessary that when entering text in the field and user saves it, the name is entered in the same user document in the displayName field.
I cleaned up the code from garbage as much as possible for better understanding.

why data are passing back but SwiftUi not updating Text

I get to pass back data via closure, so new name is passed, but my UI is not updating. The new name of the user is printed when I go back to original view, but the text above the button is not getting that new value.
In my mind, updating startingUser should be enough to update the ContentView.
my ContentView:
#State private var startingUser: UserData?
var body: some View {
VStack {
Text(startingUser?.name ?? "no name")
Text("Create start user")
.onTapGesture {
startingUser = UserData(name: "Start User")
}
}
.sheet(item: $startingUser) { userToSend in
DetailView(user: userToSend) { newOnePassedFromWhatDoneInEDitView in
startingUser = newOnePassedFromWhatDoneInEDitView
print("✅ \(startingUser?.name)")
}
}
}
my EditView:
struct DetailView: View {
#Environment(\.dismiss) var dismiss
var user: UserData
var callBackClosure: (UserData) -> Void
#State private var name: String
var body: some View {
NavigationView {
Form {
TextField("your name", text: $name)
}
.navigationTitle("edit view")
.toolbar {
Button("dismiss") {
var newData = self.user
newData.name = name
newData.id = UUID()
callBackClosure(newData)
dismiss()
}
}
}
}
init(user: UserData, callBackClosure: #escaping (UserData) -> Void ) {
self.user = user
self.callBackClosure = callBackClosure
_name = State(initialValue: user.name)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(user: UserData.example) { _ in}
}
}
my model
struct UserData: Identifiable, Codable, Equatable {
var id = UUID()
var name: String
static let example = UserData(name: "Luke")
static func == (lhs: UserData, rhs: UserData) -> Bool {
lhs.id == rhs.id
}
}
update
using these changes solves the matter, but my question remains valid, cannot understand the right reason why old code not working, on other projects, where sheet and text depends on the same #state var it is working.
adding
#State private var show = false
adding
.onTapGesture {
startingUser = UserData(name: "Start User")
show = true
}
changing
.sheet(isPresented: $show) {
DetailView(user: startingUser ?? UserData.example) { newOnePassedFromWhatDoneInEDitView in
startingUser = newOnePassedFromWhatDoneInEDitView
print("✅ \(startingUser!.name)")
}
}
The reason Text is not showing you the updated user name that you are passing in the closure is, your startingUser property will be set to nil when you dismiss the sheet because you have bind that property with sheet. Now after calling callBackClosure(newData) you are calling dismiss() to dismiss the sheet. To overcome this issue you can try something like this.
struct ContentView: View {
#State private var startingUser: UserData?
#State private var updatedUser: UserData?
var body: some View {
VStack {
Text(updatedUser?.name ?? "no name")
Text("Create start user")
.onTapGesture {
startingUser = UserData(name: "Start User")
}
}
.sheet(item: $startingUser) { userToSend in
DetailView(user: userToSend) { newUser in
updatedUser = newUser
print("✅ \(updatedUser?.name ?? "no name")")
}
}
}
}
I would suggest you to read the Apple documentation of sheet(item:onDismiss:content:) and check the example from the Discussion section to get more understanding.

SwiftUI ObservedObject does not updated when new items are added from a different view

I have a view model which handles the loading of new data once the app launches and when a new item is added. I have an issue when it comes to showing new items when are added from a new view, for example, a sheet or even a NavigationLink.
View Model
class GameViewModel: ObservableObject {
//MARK: - Properties
#Published var gameCellViewModels = [GameCellViewModel]()
var game = [GameModel]()
init() {
loadData()
}
func loadData() {
if let retrievedGames = try? Disk.retrieve("games.json", from: .documents, as: [GameModel].self) {
game = retrievedGames
}
self.gameCellViewModels = game.map { game in
GameCellViewModel(game: game)
}
print("Load--->",gameCellViewModels.count)
}
func addNew(game: GameModel){
self.game.append(game)
saveData()
loadData()
}
private func saveData() {
do {
try Disk.save(self.game, to: .documents, as: "games.json")
}
catch let error as NSError {
fatalError("""
Domain: \(error.domain)
Code: \(error.code)
Description: \(error.localizedDescription)
Failure Reason: \(error.localizedFailureReason ?? "")
Suggestions: \(error.localizedRecoverySuggestion ?? "")
""")
}
}
}
View to load the ViewModel data, leading add button is able to add and show data but the trailing which opens a new View does not update the view. I have to kill the app to get the new data.
NavigationView{
List {
ForEach(gameList.gameCellViewModels) { gameList in
CellView(gameCellViewModel: gameList)
}
}.navigationBarTitle("Games Played")
.navigationBarItems(leading: Text("Add").onTapGesture {
let arr:[Int] = [1,2,3]
self.gameList.addNew(game: GameModel(game: arr))
}, trailing: NavigationLink(destination: ContentView()){
Text("Play")
})
}
Play View sample
#State var test = ""
var body: some View {
VStack(){
TextField("Enter value", text: $test)
.keyboardType(.numberPad)
Button(action: {
var arr:[Int] = []
arr.append(Int(self.test)!)
self.gameList.addNew(game: GameModel(game: arr))
}) {
Text("Send")
}
}
}
To what I can see the issue seems to be here:
List {
// Add id: \.self in order to distinguish between items
ForEach(gameList.gameCellViewModels, id: \.self) { gameList in
CellView(gameCellViewModel: gameList)
}
}
ForEach needs something to orientate itself on in order to know what elements are already displayed and which are not.
If this did not solve the trick. Please update the code you provided to Create a minimal, Reproducible Example

How to change toggle on just one Core Data item using ForEach in SwiftUI?

How to change just one toggle in a list without subviews? I know how to make it work if I extract Subview from everything inside ForEach, but how to do it on one view?
I cannot use subview, because I have a problem later if I want to delete an item from this subview. It gives me some errors I don't know how to fix, so I am trying to make it on one view where I don't have this error.
The code for the list is quite simple:
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
var fetchRequest: FetchRequest<Item>
var items: FetchedResults<Item> { fetchRequest.wrappedValue }
#State private var doneStatus : Bool = false
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) {item in
HStack {
Text("\(item.name ?? "default item name")")
Spacer()
Toggle(isOn: self.$doneStatus) {
Text("Done")
}
.labelsHidden()
.onAppear {
self.doneStatus = item.done
}
.onTapGesture {
self.doneStatus.toggle()
item.done.toggle()
try? self.moc.save()
}
}
}
.onDelete(perform: removeItem)
}
.navigationBarTitle("Items")
.navigationBarItems(
leading:
Button(action: {
for number in 1...3 {
let item = Item(context: self.moc)
item.date = Date()
item.name = "Item \(number)"
item.done = false
do {
try self.moc.save()
}catch{
print(error)
}
}
}) {
Text("Add 3 items")
}
)
}
}
init() {
fetchRequest = FetchRequest<Item>(entity: Item.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Item.name, ascending: true)
])
}
func removeItem(at offsets: IndexSet) {
for offset in offsets {
let item = items[offset]
moc.delete(item)
}
try? moc.save()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
//Test data
let testItem = Item.init(context: context)
testItem.date = Date()
testItem.name = "Item name"
testItem.done = false
return ContentView().environment(\.managedObjectContext, context)
}
}
I am using 1 Core Data Entity: Item. With 3 attributes: date (Date), done (Boolean), name (String).
PROBLEM
When I tap on one toggle, all other toggles change as well.
I couldn't find a solution working with Core Data. I guess maybe I should use .id instead of .self? And add another attribute to my entity: id (UUID). But I tried to do it and failed.
I will appreciate any kind of help.
You bound all Toggle to one state... so
remove this
// #State private var doneStatus : Bool = false
bind Toggle dynamically to currently iterating item (note: .onAppear/.onTapGesture not needed anymore)
Toggle(isOn: Binding<Bool>(
get: { item.done },
set: {
item.done = $0
try? self.moc.save()
})) {
Text()
}
.labelsHidden()