How to keep reference to a child view with Binding inside the parent view? - ios

Inside one parent SwiftUI view, I need to keep reference to a child view which is a UIViewRepresentable. For that, I create an instance of the child view during the parent init, and allocate it to a property. This works quite well.
struct parentView: View {
var childView: ChildView
init() {
self.childView = ChildView()
}
var body: some View {
childView
}
}
My issue is when the child view needs some bindings as parameters. As the bindings are not initialized yet, Xcode raise an error. How could I solve that ?
struct parentView: View {
#State private var property1: Bool = false
var childView: ChildView
init() {
self.childView = ChildView(property1: $property1)
}
var body: some View {
childView
}
}
struct ChildView: UIViewRepresentable {
#Binding var property1: Bool
// code
}
Or may be I’m wrong, and there are other solutions to have direct access to subview’s func than keeping reference of them.

Finally found a nice solution in a YouTube video from Kavsoft:
struct parentView: View {
#State private var property1: Bool = false
var uiView = UIViewClass()
var body: some View {
VStack {
childView(uiView: $uiView, property1: $property1)
Button("Direct action to uiView") {
uiView.action()
let returnString = uiView.actionReturningString()
}
}
}
}
struct ChildView: UIViewRepresentable {
#Binding var uiView: UIViewClass
#Binding var property1: Bool
// UIViewRepresentable protocol
}

Related

SwiftUI EnvironmentObject does not redraw the current view but its parent view

I'm trying to understand how #EnvironmentObject affects redrawing when a property in ObservableObject changes.
As per Apple's doc on EnvironmentObject,
An environment object invalidates the current view whenever the observable object changes.
If the above statement is true, wouldn't the below code invalidate and recreate ChildView when you press the button in ContentView. And that should print the following output.
initializing parent
initializing child
// after pressing the button
initializing child
Contrary to my above assumption, it actually prints
initializing parent
initializing child
// after pressing the button
initializing parent
Can anyone explain why this is the case? Why is the ParentView being recreated even though ParentView is not depending on Library?
class Library: ObservableObject {
#Published var item: Int = 0
}
struct ContentView: View {
#StateObject var library: Library = Library()
var body: some View {
VStack {
ParentView()
.environmentObject(library)
Button {
library.item += 1
} label: {
Text("increment")
}
}
}
}
struct ParentView: View {
init() {
print("initializing parent")
}
var body: some View {
VStack {
Text("Parent view")
ChildView()
}
}
}
struct ChildView: View {
#EnvironmentObject var library: Library
init() {
print("initializing child")
}
var body: some View {
Text("Child view")
}
}
SwiftUI View´s can be a little tricky.
An environment object invalidates the current view whenever the observable object changes.
does not mean the object itself is recreated. It just means the body of the view gets called and the view rebuilds itself.
Remember the struct is not the View itself, it´s just a "description".
I´ve added some print statements to make this more clear:
struct ContentView: View {
#StateObject var library: Library = Library()
init(){
print("initializing content")
}
var body: some View {
VStack {
let _ = print("content body")
ParentView()
.environmentObject(library)
Button {
library.item += 1
} label: {
Text("increment")
}
}
}
}
struct ParentView: View {
init() {
print("initializing parent")
}
var body: some View {
VStack {
let _ = print("parent body")
Text("Parent view")
ChildView()
}
}
}
struct ChildView: View {
#EnvironmentObject var library: Library
init() {
print("initializing child")
}
var body: some View {
let _ = print("child body")
Text("child")
}
}
this initially prints:
initializing content
content body
initializing parent
parent body
initializing child
child body
and after pressing the button:
content body
initializing parent
child body
As you see the body of those View´s depending on Library get their respective body reevaluated.
The ParentView initializer runs because in your ContentView you call ParentView() in the body so a new struct "describing" your View is created. The ParentView´s view itself stays the same so its body var is not called.
This WWDC 2021 video about SwiftUI Views will help you better understand this.

