Swift access binding from another struct - swift

I'm new to swift so sorry if this question is confusing, I'm not 100% sure I'm even asking the right question. I'm loosely following this guide for creating a macOS app with objects in a sidebar. Unlike in that tutorial I'm using Coredata to store the objects, and I also started on macOS as opposed to expanding the app to work on macOS, but I don't think that should matter.
I'm on section 6, trying to create a binding for the selected sidebar element so I can use it for menu commands. However since I'm using Coredata instead of the data model that they're using, I'm not sure how to go about it. I got a binding to work storing the index of the selected item, as opposed to the actual object, like in the guide. However I would prefer to do it how it's done in the guide (the reason I did it the other way is I want to set a default selection in the sidebar, and I couldn't figure out how to do it this way), and I can't figure out how to get it to work with menu commands.
Here is my SwiftUI code that gets the data from Coredata and displays it, and includes the binding (The way I have it implemented using the index):
struct InstanceList: View {
#State var selectedInstanceIndex: Int? = 0
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
entity: Instance.entity(),
sortDescriptors:
[
NSSortDescriptor(
keyPath: \Instance.userOrder,
ascending: true),
NSSortDescriptor(
keyPath:\Instance.name,
ascending: true )
]
) private var instances: FetchedResults<Instance>
var body: some View {
NavigationView {
List {
ForEach(Array(zip(instances.indices, instances)), id: \.0) { index, instance in
NavigationLink(destination: InstanceView(instance: instance), tag: index, selection: $selectedInstanceIndex) {
InstanceRow(instance: instance)
}
}
.onMove(perform: move)
}
.listStyle(SidebarListStyle())
.navigationTitle("Instances")
.frame(minWidth: 150)
.toolbar {
ToolbarItem {
Button(action: toggleSidebar, label: {
Image(systemName: "sidebar.left")
})
}
}
}
}
//Some other unrelated code is down here
}
So how would I change the binding to store an Instance object the way it stores a Landmark in the tutorial, be able to set a default selection in the sidebar, and then access the binding in another struct so I can set up menu commands?
Hopefully this made sense...
Thanks!

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.

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

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.

What's best practice for programmatic movement a NavigationView in SwiftUI

I'm working on an app that needs to open on the users last used view even if the app is completly killed by the user or ios.
As a result I'm holding last view used in UserDefaults and automatically moving the user through each view in the stack until they reach their destination.
The code on each view is as follows:
#Binding var redirectionID: Int
VStack() {
List {
NavigationLink(destination: testView(data: data, moc: moc), tag: data.id, selection:
$redirectionId) {
DataRow(data: data)
}
}
}.onAppear() {
redirectionID = userData.lastActiveView
}
Is there a better / standard way to achieve this? This works reasonably on iOS 14.* but doesn't work very well on iOS 13.* On iOS 13.* The redirection regularly doesnt reach its destination page and non of the preceeding views in the stack seem to be created. Pressing back etc results in a crash.
Any help / advice would be greatly appreciated.
This sounds like the perfect use of if SceneStorage
"You use SceneStorage when you need automatic state restoration of the value. SceneStorage works very similar to State, except its initial value is restored by the system if it was previously saved, and the value is· shared with other SceneStorage variables in the same scene."
#SceneStorage("ContentView.selectedProduct") private var selectedProduct: String?
#SceneStorage("DetailView.selectedTab") private var selectedTab = Tabs.detail
It is only available in iOS 14+ though so something manual would have to be implemented. Maybe something in CoreData. An object that would have variables for each important state variable. It would work like an ObservedObject ViewModel with persistence.
Also. you can try...
"An NSUserActivity object captures the app’s state at the current moment in time. For example, include information about the data the app is currently displaying. The system saves the provided object and returns it to the app the next time it launches. The sample creates a new NSUserActivity object when the user closes the app or the app enters the background."
Here is some sample code that summarizes how to bring it all together. It isn't a minimum reproducible example because it is a part of the larger project called "Restoring Your App's State with SwiftUI" from Apple. But it gives a pretty good picture on how to implement it.
struct ContentView: View {
// The data model for storing all the products.
#EnvironmentObject var productsModel: ProductsModel
// Used for detecting when this scene is backgrounded and isn't currently visible.
#Environment(\.scenePhase) private var scenePhase
// The currently selected product, if any.
#SceneStorage("ContentView.selectedProduct") private var selectedProduct: String?
let columns = Array(repeating: GridItem(.adaptive(minimum: 94, maximum: 120)), count: 3)
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(productsModel.products) { product in
NavigationLink(destination: DetailView(product: product, selectedProductID: $selectedProduct),
tag: product.id.uuidString,
selection: $selectedProduct) {
StackItemView(itemName: product.name, imageName: product.imageName)
}
.padding(8)
.buttonStyle(PlainButtonStyle())
.onDrag {
/** Register the product user activity as part of the drag provider which
will create a new scene when dropped to the left or right of the iPad screen.
*/
let userActivity = NSUserActivity(activityType: DetailView.productUserActivityType)
let localizedString = NSLocalizedString("DroppedProductTitle", comment: "Activity title with product name")
userActivity.title = String(format: localizedString, product.name)
userActivity.targetContentIdentifier = product.id.uuidString
try? userActivity.setTypedPayload(product)
return NSItemProvider(object: userActivity)
}
}
}
.padding()
}
.navigationTitle("ProductsTitle")
}
.navigationViewStyle(StackNavigationViewStyle())
.onContinueUserActivity(DetailView.productUserActivityType) { userActivity in
if let product = try? userActivity.typedPayload(Product.self) {
selectedProduct = product.id.uuidString
}
}
.onChange(of: scenePhase) { newScenePhase in
if newScenePhase == .background {
// Make sure to save any unsaved changes to the products model.
productsModel.save()
}
}
}
}

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