Last active
July 6, 2020 16:21
-
-
Save pitt500/6d500856fb8f16f2b7818a2f814544a9 to your computer and use it in GitHub Desktop.
TXTabView is a custom view controller representable to support lazy loading and keeping the screen state for SwiftUI Views without reloading every time.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
protocol TXTabBarElementView: View { | |
associatedtype Content | |
var content: Content { get set } | |
var item: TXTabItem.Item { get set } | |
} | |
public struct TXTabItem: TXTabBarElementView { | |
var content: AnyView | |
var item: Item | |
public init<Content: View>( | |
title: String, | |
imageName: String, | |
@ViewBuilder _ content: () -> Content) | |
{ | |
self.item = Item(title: title, imageName: imageName) | |
self.content = AnyView(content()) | |
} | |
public var body: some View { | |
content | |
} | |
struct Item { | |
var title: String | |
var imageName: String | |
} | |
} | |
struct TXTabItem_Previews: PreviewProvider { | |
static var previews: some View { | |
TXTabItem(title: "Hello", imageName: "house.fill") { | |
Text("Hello!") | |
} | |
} | |
} | |
struct UITabBarControllerWrapper: UIViewControllerRepresentable { | |
var viewControllers: [UIViewController] | |
@Binding var selectedIndex: Int | |
func makeUIViewController(context: Context) -> UITabBarController { | |
let tabBar = UITabBarController() | |
tabBar.delegate = context.coordinator | |
tabBar.setViewControllers(self.viewControllers, animated: false) | |
tabBar.selectedIndex = 0 | |
return tabBar | |
} | |
func updateUIViewController(_ uiViewController: UITabBarController, context: Context) { | |
uiViewController.selectedIndex = selectedIndex | |
} | |
func makeCoordinator() -> Coodinator { | |
return Coordinator(self) | |
} | |
class Coodinator: NSObject, UITabBarControllerDelegate { | |
var parent: UITabBarControllerWrapper | |
init(_ controller: UITabBarControllerWrapper) { | |
self.parent = controller | |
} | |
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { | |
if parent.selectedIndex == tabBarController.selectedIndex { | |
popToRootOrScrollUp(on: viewController) | |
} else { | |
parent.selectedIndex = tabBarController.selectedIndex | |
} | |
} | |
// If we are in a deeper view location, move to root | |
// otherwise we check if the root screen has a scroll view | |
// and we move to the top. | |
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) | |
} | |
} | |
} | |
// Find the navigation controller in the view hierarchy | |
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 | |
} | |
// Find the first scroll view in the view hierarchy | |
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 | |
} | |
} | |
} | |
@_functionBuilder | |
public struct TabBuilder { | |
public static func buildBlock(_ items: TXTabItem...) -> [TXTabItem] { | |
items | |
} | |
public static func buildBlock(_ item: TXTabItem) -> TXTabItem { | |
item | |
} | |
} | |
public struct TXTabView: View { | |
var controllers: [UIHostingController<TXTabItem>] | |
@Binding private var selectedIndex: Int | |
public init(selection: Binding<Int>, @TabBuilder _ views: () -> [TXTabItem]) { | |
self.controllers = views().enumerated().map { | |
let hostingController = UIHostingController(rootView: $1) | |
hostingController.tabBarItem = UITabBarItem( | |
title: NSLocalizedString($1.item.title, comment: ""), | |
image: UIImage(named: $1.item.imageName) ?? UIImage(systemName: $1.item.imageName), | |
tag: $0 | |
) | |
return hostingController | |
} | |
self._selectedIndex = selection | |
} | |
public init(selection: Binding<Int>, @TabBuilder _ view: () -> TXTabItem) { | |
let element = view() | |
let hostingController = UIHostingController(rootView: element) | |
hostingController.tabBarItem = UITabBarItem( | |
title: NSLocalizedString(element.item.title, comment: ""), | |
image: UIImage(systemName: element.item.imageName), | |
tag: 0 | |
) | |
self._selectedIndex = selection | |
self.controllers = [hostingController] | |
} | |
public var body: some View { | |
UITabBarControllerWrapper(viewControllers: self.controllers, selectedIndex: $selectedIndex) | |
.edgesIgnoringSafeArea(.all) | |
} | |
} | |
struct TXTabView_Previews: PreviewProvider { | |
static var previews: some View { | |
TXTabView(selection: .constant(0)) { | |
TXTabItem(title: "Hello", imageName: "house.fill") { | |
Text("Test 1") | |
} | |
TXTabItem(title: "World", imageName: "doc.fill") { | |
Text("Test 2") | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment