Multiple windows of the same SwiftUI (mac) app share the same state - swift

so this is basically a Hail Mary, but I'm really out of ideas as to what could be causing this:
I have a small mac-app that uses the default WindowGroup, which according to the documentation ensures that.
"Each window created by the group maintains an independent state. For example, for each new window created from the group, new memory is allocated for any State or StateObject variables instantiated by the scene's view hierarchy."
Nevertheless, the NavigationView shows the same selected list across all windows. Put differently, selectedLabel shares and updates across multiple windows, even tho in my humble understanding this is not supposed to happen.
Another problem, which I don't know if it's related, is that both windowStyle and windowToolbarStyle set on this WindowGroup are ignored.
It may be a minor issue, but I'm really stuck here, so any help would be appreciated!
My MainApp (simplified):
import SwiftUI
#main
struct MainApp: App {
#State private var selectedLabel: ViewModel? = .init()
var body: some Scene {
WindowGroup {
SidebarView(selectedLabel: $selectedLabel)
}
.windowStyle(HiddenTitleBarWindowStyle())
.windowToolbarStyle(UnifiedCompactWindowToolbarStyle())
}
}
My Sidebar (also simplified):
import SFSafeSymbols
import SwiftUI
struct SidebarView: View {
#ObservedObject var viewModel = SidebarViewModel()
#Binding var selectedLabel: ViewModel?
var body: some View {
NavigationView {
VStack {
Button(action: {
viewModel.createStockList()
}, label: {
Image(systemSymbol: .plus)
})
List(viewModel.stockLists, id: \.id) { stockList in
NavigationLink(destination: StockListView(viewModel: stockList),
tag: stockList,
selection: $selectedLabel) {
Text(stockList.name)
}
}
}
}
}
}

You're storing your selectedLabel at the WindowGroup level and passing it to each sidebar. You should store that state in the SidebarView if you want it to be different.

Related

SwiftUI: Set a Published value in an ObservableObject from the UI (Picker, etc.)

