Since only one NavigationView should be used in the view hierarchy, how do you deal with the case where the initial view to be presented is determined at app start?
#main
struct heartbreaksApp: App {
let token = UserDefaults.standard.value(forKey: "token")
var body: some Scene {
return WindowGroup {
if token != nil {
Campaigns()
} else {
Login()
}
}
}
}
So if the token is nil, we'll go to the Login where a NavigationView is declared, when we login a NavigationLink takes us to Campaigns view. However, if the token is not nil we're taken directly to the Campaigns view where we have no Navigation View and so cannot navigate from there to other views. If I do declare a Navigation View in Campaigns too, I'll end up with two causing all kinds of havoc, 2 back buttons for instance, one pointing to Login and one pointing to Campaigns. I'm obviously doing this wrong...Please see picture
Remove NavigationView from login and place it at top level, like
NavigationView { // << here !!
if token != nil {
Campaigns()
} else {
Login()
}
}
Related
I'm trying to create navigation for my app using Navigation Stack and routing.
My code is functioning and navigating to views, the problem I'm having is that the view is getting called several times from within a switch statement, I have placed the nav stack in the some scene view, then added a simple link, when tapped it goes through the switch statement and picks up the value 3 times and displays the view, I placed a print statement in the switch and it's printed 3 times for my new view value, following on with database calls etc, they are also getting called 3 times.
I'm new to SwiftUI so I'm sure it's user error, any help would be appreciated, thanks.
enum Routing : Hashable {
case AddFlight
case PilotsList
case newview
}
#State var navPath = NavigationPath()
var body: some Scene {
WindowGroup {
NavigationStack (path: $navPath) {
NavigationLink(value: Routing.newview, label: {Text("Go to new view")})
.navigationDestination(for: Routing.self) { route in
switch route {
case .newview:
Text("New View")
let a = print("New view")
case .PilotsList :
PilotsListView()
case .AddFlight:
AddEditFlightView()
}
}
}
}
}
Putting this in an answer because there is a code "fix" for the reprints.
Verified the behavior both in your code and some of my own existing case statements (XCode Version 14.0.1 (14A400)). Additionally the child view's init is called the same number of multiple times, but work in .onAppear() is only called the once. The number of extra calls seems to vary. Have also verified that it happens even when there isn't a case statement but a single possible ChildView.
This makes me think it may have to do with the Layout system negotiating a size for the view. This closure is a #ViewBuilder which we can tell because we can't just put a print statement into it directly. It's being treated as a subview that's negotiating it's size with the parent. Which makes sense in hindsight, but wow, good to know!
This means that items that should only happen once should go in the .onAppear() code of the child view instead of inside of the destination closure which is a #ViewBuilder.
This code is fairly different than yours but that was mostly to check that the effect wasn't some quirk. The key is that it will only do the "onAppear" task once. Note that the perform closure for .onAppear is NOT a ViewBuilder, it is of type () -> Void. It is possible you might prefer .task{} to do an async database call, and that will also just run the once, it looks like.
struct ThreeTimePrintView:View {
#EnvironmentObject var nav:NavigationManager
var body: some View {
NavigationStack (path: $nav.navPath) {
Button("New View") {
nav.navPath.append(Routing.newview)
}.navigationDestination(for: Routing.self) { route in
switch route {
case .newview:
buildNewView().onAppear() { print("New View onAppear") }
case .PilotsList :
Text("Pilot View")
case .AddFlight:
Text("FligtView")
}
}
}
}
func buildNewView() -> some View {
print("New view")
return Text("New View")
}
}
import SwiftUI
enum Routing : Hashable {
case AddFlight
case PilotsList
case newview
}
final class NavigationManager:ObservableObject {
#Published var navPath = NavigationPath()
}
#main
struct ThreePrintCheckerApp: App {
#StateObject var nav = NavigationManager()
var body: some Scene {
WindowGroup {
ThreeTimePrintView().environmentObject(nav)
}
}
}
Partway through the 2020 Apple platform API betas, the method mentioned in the subject was added. (A similar method was added to AnyView.) Does anyone know where the corresponding API to send the external events in the first place is?
Sample for open new window in macOS using sending of Scene.handlesExternalEvents
#main
struct TestAppApp: App {
var body: some Scene {
WindowGroup {
MainView()
}
//subscribe on event of open mainView
.handlesExternalEvents(matching: Set(arrayLiteral: Wnd.mainView.rawValue))
WindowGroup {
HelperView()
}
//subscribe on event of open helperView
.handlesExternalEvents(matching: Set(arrayLiteral: Wnd.helperView.rawValue))
}
}
enum Wnd: String, CaseIterable {
case mainView = "MainView"
case helperView = "OtherView"
func open(){
if let url = URL(string: "taotao://\(self.rawValue)") {
print("opening \(self.rawValue)")
NSWorkspace.shared.open(url)
}
}
}
and 2 Windows/Views code:
extension TestAppApp {
struct MainView: View {
var body: some View {
VStack {
Button("Open Main View") {
Wnd.mainView.open()
}
Button("Open Other View") {
Wnd.helperView.open()
}
}
.padding(150)
}
}
struct HelperView: View {
var body: some View {
HStack {
Text("This is ") + Text("Helper View!").bold()
}
.padding(150)
}
}
}
Info.plist changes also needed :
My understanding is that this modifier which works on WindowGroups or Views is there to indicate that the Scene or View supports NSUserActivity which are sent by Handoff, Spotlight, SiriKit or Universal Links. In your Info.plist you declare activities that your app supports (as String identifiers) and in your App structure you indicate which Scene handles which activity so the frameworks can decide the proper Scene to open. In other words, it is not for sending NSEvents within different objects of your app but rather to let SwiftUI know which View or Scene can handle a user activity that your app advertises.
The root view of my onboarding process has a NavigationView. The root view of my app is a login page that also contains a NavigationView. That means when someone launches the app for the first time, they will go through the onboarding process and land at the login screen - resulting in a navigation view within a navigation view.
Is there a way to reset the view stack or simply remove the extra navigation view when necessary?
This is how I implemented #New Dev's solution below. First comes the Tower class. (The name helps me visualize the fact that it's an ObservableObject.) Its job is to keep track of the currentPage and let interested views know when it has changed.
import Foundation
import SwiftUI
import Combine
class Tower: ObservableObject {
enum Views {
case onboarding, login, dashboard
}
let objectWillChange = PassthroughSubject<Tower, Never>()
#Published var currentPage: Views = .onboarding {
didSet {
objectWillChange.send(self)
}
}
}
Next comes the ConductorView. It is notified by the Tower when currentPage changes, and loads the corresponding view.
struct ConductorView: View {
#EnvironmentObject var tower: Tower
var body: some View {
VStack {
if tower.currentPage == .onboarding {
ContentViewA()
} else if tower.currentPage == .login {
ContentViewB()
}
}
}
}
And lastly, a content view.
struct ContentViewA: View {
#EnvironmentObject var tower: Tower
var body: some View {
Button(action: {
self.tower.currentPage = .login
}) {
Text("Go to Login")
}
}
}
}
In addition to New Devs greatly appreciated solution, I also used this article from BLCKBIRDS.
I'll expand on my comment. NavigationView/NavigationLink aren't the only ways to change views - a simple conditional can also be used to determine which view is rendered.
So, say, you have some class that contains the state of the login/onboarding information:
class AppState: ObservableObject {
enum UserFlow {
case onboarding, login, home
}
#Published var userFlow: UserFlow = .onboarding
// ...
}
Then your RootView could determine which user flow to show:
struct RootView: View {
#EnvironmentObject var appState: AppState
var body: some View {
if appState.userFlow == .onboarding {
OnboardingRootView()
} else if appState.userFlow == .login {
LoginRootView()
} else {
ContentView()
}
}
}
I'm working on a SwiftUI practice app and I ran into an issue with the NavigationView/NavigationLink. I am currently using the Metropolitan Museum of Art API and I wanted to make a list of departments that segues to another list of objects in that department, then that list segues to the object's information. Currently the NavigationView/NavigationLink setup I have is creating multiple NavigationViews and is resulting in multiple back buttons/navigation bars. Is there a way to have the new NavigationView replace the old one or have them work in line with one another? The only way I know how to create a segue in SwiftUI is through a NavigationView/NavigationLink but creating it twice seems to be the wrong way to go about things. I have a screen shot of the current state of my app.
App Image
This is my code at the moment.
struct ContentView: View {
#ObservedObject var model = DepartmentListViewModel()
var body: some View {
NavigationView {
List {
ForEach(model.departments, id: \.self) { department in
NavigationLink(destination: DetailView(viewModel: DetailListViewModel(selectedDepartment: department))) {
Text(department.displayName)
}
}
}
.navigationBarTitle("Departments")
}
}
}
struct DetailView: View {
#ObservedObject var viewModel: DetailListViewModel
init(viewModel: DetailListViewModel) {
self.viewModel = viewModel
}
var body: some View {
NavigationView {
List {
ForEach(viewModel.objects, id: \.self) { object in
NavigationLink(destination: ObjectView(viewModel: ObjectListViewModel(selectedObject: object))) {
Text(String(object))
}
}
}
.navigationBarTitle("ObjectIDs")
}
}
}
You don't need NavigationView in your DetailView anymore, the first one handle it
I am creating a sign in page for my app and would like to present the home screen in a way that the user can not go back. In Swift UI how do I present it so the new view does not present in a card like style? I know this presenting style is now default for iOS 13.
This is what I already have.
import SwiftUI
struct Test : View {
var body: some View {
PresentationButton(Text("Click to show"), destination: Extra() )
}
}
I would like the view to present full screen.
Use a NavigationView with a NavigationButton and hide the destination view's navigation bar's back button.
For example:
struct ContentView : View {
let destinationView = Text("Destination")
.navigationBarItem(title: Text("Destination View"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Tap Here")
}
}
}
}
You can also disable the destination view's navigation bar altogether by doing let destinationView = Text("Destination").navigationBarHidden(true).