Content appears from a split second then disappears all together - swift

I'm downloading data from FireStore. Data is retrieved perfectly. I have the data and can print information. The issue is, when I tap on a text/label to push to the intended view, I perform the function using the .onAppear function. My variables, from my ObservableClass are #Published. I have the data and can even set elements based on the data retrieved. I'm using the MVVM approach and have done this a plethora of times throughout my project. However, this is the first time I have this particular issue. I've even used functions that are working in other views completely fine, yet in this particular view this problem persists. When I load/push this view, the data is shown for a split second and then the view/canvas is blank. Unless the elements are static i.e. Text("Hello World") the elements will disappear. I can't understand why the data just decides to disappear.
This is my code:
struct ProfileFollowingView: View {
#ObservedObject var profileViewModel = ProfileViewModel()
var user: UserModel
func loadFollowing() {
self.profileViewModel.loadCurrentUserFollowing(userID: self.user.uid)
}
var body: some View {
ZStack {
Color(SYSTEM_BACKGROUND_COLOUR)
.edgesIgnoringSafeArea(.all)
VStack(alignment: .leading) {
if !self.profileViewModel.isLoadingFollowing {
ForEach(self.profileViewModel.following, id: \.uid) { user in
VStack {
Text(user.username).foregroundColor(.red)
}
}
}
}
} .onAppear(perform: {
self.profileViewModel.loadCurrentUserFollowing(userID: self.user.uid)
})
}
}
This is my loadFollowers function:
func loadCurrentUserFollowing(userID: String) {
isLoadingFollowing = true
API.User.loadUserFollowing(userID: userID) { (user) in
self.following = user
self.isLoadingFollowing = false
}
}
I've looked at my code that retrieves the data, and it's exactly like other features/functions I already have. It's just happens on this view.

Change #ObservedObject to #StateObject
Update:
ObservedObject easily gets destroyed/recreated whenever a view is re-created, while StateObject stays/exists even when a view is re-created.
For more info watch this video:
https://www.youtube.com/watch?v=VLUhZbz4arg

It looks like API.User.loadUserFollowing(userID:) may be asynchronous - may run in the background. You need to update all #Published variables on the main thread:
func loadCurrentUserFollowing(userID: String) {
isLoadingFollowing = true
API.User.loadUserFollowing(userID: userID) { (user) in
DispatchQueue.main.async {
self.following = user
self.isLoadingFollowing = false
}
}
}

You might need to add like this: "DispatchQueue.main.asyncAfter"
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.profileViewModel.loadCurrentUserFollowing(userID: self.user.uid)
}
}

Related

How can I call a function of a child view from the parent view in swiftUI to change a #state variable?

I'm trying to get into swift/swiftui but I'm really struggling with this one:
I have a MainView containing a ChildView. The ChildView has a function update to fetch the data to display from an external source and assign it to a #State data variable.
I'd like to be able to trigger update from MainView in order to update data.
I've experienced that update is in fact called, however, data is reset to the initial value upon this call.
The summary of what I have:
struct ChildView: View {
#State var data: Int = 0
var body: some View {
Text("\(data)")
Button(action: update) {
Text("update") // works as expected
}
}
func update() {
// fetch data from external source
data = 42
}
}
struct MainView: View {
var child = ChildView()
var body: some View {
VStack {
child
Button(action: {
child.update()
}) {
Text("update") // In fact calls the function, but doesn't set the data variable to the new value
}
}
}
}
When googling for a solution, I only came across people suggesting to move update and data to MainView and then pass a binding of data to ChildView.
However, following this logic I'd have to blow up MainView by adding all the data access logic in there. My point of having ChildView at all is to break up code into smaller chunks and to reuse ChildView including the data access methods in other parent views, too.
I just cannot believe there's no way of doing this in SwiftUI.
Is completely understandable to be confused at first with how to deal with state on SwiftUI, but hang on there, you will find your way soon enough.
What you want to do can be achieved in many different ways, depending on the requirements and limitations of your project.
I will mention a few options, but I'm sure there are more, and all of them have pros and cons, but hopefully one can suit your needs.
Binding
Probably the easiest would be to use a #Binding, here a good tutorial/explanation of it.
An example would be to have data declared on your MainView and pass it as a #Binding to your ChildView. When you need to change the data, you change it directly on the MainView and will be reflected on both.
This solutions leads to having the logic on both parts, probably not ideal, but is up to what you need.
Also notice how the initialiser for ChildView is directly on the body of MainView now.
Example
struct ChildView: View {
#Binding var data: Int
var body: some View {
Text("\(data)")
Button(action: update) {
Text("update") // works as expected
}
}
func update() {
// fetch data from external source
data = 42
}
}
struct MainView: View {
#State var data: Int = 0
var body: some View {
VStack {
ChildView(data: $data)
Button(action: {
data = 42
}) {
Text("update") // In fact calls the function, but doesn't set the data variable to the new value
}
}
}
}
ObservableObject
Another alternative would be to remove state and logic from your views, using an ObservableObject, here an explanation of it.
Example
class ViewModel: ObservableObject {
#Published var data: Int = 0
func update() {
// fetch data from external source
data = 42
}
}
struct ChildView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
Text("\(viewModel.data)")
Button(action: viewModel.update) {
Text("update") // works as expected
}
}
}
struct MainView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
ChildView(viewModel: viewModel)
Button(action: {
viewModel.update()
}) {
Text("update") // In fact calls the function, but doesn't set the data variable to the new value
}
}
}
}

