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
}
}
Related
I'm using a two-column NavigationSplitView. Trying to figure out how to update the data model via .onSubmit modifier and use a TextField view without Binding.constant.
Within the detail section, I have TextField and TextEditor.
How to avoid Binding.contant()? I mean, I need mutation.
This is a correct way to update value property in Model?
I need a single selection in List.
Here's my sample code (70 line’s):
struct Model: Identifiable, Hashable {
var id = UUID()
var title: String = "Brand new"
var value: String = ""
func updateValue() async -> Model {
return Model(id: id, title: title, value: "The boar 🐗 is running through the field happily")
}
}
final class DataModel: ObservableObject {
#Published
var models: [Model] = [
.init(title: "First", value: "fur"),
.init(title: "Second", value: "meow"),
.init(title: "Another", value: "Make SwiftUI, not war")
]
#MainActor
func updateModel(for model: Model.ID) async -> Void {
var findModel = models.first { $0.id == model }
findModel = await findModel?.updateValue()
}
}
struct ModelView: View {
#StateObject
private var dataModel = DataModel()
#State
private var listSelection: Model.ID?
private var selectedModel: Model? {
guard let selection = listSelection else { return nil }
return dataModel.models.first { $0.id == selection }
}
var body: some View {
NavigationSplitView {
List(dataModel.models, selection: $listSelection) { model in
NavigationLink(model.title, value: model.id)
}
} detail: {
if let selectedModel {
VStack {
TextField("Title", text: .constant(selectedModel.title))
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
.submitLabel(.go)
.onSubmit {
Task {
// Update Model.value by hit `Go`
await dataModel.updateModel(for: selectedModel.id)
}
}
TextEditor(text: .constant(selectedModel.value))
}
.padding()
.navigationTitle(selectedModel.title)
}
}
}
}
struct ModelView_Previews: PreviewProvider {
static var previews: some View {
ModelView()
.colorScheme(.light)
}
}
After a couple of days, I realized what I could do.
No one answered the question, so I solved the problem this way.
The final solution is below:
struct Model: Identifiable, Hashable {
var id = UUID()
var title: String = "Brand new"
var value: String = ""
func updateValue() async -> Model {
return Model(id: id, title: title, value: "The boar 🐗 is running through the field happily")
}
}
final class DataModel: ObservableObject {
#Published
var models: [Model] = [
.init(title: "First", value: "fur"),
.init(title: "Second", value: "meow"),
.init(title: "Another", value: "Make SwiftUI, not war")
]
#MainActor
func updateModel(for model: Binding<Model>) async -> Void {
model.wrappedValue = await model.wrappedValue.updateValue()
}
func bindingToModel(_ model: Model.ID) -> Binding<Model> {
Binding<Model> {
guard let index = self.models.firstIndex(where: { $0.id == model }) else {
return Model()
}
return self.models[index]
} set: { newModel in
guard let index = self.models.firstIndex(where: { $0.id == model }) else { return }
self.models[index] = newModel
}
}
}
struct ModelView: View {
#StateObject
private var dataModel = DataModel()
#State
private var listSelection: Model.ID?
var body: some View {
NavigationSplitView {
List(dataModel.models, selection: $listSelection) { model in
NavigationLink(model.title, value: model.id)
}
} detail: {
if let listSelection, let bindModel = dataModel.bindingToModel(listSelection) {
VStack {
TextField("Title", text: bindModel.title)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
.submitLabel(.go)
.onSubmit {
Task {
// Update Model.value by hit `Go`
await dataModel.updateModel(for: bindModel)
}
}
TextEditor(text: bindModel.value)
}
.padding()
.navigationTitle(bindModel.title)
}
}
}
}
I am following a tutorial and I am trying to save to Firestore a list of Favorite products (different from the tutorial). Items are being added but if I press again the "Add to favorites" button it adds the same item again in Firestore and I want to add the item only once. This is my code
struct addToFavoritesButton : View {
#ObservedObject var favoriteListener = FavoriteListener()
//binding variable get the info from another variable does not have a value in it self
#Binding var showAlert: Bool
#Binding var averageColor:Color
#Binding var complementColor:Color
var product : Product
var body : some View {
Button {
showAlert.toggle()
self.addItemToFavorites()
} label: {
Image(systemName: "heart")
.font(.title3)
.frame(height: 50)
.frame(maxWidth: 50)
.background(self.complementColor)
.foregroundColor(self.averageColor)
.cornerRadius(25)
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Adăugat la preferate."), dismissButton: .default(Text("OK")))
}//: ALERT
}//: BODY
private func addItemToFavorites() {
var favorite: Favorite!
if self.favoriteListener.favorite != nil {//deja am un id de favorite in firestore atasat clientului
let isInFavorites = isProductInFavorites(product: product)
if isInFavorites == false {
favorite = self.favoriteListener.favorite
}
} else { //daca nu am deja un id de fav il creez
favorite = Favorite()
favorite.ownerId = "123"
favorite.id = UUID().uuidString
}
favorite.add(self.product)
favorite.saveFavoritesToFirestore()
}
func isProductInFavorites(product: Product) -> Bool {
let db = Firestore.firestore()
let docRef = db.collection("Favorite").document(product.id)
var isInFavorites = false
docRef.getDocument { (document, error) in
if let document = document, document.exists {
isInFavorites = true
} else {
isInFavorites = false
}
}
return isInFavorites
}
}
I've tried something (isProductInFavorites) but it's not working. It is still adding the item again in Firestore. Can somebody point me in the right direction?
Solution here, maybe it will help somebody.
struct addToFavoritesButton : View {
#ObservedObject var favoriteListener = FavoriteListener()
//binding variable get the info from another variable does not have a value in it self
#Binding var showAlert: Bool
#Binding var averageColor:Color
#Binding var complementColor:Color
var product : Product
var body : some View {
Button {
showAlert.toggle()
self.addItemToFavorites()
} label: {
Image(systemName: "heart")
.font(.title3)
.frame(height: 50)
.frame(maxWidth: 50)
.background(self.complementColor)
.foregroundColor(self.averageColor)
.cornerRadius(25)
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Adăugat la preferate."), dismissButton: .default(Text("OK")))
}//: ALERT
}//: BODY
private func addItemToFavorites() {
var favorite: Favorite!
if self.favoriteListener.favorite != nil {
favorite = self.favoriteListener.favorite
} else {
favorite = Favorite()
favorite.ownerId = "123"
favorite.id = UUID().uuidString
}
if !favorite.items.contains(where: { $0.id == self.product.id }) {
favorite.add(self.product)
favorite.saveFavoritesToFirestore()
}
}
}
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.
Update
Based on #Tushar Sharma's suggestion, I removed #ObservedObject tag from the place variable in both PlaceDetail.swift and EditPlaceForm.swift. This made it so that the place property was an argument for the view rather than a state object. I then created separate state variables in EditPlaceForm.swift to hold the values for the text inputs. I added an onAppear to the view that sets the state values using the provided place object. This works great, and resolves the issue of the back button disappearing. It introduced a new problem though: PlaceDetail doesn't update until after it's been closed and reopened. I then did some testing to see if updating the place variable within PlaceDetail itself caused an update. To do this I just added a button that changed the name of the place and saved it to the view context when pressed. The view still did not update until it was reloaded. Therefore, my question has switched to how to make the view update when place is updated, without using #ObservedObject in EditPlaceForm.swift, which caused the back button to disappear.
Updated Pieces of Code
PlaceDetail.swift
Original:
#ObservedObject var place: Place
Updated:
var place: Place
EditPlaceForm.swift
Updated:
//
// EditPlaceForm.swift
//
import SwiftUI
import CoreLocation
struct EditPlaceForm: View {
#Environment(\.presentationMode) private var presentationMode
#Environment(\.managedObjectContext) private var viewContext
var place: Place
#State private var name = ""
#State private var street = ""
#State private var city = ""
#State private var state = ""
#State private var zip = ""
#State private var country = ""
#State private var showAlert = false
#State private var alertText = ""
var body: some View {
NavigationView {
Form {
Section (header: Text("Name")) {
TextField("Name", text: $name)
}
Section (header: Text("Address")) {
VStack {
TextField("Street", text: $street)
TextField("City", text: $city)
HStack {
TextField("State", text: $state)
Divider()
TextField("ZIP", text: $zip)
}
TextField("Country", text: $country)
}
}
Section(header: Text("Save")) {
Button (action: {
save()
}, label: {
Text("Save")
})
}
}
.navigationTitle("Edit Place")
.alert(isPresented: $showAlert, content: {
Alert(title: Text("Error"), message: Text(alertText))
})
}
.onAppear {
loadPlaceData()
}
}
func loadPlaceData() {
name = place.name
street = place.street
city = place.city
state = place.state
zip = place.zip
country = place.country
}
func save() {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString("\(street), \(city) \(state), \(zip), \(country)", completionHandler: {
placemarks, error in
if error != nil {
print("Error forward geocoding")
print(error!.localizedDescription)
alertText = "Encountered geocoding error."
showAlert = true
return
}
if placemarks == nil {
print("No placemarks found")
alertText = "Location could not be found."
showAlert = true
return
}
let placemark = placemarks![0]
let testVars = [
placemark.thoroughfare,
placemark.locality,
placemark.administrativeArea,
placemark.country,
placemark.postalCode
]
for testVar in testVars {
if testVar == nil {
print("Not enough information to complete place")
alertText = "Not enough information."
showAlert = true
return
}
}
if placemark.location == nil {
print("Not enough information to complete place")
alertText = "Not enough information."
showAlert = true
return
}
viewContext.performAndWait {
place.street = placemark.subThoroughfare! + " " + placemark.thoroughfare!
place.city = placemark.locality!
place.state = placemark.administrativeArea!
place.country = placemark.country!
place.zip = placemark.postalCode!
place.latitude = placemark.location!.coordinate.latitude
place.longitude = placemark.location!.coordinate.latitude
do {
try viewContext.save()
presentationMode.wrappedValue.dismiss()
} catch {
print("ERROR: FAILED TO SAVE PLACE DETAILS")
alertText = "Failed to save."
showAlert = true
}
}
})
}
}
struct EditPlaceForm_Previews: PreviewProvider {
private static var viewContext = PersistenceController.preview.container.viewContext
static var previews: some View {
EditPlaceForm(place: Place.getDefault(context: viewContext))
}
}
Original Post
Setup
My app has a main page which is a navigation view. On that page, there are navigation links. One of those links leads to the "Places" page which contains a list. The list items on that page are navigation links to detail pages for CoreData entities called Place. In the detail pages I have an edit button that opens an editing form as a sheet.
Intended Behavior
You open the details view from the "Places" view, using the navigation link. You then click the edit button and change some of the information. When you click save, the editing form closes and the changes are reflected in the details view. You can then click the back button to go back to the "Places" view.
Current Behavior
You open the details view from the "Places" view, using the navigation link. You then click the edit button and change some of the information. When you save, the editing form closes and the changes are reflected in the details view, but the back button has disappeared and you are stuck in the details view.
Video: https://drive.google.com/file/d/1lzj-hOCb7hgMutQQwPl0ShR7l-c9sDib/view?usp=sharing
Code
"Places" view:
//
// PlacesList.swift
//
import SwiftUI
struct PlacesList: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity: Place.entity(), sortDescriptors: [
NSSortDescriptor(key: "name", ascending: true)
])
private var places: FetchedResults<Place>
#State private var showAddPlaceForm = false
var body: some View {
List {
ForEach (places) { place in
NavigationLink (destination: PlaceDetail(place: place)) {
PlaceRow(place: place)
}
}
.onDelete(perform: deleteRow)
}
.listStyle(PlainListStyle())
.navigationTitle(Text("Places"))
.navigationBarItems(trailing:
HStack {
Button(action: {
showAddPlaceForm = true
}, label: {
Image(systemName: "plus.circle")
.imageScale(.large)
})
.sheet(isPresented: $showAddPlaceForm) {
NewPlaceForm()
}
}
)
}
func deleteRow(at offsets: IndexSet) {
for offset in offsets {
viewContext.performAndWait {
viewContext.delete(places[offset])
try? viewContext.save()
}
}
}
}
struct PlacesList_Previews: PreviewProvider {
private static var viewContext = PersistenceController.preview.container.viewContext
static var previews: some View {
NavigationView {
PlacesList()
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
}
PlaceRow.swift
//
// PlaceRow.swift
//
import SwiftUI
struct PlaceRow: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var place: Place
var body: some View {
VStack (alignment: .leading) {
Text(place.name)
.font(.headline)
Group {
Text(place.street)
Text(place.city) + Text(", ") + Text(place.state)
}
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
struct PlaceRow_Previews: PreviewProvider {
private static var viewContext = PersistenceController.preview.container.viewContext
static var previews: some View {
List {
PlaceRow(place: Place.getDefault(context: viewContext))
}
}
}
PlaceDetail.swift
//
// PlaceDetail.swift
//
import SwiftUI
import CoreLocation
struct PlaceDetail: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var place: Place
#State private var showEditForm = false
var body: some View {
List {
VStack (alignment: .leading) {
Text("Address")
.font(.headline)
Group {
Text(place.street)
Text(place.cityStateZIPString)
Text(place.country)
}
.font(.subheadline)
.foregroundColor(.secondary)
}
VStack {
MapView(place: place)
.frame(height: 300)
}
if place.encodedAddress != nil {
Link(destination:
URL(string: "http://maps.apple.com/?daddr=\(place.encodedAddress!)")!,
label: {
HStack {
Text("Get Directions with Apple Maps")
Spacer()
Image(systemName: "chevron.forward.circle")
.imageScale(.large)
}
.foregroundColor(.blue)
})
Link(destination:
URL(string: "https://www.google.com/maps/search/?api=1&destination=\(place.encodedAddress!)")!,
label: {
HStack {
Text("Get Directions with Google Maps")
Spacer()
Image(systemName: "chevron.forward.circle")
.imageScale(.large)
}
.foregroundColor(.blue)
})
}
}
.toolbar {
Button(action: {
showEditForm = true
}, label: {
Image(systemName: "pencil.circle")
})
}
.navigationTitle(place.name)
.sheet(isPresented: $showEditForm, content: {
EditPlaceForm(place: place)
})
}
}
struct PlaceDetail_Previews: PreviewProvider {
static var viewContext = PersistenceController.preview.container.viewContext
static var previews: some View {
NavigationView {
PlaceDetail(place: Place.getDefault(context: viewContext))
.environment(\.managedObjectContext, viewContext)
}
}
}
EditPlaceForm.swift
//
// EditPlaceForm.swift
//
import SwiftUI
import CoreLocation
struct EditPlaceForm: View {
#Environment(\.presentationMode) private var presentationMode
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var place: Place
#State private var showAlert = false
#State private var alertText = ""
var body: some View {
NavigationView {
Form {
Section (header: Text("Name")) {
TextField("Name", text: $place.name)
}
Section (header: Text("Address")) {
VStack {
TextField("Street", text: $place.street)
TextField("City", text: $place.city)
HStack {
TextField("State", text: $place.state)
TextField("ZIP", text: $place.zip)
}
TextField("Country", text: $place.country)
}
}
Section(header: Text("Save")) {
Button (action: {
save()
}, label: {
Text("Save")
})
}
}
.navigationTitle("Edit Place")
.alert(isPresented: $showAlert, content: {
Alert(title: Text("Error"), message: Text(alertText))
})
}
}
func save() {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(place.address, completionHandler: {
placemarks, error in
if error != nil {
print("Error forward geocoding")
print(error!.localizedDescription)
alertText = "Encountered geocoding error."
showAlert = true
return
}
if placemarks == nil {
print("No placemarks found")
alertText = "Location could not be found."
showAlert = true
return
}
let placemark = placemarks![0]
let testVars = [
placemark.thoroughfare,
placemark.locality,
placemark.administrativeArea,
placemark.country,
placemark.postalCode
]
for testVar in testVars {
if testVar == nil {
print("Not enough information to complete place")
alertText = "Not enough information."
showAlert = true
return
}
}
if placemark.location == nil {
print("Not enough information to complete place")
alertText = "Not enough information."
showAlert = true
return
}
viewContext.performAndWait {
place.street = placemark.subThoroughfare! + " " + placemark.thoroughfare!
place.city = placemark.locality!
place.state = placemark.administrativeArea!
place.country = placemark.country!
place.zip = placemark.postalCode!
place.latitude = placemark.location!.coordinate.latitude
place.longitude = placemark.location!.coordinate.latitude
do {
try viewContext.save()
presentationMode.wrappedValue.dismiss()
} catch {
print("ERROR: FAILED TO SAVE PLACE DETAILS")
}
}
})
}
}
struct EditPlaceForm_Previews: PreviewProvider {
private static var viewContext = PersistenceController.preview.container.viewContext
static var previews: some View {
EditPlaceForm(place: Place.getDefault(context: viewContext))
}
}
I'm trying to make a simple Master-Detail-FileViewer app. In the last FileViewer view I want to have a button, which has an option to make the file favourite (every file has an "id" string, which is appended to an Environment object). When you favour them, this object is shown at the master view for quick access for the user, linking to the FileViewer view. However, when the user taps and goes there, the button is inactive - you cannot tap it and it gets black from blue. If you want to remove them from favourites, you can't.
I'd really appreciate to tell me what is wrong and how to make the button active. No error is shown and the app doesn't crash. It just doesn't work.
Thanks in advance!
The files are either "judgement" and "secondary", both have id and title properties. The second picture is the problematic one.
import SwiftUI
struct ContentView: View {
#EnvironmentObject var favouriteList: FavouritesList
var body: some View {
NavigationView {
List {
NavigationLink(destination: JudgementsView()) {
Text("Judgements")
}
NavigationLink(destination: SecondaryView()) {
Text("Secondary acts")
}
ScrollView(.horizontal, showsIndicators: false) {
VStack {
if favouriteList.items.isEmpty {
Text("Nothing favoured")
} else {
ForEach(favouriteList.items, id: \.self) { id in
VStack {
HStack {
ForEach(judgementsTAXraw.filter {
$0.id == id
}) { judgement in
NavigationLink(destination: FileViewer(file: judgement.id)) {
Text(judgement.title).padding()
}
}
}
HStack {
ForEach(secondaryTAXraw.filter {
$0.id == id
}) { secondary in
NavigationLink(destination: FileViewer(file: secondary.id)) {
Text(secondary.title).padding()
}
}
}
}
}
}
}
}
}
.navigationBarTitle(Text("Test"))
}
}
}
struct JudgementsView: View {
var body: some View {
List(judgementsTAXraw, id: \.id) { judgement in
NavigationLink(destination: FileViewer(file: judgement.id)) {
Text(judgement.title)
}
}
}
}
struct SecondaryView: View {
var body: some View {
List(secondaryTAXraw, id: \.id) { secondary in
NavigationLink(destination: FileViewer(file: secondary.id)) {
Text(secondary.title)
}
}
}
}
struct FileViewer: View {
var file: String
#State private var showCopySheet = false
#EnvironmentObject var favouriteList: FavouritesList
var body: some View {
Text(file)
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(trailing:
Button(action: {
self.showCopySheet = true
}) {
Image(systemName: "doc.on.doc").frame(minWidth: 40)
}.actionSheet(isPresented: $showCopySheet) {
ActionSheet(title: Text("What do you want to do?"), buttons: [
.destructive(Text("favText"), action: {
if let index = self.favouriteList.items.firstIndex(of: self.file) {
self.favouriteList.items.remove(at: index)
} else {
self.favouriteList.items.append(self.file)
}
}),
.cancel()
])
}
)
}
}
Aaaand in a separate file is the object:
import Foundation
class FavouritesList: ObservableObject {
#Published var items = [String]() {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "FavouredItems")
}
}
}
init() {
if let items = UserDefaults.standard.data(forKey: "FavouredItems") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([String].self, from: items) {
self.items = decoded
return
}
}
self.items = []
}
}