SwiftUI onAppear called twice when NavigationView inside TabView - swiftui

So I have a TabView where each of the tabs is embedded in a NavigationView.
On first appear of each tab I get the following lifecycle calls onAppear(), onDisappear(), onAppear(). So it looks like onAppear gets called twice. This only happens the first time. If I navigate back to the same tab, only onAppear() gets called, and only once.
Here's a minimal example:
struct Page1: View {
init() { print("Page 1 init") }
var body: some View {
NavigationView {
Text("Page 1")
.onAppear(perform: { print("Page 1 appearing") })
.onDisappear(perform: { print("Page 1 disappearing") })
}
}
}
struct Page2: View {
init() { print("Page 2 init") }
var body: some View {
NavigationView {
Text("Page 2")
.onAppear(perform: { print("Page 2 appearing") })
.onDisappear(perform: { print("Page 2 disappearing") })
}
}
}
struct ContentView: View {
var body: some View {
TabView {
Page1().tabItem { Text("Page 1") }
Page2().tabItem { Text("Page 2") }
}
}
}
And here's the result printed out:
Page 1 init
Page 2 init
Page 1 appearing
Page 1 disappearing
Page 1 appearing
Here's what happens if I click on the second tab
Page 1 init
Page 2 init
Page 1 appearing
Page 1 disappearing
Page 1 appearing
// here I clicked on second tab
Page 2 appearing
Page 2 disappearing
Page 2 appearing
Page 1 disappearing

TabView {
NavigationView {
VStack {
Color.red
.onAppear {
print("appear : red")
}
.onDisappear {
print("disappear : red")
}
}.onAppear {
print("appear")
}
}
}
Test on iOS 15 beta Simulator
The output:
appear : red
appear
disappear : red
appear : red

Related

auto repeat pageview in swiftui

