Update View on Realm Object Change - swift

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.

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
}
}
}
}

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()
}

SwiftUI #ObservedObject viewmodel in detail-view of List never released [duplicate]

This question already has answers here:
ObservedObject view-model is still in memory after the view is dismissed
(2 answers)
Closed 2 years ago.
I have a List with several items, that open a DetailView which in turn holds a viewmodel. The viewmodel is supposed to have a service class that gets initialized when the detail view appears and should be deinitialized when navigating back.
However, the first problem is that in my example below, all 3 ViewModel instances are created at the same time (when ContentView is displayed) and never get released from memory (deinit is never called).
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: DetailView()) {
Text("Link")
}
NavigationLink(destination: DetailView()) {
Text("Link")
}
NavigationLink(destination: DetailView()) {
Text("Link")
}
}
}
}
}
struct DetailView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Text("Hello \(viewModel.name)")
}
}
class ViewModel: ObservableObject {
#Published var name = "John"
private let heavyClient = someHeavyService()
init() { print("INIT VM") }
deinit { print("DEINIT VM") }
}
This is probably just how SwiftUI works, but I have a hard time thinking of a way to handle class objects that are part of a detail view's state, but are not supposed to instantiate until the detail view actually appears. An example would be video conferencing app, with rooms, where the room client that establishes connections etc. should only get initialized when actually entering the room and deinitialize when leaving the room.
I'd appreciate any suggestions on how to mange this. should I initialize the heavyClient at onAppear or something similar?
The problem is that DetailView() is getting initialized as part of the navigation link. One possible solution could be the LazyView from this post.
Implemented like so:
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
And then wrap the DetailView() in the LazyView():
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: LazyView(DetailView())) {
Text("Link")
}
NavigationLink(destination: LazyView(DetailView())) {
Text("Link")
}
NavigationLink(destination: LazyView(DetailView())) {
Text("Link")
}
}
}
}
}
The only issue with this workaround is that there seems to always be one instance of ViewModel sitting around, though it's a large improvement.

In SwiftUI, where are the control events, i.e. scrollViewDidScroll to detect the bottom of list data

In SwiftUI, does anyone know where are the control events such as scrollViewDidScroll to detect when a user reaches the bottom of a list causing an event to retrieve additional chunks of data? Or is there a new way to do this?
Seems like UIRefreshControl() is not there either...
Plenty of features are missing from SwiftUI - it doesn't seem to be possible at the moment.
But here's a workaround.
TL;DR skip directly at the bottom of the answer
An interesting finding whilst doing some comparisons between ScrollView and List:
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(1...100) { item in
Text("\(item)")
}
Rectangle()
.onAppear { print("Reached end of scroll view") }
}
}
}
I appended a Rectangle at the end of 100 Text items inside a ScrollView, with a print in onDidAppear.
It fired when the ScrollView appeared, even if it showed the first 20 items.
All views inside a Scrollview are rendered immediately, even if they are offscreen.
I tried the same with List, and the behaviour is different.
struct ContentView: View {
var body: some View {
List {
ForEach(1...100) { item in
Text("\(item)")
}
Rectangle()
.onAppear { print("Reached end of scroll view") }
}
}
}
The print gets executed only when the bottom of the List is reached!
So this is a temporary solution, until SwiftUI API gets better.
Use a List and place a "fake" view at the end of it, and put fetching logic inside onAppear { }
You can to check that the latest element is appeared inside onAppear.
struct ContentView: View {
#State var items = Array(1...30)
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text("\(item)")
.onAppear {
if let last == self.items.last {
print("last item")
self.items += last+1...last+30
}
}
}
}
}
}
In case you need more precise info on how for the scrollView or list has been scrolled, you could use the following extension as a workaround:
extension View {
func onFrameChange(_ frameHandler: #escaping (CGRect)->(),
enabled isEnabled: Bool = true) -> some View {
guard isEnabled else { return AnyView(self) }
return AnyView(self.background(GeometryReader { (geometry: GeometryProxy) in
Color.clear.beforeReturn {
frameHandler(geometry.frame(in: .global))
}
}))
}
private func beforeReturn(_ onBeforeReturn: ()->()) -> Self {
onBeforeReturn()
return self
}
}
The way you can leverage the changed frame like this:
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(0..<100) { number in
Text("\(number)").onFrameChange({ (frame) in
print("Origin is now \(frame.origin)")
}, enabled: number == 0)
}
}
}
}
The onFrameChange closure will be called while scrolling. Using a different color than clear might result in better performance.
edit: I've improved the code a little bit by getting the frame outside of the beforeReturn closure. This helps in the cases where the geometryProxy is not available within that closure.
I tried the answer for this question and was getting the error Pattern matching in a condition requires the 'case' keyword like #C.Aglar .
I changed the code to check if the item that appears is the last of the list, it'll print/execute the clause. This condition will be true once you scroll and reach the last element of the list.
struct ContentView: View {
#State var items = Array(1...30)
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text("\(item)")
.onAppear {
if item == self.items.last {
print("last item")
fetchStuff()
}
}
}
}
}
}
The OnAppear workaround works fine on a LazyVStack nested inside of a ScrollView, e.g.:
ScrollView {
LazyVStack (alignment: .leading) {
TextField("comida", text: $controller.searchedText)
switch controller.dataStatus {
case DataRequestStatus.notYetRequested:
typeSomethingView
case DataRequestStatus.done:
bunchOfItems
case DataRequestStatus.waiting:
loadingView
case DataRequestStatus.error:
errorView
}
bottomInvisibleView
.onAppear {
controller.loadNextPage()
}
}
.padding()
}
The LazyVStack is, well, lazy, and so only create the bottom when it's almost on the screen
I've extracted the LazyVStack plus invisible view in a view modifier for ScrollView that can be used like:
ScrollView {
Text("Some long long text")
}.onScrolledToBottom {
...
}
The implementation:
extension ScrollView {
func onScrolledToBottom(perform action: #escaping() -> Void) -> some View {
return ScrollView<LazyVStack> {
LazyVStack {
self.content
Rectangle().size(.zero).onAppear {
action()
}
}
}
}
}

