How to show alert on TabBar in SwiftUI?

45 Views Asked by At

I have a custom TabBar view and there is one tab (MyGamesView) on which I have custom cells. There is one button in this cell. By clicking on that button, I have to display an alert and have to call an API. But my issue is If i present an alert from that tab (MyGamesView) then this is how it looks: enter image description here

If I present my alert on MainTabBarView then it looks nice. But I have to pass selected cell's id in my API Call. and i'm not able to pass my selected cell's id onto MainTabBarView. but presenting an alert from MyGamesView doesn't look nice.

How do I present an alert from MyGamesView? I have tried .zIndex(1) on alert but it is not working. If anyone could help me, It would be highly appreciated!

Here is my MainTabBarView:

struct MainTabBarView: View {
    // MARK: - HIDING NATIVE TAB BAR
    init(){
        UITabBar.appearance().isHidden = true
    }
    
    // MARK: - Variables
   @Environment(\.rootPresentationMode) private var rootPresentationMode: Binding<RootPresentationMode>
   @EnvironmentObject private var alertManager: AlertManager
   @State private var tabBarItems = [
       TabBarItems(imageIcon: "home_icon", selectedImageIcon: "homeSelected_icon", title: "Home"),
       TabBarItems(imageIcon: "calendar_icon", selectedImageIcon: "calendarSelected_icon", title: "My Games"),
       TabBarItems(imageIcon: "notification_icon", selectedImageIcon: "notificationSelected_icon", title: "Notification"),
       TabBarItems(imageIcon: "setting_icon", selectedImageIcon: "settingSelected_icon", title: "Setting"),
   ]
   @State private var selectedTab = 0
   @State private var goToCreateMatch: Bool = false
   @State private var isLoading = false
   @State private var suggestHomeCourt: String = ""
   @StateObject var bannerVM = BannerViewModel()
       
    var body: some View {
        NavigationView {
            ZStack(alignment: .bottom) {
                TabView(selection: $selectedTab) {
                    HomeView()
                        .tag(0)
                        .environmentObject(alertManager)
                    MyGamesView()
                        .tag(1)
                        .environmentObject(alertManager)
                    NotificationView()
                        .tag(2)
                    SettingView()
                        .tag(3)
                        .environmentObject(alertManager)
                }
                .zIndex(0)
                
                // MARK: - Custom Alert for Delete Account
                if self.alertManager.presentAlertForDeleteAccount {
                    VStack {
                        CustomAlertView(alertTitle: "Delete Account", showMsg: true, alertMessage: "Are you sure you want to delete the account? You will lose all records and you cannot restore it again later.", nonFilledButtonTitle: "Cancel", filledButtonTitle: "Delete", nonFilledButtonAction: {
                             // MARK: - Button Cancel Action (Delete Account)
                            withAnimation {
                                self.alertManager.presentAlertForDeleteAccount.toggle()
                            }
                        }, filledButtonAction: {
                            // MARK: - Button Delete Action (Delete Account)
                            withAnimation {
                                self.deleteAccountApiCall()
                            }
                        }, presentAlert: $alertManager.presentAlertForDeleteAccount)
                    }
                    .zIndex(1)
                }
                
                // MARK: - Custom Alert for Logout
                if self.alertManager.presentAlertForLogOut {
                    VStack {
                        CustomAlertView(alertTitle: "Are you sure you want to Log Out?", nonFilledButtonTitle: "Cancel", filledButtonTitle: "Log Out", nonFilledButtonAction: {
                            // MARK: - Button Cancel Action (Logout)
                            withAnimation {
                                self.alertManager.presentAlertForLogOut.toggle()
                            }
                        }, filledButtonAction: {
                            // MARK: - Button Logout Action (Logout)
                            withAnimation {
                                self.logOutApiCall()
                            }
                        }, presentAlert: $alertManager.presentAlertForLogOut)
                    }
                    .zIndex(1)
                }
                
                GeometryReader { proxy in
                    VStack {
                        Spacer()
                        HStack(alignment: .bottom, spacing: 20) {
                            Spacer()
                            ForEach(0..<2) { index in
                                Button{
                                    selectedTab = index
                                } label: {
                                    CustomTabItem(imageName: tabBarItems[index].imageIcon, selectedImageName: tabBarItems[index].selectedImageIcon, title: tabBarItems[index].title, isActive: (selectedTab == index))
                                }
                            }
                           Spacer()
                           Spacer()
                            ForEach(2..<self.tabBarItems.count) { index in
                                Button{
                                    selectedTab = index
                                } label: {
                                    CustomTabItem(imageName: tabBarItems[index].imageIcon, selectedImageName: tabBarItems[index].selectedImageIcon, title: tabBarItems[index].title, isActive: (selectedTab == index))
                                }
                            }
                            Spacer()
                        }
                        .font(.footnote)
                        .padding(.top, 42)
                        .overlay(alignment: .top) {
                            VStack(spacing: -3) {
                                 // MARK: - Button Create
                                Button {
                                    self.goToCreateMatch = true
                                } label: {
                                    Image("plus_icon")
                                        .resizable()
                                        .scaledToFit()
                                        .padding()
                                        .frame(width: 60, height: 60)
                                        .foregroundStyle(.white)
                                        .background {
                                            Circle()
                                                .fill(Color.custom64B054Color)
                                                .shadow(radius: 3)
                                        }
                                }
                                .padding(9)
                                Text("Create")
                                    .latoRegularFont(size: 12)
                                    .foregroundColor(Color.black.opacity(0.6))
                            }
                        }
                        .padding(.bottom, max(32, proxy.safeAreaInsets.bottom))
                        .background {
                            CustomTabBarShape()
                                .fill(.white)
                                .shadow(color: Color.black.opacity(0.15), radius: 5, x: 0, y: -1)
                        }
                    }
                    .ignoresSafeArea(edges: .bottom)
                }
                
                // MARK: - Navigate to Create Match
                NavigationLink("", destination: CreateMatchView().navigationBarHidden(true).navigationBarBackButtonHidden(true), isActive: $goToCreateMatch)
            }
            .ignoresSafeArea()
            .navigationBarHidden(true)
            .navigationBarBackButtonHidden(true)
        }
    }
    
}

