How to Bind a Value of a Dictionary to a SwiftUI Control? - swift

I have a class DataPoint which is the value of a dictionary.
DataPoint has a member variable value that I need to bind to a Slider in SwiftUI.
I provide the data class AppData as #Environment Object to SwiftUI, the dictionary holds the instances of class DataPoint.
I fail to manage to bind the DataPoint.value member variable to the SwiftUI Slider value.
These are the relevant code snippets.
The #Published data:
class AppData: ObservableObject {
#Published var dataPoints: [UUID : DataPoint] = [:] {
...
}
The data structure:
class DataPoint: Identifiable {
var id = UUID()
var value: Double
}
The SwiftUI view of DataPoints AppDataList:
struct AppDataList: View {
#EnvironmentObject var appData: AppData
var body: some View {
NavigationView {
List(Array(appData.dataPoints.values)) { dataPoint in
NavigationLink(destination: DataPointDetail(dataPointID: dataPoint.id)) {
Text("\(dataPoint.value)")
}
}
...
}
The SwiftUI DataPointDetail view that I struggle with, it is referenced from AppDataList:
struct DataPointDetail: View {
#EnvironmentObject var appData: AppData
var dataPointID: UUID
var body: some View {
HStack(alignment: .top) {
VStack(alignment: .leading) {
Text("Data Point Detail")
Text("\(appData.dataPoints[dataPointID]!.value)")
/* This line */
/* troubles */
/* me! */
/* ---> */ Slider(value: appData.dataPoints[dataPointID]?.value, in: 0...10000)
Spacer()
Text("\(appData.dataPoints[dataPointID]!.id)")
}
}
}
}
The root content view:
struct ContentView: View {
#EnvironmentObject var appData: AppData
var body: some View {
VStack {
if appData.dataPoints.count > 0 {
AppDataList()
} else {
NoDataPoints()
}
}
}
}
The creation of the root content view in SceneDelegate:
let contentView = ContentView()
.environmentObject(appData)
I get an error in the editor: Static member 'leading' cannot be used on instance of type 'HorizontalAlignment' and it is in the line of VStack in DataPointDetail view. I believe that it has got nothing to do with the VStack.
Commenting out the Slider line produces compilable and runnable code.
How would one accomplish this?

Most quick solution is to use wrapper binding, as in below snapshot
Slider(value: Binding<Double>(
get: { self.appData.dataPoints[self.dataPointID]?.value ?? 0 },
set: { self.appData.dataPoints[self.dataPointID]?.value = $0}), in: 0...10000)

Related

Providing a #Published variable to a subclass and having it updated in SwiftUI

Trying to get my head around swiftUI and passing data between classes and the single source of truth.I have an Observable Class with a #Published variable, as the source of truth. I want to use that variable in another class allowing both classes to update the value and the underlying view.
So here is a basic example of the setup. The Classes are as follows:
class MainClass:ObservableObject{
#Published var counter:Int = 0
var class2:Class2!
init() {
class2 = Class2(counter: counter )
}
}
class Class2:ObservableObject{
#Published var counter:Int
init( counter:Int ){
self.counter = counter
}
}
And the view code is as follows. The point is the AddButton View knows nothing about the MainClass but updates the Class2 counter, which I would then hope would update the content in the view:
struct ContentView: View {
#ObservedObject var mainClass:MainClass = MainClass()
var body: some View {
VStack {
Text( "counter: \(mainClass.counter )")
AddButton(class2: mainClass.class2 )
}
.padding()
}
}
struct AddButton:View{
var class2:Class2
var body: some View{
Button("Add") {
class2.counter += 1
}
}
}
Do I need to use combine and if so How?thanks.
Your need may have more complexities than I'm understanding. But if one view needs to display the counter while another updates the counter and that counter needs to be used by other views, an environment object(MainClass) can be used. That way, the environment object instance is created(main window in my example) and everything in that view hierarchy can access the object as a single source of truth.
#main
struct yourApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(MainClass()) // <-- add instance
}
}
}
class MainClass:ObservableObject{
#Published var counter:Int = 0
}
struct ContentView: View {
#EnvironmentObject var mainClass: MainClass
var body: some View {
VStack {
Text( "counter: \(mainClass.counter )")
AddButton()
}
.padding()
}
}
struct AddButton:View{
#EnvironmentObject var mainClass: MainClass
var body: some View{
Button("Add") {
mainClass.counter += 1
}
}
}
Result:
I solved the problem by making a counter class and passing that around to other classes. See attached code.
I was thinking about doing this because I can factor my code into parts and pass them around to the relevant view, making each part and view much more modular. There will be a mother of all Classes that knows how each part should interact, and parts can be updated by views as needed.
class Main{
var counter:Counter = Counter()
}
class Counter:ObservableObject{
#Published var value:Int = 0
}
class Increment{
var counter:Counter
init(counter: Counter) {
self.counter = counter
}
}
#ObservedObject var counter:Counter = Main().counter
var body: some View {
VStack {
Text( "counter: \(counter.value )")
AddButton(increment: Increment(counter: counter) )
Divider()
Button("Main increment") {
counter.value += 1
}
}
.padding()
}
}
struct AddButton:View{
var increment:Increment
var body: some View{
Button("Add") {
increment.counter.value += 1
}
}
}

