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

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

Related

How to update an element of an array in an Observable Object

Sorry if my question is silly, I am a beginner to programming. I have a Navigation Link to a detail view from a List produced from my view model's array. In the detail view, I want to be able to mutate one of the tapped-on element's properties, but I can't seem to figure out how to do this. I don't think I explained that very well, so here is the code.
// model
struct Activity: Identifiable {
var id = UUID()
var name: String
var completeDescription: String
var completions: Int = 0
}
// view model
class ActivityViewModel: ObservableObject {
#Published var activities: [Activity] = []
}
// view
struct ActivityView: View {
#StateObject var viewModel = ActivityViewModel()
#State private var showingAddEditActivityView = false
var body: some View {
NavigationView {
VStack {
List {
ForEach(viewModel.activities, id: \.id) {
activity in
NavigationLink(destination: ActivityDetailView(activity: activity, viewModel: self.viewModel)) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
}
}
.navigationBarItems(trailing: Button("Add new"){
self.showingAddEditActivityView.toggle()
})
.navigationTitle(Text("Activity List"))
}
.sheet(isPresented: $showingAddEditActivityView) {
AddEditActivityView(copyViewModel: self.viewModel)
}
}
}
// detail view
struct ActivityDetailView: View {
#State var activity: Activity
#ObservedObject var viewModel: ActivityViewModel
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
activity.completions += 1
updateCompletionCount()
}
Text("\(activity.completeDescription)")
}
}
func updateCompletionCount() {
var tempActivity = viewModel.activities.first{ activity in activity.id == self.activity.id
}!
tempActivity.completions += 1
}
}
// Add new activity view (doesn't have anything to do with question)
struct AddEditActivityView: View {
#ObservedObject var copyViewModel : ActivityViewModel
#State private var activityName: String = ""
#State private var description: String = ""
var body: some View {
VStack {
TextField("Enter an activity", text: $activityName)
TextField("Enter an activity description", text: $description)
Button("Save"){
// I want this to be outside of my view
saveActivity()
}
}
}
func saveActivity() {
copyViewModel.activities.append(Activity(name: self.activityName, completeDescription: self.description))
print(copyViewModel.activities)
}
}
In the detail view, I am trying to update the completion count of that specific activity, and have it update my view model. The method I tried above probably doesn't make sense and obviously doesn't work. I've just left it to show what I tried.
Thanks for any assistance or insight.
The problem is here:
struct ActivityDetailView: View {
#State var activity: Activity
...
This needs to be a #Binding in order for changes to be reflected back in the parent view. There's also no need to pass in the entire viewModel in - once you have the #Binding, you can get rid of it.
// detail view
struct ActivityDetailView: View {
#Binding var activity: Activity /// here!
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
activity.completions += 1
}
Text("\(activity.completeDescription)")
}
}
}
But how do you get the Binding? If you're using iOS 15, you can directly loop over $viewModel.activities:
/// here!
ForEach($viewModel.activities, id: \.id) { $activity in
NavigationLink(destination: ActivityDetailView(activity: $activity)) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
And for iOS 14 or below, you'll need to loop over indices instead. But it works.
/// from https://stackoverflow.com/a/66944424/14351818
ForEach(Array(zip(viewModel.activities.indices, viewModel.activities)), id: \.1.id) { (index, activity) in
NavigationLink(destination: ActivityDetailView(activity: $viewModel.activities[index])) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
You are changing and increment the value of tempActivity so it will not affect the main array or data source.
You can add one update function inside the view model and call from view.
The view model is responsible for this updation.
class ActivityViewModel: ObservableObject {
#Published var activities: [Activity] = []
func updateCompletionCount(for id: UUID) {
if let index = activities.firstIndex(where: {$0.id == id}) {
self.activities[index].completions += 1
}
}
}
struct ActivityDetailView: View {
var activity: Activity
var viewModel: ActivityViewModel
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
updateCompletionCount()
}
Text("\(activity.completeDescription)")
}
}
func updateCompletionCount() {
self.viewModel.updateCompletionCount(for: activity.id)
}
}
Not needed #State or #ObservedObject for details view if don't have further action.

SwiftUI: ObservedObject/ObservableObject weird behavior

