Swiftui Go Back Programmatically from Representable to View

SwiftUI go back programmatically from representable to View

The short answer is you can't do that right now. There is neither a binding nor an environment value to set that can trigger this. My guess is there will be some kind of environment value akin to presentationMode that you can tap into but it isn't currently advertised.

You could try the current presentationMode but my real suggestion is to present your QR scanner as a sheet rather than a push. This may actually make more sense from a navigational standpoint anyway. To do it this way, in your presenter set up a @State var to handle when it's presented.

@State var presentQRScanner = false

var body: some View {
Button("Scan") {
self.presentQRScanner = true
}
.sheet(isPresented: $presentQRScanner) { QRCodeScan() }
}

Then, when you want to programmatically dismiss, your UIViewControllerRepresentable:

@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

func scannedCode() {
presentationMode.wrappedValue.dismiss()
}

Alternatively, you can drive this from the presenter too by creating a closure on the QRCodeScan that gets invoked with the code and you have your presenter dismiss.

var onCodeScanned: (Code) -> Void = { _ in }

func scannedCode() {
onCodeScanned(code)
}

and in the presenter:

var body: some View {
Button("Scan") {
self.presentQRScanner = true
}
.sheet(isPresented: $presentQRScanner) {
QRCodeScan(onCodeScanned: {
self.process($0)
self.presentQRScanner = false
})
}
}

EDIT: was not aware of the isActive binding, that should actually work for you if you still want to push your view on the nav stack instead of present it.

How to pop a UIViewControllerRepresentable from a SwiftUI navigation stack programmatically

If you create Coordinator with owner view (ie. your representable), then try to call directly when needed

ownerView.presentationMode.wrappedValue.dismiss()

Dismiss a SwiftUI View that is contained in a UIHostingController

UPDATE: From the release notes of iOS 15 beta 1:

isPresented, PresentationMode, and the new DismissAction action dismiss a hosting controller presented from UIKit. (52556186)


I ended up finding a much simpler solution than what was offered:


final class SettingsViewController: UIHostingController<SettingsView> {
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: SettingsView())
rootView.dismiss = dismiss
}

func dismiss() {
dismiss(animated: true, completion: nil)
}
}

struct SettingsView: View {
var dismiss: (() -> Void)?

var body: some View {
NavigationView {
Form {
Section {
Button("Dimiss", action: dismiss!)
}
}
.navigationBarTitle("Settings")
}
}
}

iOS: Make function calls between UIViewRepresentable and View both ways, SwiftUI

You can use computed property and closure for a callback.

Here is the example code.

