TabView with .tabViewStyle(.page) does not dynamically update [duplicate] - swift

I came across a weird Issue in SwiftUI.
I created a simple View that only holds a Button
and a TabView that uses the PageViewStyle. It seems that the TabView does not update it's content
correctly depending on the State of the Variable.
It seems that the content gets updated somehow but the View wont be updated how I would expect
Here is the Code of my View:
struct ContentView: View {
#State var numberOfPages: Int = 0
#State var selectedIndex = 0
var body: some View {
VStack {
Text("Tap Me").onTapGesture(count: 1, perform: {
self.numberOfPages = [2,5,10,15].randomElement()!
self.selectedIndex = 0
})
TabView(selection: $selectedIndex){
ForEach(0..<numberOfPages, id: \.self) { index in
Text("\(index)").background(Color.red)
}
}
.frame(height: 300)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
}.background(Color.blue)
}
}
This is how the result looks after tapping the label several Times.
The Initial State is no 0 Pages. After you tap i would expect that the content of the
TabView changes so all Pages will be scrollable and visible but just the page indicator updates it State for some reason.

TabView expects to have container of pages, but you included only one HStack (with own dynamic content), moreover chaining number of pages you have to reset tab view, so here is a fix.
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State var numberOfPages: Int = 0
var body: some View {
VStack {
Text("Tap Me").onTapGesture(count: 1, perform: {
self.numberOfPages = [2,5,10,15].randomElement()!
})
if self.numberOfPages != 0 {
TabView {
ForEach(0..<numberOfPages, id: \.self) { index in
Text("\(index)").frame(width: 300).background(Color.red)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
.frame(height: 300)
.id(numberOfPages) // << here !!
}
}
}
}

Related

SwiftUI: Trigger event after scrolling to end of a paged tabview

I'm trying to have a prompt be called after a user reaches the end of a paged TabView and tries to scroll one extra. (For example, if there are three pages, once the user scrolls to the third page and tries to scroll to a fourth, it pulls up the prompt). I tried doing this:
TabView (selection: $currentIndex) {
...
}
.tabViewStyle(.page(indexDisplayMode: .never))
.onChange(of: currentIndex) { _ in
}
However, since the user is already on the last tab, the index isn't updated so the prompt can't be called. How can I achieve this?
Doesn't this work for you?. It reacts after showing 1/2 of the last (dummy) view:
struct ContentView: View {
#State private var currentIndex = 0
#State private var showAlert = false
let colors: [Color] = [.blue, .green, .cyan, .teal, .clear]
var body: some View {
TabView (selection: $currentIndex) {
ForEach(0..<5) { i in
ZStack {
colors[i]
Text("Tab \(currentIndex)")
.font(.title)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.onChange(of: currentIndex) { _ in
if currentIndex == 4 {
currentIndex = 3
showAlert = true
}
}
.alert("You reached the end", isPresented: $showAlert) { }
}
}

NavigationLink inside a List and VStack does not work after the first pop

I am trying to use the new Pull to Refresh feature in the latest version of SWiftUI which requires a List. Enclosing the VStack in a List causes the NavigationLink to work only once. Below is a simple version of the code without the Pull To Refresh part.
There is a question that was asked 68144891 on stackoverflow and there was a refrence to a known issue link which takes you to a page not found (https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-15-beta-release-notes)
Steps o reproduce
Tap "Press Me 1" or one of the items
Tap "Show Details"
Tap Back at the top
Tap "Press Me" again will not navigate to the next screen. A grey screen blocks when you tap
The app works without the VStack
struct ContentView: View {
var body: some View {
NavigationView {
List {
VStack { // commenting VStack works
Text("Options").font(.largeTitle).bold()
ForEach(1..<5, id:\.self) { counter in
NavigationLink(destination: SubView(counter: counter)) {
Text("Press Me \(counter)").font(.headline)
}
.buttonStyle(PlainButtonStyle())
}
}
}.listStyle(.grouped)
}
}
}
struct SubView: View {
var counter: Int
#State private var showDetails = false
var body: some View {
VStack(alignment: .leading) {
Button("Show details") {
showDetails.toggle()
}
if showDetails {
Text("Clicked")
.font(.largeTitle)
}
}
}
}
Any help appreciated
Thanks much!
... follow-up to my comment
I assume you wanted this
struct ContentView: View {
var body: some View {
NavigationView {
List {
Section(Text("Options").font(.largeTitle).bold()) {
ForEach(1..<5, id:\.self) { counter in
NavigationLink(destination: SubView(counter: counter)) {
Text("Press Me \(counter)").font(.headline)
}
.buttonStyle(PlainButtonStyle())
}
}
}.listStyle(.grouped)
}
}
}

List clipping issue (bug with SwiftUI?)

I'm having this weird issue with the List in macOS Montery Beta 3, where some of the list's items are being clipped, and you have to scroll up and back down to unclip it.
Example:
https://imgur.com/a/P0TUt85
Reproduction
Create a new blank SwiftUI macOS project
Paste the following code:
struct Bla: Identifiable {
var text: String
var subtext: String
var id = UUID()
}
struct ContentView: View {
#State var data = (0..<100).map { Bla(text: "Text: \($0)", subtext: "Subtext: \($0)")}
var body: some View {
NavigationView {
List(data) { item in
VStack(alignment: .leading) {
Text(item.text)
Text(item.subtext)
.foregroundColor(.secondary)
}
}
.toolbar {
Button(action: {
var offset = 0
for i in 0..<data.count {
if i % 2 == 0 {
continue
}
data.remove(at: i - offset)
offset += 1
}
}) {
Image(systemName: "plus")
}
}
}
}
}
Run the app, and scroll the list to the middle.
Press the plus button on the toolbar. This simply removes half of the items.
You should see the clipping issue. Scrolling up and back down fixes it.
Is this something with my code, or with SwiftUI's List?
After adding the .listStyle I dont see the clipping issue anymore, try this:
List(data) { item in
VStack(alignment: .leading) {
Text(item.text)
Text(item.subtext)
.foregroundColor(.secondary)
}
}.listStyle(SidebarListStyle())

Can you animate a SwiftUI View on disappear?

I have a SwiftUI View which has a custom animation that runs onAppear. I am trying to get the view to animate onDisappear too but it just immediately vanishes.
The below example reproduces the problem - the MyText view should slide in from the left and slide out to the right. The id modifier is used to ensure a new view is rendered each time the value changes, and I have confirmed that both onAppear and onDisappear are indeed called each time, but the animation onDisappear never visibly runs. How can I achieve this?
struct Survey: View {
#State private var id = 0
var body: some View {
VStack {
MyText(text: "\(id)").id(id)
Button("Increment") {
self.id += 1
}
}
}
struct MyText: View {
#State private var offset: CGFloat = -100
let text: String
var body: some View {
return Text(text)
.offset(x: offset)
.onAppear() {
withAnimation(.easeInOut(duration: 2)) {
self.offset = 0
}
}
.onDisappear() {
withAnimation(.easeInOut(duration: 2)) {
self.offset = 100
}
}
}
}
}
Probably you wanted transition, something like
Update: re-tested with Xcode 13.4 / iOS 15.5
struct Survey: View {
#State private var id = 0
var body: some View {
VStack {
MyText(text: "\(id)")
Button("Increment") {
self.id += 1
}
}
}
struct MyText: View {
var text: String
var body: some View {
Text("\(text)").id(text)
.frame(maxWidth: .infinity)
.transition(.slide)
.animation(.easeInOut(duration: 2), value: text)
}
}
}
I'm afraid it can't work since the .onDisappear modifier is called once the view is hidden.
However there is a nice answer here :
Is there a SwiftUI equivalent for viewWillDisappear(_:) or detect when a view is about to be removed?

Why is my picker unresponsive in SwiftUI?

I've tried switching between a segmented and wheel picker, but neither register a selection when clicked.
NavigationView {
Form {
Picker(selection: self.$settings.senatorChoice, label: Text("Choose a senator")) {
ForEach(0 ..< self.customSenators.count) {
Text(self.customSenators[$0])
}
}.pickerStyle(WheelPickerStyle())
.labelsHidden()
.padding()
}
}
Double check that settings.senatorChoice is an Int. The type of your range in ForEach must match the type of your Picker's binding. (See here for more info).
In addition, you probably want to use ForEach(self.customSenators.indices, id: \.self). This prevents possible crashes and out-of-date UI if elements get added to or removed from customSenators. (See here for more info.)
Add a tag to each picker item so they are unique e.g.
Text(self.customSenators[$0]).tag($0)
The following test code registers the selection when clicked.
struct Settings {
var senatorChoice: Int = 0
}
struct ContentView: View {
#State private var settings = Settings()
#State private var customSenators = ["One","Two","Three"]
var body: some View {
NavigationView {
Form {
Picker(selection: self.$settings.senatorChoice, label: Text("Choose a senator")) {
ForEach(0 ..< customSenators.count) {
Text(self.customSenators[$0])
}
}.pickerStyle(SegmentedPickerStyle())
.labelsHidden()
.padding()
Text("value: \(customSenators[settings.senatorChoice])")
}
}
}
}