Skip to content

Instantly share code, notes, and snippets.

@ryanashcraft
Forked from Amzd/UIKitTabView.swift
Created March 25, 2021 02:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryanashcraft/aa771bf4952893249761500df7e8647d to your computer and use it in GitHub Desktop.
Save ryanashcraft/aa771bf4952893249761500df7e8647d to your computer and use it in GitHub Desktop.
UIKitTabView. SwiftUI tab bar view that respects navigation stacks when tabs are switched (unlike the TabView implementation)
/// An iOS style TabView that doesn't reset it's childrens navigation stacks when tabs are switched.
public struct UIKitTabView: View {
private var viewControllers: [UIHostingController<AnyView>]
private var selectedIndex: Binding<Int>?
@State private var fallbackSelectedIndex: Int = 0
public init(selectedIndex: Binding<Int>? = nil, @TabBuilder _ views: () -> [Tab]) {
self.viewControllers = views().map {
let host = UIHostingController(rootView: $0.view)
host.tabBarItem = $0.barItem
return host
}
self.selectedIndex = selectedIndex
}
public var body: some View {
TabBarController(controllers: viewControllers, selectedIndex: selectedIndex ?? $fallbackSelectedIndex)
.edgesIgnoringSafeArea(.all)
}
public struct Tab {
var view: AnyView
var barItem: UITabBarItem
}
}
@_functionBuilder
public struct TabBuilder {
public static func buildBlock(_ items: UIKitTabView.Tab...) -> [UIKitTabView.Tab] {
items
}
}
extension View {
public func tab(title: String, image: String? = nil, selectedImage: String? = nil, badgeValue: String? = nil) -> UIKitTabView.Tab {
func imageOrSystemImage(named: String?) -> UIImage? {
guard let name = named else { return nil }
return UIImage(named: name) ?? UIImage(systemName: name)
}
let image = imageOrSystemImage(named: image)
let selectedImage = imageOrSystemImage(named: selectedImage)
let barItem = UITabBarItem(title: title, image: image, selectedImage: selectedImage)
barItem.badgeValue = badgeValue
return UIKitTabView.Tab(view: AnyView(self), barItem: barItem)
}
}
fileprivate struct TabBarController: UIViewControllerRepresentable {
var controllers: [UIViewController]
@Binding var selectedIndex: Int
func makeUIViewController(context: Context) -> UITabBarController {
let tabBarController = UITabBarController()
tabBarController.viewControllers = controllers
tabBarController.delegate = context.coordinator
tabBarController.selectedIndex = 0
return tabBarController
}
func updateUIViewController(_ tabBarController: UITabBarController, context: Context) {
tabBarController.selectedIndex = selectedIndex
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITabBarControllerDelegate {
var parent: TabBarController
init(_ tabBarController: TabBarController) {
self.parent = tabBarController
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if parent.selectedIndex == tabBarController.selectedIndex {
popToRootOrScrollUp(on: viewController)
}
parent.selectedIndex = tabBarController.selectedIndex
}
private func popToRootOrScrollUp(on viewController: UIViewController) {
let nvc = navigationController(for: viewController)
let popped = nvc?.popToRootViewController(animated: true)
if (popped ?? []).isEmpty {
let rootViewController = nvc?.viewControllers.first ?? viewController
if let scrollView = firstScrollView(in: rootViewController.view ?? UIView()) {
let preservedX = scrollView.contentOffset.x
let y = -scrollView.adjustedContentInset.top
scrollView.setContentOffset(CGPoint(x: preservedX, y: y), animated: true)
}
}
}
private func navigationController(for viewController: UIViewController) -> UINavigationController? {
for child in viewController.children {
if let nvc = viewController as? UINavigationController {
return nvc
} else if let nvc = navigationController(for: child) {
return nvc
}
}
return nil
}
public func firstScrollView(in view: UIView) -> UIScrollView? {
for subview in view.subviews {
if let scrollView = view as? UIScrollView {
return scrollView
} else if let scrollView = firstScrollView(in: subview) {
return scrollView
}
}
return nil
}
}
}
struct ExampleView: View {
@State var text: String = ""
var body: some View {
UIKitTabView {
NavView().tab(title: "First", badgeValue: "3")
Text("Second View").tab(title: "Second")
}
}
}
struct NavView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("This page stays when you switch back and forth between tabs (as expected on iOS)")) {
Text("Go to detail")
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment