I have a sliderVal that needs to be accessed in various other places, so my understanding is that I should use a EnvironmentObject (or BindableObject, but EnvironmentObject was nice because this is used in the subviews as well) to hold those values. Something like this
struct ExampleView : View {
#EnvironmentObject var externalData : ExternalData
var body: some View {
Slider(value: self.externalData.$sliderVal, from: 1, through: 100, by: 1)
}
}
final class ExternalData : BindableObject {
let didChange = PassthroughSubject<ExternalData, Never>()
var sliderVal : Double = 5.0 {
didSet {
didChange.send(self)
}
}
}
This usually works for things like Text, however here, when I try to use the $ symbol to bind to something like a Slider, it gives a compile error of
Value of type 'ExternalData' has no member '$sliderVal'; did you mean 'sliderVal'?
But using just sliderVal gives me the error
Cannot convert value of type 'Double' to expected argument type 'Binding<_>'
Trying to use #State for sliderVal in my enviroment object builds successfully, but gives me this error at runtime
Fatal error: Accessing State<Double> outside View.body
Which makes since since #State things should be private.
What is the correct way to do this? I need to be able to access the sliderVal outside my view on a separate thread, so using a #Binding or #State doesn't work
I found an answer here, the trick is to move the $ and not use self:
struct ExampleView : View {
#EnvironmentObject var externalData : ExternalData
var body: some View {
Slider(value: $externalData.sliderVal, from: 1, through: 100, by: 1)
}
}
final class ExternalData : BindableObject {
let didChange = PassthroughSubject<ExternalData, Never>()
var sliderVal : Double = 5.0 {
didSet {
didChange.send(self)
}
}
}
Related
I have a custom view
struct CustomView: View {
#Binding var text: String
...
var body: some View {
VStack {
...
Text(SomeText)
.offset(y: text.isEmpty ? 0 : -25)
.scaleEffect(text.isEmpty ? 1 : 0.5, anchor: .leading)
.animation(.spring(), value: text.isEmpty)
...
}
the scale effect and offset animation never trigger if text is referenced from an object.
For example, I have a ViewModel such as
class SomeViewModel: ObservableObject {
#Published var text: String = ""
}
And the parent View such as
struct ParentView: View {
#State private var vm = SomeViewModel()
#State private var text = "" //this one works!
...
var body: some View {
...
CustomView(..., text: $vm.text) // no animation, but the value of v.name is updated
CustomView(..., text: $text)) // everything works, including animation
For SwiftUI to properly update based on changes of an ObservableObject, you will need to use a different property wrapper. Usually this is either #ObservedObject or #StateObject dependent on the context you are using it in.
Try using #StateObject if this is where you are first initialising the class.
StateObject Documentation
My #Binding weight variable connects to my Source of Truth further up in my code. But I also need to let my user edit this with a TextField(). So, I am trying to create a local variable of type String because TextField requires type Bindable.
Perhaps I'm approaching this wrong.
struct SetsBar: View {
#Binding var weight: Int
#Binding var reps: Int
#State var weightString: String = String(weight)
init(weight: Binding<Int>, reps: Binding<Int>) {
self._weight = weight
self._reps = reps
}
var body: some View {
HStack {
TextField("\(weight)", text: $weightString)
}
}
}
I get an error on my #State property
Cannot use instance member 'weight' within property initializer; property initializers run before 'self' is available
You can bind weight directly using TextField variant with formatter (configuring formatter as much as needed, below is simplified variant for demo), like
var body: some View {
HStack {
TextField("\(weight)", value: $weight, formatter: NumberFormatter())
}
}
I have a class like this:
class GlobalVariables: ObservableObject {
#Published var aaa = AAA()
#Published var bbb = BBB()
#Published var ccc = CCC()
}
When I want to access that, I add this to a view:
#EnvironmentObject var globalVariables : GlobalVariables
and that's it.
So, I did that to my view called MyView.
and I am happy. I can access globalVariables almost anywhere inside MyView.
But, and there is always a but, MyView contains this method:
func initNotification() {
let gv = globalVariables // 1
NotificationCenter.default
.addObserver(forName: .runOnDetectedObject,
object: nil,
queue: OperationQueue.main,
using: {notification in
globalVariables.aaa(object:myObj)) // 2
})
}
//1 and //2 compile fine, but when I run, both lines crash with
Thread 1: Fatal error: No ObservableObject of type GlobalVariables found. A View.environmentObject(_:) for GlobalVariables may be missing as an ancestor of this view.
I have this on MyApp.swift
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(GlobalVariables())
}
}
The view I am having the problem is not ContentView()
Why?
Without a Minimal Reproducible Example it is impossible to help you troubleshoot.
But, you have to pass the EnvironmentObject down.
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var globalVariables : GlobalVariables
var body: some View {
MyView().environmentObject(globalVariables)
}
}
It usually only works for about 2-3 layers. If you go any deeper than that it is pretty buggy.
In this specific case, when I try to change an #EnvironmentObject's #Published var, I find that the view is not invalidated and updated immediately. Instead, the change to the variable is only reflected after navigating away from the modal and coming back.
import SwiftUI
final class UserData: NSObject, ObservableObject {
#Published var changeView: Bool = false
}
struct MasterView: View {
#EnvironmentObject var userData: UserData
#State var showModal: Bool = false
var body: some View {
Button(action: { self.showModal.toggle() }) {
Text("Open Modal")
}.sheet(isPresented: $showModal, content: {
Modal(showModal: self.$showModal)
.environmentObject(self.userData)
} )
}
}
struct Modal: View {
#EnvironmentObject var userData: UserData
#Binding var showModal: Bool
var body: some View {
VStack {
if userData.changeView {
Text("The view has changed")
} else {
Button(action: { self.userData.changeView.toggle() }) {
Text("Change View")
}
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MasterView().environmentObject(UserData())
}
}
#endif
Is this a bug or am I doing something wrong?
This works if changeView is a #State var inside Modal. It also works if it's a #State var inside MasterView with a #Binding var inside Modal. It just doesn't work with this setup.
A couple of things.
Your setup doesn't work if you move the Button into MasterView either.
You don't have a import Combine in your code (don't worry, that alone doesn't help).
Here's the fix. I don't know if this is a bug, or just poor documentation - IIRC it states that objectWillChange is implicit.
Along with adding import Combine to your code, change your UserData to this:
final class UserData: NSObject, ObservableObject {
var objectWillChange = PassthroughSubject<Void, Never>()
#Published var changeView: Bool = false {
willSet {
objectWillChange.send()
}
}
}
I tested things and it works.
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).
In Xcode 11 GM2, If you have overridden objectWillChange, then it needs to call send() on setter of a published variable.
If you don't overridden objectWillChange, once the published variables in #EnvironmentObject or #ObservedObject change, the view should be refreshed. Since in Xcode 11 GM2 objectWillChange already has a default instance, it is no longer necessary to provide it in the ObservableObject.
I'm having a problem trying to get textfields working in SwiftUI.
I get Fatal error: Accessing State> outside View.body whenever I try to run the following code.
Anyone have a suggestion?
struct SearchRoot : View {
#State var text: String = ""
var body: some View {
HStack {
TextField($text,
placeholder: Text("type something here..."))
Button(action: {
// Closure will be called once user taps your button
print(self.$text)
}) {
Text("SEND")
}
}
}
}
I'm running Xcode Version 11.0 beta (11M336w) on macOS 10.15 Beta (19A471t)
Edit: Simplified code, still getting the same error.
struct SearchRoot : View {
#State var text: String = ""
var body: some View {
TextField($text,
placeholder: Text("type something here..."))
}
}
The compiler emits an error if the $ operator is used outside body, in a View.
The button initializer is defined as:
init(action: #escaping () -> Void, #ViewBuilder label: () -> Label)
You're using $ in an escaping closure, in the first snippet of code.
That means the action may outlive (escape) the body, hence the error.
The second snippet compiles and works fine for me.
Eureka! SwiftUI wants a single source of truth.
What I neglected to include in my original code snippets is that this struct is within a tabbed application.
To fix this I needed to define the #State var text: String = "" in the struct that creates the top level TabbedView, then use $Binding in the SearchRoot.
I'm not sure if this is works as designed or just a beta 1 issue, but it's the way it works for now.
struct ContentView : View {
#State private var selection = 0
#State private var text: String = "searching ex"
var body: some View {
TabbedView(selection: $selection){
ShoppingListRoot().body.tabItemLabel(Text("Cart")).tag(0)
SearchRoot(text: $text).body.tabItemLabel(Text("Search")).tag(1)
StoreRoot().body.tabItemLabel(Text("Store")).tag(2)
BudgetRoot().body
.tabItemLabel(Text("Budget"))
.tag(3)
SettingsRoot().body
.tabItemLabel(Text("Settings"))
.tag(4)
}
}
}