Can we have a SwiftUI TabView with more than five entries? - swiftui-tabview

I have a set of seven pages. I have made my own scrollable view.
I have a working solution where I have the current page and attach gestures to it like this...
switch page {
case .EyeTest:
EyeTestView(sd: $sd)
.gesture(DragGesture(minimumDistance: swipeMin)
.onEnded { value in
if value.translation.width < -swipeMin { page = Page.Tone }
} )
...`
This worked. It was rather clunky, but I could animate the drags better if I worked at it. However, the tab view worked beautifully with TabView for up to five entries. So, I thought I might be able to span the solution using two tab views. Here is what I did, concluding the custom scrollbar at the top...
VStack() {
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.ByEye, anchor: .center) }
.onChange(of: page) { page in
withAnimation {
proxy.scrollTo(page, anchor: .center)
}
}
}
if (page == Page.EyeTest || page == Page.Tone || page == Page.Chart) {
TabView(selection:$page) {
EyeTestView(sd: $sd).tag(Page.EyeTest)
ToneView(sd: $sd).tag(Page.Tone)
ChartView(sd: $sd).tag(Page.Chart)
ByEyeView(sd: $sd).tag(Page.ByEye)
ListView(sd: $sd).tag(Page.List)
}.tabViewStyle(PageTabViewStyle(indexDisplayMode:.never))
} else {
TabView(selection:$page) {
ChartView(sd: $sd).tag(Page.Chart)
ByEyeView(sd: $sd).tag(Page.ByEye)
ListView(sd: $sd).tag(Page.List)
CameraView(sd: $sd).tag(Page.Camera)
SettingsView(sd: $sd).tag(Page.Settings)
}.tabViewStyle(PageTabViewStyle(indexDisplayMode:.never))
}
}`
However, this does not scroll past the 5th entry.
I would be interested to know why this doesn't work, but I would be happy to replace it with something equivalent that does work. I would also like to know how people handle things like books, which have lost more pages. I feel I ought to make two synchronised scrollable lists.
Yes, maybe it is bad API to have a flat menu with more than five entries. However, some of the ones at the end of the list are rarely used, but need to be discoverable.
I am using Xcode 14.0.1 with iOS 16.0.

Here is a solution that seems to work for me. It is quite close to my original bodge: we have two TabViews, and swap between them depending on the current page so we can always have the neighbours on either side.
I had written my own scrollable index to replace the TabView indexes, but you might make do with the original fittings. I have only been doing Swift for a month, so this may not be the best code, so please post your improvements.
`
enum Page {
case EyeTest
case Tone
case Chart
case ByEye
case List
case Camera
case Settings
}
#AppStorage("menu") private var menu = "Measure"
#State private var page = Page.ByEye
func setMenu(_ page: Page) {
menu = (page == Page.EyeTest || page == Page.Tone || page == Page.Chart ) ? "Test" : "Measure"
}
func pageButton(_ select: Page, _ icon: String, _ title: String, _ proxy: ScrollViewProxy) -> some View {
return Button {
page = select
setMenu(page)
withAnimation {
proxy.scrollTo(select, anchor: .center)
}
} label: {
VStack {
Image(systemName: icon)
Text(title)
}
.frame(width: tabW)
}
.foregroundColor( page == select ? Color.white : Color.gray )
.id(select)
}
var body: some View {
VStack() {
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)
}
}
}
if (menu == "Test") {
TabView(selection:$page) {
EyeTestView(sd: $sd).tag(Page.EyeTest)
ToneView(sd: $sd).tag(Page.Tone)
ChartView(sd: $sd).tag(Page.Chart)
ByEyeView(sd: $sd).tag(Page.ByEye)
SettingsView(sd: $sd).tag(Page.Settings)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode:.never))
.onChange(of: page) { page in
setMenu(page)
}
} else {
TabView(selection:$page) {
ChartView(sd: $sd).tag(Page.Chart)
ByEyeView(sd: $sd).tag(Page.ByEye)
ListView(sd: $sd).tag(Page.List)
CameraView(sd: $sd).tag(Page.Camera)
SettingsView(sd: $sd).tag(Page.Settings)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode:.never))
.onChange(of: page) { page in
setMenu(page)
}
}
`

Related

Firebase Analytics - How to see specific content and not a controller?

I have an app full of stories, and I want to see which stories users are reading.
I already installed Firebase Analytics, but all I'm seeing is this kind of content:
I want to be able to see the specific title of the stories users are reading.
This is the ForEach loop I'm using:
ForEach(modelData.stories.shuffled()) { item in
if item.paid == false {
NavigationLink {
StoryDetails(story: item)
} label: {
GeneralCard(isLocked: false, story: item)
}
}
}
And on StoryDetails I have this code, I would like to see the title of the story inside Firebase Analytics.
VStack (spacing: 0) {
Text(story.title)
.font(AppFont.detailTitle())
.bold()
.multilineTextAlignment(.center)
.foregroundColor(.mainColor)
Text(story.category.rawValue)
.font(AppFont.detailDescription())
.multilineTextAlignment(.center)
.padding()
.foregroundColor(.mainColor)
VStack(alignment: .leading, spacing: 20) {
ForEach(story.text, id: \.self) { item in
VStack {
Text(item)
.font(AppFont.detailText())
}
}
}
//.padding(.horizontal, 10)
}
Is this something that can be achieved?
Thanks in advance
You could set this up by manually logging your screenviews instead. For SwiftUI, it is something like this:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
// Logging screen name with class and a custom parameter.
.analyticsScreen(name: "main_content",
class: "ContentView",
extraParameters: ["my_custom_param": 5])
// OR Logging screen name only, class and extra parameters are optional.
.analyticsScreen(name: "main_content")
}
}

