How to Get My Button to Reflect The Bool of My Environmentobject

How can I get my Button to reflect the Bool of my EnvironmentObject?

Method 1

You can just do a quick check if there will be an out-of-bounds error. SwiftUI sometimes holds views for a little longer and it can cause issues like this.

In ColouredText, change:

.foregroundColor(query.frames[value] ? dvColors[value] : .gray)

To:

.foregroundColor(value < query.frames.count && query.frames[value] ? dvColors[value] : .gray)

Method 2

Another way to fix this (but I probably wouldn't do this, because you are taking away the splitting of views) is to just put ColouredText's body directly where it is needed:

struct TheHStack: View {
@EnvironmentObject var query: Query

var body: some View {
print(query.frames)
return HStack (spacing: 10) {
ForEach(query.frames.indices, id: \.self) { value in
Button(action: {
query.frames[value].toggle()
print(query.frames)
print("\(value)")
}, label: {
Text("\(value + 1)")
.foregroundColor(query.frames[value] ? dvColors[value] : .gray)
})
}
}
}
}

Method 3

You can also fix this by passing in query at the same time as value. This means that they will never be out of sync.

ForEach(query.frames.indices, id: \.self) { value in
ColouredText(value: value, query: query)
}
struct ColouredText: View {
let value: Int
let query: Query

var body: some View {
/* ... */
}
}

@EnvironmentObject in SwiftUI - can't toggle bool

The issue is that you named your custom View as Button, which is also the name of the existing SwiftUI button.

Simply rename your struct to something else and your code will compile just fine.

Unrelated to your question, but there's no point in wrapping a single View in a VStack, your body can simply contain the Button.

struct MyButton: View {

@EnvironmentObject var menuState: Menu

var body: some View {
Button(action: {
self.menuState.isActive.toggle()
}) {
Text("A")
}
}
}

Change to @Published var in @EnvironmentObject not reflected immediately

Changing

final class UserData: NSObject, ObservableObject  {

to

final class UserData: ObservableObject  {

does fix the issue in Xcode11 Beta6. SwiftUI does seem to not handle NSObject subclasses implementing ObservableObject correctly (at least it doesn't not call it's internal willSet blocks it seems).

How to uniquely get Button if I extract as a subview in SwiftUI

In order to do this, you'd want to move the @State from your child view into the parent view. Then, you can share it with the child view's through a Binding.

In this example, I have one @State variable that stores which id is highlighted. When a button is pressed, it simply updates the value.

struct TipButton: View, Identifiable {
var id: Int
var tipPercentage: String
@Binding var highlightedID : Int

var body: some View {
Button {
highlightedID = id
print("Tip is... \(Float(tipPercentage) ?? 7.7)")
} label: {
Text("\(tipPercentage)%")
.font(.largeTitle)
.bold()
}
.background(id == highlightedID ? Color.green : Color.clear)
}
}

struct ButtonsView: View {

@State private var highlightedID : Int = 3

var body: some View {
TipButton(id: 1, tipPercentage: "0", highlightedID: $highlightedID)
TipButton(id: 2, tipPercentage: "10", highlightedID: $highlightedID)
TipButton(id: 3, tipPercentage: "20", highlightedID: $highlightedID)
}

}

When pressing button and toggling a Bool var all of my Bool vars toggle?

You put all buttons in one row (VStack with HStacks creates one view, so one row), and Form (being a List) sends all actions whenever any button is clicked in a row (it is designed to have one active element in a row).

So the solution would be either to remove VStack

Section(header: Text("Addons")
.font(.title3)
.foregroundColor(.blue)
.padding(.bottom, 10)
) {
VStack(alignment: .leading, spacing: 5) { // << this !!
Spacer()

and let every HStack with button live in own row...

... or instead of buttons use Image with tap gesture, like

    HStack {
Text("Steps")
Spacer()
Image(systemName: steps ? "checkmark.square" : "square")
.padding()
.onTapGesture {
self.steps.toggle()
}
}

The UI styling of Button is not displayed after being clicked

This is a pretty simple error. The @Published property wrapper will send a notification to the view to update itself as soon as its value changes.

But in your case this never happens. You defined Book as a class (reference type), so changing one of its property doesn´t force the array (valuetype) to change, so @Published doesn´t pick up the change.

Two solutions here:

  • If you insist on keeping the class use:

    func updateFavourite(forId: Int) {
    if let index = books.firstIndex(where: { $0.id == forId }) {
    objectWillChange.send() // add this
    books[index].isFavourite.toggle()
    }
    }

    this will send the notification by hand.

  • the prefered solution should be to make your model Book a struct and it will pick up the changes.

@EnvironmentObject property not working properly in swiftUI

You need to have one instance of DataStorage that gets passed around. Any time you write DataStorage() that creates a new instance.

.environmentObject will let you inject that one instance into the view hierarchy. Then, you can use the @EnvironmentObject property wrapper to access it within a View.

Inside View1, I used onAppear to set the dataStorage property on View1ViewModel -- that means that it has to be an optional on View1ViewModel since it will not be set in init. The reason I'm avoiding setting it in init is because an @EnvironmentObject is not set as of the init of the View -- it gets injected at render time.

@main
struct Example_AppApp: App {
var dataStorage = DataStorage()

var body: some Scene {
WindowGroup {
ContentView().environmentObject(dataStorage)
}
}
}

class DataStorage: ObservableObject {
@Published var cartArray = [Book]()
}

struct ContentView: View {
@State var showSheetView = false

var body: some View {
NavigationView{

ListViewDisplay()
.navigationBarItems(trailing:
Button(action: {
self.showSheetView.toggle()
}) {
Image(systemName: "cart.circle.fill")
.font(Font.system(.title))
}
)
}.sheet(isPresented: $showSheetView) {
View3()
}
}
}

struct ListViewDisplay: View {
var book = [
Book(bookId: 1 ,bookName: "Catch-22"),
Book(bookId: 2 ,bookName: "Just-Shocking" ),
Book(bookId: 3 ,bookName: "Stephen King" ),
Book(bookId: 4,bookName: "A Gentleman in Moscow"),
]

var body: some View {
List(book, id: \.id) { book in
Text(book.bookName)
NavigationLink(destination: View1(book: book)) {

}
}

}
}

struct Book: Codable, Identifiable {
var id:String{bookName}
var bookId : Int
var bookName: String

}

struct BookOption: Codable{
var name: String
var price: Int
}

class View1ViewModel : ObservableObject{

var dataStorage : DataStorage?

func addBook (bookId:Int ,bookName : String) {
guard let dataStorage = dataStorage else {
fatalError("DataStorage not set")
}
dataStorage.cartArray.append(Book(bookId:bookId, bookName: bookName)) // Adding to global array
print(dataStorage.cartArray)
}
}

struct View1: View {

@ObservedObject var vwModel = View1ViewModel()
@EnvironmentObject var datastrg: DataStorage

var book:Book

var body: some View {

Text(book.bookName).font(.title)
Spacer()
Button(action: {
vwModel.addBook(bookId: book.bookId, bookName: book.bookName)

}, label: {
Text("Add Book to Cart")

.frame(maxWidth: .infinity, minHeight: 60)
.background(Color.red)
.foregroundColor(Color.white)
.font(.custom("OpenSans-Bold", size: 24))

})
.onAppear {
vwModel.dataStorage = datastrg
}
}
}

struct View3: View {
@EnvironmentObject var datastorage : DataStorage
var body: some View {
NavigationView {
List(datastorage.cartArray,id:\.id){book in
VStack{
Text(book.bookName)
.font(.custom("OpenSans-Bold", size: 20))

}
}
.navigationBarTitle(Text("Cart"), displayMode: .inline)
}
}
}

Is this the right way for using @ObservedObject and @EnvironmentObject?

No, we use @State for view data like if a toggle isOn, which can either be a single value itself or a custom struct containing multiple values and mutating funcs. We pass it down the View hierarchy by declaring a let in the child View or use @Binding var if we need write access. Regardless of if we declare it let or @Binding whenever a different value is passed in to the child View's init, SwiftUI will call body automatically (as long as it is actually accessed in body that is).

@StateObject is for when a single value or a custom struct won't do and we need a reference type instead for view data, i.e. if persisting or syncing data (not using the new async/await though because we use .task for that). The object is init before body is called (usually before it is about to appear) and deinit when the View is no longer needed (usually after it disappears).

@EnvironmentObject is usually for the store object that holds model structs in @Published properties and is responsible for saving or syncing,. The difference is the model data is not tied to any particular View, like @State and @StateObject are for view data. This object is usually a singleton, one for the app and one with sample data for when previewing, because it should never be deinit. The advantage of @EnvironmentObject over @ObservedObject is we don't need to pass it down through each View as a let that don't need the object when we only need it further down the hierarchy. Note the reason it has to be passed down as a let and not @ObservedObject is then body would be needlessly called in the intermediate Views because SwiftUI's dependency tracking doesn't work for objects only value types.

Here is some sample code:

struct MyConfig {
var isOn = false
var message = ""

mutating func reset() {
isOn = false
message = ""
}
}

struct MyView: View {
@State var config = MyConfig() // grouping vars into their struct makes use of value semantics to track changes (a change to any of its properties is detected as a change to the struct itself) and offers testability.

var body: some View {
HStack {
ViewThatOnlyReads(config: config)
ViewThatWrites(config: $config)
}
}
}

struct ViewThatOnlyReads: View {
let config: MyConfig

var body: some View {
Text(config.isOn ? "It's on" : "It's off")
}
}

struct ViewThatWrites: View {
@Binding var config: MyConfig

var body: some View {
Toggle("Is On", isOn: $config.isOn)
}
}

Timer within EnvironmentObject view model not updating the View

Managed to resolve the issue with help of @lorem ipsum and his feedback. As per his comment, the problem lied with the fact that

it is more than likely not working because you are chaining ObservableObjects @Published will only detect a change when the object is changed as a whole now when variables change. One way to test is to wrap each SetInformationTestClass in an @ObservbleObject by using a subview that takes the object as a parameter.

After which, I managed to find similar SO answers on changes in nested view model (esp child), and made the child view model an ObservedObject. The changes in child view model got populated to the view. Please see the changed code below.

SetRestDetailView

import Foundation
import SwiftUI
import Combine

struct SetRestDetailView: View {

@EnvironmentObject var watchDayProgramVM: WatchDayProgramViewModel

var setCurrentHeartRate: Int = 120
@State var showingLog = false


var body: some View {

HStack {

let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime

let setInformatationVM = self.watchDayProgramVM.exerciseVMList[0].sets[2]

TimerText(setInformationVM: setInformatationVM, rect: rect)
.border(Color.yellow)

}

HStack {

SetTimerPlayPauseButton(isSetTimerRunningFlag: false,
playImage: "play.fill",
pauseImage: "pause.fill",
bgColor: Color.clear,
fgColor: Color.white.opacity(0.5),
rect: rect) {

print("playtimer button tapped")
self.watchDayProgramVM.exerciseVMList[0].sets[2].startTimer()


let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
print("printing elapsedRestTime from SetRestDetailView \(elapsedRestTime)")
print("printing elapsedRestTime from SetRestDetailView \(totalRestTime)")

}
.border(Color.yellow)

}

}

}

TimerText

struct TimerText: View {

@ObservedObject var setInformationVM: SetInformationTestClass

// @State var elapsedRestTime: Int
// @State var totalRestTime: Int
var rect: CGRect

var body: some View {
VStack {
Text(counterToMinutes())
.font(.system(size: 100, weight: .semibold, design: .rounded))
.kerning(0)
.fontWeight(.semibold)
.minimumScaleFactor(0.25)
.padding(-1)
}
}

func counterToMinutes() -> String {
let currentTime = setInformationVM.totalRestTime - setInformationVM.elapsedRestTime
let seconds = currentTime % 60
let minutes = Int(currentTime / 60)

if currentTime > 0 {
return String(format: "%02d:%02d", minutes, seconds)
}

else {
return ""
}
}
}


Related Topics



Leave a reply



Submit