I know that this may be tagged as a repeat question, but the ones that exist have not helped me implement delegates in Swift with SwiftUI. My issue can be see in the code below. I have a View and VM, TodaysVM. In here, I do a network request, that downloads movie data from an API, and saves the data to CoreData. In order to show graphs, the HabitVM needs to fetch the data from CoreData, and then do some long processing on it.
For this, I was looking into a way for one class to send a message notification to another class. So I stumbled onto using delegates (not sure if this is correct?). But essentially the idea would be that the TodayVM refreshes its data (which the user can do a pull to refresh on this page), it also sends a message to HabitsVM to start processing (so the data can be done by the time the user navigates to this page. I am not sure if there is a better way to do this, and I was trying to stay away from a singleton approach, passing one class into class, or creating a new instance of one of the classes.
In the code below, I have the delegates set up but 'print("Data update required!")' is never executed. From my research I understand this to be because I never set a value to the delegate? So the delegate value is nil, and therefore the
delegate?.requireUpdate(status: true)
Is never actually executed. Which makes sense, since delegate is an optional, but never assigned a value. So I was curious how I would assign it a value in SwiftUI? And if using delegates is even the best method to have to separate classes send updateRequired status updates.
import SwiftUI
import Foundation
// protocol
protocol MovieNotifierDelagate: AnyObject {
func requireUpdate(status: Bool)
}
struct HabitView {
#StateObject var habitVM = HabitsVM()
var body: some View {
Text("Hello")
}
}
struct TodaysView {
#StateObject var todayVM = TodayVM()
var body: some View {
Text("Hi!")
}
}
class HabitsVM: ObservableObject, MovieNotifierDelagate {
init() {
}
func requireUpdate(status: Bool) {
print("Data update required!")
reprocessData()
}
func reprocessData() {
print("Processing")
}
}
class TodayVM: ObservableObject {
weak var delegate: MovieNotifierDelagate?
init() {
//Does some network call and downloads data, and saves to CoreData
sendUpdate(status: true)
}
func sendUpdate(status: Bool) {
guard status else {
print ("No update")
return
}
delegate?.requireUpdate(status: true)
}
}
// Controlling Page
struct HomeView: View {
//Default the user to the today view
#State private var selection = 1
var body: some View {
TabView(selection: $selection) {
HabitView()
.tag(0)
.tabItem {
VStack {
Image(systemName: "books.vertical")
Text("Habits")
}
}
TodaysView()
.tag(1)
.tabItem {
VStack {
Image(systemName: "backward.end.alt")
Text("Rewind")
}
}
}
}
}
Related
I’m looking for the proper pattern and syntax to address my goal of having an ObservableObject instance that I can share amongst multiple views, but while keeping logic associated with it contained to another class. I’m looking to do this to allow different ‘controller’ classes to manipulate the properties of the state without the view needing to know which controller is acting on it (injected).
Here is a simplification that illustrates the issue:
import SwiftUI
class State: ObservableObject {
#Published var text = "foo"
}
class Controller {
var state : State
init(_ state: State) {
self.state = state
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var state = State()
var controller: Controller!
init() {
controller = Controller(state)
}
var body: some View {
VStack {
Text(controller.state.text) // always shows 'foo'
Button("Press Me") {
print(controller.state.text) // prints 'foo'
controller.changeState()
print(controller.state.text) // prints 'bar'
}
}
}
}
I know that I can use my ObservableObject directly and manipulate its properties such that the UI is updated in response, but in my case, doing so prevents me from having different ‘controller’ instances depending on what needs to happen. Please advise with the best way to accomplish this type of scenario in SwiftUI
To make SwiftUI view follow state updates, your controller needs to be ObservableObject.
SwiftUI view will update when objectWillChange is triggered - it's done automatically for properties annotated with Published, but you can trigger it manually too.
Using same publisher of your state, you can sync two observable objects, for example like this:
class Controller: ObservableObject {
let state: State
private var cancellable: AnyCancellable?
init(_ state: State) {
self.state = state
cancellable = state.objectWillChange.sink {
self.objectWillChange.send()
}
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var controller = Controller(State())
I got an EnvironmentObject that keeps track of the current user with a snapshotlistener connected to Firestore.
When the database get updated it triggers the EnvironmentObject as intended, but when in a child view presented with a NavigationLink the update dismisses the view, in this case PostView get dismiss when likePost() is called.
Should't the view be updated in the background?
Why is this happening, and what is the best way to avoid this?
class CurrentUser: ObservableObject {
#Published var user: User?
init() {
loadUser()
}
func loadUser() {
// firebase addSnapshotListener that sets the user property
}
}
MainView
struct MainView: View {
#StateObject var currentUser = CurrentUser()
var body some view {
TabView {
PostsView()
.enviromentObject(currentUser)
.tabItem {
Label("Posts", systemImage: "square.grid.2x2.fill")
}
}
}
}
Shows All Posts
struct PostsView: View {
#ObservableObject var viewModel = PostsViewModel()
#EnviromentObject var currentUser: CurrentUser
var body some view {
NavigationLink(destination: PostView()) {
HStack {
// Navigate to post item
}
}
}
}
Show Posts Detail
When im on this View and likes a post it's added to the document in Firestore, and triggers the snapshot listener. This causes the the PostView to be dismiss which is not what I want
struct PostView: View {
#ObservableObject var viewModel: PostViewModel
var body some view {
PostItem()
Button("Like Post") {
likePost()
// Saves the post into the current users "likedPosts" document field in Firestore
// This trigger the snapshotListener in currentUser and
}
}
}
It seems that PostsView is replaced, try to use StateObject in it, like
struct PostsView: View {
#StateObject var viewModel = PostsViewModel() // << here !!
...
I encountered a strange behaviour in my application when using Threads inside my ViewModel. It seems something is happening in Thread.init which causes a memory leak even when the deinit of the Thread object is called (I'm not even starting the thread).
I'm relatively new to Swift so it might be my fault but I don't know why. I have two Views linked together via a NavigationView. The child View has a ViewModel which creates a Thread when the initialization method is called and destroys the Thread object when the deinitialization method is called. The deinitialization method of the ViewModel is called when the child View is "closed". After navigating back to the parent View I can see in the Memory Profiler of Xcode that there is a new memory leak
I post a small example which reproduces this issue below
The HomeView: (main view)
import SwiftUI
struct HomeView: View {
#State private var showView = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: TestView(showView: self.$showView), isActive : self.$showView) {
Text("Open MainView")
}.isDetailLink(false)
.navigationBarTitle("Home")
}
}
}
}
The TestView: (child view)
import SwiftUI
struct TestView: View {
#Binding var showView : Bool
#StateObject var mainViewModel = TestViewViewModel()
var body: some View {
Button("Back") {
showView = false
}.onAppear(perform: {
mainViewModel.initViewModel()
})
.onDisappear(perform: {
mainViewModel.deinitViewModel()
})
}
}
The ViewModel:
import Foundation
class TestViewViewModel: ObservableObject {
private var testThread: TestThread?
public func initViewModel() {
testThread = TestThread.init()
}
public func deinitViewModel() {
testThread?.cancel()
testThread = nil
}
/*+++++++++++++++*/
/*+ Test thread +*/
/*+++++++++++++++*/
private class TestThread: Thread {
override init(){
print("Init RenderThread")
}
deinit {
print("deinit RenderThread")
}
override func main() {
}
}
}
That's it, no magic involved but when clicking the Back button in the child view it seems something will not be garbage collected in the ViewModel's Thread. Any hints what might be wrong? Is this a bug or is there something I have to release manually?
I am developing simple to-do app with SwiftUI. During my tests i noticed my view models never calls deinit and causing linear increase in memory usage.
I reproduced the same behavior with following code:
struct ContentView: View {
#State private var isPresented = false
var body: some View {
Button("open") {
self.isPresented = true
}
.sheet(isPresented: $isPresented) {
SheetView()
}
}
}
struct SheetView: View {
#ObservedObject var model: ViewModel
init() {
model = ViewModel()
}
var body: some View {
Form {
Toggle("Toggle Me", isOn: $model.isOn)
}
}
}
class ViewModel: ObservableObject {
#Published var isOn = false
deinit {
print("ViewModel deinit ")
}
}
When sheet is dismissed, model object never deinits. If i replace the form with VStack or ScrollView then model is deinited. Is there a solution to this?
It’s a bug. Only workaround that worked for me is using ScrollView. Then again, ScrollView comes with its own animation bugs.
EDIT
Issue seems to have been resolved in iOS 13.3.1
You're understanding deinit() wrong. When you're dismissing a View it doesn't necessarily mean it's going to call deinit() like you think. If your ViewModel was destroyed however it would call deinit() as you expect.
To demonstrate this, here’s a Person class with a name property, a simple initializer, and a printGreeting() method that prints a message:
class Person {
var name = "John Doe"
init() {
print("\(name) is alive!")
}
func printGreeting() {
print("Hello, I'm \(name)")
}
}
We’re going to create a few instances of the Person class inside a loop, because each time the loop goes around a new person will be created then destroyed:
for _ in 1...3 {
let person = Person()
person.printGreeting()
}
And now for the deinitializer. This will be called when the Person instance is being destroyed:
deinit {
print("\(name) is no more!")
}
Source: https://www.hackingwithswift.com/sixty/8/6/deinitializers
Consider we have a RootView and a DetailView. DetailView has it's own BindableObject, let's call it DetailViewModel and we have scenario:
RootView may be updated by some kind of global event e.g. missed
internet connection or by it's own data/view model
When RootView handling event it's
content is updated and this is causes new struct of DetailView to
be created
If DetailViewModel is created by DetailView on init,
there would be another reference of DetailViewModel and it's state (e.g. selected object) will be missed
How can we avoid this situation?
Store all ViewModels as EnvironmentObjects that is basically a singleton pool. This approach is causes to store unneeded objects in memory when they are not used
Pass throw all ViewModels from RootView to it's children and to children of child (has cons as above + painfull dependencies)
Store View independent DataObjects (aka workers) as EnvironmentObjects. In that case where do we store view dependent states that corresponds to Model? If we store it in View it will end up in situation where we cross-changing #States what is forbidden by SwiftUI
Better approach?
Sorry me for not providing any code. This question is on architecture concept of Swift UI where we trying to combine declarative structs and reference objects with data.
For now I don't see da way to keep references that corresponds to appropriate view only and don't keep them in memory/environment forever in their current states.
Update:
Lets add some code to see whats happening if VM is created by it's View
import SwiftUI
import Combine
let trigger = Timer.publish(every: 2.0, on: .main, in: .default)
struct ContentView: View {
#State var state: Date = Date()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: ContentDetailView(), label: {
Text("Navigation push")
.padding()
.background(Color.orange)
})
Text("\(state)")
.padding()
.background(Color.green)
ContentDetailView()
}
}
.onAppear {
_ = trigger.connect()
}
.onReceive(trigger) { (date) in
self.state = date
}
}
}
struct ContentDetailView: View {
#ObservedObject var viewModel = ContentDetailViewModel()
#State var once = false
var body: some View {
let vmdesc = "View model uuid:\n\(viewModel.uuid)"
print("State of once: \(once)")
print(vmdesc)
return Text(vmdesc)
.multilineTextAlignment(.center)
.padding()
.background(Color.blue)
.onAppear {
self.once = true
}
}
}
class ContentDetailViewModel: ObservableObject, Identifiable {
let uuid = UUID()
}
Update 2:
It seems that if we store ObservableObject as #State in view (not as ObservedObject) View keeps reference on VM
#State var viewModel = ContentDetailViewModel()
Any negative effects? Can we use it like this?
Update 3:
It seems that if ViewModel kept in View's #State:
and ViewModel is retained by closure with strong reference - deinit will never be executed -> memory leak
and ViewModel is retained by closure with weak reference - deinit invokes every time on view update, all subs will be reseted, but properties will be the same
Mehhh...
Update 4:
This approach also allows you to keep strong references in bindings closures
import Foundation
import Combine
import SwiftUI
/**
static func instanceInView() -> UIViewController {
let vm = ContentViewModel()
let vc = UIHostingController(rootView: ContentView(viewModel: vm))
vm.bind(uiViewController: vc)
return vc
}
*/
public protocol ViewModelProtocol: class {
static func instanceInView() -> UIViewController
var bindings: Set<AnyCancellable> { get set }
func onAppear()
func onDisappear()
}
extension ViewModelProtocol {
func bind(uiViewController: UIViewController) {
uiViewController.publisher(for: \.parent)
.sink(receiveValue: { [weak self] (parent) in
if parent == nil {
self?.bindings.cancel()
}
})
.store(in: &bindings)
}
}
struct ModelView<ViewModel: ViewModelProtocol>: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<ModelView>) -> UIViewController {
return ViewModel.instanceInView()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ModelView>) {
//
}
}
struct RootView: View {
var body: some View {
ModelView<ParkingViewModel>()
.edgesIgnoringSafeArea(.vertical)
}
}
Apple says that we should use ObservableObject for the data that lives outside of SwiftUI. It means you have to manage your data source yourself.
It looks like a single state container fits best for SwiftUI architecture.
typealias Reducer<State, Action> = (inout State, Action) -> Void
final class Store<State, Action>: ObservableObject {
#Published private(set) var state: State
private let reducer: Reducer<State, Action>
init(initialState: State, reducer: #escaping Reducer<State, Action>) {
self.state = initialState
self.reducer = reducer
}
func send(_ action: Action) {
reducer(&state, action)
}
}
You can pass the instance of the store into the environment of your SwiftUI app and it will be available in all views and will store your app state without data losses.
I wrote a blog post about this approach, take a look at it for more information
https://swiftwithmajid.com/2019/09/18/redux-like-state-container-in-swiftui/