Reset NagivationView stack in TabView - swiftui

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]
)

Related

Swift UI #FocusState triggering NavigationLink Twice

What am I doing wrong here? Tapping on the navigation link triggers the navigation, then the focusState value updates, causing body to run and trigger the navigation link again.
How do I prevent the link from being triggered twice causing my destination views init to fire twice?
import SwiftUI
struct ContentView: View {
#State var text: String = "text"
#FocusState var focussed: Bool
#State var isActive: Bool = false
var body: some View {
NavigationView {
List {
TextField("", text: $text)
.focused($focussed)
.onChange(of: focussed) { _ in }
let _ = Self._printChanges()
NavigationLink("Tap Me", destination: MyViewTwo(), isActive: $isActive)
}
}
}
}
struct MyViewTwo: View {
init() {
print("Init Called")
}
var body: some View {
Text("Hello View 2")
}
}

TabView is not switching tabs properly in SwiftUI

I'm having a weird problem that I can't seem to figure out with SwiftUI. I have a TabView in my ContentView, there are 3 tabs (chat list, user list, and profile) the app loads up on the chat list tab. The problem is, when I select the second tab (user list) it goes to that tab for a split second, then goes right back to the chat list. It doesn't make any sense to me. The 2 main tabs make an API call to get information from a server, and everything is working great, except that first click.
The app loads up, I click the user list tab and it shows for a split second, then goes back to the chat list tab. I can then click the user list tab again and it will go to that tab and stay there, but the first click on that tab always sends you back to the chat list tab.
I'll post up some of my code, hopefully someone will be able to see what I'm doing wrong, because I sure can't.
ContentView
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State var selectedTab = 0
#State var setup: Bool = false
#State var notificationChatID: String = ""
#ObservedObject var userModel: UserModel = UserModel()
#ObservedObject var chatModel: ChatModel = ChatModel()
#ObservedObject var appState: AppState = AppState.shared
var pushNavigationBinding : Binding<Bool> {
.init { () -> Bool in
appState.selectedChatID != nil
}
set: { (newValue) in
if !newValue {
appState.selectedChatID = nil
}
}
}
let settings = UserDefaults.standard
var body: some View {
ZStack {
if setup {
TabView(selection: $selectedTab) {
ChatList(launchedChatID: appState.selectedChatID ?? "", userModel: userModel, chatModel: chatModel)
.tabItem {
Image(systemName: "message.circle.fill")
Text("Active Chats")
}
UserList(userModel: userModel)
.tabItem {
Image(systemName: "person.3.fill")
Text("User List")
}
ProfileView()
.tabItem {
Image(systemName: "person.crop.circle")
Text("Profile")
}
}
} else {
Onboarding(userModel: userModel, isSetup: $setup)
}
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name.NewMessage), perform: { notification in
if let info = notification.userInfo {
let chatID = info["chatID"] as? String ?? ""
if chatID != "" {
chatModel.selectedChat = chatID
appState.selectedChatID = chatID
self.notificationChatID = chatID
}
}
})
}
Then my UserList
struct UserList: View {
#ObservedObject var userModel: UserModel
#ObservedObject var chatModel: ChatModel = ChatModel()
#State var action: Int? = -1
var body: some View {
NavigationView {
VStack {
List {
ForEach(0..<userModel.arrayOfUsers.count, id: \.self) { index in
ZStack(alignment: .leading) {
NavigationLink(
destination: ChatView(model: chatModel, userModel: userModel, item: $action),
tag: index,
selection: $action
) {
EmptyView().frame(width: .zero, height: .zero, alignment: .center)
}
Button(action: {
print("You selected \(userModel.arrayOfUsers[index].name)")
userModel.selectedUserName = userModel.arrayOfUsers[index].name
userModel.selectedUserID = userModel.arrayOfUsers[index].id
self.action = index
}) {
Text(userModel.arrayOfUsers[index].name)
}
}
}
}
Spacer()
}.navigationBarTitle(Text("Users"), displayMode: .inline)
}.onAppear() {
print("Inside the userlist on appear")
if userModel.arrayOfUsers.count == 0 {
ApiService.getUsers() { (res) in
switch(res) {
case .success(let response):
print("In success")
let users = response!.users!
DispatchQueue.main.async {
for user in users.users {
userModel.arrayOfUsers.append(user)
}
}
break
case .failure(let error):
print("Error getting users")
print(error)
break
}
}
}
}
}
}
My userModel.arrayOfUsers is #Published
UserModel
class UserModel: ObservableObject {
var name: String = ""
var id: String = ""
var myUserID: String = ""
#Published var arrayOfUsers: [User] = []
var selectedUserID: String = ""
var selectedUserName: String = ""
}
In the console in Xcode I see
Inside the userlist on appear
...(network stuff)
In the success
In the on appear in ChatList
So it's loading the UserList, it shows the network call out to my API, it shows the In the success from the API call in the UserList, then the very next thing is back to the In the on appear in ChatList I can't figure out why it's kicking me back to the chat list.
You're binding your TabView's current tab to $selectedTab, but not providing SwiftUI with any information on how to alter that value when the user changes tabs. And so, because selectedTab hasn't changed, when the drawing system comes to review your view structure, it still concludes that you want to see the first tab.
You should add a .tag modifier after each .tabItem to tell SwiftUI what values represent each tab. Then, when the user selects each tab, selectedTab will be updated and the tab choice will "stick".
For example:
TabView(selection: $selectedTab) {
ChatList(launchedChatID: appState.selectedChatID ?? "", userModel: userModel, chatModel: chatModel)
.tabItem {
Image(systemName: "message.circle.fill")
Text("Active Chats")
}
.tag(0)
UserList(userModel: userModel)
.tabItem {
Image(systemName: "person.3.fill")
Text("User List")
}
.tag(1)
ProfileView()
.tabItem {
Image(systemName: "person.crop.circle")
Text("Profile")
}
.tag(2)
}
Note that unless you're persisting the user's choice in some way (e.g., by declaring your state variable with #SceneStorage) you can get the same effect by not using a selection argument at all.

swiftUI : Updating Text after changes in settings

[Edit(1) to reflect posting of streamlined app to illustrate the issue : ].
[Edit (2) : completely removed EnvironmentObject and app now works ! Not understanding WHY body is refreshed as NO #State vars are being modified...Code at end of text]
I am writing an app that, at some point, displays some text, related to the contents of 2 Arrays, depending on a set of rules. These rules can be set in a Settings view, as User's preference.
So, when a user changes the rules he wants applied in Settings, that text needs to be re-assessed.
But of course, things aren't that easy.
I present my settings view as modal on my main ContentView, and when I dismiss that modal, the body of the ContentView is not redrawn...
I created an EnvironmentObject with #Published vars in order to keep track of all the user preferences (that are also written to UserDefaults), and shared that #EnvironmentObject with both my ContentView and SettingsView, in the hope that, being an observedObject, its changes would trigger a refresh of my ContentView.
Not so...
Any ideas to help me go forward on this ? Any pointers would be greatly appreciated (again!).
Posted app on GitHub has following architecture :
An appState EnvironmentObject,
A ContentView that displays a set of texts, depending on some user preferences set in
A settingsView
UserDefaults are initialized in AppDelegate.
Thanks for any help on this...
Content view :
import SwiftUI
struct ContentView: View {
#State var modalIsPresented = false // The "settingsView" modally presented as a sheet
#State private var modalViewCaller = 0 // This triggers the appropriate modal (only one in this example)
var body: some View {
NavigationView {
VStack {
Spacer()
VStack {
Text(generateStrings().text1)
.foregroundColor(Color(UIColor.systemGreen))
Text(generateStrings().text2)
} // end of VStack
.frame(maxWidth: .infinity, alignment: .center)
.lineLimit(nil) // allows unlimited lines
.padding(.all)
Spacer()
} // END of main VStack
.onAppear() {
self.modalViewCaller = 0
}
.navigationBarTitle("Test app", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
self.modalViewCaller = 6 // SettingsView
self.modalIsPresented = true
}
) {
Image(systemName: "gear")
.imageScale(.large)
}
))
} // END of NavigationView
.sheet(isPresented: $modalIsPresented, content: sheetContent)
.navigationViewStyle(StackNavigationViewStyle()) // This avoids dual column on iPad
} // END of var body: some View
// MARK: #ViewBuilder func sheetContent() :
#ViewBuilder func sheetContent() -> some View {
if modalViewCaller == 6 {
SettingsView()
}
} // END of func sheetContent
// MARK: generateStrings() : -
func generateStrings() -> (text1: String, text2: String, recapText: String, isHappy: Bool) { // minimumNumberOfEventsCheck
var myBool = false
var aString = "" // The text 1 string
var bString = "" // The text 2 string
var cString = "" // The recap string
if UserDefaults.standard.bool(forKey: kmultiRules) { // The user chose the dual rules option
let ruleSet = UserDefaults.standard.integer(forKey: kruleSelection) + 1
aString = "User chose 2 rules option"
bString = "User chose rule set # \(ruleSet)"
myBool = true
print("isDualRules true loop : generateStrings was called at \(Date().debugDescription)")
cString = "Dual rules option, user chose rule set nb \(ruleSet)"
}
else // The user chose the single rule option
{
aString = "User chose single rule option"
bString = "User had no choice : there is only one set of rules !"
myBool = false
print("isDualRules false loop : generateStrings was called at \(Date().debugDescription)")
cString = "Single rule option, user chose nothing."
}
return (aString, bString, cString, myBool)
} // End of func generatestrings() -> String
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return ContentView()
}
}
SettingsView :
import SwiftUI
import UIKit
struct SettingsView: View {
#Environment(\.presentationMode) var presentationMode // in order to dismiss the Sheet
#State public var multiRules = UserDefaults.standard.bool(forKey: kmultiRules)
#State private var ruleSelection = UserDefaults.standard.integer(forKey: kruleSelection) // 0 is rule 1, 1 is rule 2
var body: some View {
NavigationView {
List {
Toggle(isOn: $multiRules)
{
Text("more than one rule ?")
}
.padding(.horizontal)
if multiRules {
Picker("", selection: $ruleSelection){
Text("rules 1").tag(0)
Text("rules 2").tag(1)
}.pickerStyle(SegmentedPickerStyle())
.padding(.horizontal)
}
} // End of List
.navigationBarItems(
leading:
Button("Done") {
self.saveDefaults() // We try to save once more if needed
self.presentationMode.wrappedValue.dismiss() // This dismisses the view
}
)
.navigationBarTitle("Settings", displayMode: .inline)
} // END of Navigation view
} // END of some View
func saveDefaults() {
UserDefaults.standard.set(multiRules, forKey: kmultiRules)
UserDefaults.standard.set(ruleSelection, forKey: kruleSelection)
}
}
// MARK: Preview struct
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
return SettingsView()
}
}
Constants.swift file :
import Foundation
import SwiftUI
let kmultiRules = "two rules"
let kruleSelection = "rules selection"
let kappStateChanged = "appStateChanged"
AppDelegate :
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UserDefaults.standard.register(defaults: [ // We initialize the UserDefaults
"two rules": false,
"rules selection": 0, // 0 is ruel 1, 1 is rule 2
"appStateChanged": false
])
return true
}
If you have a shared #EnvironmentObject with #Published properties in two views, if you change such a property from one view, the other one will be re-execute the body property and the view will be updated.
It really helps to create simple standalone examples - not only for asking here, also for gaining a deeper understanding / getting an idea why it doesn't work in the complex case.
For example:
import SwiftUI
class TextSettings: ObservableObject {
#Published var count: Int = 1
}
struct TextSettingsView: View {
#EnvironmentObject var settings: TextSettings
var body: some View {
Form {
Picker(selection: $settings.count, label:
Text("Text Repeat Count"))
{
ForEach(Array(1...5), id: \.self) { value in
Text(String(value)).tag(value)
}
}
}
}
}
struct TextWithSettingExampleView: View {
#EnvironmentObject var settings: TextSettings
var body: some View {
Text(String(repeating: "Hello ", count: Int(settings.count)))
.navigationBarItems(trailing: NavigationLink("Settings", destination: TextSettingsView()))
}
}
struct TextWithSettingExampleView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
TextWithSettingExampleView()
}
.environmentObject(TextSettings())
}
}
Not sure I fully understand the question, but I had what I believe might be a similar problem where I never got my contentview to reflect the updates in my observed object when the changes were triggered from a modal. I solved/hacked this by triggering an action in my observed object when dismissing the modal like this:
struct ContentView: View {
//
#State var isPresentingModal = false
var body: some View {
//
.sheet(isPresented: self.$isPresentingModal) {
PresentedModalView()
.onDisappear {
//Do something here
}
}
}
}