struct LoginView: View {

// Other code

// Webview var
private var webView: LoginWebview {
LoginWebview(testdo: self.showJSAlert) {
// Here you will get the call back
print("Callback")
}
}

var body: some View {

webView // <-- Here

// Other Code

And for button action

Button(action: {

//Calls login webview and triggers update that calls the js
self.showJSAlert.toggle()
// Access your function
self.webView.printstuff()

}) {
Text("Login")
.padding()
.font(.system(size: 20))

}

And in LoginWebview

struct LoginWebview: UIViewRepresentable {
var testdo = false

var callBack: (() -> Void)? = nil
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
// Other code

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.control.callBack?() // < Example for calling callback
}

// Other code
}

Pass data from ViewController to Representable SwiftUI

The idiomatic way is to circulate a Binding instance through the UI hierarchy - this includes both the SwiftUI and the UIKit code. The Binding will transparently update the data on all views that are connected to it, regardless of who did the change.

A diagram of the data flow could look similar to this:
Sample Image

OK, getting to the implementation details, first things first you need a @State to store data that will come from the UIKit side, in order to make it available for updates to the view controller,:

struct MaskDetectionView: View {
@State var clasificationIdentifier: String = ""

Next, you need to pass this to both the view controller and the SwiftUI view:

var body: some View {
...
SwiftUIViewController(identifier: $clasificationIdentifier)
...
// this is the "VALUE HERE" from your question
Text("Clasification identifier: \(clasificationIdentifier)")

Now, that you are properly injecting the binding, you'll need to update the UIKit side of the code to allow the binding to be received.

Update your view representable to look something like this:

struct SwiftUIViewController: UIViewControllerRepresentable {

// this is the binding that is received from the SwiftUI side
let identifier: Binding<String>

// this will be the delegate of the view controller, it's role is to allow
// the data transfer from UIKit to SwiftUI
class Coordinator: ViewControllerDelegate {
let identifierBinding: Binding<String>

init(identifierBinding: Binding<String>) {
self.identifierBinding = identifierBinding
}

func clasificationOccured(_ viewController: ViewController, identifier: String) {
// whenever the view controller notifies it's delegate about receiving a new idenfifier
// the line below will propagate the change up to SwiftUI
identifierBinding.wrappedValue = identifier
}
}

func makeUIViewController(context: Context) -> ViewController{
let vc = ViewController()
vc.delegate = context.coordinator
return vc
}

func updateUIViewController(_ uiViewController: ViewController, context: Context) {
// update the controller data, if needed
}

// this is very important, this coordinator will be used in `makeUIViewController`
func makeCoordinator() -> Coordinator {
Coordinator(identifierBinding: identifier)
}
}

The last piece of the puzzle is to write the code for the view controller delegate, and the code that makes use of that delegate:

protocol ViewControllerDelegate: AnyObject {
func clasificationOccured(_ viewController: ViewController, identifier: String)
}

class ViewController: UIViewController {

weak var delegate: ViewControllerDelegate?

...

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
...

print(self.result)

// let's tell the delegate we found a new clasification
// the delegate, aka the Coordinator will then update the Binding
// the Binding will update the State, and this change will be
// propagate to the Text() element from the SwiftUI view
delegate?.clasificationOccured(self, identifier: self.result)
}

SwiftUI go to another view from UIViewController

You can create instance of UIHostingController and push this controller to navigation controller.

Example:

struct ContentView1: View {
var body: some View {
NavigationView(content: {
ViewControllerWrapper(controller: MyViewController.init())
})
.navigationTitle("View")
.navigationBarTitle("Text")
}
}

struct AnotherView: View {
var body: some View {
Text("AnotherView In Swiftui")
.font(.title)
}
}

struct ContentView1_Previews: PreviewProvider {
static var previews: some View {
ContentView1()
}
}

class MyViewController: UIViewController{
lazy var goToAnotherViewButton: UIButton = {
let button = UIButton(type: .custom)
button.layer.cornerRadius = 5
button.backgroundColor = .systemYellow
button.titleLabel?.font = UIFont.systemFont(ofSize: 22)
button.setTitle("Go", for: .normal)
button.addTarget(self, action: #selector(goToAnotherView), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let stackView = UIStackView(arrangedSubviews: [goToAnotherViewButton])
stackView.axis = .vertical
stackView.spacing = 20
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.leftAnchor.constraint(equalToSystemSpacingAfter: view.leftAnchor, multiplier: 2),
view.rightAnchor.constraint(equalToSystemSpacingAfter: stackView.rightAnchor, multiplier: 2),
stackView.topAnchor.constraint(equalToSystemSpacingBelow: view.topAnchor, multiplier: 2),
])
}

@objc
func goToAnotherView() {
//from this function I want to go to Another View
let vc = UIHostingController(rootView: AnotherView())
self.navigationController?.pushViewController(vc, animated: true)
}
}

struct ViewControllerWrapper: UIViewControllerRepresentable{
func makeUIViewController(context: UIViewControllerRepresentableContext<ViewControllerWrapper>) -> UIViewController {
guard let controller=controller else {
return UIViewController()
}
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ViewControllerWrapper>) {
}
let controller:UIViewController?
typealias UIViewControllerType = UIViewController
}

Passing data from UIViewRepresentable function to SwiftUI View

Here is one approach. Have a single source of truth that both UIKit and SwiftUI can access.

@available(iOS 15.0, *)
struct LocationView: View {
//It is better to have one source of truth
@StateObject var vm: MapViewModel = MapViewModel()

var body: some View {
ZStack {
MapView(vm: vm)
.edgesIgnoringSafeArea([.leading, .trailing, .bottom])
.onAppear(perform: {
//locationFetcher.start() //No Code provided
})
}
.overlay(
HStack{
Spacer()
Text(vm.addressLabel)
Spacer()
//Using offset is subjective since screen sizes change just center it
}


)
//Sample alert that adapts to what is
.alert(isPresented: $vm.errorAlert.isPresented, error: vm.errorAlert.error, actions: {

if vm.errorAlert.defaultAction != nil{
Button("ok", role: .none, action: vm.errorAlert.defaultAction!)
}

if vm.errorAlert.cancelAction != nil{
Button("cancel", role: .cancel, action: vm.errorAlert.cancelAction!)
}

if vm.errorAlert.defaultAction == nil && vm.errorAlert.cancelAction == nil {
Button("ok", role: .none, action: {})
}
})
}
}
//UIKit and SwiftUI will have access to this ViewModel so all the data can have one souce of truth
class MapViewModel: ObservableObject{
//All the variables live here
@Published var addressLabel: String = "222"
@Published var centerCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D()

@Published var currentLocation: CLLocationCoordinate2D? = nil
@Published var withAnnotation: MKPointAnnotation? = nil
@Published var annotation: MKPointAnnotation?
//This tuple variable allows you to have a dynamic alert in the view
@Published var errorAlert: (isPresented: Bool, error: MapErrors, defaultAction: (() -> Void)?, cancelAction: (() -> Void)?) = (false, MapErrors.unknown, nil, nil)
//The new alert requires a LocalizedError
enum MapErrors: LocalizedStringKey, LocalizedError{
case unknown
case failedToRetrievePlacemark
case failedToReverseGeocode
case randomForTestPurposes
//Add localizable.strings to you project and add these keys so you get localized messages
var errorDescription: String?{
switch self{

case .unknown:
return "unknown".localizedCapitalized
case .failedToRetrievePlacemark:
return "failedToRetrievePlacemark".localizedCapitalized

case .failedToReverseGeocode:
return "failedToReverseGeocode".localizedCapitalized

case .randomForTestPurposes:
return "randomForTestPurposes".localizedCapitalized

}
}
}
//Presenting with this will ensure that errors keep from gettting lost by creating a loop until they can be presented
func presentError(isPresented: Bool, error: MapErrors, defaultAction: (() -> Void)?, cancelAction: (() -> Void)?, count: Int = 1){
//If there is an alert already showing
if errorAlert.isPresented{
//See if the current error has been on screen for 10 seconds
if count >= 10{
//If it has dismiss it so the new error can be posted
errorAlert.isPresented = false
}
//Call the method again in 1 second
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
let newCount = count + 1
self.presentError(isPresented: isPresented, error: error, defaultAction: defaultAction, cancelAction: cancelAction, count: newCount)
}
}else{
errorAlert = (isPresented, error, defaultAction, cancelAction)
}
}

}
struct MapView: UIViewRepresentable {
@ObservedObject var vm: MapViewModel

class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView

init(_ parent: MapView) {
self.parent = parent
}

func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
if !mapView.showsUserLocation {
parent.vm.centerCoordinate = mapView.centerCoordinate
}
}


func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool){
getAddress(center: mapView.centerCoordinate)
//Just to demostrate the error
//You can remove this whenever
#if DEBUG
if Bool.random(){
self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.randomForTestPurposes, defaultAction: nil, cancelAction: nil)

}
#endif

}
//Gets the addess from CLGeocoder if available
func getAddress(center: CLLocationCoordinate2D){
let geoCoder = CLGeocoder()

geoCoder.reverseGeocodeLocation(CLLocation(latitude: center.latitude, longitude: center.longitude)) { [weak self] (placemarks, error) in
guard let self = self else { return }

if let _ = error {
//TODO: Show alert informing the user
print("error")
self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.failedToReverseGeocode, defaultAction: nil, cancelAction: nil)
return
}

guard let placemark = placemarks?.first else {
//TODO: Show alert informing the user
self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.failedToRetrievePlacemark, defaultAction: nil, cancelAction: nil)
return
}

