View not being updated when ViewModel gets new data - ios

I have an app that will make an API call once a day to retrieve new data.
When the app launches it will check if new data is available, save new data to core data, call core data to show data in a view.
Issue
After saving new data to core data my view will not display the newly save data, but instead display the default dummy data from my model. The new data will be displayed after relaunching the app.
Question
How can I display the newly save data in the view without having to relaunch the app? I'm using MVVM pattern. Below is my code with links to my code.
UVIndexNowModel
class UVIndexNowModel: ObservableObject
{
#Published var uvIndex: Int
#Published var dateTime: Date
init(uvIndex: Int = 0, dateTime: Date = Date())
{
self.uvIndex = uvIndex
self.dateTime = dateTime
}
}
UVIndexNowViewModel
class UVIndexNowViewModel: NSObject, ObservableObject
{
private var isFirstAppearance = true
private let moc = PersistentStore.shared.context
private let nowController: NSFetchedResultsController<UVHour>
#Published var data: UVIndexNowModel = UVIndexNowModel()
#Published var coreDateError: Bool = false
override init()
{
nowController = NSFetchedResultsController(fetchRequest: UVHour.uvIndexNowRequest,
managedObjectContext: moc,
sectionNameKeyPath: nil, cacheName: nil)
super.init()
nowController.delegate = self
do {
try nowController.performFetch()
let results = nowController.fetchedObjects ?? []
setUVIndex(from: results)
} catch {
print("failed to fetch items!")
}
}
func setUVIndex(from hours: [UVHour])
{
let date = Date()
let formatter = DateFormatter()
formatter.timeZone = .current
formatter.dateFormat = "MMM/d/yyyy hh a"
let todaystr = formatter.string(from: date)
print("UVIndexNowVM.setUVIndex() size of UVHour array: \(hours.count)")
for i in hours
{
let tempDateStr = formatter.string(from: i.wrappedDateTime)
if todaystr == tempDateStr
{
print("UV Now VM Date matches! \(tempDateStr)")
self.data.uvIndex = i.wrappedUVIndex
self.data.dateTime = i.wrappedDateTime
break
}
}
}
}
extension UVIndexNowViewModel: NSFetchedResultsControllerDelegate
{
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
{
print("UVIndexNowViewModel controllerDidChangeContent was called. New Stuff in DB")
guard let results = controller.fetchedObjects as? [UVHour] else { return }
setUVIndex(from: results)
}
}
UVIndexNowView
struct UVIndexNowView: View {
#ObservedObject var vm = UVIndexNowViewModel()
var body: some View {
VStack {
Text("\(vm.data.dateTime)")
Text("UV Index: \(vm.data.uvIndex)")
}
}
}

As your UVIndexNowModel is-a ObservableObject you need to observe it in separated view, like
struct UVIndexNowView: View {
#ObservedObject var vm = UVIndexNowViewModel()
var body: some View {
UVIndexNowDataView(data: vm.data) // << here !!
}
}
and
struct UVIndexNowDataView: View {
#ObservedObject var data: UVIndexNowModel
var body: some View {
VStack {
Text("\(data.dateTime)")
Text("UV Index: \(data.uvIndex)")
}
}
}

Related

How do I pass CoreData into UICalendarview

