in SwiftUI NavigationView Problem with UIViewRepresentable - ios

this is my ContentView.swift file:
struct ContentView: View {
var body: some View {
NavigationView {
MapView()
}
}
}
and this is my MapView.swift file:
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView(frame: .zero)
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
print("test")
}
}
with these lines of codes, when I run app it prints twice test in console.
but when I change ContentView and hide NavigationView from it like this:
struct ContentView: View {
var body: some View {
// NavigationView {
MapView()
// }
}
}
now it prints one test in console.
actually it mean when we use NavigationView with UIViewRepresentable, it calls twice updateUIView func and it seem it is wrong.
how can we handle it?

Related

How to change UISearchBar background shape on iOS 16

It seems like there's a bug in iOS 16 where a UISearchBar in a UISplitViewController's primary position displays with a square background. This doesn't happen if the search bar is in other positions, or if the split view is collapsed (eg. on iPhone).
I've reported it (FB10847490) but any ideas how I could work around this in the meantime? It seems like .background/.backgroundColor and searchTextField.background/backgroundColor both affect other subviews and not the view that is causing the square appearance.
Sample app:
struct ContentView: View {
var body: some View {
HostingController()
}
}
struct HostingController: UIViewControllerRepresentable {
#State private var text = ""
func makeUIViewController(context: Context) -> some UIViewController {
let controller = UISplitViewController(style: .doubleColumn)
let contentView = UIHostingController(rootView: VStack {
SearchBar(text: $text)
})
let primaryController = UINavigationController(rootViewController: contentView)
controller.setViewController(primaryController, for: .primary)
controller.setViewController(UIHostingController(rootView: SearchBar(text: $text)), for: .secondary)
controller.preferredSplitBehavior = .overlay
return controller
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
struct SearchBar: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UISearchBar {
let searchBar = UISearchBar()
searchBar.placeholder = "Search..."
searchBar.returnKeyType = .done
searchBar.enablesReturnKeyAutomatically = false
searchBar.searchBarStyle = .minimal
searchBar.text = text
searchBar.searchBarStyle = .minimal
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: Context) {
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice("iPad")
}
}

SwiftUI, Dismiss modal/page from child view

I am new to Swift/SwiftUI and want to know how to dismiss modal/page from nested child view.
Firstly, I am calling from Flutter, UIHostingController, then SwiftUI page. (currently showing as modal...)
After Navigating to SwiftUI, I am not able to use #environment data from child view.
Is there any ways for this to work?
thanks in advance.
AppDelegate.swift
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = self.window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel.init(name: "com.example.show", binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler({
(call, result) -> Void in
if call.method == "sample" {
let vc = UIHostingController(rootView: ContentView())
vc.modalPresentationStyle = .fullScreen
controller.present(vc, animated: true,completion: nil)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ContentView.swift
struct ContentView: View{
#Environment(\.presentationMode) var presentation: Binding<PresentationMode>
private var childView: ChildView()
var body: some View{
NavigationView{
ZStack{
childView
Button(action: {
// This works ************************
self.presentation.wrappedValue.dismiss()
}, label: {
Text("close")
})
}
}
}
}
ChildView.swift
struct ChildView: View{
#Environment(\.presentationMode) var presentation: Binding<PresentationMode>
var body: some View{
Button(action: {
// This won't do anything *********************************
self.presentation.wrappedValue.dismiss()
// nor this↓ **********************************************
if #available(iOS 15.0, *) {
#Environment(\.dismiss) var dismiss;
dismiss()
dismiss.callAsFunction()
}
}, label: {
Text("close")
})
}
}
Since you had another NavigationView is your ContentView, the #Environment(\.presentation) inside ChildView is of a child and not the parent. Basically those two are from completely different Navigation stacks.
In order to still keep NavigationView inside your parent ContentView, you need to pass the presentation value from constructor of ChildView instead of environment:
ContentView.swift
struct ContentView: View{
#Environment(\.presentationMode) var presentation: Binding<PresentationMode>
var body: some View{
NavigationView{
ZStack{
ChildView(parentPresentation: presentation)
Button(action: {
self.presentation.wrappedValue.dismiss()
}, label: {
Text("close")
})
}
}
}
}
In child view, use normal property instead of #Environment
ChildView.swift
struct ChildView: View{
let parentPresentation: Binding<PresentationMode>
var body: some View{
Button(action: {
self.parentPresentation.wrappedValue.dismiss()
if #available(iOS 15.0, *) {
#Environment(\.dismiss) var dismiss;
dismiss()
dismiss.callAsFunction()
}
}, label: {
Text("Close")
})
}
}
For iOS 15.0 and above, we can use the new environment value dismiss, and for that to work with child view, we should also pass it from the parent view to the child view:
ContentView.swift
struct ContentView: View {
#Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
ZStack {
ChildView(parentDismiss: dismiss)
Button {
dismiss()
} label: {
Text("close")
}
}
}
}
}
ChildView.swift
struct ChildView: View {
let parentDismiss: DismissAction
var body: some View {
Button {
parentDismiss()
} label: {
Text("Close")
}
}
}
I figured it out that NavigationView in ContentView.swift caused this issue.
Removing NavigationView, I could dismiss modal page from child view...
But this is not what I intended for :(...
var body: some View{
// NavigationView{ <--------
ZStack{
childView
Button(action: {
self.presentation.wrappedValue.dismiss()
}, label: {
Text("close")
})
}
// }
}

Swiftui: How to add Accessibility Identifier on navigationTitle

How can we add the Accessibility Identifier to NaviagationTitle Text. I know for buttons/text/Image/stack views we can use .accessibility(identifier: “some_identifier”).
struct SomeView: View {
var body: some View {
VStack {
Text("Title Text")
.accessibility(identifier: "title")
}
.navigationTitle("title") //How to add accessibilityIdentifier to Navigation bar title?
//.navigationTitle(Text("title").accessibility(identifier: "title"))
}
}
unable to add the modifier to .navigationBarTitle(Text(“title”), displayMode: .inline). Accessibility Identifiers are required for XCUI automation testing.
I don't think this is possible in SwiftUI using .accessibility(identifier:) - it might be worth submitting feedback to Apple.
However, you can still access the navigation bar by its identifier - just the default identifier is the text:
.navigationTitle("title")
let app = XCUIApplication()
app.launch()
assert(app.navigationBars["title"].exists) // true
Alternatively, you can try to access UINavigationBar using a helper extension (adapted from here):
struct NavigationBarAccessor: UIViewControllerRepresentable {
var callback: (UINavigationBar?) -> Void
private let proxyController = ViewController()
func makeUIViewController(context: UIViewControllerRepresentableContext<NavigationBarAccessor>) -> UIViewController {
proxyController.callback = callback
return proxyController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<NavigationBarAccessor>) {}
typealias UIViewControllerType = UIViewController
private class ViewController: UIViewController {
var callback: (UINavigationBar?) -> Void = { _ in }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
callback(navigationController?.navigationBar)
}
}
}
Now you can access UINavigationBar from a SwiftUI view:
struct ContentView: View {
var body: some View {
NavigationView {
Text("text")
.navigationTitle("title")
.background(
NavigationBarAccessor {
$0?.accessibilityIdentifier = "id123"
}
)
}
}
}
Note that in the above example you set accessibilityIdentifier to the UINavigationBar itself and not to the title directly.

UIViewControllerRepresentable causes memory leak when in NavigationView

I'm aware about this issue that UIViewControllerRepresentable could cause a memory leak. Even if it should be fixed with past Xcode releases, I'm facing it only when embedding it in a NavigationView. I'm using Xcode Version 11.7 (11E801a) on a physical iPhone 11 iOS 13.7
Here an example:
Memory leak
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView { // Memory leak
ZStack {
Text("Hello, World!")
ViewControllerContainer() // Memory leak
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ViewControllerContainer: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
No memory leak
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Text("Hello, World!")
ViewControllerContainer() // No memory leak
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ViewControllerContainer: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
Is it a xcode/swiftui related bug or I missing something?
EDIT:
Screenshot of Instruments/Leaks
UPDATE:
The leak is not showing up with iPhone 11 (iOS 13.7) simulator

How to give back swipe gesture in SwiftUI the same behaviour as in UIKit (interactivePopGestureRecognizer)

The interactive pop gesture recognizer should allow the user to go back the the previous view in navigation stack when they swipe further than half the screen (or something around those lines). In SwiftUI the gesture doesn't get canceled when the swipe wasn't far enough.
SwiftUI: https://imgur.com/xxVnhY7
UIKit: https://imgur.com/f6WBUne
Question:
Is it possible to get the UIKit behaviour while using SwiftUI views?
Attempts
I tried to embed a UIHostingController inside a UINavigationController but that gives the exact same behaviour as NavigationView.
struct ContentView: View {
var body: some View {
UIKitNavigationView {
VStack {
NavigationLink(destination: Text("Detail")) {
Text("SwiftUI")
}
}.navigationBarTitle("SwiftUI", displayMode: .inline)
}.edgesIgnoringSafeArea(.top)
}
}
struct UIKitNavigationView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UINavigationController {
let host = UIHostingController(rootView: content())
let nvc = UINavigationController(rootViewController: host)
return nvc
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
}
I ended up overriding the default NavigationView and NavigationLink to get the desired behaviour. This seems so simple that I must be overlooking something that the default SwiftUI views do?
NavigationView
I wrap a UINavigationController in a super simple UIViewControllerRepresentable that gives the UINavigationController to the SwiftUI content view as an environmentObject. This means the NavigationLink can later grab that as long as it's in the same navigation controller (presented view controllers don't receive the environmentObjects) which is exactly what we want.
Note: The NavigationView needs .edgesIgnoringSafeArea(.top) and I don't know how to set that in the struct itself yet. See example if your nvc cuts off at the top.
struct NavigationView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UINavigationController {
let nvc = UINavigationController()
let host = UIHostingController(rootView: content().environmentObject(nvc))
nvc.viewControllers = [host]
return nvc
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
}
extension UINavigationController: ObservableObject {}
NavigationLink
I create a custom NavigationLink that accesses the environments UINavigationController to push a UIHostingController hosting the next view.
Note: I didn't implement the selection and isActive that the SwiftUI.NavigationLink has because I don't fully understand what they do yet. If you want to help with that please comment/edit.
struct NavigationLink<Destination: View, Label:View>: View {
var destination: Destination
var label: () -> Label
public init(destination: Destination, #ViewBuilder label: #escaping () -> Label) {
self.destination = destination
self.label = label
}
/// If this crashes, make sure you wrapped the NavigationLink in a NavigationView
#EnvironmentObject var nvc: UINavigationController
var body: some View {
Button(action: {
let rootView = self.destination.environmentObject(self.nvc)
let hosted = UIHostingController(rootView: rootView)
self.nvc.pushViewController(hosted, animated: true)
}, label: label)
}
}
This solves the back swipe not working correctly on SwiftUI and because I use the names NavigationView and NavigationLink my entire project switched to these immediately.
Example
In the example I show modal presentation too.
struct ContentView: View {
#State var isPresented = false
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 30) {
NavigationLink(destination: Text("Detail"), label: {
Text("Show detail")
})
Button(action: {
self.isPresented.toggle()
}, label: {
Text("Show modal")
})
}
.navigationBarTitle("SwiftUI")
}
.edgesIgnoringSafeArea(.top)
.sheet(isPresented: $isPresented) {
Modal()
}
}
}
struct Modal: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 30) {
NavigationLink(destination: Text("Detail"), label: {
Text("Show detail")
})
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Dismiss modal")
})
}
.navigationBarTitle("Modal")
}
}
}
Edit: I started off with "This seems so simple that I must be overlooking something" and I think I found it. This doesn't seem to transfer EnvironmentObjects to the next view. I don't know how the default NavigationLink does that so for now I manually send objects on to the next view where I need them.
NavigationLink(destination: Text("Detail").environmentObject(objectToSendOnToTheNextView)) {
Text("Show detail")
}
Edit 2:
This exposes the navigation controller to all views inside NavigationView by doing #EnvironmentObject var nvc: UINavigationController. The way to fix this is making the environmentObject we use to manage navigation a fileprivate class. I fixed this in the gist: https://gist.github.com/Amzd/67bfd4b8e41ec3f179486e13e9892eeb
You can do this by descending into UIKit and using your own UINavigationController.
First create a SwipeNavigationController file:
import UIKit
import SwiftUI
final class SwipeNavigationController: UINavigationController {
// MARK: - Lifecycle
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
delegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
// This needs to be in here, not in init
interactivePopGestureRecognizer?.delegate = self
}
deinit {
delegate = nil
interactivePopGestureRecognizer?.delegate = nil
}
// MARK: - Overrides
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
duringPushAnimation = true
super.pushViewController(viewController, animated: animated)
}
var duringPushAnimation = false
// MARK: - Custom Functions
func pushSwipeBackView<Content>(_ content: Content) where Content: View {
let hostingController = SwipeBackHostingController(rootView: content)
self.delegate = hostingController
self.pushViewController(hostingController, animated: true)
}
}
// MARK: - UINavigationControllerDelegate
extension SwipeNavigationController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.duringPushAnimation = false
}
}
// MARK: - UIGestureRecognizerDelegate
extension SwipeNavigationController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == interactivePopGestureRecognizer else {
return true // default value
}
// Disable pop gesture in two situations:
// 1) when the pop animation is in progress
// 2) when user swipes quickly a couple of times and animations don't have time to be performed
let result = viewControllers.count > 1 && duringPushAnimation == false
return result
}
}
This is the same SwipeNavigationController provided here, with the addition of the pushSwipeBackView() function.
This function requires a SwipeBackHostingController which we define as
import SwiftUI
class SwipeBackHostingController<Content: View>: UIHostingController<Content>, UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.duringPushAnimation = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.delegate = nil
}
}
We then set up the app's SceneDelegate to use the SwipeNavigationController:
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let hostingController = UIHostingController(rootView: ContentView())
window.rootViewController = SwipeNavigationController(rootViewController: hostingController)
self.window = window
window.makeKeyAndVisible()
}
Finally use it in your ContentView:
struct ContentView: View {
func navController() -> SwipeNavigationController {
return UIApplication.shared.windows[0].rootViewController! as! SwipeNavigationController
}
var body: some View {
VStack {
Text("SwiftUI")
.onTapGesture {
self.navController().pushSwipeBackView(Text("Detail"))
}
}.onAppear {
self.navController().navigationBar.topItem?.title = "Swift UI"
}.edgesIgnoringSafeArea(.top)
}
}

Resources