Is it possible for a NavigationLink to perform an action in addition to navigating to another view?

I'm trying to create a button that not only navigates to another view, but also run a function at the same time. I tried embedding both a NavigationLink and a Button into a Stack, but I'm only able to click on the Button.
ZStack {
NavigationLink(destination: TradeView(trade: trade)) {
TradeButton()
}
Button(action: {
print("Hello world!") //this is the only thing that runs
}) {
TradeButton()
}
}
You can use .simultaneousGesture to do that. The NavigationLink will navigate and at the same time perform an action exactly like you want:
NavigationLink(destination: TradeView(trade: trade)) {
Text("Trade View Link")
}.simultaneousGesture(TapGesture().onEnded{
print("Hello world!")
})
You can use NavigationLink(destination:isActive:label:). Use the setter on the binding to know when the link is tapped. I've noticed that the NavigationLink could be tapped outside of the content area, and this approach captures those taps as well.
struct Sidebar: View {
#State var isTapped = false
var body: some View {
NavigationLink(destination: ViewToPresent(),
isActive: Binding<Bool>(get: { isTapped },
set: { isTapped = $0; print("Tapped") }),
label: { Text("Link") })
}
}
struct ViewToPresent: View {
var body: some View {
print("View Presented")
return Text("View Presented")
}
}
The only thing I notice is that setter fires three times, one of which is after it's presented. Here's the output:
Tapped
Tapped
View Presented
Tapped
NavigationLink + isActive + onChange(of:)
// part 1
#State private var isPushed = false
// part 2
NavigationLink(destination: EmptyView(), isActive: $isPushed, label: {
Text("")
})
// part 3
.onChange(of: isPushed) { (newValue) in
if newValue {
// do what you want
}
}
This works for me atm:
#State private var isActive = false
NavigationLink(destination: MyView(), isActive: $isActive) {
Button {
// run your code
// then set
isActive = true
} label: {
Text("My Link")
}
}
Use NavigationLink(_:destination:tag:selection:) initializer and pass your model's property as a selection parameter. Because it is a two-way binding, you can define didset observer for this property, and call your function there.
struct ContentView: View {
#EnvironmentObject var navigationModel: NavigationModel
var body: some View {
NavigationView {
List(0 ..< 10, id: \.self) { row in
NavigationLink(destination: DetailView(id: row),
tag: row,
selection: self.$navigationModel.linkSelection) {
Text("Link \(row)")
}
}
}
}
}
struct DetailView: View {
var id: Int;
var body: some View {
Text("DetailView\(id)")
}
}
class NavigationModel: ObservableObject {
#Published var linkSelection: Int? = nil {
didSet {
if let linkSelection = linkSelection {
// action
print("selected: \(String(describing: linkSelection))")
}
}
}
}
It this example you need to pass in your model to ContentView as an environment object:
ContentView().environmentObject(NavigationModel())
in the SceneDelegate and SwiftUI Previews.
The model conforms to ObservableObject protocol and the property must have a #Published attribute.
(it works within a List)
I also just used:
NavigationLink(destination: View()....) {
Text("Demo")
}.task { do your stuff here }
iOS 15.3 deployment target.

