SwiftUI - Fullscreen nested TabViews without visual glitch - swiftui

I'm trying to get nested TabViews working in SwiftUI to achieve an onboarding flow prior to the main tabbed app screen but am running in to a non-obvious visual glitch.
I'd like the onboarding portion to be full-screen, ignoring safe areas, but the nested, tabbed, main app screen to honour safe areas.
The code below shows the glitch off: it's full-screen for the first couple of screens, but then - only on initial display - the tabs are below the safe area. If I swipe back and forwards again the tabs do then honour the safe area.
I've tried various combinations of .edgesIgnoringSafeArea(), .frame(), .offset(), as well as trying to make use of the values that GeometryReader provides. Removing the one .edgesIgnoringSafeArea() that is there gives me the (expected) bars below and above my tabs. Due to the swipe transitions I need the tabs to be full-screen.
I've also tried using .overlays to achieve the desired appearance and while this does work it requires more complex state manipulation and just feels wrong.
Finally, I've played around with nesting NavigationViews and TabViews and, as reported elsewhere, that rarely ends well.
I'd be grateful if someone could explain why I'm seeing this glitch (i.e. the gap in my understanding of SwiftUI's rendering/lifecycle, and why the nested tab bar changes position the second time it appears), and if there's a canonical way of achieving what I want. TIA.
// A simple tab view that can self-advance to the next tab.
// Included to simplify the main ContentView
struct SimpleTab: View {
#Binding var tab: Int
let label: String
let backgroundColour: Color
let withNext: Bool
var body: some View {
VStack {
Spacer()
Text(label)
.frame(maxWidth: .infinity, alignment: .center)
if withNext {
Button {
withAnimation {
tab += 1
}
} label: {
Text("Next ->")
}
}
Spacer()
}
.background(backgroundColour
.opacity(0.5))
}
}
struct ContentView: View {
#State var tab = 0
#State var nestedTab = 0
var body: some View {
VStack {
TabView(selection: $tab) {
SimpleTab(tab: $tab, label: "Onboarding 1", backgroundColour: .red, withNext: true)
.tag(0)
SimpleTab(tab: $tab, label: "Onboarding 2", backgroundColour: .green, withNext: true)
.tag(1)
TabView(selection: $nestedTab) {
SimpleTab(tab: $nestedTab, label: "Nested 0", backgroundColour: .blue, withNext: false)
.tabItem {
Label("Nested 0", systemImage: "0.circle")
}
.tag(0)
SimpleTab(tab: $nestedTab, label: "Nested 1", backgroundColour: .blue, withNext: false)
.tabItem {
Label("Nested 1", systemImage: "1.circle")
}
.tag(1)
}
.tabViewStyle(DefaultTabViewStyle())
.tag(2)
}
.edgesIgnoringSafeArea(.all)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.transition(.slide)
}
}
}

Related

SwiftUI bottom sheet like note app's format tab

Currently, I have to implement bottom sheet. And I found the very example of my need.
Is this component system component of swift or swiftui?
OR do I need to implement on my own?
PLEASE LET ME KNOW IF U HAVE SOME INFOS! XD
At first I implement with ZStack, drag gesture but the animation is not what I expected.
I need Information about whether there is component like .sheet(isPresented: Bool, content: View) of the modal like above image.
As our friend said before, it is a sheet. Inside the sheet you can either define a new view or call any of your views. Then you have to use the modifier .presentationDetents which receive a Set of PresentationDetents to say where the view has to stop when appearing on the screen. This modifier must be apply to the content of the sheet and not directly to the sheet.
struct ContentView: View {
#State var isSheetShown = false
var body: some View {
VStack {
Button("Show view"){
isSheetShown = true
}
}.sheet(isPresented: $isSheetShown, content: {
StackOfButtons()
.presentationDetents([.medium])
})
.padding()
}
}
Finally, to create that stack type of buttons you can put them all in a HStack, give them individually some padding, set a little of spacing in the HStack and the round the corners of the stack. Something like this:
struct StackOfButtons: View {
var body: some View {
HStack(spacing: 2){
Button {
print("Hola que ase")
} label: {
Image(systemName: "list.bullet")
.padding()
.background(.thinMaterial)
.foregroundColor(.black)
}
Button {
print("Hola que ase")
} label: {
Image(systemName: "list.dash")
.padding()
.background(.thinMaterial)
.foregroundColor(.black)
}
Button {
print("Hola que ase")
} label: {
Image(systemName: "list.number")
.padding()
.background(.thinMaterial)
.foregroundColor(.black)
}
}.cornerRadius(10)
}
}
Result

What is the height of the SwiftUI TabView index with PageTabViewStyle?

I am writing an application using Xcode 14.0.1, and testing on an iPhone 12 mini running iOS 16.0. The current project build is for iOS 14.7. Here is my TabView...
TabView {
ByEyeView()
.tabItem { Label("ByEye", systemImage: "eye") }
ChartView()
.tabItem { Label("Chart", systemImage: "square.grid.4x3.fill") }
ListView()
.tabItem { Label("List", systemImage: "list.bullet") }
EditView()
.tabItem { Label("Edit", systemImage: "square.and.pencil") }
CameraView()
.tabItem { Label("Camera", systemImage: "camera") }
SettingsView()
.tabItem { Label("Settings", systemImage: "gear") }
}
//.labelStyle(TitleAndIconLabelStyle())
//.padding(8)
//.ignoresSafeArea(edges: .bottom)
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
This gives a capsule at the bottom of the page with a small version of the icon and no text. I cannot enlarge the Label with .frame, and the .labelStyle() setting is ignored. I take it this is part of PageTabViewStyle() - the index is supposed to be small, and I can probably not change that. But the index sits over the view content, so I need its height if I am to keep buttons clear of it.
Can I find out the index height? Or does PageTabViewStyle assume that the index is small and you should work around it?
The commented-out .ignoreSafeArea() moves the index down while the page remains the same. The .padding() keeps it a bit clear of the bar at the bottom. This is what I am working with for now. This is foul: it will not work with other devices or screen orientations.
The bigger picture:
I have six entries. That does not fit in the default view, so I get a ... More tag which leads to an extra menu. Ugly. I like the PageTabViewStyle method of scrolling, but I want an index with a known height - preferably one that uses the full labels and sits at the bottom of the TabView layout, under the tabbed views.
This was one of those 'Magic Eye' things when you stare at it for days and it makes no sense, and suddenly everything rearranges itself...
Maybe TabPageViewStyle was intended to be for pages where there is no visible index, or overlaying a small index does no harm. This would work for browsing images. All the cunning has gone into making the index view unobtrusive. If you need to know how big it is, then perhaps TabPageViewStyle is not what you want.
What I said I wanted was actually a Scrollable horizontal list, followed by the currently selected list. Something like this...
let tabW = CGFloat(UIScreen.main.bounds.width / 5.0)
enum Page {
case ByEye
case Chart
case List
case Edit
case Camera
case Settings
}
#State private var page = Page.ByEye
func pageButton(_ select: Page, _ icon: String, _ title: String) -> some View {
return Button {
page = select
} label: {
VStack {
Image(systemName: icon)
Text(title)
} .frame(width: tabW)
} .foregroundColor( page == select ? Color.white : Color.gray )
}
var body: some View {
VStack() {
ScrollView(.horizontal) {
HStack() {
pageButton(Page.ByEye, "eye", "ByEye")
pageButton(Page.Chart, "square.grid.4x3.fill", "Chart")
pageButton(Page.List, "list.bullet", "List")
pageButton(Page.Edit, "square.and.pencil", "Edit")
pageButton(Page.Camera, "camera", "Camera")
pageButton(Page.Settings, "gear", "Settings")
}
}
switch page {
case .ByEye:
ByEyeView()
case .Chart:
ChartView()
case .List:
ListView()
case .Edit:
EditView()
case .Camera:
CameraView()
case .Settings:
SettingsView()
}
Spacer()
}
It is not much longer than my original version. It is not as pretty is it could be - when you overflow the title bar you get half an icon, where an ellipsis would be better. But I can fix that later.
The other answer is to write your own index table....
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack() {
pageButton(Page.EyeTest, "eyeglasses", "EyeTest", proxy)
pageButton(Page.Tone, "pause.rectangle", "Tone", proxy)
pageButton(Page.Chart, "square.grid.4x3.fill", "Chart", proxy)
pageButton(Page.ByEye, "eye", "ByEye", proxy)
pageButton(Page.List, "list.bullet", "List", proxy)
pageButton(Page.Camera, "camera", "Camera", proxy)
pageButton(Page.Settings, "gear", "Settings", proxy)
}
}
.onAppear { proxy.scrollTo(page, anchor: .center) }
.onChange(of: page) { page in
withAnimation {
proxy.scrollTo(page, anchor: .center)
}
}
}
This particular one has button-sized icons and text. 'page' is an enum, and also the tags of the TabView. If you stick it in the layout, you can make it fit around the TabView. You will want to hide the TabView index, which you can do with...
.tabViewStyle(PageTabViewStyle(indexDisplayMode:.never))