iOS Swift SwiftUI - Use NavigationLink after clicking on subtext

I got this text:
Text("Indem du fortfährst, stimmst du unseren ") +
Text("Nutzungsbedingungen")
.underline()
.foregroundColor(Color("ClickableLink")) +
Text(" und unserer ") +
Text("Datenschutzerklärung")
.underline()
.foregroundColor(Color("ClickableLink")) +
Text(" zu")
I'd like to open a new view using NavigationLink after taping on Nutzungsbedingungen or Datenschutzerklärung, both need to open different views.
I've seen those answers:
SwiftUI tappable subtext
but those are either not what I need or trying them gives me errors and I don't know how to modify them since I'm absolutely new to swift/swiftui
Here are two possible approaches, one using the new NavigationStack with NavigationDestination and the other using popOver.
Both have the downside that you can't concatenate Text views within them. But you can still work with HStack/VStack to define the layout.
enum Legals: Identifiable {
case nutzungsbedingungen
case datenschutzerklärung
var id: Legals { self }
}
struct ContentView: View {
#State private var selected: Legals?
var body: some View {
NavigationStack {
// NavigationLink version
VStack(alignment: .leading) {
Text("Indem du fortfährst, stimmst du unseren")
NavigationLink("Nutzungsbedingungen", value: Legals.nutzungsbedingungen)
Text("und unserer")
NavigationLink("Datenschutzerklärung", value: Legals.datenschutzerklärung)
Text("zu")
}
.navigationDestination(for: Legals.self) { selection in
switch selection {
case .datenschutzerklärung:
Text("Datenschutzerklärung")
case .nutzungsbedingungen:
Text("Nutzungsbedingungen")
}
}
Divider()
// popOver Version
VStack(alignment: .leading) {
Text("Indem du fortfährst, stimmst du unseren ")
Text("Nutzungsbedingungen")
.underline()
.onTapGesture {
selected = .nutzungsbedingungen
}
Text("und unserer ")
Text("Datenschutzerklärung")
.underline()
.onTapGesture {
selected = .nutzungsbedingungen
}
Text("zu")
}
.popover(item: $selected) { selected in
switch selected {
case .datenschutzerklärung:
Text("Datenschutzerklärung")
case .nutzungsbedingungen:
Text("Nutzungsbedingungen")
}
}
}
}
}

SwiftUI - Error with content in a view component