SwiftUI dismiss modal sheet presented from NavigationView (Xcode Beta 5)

I am attempting to dismiss a modal view presented via a .sheet in SwiftUI - called by a Button which is within a NavigationViews navigationBarItems, as per below:
struct ModalView : View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button(action: {
self.presentationMode.value.dismiss()
}, label: { Text("Save")})
}
}
struct ContentView : View {
#State var showModal: Bool = false
var body: some View {
NavigationView {
Text("test")
.navigationBarTitle(Text("Navigation Title Text"))
.navigationBarItems(trailing:
Button(action: {
self.showModal = true
}, label: { Text("Add") })
.sheet(isPresented: $showModal, content: { ModalView() })
)
}
}
}
The modal does not dismiss when the Save button is tapped, it just remains on screen. The only way to get rid of it is swiping down on the modal.
Printing the value of self.presentationMode.value always shows false so it seems to think that it hasn't been presented.
This only happens when it is presented from the NavigationView. Take that out and it works fine.
Am I missing something here, or is this a beta issue?
You need to move the .sheet outside the Button.
NavigationView {
Text("test")
.navigationBarTitle(Text("Navigation Title Text"))
.navigationBarItems(trailing:
Button("Add") {
self.showModal = true
}
)
.sheet(isPresented: $showModal, content: { ModalView() })
}
You can even move it outside the NavigationView closure.
NavigationView {
Text("test")
.navigationBarTitle(Text("Navigation Title Text"))
.navigationBarItems(trailing:
Button("Add") { self.showModal = true }
)
}
.sheet(isPresented: $showModal, content: { ModalView() })
Notice you can also simplify the Button call if you have a simple text button.
The solution is not readily apparent in the documentation and most tutorials opt for simple solutions. But I really wanted a button in the NavigationBar of the sheet that would dismiss the sheet. Here is the solution in six steps:
Set the DetailView to not show.
Add a button to set the DetailView to show.
Call the .sheet(isPresented modifier to display the sheet.
Wrap the view that will appear in the sheet in a NavigationView because we want to display a .navigationBarItem button.
PresentationMode is required to dismiss the sheet view.
Add a button to the NavBar and call the dismiss method.
import SwiftUI
struct ContentView: View {
// 1
#State private var showingDetail = false
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Button("Show Detail") {
showingDetail = true // 2
}
// 3
.sheet(isPresented: $showingDetail) {
// 4
NavigationView {
DetailView()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct DetailView: View {
// 5
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Detail View!")
// 6
.navigationBarItems(leading: Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "x.circle")
.font(.headline)
.foregroundColor(.accentColor)
})
}
}