I recently encountered the following problem in SwiftUI with ObservedObject/ObservableObject:
If the ObservedObject/ObservableObject is publishing in a View, the body property is recalculated - as expected.
But if there is a sub View in the body property which also has an ObservedObject, the View, strangely enough, not only recalculates the body property of the sub View, but the entire object.
The state of the ObservedObject is of course lost from the sub View.
Of course, you can prevent this by adding the ObservableObject to the sub View through .environmentObject(), but I don't think that's the best solution, especially with more complex view hierarchies.
Here an example Code:
struct ContentView: View {
#ObservedObject var contentViewModel: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
Button {
self.contentViewModel.counter += 1
} label: {
Text(String(self.contentViewModel.counter))
}
SubView()
}
}
}
class ContentViewModel: ObservableObject {
#Published var counter: Int = 0
}
And the sub View:
struct SubView: View {
#ObservedObject var subViewModel: SubViewModel = SubViewModel()
var body: some View {
Button {
self.subViewModel.counter += 1
} label: {
Text(String(self.subViewModel.counter))
}
}
}
class SubViewModel: ObservableObject {
#Published var counter: Int = 0
}
And here how the sample Code looks/works:
The last weird thing I realised, this is only the case if you use Observed Object. If I would replace in the sub View #ObservedObject var subViewModel: SubViewModel = SubViewModel() with #State var counter: Int = 0 it is working fine again and the state is preserved.
Maybe im missing out on something, but im really confused. Im very grateful for any answers and solutions. If you have further questions fell free to leave a comment, I will answer it within 24h (as long as the question is open).
Declare your ViewModel with #StateObject. #StateObject is not recreated for every view re-render more
struct SubView: View {
#StateObject var subViewModel: SubViewModel = SubViewModel() //<--- Here
var body: some View {
Button {
self.subViewModel.counter += 1
} label: {
Text(String(self.subViewModel.counter))
}
}
}
I found a perfect solution:
Either you replace the #ObservedObject with #StateObject (thx #Raja Kishan) in iOS 14 (SwiftUI v2.0) or you can create a Wrapper for the View and simulate the behaviour of #StateObject in iOS 13 (SwiftUI v1.0):
struct SubViewWrapper: View {
#State var subViewModel: SubViewModel = SubViewModel()
var body: some View {
SubView(subViewModel: self.subViewModel)
}
}
and then use SubViewWrapper() in ContentView instead of SubView()
struct ContentView: View {
#ObservedObject var contentViewModel: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
Button {
self.contentViewModel.counter += 1
} label: {
Text(String(self.contentViewModel.counter))
}
SubViewWrapper()
}
}
}

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"))
}
}
}
}

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

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)

SwiftUI store View as a variable while passing it some bindings

I want to store a View as a variable for later use, while passing that View some Bindings.
Here's what I've tried:
struct Parent: View {
#State var title: String? = ""
var child: Child!
init() {
self.child = Child(title: self.$title)
}
var body: some View {
VStack {
child
//...
Button(action: {
self.child.f()
}) {
//...
}
}
}
}
struct Child: View {
#Binding var title: String?
func f() {
// complex work from which results a string
self.title = <that string>
}
var body: some View {
// ...
}
}
It compiles correctly and the View shows as expected, however when updating from the child the passed Binding from the parent, the variable never gets updated. You can even do something like this (from the child):
self.title = "something"
print(self.title) // prints the previous value, in this case nil
I don't know if this is a bug or not, but directly initializing the child in the body property does the trick. However, I need that child as a property to access its methods.
If you want to change something from Parent for the child, binding is the right way. If that's complicated, you have to use DataModel.
struct Parent: View {
#State var title: String? = ""
var body: some View {
VStack {
Child(title: $title)
Button(action: {
self.title = "something"
}) {
Text("click me")
}
}
}
}
struct Child: View {
#Binding var title: String?
var body: some View {
Text(title ?? "")
}
}
This is counter to the design of the SwiftUI framework. You should not have any persistent view around to call methods on. Instead, views are created and displayed as needed in response to your app's state changing.
Encapsulate your data in an ObservableObject model, and implement any methods you need to call on that model.
Update
It is fine to have such a function defined in Child, but you should only be calling it from within the Child struct definition. For instance, if your child view contains a button, that button can call the child's instance methods. For example,
struct Parent: View {
#State private var number = 1
var body: some View {
VStack {
Text("\(number)")
Child(number: $number)
}
}
}
struct Child: View {
#Binding var number: Int
func double() {
number *= 2
}
var body: some View {
HStack {
Button(action: {
self.double()
}) {
Text("Double")
}
}
}
}
But you wouldn't try to call double() from outside the child struct. If you wanted a function that can be called globally, put it in a data model. This is especially true if the function call is making network requests, as the model will stick around outside your child view, even if it is recreated due to layout changing.
class NumberModel: ObservableObject {
#Published var number = 1
func double() {
number *= 2
}
}
struct Parent: View {
#ObservedObject var model = NumberModel()
var body: some View {
VStack {
Text("\(model.number)")
Button(action: {
self.model.double()
}) {
Text("Double from Parent")
}
Child(model: model)
}
}
}
struct Child: View {
#ObservedObject var model: NumberModel
var body: some View {
HStack {
Button(action: {
self.model.double()
}) {
Text("Double from Child")
}
}
}
}