I have a swiftui project. I am using pageview. Normally, when it comes to the last page, it does not scroll further because the pages are finished. What I want is this: the pages continue after reaching the last page. Let's start again from the first page. so I want my page order to be like this:
page 1 -> page 2 -> page 3 -> page 1 -> page 2 -> page 3 -> page 1 .....
I want it to repeat itself like this all the time.
struct PageControllerView: View {
var body: some View {
TabView {
Text("page 1")
Text("page 2")
Text("page 3")
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
Well you need to use the TabView with a selection:
struct PageControllerView: View {
#State var selection = 1
var body: some View {
TabView(selection: $selection) {
Text("page 3").tag(0) //same as last view for swipe effect
Text("page 1").tag(1)
Text("page 2").tag(2)
Text("page 3").tag(3)
Text("page 1").tag(4) //same as first view for swipe effect
}//.tabViewStyle(PageTabViewStyle()) BTW, You don't need that
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.onChange(of: selection) { newValue in
if selection == 4 { //last tag + 1
selection = 1 //first tag
}else if selection == 0 { //first tag - 1
selection = 3 //last tag
}
}
}
}
This also works with ForEach, but don't forget to choose a suitable tag.

How to reset the toolbar back to its original position

I'm trying to setup a toolbar in watchos which displays a button when I scroll the view down. Everything works, the button scrolls down and I can navigate to another page. When returning however, I would like the scrollview to be in the same position as when the app loads, so without the button being visible.
The code I have now is:
struct ContentView: View {
#State private var selectedPage: String? = nil
var body: some View {
ScrollView {
VStack(alignment: .leading) {
ForEach(0..<100) {
Text("Row \($0)")
}
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Settings") {
selectedPage = "Settings"
}
}
}
.background(
NavigationLink(destination: SettingsView(), tag: "Settings",selection: $selectedPage) {}
.hidden()
)
.navigationTitle {
Text("Navigation")
}
}
}
}
I Have tried using a scrollViewReader, but think I'm looking into the wrong directing as it allows the scrollView to go to a certain position, but the toolbar seems to be no part of it and stays in view. When reading the scrollviews offset position (not in code but check gif), the offset is 0 when the button is not visible (initial state). When scrolling down the offset goes up, but when scrolling up until the button becomes visible the offset of the scrollview is 0 as well.
struct ContentView: View {
#State private var selectedPage: String? = nil
var body: some View {
ScrollView {
ScrollViewReader { reader in
VStack(alignment: .leading) {
ForEach(0..<100) { i in
Text("Row \(i)")
.id(i)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Settings") {
selectedPage = "Settings"
}
}
}
.background(
NavigationLink(destination: SettingsView(), tag: "Settings",selection: $selectedPage) {}
.hidden()
)
.onAppear {
withAnimation {
reader.scrollTo(0, anchor: .top)
}
}
.navigationTitle {
Text("Navigation")
}
}
}
}
}
}
When choosing 1 as value to scroll to the button is pushed back to the top and in the list id 1 is selected.
.onAppear {
withAnimation {
reader.scrollTo(1, anchor: .top)
}
}
So it's working with id 1 and higher, but when using id 0 the view is not reset.
So how to reset the view to the initial state with the button being hidden on top ?
Solved it simply by adding a #State property which is true when loading the view and which changes to false if navigated away from the view and then conditionally show the item within the .toolbar view modifier.
#State var loadedMainView = true
var body: some view {
ScrollView {
// .... code
}
.toolbar {
ToolbarItem {
if loadedMainView {
Button("My Button") {
}
}
}
.onAppear {
loadedMainView = true
}
.onDissappear {
loadedMainView.toggle()
}

Reset NagivationView stack in TabView

I have a tabview with two tabs (tabs A and B).
Clicking tab A opens a master View. In that master view there is a navigation link to Page 1. Within Page 1 there is also a link to Page 2.
When the user is on Page 1 or 2, and I tap Tab A, it doesn’t revert to master View. Similarly if the user clicks Tab B and then Tab A again, it returns to Page 1 or 2 (whichever the user was on), rather than master View.
How to I make the navigation stack reset in both cases?
Thanks!
That's because the View won't be rerendered. Here is a possible approach how to achieve your behavior:
You can use ProxyBinding for the TabView to detect changes and then reset the NavigationLink by changing the internal State variable.
struct ContentView: View {
#State var activeView: Int = 0
#State var showNavigation: Bool = false
var body: some View {
TabView(selection: Binding<Int>(
get: {
activeView
}, set: {
activeView = $0
showNavigation = false //<< when pressing Tab Bar Reset Navigation View
}))
{
NavigationView {
NavigationLink("Click", destination: Text("Page A"), isActive: $showNavigation)
}
.tabItem {
Image(systemName: "1.circle")
Text("First")
}
.tag(0)
Text("Second View")
.padding()
.tabItem {
Image(systemName: "2.circle")
Text("Second")
}
.tag(1)
}
}
}
You can create RootView with MainView
import SwiftUI
struct RootView: View {
#ObservedObject var viewModel = RootViewModel()
init(){
viewModel.prepare()
}
var body: some View {
MainView(tab: viewModel.mainTab)
.id(UUID().uuidString)
}
}
Create RootViewModel with listeners to screen updating
import SwiftUI
class RootViewModel: ObservableObject{
#Published var mainTab: SelectedTab = .firstTab
let mainScreenNotification = NSNotification.Name("mainScreenNotification")
private var observerMain: Any?
func prepare(){
observerMain = NotificationCenter.default.addObserver(forName: mainScreenNotification, object: nil, queue: nil, using: { [unowned self] notification in
self.mainTab = (notification.userInfo?["selectedTab"])! as! SelectedTab
})
}
}
enum SelectedTab {
case firstTab, secondTab
}
Run this to inflating new tab screen from tab child:
NotificationCenter.default.post(name:
NSNotification.Name("mainScreenNotification"),
object: nil,
userInfo: ["selectedTab": SelectedTab.firstTab]
)

TabView with "PageTabViewStyle" does not update it's content when #State var changes

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 !!
}
}
}
}

SwiftUI Splitview the DetailView not in Navigation?

I try to create a splitview in SwiftUI, and I have 2 issues
1. the Detail-View not showing in the navigation
2. if I go to Detail-View from master view it shows in navigation (Navigation top bar), but when it goes to next page, back button not work
struct MainView: View {
var body: some View {
NavigationView {
ListView()
DetailView()
}
}
}
struct ListView: View {
let menuItems = [MenuItem(name: "Login"),
MenuItem(name: "KS & Token"),
MenuItem(name: "Household & Domain"),
MenuItem(name: "Register")]
var body: some View {
VStack{
List{
ForEach(self.menuItems, id:\.id) { item in
NavigationLink(destination: LoginView(menuItem: item)){
Text(item.name)
}
}
}
Spacer()
}
.navigationBarTitle(Text("User Menu"))
}
}
the issue is:
1. DetailView() not showing Navigation top bar
2. in LoginView, when I go to next page with NavigationLink, the app does not show back button, and also add it by code, not help