SwiftUI retain cycle in view hierarchy

I'm having the following view hierarchy which has a retain cycle, that's the simplest I could make to reproduce the issue. All viewmodels and properties has to stay as they are needed in the original solution:
import SwiftUI
struct MainView: View {
#StateObject var viewModel = MainViewModel()
var body: some View {
NavigationView { [weak viewModel] in
VStack {
Button("StartCooking") {
viewModel?.show()
}
if viewModel?.isShowingContainerView == true {
ContainerView()
}
Button("StopCooking") {
viewModel?.hide()
}
}
}
.navigationViewStyle(.stack)
}
}
final class MainViewModel: ObservableObject {
#Published var isShowingContainerView = false
func show() {
isShowingContainerView = true
}
func hide() {
isShowingContainerView = false
}
}
struct ContainerView: View {
#Namespace var namespace
var body: some View {
VStack {
SubView(
namespace: namespace
)
}
}
}
struct SubView: View {
#StateObject var viewModel = SubViewModel()
var namespace: Namespace.ID
var body: some View {
Text("5 min")
.matchedGeometryEffect(id: UUID().uuidString, in: namespace)
.onTapGesture {
foo()
}
}
private func foo() {}
}
final class SubViewModel: ObservableObject {}
If I run the app, tap on StartCooking, than on StopCooking and check the memory graph, I still see an instance of SubViewModel, which means that there is a leak in this code.
If I remove:
NavigationView OR
The VStack from ContainerView OR
matchedGeometryEffect OR
tapGesture
The retain cycle is resolved. Unfortunately I need all these. Can you see what the issue might be and how could it be solved?
Looks like a SwiftUI bug. A possible workaround (if sub-view is one or limited set) is to use view model factory to provided instances.
Here is an example for one view:
struct SubView: View {
#StateObject var viewModel = SubViewModel.shared // single instance !!
// .. other code
}
final class SubViewModel: ObservableObject {
static var shared = SubViewModel() // << this !!
}
I could kind of workaround it by making every property optional in the SubViewModel and running a function when the SubViews disappear, which makes them nil. The SubViewModel still stays in the memory, but will not take up that much space.
Interestingly I even tried to make the viewmodel optional, and make it nil when the view disappears, but it still stayed in the memory.

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 - EnvironmentObject Published var to NavigationLink's Selection?

I have a class I use as EnvironmentObject with a screen variable I want to use to control what screen the user is in. I use network calls to change this value, expiration seconds to move to another view, so I need my NavigationView to move to the adequate screen when this value changes.
For the class I have something like this:
class MyClass: NSObject, ObservableObject {
#Published var screen: String? = "main"
}
And for the main view I have something like this:
struct ContentView: View {
#EnvironmentObject var myClass: MyClass
var body: some View {
ZStack {
NavigationView() {
NavigationLink(destination: MainView(), tag: "main", selection: self.$myClass.screen)
{
EmptyView()
}
}
}
}
}
I don't seem to be able to work this way.
As a workaround I have done:
struct ContentView: View {
#EnvironmentObject var myClass: MyClass
var body: some View {
VStack {
if (self.myClass.screen == "main") {
MainView()
} else if (self.myClass.screen == "detail") {
DetailView()
}
}
}
But as you see, is not pretty. And I don't get any animations when changing screens.
Does anyone have an idea how to do this or how should I approach this situation?
Your second approach is correct. You just need animations. Unfortunately they only work when changing #State variables.
For this you need to create a new #State variable and assign it in .onReceive:
struct ContentView: View {
#EnvironmentObject var myClass: MyClass
#State var screen: String?
var body: some View {
VStack {
if screen == "main" {
MainView()
} else if screen == "detail" {
DetailView()
}
}
.onReceive(myClass.$screen) { screen in
withAnimation {
self.screen = screen
}
}
}
}

SwiftUI #Binding update doesn't refresh view

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.

Resources