I'm trying to use my CoreData entity 'Item' to show decorations on a calendar (using UIcalendarview in iOS16). I have my view called CalView calling the CalendarHelper struct (where the UICalendarView is coded and setup). It is showing decorations on every day currently, but I want to only show decorations on the days where there is a CoreData entry, and then enable navigation when you click on those days to the detail view. The problem is that I can't figure out how to get the CoreData entity into the CalendarHelper and UICalendarView to use it. It's currently giving me an error stating that I can't pass a FetchedResult in to an expected ObservedResult. But when I change them all to FetchedResult on that function, it gives me an error saying it's expecting a type of Item.
NOTE: I posted this over here as well but I think I messed up that post by deleting the original when I updated it. https://stackoverflow.com/questions/75138925/pass-coredata-into-uicalendarview-to-show-decorations-and-click-on-date-for-deta
CALVIEW:
struct CalView: View {
var body: some View {
ZStack {
VStack {
HStack {
CalendarHelper(interval: DateInterval(start: .distantPast, end: .distantFuture))
}
}
}
}
}
CALENDARHELPER:
struct CalendarHelper: UIViewRepresentable {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: SleepSort.default.descriptors,
animation: .default)
private var items: FetchedResults<Item>
let interval: DateInterval
func makeUIView(context: Context) -> UICalendarView {
let calView = UICalendarView()
calView.delegate = context.coordinator
calView.calendar = Calendar(identifier: .gregorian)
calView.availableDateRange = interval
calView.fontDesign = .rounded
let dateSelection = UICalendarSelectionSingleDate(delegate: context.coordinator)
calView.selectionBehavior = dateSelection
calView.wantsDateDecorations = true
return calView
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self, items: items)
}
func updateUIView(_ uiView: UICalendarView, context: Context) {
}
class Coordinator: NSObject, UICalendarViewDelegate, UICalendarSelectionSingleDateDelegate {
var parent: CalendarHelper
#ObservedObject var items: Item
init(parent: CalendarHelper, items: ObservedObject<Item>) {
self.parent = parent
self._items = items
}
#MainActor
func calendarView(_ calendarView: UICalendarView, decorationFor dateComponents: DateComponents) -> UICalendarView.Decoration? {
let font = UIFont.systemFont(ofSize: 10)
let configuration = UIImage.SymbolConfiguration(font: font)
let image = UIImage(systemName: "star.fill", withConfiguration: configuration)?.withRenderingMode(.alwaysOriginal)
return .image(image)
}
func dateSelection(_ selection: UICalendarSelectionSingleDate,
didSelectDate dateComponents: DateComponents?) {
}
func dateSelection(_ selection: UICalendarSelectionSingleDate,
canSelectDate dateComponents: DateComponents?) -> Bool {
return true
}
}
}
I tested for a while to pass in FetchResults to the coordinator (as SwiftUI #FetchRequest works on a view only), and did not find a solution, but the recommendation to use a standard NSFetchRequest in or from the coordinator. The decorationFor method is called once for each calendar day in the displayed month - this makes the change lagging if that makes 30 NSFetchrequests - so we need to fetch once per month displayed and store the result.
Here is my my calendarView decorationFor method (simplified):
// --------------------------------------------------------------------------
//helper function to compare two dateComponents on day granularity
extension DateComponents {
func sameDate(source: DateComponents) -> Bool {
if let lhs = self.date,
let rhs = source.date,
Calendar.current.startOfDay(for: lhs) == Calendar.current.startOfDay(for: rhs) {
return true
} else {
return false
}
}
}
// -------------------------------------------------------
#MainActor func calendarView(_ calendarView: UICalendarView, decorationFor dateComponents: DateComponents) -> UICalendarView.Decoration? {
//in the coordinator, define:
// private var currentVisibleRange = DateComponents()
// private var result : [YourClass] = []
//check if the requested dateComponents are within the currentVisibleRange
if !currentVisibleRange.sameDate(source: calendarView.visibleDateComponents) {
//here the requested date is not in the currentVisibleRange, so we have to do a new fetch
let myFetch = makeMyFetchRequest(dateComponents) //whatever fetch request you need, based on NSFetchRequest
do {
result = try context.fetch(myFetch)
//result now contains an Array of NSManagedObjects, might need to be sorted, dependent on the underlying data
} catch {
print("Failed to fetch: \(error)")
}
//now we have to store the new range to avoid doing it again for the same range
currentVisibleRange = calendarView.visibleDateComponents
return .customView {
//return any UIView, e.g. a UILabel, or use UIHostingController if you want to return a Swift view
//the view must be a decoration for dateComponents.day
makeMyUILabel(forDay: dateComponents.day)
}
}//calendarView
This works well also on large datasets, one fetch per month displayed is sustainable and shows good performance

Trying to save data in UserDefaults and show them in list view

Trying to save some data in UserDefaults but I'm getting nil in the view.
I don't know where is the problem
This is my code in ContentView:
var saveButton: some View {
Button("Save Meal") {
let meal = Meal(name: self.mealGenerator.currentMeal!.name,
imageUrlString: self.mealGenerator.currentMeal!.imageUrlString,
ingredients: self.mealGenerator.currentMeal!.ingredients,
instructions: self.mealGenerator.currentMeal!.instructions,
area: self.mealGenerator.currentMeal!.area,
category: self.mealGenerator.currentMeal!.category)
self.savedMeals.meals.append(meal)
self.savedMeals.saveMeals()
}
This is my class I'm trying to save:
class SavedMeals: ObservableObject {
#Published var meals: [Meal]
func saveMeals() {
if let encoded = try? JSONEncoder().encode(meals) {
UserDefaults.standard.set(encoded, forKey: "Meals")
}
}
init() {
if let meals = UserDefaults.standard.data(forKey: "Meals") {
if let decoded = try? JSONDecoder().decode([Meal].self, from: meals) {
self.meals = decoded
return
}
}
self.meals = []
}
}
And I'm trying to list in a view:
struct SavedMealsView: View {
#ObservedObject var savedMeals: SavedMeals
var body: some View {
NavigationView {
List(savedMeals.meals) { meal in
Text(meal.name)
}
.navigationBarTitle("Saved Meals", displayMode: .inline)
}
}
}
You do meals = [] at the end of your init regardless of what you decode. Perhaps this will work better:
init() {
if data = UserDefaults.standard.data(forKey: "Meals") {
do {
meals = try JSONDecoder().decode([Meal].self, from: meals)
} catch {
assertionFailure("Oops!")
meals = []
}
} else {
meals = []
}
}

SwiftUI list not updating when the array changes

VStack(spacing: 0){
List{
ForEach(postsData.fetchedPosts, id: \.postID) { post in
SocialPostView(post: post, showAccount: self.$showAccount, fetchedUser: self.$fetchedUser)
.padding(.vertical)
.listRowInsets(EdgeInsets())
.onAppear {
self.elementOnAppear(post)
}
}
}
.pullToRefresh(isShowing: $isShowing) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isShowing = false
self.postsData.newFetch = true
self.postsData.fetchPosts(userInfo: self.userInfo)
}
}
}
private func elementOnAppear(_ post: Post) {
/* onAppear on the view is called when a view appears on screen.
elementOnAppear asks the view model if the element is the last one.
If so, we ask the view model to fetch new data. */
if self.postsData.isLastPostInList(post: post) {
self.postsData.fetchPosts(userInfo: self.userInfo)
}
}
When each list element appears, it checks if it's the last element in the array. If it is, it fetches more from Firestore and updates fetchedPosts. However, when the array updates, the List is not updated, so no new elements show up.
This is the ObservableObject, which publishes the array.
class SocialObservable: ObservableObject{
let db = Firestore.firestore()
let objectWillChange = ObservableObjectPublisher()
#Published var fetchedPosts = [Post]()
#Published var lastSnap : DocumentSnapshot?
#Published var reachedEnd = false
var currentListener: ListenerRegistration?
var newFetch = false {
willSet{
objectWillChange.send()
}
}
init(userInfo: UserData){
print(userInfo.uid)
fetchPosts(userInfo: userInfo)
}
func fetchPosts(userInfo: UserData){
var first: Query?
if lastSnap == nil || newFetch {
//not last snapshot, or just updated feed
if newFetch{
newFetch.toggle()
fetchedPosts = [] // clean up if new fetch
}
first = db.collection("posts")
.whereField("availability", arrayContains: userInfo.uid)
.order(by: "date", descending: true)
.limit(to: 1)
}
else {
first = db.collection("posts")
.whereField("availability", arrayContains: userInfo.uid)
.order(by: "date", descending: true)
.start(afterDocument: lastSnap!)
.limit(to: 1)
}
first?.getDocuments(completion: { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error: \(error.debugDescription)")
return
}
let doc = snapshot.documents.map({postFromDB(obj: $0.data(), id: $0.documentID)})
doc.map({print($0.postID)})
// append to fetched posts
self.fetchedPosts = self.fetchedPosts + doc
print(self.fetchedPosts.count)
//prepare for the next fetch
guard let lastSnapshot = snapshot.documents.last else {
// the collection is empty. no data fetched
self.reachedEnd = true
return
}
// save last snapshot
self.lastSnap = lastSnapshot
})
}
func isLastPostInList(post: Post) -> Bool {
return post.postID == fetchedPosts.last?.postID
}
}
Is there any workaround for this?
A couple of things
class SocialObservable: ObservableObject{
let db = Firestore.firestore()
// let objectWillChange = ObservableObjectPublisher() // << remove this
// ...
var newFetch = false {
willSet{
self.objectWillChange.send() // ObservableObject has default
}
}
// ...
and on update modify published on main queue
doc.map({print($0.postID)})
// append to fetched posts
DispatchQueue.main.async {
self.fetchedPosts = self.fetchedPosts + doc
}
print(self.fetchedPosts.count)

Escaping closure captures mutating 'self' parameter, Firebase

I have the following code, How can i accomplish this without changing struct into class. Escaping closure captures mutating 'self' parameter,
struct RegisterView:View {
var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
self.names = name //here is the error
}){(error) in
print("Error: \(error)")
}
init(){
LoadPerson()
}a
var body:some View{
//ui code
}
}
Firebasemanager.swift
struct FirebaseManager {
func fetchPerson(
success: #escaping (Person) -> (),
failure: #escaping (String) -> ()
) {
Database.database().reference().child("Person")
.observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
success(Person(dictionary: dictionary))
}
}) { (error) in
failure(error.localizedDescription)
}
}
}
SwiftUI view can be created (recreated) / copied many times during rendering cycle, so View.init is not appropriate place to load some external data. Use instead dedicated view model class and load explicitly only when needed.
Like
class RegisterViewModel: ObservableObject {
#Published var names = [String]()
func loadPerson() {
// probably it also worth checking if person has already loaded
// guard names.isEmpty else { return }
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
struct RegisterView: View {
// in SwiftUI 1.0 it is better to inject view model from outside
// to avoid possible recreation of vm just on parent view refresh
#ObservedObject var vm: RegisterViewModel
// #StateObject var vm = RegisterViewModel() // << only SwiftUI 2.0
var body:some View{
Some_Sub_View()
.onAppear {
self.vm.loadPerson()
}
}
}
Make the names property #State variable.
struct RegisterView: View {
#State var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success: { person in
guard let name = person.name else { return }
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
//...
}

SwiftUI Use EnvironmentObject as a model for my ViewModel

I have an EnvironmentObject which stores user data. After making a call to Firestore I'm filling it with data and using it everywhere. The problem occurs in my view model. I can easily retrieve data from this model but for more complex things I need to use a view model.
Can EnvironmentObject be used as a model for a view model or it should be used only for local preferences in the app, like storing some default values or preferences?
Is it better to use a separate model for my UserViewModel?
While it is very easy to access its data, It is very hard to fill it with dynamic data after making an external call for example. Especially in the SceneDelegate, where it's almost impossible because I can not make a network call there.
SceneDelegate
let userData = UserData()
Auth.auth().addStateDidChangeListener { (auth, user) in
if user != nil {
if let userDefaults = UserDefaults.standard.dictionary(forKey: "userDefaults") {
userData.profile = Profile(userDefaults: userDefaults)
userData.uid = userDefaults["uid"] as? String ?? ""
userData.documentReference = Firestore.firestore().document(userDefaults["documentReference"] as! String)
userData.loggedIn = true
}
}
}
window.rootViewController = UIHostingController(rootView: tabViewContainerView.environmentObject(userData))
UserData
final class UserData: ObservableObject {
#Published var profile = Profile.default
#Published var loggedIn: Bool = Auth.auth().currentUser != nil ? true : false
#Published var uid: String = ""
#Published var documentReference: DocumentReference = Firestore.firestore().document("")
#Published var savedItems = [SavedItem]()
init(document: DocumentSnapshot? = nil) {
if let document = document {
print("document")
let messagesDataArray = document["saved"] as? [[String: Any]]
let parsedMessaged = messagesDataArray?.compactMap {
return SavedItem(dictionary: $0)
}
self.savedItems = parsedMessaged ?? [SavedItem]()
}
}
}
UserViewModel
class UserViewModel: ObservableObject {
#Published var userData: UserData?
init(userData: UserData? = nil) {
self.userData = userData
}

Resources