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
Compiling for iOS 10.3, But Module 'swiftuicharts' Has a Minimum Deployment Target of iOS 13.0
Optional Protocol Requirements, I Can't Get It to Work
Calling Consecutive Animations in Swift with Completion Handler
How to Specify The Name of The Output Executable
Generating Random Doable Math Problems Swift
Tvos Button Inside Navigationlink Is Not Working
Swift Getnameinfo Unreliable Results for Ipv6
What Is The Advantage of Closure Stored Property Initialisation
Create Custom Action in a Class for Use in Interface Builder
Load a Spritekit Scene from Another Bundle
My UIcollectionview Does Not Scroll Smoothly Using Swift
Implement a Crosshair Kind Behaviour in Realitykit
Alamofire Request Not Working in Swift 4 Project
How to Disable a Button of a Nstoolbar of Macos X in Swift
How to Delay Animation in Swiftui
Swift Bug? Calling Super Class Method When Subclass with Generic Type
Add Catextlayer on Top of Nsimageview
Swift Package: Ld: Warning: Dylib Was Built for Newer Macos Version (11.0) Than Being Linked (10.15)