Tabbar middle button utility function in SwiftUI

Viewed 3292

I'm trying to reproduce a "Instagram" like tabBar which has a "Utility" button in the middle which doesn't necessarily belong to the tabBar eco system.

I have attached this gif to show the behaviour I am after. To describe the issue. The tab bar in the middle (Black plus) is click a ActionSheet is presented INSTEAD of switching the view.

enter image description here

How I would do this in UIKit is simply use the

override func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
    print("Selected item")
}

Function from the UITabBarDelegate. But obviously we can't do this in SwiftUI so was looking to see if there was any ideas people have tried. My last thought would be to simply wrap it in a UIView and use it with SwiftUI but would like to avoid this and keep it native.

I have seen a write up in a custom TabBar but would like to use the TabBar provided by Apple to avoid any future discrepancies.

Thanks!

Edit: Make the question clearer.

3 Answers

Thanks to Aleskey for the great answer (Marked as correct). I evolved it a little bit in addition to a medium article that was written around a Modal. I found it to be a little different

Here's the jist.

A MainTabBarData which is an Observable Object

final class MainTabBarData: ObservableObject {

    /// This is the index of the item that fires a custom action
    let customActiontemindex: Int

    let objectWillChange = PassthroughSubject<MainTabBarData, Never>()

    var previousItem: Int

    var itemSelected: Int {
        didSet {
            if itemSelected == customActiontemindex {
                previousItem = oldValue
                itemSelected = oldValue
                isCustomItemSelected = true
            }
            objectWillChange.send(self)
        }
    }

    func reset() {
        itemSelected = previousItem
        objectWillChange.send(self)
    }

    /// This is true when the user has selected the Item with the custom action
    var isCustomItemSelected: Bool = false

    init(initialIndex: Int = 1, customItemIndex: Int) {
        self.customActiontemindex = customItemIndex
        self.itemSelected = initialIndex
        self.previousItem = initialIndex
    }
}

And this is the TabbedView

struct TabbedView: View {

    @ObservedObject private var tabData = MainTabBarData(initialIndex: 1, customItemIndex: 2)


    var body: some View {

        TabView(selection: $tabData.itemSelected) {
            Text("First Screen")
                .tabItem {
                    VStack {
                        Image(systemName: "globe")
                            .font(.system(size: 22))
                        Text("Profile")
                    }

                }.tag(1)

            Text("Second Screen")
                .tabItem {
                    VStack {
                        Image(systemName: "plus.circle")
                            .font(.system(size: 22))
                        Text("Profile")
                    }
            }.tag(2)

            Text("Third Screen")
                .tabItem {
                    VStack {
                        Image(systemName: "number")
                            .font(.system(size: 22))
                        Text("Profile")
                    }
            }.tag(3)

        }.actionSheet(isPresented: $tabData.isCustomItemSelected) {
            ActionSheet(title: Text("SwiftUI ActionSheet"), message: Text("Action Sheet Example"),
                        buttons: [
                            .default(Text("Option 1"), action: option1),
                            .default(Text("Option 2"), action: option2),
                            .cancel(cancel)
                        ]
            )
        }



    }

    func option1() {
        tabData.reset()
        // ...
    }

    func option2() {
        tabData.reset()
        // ...
    }

    func cancel() {
        tabData.reset()
    }
}

struct TabbedView_Previews: PreviewProvider {
    static var previews: some View {
        TabbedView()
    }
}

Similar concept, just uses the power of SwiftUI and Combine.

You could introduce new @State property for storing old tag of presented tab. And perform the next method for each of your tabs .onAppear { self.oldSelectedItem = self.selectedItem } except the middle tab. The middle tab will be responsible for showing the action sheet and its method will look the following:

.onAppear { 
self.shouldShowActionSheet.toggle() 
self.selectedItem = self.oldSelectedItem
}

Working example:

import SwiftUI

struct ContentView: View {
    @State private var selectedItem = 1
    @State private var shouldShowActionSheet = false
    @State private var oldSelectedItem = 1

    var body: some View {
        TabView (selection: $selectedItem) {
            Text("Home")
                .tabItem { Image(systemName: "house") }
                .tag(1)
                .onAppear { self.oldSelectedItem = self.selectedItem }
            Text("Search")
                .tabItem { Image(systemName: "magnifyingglass") }
                .tag(2)
                .onAppear { self.oldSelectedItem = self.selectedItem }
            Text("Add")
                .tabItem { Image(systemName: "plus.circle") }
                .tag(3)
                .onAppear {
                    self.shouldShowActionSheet.toggle()
                    self.selectedItem = self.oldSelectedItem
                }
            Text("Heart")
                .tabItem { Image(systemName: "heart") }
                .tag(4)
                .onAppear { self.oldSelectedItem = self.selectedItem }
            Text("Profile")
                .tabItem { Image(systemName: "person.crop.circle") }
                .tag(5)
                .onAppear { self.oldSelectedItem = self.selectedItem }
        }
        .actionSheet(isPresented: $shouldShowActionSheet) { ActionSheet(title: Text("Title"), message: Text("Message"), buttons: [.default(Text("Option 1"), action: option1), .default(Text("Option 2"), action: option2) , .cancel()]) }
    }

    func option1() {
        // do logic 1
    }

    func option2() {
        // do logic 2
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Previous answers did not help me so I'm pasting my complete solution.


import SwiftUI
import UIKit

enum Tab {
    case map
    case recorded
}


@main
struct MyApp: App {

    @State private var selectedTab: Tab = .map
    @Environment(\.scenePhase) private var phase
    var body: some Scene {
        WindowGroup {
            VStack {
                switch selectedTab {
                case .map:
                    NavigationView {
                        FirstView()
                    }
                case .recorded:
                    NavigationView {
                        SecondView()
                    }
                }
                CustomTabView(selectedTab: $selectedTab)
                    .frame(height: 50)
            }
        }
    }
}


struct FirstView: View {
    var body: some View {
        Color(.systemGray6)
            .ignoresSafeArea()
            .navigationTitle("First view")
    }
}

struct SecondView: View {
    var body: some View {
        Color(.systemGray6)
            .ignoresSafeArea()
            .navigationTitle("second view")
    }
}

struct CustomTabView: View {
    @Binding var selectedTab: Tab
    var body: some View {
        HStack {
            Spacer()
            Button {
                selectedTab = .map
            } label: {
                VStack {
                    Image(systemName: "map")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 25, height: 25)
                    Text("Map")
                        .font(.caption2)
                }
                .foregroundColor(selectedTab == .map ? .blue : .primary)
            }
            .frame(width: 60, height: 50)
            Spacer()
            Button {
                
            } label: {
                ZStack {
                    Circle()
                        .foregroundColor(.secondary)
                        .frame(width: 80, height: 80)
                        .shadow(radius: 2)
                    Image(systemName: "plus.circle.fill")
                        .resizable()
                        .foregroundColor(.primary)
                        .frame(width: 72, height: 72)
                }
                .offset(y: -2)
            }
            Spacer()
            
            Button {
                selectedTab = .recorded
            } label: {
                VStack {
                    Image(systemName: "chart.bar")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 25, height: 25)
                    Text("Recorded")
                        .font(.caption2)
                }
                .foregroundColor(selectedTab == .recorded ? .blue : .primary)
            }
            .frame(width: 60, height: 50)
            Spacer()
        }
    }
}

Related