In my Watch app, the WatchDelegate class is the WCSession delegate and handles transferring data with the iOS app.
When the delegate receives some data it stores it in the UserDefaults.
When the Watch app is launched it reads the existing data stored in the defaults and creates the view.
I have a WatchApp file which contains this:
@main
struct WatchApp: App {
var body: some Scene {
WindowGroup {
if(getItems().count == 0) {
NoEventsView()
} else {
ItemsListView(available: getItems())
}
}
}
}
As you can see, if there are no events in the defaults it shows the NoEventsView; and if there are some it shows the EventsListView.
When the Watch delegate receives a change in the events, I need to refresh this view. The delegate can receive zero or more events.
How on Earth do I do that?
In iOS I could call a method to reload a table of data, or post a notification to another view controller to do that, but in the Watch and with SwiftUI there doesn't seem to be any obvious way of refreshing a view.
Is there any way of telling the App struct to refresh, or a particular view? For example, if I extracted the if statement into its own "struct WhichView: View", could I tell that to refresh?
I've read a LOT on the net these past few days on @State vars, @ObservedObject, @Published etc, but nothing I've seen works, or is far too weird and complex for my situation.
I literally just want WatchApp or WhichView to redraw when I tell it to. How do I do that?
Thanks.
For anyone coming across this thread, here's how I got it to work. It might not be 100% correct, but it's definitely working here.
Background:
- iOS app in Objective-C with a SwiftUI Watch extension ("Watch App").
- Watch App has a delegate ("WatchAppDelegate") conforming to WKApplicationDelegate and WCSessionDelegate.
- WatchConnectivity session delegate methods are in the WatchAppDelegate.
- WatchAppDelegate also contains my ModelData class. This contains some
@Published
vars that other Views can see, and will refresh when those values change. The class is also an@ObservableObject
.
WatchAppDelegate.swift
// Instantiate the model data here, *once only*. This is your single source of truth.
let modelData: ModelData = ModelData()
class ModelData : ObservableObject {
@Published var availableItems: [ItemDetail] = defaultsGetAvailableItems() // Gets an array of ItemDetail from the defaults
@Published var mainItem: ItemDetail = defaultsGetMainItem() // Again, from the defaults
}
class WatchAppDelegate : NSObject, WKApplicationDelegate, WCSessionDelegate, ObservableObject {
var session: WCSession?
override init() {
super.init()
if(WCSession.isSupported()) {
if(session == nil) {
session = WCSession.default // Start the Watch Connectivity session
session!.delegate = self
session!.activate()
}
}
}
// Delegate methods go here, session didReceiveMessage etc.
...
}
When the WCSession delegate methods are called and triggered in WatchAppDelegate, the delegate deals with the new data that's come in. So, for example, the iOS app has just sent a message dictionary with, maybe: "newItems" : arrayOfNewItems
. The delegate takes those new items and stores them in the defaults.
At that point, you also update the modelData: modelData.availableItems = defaultsGetAvailableItems()
and modelData.mainItem = defaultsGetMainItem()
. Your one source of truth has been updated with the new data.
Okay, the WatchApp is the @main
entry point into the app, and you need to start the delegate in there:
WatchApp.swift
@main
struct WatchApp: App {
@WKApplicationDelegateAdaptor private var appDelegate: WatchAppDelegate // Launches the delegate, which creates the modelData instance
var body: some Scene {
WindowGroup {
AppContentView(modelData: modelData)
.environmentObject(modelData) // Give access to modelData to the receiving view (probably, I dunno, but it's necessary)
}
}
}
struct AppContentView : View {
@ObservedObject var modelData: ModelData
var body: some View {
VStack {
if(modelData.availableitems.count == 0) { // If there are no items, display an appropriate view and message
NoItemsView()
.environmentObject(modelData)
} else { // We have items
ItemsListView()
.environmentObject(modelData)
}
...
}
ItemsListView.swift
struct ItemsListView: View {
@EnvironmentObject var modelData: ModelData // To access the data
var body: some View {
VStack {
NavigationView {
List {
// List of items
ForEach(modelData.availableItems) { item in
NavigationLink {
ItemDetailView(item: item)
} label: {
TableRowView(item: item)
}
}
}
}
}
}
}
ItemDetailView.swift
struct ItemDetailView: View {
@EnvironmentObject var modelData: ModelData
var event: ItemDetail
// This lets us access the particular item in the array by its index
var itemIndex: Int {
modelData.availableItems.firstIndex(where: { $0.id == item.id })!
}
var body: some View {
...
Happy to be told what I've done is wrong or bad or bad practice or whatever, but it's working right now...!