SwiftUI: Forcing an Update

Normally, we're restricted from discussing Apple prerelease stuff, but I've already seen plenty of SwiftUI discussions, so I suspect that it's OK; just this once.
I am in the process of driving into the weeds on one of the tutorials (I do that).
I am adding a pair of buttons below the swipeable screens in the "Interfacing With UIKit" tutorial: https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
These are "Next" and "Prev" buttons. When at one end or the other, the corresponding button hides. I have that working fine.
The problem that I'm having, is accessing the UIPageViewController instance represented by the PageViewController.
I have the currentPage property changing (by making the PageViewController a delegate of the UIPageViewController), but I need to force the UIPageViewController to change programmatically.
I know that I can "brute force" the display by redrawing the PageView body, reflecting a new currentPage, but I'm not exactly sure how to do that.
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
#State var currentPage = 0
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
VStack {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
HStack(alignment: .center) {
Spacer()
if 0 < currentPage {
Button(action: {
self.prevPage()
}) {
Text("Prev")
}
Spacer()
}
Text(verbatim: "Page \(currentPage)")
if currentPage < viewControllers.count - 1 {
Spacer()
Button(action: {
self.nextPage()
}) {
Text("Next")
}
}
Spacer()
}
}
}
func nextPage() {
if currentPage < viewControllers.count - 1 {
currentPage += 1
}
}
func prevPage() {
if 0 < currentPage {
currentPage -= 1
}
}
}
I know the answer should be obvious, but I'm having difficulty figuring out how to programmatically refresh the VStack or body.
2021 SWIFT 1 and 2 both:
IMPORTANT THING! If you search for this hack, probably you doing something wrong! Please, read this block before you read hack solution!!!!!!!!!!
Your UI wasn't updated automatically because of you miss something
important.
Your ViewModel must be a class wrapped into ObservableObject/ObservedObject
Any field in ViewModel must be a STRUCT. NOT A CLASS!!!! Swift UI does not work with classes!
Must be used modifiers correctly (state, observable/observedObject, published, binding, etc)
If you need a class property in your View Model (for some reason) - you need to mark it as ObservableObject/Observed object and assign them into View's object !!!!!!!! inside init() of View. !!!!!!!
Sometimes is needed to use hacks. But this is really-really-really exclusive situation! In most cases this wrong way! One more time: Please, use structs instead of classes!
Your UI will be refreshed automatically if all of written above was used correctly.
Sample of correct usage:
struct SomeView : View {
#ObservedObject var model : SomeViewModel
#ObservedObject var someClassValue: MyClass
init(model: SomeViewModel) {
self.model = model
//as this is class we must do it observable and assign into view manually
self.someClassValue = model.someClassValue
}
var body: some View {
//here we can use model.someStructValue directly
// or we can use local someClassValue taken from VIEW, BUT NOT value from model
}
}
class SomeViewModel : ObservableObject {
#Published var someStructValue: Bool = false
var someClassValue: MyClass = MyClass() //myClass : ObservableObject
}
And the answer on topic question.
(hacks solutions - prefer do not use this)
Way 1: declare inside of view:
#State var updater: Bool = false
all you need to do is call updater.toggle()
Way 2: refresh from ViewModel
Works on SwiftUI 2
public class ViewModelSample : ObservableObject
func updateView(){
self.objectWillChange.send()
}
}
Way 3: refresh from ViewModel:
works on SwiftUI 1
import Combine
import SwiftUI
class ViewModelSample: ObservableObject {
private let objectWillChange = ObservableObjectPublisher()
func updateView(){
objectWillChange.send()
}
}
This is another solution what worked for me, using id() identifier. Basically, we are not really refreshing view. We are replacing the view with a new one.
import SwiftUI
struct ManualUpdatedTextField: View {
#State var name: String
var body: some View {
VStack {
TextField("", text: $name)
Text("Hello, \(name)!")
}
}
}
struct MainView: View {
#State private var name: String = "Tim"
#State private var theId = 0
var body: some View {
VStack {
Button {
name += " Cook"
theId += 1
} label: {
Text("update Text")
.padding()
.background(Color.blue)
}
ManualUpdatedTextField(name: name)
.id(theId)
}
}
}
Setting currentPage, as it is a #State, will reload the whole body.