Here is my MyGamesView:

struct MyGamesView: View {
    // MARK: - Variables
    @State private var playerName: String = ""
    @State private var pastShown: Bool = false
    @State private var presentAlert: Bool = false
    @State private var goToGroupChat: Bool = false
    private var items = ["Upcoming", "Past"]
    @GestureState private var dragState = CGSize.zero
    @EnvironmentObject private var alertManager: AlertManager
    @State private var userData = UtilityMethods.getUserData()
    @StateObject private var matchListViewModel = MatchListViewModel()
    @State private var selectedMatch: MatchListModel?
    @State private var isLoading = false
    @StateObject var bannerVM = BannerViewModel()
    @State private var type: String = "2" //type(1=>all, 2=>my games upcoming, 3=>my games past)
    @State private var page: Int = 1
    
    var body: some View {
            ZStack {
                VStack {
                    HStack {
                        VStack(alignment: .leading) {
                            Text("Hello")
                                .robotoRegularFont(size: 14)
                                .foregroundColor(Color.custom64B054Color)
                            Text("\(self.userData?.firstName ?? "") \(self.userData?.lastName ?? "")")
                                .robotoRegularFont(size: 14)
                                .foregroundColor(Color.custom333333Color)
                        }
                        Spacer()
                        Image("appIcon_icon")
                            .resizable()
                            .frame(width: 32, height: 32)
                    }
                    .padding(.top, 24)
                    
                    // MARK: - Upcoming and Past Views
                    VStack(spacing: 8) {
                        HStack(alignment: .center, spacing: 0) {
                            Spacer()
                            
                            // MARK: - Button Upcoming
                            ZStack {
                                Button {
                                    self.pastShown = false
                                    self.type = "2"
                                    DispatchQueue.main.async {
                                        self.matchListApiCall(type: self.type)
                                    }
                                } label: {
                                    Text("Upcoming")
                                        .font(.custom(self.pastShown ? "Roboto-Light" : "Roboto-Medium", size: 15))
                                        .foregroundColor(self.pastShown ? .black.opacity(0.7) : .black)
                                }
                            }
                            .frame(width: UIScreen.main.bounds.width / 2 - 18)
                            Spacer()
                            Spacer()
                            
                            // MARK: - Button Past
                            ZStack {
                                Button {
                                    self.pastShown = true
                                    self.type = "3"
                                    DispatchQueue.main.async {
                                        self.matchListApiCall(type: self.type)
                                    }
                                } label: {
                                    Text("Past")
                                        .font(.custom(self.pastShown ? "Roboto-Medium" : "Roboto-Light", size: 15))
                                        .foregroundColor(self.pastShown ? .black : .black.opacity(0.7))
                                }
                            }
                            .frame(width: UIScreen.main.bounds.width / 2 - 18)
                            Spacer()
                        }
                        
                        let upcoming = Capsule()
                            .fill(Color.black.opacity(self.pastShown ? 0.2 : 1))
                            .frame(maxWidth: .infinity, maxHeight: 1.5)
                        let past = Capsule()
                            .fill(Color.black.opacity(self.pastShown ? 1 : 0.2))
                            .frame(maxWidth: .infinity, maxHeight: 1.5)
                        
                        HStack(alignment: .center, spacing: 0) {
                            upcoming
                            past
                        }
                    }
                    .padding(.top, 8)
                    .padding(.horizontal, -18)
                    
                    ScrollView(.vertical, showsIndicators: false) {
                        // MARK: - Past View
                        if self.pastShown {
                            LazyVStack(spacing: 20) {
                                ForEach(self.matchListViewModel.matchListData.indices, id: \.self) { index in
                                    MyGamesMatchInfoCell(matchListModel: self.matchListViewModel.matchListData[index], isPastMatch: true, messageButtonAction: {
                                        self.goToGroupChat.toggle()
                                    })
                                }
                            }
                            .padding(.bottom, 100)
                        } else {
                            // MARK: - Upcoming View
                            LazyVStack(spacing: 20) {
                                ForEach(self.matchListViewModel.matchListData.indices, id: \.self) { index in
                                    MyGamesMatchInfoCell(matchListModel: self.matchListViewModel.matchListData[index], messageButtonAction: {
                                        self.goToGroupChat.toggle()
                                    }, addGuestButtonAction: {
                                        withAnimation {
                                            self.selectedMatch = self.matchListViewModel.matchListData[index]
                                            self.alertManager.presentAlertForAddGuestOnUpcomingView.toggle()
                                        }
                                    }, leaveMatchButtonAction: {
                                        withAnimation {
                                            self.selectedMatch = self.matchListViewModel.matchListData[index]
                                            self.alertManager.presentAlertForLeaveMatch.toggle()
                                        }
                                    })
                                }
                            }
                            .padding(.bottom, 100)
                        }
                    }
                    .padding(.top)
                    .ignoresSafeArea()
                }
                .padding(.horizontal, 18)
                
                 // MARK: - Swipe Gesture to switch between Upcoming and Past views
                .gesture(DragGesture()
                    .onEnded { value in
                        print("value ",value.translation.width)
                        let direction = Utility.shared.detectDirection(value: value)
                        if direction == .left {
                            print("Upcoming action")
                            self.pastShown = false
                            self.type = "2"
                            DispatchQueue.main.async {
                                self.matchListApiCall(type: self.type)
                            }
                        }
                        if direction == .right {
                            print("Past action")
                            self.pastShown = true
                            self.type = "3"
                            DispatchQueue.main.async {
                                self.matchListApiCall(type: self.type)
                            }
                        }
                    }
                )
                
                if self.isLoading {
                    ActivityIndicator()
                }
                
                // MARK: - Custom Alert for Add Guest on UpcomingView
                if self.alertManager.presentAlertForAddGuestOnUpcomingView {
                    VStack {
                        CustomAlertView(alertTitle: "Are you sure you want to add a Guest in this match?", nonFilledButtonTitle: "No", filledButtonTitle: "Yes", nonFilledButtonAction: {
                            // MARK: - Button No Action
                            withAnimation {
                                self.alertManager.presentAlertForAddGuestOnUpcomingView.toggle()
                            }
                        }, filledButtonAction: {
                            // MARK: - Button Yes Action
                            withAnimation {
                                self.joinMatchApiCall(matchId: "\(self.selectedMatch?.id ?? 0)", type: "1", status: "2")
                            }
                        }, presentAlert: $alertManager.presentAlertForAddGuestOnUpcomingView)
                    }
                    .zIndex(1)
                }
                
                // MARK: - Navigate to Group Chat
                NavigationLink("", destination: GroupChatView().navigationBarHidden(true).navigationBarBackButtonHidden(true), isActive: $goToGroupChat)
            }
            .banner(data: $bannerVM.bannerData, show: $bannerVM.showBanner)
        
        // MARK: - onAppear Method
            .onAppear {
                self.userData = UtilityMethods.getUserData()
                self.initialDetails()
            }
        
            .navigationBarHidden(true)
            .navigationBarBackButtonHidden(true)
    }
}

