SwiftUI button not called on UIInputViewController - swift

I am woking on an iOS Custom Keyboard Extension. SwiftUI buttons are showing properly but never gets called!
import SwiftUI
class KeyboardViewController: UIInputViewController {
override func viewDidLoad() {
super.viewDidLoad()
let vc = UIHostingController(rootView: MyKeyButtons())
vc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(vc.view)
}
}
struct MyKeyButtons: View {
let data: [String] = ["A", "B", "C"]
var body: some View {
HStack {
ForEach(data, id: \.self) { aData in
Button(action: {
print("button pressed!") // Not working!
}) {
Text(aData).fontWeight(.bold).font(.title)
.foregroundColor(.white).padding()
.background(Color.purple)
}
}
}
}
}
For easier understanding, here is the full: https://github.com/ask2asim/KeyboardTest1

Related

Presenting two sheets in SwiftUI

I'm trying to present two sheets in SwiftUI. The first sheet (SecondScreen) opens up on the Main Page (tapping the Navigation Tool Bar Icon) and the second sheet is a ShareSheet which should pop up inside the SecondScreen as an option. I have used a Form to build the SecondScreen. In the Simulator and on my device, the ShareSheet doesn't appear. I hope this is just a bug and not something Apple doesn't allow without big UI changes.
I tried to open the ShareSheet, while having the SecondScreen as a .fullScreenCover., instead of .sheet but the button still doesn't react.
Example
import SwiftUI
struct ContentView: View {
#State var showMore: Bool = false
var body: some View {
NavigationView {
Text("Main Page")
.padding()
.navigationBarTitle("Main Page")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showMore.toggle()
}, label: {
Image(systemName: "ellipsis.circle")
})
.sheet(isPresented: $showMore, content: {
SecondScreen()
})
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct SecondScreen: View {
var body: some View {
NavigationView {
Form {
Section {
Button(action: {
ShareID (Info: "https://www.google.com")}, label: { Text("Share")
})
}
}
}
}
}
}
func ShareID(Info: String){
let infoU = Info
let av = UIActivityViewController(activityItems: [infoU], applicationActivities: nil)
UIApplication.shared.windows.first?
.rootViewController?.present(av, animated: true,
completion: nil)
}
Thank you!
this is another approach to popup your sheets, even works on my mac:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var showMore: Bool = false
var body: some View {
NavigationView {
Text("Main Page")
.padding()
.navigationBarTitle("Main Page")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showMore.toggle() }) {
Image(systemName: "ellipsis.circle")
}
.sheet(isPresented: $showMore) {
SecondScreen()
}
}
}
}
}
}
struct SecondScreen: View {
#State var shareIt = false
#State var info = "https://www.google.com"
var body: some View {
Button(action: {shareIt = true}) {
Text("Share")
}
.sheet(isPresented: $shareIt, onDismiss: {shareIt = false}) {
ShareSheet(activityItems: [info as Any])
}
}
}
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
let activityItems: [Any]
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
let callback: Callback? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { }
}

SwiftUI hide a view by touching outside of its bounds

