I'm trying to show a view with a loader in full screen. I want also to overlay the TabBar, but I don't know how to do it. Let me show my code.
This is ProgressViewModifier
.
// MARK: - View - Extension
extension View {
/// Show a loader binded to `isShowing` parameter.
/// - Parameters:
/// - isShowing: `Bool` value to indicate if the loader is to be shown or not.
/// - text: An optional text to show below the spinning circle.
/// - color: The color of the spinning circle.
/// - Returns: The loader view.
func progressView(
isShowing: Binding <Bool>,
backgroundColor: Color = .black,
dimBackground: Bool = false,
text : String? = nil,
loaderColor : Color = .white,
scale: Float = 1,
blur: Bool = false) -> some View {
self.modifier(ProgressViewModifier(
isShowing: isShowing,
backgroundColor: backgroundColor,
dimBackground: dimBackground,
text: text,
loaderColor: loaderColor,
scale: scale,
blur: blur)
)
}
}
// MARK: - ProgressViewModifier
struct ProgressViewModifier : ViewModifier {
@Binding var isShowing : Bool
var backgroundColor: Color
var dimBackground: Bool
var text : String?
var loaderColor : Color
var scale: Float
var blur: Bool
func body(content: Content) -> some View {
ZStack { content
if isShowing {
withAnimation {
showProgressView()
}
}
}
}
}
// MARK: - Private methods
extension ProgressViewModifier {
private func showProgressView() -> some View {
ZStack {
Rectangle()
.fill(backgroundColor.opacity(0.7))
.ignoresSafeArea()
.background(.ultraThinMaterial)
VStack (spacing : 20) {
if isShowing {
ProgressView()
.tint(loaderColor)
.scaleEffect(CGFloat(scale))
if text != nil {
Text(text!)
.foregroundColor(.black)
.font(.headline)
}
}
}
.background(.clear)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
This is the RootTabView
, the one containing the TabBar.
struct RootTabView: View {
var body: some View {
TabView {
AddEverydayExpense()
.tabItem {
Label("First", systemImage: "1.circle")
}
AddInvestment()
.tabItem {
Label("Second", systemImage: "2.circle")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
RootTabView()
}
}
This is my view.
struct AddEverydayExpense: View {
@ObservedObject private var model = AddEverydayExpenseVM()
@State private var description: String = ""
@State private var cost: String = ""
@State private var date: Date = Date()
@State private var essential: Bool = false
@State private var month: Month?
@State private var category: Category?
private var isButtonDisabled: Bool {
return description.isEmpty ||
cost.isEmpty ||
month == nil ||
category == nil
}
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("", text: $description, prompt: Text("Descrizione"))
TextField("", text: $cost, prompt: Text("10€"))
.keyboardType(.numbersAndPunctuation)
DatePicker(date.string(withFormat: "EEEE"), selection: $date)
HStack {
CheckboxView(checked: $essential)
Text("È considerata una spesa essenziale?")
}
.onTapGesture {
essential.toggle()
}
}
Section {
Picker(month?.name ?? "Mese di riferimento", selection: $month) {
ForEach(model.months) { month in
Text(month.name).tag(month as? Month)
}
}
Picker(category?.name ?? "Categoria", selection: $category) {
ForEach(model.categories) { category in
Text(category.name).tag(category as? Category)
}
}
}
Section {
Button("Invia".uppercased()) { print("Button") }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.font(.headline)
.listRowBackground(isButtonDisabled ? Color.gray.opacity(0.5) : Color.blue)
.foregroundColor(Color.white.opacity(isButtonDisabled ? 0.5 : 1))
.disabled(!isButtonDisabled)
}
}
Spacer()
}
.navigationTitle("Aggiungi Spesa")
}
.progressView(isShowing: $model.isFetching, blur: true)
}
}
As you can see, there is the line .progressView(isShowing: $model.isFetching, blur: true)
that does the magic. The problem is that the loader is only shown on the current view, but not on the tab. .
How can I achieve the result?
CodePudding user response:
If you want the progress view to cover the entire view (including the tab bar), it has to be in the view hierarchy at or above the TabBar
. Right now, it's below the TabBar
in the child views.
Because the state will need to be passed up to the parent (the owner of the TabBar
), you'll need some sort of state management that you can pass down to the children. This could mean just passing a Binding
to a @State
. I've chosen to show how to achieve this with an ObservableObject
passed down the hierarchy using an @EnvironmentObject
so that you don't have to explicitly pass the dependency.
class ProgressManager : ObservableObject {
@Published var inProgress = false
}
struct ContentView : View {
@StateObject private var progressManager = ProgressManager()
var body: some View {
TabView {
AddEverydayExpense()
.tabItem {
Label("First", systemImage: "1.circle")
}
AddInvestment()
.tabItem {
Label("Second", systemImage: "2.circle")
}
}
.environmentObject(progressManager)
.progressView(isShowing: $progressManager.inProgress) //<-- Note that this is outside of the `TabBar`
}
}
struct AddEverydayExpense : View {
@EnvironmentObject private var progressManager : ProgressManager
var body: some View {
Button("Progress") {
progressManager.inProgress = true
}
}
}