Here is my custom AlertView:

struct CustomAlertView: View {
    // MARK: - Variables
    var alertTitle: String
    var showMsg: Bool? = false
    var isOneButton: Bool? = false
    var alertMessage: String? = ""
    var nonFilledButtonTitle: String
    var filledButtonTitle: String
    var nonFilledButtonAction: (()->Void)?
    var filledButtonAction: (()->Void)?
    @Binding var presentAlert: Bool
    
    var body: some View {
        if self.presentAlert {
            ZStack {
                Color.black.opacity(0.58)
                    .edgesIgnoringSafeArea(.all)
                    .onTapGesture {
                        self.presentAlert = false
                    }
                VStack(alignment: .center) {
                     // MARK: - Alert Title
                    Text(self.alertTitle)
                        .robotoBoldFont(size: 16)
                        .foregroundColor(Color.black.opacity(0.94))
                        .multilineTextAlignment(.center)
                    
                    if self.showMsg ?? false {
                        Text(self.alertMessage ?? "")
                            .robotoBoldFont(size: 12)
                            .foregroundColor(Color.customA5A5A5Color)
                            .multilineTextAlignment(.center)
                            .padding(.top, 1)
                            .padding(.bottom, 8)
                    }
                    
                    VStack {
                        Rectangle()
                            .fill(Color.black.opacity(0.1))
                            .frame(maxWidth: .infinity, maxHeight: 1)
                    }
    //                Divider()
                    .padding(.bottom, 20)
                    HStack(spacing: 24) {
                        Spacer()
                        if self.isOneButton ?? false {
                            // MARK: - Filled Button
                            Button {
                                self.filledButtonAction?()
                            } label: {
                                Text("Okay")
                                    .robotoBoldFont(size: 16)
                                    .foregroundColor(Color.white)
                                    .padding()
                                    .background(
                                        Rectangle()
                                            .foregroundColor(.clear)
                                            .frame(width: 115, height: 44)
                                            .background(Color.custom64B054Color)
                                            .cornerRadius(12)
                                            .overlay(
                                                RoundedRectangle(cornerRadius: 12)
                                                    .stroke(Color.custom64B054Color.opacity(0.5), lineWidth: 1)
                                            )
                                    )
                            }
                            Spacer()
                        } else {
                            // MARK: - Non-Filled Button
                           Button {
                               self.nonFilledButtonAction?()
                           } label: {
                               Text(self.nonFilledButtonTitle)
                                   .robotoBoldFont(size: 16)
                                   .foregroundColor(Color.black.opacity(0.94))
                                   .padding()
                                   .background(
                                       Rectangle()
                                           .foregroundColor(.clear)
                                           .frame(width: 115, height: 44)
                                           .background(.white)
                                           .cornerRadius(12)
                                           .overlay(
                                               RoundedRectangle(cornerRadius: 12)
                                                   .stroke(Color.custom64B054Color.opacity(0.5), lineWidth: 1)
                                           )
                                   )
                           }
                           Spacer()
                           
                           // MARK: - Filled Button
                           Button {
                               self.filledButtonAction?()
                           } label: {
                               Text(self.filledButtonTitle)
                                   .robotoBoldFont(size: 16)
                                   .foregroundColor(Color.white)
                                   .padding()
                                   .background(
                                       Rectangle()
                                           .foregroundColor(.clear)
                                           .frame(width: 115, height: 44)
                                           .background(Color.custom64B054Color)
                                           .cornerRadius(12)
                                           .overlay(
                                               RoundedRectangle(cornerRadius: 12)
                                                   .stroke(Color.custom64B054Color.opacity(0.5), lineWidth: 1)
                                           )
                                   )
                           }
                           Spacer()
                        }
                    }
                    .padding(.bottom, 10)
                    .frame(maxWidth: .infinity)
                }
                .padding()
                .frame(width: UIScreen.main.bounds.width - 36)
                .background(
                    Color.white
                )
                .cornerRadius(20)
            }
            .zIndex(2)
        }
    }
}
1

