Is there any way to define computed property as #Published property in ObservableObject?
I know it is possible in the case of #Binding.
var computedValue: Binding<Double> { Binding (
get: {
self.value + 3.0
},
set: { _ in }
)}
You can use willSet for Published values.
#Published var myVar : Bool = false {
willSet {
objectWillChange.send()
print("Will set to new variable, which is \(newValue)")
}
}
you can access newValue for the new Value inside the willSet function and your variable
Related
I need to start an object during the init of another one.
But this object uses a #Binding variable to a #State variable, like this:
struct MainView:View {
#State var myVar = false
private var myObject:MyObject
init()
let myObject($myVar)
self.myObject = myObject
}
but I cannot pass $myVar to myObject because self was not initialized alreay, so I tried to
struct MainView:View {
#State var myVar = false
private var myObject:MyObject
init()
let myObject(.constant(false))
self.myObject = myObject
}
This is MyObject
class MyObject {
#Binding var myVar:Bool
init(_ myVar:Binding<Bool>) }
self._myvar = myVar
}
but by doing so, myVar inside MyObject is permanently locked in the false state and cannot be changed.
How do I pass a dummy value to MyObject during initialization that does not lock the variable forever?
You can make the compiler accept it. But it will not work (here the Toggle will not move) :
struct MainView: View {
#State private var myVar = false
var myObject: MyObject!
init() {
myObject = MyObject($myVar)
}
var body: some View {
VStack {
Text(myVar.description)
Toggle("", isOn: myObject.$myVar)
}
}
}
Because it doesn't make much sense to have myVar (the State) and myObject (with the Binding) in the same view, I made two different views, a MainView and a SubView:
import SwiftUI
struct MainView: View {
#State private var myVar = false
var body: some View {
VStack {
Text(myVar.description)
Toggle("MainView :", isOn: $myVar)
SubView($myVar)
}
.background(Color.blue.opacity(0.3))
}
}
struct SubView: View {
var myObject: MyObject
init(_ myVar: Binding<Bool>) {
myObject = MyObject(myVar)
}
var body: some View {
VStack {
Text(myObject.myVar.description)
Toggle("SubView :", isOn: myObject.$myVar)
}.background(Color.yellow)
}
}
class MyObject: ObservableObject {
#Binding var myVar: Bool
init(_ myVar: Binding<Bool>) {
_myVar = myVar
}
}
It is indeed a two-way Binding: the Toggle of the SubView acts on that of the MainView and vice versa.
Now you will have to find a use for this object. Because you could have exposed the property myVar (with the propertyWrapper #Binding) directly in the SubView.
I'm trying to figure out how to link the #Binding passed into a custom View to an #Published from that view's model. Essentially I'm trying to create a reusable integer only TextField. I'm using the below code, which works to set the integer value into the text field, but what I can't figure out is how to update the binding when the text changes.
private class IntegerTextFieldValue: ObservableObject {
#Published var value = "" {
didSet {
let numbersOnly = value.filter { $0.isNumber }
if value != numbersOnly {
value = numbersOnly
}
}
}
}
struct IntegerTextField: View {
#Binding var value: Int?
#StateObject private var fieldValue = IntegerTextFieldValue()
var placeholder = ""
var body: some View {
TextField(placeholder, text: $fieldValue.value)
.keyboardType(.numberPad)
.onAppear {
if let value = value {
fieldValue.value = "\(value)"
}
}
}
}
If I understand you correctly
.onChange (of: fieldValue.value) { vl in
value = vl
}
this modifier updates the binding value to $fieldValue.value
Here is modified code to demo a possible approach (tested with Xcode 12.1 / iOS 14.1):
private class IntegerTextFieldValue: ObservableObject {
#Published var value = "" {
didSet {
let numbersOnly = value.filter { $0.isNumber }
if value != numbersOnly {
value = numbersOnly
}
if let number = Int(value) {
numberValue = number
}
}
}
#Published var numberValue: Int = 0
}
struct IntegerTextField: View {
#Binding var value: Int?
#StateObject private var fieldValue = IntegerTextFieldValue()
var placeholder = ""
var body: some View {
TextField(placeholder, text: $fieldValue.value)
.keyboardType(.numberPad)
.onAppear {
if let value = value {
fieldValue.value = "\(value)"
}
}
.onChange(of: fieldValue.numberValue) {
if $0 != self.value {
self.value = $0
}
}
}
}
Given the following...
import SwiftUI
class ViewModel: ObservableObject {
var value: Bool
init(value: Bool) {
self.value = value
}
func update() {
value = !value
}
}
struct A: View {
#ObservedObject let viewModel: ViewModel
init(value: Bool) {
viewModel = ViewModel(value: value)
}
var body: some View {
Text("\(String(viewModel.value))")
.onTapGesture {
viewModel.update()
}
}
}
struct B: View {
#State var val = [true, false, true]
var body: some View {
A(value: val[0])
}
}
How do I get viewModel to update B's val? It looks like I should be able to use #Binding inside of A but I can't use #Binding inside ViewModel, which is where I want the modification code to run. Then, I don't think I'd need #ObservedObject because the renders would flow from B.
You either need Binding, or an equivalent that does the same thing, in ViewModel. Why do you say you can't use it?
struct A: View {
#ObservedObject var model: Model
init(value: Binding<Bool>) {
model = .init(value: value)
}
var body: some View {
Text(String(model.value))
.onTapGesture(perform: model.update)
}
}
extension A {
final class Model: ObservableObject {
#Binding private(set) var value: Bool
init(value: Binding<Bool>) {
_value = value
}
func update() {
value.toggle()
}
}
}
struct B: View {
#State var val = [true, false, true]
var body: some View {
A(value: $val[0])
}
}
If you want to update the value owned by a parent, you need to pass a Binding from the parent to the child. The child changes the Binding, which updates the value for the parent.
Then you'd need to update that Binding when the child's own view model updates. You can do this by subscribing to a #Published property:
struct A: View {
#ObservedObject var viewModel: ViewModel
#Binding var value: Bool // add a binding
init(value: Binding<Bool>) {
_value = value
viewModel = ViewModel(value: _value.wrappedValue)
}
var body: some View {
Button("\(String(viewModel.value))") {
viewModel.update()
}
// subscribe to changes in view model
.onReceive(viewModel.$value, perform: {
value = $0 // update the binding
})
}
}
Also, don't forget to actually make the view model's property #Published:
class ViewModel: ObservableObject {
#Published var value: Bool
// ...
}
I am learning SwiftUI and having trouble with data flow.
I can't figure out how to create a "state"-variable to keep track of a list of published child items.
I need a variable that is true if at least one child item is selected. These items are mapped to SwiftUI toggle-switches, which works. (Making a computed property is not allowed)
Property wrapper cannot be applied to a computed property
class Main : ObservableObject {
#Published var items : [Item]
//This is what i want, but not allowed
#Published var selectedItemsExist : Bool {
get {
var exists = false
for item in items {
if item.selected {
exists = true
break
}
}
return exists
}
}
init(items: [Item]) {
self.items = items
}
}
class Item : Identifiable, ObservableObject {
var id = UUID()
var name : String!
#Published var selected : Bool = false
}
And then in my view be able to do something like
if main.selectedItemsExist {
Text("This text is shown if at least on toggle is on")
}
Any guidance appreciated!
Please find below the demo of approach how this could be done. Tested as worked with Xcode 11.2 / iOS 13.2
class Main : ObservableObject {
#Published var items : [Item] {
didSet { // << as item is value array will be updated on item changed
var exists = false
for item in items {
if item.selected {
exists = true
break
}
}
self.selectedItemsExist = exists
}
}
#Published var selectedItemsExist : Bool = false
init(items: [Item]) {
self.items = items
}
}
struct Item : Identifiable { // << value type, so changed on any property change
var id = UUID()
var name : String!
var selected : Bool = false
init(_ name: String) {
self.name = name
}
}
struct TestCalculablePublishing: View {
#ObservedObject var main = Main(items: [Item("1"), Item("2"), Item("3")])
var body: some View {
VStack {
Button("Test") {
self.main.items[1].selected.toggle() // << just for test purpose
}
if main.selectedItemsExist {
Text("This text is shown if at least on toggle is on")
}
}
}
}
struct TestCalculablePublishing_Previews: PreviewProvider {
static var previews: some View {
TestCalculablePublishing()
}
}
I want a #Published variable to be persisted, so that it's the same every time when I relaunch my app.
I want to use both the #UserDefault and #Published property wrappers on one variable. For example I need a '#PublishedUserDefault var isLogedIn'.
I have the following propertyWrapper
import Foundation
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
This is my Settings class
import SwiftUI
import Combine
class Settings: ObservableObject {
#Published var isLogedIn : Bool = false
func doLogin(params:[String:String]) {
Webservice().login(params: params) { response in
if let myresponse = response {
self.login = myresponse.login
}
}
}
}
My View class
struct HomeView : View {
#EnvironmentObject var settings: Settings
var body: some View {
VStack {
if settings.isLogedIn {
Text("Loged in")
} else{
Text("Not Loged in")
}
}
}
}
Is there a way to make a single property wrapper that covers both the persisting and the publishing?
import SwiftUI
import Combine
fileprivate var cancellables = [String : AnyCancellable] ()
public extension Published {
init(wrappedValue defaultValue: Value, key: String) {
let value = UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
self.init(initialValue: value)
cancellables[key] = projectedValue.sink { val in
UserDefaults.standard.set(val, forKey: key)
}
}
}
class Settings: ObservableObject {
#Published(key: "isLogedIn") var isLogedIn = false
...
}
Sample: https://youtu.be/TXdAg_YvBNE
Version for all Codable types check out here
To persist your data you could use the #AppStorage property wrapper.
However, without using #Published your ObservableObject will no longer put out the news about the changed data. To fix this, simply call objectWillChange.send() from the property's willSet observer.
import SwiftUI
class Settings: ObservableObject {
#AppStorage("Example") var example: Bool = false {
willSet {
// Call objectWillChange manually since #AppStorage is not published
objectWillChange.send()
}
}
}
It should be possible to compose a new property wrapper:
Composition was left out of the first revision of this proposal,
because one can manually compose property wrapper types. For example,
the composition #A #B could be implemented as an AB wrapper:
#propertyWrapper
struct AB<Value> {
private var storage: A<B<Value>>
var wrappedValue: Value {
get { storage.wrappedValue.wrappedValue }
set { storage.wrappedValue.wrappedValue = newValue }
}
}
The main benefit of this approach is its predictability: the author of
AB decides how to best achieve the composition of A and B, names it
appropriately, and provides the right API and documentation of its
semantics. On the other hand, having to manually write out each of the
compositions is a lot of boilerplate, particularly for a feature whose
main selling point is the elimination of boilerplate. It is also
unfortunate to have to invent names for each composition---when I try
the compose A and B via #A #B, how do I know to go look for the
manually-composed property wrapper type AB? Or maybe that should be
BA?
Ref: Property WrappersProposal: SE-0258
You currently can't wrap #UserDefault around #Published since that is not currently allowed.
The way to implement #PublishedUserDefault is to pass an objectWillChange into the wrapper and call it before setting the variable.
struct HomeView : View {
#StateObject var auth = Auth()
#AppStorage("username") var username: String = "Anonymous"
var body: some View {
VStack {
if username != "Anonymous" {
Text("Logged in")
} else{
Text("Not Logged in")
}
}
.onAppear(){
auth.login()
}
}
}
import SwiftUI
import Combine
class Auth: ObservableObject {
func login(params:[String:String]) {
Webservice().login(params: params) { response in
if let myresponse = response {
UserDefaults.standard.set(myresponse.login, forKey: "username")`
}
}
}
}