SwiftUI does not show ProgressView until after task is complete

I have a swift UI view that when tapped should show a progress view:
struct ProjectItem: View {
#EnvironmentObject var controller: ProjectController
#State var showLoadingIcon: Bool = false
let document: Document
var body: some View {
VStack {
ZStack {
Text(document.name).font(Interface.Text.PopoverDialogLabel)
Text(document.editTime.toString(true)).font(.caption2).foregroundColor(.gray)
if showLoadingIcon {
ProgressView()
}
}
.padding(Interface.Sizes.StandardPadding)
.if(controller.editedDocumentID == nil) { $0.onTapGesture(count: 1, perform: {
// Open Project
showLoadingIcon = true //This occours after TransitionView
controller.openDocument(document: document)
TransitionView() //this happens before the progressView is shown
})}
}
}
When tapped it can take a couple of seconds to open the document and we would like to show a progressView to the user to display something is happening. However the progressView will only show to the user after the document has loaded.
In the view controller the openDoucment simply calls part of an app:
func openDocument(document: Document) {
app.setProject(document.id) //this takes a few seconds
}
app.setProject(document.id) is on the main thread and ideally, this will be moved to its own thread in the future but we cannot for now.
How can the progress view be displayed before the loadDocument call is made?
I have tried to wrap the following into a Task{}
controller.openDocument(document: document)
TransitionView()
I have also made the openDocument call async and sync which did not fix the issue.
I have also disabled the transitionView call and can see from my breakpoints that controller.openDocument call occurs before the
if showLoadingIcon {
ProgressView()
}
switches to showLoadingicon is switched - meaning that showLoadingIcons is checked by the app after controller.openDocument is completed and is shown.
The problem is that you are doing too much controller stuff in the view. Move the logic to control the view into the project controller.
In the controller – assuming it conforms to ObservableObject – add an enum and a state variable
enum ProjectState {
case idle, loading, loaded
}
#Published var state : ProjectState = .idle
and you have to make setProject really asynchronous to indicate when loading the data has been finished
func openDocument(document: Document) {
Task { #MainActor in
state = .loading
await app.setProject(document.id)
state = .loaded
}
}
Otherwise use a completion handler. As the code of setDocument is not part of the question you have to change it yourself.
In the view the conditional view modifier .if is very bad practice because it's not needed at all. You can disable the tap gesture much simpler with .allowsHitTesting. To show the appropriate view switch on controller.state
struct ProjectItem: View {
#EnvironmentObject var controller: ProjectController
let document: Document
var body: some View {
VStack {
ZStack {
Text(document.name).font(Interface.Text.PopoverDialogLabel)
Text(document.editTime.toString(true)).font(.caption2).foregroundColor(.gray)
switch controller.state {
case .idle: EmptyView()
case .loading: ProgressView()
case .loaded: TransitionView()
}
}
}
.padding(Interface.Sizes.StandardPadding)
.allowsHitTesting(controller.editedDocumentID == nil)
.onTapGesture {
controller.openDocument(document: document)
}
}
}

Update View on Realm Object Change

I am updating an object in my Realm Database but I am having a hard time getting it to reload any of my views as a result. R.realm is a wrapper around realm. Both it and ActiveManager are working correctly as I can look at the results in Realm Studio and see them updating. How can I trigger a refresh when returning to the Items screen after toggling active on the Item screen? Is there anyway of just adding some kind of observer to the App entry point so that whenever the Realm database is changed for any reason it invalidates all the views and causes a refresh everywhere? I'm coming from a typescript/react background so I'm having a hard time wrapping my head around the way swift is handling all this. Code below, I've truncated irrelevant parts for brevity
ManageView
struct ManageView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: ItemsView(kind: ITEM_KIND.Area)) {
Text("Areas")
}
NavigationLink(destination: ItemsView(
kind: ITEM_KIND.Scheme
)) {
Text("Schemes")
}
ItemsView
struct ItemsView: View {
#ObservedResults(Active.self) var active
#State var showAddItemModal: Bool = false
var kind: ITEM_KIND
var body: some View {
VStack {
List {
Section("Current") {
ForEach(getCurrent(), id: \._id) { item in
VStack {
NavigationLink(destination: ItemView(item: item)) {
Text("\(item.title)")
}
}
}
...
func getCurrent() -> Results<Item> {
let currentPeriod = R.realm.getByKey(Period.self, key: ActiveManager.shared.getPeriod())!
return R.realm.getWhere(Item.self) { item in item.kind == self.kind && item._id.in(currentPeriod.items) }
}
ItemView
struct ItemView: View {
#ObservedRealmObject var item: Item
#State var isActive: Bool = false
func viewWillAppear() {
print("appear")
isActive = ActiveManager.shared.getItems().contains(item._id)
}
var body: some View {
ScrollView {
VStack {
ZStack {
Toggle("Active", isOn: $isActive)
.padding(.horizontal)
.onChange(of: isActive) { value in
if value {
ActiveManager.shared.addItem(item: item)
} else {
ActiveManager.shared.removeAllItems()
}
}
Will come back and post a more thorough solution at a later date but I couldn't get Realm's property wrappers to work so in the top Manage View I created some observable objects that contained the item arrays wrapped with the #Published wrapper and then some functions for updating the arrays. I then passed these observable objects down into Items View and again into Item view. Whenever I changed my item I then used one of the update array functions on the observable object to trigger a state refresh.
item.realm != nil
is a solution wich work fine for me, but it throws a warning. I call it after dismissing a sheet.

When exactly SwiftUI releases ObservableObjects

I am trying to learn how SwiftUI works internally in terms of memory management. I have little doubt about it.
When I add a NavigationLink to the 2nd View which has some Search Functionality and also loading some data from the cloud.
Now when I came back to the root view, my observableObject class is still in memory.
Does anyone have any idea how SwiftUI manages the memory and release objects?
Here is a sample code of my experiment.
struct ContentView: View {
var body: some View {
NavigationView {
DemoView(screenName: "Home")
.navigationBarHidden(true)
}
}
}
struct DemoView:View {
var screenName:String
var body: some View {
VStack{
NavigationLink(destination: SecondView(viewModel:SecondViewModel())) {
Text("Take Me To Second View")
}
Text(self.screenName)
}
}
}
// Second View
class SecondViewModel:ObservableObject {
#Published var search:String = ""
#Published var user:[String] = []
func fetchRecords() -> Void {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) { [weak self] in
self?.user = ["Hello", "There"]
}
}
}
struct SecondView:View {
#ObservedObject var viewModel:SecondViewModel
var body: some View {
VStack {
TextField("Search Here", text: $viewModel.search)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
List(self.viewModel.user, id:\.self) { user in
Text("User \(user)")
}
}.onAppear{
self.viewModel.fetchRecords()
}
}
}
And this is what I received in-memory graph.
The object lifecycle in SwiftUI is as usual. An object is deallocated by ARC when there are no more references to it. You can add deinit { print("deinit")}
to your SecondViewModel and see when the object is deallocated. And yes, in your case a new SecondViewModel object will be created each time the DemoView body is evaluated, which is probably not what you want. I guggest you initialize and store the SecondViewModel object outside of the view hierarchy, and pass a reference to this global object in DemoView.body .
Ok, I probably don't remember other similar post on the same issue, but the reason of it because your SecondView, cause it is a value, still is in NavigationView when you press back, as long as until another NavigationLink is activated.
So you need either to have different independent life-cycle for SecondViewModel or, if remain as-is, to add some reset/cleanup for it, so only pure empty object left, ie
}.onAppear{
self.viewModel.fetchRecords()
}.onDisappear {
self.viewModel.cleanup()
}

Dismiss navigation view when Core Data object is deleted

I'm attempting to use SwiftUI and CoreData to build a macOS application. This application's main window has a NavigationView, with list items bound to a fetch request, and selecting any of these items populates the detail view. The navigation view goes kind of like this:
NavigationView {
VStack(spacing: 0) {
List(fetchRequest) { DetailRow(model: $0) }
.listStyle(SidebarListStyle())
HStack {
Button(action: add) { Text("+") }
Button(action: remove) { Text("-") }
}
}
Text("Select a model object")
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
DetailRow is a NavigationLink that also defines the detail view:
NavigationLink(destination: ModelDetail(model: model)) {
Text(model.name)
}
I believe that the contents of ModelDetail isn't very important; either way, I'm fairly flexible with it.
In the navigation view, the "-" button, which calls the remove method, should delete the currently-selected model object and return to the default, empty detail view. Unfortunately, I'm struggling to come up with the right way to do this. I believe that I need the following interactions to happen:
subview communicates to navigation view which model object is currently selected
user clicks "-" button, navigation view's remove method deletes currently selected object
subview notices that its model object is being deleted
→ subview calls PresentationMode.dismiss()
Step 3 is the one I'm struggling with. Everything is working out alright so far without using view-model classes on top of the Core Data classes, but I feel stuck trying to figure out how to get the subview to call dismiss(). This needs to happen from the detail view, because it gets the PresentationMode from the environment, and the NavigationView changes it.
While I can get a Binding to the model's isDeleted property through #ObservedObject, I don't know how I can actually react to that change; Binding appears to use publishers under the hood, but they don't expose a publisher that I could hook up to with onPublish, for instance.
KVO over isDeleted might be possible, but listening from a value type isn't great; there's no good place to remove the observer, which could become problematic were the app to run for too long.
What's the guidance for this type of problem?
Heres my solution.
This is my NoteDetailView. It allows deletion from this view, or the "master" view in the Navigation hierarchy. This solution works on Mac, iPad, and iPhone.
I added an optional dateDeleted to my Entity. When a record is deleted, I simply add a value of Date() to this attribute and save the context. In my FetchRequests, I simply predicate for dateDeleted = nil. I'm going to add a trash can and stuff to my app later so people can view or permanently empty their trash.
Then I use a state variable and a notification to clear my View. You can change the code up for the functionality you want:
struct NoteDetailView: View {
var note: Note
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#Environment(\.managedObjectContext) var managedObjectContext
#State var noteBody: String = ""
#State var showEditNoteView: Bool = false
#State var showEmptyView: Bool = false
init(note: Note) {
self.note = note
self._noteBody = State(initialValue: note.body)
}
var body: some View {
VStack {
if (!showEmptyView) {
Text("NOT DELETED")
}
else {
EmptyView()
}
}
.navigationBarTitle(!showEmptyView ? note.title : "")
.navigationBarItems(trailing:
HStack {
if (!showEmptyView) {
Button(action: {
self.showEditNoteView.toggle()
}, label: {
NavBarImage(image: "pencil")
})
.sheet(isPresented: $showEditNoteView, content: {
EditNoteView(note: self.note).environment(\.managedObjectContext, self.managedObjectContext)
})
}
}
)
.onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)) { _ in
if (self.note.dateDeleted != nil) {
self.showEmptyView = true
self.presentationMode.wrappedValue.dismiss()
}
}
}
}