I am trying to set the value of a #State var in var a of struct A from a var b of struct B, but it doesn't work. I need to use a #State var because I am passing it as a binding. Ex:
struct A : View {
#State var myBindableVar = ""
var body : some View {
TextField(self.$myBindableVar) ...
}
}
struct B : View {
#State var a : A
var body : some View {
Button(action: { self.a.myBindableVar = "???" }) { ... }
}
}
myBindableVar isn't set to "???" when the button is tapped. Why?
You need to use #Binding to achieve this. Here is some example code. I let View B appear inside View A so that you can directly see the working result on screen:
struct A : View {
#State var myBindableVar = ""
var body : some View {
VStack {
Text(myBindableVar)
Spacer()
B(myBindableVar: $myBindableVar)
}
}
}
struct B : View {
#Binding var myBindableVar : String
var body : some View {
Button(action: { self.myBindableVar = "Text appears" }) {
Text("Press to change")
}
}
}
Related
I have an ImageEditView that contains an ImageCanvasView and an ImageCaptureButton.
Ideally, I want ImageCaptureButton to call a method on ImageCanvasView called takeScreenshot.
How do I achieve this in SwiftUI? I've been thinking of trying to save ImageCanvasView into a variable in ImageEditView so that my ImageCaptureButton can then call its method, but SwiftUI's declarative nature means this isn't possible.
----- EDIT below -----
The flow is as follows:
ImageSelectView (user selects an image)
ImageEditView (user edits an image) - this view contains ImageCanvasView and ImageCaptureButton
ImageShareView (user shares the image)
The following is ImageEditView
import SwiftUI
struct ImageEditView: View {
#State var selectedImage: Image
#State private var isNavLinkPresented = false
#State private var imageSnapshot: UIImage = UIImage()
var canvasView: ImageCanvasView = ImageCanvasView(selectedImage: $selectedImage)
// this won't work: Cannot use instance member '$selectedImage' within property initializer; property initializers run before 'self' is available
var body: some View {
VStack {
canvasView
Spacer()
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: ImageShareView(imageToShare: $imageSnapshot), isActive: $isNavLinkPresented) {
Text("Next")
.onTapGesture {
imageSnapshot = canvasView.takeScreenshot()
isNavLinkPresented = true
}
}
}
}
.navigationBarTitle(Text("Edit image"))
}
}
You are in the right direction. By creating var of the view, you can call the function.
Here is the example demo
struct ImageEditView: View {
var canvasView: ImageCanvasView = ImageCanvasView()
var body: some View {
VStack {
Text("ImageEditView")
canvasView
Button("ImageCaptureButton") {
canvasView.takeScreenshot()
}
}
}
}
struct ImageCanvasView: View {
var body: some View {
Text("ImageCanvasView")
}
func takeScreenshot() {
print(#function + " Tapped ")
}
}
You can also use computed property to pass data
struct ImageEditView: View {
var data: String = ""
var canvasView: ImageCanvasView {
ImageCanvasView(data: data)
}
// Body code
}
struct ImageCanvasView: View {
var data: String
var body: some View {
Text("ImageCanvasView")
}
func takeScreenshot() {
print(#function + " Tapped ")
}
}
EDIT
Use init to use the same instance of ImageCanvasView.
struct ImageEditView: View {
#State var selectedImage: Image
#State private var isNavLinkPresented = false
#State private var imageSnapshot: UIImage = UIImage()
var canvasView: ImageCanvasView!
init(selectedImage: Image) {
self.selectedImage = selectedImage
canvasView = ImageCanvasView(selectedImage: $selectedImage)
}
// Other code
What is the best practice to share variables between views?
My app has only one view. But as it gets more and more complicated, I think I should separate it into several views. Also to separate the methods.
I started with something like this:
struct ContentView: View {
#State var str: String = "String"
var body: some View {
VStack(alignment: .leading) {
Text(str)
TextField("Input", text: $str)
Button("button", action: { doSomething() })
}.padding()
}
func doSomething() {
str = str + " " + str
}
}
And want to go there:
class GlobalVars: ObservableObject {
#Published var str: String = "Initial String"
}
struct ContentView: View {
#ObservedObject var globalvars = GlobalVars()
var body: some View {
VStack(alignment: .leading) {
DisplayView()
EditView()
ControlView()
}.padding()
}
}
struct DisplayView: View {
#Binding var str: String
var body: some View {
Text(self.globalvars.str)
}
}
struct EditView: View {
#Binding var str: String
var body: some View {
TextField("Input", text: self.$str)
}
}
struct ControlView: View {
#Binding var str: String
var body: some View {
Button("button", action: { doSomething() })
}
}
func doSomething() {
#Binding var str: String
self.str = self.str + " " + self.str
}
I tried with #Published, #ObservedObject and #Binding. But don't get it. Thank you for any pointer in advance.
There are a number of ways to approach this.
My choice would probably be passing the binding just to the variable that you need access to. That might look like this:
class GlobalVars: ObservableObject {
#Published var str: String = "Initial String"
}
struct ContentView: View {
#ObservedObject var globalvars = GlobalVars()
var body: some View {
VStack(alignment: .leading) {
DisplayView(str: globalvars.str) //don't need a binding here since it doesn't get modified
EditView(str: $globalvars.str)
ControlView(str: $globalvars.str)
}.padding()
}
}
struct DisplayView: View {
var str: String //don't need a binding here since it doesn't get modified
var body: some View {
Text(str)
}
}
struct EditView: View {
#Binding var str: String
var body: some View {
TextField("Input", text: $str)
}
}
struct ControlView: View {
#Binding var str: String
var body: some View {
Button("button", action: { doSomething() })
}
func doSomething() {
str = str + " " + str
}
}
Note that now in ContentView, there's a parameter passed to each of the subviews, containing a binding (using the $ sign) to the GlobalVars str property.
Also, doSomething got moved into the body of ControlView
You could also use EnvironmentObject to handle this. I'm personally not as big of a fan of this approach because I'd rather see explicitly where my parameters are going. It also gives the subviews access to the entire ObservableObject, which isn't really necessary. But, it shows you the principal:
class GlobalVars: ObservableObject {
#Published var str: String = "Initial String"
}
struct ContentView: View {
#ObservedObject var globalvars = GlobalVars()
var body: some View {
VStack(alignment: .leading) {
DisplayView()
EditView()
ControlView()
}.padding()
.environmentObject(globalvars)
}
}
struct DisplayView: View {
#EnvironmentObject var globalvars : GlobalVars
var body: some View {
Text(globalvars.str)
}
}
struct EditView: View {
#EnvironmentObject var globalvars : GlobalVars
var body: some View {
TextField("Input", text: $globalvars.str)
}
}
struct ControlView: View {
#EnvironmentObject var globalvars : GlobalVars
var body: some View {
Button("button", action: { doSomething() })
}
func doSomething() {
globalvars.str = globalvars.str + " " + globalvars.str
}
}
Note that now, globalvars is passed to the children by being placed in the view hierarchy with .environmentObject. Each subview has access to it by declaring a property of #EnvironmentObject var globalvars : GlobalVars
You could also do kind of a hybrid model where you explicitly pass the ObservableObject as a parameter to the child view:
struct ContentView: View {
#ObservedObject var globalvars = GlobalVars()
var body: some View {
VStack(alignment: .leading) {
DisplayView(globalvars: globalvars)
}.padding()
.environmentObject(globalvars)
}
}
struct DisplayView: View {
#ObservedObject var globalvars : GlobalVars
var body: some View {
Text(globalvars.str)
}
}
Here I got an extension which works fine for updating itself at lunch, but after Lunch or Appear, it does not reports the change of data to its View.
My Goal: I want my Extension get sensitive to value that is reporting.
import SwiftUI
struct ContentView: View {
#State var changeString: String = "Hello, world!"
var body: some View {
customText().stringOfText(changeString).padding()
Button("change") { changeString = "Omid" }.padding()
Text(changeString).padding()
}
}
struct customText: View {
#State var stringOfText = ""
var body: some View
{
Text(stringOfText)
}
}
extension customText {
func stringOfText(_ text: String) -> customText {
customText(stringOfText: text)
}
}
It should not be state inside, because state is not recreated, ie. you need
struct customText: View {
var stringOfText: String = "" // << here !!
var body: some View {
Text(stringOfText)
}
}
I have a SwiftUI List, that changes an attribute on a row, e.g. color on a tap.
Now I want to start an action e.g. reset the color, if another row is tapped.
I´m looking for an event, that the row receives ,if it is deselected.
Here my example code:
struct ContentView: View {
#State var data : [String] = ["first","second","third","4th","5th"]
var body: some View {
List {
ForEach (data, id: \.self) {
item in
ColoredRow(text: item)
}
}
}
}
struct ColoredRow: View {
var text: String = ""
#State var col : Color = Color.white
var body: some View{
Text("\(text)")
.background(col)
.onTapGesture {
self.col = Color.red
}
// .onDeselect {
// print("disappeare \(self.text)")
// self.col = Color.white
// }
}
}
Let' recall that SwiftUI is reactive (ie. state-driven, not event-driven), so if we wan't to change something in UI we need to find a way to change it via state (either UI or model, but state).
So, below is slightly modified your code to show possible approach. Tested with Xcode 11.2 / iOS 13.2.
struct ContentView: View {
#State var data : [String] = ["first","second","third","4th","5th"]
#State private var selectedItem: String? = nil
var body: some View {
List {
ForEach (data, id: \.self) {
item in
ColoredRow(text: item, selection: self.$selectedItem)
}
}
}
}
struct ColoredRow: View {
var text: String = ""
#Binding var selection: String?
#State var col : Color = Color.white
var body: some View{
Text("\(text)")
.background(selection == text ? Color.red : Color.white)
.onTapGesture {
self.selection = (self.selection == self.text ? nil : self.text)
}
}
}
I'm building a UI component with SwiftUI that should have trigger from outside to turn on animation and some inner preparations for it. In examples below it's prepareArray() function.
My first approach was to use bindings, but I've found that there is no way to listen when #Binding var changes to trigger something:
struct ParentView: View {
#State private var animated: Bool = false
var body: some View {
VStack {
TestView(animated: $animated)
Spacer()
Button(action: {
self.animated.toggle()
}) {
Text("Toggle")
}
Spacer()
}
}
}
struct TestView: View {
#State private var array = [Int]()
#Binding var animated: Bool {
didSet {
prepareArray()
}
}
var body: some View {
Text("\(array.count): \(animated ? "Y" : "N")").background(animated ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
}
private func prepareArray() {
array = [1]
}
}
Why then it allows didSet listener for #Binding var if it's not working?! Then I switched to simple Combine signal since it's can be caught in onReceive closure. But #State on signal was not invalidating view on value pass:
struct ParentView: View {
#State private var animatedSignal = CurrentValueSubject<Bool, Never>(false)
var body: some View {
VStack {
TestView(animated: animatedSignal)
Spacer()
Button(action: {
self.animatedSignal.send(!self.animatedSignal.value)
}) {
Text("Toggle")
}
Spacer()
}
}
}
struct TestView: View {
#State private var array = [Int]()
#State var animated: CurrentValueSubject<Bool, Never>
var body: some View {
Text("\(array.count): \(animated.value ? "Y" : "N")").background(animated.value ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1)).onReceive(animated) { animated in
if animated {
self.prepareArray()
}
}
}
private func prepareArray() {
array = [1]
}
}
So my final approach was to trigger inner state var on signal value:
struct ParentView: View {
#State private var animatedSignal = CurrentValueSubject<Bool, Never>(false)
var body: some View {
VStack {
TestView(animated: animatedSignal)
Spacer()
Button(action: {
self.animatedSignal.send(!self.animatedSignal.value)
}) {
Text("Toggle")
}
Spacer()
}
}
}
struct TestView: View {
#State private var array = [Int]()
let animated: CurrentValueSubject<Bool, Never>
#State private var animatedInnerState: Bool = false {
didSet {
if animatedInnerState {
self.prepareArray()
}
}
}
var body: some View {
Text("\(array.count): \(animatedInnerState ? "Y" : "N")").background(animatedInnerState ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1)).onReceive(animated) { animated in
self.animatedInnerState = animated
}
}
private func prepareArray() {
array = [1]
}
}
Which works fine, but I can't believe such a simple task requires so complicated construct! I know that SwiftUI is declarative, but may be I'm missing more simple approach for this task? Actually in real code this animated trigger will have to be passed to one more level deeper(
It is possible to achieve in many ways, including those you tried. Which one to choose might depend on real project needs. (All tested & works Xcode 11.3).
Variant 1: modified your first try with #Binding. Changed only TestView.
struct TestView: View {
#State private var array = [Int]()
#Binding var animated: Bool
private var myAnimated: Binding<Bool> { // internal proxy binding
Binding<Bool>(
get: { // called whenever external binding changed
self.prepareArray(for: self.animated)
return self.animated
},
set: { _ in } // here not used, so just stub
)
}
var body: some View {
Text("\(array.count): \(myAnimated.wrappedValue ? "Y" : "N")")
.background(myAnimated.wrappedValue ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
}
private func prepareArray(for animating: Bool) {
DispatchQueue.main.async { // << avoid "Modifying state during update..."
self.array = animating ? [1] : [Int]() // just example
}
}
}
Variant2 (my preferable): based on view model & publishing, but requires changes both ParentView and TestView, however in general simpler & clear.
class ParentViewModel: ObservableObject {
#Published var animated: Bool = false
}
struct ParentView: View {
#ObservedObject var vm = ParentViewModel()
var body: some View {
VStack {
TestView()
.environmentObject(vm) // alternate might be via argument
Spacer()
Button(action: {
self.vm.animated.toggle()
}) {
Text("Toggle")
}
Spacer()
}
}
}
struct TestView: View {
#EnvironmentObject var parentModel: ParentViewModel
#State private var array = [Int]()
var body: some View {
Text("\(array.count): \(parentModel.animated ? "Y" : "N")")
.background(parentModel.animated ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
.onReceive(parentModel.$animated) {
self.prepareArray(for: $0)
}
}
private func prepareArray(for animating: Bool) {
self.array = animating ? [1] : [Int]() // just example
}
}