I'm trying to implement pop up alert, which basicly are view that appears on another view, how can i dismiss this alert view on top, when i tap outside of white box? screen
My logic of calling this alert: I have my main view, where i have facebook signIn button, which should call this alert view, my alert view is just another swiftui view, which has UIViewRepresentable object inside to make hyperlinks inside my text.
MAIN VIEW:
//
// WelcomeView.swift
//
//
//
//
import SwiftUI
import FacebookLogin
import SDWebImageSwiftUI
import NavigationStack
import AVKit
struct WelcomeView: View {
#State var showTermsAndConditionsAlert: Bool = false
ZStack { // 1 outer
ZStack { // 2 inner
VStack() {
ZStack(alignment: .top) {
// my video player here
HStack {
// some other ui elements
}
VStack {
// logo
}
}
}
HStack(alignment: .bottom) {
VStack {
Button(action: {
appLog.debug("Continue with Facebook")
showTermsAndConditionsAlert = true
}) {
AuthButtonView(imageIdentifier: "ic_facebook", buttonTitle: "welcome_page_sign_up_btn_fb".localized)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(5)
}
// email button sign up
}
// email button sign in
}) {
}
// push views
}
.ignoresSafeArea()
}
}
.padding(.bottom, Theme.pageBottomPadding)
if showTermsAndConditionsAlert {
TermsConditionsAlertView() {
self.showTermsAndConditionsAlert = false
self.viewModel.facebookLogin()
}
}
}
.onTapGesture(perform: {
// TODO: close dialog only when it is opened and press outside of dialog bound
self.showTermsAndConditionsAlert = false
})
}
.navigationBarTitle("", displayMode: .automatic)
.onAppear {
self.player.isMuted = true
self.player.play()
}
.onDisappear {
self.player.pause()
}
.onReceive(viewModel.$state.dropFirst(), perform: { state in
// switch state
}
})
}
}
ALERT VIEW: HyperLinkTextView - uirepresantable uiview
//
// TermsConditionsAlertView.swift
//
//
//
//
import SwiftUI
import NavigationStack
struct TermsConditionsAlertView: View {
var callback: (() -> ())?
var body: some View {
ZStack {
Color.black.opacity(0.8).edgesIgnoringSafeArea(.all)
VStack(alignment: .leading, spacing: 0) {
// Text here
HyperLinkTextView(text: "terms_conditions_alert_view_description".localized,
links: [Hyperlink(word: "terms_conditions".localized, url: NSURL(string: Constants.termsURL)!),
Hyperlink(word: "privacy_policy".localized, url: NSURL(string: Constants.privacyPolicyURL)!)])
Button(action: {
appLog.debug("Agree and sign up pressed")
callback?()
}) {
// button struct
}
}
}
}
}
HYPERLINK UI VIEW:
//
// HyperLinkTextView.swift
//
//
//
//
import SwiftUI
struct Hyperlink {
var word: String
var url: NSURL
}
struct HyperLinkTextView: UIViewRepresentable {
private var text: String
private var links: [Hyperlink]
init(text: String, links: [Hyperlink]) {
self.text = text
self.links = links
}
func makeUIView(context: Self.Context) -> UITextView {
let attributedString = NSMutableAttributedString(string: text)
links.forEach { hyperlink in
let linkAttributes = [NSAttributedString.Key.link: hyperlink.url]
var nsRange = NSMakeRange(0, 0)
if let range = text.range(of: hyperlink.word) {
nsRange = NSRange(range, in: text)
}
attributedString.setAttributes(linkAttributes, range: nsRange)
attributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSNumber(value: 1), range: nsRange)
}
let textView = UITextView()
textView.isEditable = false
textView.delegate = context.coordinator
textView.attributedText = attributedString
textView.linkTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.blue]
textView.isUserInteractionEnabled = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = UIFont(name: "ArialMT", size: 18)
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
public class Coordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate {
weak var textView: UITextView?
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction, replacementText text: String) -> Bool {
return true
}
}
}
Make a full screen view with a ZStack containing a view that is translucent, and put your alert on top of that. Add a tap gesture to the translucent view that dismisses the entire view.

SwiftUI generic navigation link using block variable

