I am developing a mobile app on iOS and need to track some data when a user clicks a button. However, nothing is shown when I try to get the data according to the official doc. Here is my snippet:
import SwiftUI
import FirebaseStorage
import FirebaseCore
import FirebaseFirestore
struct Station_View: View {
#State private var showingAlert = false
var ref: Firestore!
var station_ : station
var food : [food] = []
var body: some View {
VStack(alignment: .leading, spacing: 10, content: {
VStack {
ForEach(station_.menu_items, id: \.self) { i in
Divider()
.frame(width: 400, height: 1)
.background(Color("Black"))
.padding(.vertical,0)
HStack {
VStack (alignment: .leading) {
Text(i.name + ", " + i.calories + "cal, protein: " + i.protein)
.font(.headline)
.foregroundColor(Color("Black"))
}.padding(.leading, 8)
Spacer()
if (Int(i.protein)! > 10) {
Button(action: {
// print("Button action")
////////// I retrieved data here //////////////////
let docRef = ref?.collection("users").document("7lqIqxc7SGPrbRhhQWZ0rdNuKnb2")
docRef?.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
} else {
print("Document does not exist")
}
}
////////// I retrieved data here //////////////////
self.showingAlert = true
}) {
HStack {
Image(systemName: "p.circle")
Text("+50xp")
}.padding(10.0)
.overlay(
RoundedRectangle(cornerRadius: 6.0)
.stroke(lineWidth: 2.0)
)
}
.alert(isPresented: $showingAlert) {
() -> Alert in
Alert(title: Text("Congratulations!"), message: Text("You had a protein meal, XP+50!"), dismissButton: .default(Text("OK")))
}
}
if (i.is_vegan) {
Button(action: {
// print("Button action")
////////// I retrieved data here //////////////////
let docRef = ref?.collection("users").document("7lqIqxc7SGPrbRhhQWZ0rdNuKnb2")
docRef?.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
} else {
print("Document does not exist")
}
}
////////// I retrieved data here //////////////////
self.showingAlert = true
}) {
HStack {
Image(systemName: "leaf")
Text("+50xp")
}.padding(10.0)
.overlay(
RoundedRectangle(cornerRadius: 6.0)
.stroke(lineWidth: 2.0)
)
}
.alert(isPresented: $showingAlert) {
() -> Alert in
Alert(title: Text("Congratulations!"), message: Text("You had a vegan meal, XP+50!"), dismissButton: .default(Text("OK")))
}
}
}
.padding(.init(top: 12, leading: 0, bottom: 12, trailing: 0))
}
}
} )
}
}
What can I do to make it come true? I am expecting to update only one key-value pair while the others remain the same when the data is collected back.
Firstly, when working with SwiftUI you should always use a ViewModel. This is a weird transition at first but it will make your code infinitely easier to understand and keep track of. Here's the basic structure.
View Model
class YourViewModel: ObservableObject {
#Published var isTrue = false
func isValueTrue(){
print(isTrue.description)
}
}
Notice that there are a few things going on here, the ObservableObject and #Published essentially this means that the object YourViewModel can be observed with published properties, or the ones that can be bound to a view. To use it in a view you can do this.
View
struct YourView: View {
//This is your ViewModel reference.
//Use is to bind all your details to the view with
//something like yourViewModel.firstName or $yourViewModel.firstName
#observedObject var yourViewModel = YourViewModel()
var body: some View {
Button("Change Bool") {
yourViewModel.isTrue.toggle()
yourViewModel.isValueTrue()
}
}
}
This is the basic structure for an MVVM pattern and will save you tons of space in your view, making it much much easier to read and maintain. Typically you'll have a separate .swift file for the View and for the ViewModel try not to combine them, and abstract as much as you can.
To answer the ultimate question, how do you retrieve data from Firebase and update that same data? Well, the answer is as follows, I will demonstrate using a function and a property within a ViewModel that you can Bind to your views to update them.
Getting Firebase Data
//These properties are a part of the VIEWMODEL and can be bound to the view
//Using yourViewModel.someProperty
#Published var firstName = ""
#Published var lastName = ""
#Published var email = ""
func fetchFirebaseData() {
guard let uid = Auth.auth().currentUser?.uid else {
print("Handle Error")
return
}
//Create Database Reference
let db = Firestore.firestore()
//Reference the collection and document. In this example
//I'm accessing users/someUserId
let fsUserProfile = db.collection("users").document(uid)
//Request the document
fsUserProfile.getDocument { (snapshot, err) in
if err != nil { return }
self.fetchImageFromURL(url: URL(string: snapshot?.get("profile_image_url") as? String ?? "")!)
self.firstName = snapshot?.get("first_name") as? String ?? ""
self.lastName = snapshot?.get("last_name") as? String ?? ""
self.email = snapshot?.get("email") as? String ?? ""
}
}
Updating Firebase Data
This is a simple way of updating your firebase data. This is handled by passing a dictionary with a key which is the field and a value associated with it. WARNING: DO NOT use setData(...) it will clear everything else that you had in there. setData(...) is useful for first time data creation such as registering an account, creating a new entry, etc..
func updateFirebaseData(firstName: String) {
if let user = Auth.auth().currentUser {
let db = Firestore.firestore()
db.collection("users").document(user.uid).updateData(["first_name": firstName])
}
}
Usage
struct YourView: View {
#observedObject var yourViewModel = YourViewModel()
var body: some View {
VStack {
//Fetching Example
VStack {
Button("Fetch Data") {
yourViewModel.fetchFirebaseData()
}
Text(yourViewModel.firstName)
}
//Setting Example
VStack {
Button("Update Data") {
//You could have this "John" value, a property
//of your ViewModel as well, or a text input, or whatever
//you want.
yourViewModel.updateFirebaseData(firstName: "John")
}
}
}
}
}
Notice how much cleaner the MVVM structure is when working in SwiftUI, once you do it for a few days, it will become second nature.
Related
I am trying to transfer the same API data from the first view and display the rest of the details in the second view
The problem is that the same first data is displayed in all index
struct model: Codable, Identifiable {
let id = UUID()
var details : String
var title : String
}
and fetch data here
class Api : ObservableObject{
#Published var models : [model] = []
func getData (url : String) {
guard let url = URL(string: url) else { return }
var request = URLRequest(url: url)
request.setValue("Bearer \(APIgetURL.Token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, responce, err in
guard let data = data else { return }
do {
let dataModel = try JSONDecoder().decode([model].self, from: data)
DispatchQueue.main.async {
self.models = dataModel
}
} catch {
print("error: ", error)
}
}
.resume()
}
}
Here I present the data in the first view
ForEach(modelname.models) { item in
NavigationLink(destination: DetilesTap1(modelname: modelname)) {
Rectangle()
.cornerRadius(15)
.frame(width: .infinity, height: 40)
.foregroundColor(.white)
.shadow(color: .black.opacity(0.3), radius: 20)
.padding(.trailing).padding(.leading)
.overlay(
HStack {
Text(item.title)
.padding(.trailing,30).padding(.leading,30)
Spacer()
Image(systemName: "arrow.down.doc.fill")
.padding(.trailing,30).padding(.leading,30)
}
)
}
}
Here is the second view
ForEach(modelname.models) { item in
VStack(alignment: .center) {
VStack(spacing: 10) {
Text(item.title)
Text(item.details)
Spacer()
}
}
}
I tried to put the id and the same problem
Despite all the explanations and methods available, they all have the same problem
You haven't posted a Minimal Reproducible Example (MRE), so this is an educated guess. Assuming that
#StateObject var modelname = Api()
Then your first view should be:
// keep you naming consistent. item is really of type Model, so call it model.
ForEach(modelname.models) { model in
// Pass just the model data you want to display, not everything.
NavigationLink(destination: DetilesTap1(model: model)) {
...
}
in your second view:
struct DetailView: View {
let model: Model
var body: some View {
// the two VStack were redundant.
VStack(alignment: .center, spacing: 10) {
Text(model.title)
Text(model.details)
Spacer()
}
}
}
You just need to pass the data you want to display, not everything all over again. That is the point of the ForEach. It allows you to take each element and deal with it individually no indexes needed. Please study Apple’s SwiftUI Tutorials & Stanford’s CS193P. You need a better base of knowledge on how SwiftUI works.
Intro
Hi there. I recently asked a different question asking how to implement user-triggered FCMs. I quickly realised that in order to implement FCMs I needed to add another feature to my app, which is why I am here now. I'm trying, trust me.
My question / problem
In the picture I attached, you can see the text that is supposed to show when the user clicks on the friends tab. But it doesn't do that. It does when I refresh the list, because I put the function in the refreshable attribute. I did also put the function in the view initialisation, but it doesn't do what I want it to do. So my question would be, a) why it doesn't load? and b) what would be an approach to solve my problem?
Code reference
This function is called in the init{} of the view and .refreshable{} of the list inside the view. (I also tried adding it via .onAppear{} in the NavigationLink of the parent view.)
#State var bestfriend: String = ""
func getBestie() {
let db = Firestore.firestore()
let docRef = db.collection("users").document(email)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let bestie = document.get("bestie") as? String ?? "error: bestie"
bestfriend = String(bestie)
} else {
print("Document does not exist")
}
}
}
Image for reference
Thanks and annotation
Thank you very much in advance, I'm amazed every day by how amazing this community is and how so many people are willing to help. If you need me to add anything else, of course I will do that. I hope I'll be wise enough one day to help other people with their problems as well.
Edit
The view
import Firebase
import FirebaseAuth
import SDWebImage
import SDWebImageSwiftUI
import SwiftUI
struct View_Friend_Tab: View {
#ObservedObject var friends_model = Model_User()
#State var friendWho = ""
init() {
friends_model.getFriendlist()
//friends_model.getBestie()
getBestie()
}
//VARS
let gifurl = URL(string: "https://c.tenor.com/BTCEb08QgBgAAAAC/osita-iheme-aki-and-pawpaw.gif")
let avatarURL = URL(
string:
"https://firebasestorage.googleapis.com/v0/b/universerp-72af2.appspot.com/o/avatars%2Fanime-girl-white-hair-1-cropped.jpg?alt=media&token=efba4215-850d-41c8-8c90-385f7a572e94"
)
#State var showingAlert = false
#State var showFriendRequests = false
#State var bestfriend: String = ""
func getBestie() {
let db = Firestore.firestore()
let docRef = db.collection("users").document(email)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let bestie = document.get("bestie") as? String ?? "error: bestie"
bestfriend = String(bestie)
} else {
print("Document does not exist")
}
}
}
var body: some View {
if !showFriendRequests {
VStack {
NavigationView {
/*
List (friends_model.friend_list) { item in
HStack {
Text(item.email)
Spacer()
}
}
.refreshable{
friends_model.getFriendlist()
}
.listStyle(.grouped)
*/
List {
Section(header: Text("management")) {
NavigationLink(destination: View_Friend_Best_Tab()) {
Label("Select your bestie", systemImage: "star.fill")
}
NavigationLink(destination: View_Friend_Requests_Tab()) {
Label("Manage friend requests", systemImage: "person.fill.questionmark")
}
NavigationLink(destination: View_Friend_Add_Tab()) {
Label("Add friend", systemImage: "person.fill.badge.plus")
}
}
ForEach(friends_model.friend_list) { item in
let avURL = URL(string: item.avatarURL)
Section(header: Text(item.username)) {
HStack {
VStack(alignment: .leading) {
if bestfriend == item.email {
Text("Is your Shin'yū")
.foregroundColor(Color("lightRed"))
.fontWeight(.bold)
.font(.footnote)
}
Text(item.username)
.fontWeight(.bold)
.frame(alignment: .leading)
Text(item.email)
.font(.footnote)
.multilineTextAlignment(.leading)
}
Spacer()
WebImage(url: avURL)
.resizable(resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.clipShape(Circle())
.shadow(radius: 5)
.overlay(Circle().stroke(Color.black, lineWidth: 1))
}
Button("Remove", role: .destructive) {
showingAlert = true
}
.alert("Do you really want to remove this friend?", isPresented: $showingAlert) {
HStack {
Button("Cancel", role: .cancel) {}
Button("Remove", role: .destructive) {
friendWho = item.email
removeFriend()
withAnimation {
friends_model.getFriendlist()
}
}
}
}
}
}
}
.navigationTitle("Your Friends")
.navigationViewStyle(.automatic)
.refreshable {
friends_model.getFriendlist()
getBestie()
}
.listStyle(.insetGrouped)
Spacer()
}
}
} else {
View_Friend_Requests_Tab()
}
}
}
struct View_Friend_Tab_Previews: PreviewProvider {
static var previews: some View {
View_Friend_Tab()
}
}
As previously stated, the function is being called in the init block and when the list is refreshed.
As a general recommendation, use view models to keep your view code clean.
In SwiftUI, a view's initialiser should not perform any expensive / long-running computations. Keep in mind that in SwiftUI, a view is only a description of your UI, not the UI itself. Any state management should be handled outside of the initialiser.
In your case, use .onAppear or .task:
import Firebase
import FirebaseAuth
import SDWebImage
import SDWebImageSwiftUI
import SwiftUI
class FriendsViewModel: ObservableObject {
#Published var bestfriend: String = ""
func getBestie() {
let db = Firestore.firestore()
let docRef = db.collection("users").document(email)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
// consider using Codable for mapping - see https://peterfriese.dev/posts/firestore-codable-the-comprehensive-guide/
let bestie = document.get("bestie") as? String ?? "error: bestie"
self.bestfriend = String(bestie)
} else {
print("Document does not exist")
}
}
}
// add other functions and properties here.
}
struct FriendsView: View {
#ObservedObject var viewModel = FriendsViewModel()
//VARS
let gifurl = URL(string: "https://c.tenor.com/BTCEb08QgBgAAAAC/osita-iheme-aki-and-pawpaw.gif")
let avatarURL = URL(
string:
"https://firebasestorage.googleapis.com/v0/b/universerp-72af2.appspot.com/o/avatars%2Fanime-girl-white-hair-1-cropped.jpg?alt=media&token=efba4215-850d-41c8-8c90-385f7a572e94"
)
#State var showingAlert = false
#State var showFriendRequests = false
#State var bestfriend: String = ""
var body: some View {
if !showFriendRequests {
VStack {
NavigationView {
List {
Section(header: Text("management")) {
NavigationLink(destination: SelectBestieView()) {
Label("Select your bestie", systemImage: "star.fill")
}
NavigationLink(destination: ManageFriendRequestsView()) {
Label("Manage friend requests", systemImage: "person.fill.questionmark")
}
NavigationLink(destination: AddFriendsView()) {
Label("Add friend", systemImage: "person.fill.badge.plus")
}
}
ForEach(viewModel.friends) { friend in
let avURL = URL(string: friend.avatarURL)
Section(header: Text(friend.username)) {
HStack {
VStack(alignment: .leading) {
if bestfriend == friend.email {
Text("Is your Shin'yū")
.foregroundColor(Color("lightRed"))
.fontWeight(.bold)
.font(.footnote)
}
Text(friend.username)
.fontWeight(.bold)
.frame(alignment: .leading)
Text(friend.email)
.font(.footnote)
.multilineTextAlignment(.leading)
}
Spacer()
WebImage(url: avURL)
.resizable(resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.clipShape(Circle())
.shadow(radius: 5)
.overlay(Circle().stroke(Color.black, lineWidth: 1))
}
Button("Remove", role: .destructive) {
showingAlert = true
}
.alert("Do you really want to remove this friend?", isPresented: $showingAlert) {
HStack {
Button("Cancel", role: .cancel) {}
Button("Remove", role: .destructive) {
friendWho = friend.email
viewModel.removeFriend()
withAnimation {
viewModel.getFriendlist()
}
}
}
}
}
}
}
.navigationTitle("Your Friends")
.navigationViewStyle(.automatic)
.onAppear {
viewModel.getFriendlist()
viewModelgetBestie()
}
.refreshable {
viewModel.getFriendlist()
viewModelgetBestie()
}
.listStyle(.insetGrouped)
Spacer()
}
}
} else {
FriendRequestsView()
}
}
}
struct FriendsViewPreviews: PreviewProvider {
static var previews: some View {
FriendsView()
}
}
I am trying to create an Image carousel using tabview and loading pictures from firebase. Without showing any error message or code tabview crashing. Please shed some light on what's going wrong here.
struct HomeView : View{
var body : View{
NavigationView{
VStack{
ScrollView{
CategoryView(homeViewModel: homeViewModel)
PosterView(homeViewModel: homeViewModel)
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarTitle("")
}
}
}
struct PosterView : View {
#StateObject var homeViewModel : HomeViewModel = HomeViewModel()
#State var currentIndex: Int = 0
var timer = Timer.publish(every: 3, on: .main, in: .common)
func next(){
withAnimation{
currentIndex = currentIndex < homeViewModel.posterList.count ? currentIndex +
1 : 0
}
}
var body: some View{
Divider()
GeometryReader{ proxy in
VStack{
TabView(selection: $currentIndex){
ForEach(homeViewModel.posterList){ item in
let imgURL = homeViewModel.trendingImgDictionary[item.id ?? ""]
AnimatedImage(url: URL(string: imgURL ?? ""))
}
}.tabViewStyle(PageTabViewStyle())
.padding()
.frame(width: proxy.size.width, height: proxy.size.height)
.onReceive(timer) { _ in
next()
}
.onTapGesture {
print("Tapped")
}
}
}
}
}
ViewModel: It contains two methods to fetch data and pictures from Firebase. That's working fine and am getting proper data. The only issue is while displaying it tabview crashes without showing any error messages.
class HomeViewModel : ObservableObject {
#Published var posterList : [TrendingBanner] = []
#Published var trendingImgDictionary : [String : String] = [:]
init() {
self.fetchTrendingList()
}
func fetchTrendingList() {
self.posterList.removeAll()
firestore.collection(Constants.COL_TRENDING).addSnapshotListener { snapshot, error in
guard let documents = snapshot?.documents else{
print("No Documents found")
return
}
self.posterList = documents.compactMap({ (queryDocumentSnapshot) -> TrendingBanner? in
return try? queryDocumentSnapshot.data(as:TrendingBanner.self )
})
print("Trending list \(self.posterList.count)")
print(self.posterList.first?.id)
let _ = self.posterList.map{ item in
self.LoadTrendingImageFromFirebase(id: item.id ?? "")
}
}
}
func LoadTrendingImageFromFirebase(id : String) {
let storageRef = storageRef.reference().child("trending/\(id)/\(id).png")
storageRef.downloadURL { (url, error) in
if error != nil {
print((error?.localizedDescription)!)
return
}
self.trendingImgDictionary[id] = url!.absoluteString
print("Trending img \(self.trendingImgDictionary)")
}
}
If you open SwiftUI module sources, you'll see the comment on top of TabView:
Tab views only support tab items of type Text, Image, or an image
followed by text. Passing any other type of view results in a visible but
empty tab item.
You're using AnimatedImage which is probably not intended to be supported by TabView.
Update
I made a library that liberates the SwiftUI _PageView which can be used to build a nice tab bar. Check my story on that.
Had the same issue recently on devices running iOS 14.5..<15. Adding .id() modifier to the TabView solved it.
Example:
TabView(selection: $currentIndex) {
ForEach(homeViewModel.posterList) { item in
content(for: item)
}
}
.tabViewStyle(PageTabViewStyle())
.id(homeViewModel.posterList.count)
Am trying to implement like and unlike button, to allow a user to like a card and unlike. When a user likes a button the id is saved and the icon changes from line heart to filled heart. I can save the id correctly but the issue is that at times the icon does not switch to filled one to show the user the changes especially after selecting the first one. The subsequent card won't change state but remain the same, while it will add save the id correctly. To be able to see the other card I have to, unlike the first card it cand display both like at the same time. I have tried both Observable and Environmental.
My Class to handle like and unlike
import Foundation
import Disk
class FavouriteRest: ObservableObject {
#Published private var fav = [Favourite]()
init() {
getFav()
}
func getFav(){
if let retrievedFav = try? Disk.retrieve("MyApp/favourite.json", from: .documents, as: [Favourite].self) {
fav = retrievedFav
} else {
print("")
}
}
//Get single data
func singleFave(id: String) -> Bool {
for x in fav {
if id == x.id {
return true
}
return false
}
return false
}
func addFav(favourite: Favourite){
if singleFave(id: favourite.id) == false {
self.fav.append(favourite)
self.saveFave()
}
}
//Remove Fav
func removeFav(_ favourite: Favourite) {
if let index = fav.firstIndex(where: { $0.id == favourite.id }) {
fav.remove(at: index)
saveFave()
}
}
//Save Fav
func saveFave(){
do {
try Disk.save(self.fav, to: .documents, as: "SmartParking/favourite.json")
}
catch let error as NSError {
fatalError("""
Domain: \(error.domain)
Code: \(error.code)
Description: \(error.localizedDescription)
Failure Reason: \(error.localizedFailureReason ?? "")
Suggestions: \(error.localizedRecoverySuggestion ?? "")
""")
}
}
}
Single Card
#EnvironmentObject var favourite:FavouriteRest
HStack(alignment: .top){
VStack(alignment: .leading, spacing: 4){
Text(self.myViewModel.myModel.title)
.font(.title)
.fontWeight(.bold)
Text("Some text")
.foregroundColor(Color("Gray"))
.font(.subheadline)
.fontWeight(.bold)
}
Spacer()
VStack{
self.favourite.singleFave(id: self.myViewModel.myModel.id) ? Heart(image: "suit.heart.fill").foregroundColor(Color.red) : Heart(image: "suit.heart").foregroundColor(Color("Gray"))
}
.onTapGesture {
if self.favourite.singleFave(id: self.myViewModel.myModel.id) {
self.favourite.removeFav(Favourite(id: self.myViewModel.myModel.id))
} else {
self.favourite.addFav(favourite: Favourite(id: self.myViewModel.myModel.id))
}
}
}
I was able to solve the question by moving the code inside the card view and using #States as shown below.
#State private var fav = [Favourite]()
#State var liked = false
VStack{
// Heading
HStack(alignment: .top){
VStack{
self.liked ? Heart(image:"suit.heart.fill").foregroundColor(Color.red) : Heart(image: "suit.heart").foregroundColor(Color("Gray"))
}
.onTapGesture {
if self.liked {
self.removeFav(self.singleFav!)
} else {
let faveID = self.myViewModel.myModel.id
let myFav = Favourite(id:faveID)
self.fav.append(myFav)
self.saveFave()
}
}
}
In the method for fetch and remove, I updated the #State var liked. Everything working as expected now.
I have a simple SwiftUI, CoreData application. The architecture is the basic list with
a second view for viewing the detail or editing the detail. The basic structure seems
to work with one important exception. When editing a record, the first edit after
app start is properly visible after returning to the ContentView list. The second and
further edits do not appear on the list when returning to the ContentView. The database
changes are correctly saved. Restarting the app will display the correct data. I have
also created a TabView. If I disable the return-to-main-list-after-edit code and use
just the TabView to switch, the changes are always presented.
Here's the code (I have removed most of the repetitive data fields).
In SceneDelegate:
let managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let tabby = TabBar().environment(\.managedObjectContext, managedObjectContext)
window.rootViewController = UIHostingController(rootView: tabby)
In ContentView:
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: ToDoItem.getAllToDoItems()) var toDoItems: FetchedResults<ToDoItem>
#State private var newToDoItem = ""
#State private var gonnaShow = true
#State private var show = false
var body: some View {
NavigationView {
List {
Section(header: Text("Records")) {
ForEach(self.toDoItems) { toDoItem in
NavigationLink(destination: EditToDo(toDoItem: toDoItem)) {
ToDoItemView(
idString: toDoItem.myID.uuidString,
title: toDoItem.title!,
firstName: toDoItem.firstName!,
lastName: toDoItem.lastName!,
createdAt: self.localTimeString(date: toDoItem.createdAt!)
)
}
}//for each
.onDelete { indexSet in
let deleteItem = self.toDoItems[indexSet.first!]
self.managedObjectContext.delete(deleteItem)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
.onMove(perform: move)
}
}
.navigationBarTitle("Customers")
.navigationBarItems(trailing: EditButton())
}
}
Separate file EditToDo:
#Environment(\.managedObjectContext) var managedObjectContext
#Environment(\.presentationMode) var presentationMode
var toDoItem: ToDoItem
#State private var updatedTitle: String = "No Title"
#State private var updatedFirstName: String = "No First Name"
//more data fields
#State private var updatedDate: Date = Date()
#State private var updatedDateString: String = "July 2019"
var body: some View {
ScrollView {
VStack {
Image("JohnForPosting")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 80, height: 80)
.clipShape(Circle())
VStack(alignment: .leading) {
Text("ToDo Title:")
.padding(.leading, 5)
.font(.headline)
TextField("Enter a Title", text: $updatedTitle)
.onAppear {
self.updatedTitle = self.toDoItem.title ?? ""
}
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(10)
VStack(alignment: .leading) {
Text("First Name:")
.padding(.leading, 5)
.font(.headline)
TextField("Enter a First Name", text: $updatedFirstName)
.onAppear {
self.updatedFirstName = self.toDoItem.firstName ?? ""
}
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(10)
//more data fields
VStack {
Button(action: ({
self.toDoItem.title = self.updatedTitle
self.toDoItem.firstName = self.updatedFirstName
//more data fields
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
self.updatedTitle = ""
self.updatedFirstName = ""
//more data fields
self.presentationMode.wrappedValue.dismiss()
})) {
Text("Save")
}
.padding()
}
.padding(10)
Spacer()
}
}
}
Separate file ToDoItemView:
var idString: String = ""
var title: String = ""
var firstName: String = ""
//more data fields
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("\(firstName) \(lastName)")
.font(.headline)
Text("\(createdAt) and \(idString)")
.font(.caption)
}
}
}
Xcode 11 - I guess this is the real release (post GM seed 2), Catalina Beta 19A558d,
iOS13.1
I thought the #Environment changes would always cause the body of ContentView to be
redrawn. And it is the weird behavior of first edit working, others not. Any guidance
would be appreciated.
I have the same problem with my current project, same basic code as you as well. A list of Projects fed by a FetchRequest that navigates to a second view for editing a Project.
Like you, if I make changes in the detail view, those changes are reflected in the List, but only the first time. If I reload the List by changing to a different tab and back again, the List will update showing the updated data.
The root of the problem comes down to creating a dynamic list using the results of a #FetchRequest. FetchRequest returns a Set of NSManagedObjects (reference types). Since the List is seeded with a Set of reference types, SwiftUI would only update the view when a reference changed, not necessarily when one of the properties of the object changed.
I think #FetchRequest would be fine for things like Pickers or menu options, but for a dynamic list of TodoItems, a better solution might be to create a TodoManager.
The TodoManager would be the single source of truth for all TodoItems. It would also setup a NSManagedObjectContextObserver that would trigger a objectWillChange.send() notification when any change was made, thus signaling that SwiftUI should refresh the list.
class TodoManager: ObservableObject {
private var moc: NSManagedObjectContext
public let objectWillChange = PassthroughSubject<Void, Never>()
var todoList: [TodoItem] = []
init( context: NSManagedObjectContext ) {
moc = context
setupNSManagedObjectContextObserver( context: context )
todoList = Array( TodoItem.fetch(in: context ))
}
func setupNSManagedObjectContextObserver( context moc: NSManagedObjectContext ) {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(managedObjectContextObjectsDidChange), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: moc)
notificationCenter.addObserver(self, selector: #selector(managedObjectContextWillSave), name: NSNotification.Name.NSManagedObjectContextWillSave, object: moc)
notificationCenter.addObserver(self, selector: #selector(managedObjectContextDidSave), name: NSNotification.Name.NSManagedObjectContextDidSave, object: moc)
}
#objc func managedObjectContextObjectsDidChange(notification: NSNotification) {
self.objectWillChange.send() // Crude but works
guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 { }
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 { }
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 { }
}
#objc func managedObjectContextWillSave(notification: NSNotification) {
guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 { }
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 { }
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 { }
}
#objc func managedObjectContextDidSave(notification: NSNotification) {
guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 { }
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 { }
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 { }
}
}
TodoManager is pretty dumb in that it sends an objectWillChange notification anytime any data in the ManagedObjectContext is changed.
Steve
Xcode 11.1 GM seed resolves this issue. For what it's worth, the Preview function for the core data fed list still does not work.