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)
}
2
Answers
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 ANYclass
orstruct
Put at
class
orstruct
level the declaration for the managerThen call
When you want to log a message.
In the ViewModel you would
Like below
If your
View
won’t be at the highest level you need to call.In the
ContentView
orAppDelegate
so theViewModel
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 theViewModel
.Here is the rest of the code you need to get this sample working.
Only the one
View
that shows the messages needs access to theViewModel
and only oneViewModel
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
fromosLog
withlog
,info
,debug
anderror
methods.The specific methods call the
osLog
and3rd party Analytics
Services
corresponding methods.Also, the
error
method sends a notification that aViewModel
at the top level receives and shows anAlert
.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.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
2. Define where logs should be
Note: An
EnvironmentObject
is a good choice for such an object!3. Define how to show logs
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