0

I've already asked here how to get rid of an out of range index error when using page styled TabView when I get data from API. There I was advised to do this:

Add an if rockets.isEmpty clause around the entire TabView and display something else if the array is empty (like a loading indicator)

// MARK: - API
class InfoApi {
    func getRockets(completion: @escaping ([RocketInfo]) -> ()) {
        guard let url = URL(string: "https://api.spacexdata.com/v4/rockets") else {
            return
        }
        
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            do {
                let rocketsInfo = try JSONDecoder().decode([RocketInfo].self, from: data!)
                DispatchQueue.main.async {
                    completion(rocketsInfo)
                }
            } catch {
                print(error.localizedDescription)
            }
        }
        .resume()
    }
}

// MARK: - ROCKET MODEL
struct RocketInfo: Codable, Identifiable {
    let id = UUID()
    let name: String
    let country: String
}

// MARK: - CONTENT VIEW
struct ContentView: View {
    @State var rockets: [RocketInfo] = []
    
    var body: some View {
        Group {
            if rockets.isEmpty {
                ProgressView()
            } else {
                NavigationView {
                    TabView {
                        ForEach(rockets) { rocket in
                            ScrollView(.vertical, showsIndicators: false) {
                                VStack(alignment: .center) {
                                    Image(systemName: "globe")
                                        .renderingMode(.original)
                                        .resizable()
                                        .scaledToFit()
                                        .frame(height: 190, alignment: .center)
                                    
                                    ZStack {
                                        RoundedRectangle(cornerRadius: 32)
                                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 865, maxHeight: .infinity, alignment: .center)
                                            .foregroundColor(Color.gray)
                                        
                                        VStack(spacing: 40) {
                                            HStack {
                                                Text("Name")
                                                    .font(.title)
                                                Spacer()
                                                Text(rocket.name)
                                                    .font(.title)
                                            }
                                        } //: VSTACK
                                        .padding(.horizontal)
                                    } //: ZSTACK
                                } //: VSTACK
                                .navigationBarTitleDisplayMode(.inline)
                                .navigationBarHidden(true)
                            } //: SCROLL
                        } //: LOOP
                    } //: TAB
                    .tabViewStyle(.page)
                    .edgesIgnoringSafeArea(.vertical)
                } //: NAVIGATION
            }
        } //: GROUP
        .onAppear {
            InfoApi().getRockets { rockets in
                self.rockets = rockets
            }
        }
        .edgesIgnoringSafeArea(.vertical)
    }
}

// MARK: - PREVIEW
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

It helped, but there was another problem. To wrap TabView in an if-else clause, I had to add some other View outside it (for example, NavigationView) and call the onAppear method for this View to make everything work. There was a problem with the fact that I cannot completely remove the white safe area at the top of NavigationView.

That's what it looks like in the Simulator

If I wrap TabView in HStack instead of NavigationView, then when scrolling ScrollView to the end, I see a white bar at the very bottom, which I also don't know how to get rid of.

This white space at the bottom of the ScrollView I was talking about

The View that I want to create

bowtie
  • 29
  • 6
  • I've left a number of possible solutions -- none are full-code since what you've included isn't a [mre], but it should be enough to get you started. – jnpdx Apr 08 '22 at 19:56

2 Answers2

1

There are a number of potential solutions to this. All involve getting rid of the NavigationView since it seems that your only use for it was that you needed some sort of wrapper view.

Solution #1: Move your isEmpty check inside the TabView

var body: some View {
  TabView {
    if rockets.isEmpty {
      //ProgressView
    } else {
      //your tabs
    }
  }.onAppear {
    //API call
  }
}

Solution #2: Instead of NavigationView, wrap your content in a Group:

var body: some View {
  Group {
    if !rockets.isEmpty {
      TabView { /*...*/ }
    }
  }.onAppear {
    //API call
  }
}

Solution #3: call onAppear on the progress view when it appears

if rockets.isEmpty {
  ProgressView()
   .onAppear {
     //API call
   }
} else {
  TabView {
    //tab content
  }
}

Edit: complete example with mocked objects/API

struct TabBarView: View {
    @State var rockets: [RocketInfo] = []
    
    var body: some View {
        Group {
            if rockets.isEmpty {
                ProgressView()
            } else {
                NavigationView {
                    TabView {
                        
                        ForEach(rockets) { rocket in
                            Text(rocket.title)
                        }
                        
                    }
                    .tabViewStyle(.page)
                }
            }
        }
        .onAppear {
            InfoApi().getRockets { rockets in
                self.rockets = rockets
            }
        }
    }
}
jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • thank you for your answers) Each of your solutions work until I set a paged style to my TabView. – bowtie Apr 08 '22 at 22:06
  • And what happens then? – jnpdx Apr 08 '22 at 22:08
  • For example, I use your solution #3. It works, I can switch between tabs, but when I add **.tabViewStyle(.page)** to my TabView, I see the ProgressView() screen all the time and nothing else happens. – bowtie Apr 08 '22 at 22:13
  • That seems like it would be an unusual and unlikely situation. Are you sure you're putting `.tabViewStyle(.page)` in the right place (ie attached to the `TabView` closure)? If so, can you update your original question with the code you've tried (ideally with everything unnecessary removed)? – jnpdx Apr 08 '22 at 22:18
  • Sure, just did it. – bowtie Apr 08 '22 at 22:30
  • Can you post your attempt at solution #1? – jnpdx Apr 08 '22 at 22:32
  • Did it. Have the same problem. I see the ProgressView() screen all the time. – bowtie Apr 08 '22 at 22:40
  • Unfortunately, I can't run your code because it's missing too many types, but I did manage to see the same issue with solution #1 when I created my own minimal example. See the update to my post -- it's solution 3 and my self contained minimal example proves it works. – jnpdx Apr 08 '22 at 22:45
  • Yes, you're right. I tried it and it worked. But I realised why it didn't work with my code – NavigationView. If you try to add NavigationView inside the TabView, you'll face the error I was talking about. Unfortunately, in my project I have to use NavigationView, because I have NavigationLink button that opens another View. Sadly, the very first code where I used NavigationView instead of Group, like you did in your solution, remains the only solution close to what I have to do to pass my project and try to get an internship. – bowtie Apr 08 '22 at 23:18
  • I've updated my example to include a `NavigationView`. Still works for me (note that I put it *outside* of the `TabView` -- putting in *inside* like you have would be a very unusual and uncommon design decision on iOS -- I've never seen an app do that). – jnpdx Apr 08 '22 at 23:21
  • By the way, this sort of confusion is why you should really try to include a [mre] -- otherwise, you run the risk of a solution not working for you because your needs are different than what the answerer can infer from incomplete code. – jnpdx Apr 08 '22 at 23:22
  • The updated code in your question works fine for me. If it doesn't for you, can you list what device/simulator and iOS version? – jnpdx Apr 09 '22 at 00:03
  • I updated my code to make it a minimal reproducible example. I tried to use NavigationView as you showed and the result looks the same as when I used NavigationView as a wrapper for if-else statement. I mean, the white stripe on top is still in place, and I don't understand how to get rid of it. At the end of my question, I added a link to the image of how I want the screen of my application to look like, that is, the picture in the View would be touched the top of the screen. – bowtie Apr 09 '22 at 00:38
  • Yeah, without both a `TabView` *and* `NavigationView` I agree that it seems challenging to get that top image to ignore the safe area – jnpdx Apr 09 '22 at 01:37
0

Unfortunately, it turned out that it is quite impossible to get rid of navbar if you use both TabBar and NavigationView in one View.

bowtie
  • 29
  • 6