SwiftUI Is there any built in view that kind of slides in from the side and takes up 3/4 of the screen?

This is probably a custom view but in the Reddit app there's a toolbar and the top left button(3 lines) opens this kind of view from the side that moves the current view to the right so you can only see about 25% of it and a new view that takes up about 75% of the screen slides in. Is there anything like this built into SwiftUI and if there isn't how would I go about implementing something like this?
This is my custom side bar behave similarly to what you just mentioned, you can try it. (Images and Code are below)
Before click:
After clicked:
struct ContentView: View {
#State var isClicked = false
var body: some View {
HStack {
Rectangle()
.fill(.orange)
.frame(width: isClicked ? UIScreen.main.bounds.width * 0.75 : 0)
VStack {
HStack {
Button {
withAnimation {
isClicked.toggle()
}
} label: {
Image(systemName: "menucard.fill")
.padding(.leading)
}
Spacer()
}
Spacer()
}
}
}
}

Navigationlink question in Xcode 13.1 -- Blanks instead of links?

Sorry for the newbie question, but I'm stuck on why Navigationlink produces no links at all. Xcode compiles, but there's a blank for where the links to the new views are. This particular view is View 3 from ContentView, so the structure is ContentView -> View 2 -> View 3 (trying to link to View 4).
struct MidnightView: View {
var hourItem: HoursItems
#State var showPreferencesView = false
#State var chosenVersion: Int = 0
#State var isPsalmsExpanded: Bool = false
#State var showLXX: Bool = false
var body: some View {
ScrollView (.vertical) {
VStack (alignment: .center) {
Group {
Text (hourItem.hourDescription)
.font(.headline)
Text ("Introduction to the \(hourItem.hourName)")
.font(.headline)
.bold()
.frame(maxWidth: .infinity, alignment: .center)
ForEach (tenthenou, id: \.self) {
Text ("\($0)")
Text ("\(doxasi)")
.italic()
}
}
.padding()
NavigationView {
List {
ForEach (midnightHours, id:\.id) {watch in
NavigationLink ("The \(watch.watchName)", destination: MidnightWatchView (midnightItem: watch, chosenVersion: self.chosenVersion, isPsalmsExpanded: self.isPsalmsExpanded, showLXX: self.showLXX))
}
}
}
Group {
Text ("Absolution of the \(hourItem.hourName)")
.font(.headline)
Text (absolutionTexts[(hourItem.hourName)] ?? " ")
Divider()
Text ("Conclusion of Every Hour")
.font(.headline)
Text (hourConclusion)
Divider()
Text (ourFather)
}
.padding()
}
}
.navigationBarTitle ("The Midnight Hour", displayMode: .automatic)
.navigationBarItems (trailing: Button (action: {self.showPreferencesView.toggle()}) {Text (psalmVersions[chosenVersion])}
.sheet(isPresented: $showPreferencesView) {PreferencesView(showPreferencesView: self.$showPreferencesView, chosenVersion: self.$chosenVersion, isPsalmsExpanded: self.$isPsalmsExpanded, showLXX: self.$showLXX)})
}
}
The cause of the NavigationLink not appearing is probably due to the fact that you're including a List within a ScrollView. Because List is a scrolling component as well, the sizing gets messed up and the List ends up with a height of 0. Remove the List { and corresponding } and your links should appear.
There are a number of other potential issues in your code (as I alluded to in my comment), including a NavigationView in the middle of a ScrollView. I'd remove that as well, as you probably have a NavigationView higher up in your view hierarchy already.

