Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save MihaelIsaev/0c0950ccec4a74fee67ba2b052d3d6eb to your computer and use it in GitHub Desktop.

Select an option

Save MihaelIsaev/0c0950ccec4a74fee67ba2b052d3d6eb to your computer and use it in GitHub Desktop.

Revisions

  1. MihaelIsaev created this gist Jun 26, 2020.
    312 changes: 312 additions & 0 deletions UIKitPlus-TabBarImplementation.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,312 @@
    import UIKitPlus

    class MainViewController: ViewController {
    override var statusBarStyle: StatusBarStyle {
    if let controller = controllers[current]?.protocolController {
    if let controller = controller as? ViewController {
    return controller.statusBarStyle
    }
    return .from(controller.preferredStatusBarStyle)
    }
    return .default
    }

    lazy var home = WrappedViewControllerView.home(self)
    lazy var statistics = WrappedViewControllerView.statistics(self)
    lazy var trophy = WrappedViewControllerView.trophy(self)
    lazy var marketplace = WrappedViewControllerView.marketplace(self)
    lazy var balance = WrappedViewControllerView.balance(self)

    typealias Controllers = [HomeTab: WrappedViewControllerable]
    lazy var controllers: Controllers = [.home: home, .statistics: statistics, .trophy: trophy, .marketplace: marketplace, .balance: balance]

    @UState var current: HomeTab = .home

    lazy var bottomBar = BottomTabBar($current)

    var presentingGoalEndedView = false

    override func buildUI() {
    super.buildUI()
    view.backgroundColor = 0xf5f6f8.color
    body {
    home
    statistics
    trophy
    marketplace
    balance
    bottomBar
    }
    switchToController(current)
    $current.listen { old, new in
    guard old != new else { return }
    self.switchToController(new)
    }
    }

    func switchToController(_ new: HomeTab) {
    controllers.forEach { $0.value.hidden($0.key != new) }
    navigationController?.setNeedsStatusBarAppearanceUpdate()
    }

    override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    (navigationController as? NavigationController<MainViewController>)?.isSwipeBackEnabled = true
    }
    }

    #if canImport(SwiftUI) && DEBUG
    import SwiftUI
    @available(iOS 13.0, *)
    struct MainViewController_Preview: PreviewProvider, DeclarativePreview {
    static var preview: Preview {
    Preview {
    MainViewController()
    }
    .colorScheme(.dark)
    .device(.iPhoneX)
    .language(.en)
    }
    }
    #endif

    // MARK - AppDelegate

    extension AppDelegate {
    static var shared: AppDelegate {
    UIApplication.shared.delegate as! AppDelegate
    }
    }

    extension AppDelegate {
    var safeInsets: UIEdgeInsets {
    window?.safeInsets ?? .zero
    }
    }

    // MARK: - Tab images

    extension UIImage {
    static var homeTab: UIImage? { UIImage(named: "tabBarHome") }
    static var statisticsTab: UIImage? { UIImage(named: "tabBarStatistics") }
    static var trophyTab: UIImage? { UIImage(named: "tabBarTrophy") }
    static var marketplaceTab: UIImage? { UIImage(named: "tabBarMarketplace") }
    static var balanceTab: UIImage? { UIImage(named: "tabBarBalance") }
    }

    // MARK: - Tab titles

    extension String {
    static var homeTitle: String { String(.en("___"), .ru("___")) }
    static var statisticsTitle: String { String(.en("___"), .ru("___")) }
    static var trophyTitle: String { String(.en("___"), .ru("___")) }
    static var marketplaceTitle: String { String(.en("___"), .ru("___")) }
    static var balanceTitle: String { String(.en("___"), .ru("___")) }
    }

    // MARK: - Tab colors

    extension UIColor {
    static var tabBarActive = 0x3EC900.color
    static var tabBarInactive = UIColor.lightGray
    }

    // MARK: - Tab types

    enum HomeTab {
    case home, statistics, trophy, marketplace, balance
    var title: String {
    switch self {
    case .home: return .homeTitle
    case .statistics: return .statisticsTitle
    case .trophy: return .trophyTitle
    case .marketplace: return .marketplaceTitle
    case .balance: return .balanceTitle
    }
    }
    var icon: UIImage? {
    switch self {
    case .home: return .homeTab
    case .statistics: return .statisticsTab
    case .trophy: return .trophyTab
    case .marketplace: return .marketplaceTab
    case .balance: return .balanceTab
    }
    }
    }

    // MARK: TabBar view

    class BottomTabBar: UView {
    var state: UState<HomeTab>

    init (_ state: UState<HomeTab>) {
    self.state = state
    super.init(frame: .zero)
    }

    required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
    }

    override func buildView() {
    super.buildView()
    background(.white)
    border(.top, 1, 0xF3F3F3)
    height(84 + AppDelegate.shared.safeInsets.bottom)
    edgesToSuperview(leading: 0, trailing: 0, bottom: 0)
    body {
    UHStack {
    BottomTabBarButton(.home, state: state)
    BottomTabBarButton(.statistics, state: state)
    BottomTabBarButton(.trophy, state: state)
    BottomTabBarButton(.marketplace, state: state)
    BottomTabBarButton(.balance, state: state)
    }
    .centerXInSuperview()
    .topToSuperview(16)
    .height(50)
    .distribution(.equalSpacing)
    }
    }
    }

    // MARK: TabBar item view

    class BottomTabBarButton: UView {
    let image: UIImage?
    let title: String
    let type: HomeTab
    let state: UState<HomeTab>

    lazy var imageView = Image(image)
    lazy var titleLabel = Text(title)

    init(_ type: HomeTab, state: UState<HomeTab>) {
    self.title = type.title
    self.image = type.icon
    self.type = type
    self.state = state
    super.init(frame: .zero)
    updateSelection()
    state.listen(updateSelection)
    onTapGesture(tapAction)
    }

    required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
    }

    override func buildView() {
    super.buildView()
    body {
    UVStack {
    imageView
    .mode(.scaleAspectFit)
    .size(25)
    .topToSuperview()
    .centerXInSuperview()
    titleLabel
    .top(to: .bottom, of: imageView, 4)
    .edgesToSuperview(leading: 0, trailing: 0, bottom: 0)
    .alignment(.center)
    .font(.helveticaNeueMedium, 12)
    }.edgesToSuperview().width(75 !! .iPhone6(70) !! .iPhone5(70))
    }
    }

    func updateSelection() {
    imageView.tint(state.wrappedValue == type ? .tabBarActive : .tabBarInactive)
    titleLabel.color(state.wrappedValue == type ? .tabBarActive : .tabBarInactive)
    }

    func tapAction() {
    guard state.wrappedValue != type else { return }
    ImpactFeedback.selected()
    state.wrappedValue = type
    }
    }

    // MARK: - Helper extensions

    extension WrappedViewControllerView {
    static func home(_ parent: MainViewController) -> WrappedViewControllerView<NavigationController<ViewController1>> {
    let r = improve(_home(parent)).bottom(to: .top, of: parent.bottomBar, -20)
    r.inner.style(.transparent).hideNavigationBar()
    return r
    }

    static func statistics(_ parent: MainViewController) -> WrappedViewControllerView<NavigationController<ViewController2>> {
    let r = improve(_statistics(parent)).bottom(to: .top, of: parent.bottomBar, -20)
    r.inner.style(.transparent).hideNavigationBar()
    return r
    }

    static func trophy(_ parent: MainViewController) -> WrappedViewControllerView<NavigationController<ViewController3>> {
    let r = improve(_trophy(parent)).bottom(to: .top, of: parent.bottomBar, -20)
    r.inner.style(.transparent).hideNavigationBar()
    return r
    }

    static func marketplace(_ parent: MainViewController) -> WrappedViewControllerView<NavigationController<ViewController4>> {
    let r = improve(_marketplace(parent)).bottom(to: .top, of: parent.bottomBar, -20)
    r.inner.style(.transparent).hideNavigationBar()
    return r
    }

    static func balance(_ parent: MainViewController) -> WrappedViewControllerView<NavigationController<ViewController5>> {
    let r = improve(_balance(parent)).bottom(to: .top, of: parent.bottomBar, -20)
    r.inner.style(.transparent).hideNavigationBar()
    return r
    }

    static func improve<V>(_ v: V) -> V {
    (v as? UView)?.edgesToSuperview(top: 0, leading: 0, trailing: 0)
    return v
    }
    }

    // MARK: Tricky implementation to make generics work

    extension WrappedViewControllerView {
    fileprivate static func _home(_ parent: MainViewController) -> WrappedViewControllerView<NavigationController<ViewController1>> {
    .init(.init(.init(parent)), parent: parent)
    }

    fileprivate static func _statistics(_ parent: MainViewController) -> WrappedViewControllerView<NavigationController<ViewController2>> {
    .init(.init(.init(parent)), parent: parent)
    }

    fileprivate static func _trophy(_ parent: MainViewController) -> WrappedViewControllerView<NavigationController<ViewController3>> {
    .init(.init(.init(parent)), parent: parent)
    }

    fileprivate static func _marketplace(_ parent: MainViewController) -> WrappedViewControllerView<NavigationController<ViewController4>> {
    .init(.init(.init(parent)), parent: parent)
    }

    fileprivate static func _balance(_ parent: MainViewController) -> WrappedViewControllerView<NavigationController<ViewController5>> {
    .init(.init(.init(parent)), parent: parent)
    }
    }

    // MARK: - Tab Controllers

    class MainSubController: ViewController {
    let mainViewController: MainViewController

    init (_ mainViewController: MainViewController) {
    self.mainViewController = mainViewController
    super.init()
    }

    required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
    }
    }

    class ViewController1: MainSubController {}
    class ViewController2: MainSubController {}
    class ViewController3: MainSubController {}
    class ViewController4: MainSubController {}
    class ViewController5: MainSubController {}