When my watchOS app starts, AppDelegate
's applicationDidFinishLaunching kicks off a background thread which does some things and updates the View
.
In SwiftUI, the View
cannot be updated just like that. All View
s are functions of state. Altering the state would 'refresh' the View
.
With the above concept in mind, there's the following MessageView
:
public struct MessageView: View {
init(_ message: String) {
self.message = message
}
@State
public var message: String
public var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text(message)
}
.padding()
}
}
This MessageView
is initialised with sample data in a StaticContext
... even before the AppDelegate
is invoked.
public struct StaticContext {
public static var messageView: MessageView = MessageView("[Lifecycle]: Hellow World!")
}
// App Struct
@main
struct WatchOSStartup_Watch_AppApp: App {
@WKApplicationDelegateAdaptor
var appDelegate: AppDelegate
var body: some Scene {
WindowGroup {
StaticContext.messageView
}
}
}
With the above style, I'm able to store a reference to the MessageView
before giving it to SwiftUI. When my background thread is done with its work (most of it is in C++), it can use this reference to alter the state of MessageView
and therefore update MessageView
.
@objc
static func updateUIMessage() {
StaticContext.messageView.message = "[Presentation]: Hello World!"
}
When state is altered, the View
must refresh and the new data is supposed to get displayed? But I only see the previous message. Not sure why... Is this approach even okay? If not, how to update a SwiftUI View
from background thread?
PS:
This is only a basic model. Eventually, I'm expecting to have bunch of properties which can affect a View
. So, I should be using @StateObject
, @ObservedObject
and have a custom type (conforming to the ObservableObject
protocol) as the state of the View
. I wanted to test a very basic model first.
After some reading and this helpful WWDC video about Data Essentials in SwiftUI, I'm able to update a View
from a background thread.
This is my sample data model:
public class MessageViewData: ObservableObject {
init(_ message: String) {
self.message = message
}
@Published
public var message: String
}
This is the MessageView
:
public struct MessageView: View {
public init(messageData: MessageViewData) {
self.messageData = messageData
}
// Not a Source of Truth.
@ObservedObject
var messageData: MessageViewData
public var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text(messageData.message)
}
.padding()
}
}
As shown above, the MessageView
doesn't hold on to the data (aka the 'Source of Truth'), but only a reference to it. The lifecycle of the data is managed externally. In my case, I've created a static object of type MessageViewData
public class AppDelegate: NSObject, WKApplicationDelegate {
public func applicationDidFinishLaunching() {
// UI related initialization
// Note: SwiftUI view is not initialzed yet.
// Run a background thread for 10 sec to mock the application logic of
// 1) Initialize other app components
// 2) Read from disk
// And finally, update the UI by updating 'Source of Truth'
let queue: DispatchQueue = DispatchQueue.global(qos: .background)
queue.async {
var presentDate: Date = Date.now
Log("Start time = " + String(describing: presentDate))
for i in 1...2 {
// Sleep for 5 sec
Thread.sleep(forTimeInterval: 5)
// Then count
presentDate = Date.now
Log(String(format: "%d) %@", i, String(describing: presentDate)))
}
Log("End time = " + String(describing: presentDate))
DispatchQueue.main.async {
Log("Updating Source of Truth...")
AppDelegate.messageData.message = "Update"
}
}
}
// Source of Truth of the data passed to SwiftUI. The lifecycle of this data
// is managed by the AppDelegate and not any SwiftUI View.
public static var messageData: MessageViewData = MessageViewData("Default")
}
The data initialised in AppDelegate is injected into MessageView as shown below:
@main
struct WatchOSStartup_Watch_AppApp: App {
@WKApplicationDelegateAdaptor
var appDelegate: AppDelegate
var body: some Scene {
WindowGroup {
// Inject data to SwiftUI view.
// The Source of Truth is defined as a static member in
// the AppDelegate.
MessageView(messageData: (AppDelegate.messageData))
}
}
}