SwiftUI How to have class viewable from all views

I am fairly new to SwiftUI and I am trying to build an app where you can favorite items in a list. It works in the ContentView but I would like to have the option to favorite and unfavorite an item in its DetailView.
I know that vm is not in the scope but how do I fix it?
Here is some of the code in the views. The file is long so I am just showing the relevant code
struct ContentView: View {
#StateObject private var vm = ViewModel()
//NavigationView with a List {
//This is the code I call for showing the icon. The index is the item in the list
Image(systemName: vm.contains(index) ? "heart.fill" : "heart")
.onTapGesture{
vm.toggleFav(item: index)
}
}
struct DetailView: View {
Hstack{
Image(systemName: vm.contains(entry) ? "heart.fill" : "heart") //Error is "Cannot find 'vm' in scope"
}
}
Here is the code that that vm is referring to
import Foundation
import SwiftUI
extension ContentView {
final class ViewModel: ObservableObject{
#Published var items = [Biase]()
#Published var showingFavs = false
#Published var savedItems: Set<Int> = [1, 7]
// Filter saved items
var filteredItems: [Biase] {
if showingFavs {
return items.filter { savedItems.contains($0.id) }
}
return items
}
private var BiasStruct: BiasData = BiasData.allBias
private var db = Database()
init() {
self.savedItems = db.load()
self.items = BiasStruct.biases
}
func sortFavs(){
withAnimation() {
showingFavs.toggle()
}
}
func contains(_ item: Biase) -> Bool {
savedItems.contains(item.id)
}
// Toggle saved items
func toggleFav(item: Biase) {
if contains(item) {
savedItems.remove(item.id)
} else {
savedItems.insert(item.id)
}
db.save(items: savedItems)
}
}
}
This is the list view...
enter image description here
Detail view...
enter image description here
I tried adding this code under the List(){} in the ContentView .environmentObject(vm)
And adding this under the DetailView #EnvironmentObject var vm = ViewModel() but it said it couldn't find ViewModel.
To put the view model inside the ContentView struct is wrong. Delete the enclosing extension.
If the view model is supposed to be accessed from everywhere it must be on the top level.
In the #main struct create the instance of the view model and inject it into the environment
#main
struct MyGreatApp: App {
#StateObject var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
And in any struct you want to use it add
#EnvironmentObject var vm : ViewModel
without parentheses.

SwiftUI - Nested links within NavigationStack inside a NavigationSplitView not working

I'm playing around with the new navigation API's offered in ipadOS16/macOS13, but having some trouble working out how to combine NavigationSplitView, NavigationStack and NavigationLink together on macOS 13 (Testing on a Macbook Pro M1). The same code does work properly on ipadOS.
I'm using a two-column NavigationSplitView. Within the 'detail' section I have a list of SampleModel1 instances wrapped in a NavigationStack. On the List I've applied navigationDestination's for both SampleModel1 and SampleModel2 instances.
When I select a SampleModel1 instance from the list, I navigate to a detailed view that itself contains a list of SampleModel2 instances. My intention is to navigate further into the NavigationStack when clicking on one of the SampleModel2 instances but unfortunately this doesn't seem to work. The SampleModel2 instances are selectable but no navigation is happening.
When I remove the NavigationSplitView completely, and only use the NavigationStack the problem does not arise, and i can successfully navigate to the SampleModel2 instances.
Here's my sample code:
// Sample model definitions used to trigger navigation with navigationDestination API.
struct SampleModel1: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel1(), SampleModel1(), SampleModel1()]
}
struct SampleModel2: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel2(), SampleModel2(), SampleModel2()]
}
// The initial view loaded by the app. This will initialize the NavigationSplitView
struct ContentView: View {
enum NavItem {
case first
}
var body: some View {
NavigationSplitView {
NavigationLink(value: NavItem.first) {
Label("First", systemImage: "house")
}
} detail: {
SampleListView()
}
}
}
// A list of SampleModel1 instances wrapped in a NavigationStack with multiple navigationDestinations
struct SampleListView: View {
#State var path = NavigationPath()
#State var selection: SampleModel1.ID? = nil
var body: some View {
NavigationStack(path: $path) {
List(SampleModel1.samples, selection: $selection) { model in
NavigationLink("\(model.id)", value: model)
}
.navigationDestination(for: SampleModel1.self) { model in
SampleDetailView(model: model)
}
.navigationDestination(for: SampleModel2.self) { model in
Text("Model 2 ID \(model.id)")
}
}
}
}
// A detailed view of a single SampleModel1 instance. This includes a list
// of SampleModel2 instances that we would like to be able to navigate to
struct SampleDetailView: View {
var model: SampleModel1
var body: some View {
Text("Model 1 ID \(model.id)")
List (SampleModel2.samples) { model2 in
NavigationLink("\(model2.id)", value: model2)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I removed this unclear ZStack and all works fine. Xcode 14b3 / iOS 16
// ZStack { // << this !!
SampleListView()
// }
Apple just releases macos13 beta 5 and they claimed this was resolved through feedback assistant, but unfortunately this doesn't seem to be the case.
I cross-posted this question on the apple developers forum and user nkalvi posted a workaround for this issue. I’ll post his example code here for future reference.
import SwiftUI
// Sample model definitions used to trigger navigation with navigationDestination API.
struct SampleModel1: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel1(), SampleModel1(), SampleModel1()]
}
struct SampleModel2: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel2(), SampleModel2(), SampleModel2()]
}
// The initial view loaded by the app. This will initialize the NavigationSplitView
struct ContentView: View {
#State var path = NavigationPath()
enum NavItem: Hashable, Equatable {
case first
}
var body: some View {
NavigationSplitView {
List {
NavigationLink(value: NavItem.first) {
Label("First", systemImage: "house")
}
}
} detail: {
SampleListView(path: $path)
}
}
}
// A list of SampleModel1 instances wrapped in a NavigationStack with multiple navigationDestinations
struct SampleListView: View {
// Get the selection from DetailView and append to path
// via .onChange
#State var selection2: SampleModel2? = nil
#Binding var path: NavigationPath
var body: some View {
NavigationStack(path: $path) {
VStack {
Text("Path: \(path.count)")
.padding()
List(SampleModel1.samples) { model in
NavigationLink("Model1: \(model.id)", value: model)
}
.navigationDestination(for: SampleModel2.self) { model in
Text("Model 2 ID \(model.id)")
.navigationTitle("navigationDestination(for: SampleModel2.self)")
}
.navigationDestination(for: SampleModel1.self) { model in
SampleDetailView(model: model, path: $path, selection2: $selection2)
.navigationTitle("navigationDestination(for: SampleModel1.self)")
}
.navigationTitle("First")
}
.onChange(of: selection2) { newValue in
path.append(newValue!)
}
}
}
}
// A detailed view of a single SampleModel1 instance. This includes a list
// of SampleModel2 instances that we would like to be able to navigate to
struct SampleDetailView: View {
var model: SampleModel1
#Binding var path: NavigationPath
#Binding var selection2: SampleModel2?
var body: some View {
NavigationStack {
Text("Path: \(path.count)")
.padding()
List(SampleModel2.samples, selection: $selection2) { model2 in
NavigationLink("Model2: \(model2.id)", value: model2)
// This also works (without .onChange):
// Button(model2.id.uuidString) {
// path.append(model2)
// }
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How can I get a struct's function that updates a variable in another view also refresh that view when changed?

import SwiftUI
import Combine
struct ContentView: View {
var subtract = MinusToObject()
var body: some View {
VStack{
Text("The number is \(MyObservedObject.shared.theObservedObjectToPass)")
Text("Minus")
.onTapGesture {
subtract.subtractIt()
}
NavigationView {
NavigationLink(destination: AnotherView()) {
Text("Push")
}.navigationBarTitle(Text("Master"))
}
}
}
}
class MyObservedObject: ObservableObject {
static let shared = MyObservedObject()
private init() { }
#Published var theObservedObjectToPass = 6
}
struct MinusToObject {
func subtractIt() {
MyObservedObject.shared.theObservedObjectToPass -= 1
}
}
When I hit the Minus to call the function of my subtract instance, I know the value changes because if I go to another View I can see the new value, but the current view doesn't update.
I think I have to put a property wrapper around var subtract = MinusToObject() (I've tried several pretty blindly) and I feel like I should put a $ somewhere for a two-way binding, but I can't figure out quite where.
The MinusToObject is redundant. Here is way (keeping MyObservedObject shared if you want)...
struct ContentView: View {
#ObservedObject private var vm = MyObservedObject.shared
//#StateObject private var vm = MyObservedObject.shared // << SwiftUI 2.0
var body: some View {
VStack{
Text("The number is \(vm.theObservedObjectToPass)")
Text("Minus")
.onTapGesture {
self.vm.theObservedObjectToPass -= 1
}
NavigationView {
NavigationLink(destination: AnotherView()) {
Text("Push")
}.navigationBarTitle(Text("Master"))
}
}
}
}

SwiftUI SceneDelegate - contentView Missing argument for parameter 'index' in call

I am trying to create a list using ForEach and NavigationLink of an array of data.
I believe my code (see the end of the post) is correct but my build fails due to
"Missing argument for parameter 'index' in call" and takes me to SceneDelegate.swift a place I haven't had to venture before.
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
I can get the code to run if I amend to;
let contentView = ContentView(habits: HabitsList(), index: 1)
but then all my links hold the same data, which makes sense since I am naming the index position.
I have tried, index: self.index (which is what I am using in my NavigationLink) and get a different error message - Cannot convert value of type '(Any) -> Int' to expected argument type 'Int'
Below are snippets of my code for reference;
struct HabitItem: Identifiable, Codable {
let id = UUID()
let name: String
let description: String
let amount: Int
}
class HabitsList: ObservableObject {
#Published var items = [HabitItem]()
}
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var index: Int
var body: some View {
NavigationView {
List {
ForEach(habits.items) { item in
NavigationLink(destination: HabitDetail(habits: self.habits, index: self.index)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}
}
}
}
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var habits: HabitsList
var index: Int
var body: some View {
NavigationView {
Form {
Text(self.habits.items[index].name)
}
}
}
}
You probably don't need to pass the whole ObservedObject to the HabitDetail.
Passing just a HabitItem should be enough:
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
let item: HabitItem
var body: some View {
// remove `NavigationView` form the detail view
Form {
Text(item.name)
}
}
}
Then you can modify your ContentView:
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var body: some View {
NavigationView {
List {
// for every item in habits create a `linkView`
ForEach(habits.items, id:\.id) { item in
self.linkView(item: item)
}
}
}
}
// extract to another function for clarity
func linkView(item: HabitItem) -> some View {
// pass just a `HabitItem` to the `HabitDetail`
NavigationLink(destination: HabitDetail(item: item)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}