Here I would like to create navigation code, I'm able to do this Swift UIKit. I'm trying the same functionality in SwiftUI, but I'm facing an issue with my code. How can we convert the SwiftUI view to AnyView.
Is there any other way to achieve the same functionality in SwiftUI?
Your help would be greatly appreciated.!!
/// Swift code
public struct Navigator {
public var onLoginSuccess: (UINavigationController) -> Void = { navigationController in
navigationController.pushViewController(UIViewController(), animated: true)
}
}
/// Usage
var router = Navigator()
router.onLoginSuccess = { nav in
nav.pushViewController(UIViewController(), animated: true)
}
/// SwiftUI Code
struct Navigator {
static var onTap: (AnyView) -> Void = { view in
_ = view.navigate(to: Text("SS"))
}
}
extension View {
func navigate<SomeView: View>(to view: SomeView) -> some View {
modifier(NavigateModifier(destination: view))
}
}
fileprivate struct NavigateModifier<SomeView: View>: ViewModifier {
fileprivate let destination: SomeView
fileprivate func body(content: Content) -> some View {
NavigationView {
ZStack {
content
NavigationLink(destination: destination) {
EmptyView()
}
}
}
}
}
/// Usage
NavigationView {
Button("Home") {
Navigator.onTap(self)
}
}
Here is another solution code working fine with a single destination, but I can't change destination runtime. Router.onLogin should accept destination view.
struct ContentView: View {
var body: some View {
NavigationView {
HStack {
NavigationLink(destination: Router.onLogin) {
Text("HOME")
}
}
}
}
}
struct Router {
#ViewBuilder
static var onLogin: some View {
Text("Hello")
}
}
This wouldn't work because in Navigator onTap return void and it will not push view on any view.
But you can do by this
extension View {
/// Navigate to a new view.
/// - Parameters:
/// - view: View to navigate to.
/// - binding: Active binding
func navigate<NewView: View>(to view: NewView, when binding: Binding<Bool>) -> some View {
ZStack {
self
NavigationLink(
destination: view,
isActive: binding
) {
EmptyView()
}
}
}
}
Usage:
struct ContentView: View {
#State private var isNextScreen: Bool = false
var body: some View {
NavigationView {
Button("Home") {
isNextScreen.toggle()
}.navigate(to: Text("SS"),when: $isNextScreen)
}
}
}
Update
As you mention in a comment, you want multiple and dynamic destinations.
Then you can use it this way.
View extension for navigation
extension View {
func navigate(to view: Binding<Navigator?>) -> some View {
ZStack {
self
if let wrappedValue = view.wrappedValue {
NavigationLink(
destination: wrappedValue.navigateView,
tag: wrappedValue,
selection: view,
label: {EmptyView()})
}
}
}
}
Create Navigator
enum Navigator: Identifiable {
case onTap
case onLogin
var id: Navigator {
return self
}
#ViewBuilder
var navigateView: some View {
switch self {
case .onTap:
Text("SS")
case .onLogin:
Text("Login View")
}
}
}
Usage Content View
struct ContentView: View {
#State private var nextScreen: Navigator? = nil
var body: some View {
NavigationView {
VStack{
Button("Home") {
nextScreen = .onTap
}
Button("Login") {
nextScreen = .onLogin
}
}.navigate(to: $nextScreen)
}
}
}
Here I got one solution to this issue, but not sure it's the best. I believe still we can improve this.
struct LoginView: View {
var loginAction = Router().onLogin(AnyView(Text("Actual View Wll This")))
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: loginAction) {
Text("Show View")
}
NavigationLink(destination: AnyView(Text("Detail"))) {
Text("Show Detail View")
}
}
}
}
}
extension LoginView {
struct Router {
var onLogin:(AnyView) -> AnyView = { links in
return AnyView(links)
}
}
}
/// Now I can change the 'Destination' for 'loginAction' in a scene and where ever I want.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
var contentView = LoginView()
contentView.loginAction = AnyView(Text("Changed Route to here"))
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
// After some improvements
struct LoginView: View {
var loginAction = Router().onLogin(Text("Actual Detail View"))
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: loginAction) {
Text("Show View")
}
}
}
}
}
extension LoginView {
struct Router<Destination: View> {
var onLogin:(Destination) -> Destination = { links in
return links
}
}
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
var contentView = LoginView()
contentView.loginAction = LoginView.Router().onLogin(Text("Changed Route here .!!"))
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}

Present embedded SwiftUI Subview in Full Screen, UIHostingController