There are 1 best solutions below

0
Stepan Maksymov On

Okay, here is some suggestions:

  1. Your views are too big! hard to analyse and see something
  2. too much @State values in views.
  3. Stop using zIndexes, it's not needed at all.
  4. Try to avoid indexes in ForEach

now explanations:

  1. you should split your view on blocks with sub vars (recommended) or sub funcs (if you need to pass something for blocks construction) for example add under your body:
private var pastShownView: some View {
    LazyVStack(spacing: 20) {                            
        ForEach(self.matchListViewModel.matchListData.indices, 
                id: \.self) { index in
            MyGamesMatchInfoCell(
                matchListModel: self.matchListViewModel.matchListData[index], 
                isPastMatch: true, 
                messageButtonAction: {
                    self.goToGroupChat.toggle()
                }
            )
        }
    }
    .padding(.bottom, 100)
}

and then in code you will have:

if self.pastShown {
    pastShownView
}

If you return different views in block then use @ViewBuilder before private to avoid error. So you must update your main body so it will in fact contain only block calls and logic of display, this will be much more readable/update-able.

  1. too many states, you better create viewModel that implements ObservableObject for your big views and keep all states there as @Published and use this view model in view as @StateObject and contact your values from viewModel.someVar or bind state to render as $viewModel.someVar, view should be as light as possible, so remove all that vars from it.

  2. zIndex is not needed, order of view in ZStack is enough, if views in order 1,2,3 then 3 will be the top most, 2 below it, 1 below 3 and 2

  3. Avoid Indexes, coz you can get unexpected crash in some circumstances and read about how SwiftUI updates view in foreach, read what is Identifiable and how you can control it providing custom id to models of collection, you can make it super smooth if you do it right.

and the solution for your alert is to put is at very bottom of the ZStack, + to make it smooth add .transition modificator with transition animation you want.