setting computed property in a SwiftUI view doesn't compile - swift

Trying to set the computed property s in a SwiftUI view gets compiler error "Cannot assign to property: 'self' is immutable".
How do I have to I call the setter?
struct Test: View{
#State var _s = "test"
#State var _s2 = true
private var s : String
{ get { _s }
set (new)
{ _s = "no test"
_s2 = false
// do something else
}
}
var body: some View
{ Text("\(s)")
.onTapGesture {
self.s = "anyting" // compiler error
}
}
}

Aha... I see. Just use non mutating set
private var s : String
{ get { _s }
nonmutating set (new)
{ _s = "no test"
_s2 = false
// do something else
}
}

That is, why you already have #State property wrapper in your View.
struct Test: View{
#State var s = "test"
var body: some View {
Text("\(s)")
.onTapGesture {
self.s = "anyting" // compiler error
}
}
}
You able to change s directly from your code because s is wrapped with #State.
this is functional equivalent of the above
struct Test: View{
let s = State<String>(initialValue: "alfa")
var body: some View {
VStack {
Text("\(s.wrappedValue)")
.onTapGesture {
self.s.wrappedValue = "beta"
}
}
}
}
Or if Binding is needed
struct Test: View{
let s = State<String>(initialValue: "alfa")
var body: some View {
VStack {
TextField("label", text: s.projectedValue)
}
}
}

Related

Is there a simpler way to zip two .onReceive in swiftui

#State private var showUpEmotion = false
#State private var showDownEmotion = false
When two pieces of data from an observableobject come online some view is shown
HStack {
if showUpEmotion && showDownEmotion
{
SomeViewIsShown()
}
}
.onReceive(model.$meLike) { value in
withAnimation {
if value != nil {
showUpEmotion = true
} else {
showUpEmotion = false
}
}
}
.onReceive(model.$meDislike) { value in
withAnimation {
if value != nil {
showDownEmotion = true
} else {
showDownEmotion = false
}
}
}
Is there a simpler/cleaner way to zip that data from ObservableObject ?
naturally withAnimation does not compile in the observableobject proper -> I have to use that inside the view :(
It's not clear from the question what you're trying to achieve, but possibly moving some of your Bools into structs would simplify?
struct ShowEmotions {
var up = false
var down = false
var both: Bool {
up && down
}
}
struct Likes {
var like = false
var dislike = false
}
class Model: ObservableObject {
#Published var me = Likes()
}
struct ContentView: View {
#State private var showEmotions = ShowEmotions()
#StateObject var model = Model()
var body: some View {
HStack {
if showEmotions.both {
SomeViewIsShown()
}
}
.onReceive(model.$me) { me in
withAnimation {
showEmotions.up = me.like
showEmotions.down = me.dislike
}
}
}
}
This way you only need a single onReceive

SwiftUI Custom Environment Value

I try to make custom environment key to read its value as shown in the code below, I read many resources about how to make it and all have the same approach.
Example Code
struct Custom_EnvironmentValues: View {
#State private var isSensitive = false
var body: some View {
VStack {
// Update the value here <---
Toggle(isSensitive ? "Sensitive": "Not sensitive", isOn: $isSensitive)
PasswordField(password: "123456")
.isSensitive(isSensitive)
}.padding()
}
}
struct PasswordField: View {
let password: String
#Environment(\.isSensitive) private var isSensitive
var body: some View {
HStack {
Text("Password")
Text(password)
// It should update the UI here but that not happened <---
.foregroundColor(isSensitive ? .red : .green)
.redacted(reason: isSensitive ? .placeholder: [])
}
}
}
// 1
private struct SensitiveKey: EnvironmentKey {
static let defaultValue: Bool = false
}
// 2
extension EnvironmentValues {
var isSensitive: Bool {
get { self[SensitiveKey.self] }
set { self[SensitiveKey.self] = newValue }
}
}
// 3
extension View {
func isSensitivePassword(_ value: Bool) -> some View {
environment(\.isSensitive, value)
}
}
When I try to make a custom environment value and read it, its not work, the key value not update at all.
You just need to inject into the environment
struct Custom_EnvironmentValues: View {
#State private var isSensitive = false
var body: some View {
VStack {
Toggle(isSensitive ? "Sensitive": "Not sensitive", isOn: $isSensitive)
PasswordField(password: "123456")
.isSensitivePassword(isSensitive) //your function name
}.padding()
}
}

Is didSet on a #Binding file specific?

Essentially, I'm nesting #Binding 3 layers deep.
struct LayerOne: View {
#State private var doubleValue = 0.0
var body: some View {
LayerTwo(doubleValue: $doubleValue)
}
}
struct LayerTwo: View {
#Binding var doubleValue: Double {
didSet {
print(doubleValue)
}
}
var body: some View {
LayerThree(doubleValue: $doubleValue)
}
}
struct LayerThree: View {
#Binding var doubleValue: Double {
didSet {
print(doubleValue) // Only this print gets run when doubleValue is updated from this struct
}
}
var body: Some view {
// Button here changes doubleValue
}
}
Whichever struct I change doubleValue in is the one where the didSet will get run, so for example if I change it in LayerThree only that one will print, none of the others will.
I am able to watch for changes with .onChange(of: doubleValue) which will then get run when it changes but it's not making sense to me why didSet won't get run except on the struct where it's changed from.
Is #Binding struct specific?
Using property observers like didSet on values wrapped in PropertyWrappers will not have the "normal" effect because the value is being set inside the wrapper.
In SwiftUI, if you want to trigger an action when a value changes, you should use the onChange(of:perform:) modifier.
struct LayerTwo: View {
#Binding var doubleValue: Double
var body: some View {
LayerThree(doubleValue: $doubleValue)
.onChange(of: doubleValue) { newValue
print(newValue)
}
}
}
To see why this happens, we can unveil the syntactic sugar of property wrappers. #Binding var doubleValue: Double translates to:
private var _doubleValue: Binding<Double>
var doubleValue: Double {
get { _doubleValue.wrappedValue }
set { _doubleValue.wrappedValue = newValue }
}
init(doubleValue: Binding<Double>) {
_doubleValue = doubleValue
}
Whatever you do in didSet will be put after the line _doubleValue.wrappedValue = newValue. It should be very obvious why when you update doubleValue in layer 3, The didSet of doubleValue in layer 2 or 1 doesn't get called. They are simply different computed properties!
swiftPunk's solution works by creating a new binding whose setter sets the struct's doubleValue, hence calling didSet:
Binding(get: { return doubleValue },
set: { newValue in doubleValue = newValue }
// ^^^^^^^^^^^^^^^^^^^^^^
// this will call didSet in the current layer
Now all working:
struct ContentView: View {
var body: some View {
LayerOne()
}
}
struct LayerOne: View {
#State private var doubleValue:Double = 0.0 {
didSet {
print("LayerOne:", doubleValue)
}
}
var body: some View {
LayerTwo(doubleValue: Binding(get: { return doubleValue }, set: { newValue in doubleValue = newValue } ))
}
}
struct LayerTwo: View {
#Binding var doubleValue: Double {
didSet {
print("LayerTwo:", doubleValue)
}
}
var body: some View {
LayerThree(doubleValue: Binding(get: { return doubleValue }, set: { newValue in doubleValue = newValue } ))
}
}
struct LayerThree: View {
#Binding var doubleValue: Double {
didSet {
print("LayerThree:", doubleValue)
}
}
var body: some View {
Text(String(describing: doubleValue))
Button("update value") {
doubleValue = Double.random(in: 0.0...100.0)
}
.padding()
}
}
print results:
LayerOne: 64.58963263686678
LayerTwo: 64.58963263686678
LayerThree: 64.58963263686678

