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:
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
Adding Items to The Dock Menu from My View Controller in My Cocoa App
Swiftui JSON Won't Print Title in Scrollview Hstack (But Will in List)
How to Constrain 'self' to a Generic Type
Urlsession Datatask Method Returns 0 Bytes of Data
Skease Action, How to Use Float Changing Action Setter Block
Index or Range of Second Ocurence of Bytes in File
#If Canimport(Coreimage) Not Working in Swift Package Manager
Calling C++ from Swift - What Is The Equivalent of Std::Vector<T>
Images Being Flipped When Adding to Nsattributedstring
How to Modify Codable Class Properties
Error Combining Nscalendarunit with or (Pipe) in Swift 2.0
List Inside Scrollview Is Not Displayed on Watchos
Convert Ble Current Time to Date
How to Get Address in English Language Only Using Gmsgeocoder
How to Resize an Image That Is Being Printed