Update:
This question is already solved (see responses below). The correct way to do this is to get your Binding by projecting the
ObservableObject For example, $options.refreshRate.
TLDR version:
How do I get a SwiftUI Picker (or other API that relies on a local Binding) to immediately update my ObservedObject/EnvironmentObject. Here is more context...
The scenario:
Here is something I consistently need to do in every SwiftUI app I create...
I always make some class that stores any user preference (let's call this class Options and I make it an ObservableObject.
Any setting that needs to be consumed is marked with #Published
Any view that consumes this brings it in as a #ObservedObject or #EnvironmentObject and subscribes to changes.
This all works quite nicely. The trouble I always face is how to set this from the UI. From the UI, here is usually what I'm doing (and this should all sound quite normal):
I have some SwiftUI view like OptionsPanel that drives the Options class above and allows the user to choose their options.
Let's say we have some option defined by an enum:
enum RefreshRate {
case low, medium, high
}
Naturally, I'd choose a Picker in SwiftUI to set this... and the Picker API requires that my selection param be a Binding. This is where I find the issue...
The issue:
To make the Picker work, I usually have some local Binding that is used for this purpose. But, ultimately, I don't care about that local value. What I care about is immediately and instantaneously broadcasting that new value to the rest of the app. The moment I select a new refresh rate, I'd like immediately know that instant about the change. The ObservableObject (the Options class) object does this quite nicely. But, I'm just updating a local Binding. What I need to figure out is how to immediately translate the Picker's state to the ObservableObject every time it's changed.
I have a solution that works... but I don't like it. Here is my non-ideal solution:
The non-ideal solution:
The first part of the solution is quite actually fine, but runs into a snag...
Within my SwiftUI view, rather than do the simplest way to set a Binding with #State I can use an alternate initializer...
// Rather than this...
#ObservedObject var options: Options
#State var refreshRate: RefreshRate = .medium
// Do this...
#ObservedObject var options: Options
var refreshRate: Binding<RefreshRate>(
get: { self.options.refreshRate },
set: { self.options.refreshRate = $0 }
)
So far, this is great (in theory)! Now, my local Binding is directly linked to the ObservableObject. All changes to the Picker are immediately broadcast to the entire app.
But this doesn't actually work. And this is where I have to do something very messy and non-ideal to get it to work.
The code above produces the following error:
Cannot use instance member 'options' within property initializer; property initializers run before 'self' is available
Here my my (bad) workaround. It works, but it's awful...
The Options class provides a shared instance as a static property. So, in my options panel view, I do this:
#ObservedObject var options: Options = .shared // <-- This is still needed to tell SwiftUI to listen for updates
var refreshRate: Binding<RefreshRate>(
get: { Options.shared.refreshRate },
set: { Options.shared.refreshRate = $0 }
)
In practice, this actually kinda works in this case. I don't really need to have multiple instances... just that one. So, as long as I always reference that shared instance, everything works. But it doesn't feel well architected.
So... does anyone have a better solution? This seems like a scenario EVERY app on the face of the planet has to tackle, so it seems like someone must have a better way.
(I am aware some use an .onDisapear to sync local state to the ObservedObject but this isn't ideal either. This is non-ideal because I value having immediate updates for the rest of the app.)
The good news is you're trying way, way, way too hard.
The ObservedObject property wrapper can create this Binding for you. All you need to say is $options.refreshRate.
Here's a test playground for you to try out:
import SwiftUI
enum RefreshRate {
case low, medium, high
}
class Options: ObservableObject {
#Published var refreshRate = RefreshRate.medium
}
struct RefreshRateEditor: View {
#ObservedObject var options: Options
var body: some View {
// vvvvvvvvvvvvvvvvvvvv
Picker("Refresh Rate", selection: $options.refreshRate) {
// ^^^^^^^^^^^^^^^^^^^^
Text("Low").tag(RefreshRate.low)
Text("Medium").tag(RefreshRate.medium)
Text("High").tag(RefreshRate.high)
}
.pickerStyle(.segmented)
}
}
struct ContentView: View {
#StateObject var options = Options()
var body: some View {
VStack {
RefreshRateEditor(options: options)
Text("Refresh rate: \(options.refreshRate)" as String)
}
.padding()
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(ContentView())
It's also worth noting that if you want to create a custom Binding, the code you wrote almost works. Just change it to be a computed property instead of a stored property:
var refreshRate: Binding<RefreshRate> {
.init(
get: { self.options.refreshRate },
set: { self.options.refreshRate = $0 }
)
}
If I understand your question correctly, you want
to Set a Published value in an ObservableObject from the UI (Picker, etc.) in SwiftUI.
There are many ways to do that, I suggest you use a ObservableObject class, and use it directly wherever you need a binding in a view, such as in a Picker.
The following example code shows one way of setting up your code to do that:
import Foundation
import SwiftUI
// declare your ObservableObject class
class Options: ObservableObject {
#Published var name = "Mickey"
}
struct ContentView: View {
#StateObject var optionModel = Options() // <-- initialise the model
let selectionSet = ["Mickey", "Mouse", "Goofy", "Donald"]
#State var showSheet = false
var body: some View {
VStack {
Text(optionModel.name).foregroundColor(.red)
Picker("names", selection: $optionModel.name) { // <-- use the model directly as a $binding
ForEach (selectionSet, id: \.self) { value in
Text(value).tag(value)
}
}
Button("Show other view") { showSheet = true }
}
.sheet(isPresented: $showSheet) {
SheetView(optionModel: optionModel) // <-- pass the model to other view, see also #EnvironmentObject
}
}
}
struct SheetView: View {
#ObservedObject var optionModel: Options // <-- receive the model
var body: some View {
VStack {
Text(optionModel.name).foregroundColor(.green) // <-- show updated value
}
}
}
If you really want to have a "useless" intermediate local variable, then use this approach:
struct ContentView: View {
#StateObject var optionModel = Options() // <-- initialise the model
let selectionSet = ["Mickey", "Mouse", "Goofy", "Donald"]
#State var showSheet = false
#State var localVar = "" // <-- the local var
var body: some View {
VStack {
Text(optionModel.name).foregroundColor(.red)
Picker("names", selection: $localVar) { // <-- using the localVar
ForEach (selectionSet, id: \.self) { value in
Text(value).tag(value)
}
}
.onChange(of: localVar) { newValue in
optionModel.name = newValue // <-- update the model
}
Button("Show other view") { showSheet = true }
}
.sheet(isPresented: $showSheet) {
SheetView(optionModel: optionModel) // <-- pass the model to other view, see also #EnvironmentObject
}
}
}

Better way use #State or #ObservableObject

Hi everyone I have a question about #State vs #ObservableObject with SwiftUI
I have a view that contains a LazyHGrid
To have a custom cell of the LazyHGrid I preferred to create a new struct with the custom cell.
The view hierarchy is composed as follows:
struct View1 -> struct LazyHGrid -> struct LazyHGridCustomCell
In View1 I have a text that must be replaced with content of the LazyHGridCustomCell every time it is selected.
At this point in view of my hierarchy should I use #State & #Binding to update the text or would it be better #ObservableObject?
In case I wanted to use the #State wrapper I would find myself like this:
struct View1 (#State)
struct LazyHGrid (#Binding)
struct LazyHGridCustomCell (#Binding)
I was wondering if this is the right way or consider #ObservableObject
I created a code example based on my question .. It was created just to let you understand what I mean to avoid being misunderstood
I was wondering if it is right to create such a situation or use an #ObservableObject
In case this path is wrong can you show me an example of the right way to go to get the correct result?
Thanks for suggestion
struct View1: View {
#State private var name: String
var body: some View {
Text(name)
LazyHGridView(name: $name)
}
}
struct LazyHGridView: View {
#Binding var name: String
var body: some View {
LazyHGrid(rows: Array(repeating: GridItem(), count: 2)) {
ForEach(reservationTimeItems) { item in
LazyHGridCustomCell(name: $name)
}
}
}
}
struct LazyHGridCustomCell: View {
#Binding var name: String
var body: some View {
Text(name)
.foregroundColor(.white)
}
}
According to Data Essentials in SwiftUI (WWDC 2020) at 9:46, you should be using State because ObservableObject is for model data.
State is designed for transient UI state that is local to a view. In
this section, I want to move your attention to designing your model
and explain all the tools that SwiftUI provides to you. Typically, in
your app, you store and process data by using a data model that is
separate from its UI. This is when you reach a critical point where
you need to manage the life cycle of your data, including persisting
and syncing it, handle side-effects, and, more generally, integrate it
with existing components. This is when you should use
ObservableObject. First, let's take a look at how ObservableObject is
defined.

How can I make my View stop unnecessary rendering with using CustomType for Binding in SwiftUI?

I have a CustomType called AppData, and it look like this:
struct AppData {
var stringOfText: String
var colorOfText: Color
}
I am using this AppData in my Views as State or Binding, I have 2 Views in my project called: ContentView and another one called BindingView. In my BindingView I am just using Color information of AppData. And I am expecting that my BindingView render or response for Color information changes! How ever in the fact BindingView render itself even for stringOfText Which is totally unnecessary, because that data is not used in View. I thought that maybe BindingView not just considering for colorOfText but also for all package that cary this data and that is appData So I decided help BindingView to understand when it should render itself, and I made that View Equatable, But that does not helped even. Still BindingView refresh and render itself on changes of stringOfText which it is wasting of rendering. How can I solve this issue of unnecessary rendering while using CustomType as type for my State or Binding.
struct ContentView: View {
#State private var appData: AppData = AppData(stringOfText: "Hello, world!", colorOfText: Color.purple)
var body: some View {
print("rendering ContentView")
return VStack(spacing: 20) {
Spacer()
EquatableView(content: BindingView(appData: $appData))
//BindingView(appData: $appData).equatable()
Spacer()
Button("update stringOfText from ContentView") { appData.stringOfText += " updated"}
Button("update colorOfText from ContentView") { appData.colorOfText = Color.red }
Spacer()
}
}
}
struct BindingView: View, Equatable {
#Binding var appData: AppData
var body: some View {
print("rendering BindingView")
return Text("123")
.bold()
.foregroundColor(appData.colorOfText)
}
static func == (lhs: BindingView, rhs: BindingView) -> Bool {
print("Equatable function used!")
return lhs.appData.colorOfText == rhs.appData.colorOfText
}
}
When using Equatable (and .equatable(), EquatableView()) on Views, SwiftUI makes some decisions about when to apply our own == functions and when it is going to compare the parameters on its own. See another one of my answers with more details about this here: https://stackoverflow.com/a/66617961/560942
In this case, it appears that even if Equatable is declared, SwiftUI skips it because it must be deciding that the POD (plain old data) in the Binding is determined to be non-equal and therefore it's going to refresh the view (again, even though one would think that the == would be enough to force it not to).
In the example you gave, obviously it's trivial for the system to re-render the Text element, so it doesn't really matter if this re-render happened. But, in the even that there actually are consequences to re-rendering, you could encapsulate the non-changing parts into a separate child view:
struct BindingView: View {
#Binding var appData: AppData
var body: some View {
print("rendering BindingView")
return BindingChildView(color: appData.colorOfText)
}
//no point in declaring == since it won't get called (at least with the current parameters
}
struct BindingChildView : View {
var color: Color
var body: some View {
print("rendering BindingChildView")
return Text("123")
.bold()
.foregroundColor(color)
}
}
In the above code, although the BindingView is re-rendered each time (although at basically zero cost, because nothing will change), the new child view is skipped because its parameters are equatable (even without declaring Equatable). So, in a non-contrived example, if the child view were expensive to render, this would solve the issue.

SwiftUI ObservedObject not updating in View

class Room: ObservableObject { ... }
Contact: ObservableObject {
var chatRoom: Room
}
class Account: ObservableObject {
var rooms: Room { … }
var contacts: [Contact] {
return rooms.map {
Contact(chatRoom: $0)
}
}
func listenForRoomEvents() {
// Called on instantiation of a Room, this fires self.objectWillChange on room updates and is working properly
}
}
struct RoomView: View {
#ObservedObject var room: Room
}
/
THIS IS WORKING
/
struct ParentView: View {
#EnvironmentObject account: Account
var body: some View {
RoomsView(account.rooms)
.onAppear {
self.account.listenForRoomEvents()
}
}
}
struct RoomsView: View {
var rooms: [Room]
var body: some View {
ForEach(rooms) { room in
NavigationLink(destination: RoomView(room: room)) {
RoomListItemView(room: room)
}
}
}
}
/
THIS IS NOT WORKING
/
struct ParentView: View {
#EnvironmentObject account: Account
var body: some View {
Child1(contacts: account.contacts)
.onAppear {
self.account.listenForRoomEvents()
}
}
}
struct Child1: View {
#State var selectedContact: Contact?
var contacts: [Contact]
var body: some View {
RoomView(selectedContact.chatRoom)
UserSelectorView(contacts: contacts, selectedUser: $selectedContact) // View allowing selection of a user
}
}
I outlined my setup above; basically, I am instantiating a RoomView object with a Room instance containing all the chat events and other details. Child1 holds a selected contact state variable which is bound to two of its own subviews, one of which allows for the user to select a different contact and such.
What does not make sense to me is that the RoomView renders just fine with all it's events, but in the second solution I have it does not update when new messages come in or when one should be displayed after sending, for instance. I am passing a reference to the same Room object to it, but cannot for the life of me get it to update properly like it does in the first solution.
When I select a new user and go back to the previous one, the messages are all updated as expected.
Here is what I have tried so far:
Making Contact.chatRoom a Published variable, and then calling self.objectWillChange.send() whenever chatRoom does
Okay I finally figured this out, I have no idea why this works and it might be a dumb solution; I needed to not only pass the selectedContact as a parameter, but also the room as another parameter. The code in the outline isnt exactly as it is in my source, but if you ever run into a problem where a class variable isnt updating properly in a view try to pass the variable down from higher up in the chain.
Which version of Xcode are you running? If you are running Xcode 12.2 beta2, I'd recommend you to try it with Xcode 12.0.
I saw a similar issue with my code. After wasting a lot of hours, I finally figure it out that Xcode (I was runnig Xcode 12.2 beta2) has a bug.
SwiftUI: Updating an array item does not update the child UI immediately

Managing SwiftUI state in a typical list -> details app

I'm building an app that's similar in structure to the Apple tutorial. My app has a ListView, which navigates to a DetailsView. The DetailsView is composed of a UIKit custom UIView, which I wrap with a UIViewRepresentable. So far, so good.
Now I have for now a list (let's say, of addresses) that I instantiate in memory, to be replaced with core data eventually. I'm able to bind (using #EnvironmentObject) the List<Address> to the ListView.
Where I'm stuck is binding the elements for each DetailsView. The Apple tutorial, referenced above, does something which I think isn't great - for some reason (that I can't figure out), it:
Binds the List to the details view (using #EnvironmentObject)
Passes the element (in the Apple tutorial case, landmark, in my case, an address) to the details view
During updating in response to a user gesture, it effectively searches the List for the element, to update the element in the list. This seems expensive especially if the list is large.
Here's the code for #3 which to me is suspect:
Button(action: {
self.userData.landmarks[self.landmarkIndex].isFavorite.toggle()
})
In their code, self.landmarkIndex does a linear search:
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
What I'm trying to do is to bind the element directly to the DetailsView and have updates to the element update the list. So far, I have been unable to achieve this.
Does anyone know the right way? It seems like the direction the tutorial is pointing to does not scale.
Instead of passing a Landmark object, you can pass a Binding<Landmark>.
LandmarkList.swift: Change the iteration from userData.landmark to their indices so you can get the binding. Then pass the bidding into LandmarkDetail and LandmarkRow
struct LandmarkList: View {
#EnvironmentObject private var userData: UserData
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.landmarks.indices) { index in
if !self.userData.showFavoritesOnly || self.userData.landmarks[index].isFavorite {
NavigationLink(
destination: LandmarkDetail(landmark: self.$userData.landmarks[index])
.environmentObject(self.userData)
) {
LandmarkRow(landmark: self.$userData.landmarks[index])
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
LandmarkDetail.swift: Change landmark into Binding<Landmark> and toggle the favorite based on the binding
#Binding var landmark: Landmark
.
.
.
Button(action: {
self.landmark.isFavorite.toggle()
}) {
if self.landmark
.isFavorite {
Image(systemName: "star.fill")
.foregroundColor(Color.yellow)
} else {
Image(systemName: "star")
.foregroundColor(Color.gray)
}
}
LandmarkRow.swift: Change landmark to a Binding
#Binding var landmark: Landmark
Here is an example of approach to use binding directly to model Address item
Assuming there is view model like, where Address is Identifiable struct
class AddressViewModel: ObservableObject {
#Published var addresses: [Address] = []
}
So somewhere in ListView u can use the following
ForEach (Array(vm.addresses.enumerated()), id: \.element.id) { (i, address) in
NavigationLink("\(address.title)",
destination: DetailsView(address: self.$vm.addresses[i])) // pass binding !!
}