How to refresh a View?

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.

Answered by darkpaw in 725154022

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...!

Have a look here: https://developer.apple.com/tutorials/swiftui/creating-a-watchos-app

At step 4 they show how to create a state var to do this.

@main
struct LandmarksApp: App {
  @StateObject private var modelData = ModelData()

   var body: some Scene
      WindowGroup {
        ContentView()
       •environmentObject(modelData)
     }
  }
}

Thanks, but in their Landmarks example the modelData is only ever set once: ModelData.swift:12 = @Published var landmarks: [Landmark] = load("landmarkData.json"). I need to update the data in that model so that it can be used in the content views.

Let's say I have a singleton, and the delegate calls the singleton's methods when it receives data. So:

  • The iOS app sends new data to the Watch.
  • The Watch delegate receives the new data.
  • The delegate calls a method in the singleton to store the information in the group defaults.
  • That data is not yet available to the modelData.

How do I get that new data into the modelData?

final class ModelData : ObservableObject {
	@Published var items: [ItemDetail] = getItems()
}

class WatchAppSingleton {
	@EnvironmentObject var modelData: ModelData
...

At this point I can access what's in the modelData, but not write to it.

If I try something like modelData.items = <some new items> Xcode tells me: No ObservableObject of type ModelData found. A View.environmentObject(_:) for ModelData may be missing as an ancestor of this.

Does the line @Published var items: [ItemDetail] = getItems() execute once or multiple times? How do I get it to grab the data again?

I can't be the only person who needs to update the data in their model? Am I just doing it wrong?!

Ah, right, got it...

This:

class WatchAppSingleton {
	@EnvironmentObject var modelData: ModelData

should be this:

class WatchAppSingleton {
    @StateObject var modelData: ModelData = ModelData()

An error message more like this would be great; modelData is marked as an @EnvironmentObject. Did you mean @StateObject?

Accepted Answer

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...!

How to refresh a View?
 
 
Q