Swift UI - Using UIVIewControllerRepresentable to update logs - ios

The logic I'm trying to create for my logging in the app is:
A ScrollView with a frame to control the height and allow the user to see logs from actions in the app, the logs should be scrollable to scroll up on previous appended logs.
I've created a log view model which allows the log to be set and then appends to a log array and then get.
The logs are set through actions in callbacks from various view controllers and actions from the user.
currently I have the logs being retrieved in the UIViewControllerRepresentable - updateUIViewController method.
The code works for each callback and for the user actions, the problems are: 5a. It's not scrollable to go to the top of the log messages, 5b. The log messages keep showing on the screen as updateUIViewController is continuously being called.
I was trying to think of a way to empty the array after each action, but not sure the best way to go about this.
Code:
LogViewModel:
import UIKit
import SwiftUI
class LogViewModel: ObservableObject{
#Published var mTime: String = ""
#Published var id: String = "#"
#Published var mMessage: String = ""
private var fullLogMessages: [String] = [""]
func setTimeFormatter() -> DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
return formatter
}
func setTheTime(date: Date){
self.mTime = setTimeFormatter().string(from: date)
}
func getTheTime() -> String{
return self.mTime
}
func setTheMessage(mMessage: String) {
ThreadUtil.runAsyncOnMainThread { [weak self] in
self?.mMessage = mMessage
}
}
func getTheMessage() -> String {
return self.mMessage
}
func getTheFullLogMessage() -> [String] {
let fullLog: String = getTheTime() + " - " + getTheGivenId() + " - " + getTheMessage()
self.fullLogMessages.append(fullLog)
return self.fullLogMessages
}
func setTheGivenId(id: String) {
ThreadUtil.runAsyncOnMainThread { [weak self] in
self?.id = id
}
}
func getTheGivenId() -> String {
return self.id
}
}
Controllers:
In each controller I've created a method like this to set the log messages:
func setTheLogMessages(message: String) {
self.logViewModel.setTheTime(date: date)
self.logViewModel.setTheMessage(mMessage: message)
}
In the view I have the UIViewControllerRepresentable:
struct MyScreenView_UI: UIViewControllerRepresentable{
#ObservedObject var viewModel: myScreenViewModel
#ObservedObject var logViewModel: LogViewModel
#Binding var fullLogMessage: [String]
func makeUIViewController(context: Context) -> some myViewController {
print(#function)
return myViewController(viewModel: viewModel, logViewModel: logViewModel)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
print(#function)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
fullLogMessage = logViewModel.getTheFullLogMessage()
}
}
}
and the ScrollView for the UI:
ScrollView{
VStack(alignment: .leading, content: {
Text("Logs")
.font(.footnote).fontWeight(.medium)
ForEach($fullLogMessage, id: \.self) { $logMessage in
Text(logMessage)
.font(.custom("FONT_NAME", size: 12))
.disabled(true)
}
})
.frame(width: 400, height: 50, alignment: .leading)
}

You haven't provided a Minimal Reproducible Example but here is a simplified version of what seems to be what you are trying to do.
First, add a LogManager that can be created by ANY class or struct
struct LogManager{
var name: String
///Simplified post that takes in the String and uses the name as the source
func postMessage(message: String){
postMessage(message: .init(timestamp: Date(), message: message, source: name))
}
//MARK: Notification
///Sends a Notification with the provided message
func postMessage(message: Message){
NotificationCenter.default.post(name: .logManagerMessage, object: message)
}
///Adds an observer to the manager's notification
func observeMessageNotification(observer: Any, selector: Selector){
NotificationCenter.default.addObserver(observer, selector: selector, name: .logManagerMessage, object: nil)
}
}
Put at class or struct level the declaration for the manager
private let log = LogManager(name: "YourClassStructName")
Then call
log.postMessage(message: "your message here")
When you want to log a message.
In the ViewModel you would
subscribe to the notifications
maintain the array
Like below
class AppLogSampleViewModel: ObservableObject{
static let shared: AppLogSampleViewModel = .init()
private let manager = LogManager(name: "AppLogSampleViewModel")
#Published var messages: [Message] = []
private init(){
//Observe the manager's notification
manager.observeMessageNotification(observer: self, selector: #selector(postMessage(notification:)))
}
///Puts the messages received into the array
#objc
func postMessage(notification: Notification){
if notification.object is Message{
messages.append(notification.object as! Message)
}else{
messages.append(.init(timestamp: Date(), message: "Notification received did not have message", source: "AppLogSampleViewModel :: \(#function)"))
}
}
}
If your View won't be at the highest level you need to call.
let startListening: AppLogSampleViewModel = .shared
In the ContentView or AppDelegate so the ViewModel starts listening as quickly as possible. You won't necessarily use it for anything but it needs to be called as soon as you want it to start logging messages.
Only the View that shows the messages uses the instance of the ViewModel.
struct AppLogSampleView: View {
#StateObject var vm: AppLogSampleViewModel = .shared
//Create this variable anywhere in your app
private let log = LogManager(name: "AppLogSampleView")
var body: some View {
List{
Button("post", action: {
//Post like this anywhere in your app
log.postMessage(message: "random from button")
})
DisclosureGroup("log messages"){
ForEach(vm.messages, content: { message in
VStack{
Text(message.timestamp.description)
Text(message.message)
Text(message.source)
}
})
}
}
}
}
Here is the rest of the code you need to get this sample working.
struct AppLogSampleView_Previews: PreviewProvider {
static var previews: some View {
AppLogSampleView()
}
}
extension Notification.Name {
static let logManagerMessage = Notification.Name("logManagerMessage")
}
struct Message: Identifiable{
let id: UUID = .init()
var timestamp: Date
var message: String
var source: String
}
Only the one View that shows the messages needs access to the ViewModel and only one ViewModel subscribes to the notifications.
All the other classes and structs will just create an instance of the manager and call the method to post the messages.
There is no sharing/passing between the classes and structs everybody gets their own instance.
Your manager can have as many or as little methods as you want, mine usually mimics the Logger from osLog with log, info, debug and error methods.
The specific methods call the osLog and 3rd party Analytics Services corresponding methods.
Also, the error method sends a notification that a ViewModel at the top level receives and shows an Alert.
To get all this detail working it takes a lot more code but you have the pattern with the code above.
In your code, in the updateUIViewController you break the single source if truth rule by copying the messages and putting them in another source of truth, right here.
fullLogMessage = logViewModel.getTheFullLogMessage()
This is also done without a check to make sure that you don't go into an infinite loop. Anytime there is code in an update method you should check that the work actually needs to be done. Such as comparing that the new location doesn't already match the old location.

It seems like you made it very complicated. Let's do this with a simple approach.
1. Define what is a Log
Each log should be identifiable
Each log should represent it's creation date
Each log should have a message
struct Log: Equatable, Hashable {
let id = UUID()
let date = Date()
let message: String
}
2. Define where logs should be
Changes in the logs should be observable
Logs should be accessible in any view
import Combine
class LogManager: ObservableObject {
#Published var logs = [Log]()
}
Note: An EnvironmentObject is a good choice for such an object!
3. Define how to show logs
import SwiftUI
extension Log: Identifiable { }
struct ContentView: View {
#EnvironmentObject private var logManager: LogManager
var body: some View {
List(logManager.logs) { log in
HStack {
Text(log.message)
Text(log.date.ISO8601Format()) // Or any format you like
}
}
}
}
This UI is just a simple sample and could be replaced by any other UI
Note: ScrollToTop is automatically enabled in the List
Note: You may want to use a singleton or injected logger because of the nature of the logger
Note: Don't forget to create and pass in the environment object

Related

SwiftUI - Create popup that displays an updating progress message when executing long code

I am creating an app that has some code that takes a bit to execute. I want it to hang the app as I don't want the user to make any changes when it's executing. It's a Multiplatform app so when executing in macOS it automatically changes the mouse to a rolling ball image so that works-ish. In iOS there's no feedback at all. I want to create a popup (thinking alert but not too fussy) that displays an updating message showing the user what's happening and making it obvious they have to wait.
Right now my View calls a class that executes the code so I wanted to somehow pass a variable that gets updated in the class but is visible in the View in real-time. Ideally I would want to be able to use this and call different methods each time from other Views but still use a popup with messages updating the user while the code executes.
To simplify this I created a mini project but I can't get it to work on either the macOS OR iOS as the View (app) isn't updated until after the code finishes executing (also have print statements to know what's happening). I've been trying #StateObject, #Published, ObservableObject, etc to no avail.
Code: ContentView.swift
import SwiftUI
import CoreData
struct ContentView: View {
#StateObject var progress = myProgress()
#State var showProgress:Bool = false
var body: some View {
NavigationView {
VStack {
Button(action: {
print("Pressed Button 1")
showProgress = true
}, label: {
Text("Execute Option")
})
.sheet(isPresented: $showProgress, content: {
Text(progress.message)
.onAppear() {
Task {
let longMethod = longMethodsCalled(currProgress: progress)
print("About to call method - \(progress.message)")
let error = longMethod.exampleOne(title: "My Passed In Title")
// Here I can use the returned value if it's an object or now if it passed if it's String?
print("Error: \(error ?? "No error")")
print("after printing error - \(progress.message)")
}
// If this is commented out it just shows the last message after code finishes
showProgress = false
}
})
}
}
}
}
Other file: longMethodsCalled.swift
import Foundation
import SwiftUI
class myProgress: ObservableObject {
#Published var message = "progressing..."
func changeMessage(newMessage:String) {
message = newMessage
print("Message changing. Now: \(message)")
self.objectWillChange.send()
}
}
class longMethodsCalled {
#State var progress: myProgress
init(currProgress:myProgress) {
progress = currProgress
}
public func exampleOne(title:String) -> String? {
print("in example one - \(title)")
progress.changeMessage(newMessage: "Starting example one")
sleep(1)
print("after first sleep")
progress.changeMessage(newMessage: "Middle of example one")
sleep(1)
progress.changeMessage(newMessage: "About to return - example one")
return "result of example one"
}
}
I guess I'm wondering if this is even possible? And if so how can I go about it. I can't tell if I'm close or completely out to lunch. I would REALLY love a way to update my users when my code executes.
Thanks for any and all help.
Here is an example using binding to do all view update in another struct. It is using async and await. For the sleep(), it use Task.sleep which does not lock queues.
struct LongMethodCallMessage: View {
#State var showProgress:Bool = false
#State var progressViewMessage: String = "will do something"
var body: some View {
NavigationView {
VStack {
Button(action: {
print("Pressed Button 1")
progressViewMessage = "Pressed Button 1"
showProgress = true
}, label: {
// text will be return value
// so one can see that it ran
Text(progressViewMessage)
})
.sheet(isPresented: $showProgress, content: {
// create the vue that will display the progress
TheLongTaskView(progressViewMessage: $progressViewMessage, showProgress: $showProgress)
})
}
}
}
}
struct TheLongTaskView: View, LongMethodsCalledMessage {
#Binding var progressViewMessage: String
#Binding var showProgress: Bool
var body: some View {
Text(progressViewMessage)
.onAppear() {
// create the task setting this as delegate
// to receive message update
Task {
let longMethod = LongMethodsCalled(delegate: self)
print("About to call method - \(progressViewMessage)")
let error = await longMethod.exampleOne(title: "My Passed In Title")
// Here I can use the returned value if it's an object or now if it passed if it's String?
print("Error: \(error ?? "No error")")
print("after printing error - \(progressViewMessage)")
// save the error message and close view
progressViewMessage = error!
showProgress = false
}
}
}
// updating the text
func changeMessage(newMessage:String) {
print("changeMessage: \(newMessage)")
progressViewMessage = newMessage
}
}
// a protocol to update the text in the long running task
protocol LongMethodsCalledMessage {
func changeMessage(newMessage:String)
}
class LongMethodsCalled {
var delegate: LongMethodsCalledMessage
init(delegate: LongMethodsCalledMessage) {
self.delegate = delegate
}
// make the method async
public func exampleOne(title:String) async -> String? {
print("in example one - \(title)")
self.delegate.changeMessage(newMessage: "Starting example one")
// this wait enable the text to update
// the sleep() may lock and prevent main queue to run
try! await Task.sleep(nanoseconds: 2_000_000_000)
print("after first sleep")
self.delegate.changeMessage(newMessage: "Middle of example one")
try! await Task.sleep(nanoseconds: 2_000_000_000)
print("after second sleep")
self.delegate.changeMessage(newMessage: "About to return - example one")
return "result of example one"
}
}

How do you edit an ObservableObject’s properties in SwiftUI from another class?

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

swiftUI modify state var from remote closure

I've got an application written in swift/swiftUI.
the logic part composed of events generator that enable the client to register a callback for each event before the UI part starts.
init.swift
-----------
func applicationDidFinishLaunching(_ aNotification: Notification) {
let event1Handler : eventHandler = {(data) in
ContentView().alert_handler.showAlert = true
}
let event2Handler : eventHandler = {(data) in
...
}
eventGenerator.sharedInstance().registerUIEvent1(event1Handler, event2: event2Handler)
window = NSWindow(...)
window.contentView = NSHostingView(rootView: ContentView())
...
}
in the UI part, there's an optional alert that is presented depent on the showAlert that can be set from the closure from the previous file...
class alertHandler: ObservableObject {
#Published var showAlert = false
}
struct ContentView: View {
#ObservedObject var alert_handler = alertHandler()
var body: some View {
GeometryReader { metrics in
.....
}.alert(isPresented: $alert_handler.showAlert, content: {
Alert(title: Text("Alert:"),
message: Text("press OK to execute default action..."),
dismissButton: Alert.Button.default(
Text("Press ok here"), action: { print("Hello world!") }
)
)
})
Unfortunately I couldn't see the alert appears when the first event was triggered. Perhaps anyone can tell me if I'd doing anything wrong ? or suggest an alternative approach to modify swiftUI struct variable (event_handler) from remote closure ?
I believe that my the problem may derived from ContentView module which is not a singleton, so when I set showAlert I'm doing so for another instance of ContentView. How can I fix my code to access showAlert that belongs to the currently running instance of ContentView ?
Your suspicion about ContentView not being a singleton instance is correct. You can solve this by owning your alertHandler in the parent (in this case, the app delegate) and passing that down to ContentView.
var handler = alertHandler()
func applicationDidFinishLaunching(_ aNotification: Notification) {
let event1Handler : eventHandler = { (data) in
self.handler.showAlert = true //<-- Here
}
let event2Handler : eventHandler = {(data) in
...
}
eventGenerator.sharedInstance().registerUIEvent1(event1Handler, event2: event2Handler)
window = NSWindow(...)
window.contentView = NSHostingView(rootView: ContentView(alert_handler: handler))
...
}
struct ContentView: View {
#ObservedObject var alert_handler : alertHandler //<-- Here
That way, when you modify the showAlert property, the ContentView gets updated because it's the same instance of alertHandler.
Note: I'd consider adopting the Swift conventions of capitalizing type names and using camel case rather than snake case -- it'll make it easier for others to read your code.

How to reload UITableView from different controller

I am attempting to make a to-do app with a controller for both "this week" and "next week." (Objects with a date in the current week will be shown in the thisWeek view controller, while objects with a date next week will be shown in the nextWeek view controller. However, sometimes objects will not appear in the table until the app restarts, and I'm not sure why this is happening or how to fix it.
My theory is that each table in the different controllers needs to be reloaded each time data is added/changed. I just don't know how to access the table from a different controller than the one I am in.
Attempt 1 I tried to use a singleton that looked like this:
class ToDoTables {
private init() {}
static let shared = ToDoTables()
lazy var thisTable: UITableView = {
let thisWeek = UITableView()
return thisWeek
}()
lazy var nextTable: UITableView = {
let nextWeek = UITableView()
return nextWeek
}()
}
then to be called like:
let thisTable = ToDoTables.shared.thisTable
However this did not work because I initially created the table using storyboard and just used an IBOutlet in the controller, and couldn't find way to keep the outlet and also use singleton. Next, I tried to remove the outlet and just use the code above. When I simulated the app, it crashed because the outlet couldn't be found or something like that (which was expected).
Attempt 2
I tried to access the other controller's table by creating an instance of the vc and accessing the table that way:
NextWeekController().nextTable.reloadData()
but got an error that said "Unexpectedly found nil while implicitly unwrapping an Optional value"
So I'm not sure if that is my tables need to be reloaded each time, or if it is something else.
Overview of non-functioning operations (an app restart is needed, or the controller/table needs loaded again):
something created in the nextWeek controller with a date in the current week will not appear
when an object in created in nextWeek controller, then date is changed from next week to this week
something created in the thisWeek controller with a date in the next week will not appear
when an object in created in thisWeek controller, then date is changed from this week to next week
My theory is that each table in the different controllers needs to be reloaded each time data is added/changed.
You are correct. For UITableView.reloadData(), the Apple Docs state to:
Call this method to reload all the data that is used to construct the
table
In other words, it is necessary to reload the table view's data with this method if the data is changed.
I just don't know how to access the table from a different controller
than the one I am in.
One solution is to create global references to all your view controllers through one shared instance.
// GlobalVCs.swift
class GlobalVCs {
static var shared = GlobalVCs()
var nameOfVC1!
var nameOfVC2!
var nameOfVC3!
// and so on
private init() {}
}
Then set the global vc references from each view controller's viewDidLoad() method.
Example:
// VC1.swift
override func viewDidLoad() {
GlobalVCs.shared.vc1 = self
}
Then you can access a view controller anywhere with GlobalVCs.shared.vc1, GlobalVCs.shared.vc2, etc.
Sorry, but need to say that this is wrong approach. We need to follow to MCV model where we have data model, view and controller. If you example for to-do model you could have:
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
struct ToDoItem: Codable, Identifiable {
static let collection = "todos"
#DocumentID var id: String?
var uid: String;
var done: Bool;
var timestamp: Timestamp;
var description: String
}
Then you need to have repository for this type of data, for example if we are using Firebase we can have
import Foundation
import Combine
import FirebaseFirestore
import FirebaseFirestoreSwift
class BaseRepository<T> {
#Published var items = Array<T>()
func add(_ item: T) { fatalError("This method must be overridden") }
func remove(_ item: T) { fatalError("This method must be overridden") }
func update(_ item: T) { fatalError("This method must be overridden") }
}
and then we have ToDoRepository (singleton object)
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
import Resolver
import CoreLocation
import Combine
class ToDoRepository: BaseRepository<ToDoItem>, ObservableObject {
var db = Firestore.firestore()
uid: String? {
didSet {
load()
}
}
private func load() {
db.collection(ToDoItem.collection).whereField("uid", isEqualTo: uid).order(by: "timestamp").addSnapshotListener {
( querySnapshot, error ) in
if let querySnapshot = querySnapshot {
self.items = querySnapshot.documents.compactMap { document -> ToDoItem? in
try? document.data(as: ToDoItem.self)
}
}
}
}
}
Now we need to register repository in AppDelegate+Injection.swift:
import Foundation
import Resolver
extension Resolver: ResolverRegistering {
public static func registerAllServices() {
// Services
register { ToDoRepository() }.scope(application)
}
}
After that we can use ToDoRepository in any controller :
import UIKit
import Combine
import Resolver
class MyController: UIViewController {
private var cancellables = Set<AnyCancellable>()
#LazyInjected var toDoRepository: ToDoRepository
// and listen and update any table view like this
override func viewDidLoad() {
super.viewDidLoad()
toDoRepository.$items
.receive(on: DispatchQueue.main)
.sink { [weak self] todos
self?.todos = todos
tableView.reloadData()
}
.store(in: &cancellables)
}
}

Keep reference on view/data model after View update

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/

Resources