I'm trying to build a reusable navigation tab in SwiftUI and I'm facing some challenges.
I come from ReactJS and wanted to create a component that I can pass the tab images and the different views.
This is the code I have so far which works pretty well:
struct Navigation<Content: View>: View {
#State var selectedIndex = 0
var tabBarImageNames: [String]
let content: [Content]
init(tabBarImageNames: [String], _ content: Content...) {
self.content = content
self.tabBarImageNames = tabBarImageNames
}
var body: some View {
VStack{
ZStack {
content[selectedIndex]
}
Spacer()
HStack {
ForEach(0 ..< tabBarImageNames.count) { tabNum in
[ ... ]
}
}
}
}
}
I can call Navigation with these arguments an everything works as expected:
Navigation(
tabBarImageNames: ["house.fill", "chart.bar.xaxis", "target", "bubble.left"],
VStack {
Text("First view")
.navigationTitle("First Tab")
},
VStack {
Text("Second view")
.navigationTitle("Second Tab")
},
VStack {
Text("Third view")
.navigationTitle("Third Tab")
},
VStack {
Text("Fourth view")
.navigationTitle("Fourth Tab")
}
)
The real problem 🚨
The issue with the above code is that if the Views I'm passing as arguments are not exactly the same (when I say exactly I literally mean exactly the same type and number of components), Xcode complains with the following error:
I'm pretty sure it's something not very difficult to solve, but given my short experience with this language I'm struggling to find the right answer. I tried to change the init content protocol to AnyView instead of Content among other things, but no luck so far.
Looking for help 🗜
Anyone with SwiftUI experience that can help me out and explain me what I'm missing? Also open to feedback on improvements on the code.
Can't use any type of library or external package btw, everything has to be built manually.
Thanks Stack overflow!

SwiftUI Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift

I have a environment object that is an array, and a panel in my settings page where the user can add/remove values to the array...here is the list of items in the array displayed visually for the user:
ScrollView {
VStack {
ForEach(self.env.numbers.indices, id: \.self) { number in
ZStack {
HStack {
Text("#\(number + 1): ")
.font(Font.custom(K.font.montserratMedium, size: 17.0))
.foregroundColor(Color(K.SettingsTextColor))
// Spacer()
NumberTextField(self.env.numbers[number], text: self.$env.numbers[number])
}
}
}
}
}
and at the bottom of my view is a toggle to change the number of items in the array:
Stepper(onIncrement: {
self.env.numbers.append("12345")
print("default number should be added")
print(self.env.numbers.indices)
print(self.env.numbers)
print("count = \(self.env.numbers.count)")
},
onDecrement: {
if self.env.numbers.count != 0 {
print("number should be removed")
self.env.numbers.removeLast(1)
// also tried self.env.numbers = self.env.numbers.droplast() but this didnt work
print(self.env.numbers.indices)
print(self.env.numbers)
print("count = \(self.env.numbers.count)")
}
the NumberTextField view is a UIKit UITextField wrapper, and seems to work fine. Everything about this works fine until I try to decrease the stepper, and I get the Index out of range error (which surprisingly shows the error happening in the app delegate)...any ideas how to fix this?

Prevent NavigationLink from processing state variable

I don't like my title, but it's the best I could think of. Basically, I have two Views: SearchInput and SearchResultList. The user enters a search term with SearchInput, taps "Go" and then sees the results of the search with SearchResultList. The code is:
struct SearchInput: View {
#State private var searchTerm: String = ""
var searchResult: [RTDocument] {
return RuntimeDataModel.shared.fetchRTDocuments(matching: self.searchTerm)
}
var body: some View {
NavigationView {
VStack {
TextField("Enter search term...", text: self.$searchTerm)
NavigationLink(destination: SearchResultList(
rtDocuments: self.searchResult
)) { Text("Go") } //NavigationLink
} //VStack
} //NavigationView
} //body
} //SearchInput
The issue I'm having is that every time the user enters a character in TextField, the bound-state variable searchTerm is updated which causes NavigationLink to reevaluate its destination -- which causes a Core Data fetch up in the computed searchResult variable.
I'd like the fetch to happen only once, when the user taps "Go". Is there a way to accomplish that?
This is expected behaviour, so in general you should redesign your SearchResultList, but this can help as a walkaround:
Declare a state on your input:
#State var shouldNavigate = false
Then separate the button form the navigation link.
Group {
Button("Go") {
self.shouldNavigate = true
}
NavigationLink(destination: Text("SearchResultList"),
isActive: $shouldNavigate) {
EmptyView()
}
}

Resources