Disable or ignore taps on TabView in swiftui

I have a pretty usual app with a TabView. However, when a particular process is happening in one of the content views, I would like to prevent the user from switching tabs until that process is complete.
If I use the disabled property on the TabView itself (using a #State binding to drive it), then the entire content view seems disabled - taps don't appear to be getting through to buttons on the main view.
Example:
struct FooView: View {
var body: some View {
TabView {
View1().tabItem(...)
View2().tabItem(...)
}
.disabled(someStateVal)
}
}
Obviously, I want the View1 to still allow the user to, you know, do things. When someStateVal is true, the entire View1 doesn't respond.
Is there a way to prevent changing tabs based on someStateVal?
Thanks!
I could not find a way to individually disable a tabItem, so here is
an example idea until someone comes up with more principled solution.
The trick is to cover the tab bar with a clear rectangle to capture the taps.
struct ContentView: View {
#State var isBusy = false
var body: some View {
ZStack {
TabView {
TestView(isBusy: $isBusy)
.tabItem {Image(systemName: "globe")}
Text("textview 2")
.tabItem {Image(systemName: "info.circle")}
Text("textview 3")
.tabItem {Image(systemName: "gearshape")}
}
VStack {
Spacer()
if isBusy {
Rectangle()
.fill(Color.white.opacity(0.001))
.frame(width: .infinity, height: 50)
}
}
}
}
}
struct TestView: View {
#Binding var isBusy: Bool
var body: some View {
VStack {
Text("TestView")
Button(action: {
isBusy.toggle()
}) {
Text("Busy \(String(isBusy))").frame(width: 170, height: 70)
}
}
}
}
I use another trick. Just hide the tab image.
struct FooView: View {
var body: some View {
TabView {
View1().tabItem{Image(systemName: someStateVal ? "": "globe")}
View2().tabItem{Image(systemName: someStateVal ? "": "gearshape")}
}
}
}