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:

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)
}
}
}
Okay, here is some suggestions:
now explanations:
and then in code you will have:
If you return different views in block then use
@ViewBuilderbefore 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.too many states, you better create viewModel that implements
ObservableObjectfor your big views and keep all states there as@Publishedand use this view model in view as @StateObject and contact your values fromviewModel.someVaror bind state to render as$viewModel.someVar, view should be as light as possible, so remove all that vars from it.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
Avoid Indexes, coz you can get unexpected crash in some circumstances and read about how SwiftUI updates view in foreach, read what is
Identifiableand 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.transitionmodificator with transition animation you want.