I try to maing a global publisher,like:
#Published var counter = 0
But here is a error:
Property wrappers are not yet supported in top-level code
How to share global Publisher variables between models?
If you want a global variable that publishes a value in a similar way to an #Published property you can do
var counter = CurrentValueSubject<Int,Never>(0)
let subscription = counter.sink { print("The counter is \($0)") }
counter1.value = 3
But if you are using SwiftUI then as Joakim Danielson pointed out in comments you should look into making the value part of the Environment rather than having a global.
That solution would look something like:
private struct EnvironmentCounter: EnvironmentKey {
typealias Value = Int
static var defaultValue: Int = 0
static let farReachingCounter: String = "Far Reaching Counter"
}
extension EnvironmentValues {
var farReachingCounter: Int {
get { self[EnvironmentCounter.self] }
set { self[EnvironmentCounter.self] = newValue }
}
}
struct LeafView : View {
#Environment(\.farReachingCounter) var counter : Int
var body: some View {
Text("The counter is \(counter)")
}
}
struct IntermediateView : View {
var body: some View {
LeafView()
}
}
struct TopLevelView: View {
#State var counter = 0
var body: some View {
VStack {
IntermediateView()
HStack {
Button(action: { counter = counter + 1 }) {
Text("Increment")
}
Button(action: { counter = counter - 1 }) {
Text("Decrement")
}
}
}.environment(\.farReachingCounter, counter)
}
}
PlaygroundSupport.PlaygroundPage.current.liveView = UIHostingController(rootView: TopLevelView())
This code declares an EnvironmentKey that views can use to pull values out of the Environment. In this case the key is farReachingCounter.
Then it declares an extension on EnvironmentValues that gets and sets the value in and gets the value from the environment.
Finally I show a SwiftUI hierarchy where the TopLevelView defines the counter value for its whole hierarchy. There is an IntermediateView that doesn't reference the court counter at all (it's just there to show that the counter passes down the view hierarchy through the environment). Then the LeafView shows how you can pull a custom value out of the environment.
The farReachingCounter is not global to the whole module, but it is part of the SwiftUI environment from the TopLevelView down.
Here is the solution :
class Counter: ObservableObject {
#Published var counter = 0
static var shared = Counter.init()
private init() { //Just to hide the method
}
}
Related
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
}
}
}
How can I connect Binding value of View to Published value of ObservableObject?
The goal: all changes of objValue will be reflected on viewValue and vice versa.
Note: Do not propose direct onChange(obj.objValue) { viewValue = $0 } modifier. It will trigger extra drawing cycle of View (first for objValue and second for viewValue).
class MyObject: ObservableObject {
#Published var objValue: Int = 0
}
struct MyView: View {
#Binding var viewValue: Int
#StateObject var obj = MyObject()
var body: some View {
Text("Placeholder")
.onChange(viewValue) {
//Do something
}
}
}
Here is the working solution (not directly with Combine yet) that is utilising the View adapter that let us to avoid extra redraw of the MyView body.
By passing the Binding value to ValueReader View, only its body will be triggered to redraw, then it is just passing the new result outside and we can work with it. Here we assign the updated value of viewValue to objValue.
This technique is avoiding extra redraw cycles, MyView body will be redrawn only ONCE, no matter if objValue or viewValue was changed first.
Because viewValue is not being used directly in the body, only ValueReader will be redrawn directly on viewValue change skipping MyView's body redraw.
class MyObject: ObservableObject {
#Published var objValue: Int = 0
}
struct MyView: View {
#Binding var viewValue: Int
#StateObject var obj = MyObject()
var body: some View {
ZStack {
ValueReader(value: $viewValue) { newValue in
obj.objValue = newValue //Mirroring viewValue to obj.objValue
}
Text("Placeholder")
.onChange(of: obj.objValue, perform: handleValue)
}
}
private func handleValue(_ value: Int) {
viewValue = value //Mirroring obj.objValue to viewValue
//Do any job here. For example just send analytics
}
private struct ValueReader: View {
#Binding var value: Int
let onChange: (_ newValue: Int) -> ()
var body: some View {
Color.clear
.onChange(of: value) { newValue in
onChange(newValue)
}
}
}
}
I tried to encapsulate the gesture in a class and then call it in another view.
My project can run well, and it can be built well. but Xcode12 give me a pink error. This is my code
class GestureClass: ObservableObject {
private var minZoom:CGFloat=1
private var maxZoom:CGFloat=4.00
#GestureState private var magnificationLevel:CGFloat=1
#Published public var zoomLevel:CGFloat=1
#Published public var current:CGFloat=1
var magnify: some Gesture {
MagnificationGesture()
.updating($magnificationLevel, body:{(value,state,_) in
return state=value
})
.onChanged({ value in
self.current = value
})
.onEnded({ value in
self.zoomLevel = self.setZoom(value)
self.current = 1
})
}
func setZoom(_ gesturevalue:CGFloat) -> CGFloat {
return max(min(self.zoomLevel*gesturevalue, self.maxZoom), self.minZoom)
}
func gestureresult() -> CGFloat {
return self.setZoom(self.current)
}
}
struct GestureModelView: View {
#EnvironmentObject var gesturemodel:GestureClass
var body: some View {
let myges = gesturemodel.magnify
HStack {
Rectangle()
.foregroundColor(.blue)
.frame(width:200*gesturemodel.gestureresult(), height:200)
.gesture(myges)
}
}
}
I deleted this code in the computed property, and the error disappeared. But i really don't know why。
.updating($magnificationLevel, body:{(value,state,_) in
return state=value
})
A bad case of SwiftUI magic not working for me, and I am loosing my sanity here. Why is the text not updating its value here? Why is the body not reevaluated after each increment() call?
class ReadingStateVM: ObservableObject {
#Published var value = 0
func increment() {
value = value + 1
print("value \(value)")
}
}
struct ReadingStateView: View {
var viewModel = ReadingStateVM()
var body: some View {
Text("State \(viewModel.value)")
.onTapGesture {
self.viewModel.increment()
}
}
}
You need to add the #ObservedObject property wrapper so when changes happen to your view model, the view will also update.
#ObservedObject var viewModel = ReadingStateVM()
In SwiftUI we use structs and mutating funcs for data, not objects. This is because SwiftUI takes advantage of value-semantics for tracking changes. To learn this watch WWDC 2020 Data Essentials in SwiftUI at 3:58. With this in mind, your code should now look like this:
struct ContentViewConfig {
var counter = 0
var anotherRelatedVar = 0
mutating func increment() {
counter = counter + 1
print("counter: \(counter)")
}
}
struct ContentView: View {
#State var config = ContentViewConfig()
var body: some View {
Button("State \(config.counter)"){
config.increment()
}
}
}
Furthermore, ObservableObject is part of the Combine framework so you usually only use it when you want to use assign to set the result of a Combine pipeline to an #Published var. And as the other answer states, this object needs to be declared with #ObservedObject for the body to be called when an #Published property changes.
I have a #State variable that I that I want to add a certain constraint to, like this simplified example:
#State private var positiveInt = 0 {
didSet {
if positiveInt < 0 {
positiveInt = 0
}
}
}
However this doesn't look so nice (it seems to be working though) but what I really want to do is to subclass or extend the property wrapper #State somehow so I can add this constraint in it's setter. But I don't know how to do that. Is it even possible?
You can't subclass #State since #State is a Struct. You are trying to manipulate your model, so you shouldn't put this logic in your view. You should at least rely on your view model this way:
class ContentViewModel: ObservableObject {
#Published var positiveInt = 0 {
didSet {
if positiveInt < 0 {
positiveInt = 0
}
}
}
}
struct ContentView: View {
#ObservedObject var contentViewModel = ContentViewModel()
var body: some View {
VStack {
Text("\(contentViewModel.positiveInt)")
Button(action: {
self.contentViewModel.positiveInt = -98
}, label: {
Text("TAP ME!")
})
}
}
}
But since SwiftuUI is not an event-driven framework (it's all about data, model, binding and so forth) we should get used not to react to events, but instead design our view to be "always consistent with the model". In your example and in my answer here above we are reacting to the integer changing overriding its value and forcing the view to be created again. A better solution might be something like:
class ContentViewModel: ObservableObject {
#Published var number = 0
}
struct ContentView: View {
#ObservedObject var contentViewModel = ContentViewModel()
private var positiveInt: Int {
contentViewModel.number < 0 ? 0 : contentViewModel.number
}
var body: some View {
VStack {
Text("\(positiveInt)")
Button(action: {
self.contentViewModel.number = -98
}, label: {
Text("TAP ME!")
})
}
}
}
Or even simpler (since basically there's no more logic):
struct ContentView: View {
#State private var number = 0
private var positiveInt: Int {
number < 0 ? 0 : number
}
var body: some View {
VStack {
Text("\(positiveInt)")
Button(action: {
self.number = -98
}, label: {
Text("TAP ME!")
})
}
}
}
You can't apply multiple propertyWrappers, but you can use 2 separate wrapped values. Start with creating one that clamps values to a Range:
#propertyWrapper
struct Clamping<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>
init(wrappedValue value: Value, _ range: ClosedRange<Value>) {
precondition(range.contains(value))
self.value = value
self.range = range
}
var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
}
Next, create an ObservableObject as your backing store:
class Model: ObservableObject {
#Published
var positiveValue: Int = 0
#Clamping(0...(.max))
var clampedValue: Int = 0 {
didSet { positiveValue = clampedValue }
}
}
Now you can use this in your content view:
#ObservedObject var model: Model = .init()
var body: some View {
Text("\(self.model.positiveValue)")
.padding()
.onTapGesture {
self.model.clampedValue += 1
}
}