SwiftUI #Binding update doesn't refresh view - binding

I feel like I'm missing something very basic, but this example SwiftUI code will not modify the view (despite the Binding updating) when the button is clicked
Tutorials I have read suggest this is the correct way to use a binding and the view should refresh automatically
import SwiftUI
struct ContentView: View {
#Binding var isSelected: Bool
var body: some View {
Button(action: {
self.isSelected.toggle()
}) {
Text(isSelected ? "Selected" : "Not Selected")
}
}
}
struct ContentView_Previews: PreviewProvider {
#State static var selected: Bool = false
static var previews: some View {
ContentView(isSelected: $selected)
}
}

You have not misunderstood anything. A View using a #Binding will update when the underlying #State change, but the #State must be defined within the view hierarchy. (Else you could bind to a publisher)
Below, I have changed the name of your ContentView to OriginalContentView and then I have defined the #State in the new ContentView that contains your original content view.
import SwiftUI
struct OriginalContentView: View {
#Binding var isSelected: Bool
var body: some View {
Button(action: {
self.isSelected.toggle()
}) {
Text(isSelected ? "Selected" : "Not Selected")
}
}
}
struct ContentView: View {
#State private var selected = false
var body: some View {
OriginalContentView(isSelected: $selected)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI View affects #Binding. #State affects SwiftUI View.
#State var affects the view, but to affect another #State it must be used as binding by adding leading $ to value name and it works only inside SwiftUI.
To trigger SwiftUI change from outside, i.e. to deliver/update Image, use Publisher that looks like this:
// Declare publisher in Swift (outside SwiftUI).
public let imagePublisher = PassthroughSubject<Image, Never>()
// It must be handled within SwiftUI.
struct ContentView: View {
// Declare a #State that updates View.
#State var image: Image = Image(systemName: "photo")
var body: some View {
// Use #State image declaration
// and subscribe this value to publisher "imagePublisher".
image.onReceive(imagePublisher, perform: { (output: Image) in
self.image = output // Whenever publisher sends new value, old one to be replaced
})
}
}
// And this is how to send value to update SwiftUI from Swift:
imagePublisher.send(Image(systemName: "photo"))

In the top Level of SwiftUI, #Binding cannot refresh View hierarchy unless manually adding a #state or other refreshing triggers.
struct ContentView: View {
#Binding var isSelected : Bool
#State var hiddenTrigger = false
var body: some View {
VStack {
Text("\(hiddenTrigger ? "" : "")")
Button(action: {
self.isSelected.toggle()
self.hiddenTrigger = self.isSelected
}) {
Text(self.isSelected? "Selected" : "not Selected")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var selected: Bool = false
static var previews: some View {
ContentView(isSelected: Binding<Bool>(get: {selected}, set: { newValue in
selected = newValue}))
}
}

Looking into this some more I think I understand what's happening.
In this instance I want to use #Binding as I'm building a custom control (like SwiftUI's native Toggle, which also binds to a Bool)
The issue is that the static state in ContentView_Previews (i.e., the line #State static var selected: Bool = false) does not trigger a re-render of the preview when the state changes, so even though the selected state has changed due to interaction with the control, the control (a child of ContentView_Previews) does not re-render itself
This makes it tough to test controls in isolation in the SwiftUI preview, however moving the state into a dummy ObservableObject instance functions correctly. Here's the code:
import SwiftUI
import Combine
class SomeData: ObservableObject {
#Published var isOn: Bool = false
}
struct MyButton: View {
#Binding var isSelected: Bool
var body: some View {
Button(action: {
self.isSelected.toggle()
}) {
Text(isSelected ? "Selected" : "Not Selected")
}
}
}
struct ContentView: View {
#EnvironmentObject var data: SomeData
var body: some View {
MyButton(isSelected: $data.isOn)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(SomeData())
}
}
It seems that a change in #State static var doesn't trigger a preview re-render. In the above code my #Binding example is moved into MyButton and the content view's dummy environment instance is bounds to its isSelected property. Tapping the button updates the view as expected in the SwiftUI preview.

You need to use #State instead of #Binding.
If the UI should update when its value changes, you designate a variable as a
#State variable. It is the source of truth.
You use #Binding instead of #State, when the view doesn't own this data and its not the source of truth.
Here is your variable:
#State var isSelected: Bool

In my case, having the #Binding or #State control a top level if statement caused issues.
I put the if check inside a top level VStack and it started working fine.
struct ContentView: View {
#Binding var value: Bool // #State breaks too
var body: some View {
// Add a VStack here to fix the bug
if value { // Top level `if` based on #State or #Binding won't work
Text("view 1")
} else {
Text("view 2")
.onAppear {
value = true // Won't trigger update
}
}
}
}
This was only sometimes though.. Depending on what the rest of the view hierarchy looked like. My view hierarchy was nested inside a NavigationView, a TabView, a ZStack, etc. I'm not sure what the minimum requirements are to trigger this. Really weird behavior.

Related

SwiftUI: Is it possible to layer these views without each one being scrollable?

I am presenting a sheet that will present a slider within a section which is within a form. I was trying to add my subview AddDoujin into this view as well. But the problem is, is that both views are scrollable and aren't layering on top of each other and seems like it is being added to the bottom (if that makes sense). My goal is to make the AddDoujin view right under the slider rather than both being each their own view and both of them being scrollable. Sorry if this is unclear and you need more information.
import SwiftUI
struct TestingAddDoujin: View {
//Varaibles
#State private var InputDoujin:String = ""
var DoujinApi:DoujinAPI
#Binding var isPresented:Bool
#State private var RedoEntry:Bool = false
var PickerOptions = ["Doujin", "Hentai"]
#State var PickerSelected = ""
#State var CurrentSelectionForPicker = 0
var body: some View {
GeometryReader { geo in
NavigationView{
VStack {
ZStack{
Form{
Section(header: Text("What you you looking for?")) {
Picker(selection: $CurrentSelectionForPicker, label: Text("Please select one")) {
ForEach(0..<PickerOptions.count) {
Text("\(self.PickerOptions[$0])")
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
AddDoujin(DoujinApi: DoujinApi, isPresented: $isPresented)
}
}
}
}
}
struct TestingAddDoujin_Previews: PreviewProvider {
static var previews: some View {
TestingAddDoujin(DoujinApi: DoujinAPI(), isPresented: .constant(false))
}
}
It seems like if you just move AddDoujin(DoujinApi: DoujinApi, isPresented: $isPresented) right below .pickerStyle(SegmentedPickerStyle()) You will get the desired result.
I'm guessing you've tried this. So maybe show and tell us more about what you are trying to accomplish.
btw. In Swift it's not convention to capitalize the first letter of a variable.

How to create a view that works with #Binding. It should refresh itself while working with #Binding data

This question is identical to SwiftUI #Binding update doesn't refresh view, but the accepted answer is not applicable for my case.
The accepted answer says
A View using a #Binding will update when the underlying #State change, but the #State must be defined within the view hierarchy. (Else you could bind to a publisher)
In my case, the view hierarchy doesn't have the view which is having the #State. The view having the binding is presented modally to the user.
To summarize the issue again
I want to create a view, similar to Toggle which initializes from a Binding. This view will show the contents from the wrapped value and as it performs the updates, the original storage of the value will get updated automatically.
As I have learnt, updating the #Binding in a view, doesn't invalidate it. Then how to implement such a view.
Also I can't depend on the parent view to eventually update this view, because the view is shown on a modally presented screen.
I don't want to use workarounds like using a #State to explicitly trigger a refresh. So what is the correct way to implement such a view.
Code example
The view TextModifier takes a Binding. The view does some modifications to the view. For now it just appends "_Updated" to the value passed.
I initialize the view as TextModifier(text: <some_binding_var>)
struct TextModifier: View {
#Binding var text: String
var body: some View {
Text(text)
.onTapGesture {
text += "_Updated"
}
}
}
This view shows the text and on tapping it updates it in the original source, but as expected the view doesn't update itself on tapping.
So, how to implement this view so that it also updates itself when it updates the binding value.
The accepted answer to the linked question also says
Else you could bind to a publisher
I don't know how to do this. Does anybody know how to implement this and also provide a code example. Thanks.
Updated with full code and gif
struct ContentView: View {
#ObservedObject var viewModel = TestViewModel()
var body: some View {
List {
ForEach(viewModel.itemsList, id: \.self) { item in
ItemView(text: $viewModel.itemsList[getItemIndex(item)])
}
}
}
private func getItemIndex(_ item: String) -> Int {
viewModel.itemsList.firstIndex { $0 == item }!
}
}
class TestViewModel: ObservableObject {
#Published var itemsList = ["Item 1", "Item 2", "Item 3"]
}
struct ItemView: View {
#Binding var text: String
#State private var showEditorView = false
var body: some View {
Text(text)
.onTapGesture {
showEditorView = true
}
.sheet(isPresented: $showEditorView) {
TextModifier(text: $text, showView: $showEditorView)
}
}
}
struct TextModifier: View {
#Binding var text: String
#Binding var showView: Bool
var body: some View {
VStack(spacing: 20) {
Text("Tap on the text to update it")
.foregroundColor(.blue)
Text(text)
.onTapGesture {
text += "_Updated"
}
Button {
showView = false
} label: {
Text("Dismiss")
.foregroundColor(.blue)
}
}
}
}

SwiftUI pass by reference a class object to another view

I'm trying to learn SwiftUI and I'm going to develop a simple app with tab views and sharing core motion data between those views.
The main idea is to create a motion manager object (like here) and use the sensor values in all views.
ContentView.swift:
import SwiftUI
struct ContentView: View {
#State private var selection = 1
#State private var viewNames : [String] = ["View1", "View2"]
var body: some View {
TabView(selection: $selection){
View1(viewName: $viewNames[0]).tag(0)
View2(viewName: $viewNames[1]).tag(1)
}
}
}
View1.swift:
import SwiftUI
struct View1 : View {
#Binding var viewName : String
var body: some View {
Text("First View")
.font(.title)
.foregroundColor(Color.gray)
.tabItem {
VStack {
Image(systemName: "star")
Text(viewName)
}
}
}
}
View2.swift:
struct View2 : View {
#Binding var viewName : String
var body: some View {
VStack {
Text("Second View")
.font(.title)
.foregroundColor(Color.green)
.padding(.top)
View21(motionManager: MotionManager())
}.tabItem {
VStack {
Image(systemName:"heart")
Text(viewName)
}
}
}
}
View21.swift
struct View21 : View {
#ObservedObject var motionManager : MotionManager
#State private var showDetails = false
var body: some View{
Text(String(format: "%.2f", motionManager.x))
}
With these code I can use the sensor data in View21, but I can't access the data in the views in the hierarchy above.
Furthermore I created #ObservedObject in the ContentView (like here) and passed it through all views. My app work in the simulator, but it doesn't on the real device.
I can see sensor data changing, but I can't switch the tab views.
I've tried to use #EnvironementObject instead of #ObservedObject, but the behavior is the same.
I'll be very thankful for any help and tipps to my issue.
Best wishes
Okay, so Paulw11 is right that the you probably want to inject your ObservableObject into your environment, then in each view that wants to access that instance, you just add a property with the #EnvironmentObject property wrapper. Below I've thrown together the simplest example I could think of, so that you can get the idea of how it works.
import SwiftUI
import Combine
class ManagerPlaceholder: ObservableObject {
#Published var propertyOne: Double = 1.0
#Published var propertyTwo: Double = 2.0
func action() {
propertyOne = Double.random(in: 0.0..<100.00)
propertyTwo = Double.random(in: 0.0..<100.00)
}
}
struct ContentView: View {
#EnvironmentObject var manager: ManagerPlaceholder
var body: some View {
TabView {
Subview()
.tabItem { Label("First", systemImage: "hexagon.fill") }
.tag(1)
Subview()
.tabItem { Label("Second", systemImage: "circle.fill") }
.tag(2)
}
}
}
struct Subview: View {
#EnvironmentObject var manager: ManagerPlaceholder
var body: some View {
VStack {
Text("Prop One: \(manager.propertyOne)").padding()
Text("Prop Two: \(manager.propertyTwo)").padding()
Button("Change", action: manager.action).padding()
}
}
}
So above is
A simple ObservableObject - All it does is set two Doubles with a random value (notice the properties you want to observe are marked as #Published)
A tab view
A simple sub-view
Notice that both of the views have a #EnvironmentObject var manager: ManagerPlaceholder. You don't set that property directly. That line says that you want to reference a ManagerPlaceholder instance that's in the Environment. The environment is a sort of "pool" of storage that SwiftUI manages for you. You can add an instance of an object to it, then reference it in sub-views that need it.
So to make sure that's in the environment you add it when instantiating a view (could be the tab view, or any super-view). So for example in your _NAME_App.swift (for an iOS 14 target) or SceneDelegate.swift (for an iOS 13 target), you'd instantiate your view like this:
ContentView().environmentObject(ManagerPlaceholder())
If you run the code above you'll see when you hit the Change button it randomly sets the two properties, and both subviews will see the exact same values when you switch back and forth, because they're both referencing the same instance.
Feel free to comment if anything is unclear.

Reload view when #Published is changed

I want to be able to scan barcodes, then show the barcode at the bottom of the screen in a sheet, or a separate view.
When I update the 'barcode' variable which is #Published and accessed in other methods with #ObserveableObject, it doesn't update the view with the data from the barcode.
Content View
class ScannedCode: ObservableObject {
#Published var barcode = ""
}
struct ContentView: View {
#ObservedObject var barcode = ScannedCode()
var body: some View {
ZStack {
ScannerView()
FoundItemSheet()
}
}
}
Scanner View - scannedCode is linked to the #Published variable, so when this changes, I want it to reload FoundItemSheet() as a barcode has been found by the scanner
class Coordinator: BarcodeScannerCodeDelegate, BarcodeScannerErrorDelegate {
#ObservedObject var scannedCode = ScannedCode()
private var scannerView: ScannerView
init(_ scannerView: ScannerView) {
self.scannerView = scannerView
}
func scanner(_ controller: BarcodeScannerViewController, didCaptureCode code: String, type: String) {
self.scannedCode.barcode = code
controller.resetWithError(message: "Error message")
}
func scanner(_ controller: BarcodeScannerViewController, didReceiveError error: Error) {
print(error)
}
}
FoundItemSheet() Loads BottomSheetView which is shown at the bottom of the screen in a box over the camera. I want this to update with the barcode data when it's found.
struct FoundItemSheet: View {
#State private var bottomSheetShown = false
var body: some View {
GeometryReader { geometry in
BottomSheetView(
isOpen: self.$bottomSheetShown,
maxHeight: geometry.size.height * 0.7
) {
Color.blue
}
}.edgesIgnoringSafeArea(.all)
}
}
BottomSheetView - I have declared the #Published barcode variable in here, so when it changes, I want contentsInSlide to reload with the new barcode.
struct BottomSheetView<Content: View>: View {
#Binding var isOpen: Bool
#State var showingDetail = false
#ObservedObject var scannedCode = ScannedCode()
....
private var contentInSlide: some View {
VStack {
Text("Value is: \(scannedCode.barcode)") //Doesn't show any value for the barcode
.foregroundColor(ColorManager.beautytruthGreen)
.font(.system(size: 22, weight: .medium))
Button(action: {
self.showingDetail.toggle()
}
}
The problem is that all of your views create their own ScannedCode instances. You need to create it only on the type that creates all other views and inject the same instance into each one of them.
From your code snippets it isn't really clear which view is the parent of which, so it's hard to give you a definitive answer with a code example, but in general, you should never create an #ObservableObject in a view itself, since that object will be recreated as soon as the view is reloaded. Instead, you should be creating your #ObservableObject on the view model or the parent view and injecting it into the child view that needs reloading.
Using below code, whenever scannedCode on the ParentView is updated, it reloads its ChildView with the updated ScannedCode.
struct ParentView: View {
#Published var scannedCode = ScannedCode()
var body: some View {
ChildView(scannedCode: scannedCode)
}
}
struct ChildView: View {
#ObservedObject var scannedCode: ScannedCode
var body: some View {
Text(scannedCode.barCode)
}
}

SwiftUI NavigationLink pops automatically which is unexpected

I am having some issues with a NavigationLink on an iPad with split view (landscape). Here is an example:
Here is the code to reproduce the issue:
import SwiftUI
final class MyEnvironmentObject: ObservableObject {
#Published var isOn: Bool = false
}
struct ContentView: View {
#EnvironmentObject var object: MyEnvironmentObject
var body: some View {
NavigationView {
NavigationLink("Go to FirstDestinationView", destination: FirstDestinationView(isOn: $object.isOn))
}
}
}
struct FirstDestinationView: View {
#Binding var isOn: Bool
var body: some View {
NavigationLink("Go to SecondDestinationView", destination: SecondDestinationView(isOn: $isOn))
}
}
struct SecondDestinationView: View {
#Binding var isOn: Bool
var body: some View {
Toggle(isOn: $isOn) {
Text("Toggle")
}
}
}
// Somewhere in SceneDelegate
ContentView().environmentObject(MyEnvironmentObject())
Does anyone know a way to fix this? An easy fix is to disable split view, but that is not possible for me.
When something within EnvironmentObject changes, it will render the whole view again including NavigationLink. That's the root cause of automatic pop back.
My research on it:
OK on iOS 15 (seems Apple fixed)
Still broken on iOS 14
The reason why "This bug went away when I dropped the #EnvironmentObject and went with an #ObservedObject instead." #Jon Vogel mentioned is ObservedObject is a local state, which will not be affected by other views while EnvironmentObject is global state and can change from any other remote views.
Ok, here is my investigation results (tested with Xcode 11.2) and below is the code that works.
In iPad NavigationView got into Master/Details style, so ContentView having initial link is active and process bindings update from environmentObject, so refresh, which result in activating link of details view via same binding, thus corrupting navigation stack. (Note: this is absent in iPhone due to stack style, which deactivates root view).
So, probably this is workaround, but works - the idea is not to pass binding from view to view, but use environmentObject directly in final view. Probably this will not always be a case, but anyway in such scenarios it is needed to avoid root view refresh, so it should not have same binding in body. Something like that.
final class MyEnvironmentObject: ObservableObject {
#Published var selection: Int? = nil
#Published var isOn: Bool = false
}
struct ContentView: View {
#EnvironmentObject var object: MyEnvironmentObject
var body: some View {
NavigationView {
List {
NavigationLink("Go to FirstDestinationView", destination: FirstDestinationView())
}
}
}
}
struct FirstDestinationView: View {
var body: some View {
List {
NavigationLink("Go to SecondDestinationView", destination: SecondDestinationView())
}
}
}
struct SecondDestinationView: View {
#EnvironmentObject var object: MyEnvironmentObject
var body: some View {
VStack {
Toggle(isOn: $object.isOn) {
Text("Toggle")
}
}
}
}
You need can use isDetailLink(_:) to fix that, e.g.
struct FirstDestinationView: View {
#Binding var isOn: Bool
var body: some View {
NavigationLink("Go to SecondDestinationView", destination: SecondDestinationView(isOn: $isOn))
.isDetailLink(false)
}
}

Resources