How to use Bind an Associative Swift enum?

I have a GroupView that accepts a binding as a parameter because I want the GroupView to modify the data in the enum.
Can some help me on how to accomplish this?
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
GroupView(group: /* What do i put here? */) // <----------------
}
}
}
struct GroupView: View {
#Binding var group: Group
var body: some View {
Text("Hello World")
}
}
class ViewModel : ObservableObject {
#Published var instruction: Instruction!
init() {
instruction = .group(Group(groupTitle: "A Group struct"))
}
}
enum Instruction {
case group(Group)
}
struct Group { var groupTitle: String }
Well, this certainly will work... but probably there's a better approach to your problem. But no one is in a better position than you, to determine that. So I'll just answer your question about how to pass a binding.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
GroupView(group: viewModel.groupBinding)
}
}
}
class ViewModel : ObservableObject {
#Published var instruction: Instruction!
init() {
instruction = .group(Group(groupTitle: "A Group struct"))
}
var groupBinding: Binding<Group> {
return Binding<Group>(get: {
if case .group(let g) = self.instruction {
return g
} else {
return Group(groupTitle: "")
}
}, set: {
self.instruction = .group($0)
})
}
}

How to delete an object in SwiftUI which is marked with #ObjectBinding?

I want to delete an object which is marked as #ObjectBinding, in order to clean up some TextFields for example.
I tried to set the object reference to nil, but it didn't work.
import SwiftUI
import Combine
class A: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var text = "" { didSet { didChange.send() } }
}
class B {
var property = "asdf"
}
struct DetailView : View {
#ObjectBinding var myObject: A = A() //#ObjectBinding var myObject: A? = A() -> Gives an error.
#State var mySecondObject: B? = B()
var body: some View {
VStack {
TextField($myObject.text, placeholder: Text("Enter some text"))
Button(action: {
self.test()
}) {
Text("Clean up")
}
}
}
func test() {
//myObject = nil
mySecondObject = nil
}
}
If I try to use an optional with #ObjectBinding, I'm getting the Error
"Cannot convert the value of type 'ObjectBinding' to specified type
'A?'".
It just works with #State.
Regards
You can do something like this:
class A: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var form = FormData() { didSet { didChange.send() } }
struct FormData {
var firstname = ""
var lastname = ""
}
func cleanup() {
form = FormData()
}
}
struct DetailView : View {
#ObjectBinding var myObject: A = A()
var body: some View {
VStack {
TextField($myObject.form.firstname, placeholder: Text("Enter firstname"))
TextField($myObject.form.lastname, placeholder: Text("Enter lastname"))
Button(action: {
self.myObject.cleanup()
}) {
Text("Clean up")
}
}
}
}
I absolutely agree with #kontiki , but you should remember to don't use #State when variable can get outside. #ObjectBinding right way in this case. Also all new way of memory management already include optional(weak) if they need it.
Check this to get more information about memory management in SwiftUI
Thats how to use #ObjectBinding
struct DetailView : View {
#ObjectBinding var myObject: A
and
DetailView(myObject: A())