Skip to content

Instantly share code, notes, and snippets.

@pitt500
Last active July 6, 2020 16:21
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 pitt500/6d500856fb8f16f2b7818a2f814544a9 to your computer and use it in GitHub Desktop.
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.
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