I work on a Storyboard Swift App want to present a SwiftUI Subview in Full-Screen. I embedded the SwiftUI View using UIHostingController, like this:
let contentView = UIHostingController(rootView: ContentView())
override func viewDidLoad() {
super.viewDidLoad()
addChild(contentView)
view.addSubview(contentView.view)
}
And this is my SwiftUI ContentView() Subview:
import SwiftUI
import UIKit
struct ContentView: View {
var body: some View {
Text("").fullScreenCover(isPresented: /*#START_MENU_TOKEN#*/.constant(true)/*#END_MENU_TOKEN#*/, content: {
FullScreenView.init()
})
}
}
struct FullScreenView: View{
var body: some View {
NavigationView{
MasterView()
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
struct MasterView: View {
var body: some View {
Form {
Section(header: Text("HEADER")) {
Section {
NavigationLink(destination: UIKitView()) { Text("NAVLINK TEXT") }
}
}
}
.navigationBarTitle("NAVBAR TEXT")
}
}
struct UIKitView: UIViewControllerRepresentable {
typealias UIViewControllerType = SwipeViewController
func makeUIViewController(context: Context) -> SwipeViewController {
let sb = UIStoryboard(name: "Storyboard", bundle: nil)
let viewController = sb.instantiateViewController(identifier: "vc") as! SwipeViewController
return viewController
}
func updateUIViewController(_ uiViewController: SwipeViewController, context: Context) {
}
}
Is there a better way to do it? As you can see, I use an empty text field to add the .fullScreenCover :/
Thanks for any help!!

SwiftUI: Pop to root view when selected tab is tapped again

Starting point is a NavigationView within a TabView. I'm struggling with finding a SwiftUI solution to pop to the root view within the navigation stack when the selected tab is tapped again. In the pre-SwiftUI times, this was as simple as the following:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let navController = viewController as! UINavigationController
navController.popViewController(animated: true)
}
Do you know how the same thing can be achieved in SwiftUI?
Currently, I use the following workaround that relies on UIKit:
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let navigationController = UINavigationController(rootViewController: UIHostingController(rootView: MyCustomView() // -> this is a normal SwiftUI file
.environment(\.managedObjectContext, context)))
navigationController.tabBarItem = UITabBarItem(title: "My View 1", image: nil, selectedImage: nil)
// add more controllers that are part of tab bar controller
let tabBarController = UITabBarController()
tabBarController.viewControllers = [navigationController /* , additional controllers */ ]
window.rootViewController = tabBarController // UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
Here is possible approach. For TabView it gives the same behaviour as tapping to the another tab and back, so gives persistent look & feel.
Tested & works with Xcode 11.2 / iOS 13.2
Full module code:
import SwiftUI
struct TestPopToRootInTab: View {
#State private var selection = 0
#State private var resetNavigationID = UUID()
var body: some View {
let selectable = Binding( // << proxy binding to catch tab tap
get: { self.selection },
set: { self.selection = $0
// set new ID to recreate NavigationView, so put it
// in root state, same as is on change tab and back
self.resetNavigationID = UUID()
})
return TabView(selection: selectable) {
self.tab1()
.tabItem {
Image(systemName: "1.circle")
}.tag(0)
self.tab2()
.tabItem {
Image(systemName: "2.circle")
}.tag(1)
}
}
private func tab1() -> some View {
NavigationView {
NavigationLink(destination: TabChildView()) {
Text("Tab1 - Initial")
}
}.id(self.resetNavigationID) // << making id modifiable
}
private func tab2() -> some View {
Text("Tab2")
}
}
struct TabChildView: View {
var number = 1
var body: some View {
NavigationLink("Child \(number)",
destination: TabChildView(number: number + 1))
}
}
struct TestPopToRootInTab_Previews: PreviewProvider {
static var previews: some View {
TestPopToRootInTab()
}
}
Here's an approach that uses a PassthroughSubject to notify the child view whenever the tab is re-selected, and a view modifier to allow you to attach .onReselect() to a view.
import SwiftUI
import Combine
enum TabSelection: String {
case A, B, C // etc
}
private struct DidReselectTabKey: EnvironmentKey {
static let defaultValue: AnyPublisher<TabSelection, Never> = Just(.Mood).eraseToAnyPublisher()
}
private struct CurrentTabSelection: EnvironmentKey {
static let defaultValue: Binding<TabSelection> = .constant(.Mood)
}
private extension EnvironmentValues {
var tabSelection: Binding<TabSelection> {
get {
return self[CurrentTabSelection.self]
}
set {
self[CurrentTabSelection.self] = newValue
}
}
var didReselectTab: AnyPublisher<TabSelection, Never> {
get {
return self[DidReselectTabKey.self]
}
set {
self[DidReselectTabKey.self] = newValue
}
}
}
private struct ReselectTabViewModifier: ViewModifier {
#Environment(\.didReselectTab) private var didReselectTab
#State var isVisible = false
let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content
.onAppear {
self.isVisible = true
}.onDisappear {
self.isVisible = false
}.onReceive(didReselectTab) { _ in
if self.isVisible, let action = self.action {
action()
}
}
}
}
extension View {
public func onReselect(perform action: (() -> Void)? = nil) -> some View {
return self.modifier(ReselectTabViewModifier(perform: action))
}
}
struct NavigableTabViewItem<Content: View>: View {
#Environment(\.didReselectTab) var didReselectTab
let tabSelection: TabSelection
let imageName: String
let content: Content
init(tabSelection: TabSelection, imageName: String, #ViewBuilder content: () -> Content) {
self.tabSelection = tabSelection
self.imageName = imageName
self.content = content()
}
var body: some View {
let didReselectThisTab = didReselectTab.filter( { $0 == tabSelection }).eraseToAnyPublisher()
NavigationView {
self.content
.navigationBarTitle(tabSelection.localizedStringKey, displayMode: .inline)
}.tabItem {
Image(systemName: imageName)
Text(tabSelection.localizedStringKey)
}
.tag(tabSelection)
.navigationViewStyle(StackNavigationViewStyle())
.keyboardShortcut(tabSelection.keyboardShortcut)
.environment(\.didReselectTab, didReselectThisTab)
}
}
struct NavigableTabView<Content: View>: View {
#State private var didReselectTab = PassthroughSubject<TabSelection, Never>()
#State private var _selection: TabSelection = .Mood
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
let selection = Binding(get: { self._selection },
set: {
if self._selection == $0 {
didReselectTab.send($0)
}
self._selection = $0
})
TabView(selection: selection) {
self.content
.environment(\.tabSelection, selection)
.environment(\.didReselectTab, didReselectTab.eraseToAnyPublisher())
}
}
}
Here's how I did it:
struct UIKitTabView: View {
var viewControllers: [UIHostingController<AnyView>]
init(_ tabs: [Tab]) {
self.viewControllers = tabs.map {
let host = UIHostingController(rootView: $0.view)
host.tabBarItem = $0.barItem
return host
}
}
var body: some View {
TabBarController(controllers: viewControllers).edgesIgnoringSafeArea(.all)
}
struct Tab {
var view: AnyView
var barItem: UITabBarItem
init<V: View>(view: V, barItem: UITabBarItem) {
self.view = AnyView(view)
self.barItem = barItem
}
}
}
struct TabBarController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UITabBarController {
let tabBarController = UITabBarController()
tabBarController.viewControllers = controllers
tabBarController.delegate = context.coordinator
return tabBarController
}
func updateUIViewController(_ uiViewController: UITabBarController, context: Context) { }
}
extension TabBarController {
func makeCoordinator() -> TabBarController.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITabBarControllerDelegate {
var parent: TabBarController
init(_ parent: TabBarController){self.parent = parent}
var previousController: UIViewController?
private var shouldSelectIndex = -1
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
shouldSelectIndex = tabBarController.selectedIndex
return true
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if shouldSelectIndex == tabBarController.selectedIndex {
if let navVC = tabBarController.viewControllers![shouldSelectIndex].nearestNavigationController {
if (!(navVC.popViewController(animated: true) != nil)) {
navVC.viewControllers.first!.scrollToTop()
}
}
}
}
}
}
extension UIViewController {
var nearestNavigationController: UINavigationController? {
if let selfTypeCast = self as? UINavigationController {
return selfTypeCast
}
if children.isEmpty {
return nil
}
for child in self.children {
return child.nearestNavigationController
}
return nil
}
}
extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.safeAreaInsets.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: view)
}
}
Then in ContentView.swift I use it like this:
struct ContentView: View {
var body: some View {
ZStack{
UIKitTabView([
UIKitTabView.Tab(
view: FirstView().edgesIgnoringSafeArea(.top),
barItem: UITabBarItem(title: "Tab1", image: UIImage(systemName: "star"), selectedImage: UIImage(systemName: "star.fill"))
),
UIKitTabView.Tab(
view: SecondView().edgesIgnoringSafeArea(.top),
barItem: UITabBarItem(title: "Tab2", image: UIImage(systemName: "star"), selectedImage: UIImage(systemName: "star.fill"))
),
])
}
}
}
Note that when the user is already on the root view, it scrolls to top automatically
Here's what I did with introspect swiftUI library.
https://github.com/siteline/SwiftUI-Introspect
struct TabBar: View {
#State var tabSelected: Int = 0
#State var navBarOne: UINavigationController?
#State var navBarTwo: UINavigationController?
#State var navBarThree: UINavigationController?
var body: some View {
return TabView(selection: $tabSelected){
NavView(navigationView: $navBarOne).tabItem {
Label("Home1",systemImage: "bag.fill")
}.tag(0)
NavView(navigationView: $navBarTwo).tabItem {
Label("Orders",systemImage: "scroll.fill" )
}.tag(1)
NavView(navigationView: $navBarThree).tabItem {
Label("Wallet", systemImage: "dollarsign.square.fill" )
// Image(systemName: tabSelected == 2 ? "dollarsign.square.fill" : "dollarsign.square")
}.tag(2)
}.onTapGesture(count: 2) {
switch tabSelected{
case 0:
self.navBarOne?.popToRootViewController(animated: true)
case 1:
self.navBarTwo?.popToRootViewController(animated: true)
case 2:
self.navBarThree?.popToRootViewController(animated: true)
default:
print("tapped")
}
}
}
}
NavView:
import SwiftUI
import Introspect
struct NavView: View {
#Binding var navigationView: UINavigationController?
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: Text("Detail view")) {
Text("Go To detail")
}
}.introspectNavigationController { navController in
navigationView = navController
}
}
}
}
This actually isn't the best approach because it makes the entire tab view and everything inside of it have the double-tap gesture which would pop the view to its root. My current fix for this allows for one tap to pop up root view haven't figured out how to add double tap
struct TabBar: View {
#State var tabSelected: Int = 0
#State var navBarOne: UINavigationController?
#State var navBarTwo: UINavigationController?
#State var navBarThree: UINavigationController?
#State var selectedIndex:Int = 0
var selectionBinding: Binding<Int> { Binding(
get: {
self.selectedIndex
},
set: {
if $0 == self.selectedIndex {
popToRootView(tabSelected: $0)
}
self.selectedIndex = $0
}
)}
var body: some View {
return TabView(selection: $tabSelected){
NavView(navigationView: $navBarOne).tabItem {
Label("Home1",systemImage: "bag.fill")
}.tag(0)
NavView(navigationView: $navBarTwo).tabItem {
Label("Orders",systemImage: "scroll.fill" )
}.tag(1)
NavView(navigationView: $navBarThree).tabItem {
Label("Wallet", systemImage: "dollarsign.square.fill" )
// Image(systemName: tabSelected == 2 ? "dollarsign.square.fill" : "dollarsign.square")
}.tag(2)
}
}
func popToRootView(tabSelected: Int){
switch tabSelected{
case 0:
self.navBarOne?.popToRootViewController(animated: true)
case 1:
self.navBarTwo?.popToRootViewController(animated: true)
case 2:
self.navBarThree?.popToRootViewController(animated: true)
default:
print("tapped")
}
}
}
I took an approach similar to Asperi
Use a combination of a custom binding, and a separately stored app state var for keeping state of the navigation link.
The custom binding allows you to see all taps basically even when the current tab is the one thats tapped, something that onChange of tab selection binding doesn't show. This is what imitates the UIKit TabViewDelegate behavior.
This doesn't require a "double tap", if you just a single tap of the current, if you want double tap you'll need to implement your own tap/time tracking but shouldn't be too hard.
class AppState: ObservableObject {
#Published var mainViewShowingDetailView = false
}
struct ContentView: View {
#State var tabState: Int = 0
#StateObject var appState = AppState()
var body: some View {
let binding = Binding<Int>(get: { tabState },
set: { newValue in
if newValue == tabState { // tapped same tab they're already on
switch newValue {
case 0: appState.mainViewShowingDetailView = false
default: break
}
}
tabState = newValue // make sure you actually set the storage
})
TabView(selection: binding) {
MainView()
.tabItem({ Label("Home", systemImage: "list.dash") })
.tag(0)
.environmentObject(appState)
}
}
}
struct MainView: View {
#EnvironmentObject var appState: AppState
var body: {
NavigationView {
VStack {
Text("Hello World")
NavigationLink(destination: DetailView(),
isActive: $appState.mainViewShowingDetailView,
label: { Text("Show Detail") })
}
}
}
}
struct DetailView: View {
...
}
iOS 16 / NavigationStack approach with PassthroughSubject
Uses willSet on selectedTab to get the tap event, and uses a PassthroughSubject for sending the event to the children. This is picked up by the .onReceived and calls a function for popping the views from the NavigationStack
Did a full write up here: https://kentrobin.com/home/tap-tab-to-go-back/ and created a working demo project here: https://github.com/kentrh/demo-tap-tab-to-go-back
class HomeViewModel: ObservableObject {
#Published var selectedTab: Tab = .tab1 {
willSet {
if selectedTab == newValue {
subject.send(newValue)
}
}
}
let subject = PassthroughSubject<Tab, Never>()
enum Tab: Int {
case tab1 = 0
}
}
struct HomeView: View {
#StateObject var viewModel: HomeViewModel = .init()
var body: some View {
TabView(selection: $viewModel.selectedTab) {
Tab1View(subject: viewModel.subject)
.tag(HomeViewModel.Tab.tab1)
.tabItem {
Label("Tab 1", systemImage: "1.lane")
Text("Tab 1", comment: "Tab bar title")
}
}
}
}
struct Tab1View: View {
#StateObject var viewModel: Tab1ViewModel = .init()
let subject: PassthroughSubject<HomeViewModel.Tab, Never>
var body: some View {
NavigationStack(path: $viewModel.path) {
List {
NavigationLink(value: Tab1ViewModel.Route.viewOne("From tab 1")) {
Text("Go deeper to OneView")
}
NavigationLink(value: Tab1ViewModel.Route.viewTwo("From tab 1")) {
Text("Go deeper to TwoView")
}
}
.navigationTitle("Tab 1")
.navigationDestination(for: Tab1ViewModel.Route.self, destination: { route in
switch route {
case let .viewOne(text):
Text(text)
case let .viewTwo(text):
Text(text)
}
})
.onReceive(subject) { tab in
if case .tab1 = tab { viewModel.tabBarTapped() }
}
}
}
}
class Tab1ViewModel: ObservableObject {
#Published var path: [Route] = []
func tabBarTapped() {
if path.count > 0 {
path.removeAll()
}
}
enum Route: Hashable {
case viewOne(String)
case viewTwo(String)
}
}