let streetNumber = placemark.subThoroughfare ?? ""
let streetName = placemark.thoroughfare ?? ""

DispatchQueue.main.async {
self.parent.vm.addressLabel = String("\(streetName) | \(streetNumber)")
print(self.parent.vm.addressLabel)

}
}
}
}

func makeCoordinator() -> Coordinator {
Coordinator(self)
}

func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = false
return mapView
}

func updateUIView(_ uiView: MKMapView, context: Context) {
if let currentLocation = vm.currentLocation {
if let annotation = vm.withAnnotation {
uiView.removeAnnotation(annotation)
}
uiView.showsUserLocation = true
let region = MKCoordinateRegion(center: currentLocation, latitudinalMeters: 1000, longitudinalMeters: 1000)
uiView.setRegion(region, animated: true)
} else if let annotation = vm.withAnnotation {
uiView.removeAnnotations(uiView.annotations)
uiView.addAnnotation(annotation)
}
}
}

Display system back button on SwiftUI NavigationView with no previous NavigationLink

You may try the following:

private var backButton: some View {
Button(action: {}) {
Image(systemName: "chevron.left")
.scaleEffect(0.83)
.font(Font.title.weight(.medium))
}
}

Optionally you can apply .offset as well, but this may not adapt properly for larger accessibility font sizes:

.offset(x: -7, y: 0)


Related Topics



Leave a reply



Submit