SwiftUI Combine: TabView is not updating on selection when property stored in different viewmodel - ios

I'm working on Tabview with page style and I want to scroll tabview on button actions. Buttons are added inside NavigationMenu.
NavigationMenu view and NavigationModel(ViewModel) are separated from a parent.
Selection handling is done inside NavigationModel.
On tab page swipe I'm able to see the change in NavigationMenu which is fine.
But if I tap on buttons the tabview page is not swiping. Even I receive change event on method onReceive.
Code:
import SwiftUI
import Combine
final class NavigationModel: ObservableObject {
#Published var selectedItem = ""
#Published var items: [String] = [
"Button 1", "Button 2", "Button 3"
]
}
struct NavigationMenu: View {
#ObservedObject var viewModel: NavigationModel
var body: some View {
HStack {
ForEach(0..<3, id: \.self) { index in
let title = viewModel.items[index]
Button {
viewModel.selectedItem = title
} label: {
Text(title)
.font(.system(.body))
.padding()
.foregroundColor(
viewModel.selectedItem == title ? .white : .black
)
.background(viewModel.selectedItem == title ? .black : .yellow)
}
}
}
}
}
final class TabViewModel: ObservableObject {
var navModel = NavigationModel()
}
struct TabviewWithMenuView: View {
#ObservedObject var viewModel = TabViewModel()
var body: some View {
parentView
}
private var parentView: some View {
VStack(spacing: 0) {
Spacer()
NavigationMenu(viewModel: viewModel.navModel)
pageView
}
.onReceive(viewModel.navModel.$selectedItem) { output in
print("Button tapped:", output)
}
}
private var pageView: some View {
TabView(selection: $viewModel.navModel.selectedItem) {
ForEach(0..<3, id: \.self) { index in
let tag = viewModel.navModel.items[index]
item(tag: tag)
.tag(tag)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.transition(.slide)
}
private func item(tag: String) -> some View {
VStack {
Text("PAGE: " + tag)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
}
}
Image:

ObservableObject inside ObservableObject is not observed, we need to observe explicitly the instance which is changed.
A possible solution in this case is to separate PageView and inject navigation view model to it so it would be observed.
Tested with Xcode 13.3 / iOS 15.4
Here is main part:
NavigationMenu(viewModel: viewModel.navModel)
PageView(navModel: viewModel.navModel)
...
struct PageView: View {
#ObservedObject var navModel: NavigationModel
var body: some View {
pageView
}
// ....
}
Test module in project is here

Related

How do I change TabView from a button in a separate file?

I am trying to create a button that changes the TabView when tapped. Everything I have tried so far has not worked. I think need to use the #Binding property wrapper for currentTab but can't seem to get it to work.
Here is the file with the TabView:
struct MainPage: View {
#StateObject var appModel: AppViewModel = .init()
#Namespace var animation
// Hiding Tab Bar...
init () {
UITabBar.appearance().isHidden = true
}
var body: some View {
VStack(spacing: 0){
// Tab View...
TabView(selection: $appModel.currentTab) {
View1()
.tag(Tab.View1)
.setUpTab()
View2()
.tag(Tab.View2)
.setUpTab()
View3()
.tag(Tab.View3)
.setUpTab()
View4()
.tag(Tab.View4)
.setUpTab()
View5()
.tag(Tab.View5)
.setUpTab()
}
.overlay(alignment: .bottom) {
CustomTabBar(currentTab: $appModel.currentTab, animation: animation)
.offset(y: appModel.showDetailViewTab ? 150 : 0)
}
}
}
Here is the AppViewModel file:
class AppViewModel: ObservableObject {
#Published var currentTab: Tab = .Market
}
Here is the button I want to change the tab view:
struct View1Button: View {
var body: some View {
Button(action: {
print("Take to View1 Button Tapped...")
}) {
HStack {
Image("View1Icon")
VStack {
Text("Go to View1")
}
}
}
}
}
}
How do I change the tab view when this button is tapped?
I think this code does what you are trying to achieve.
Basically, you have to share the state property among the views. This way, the tab can be changed programmatically.
enum Tab {
case View1
case View2
case View3
case View4
case View5
}
struct ContentView: View {
#StateObject var appModel = AppViewModel()
init () {
UITabBar.appearance().isHidden = true
}
var body: some View {
VStack(spacing: 0) {
TabView(selection: $appModel.currentTab) {
CustomView(tab: $appModel.currentTab, viewNumber: 1)
.tag(Tab.View1)
CustomView(tab: $appModel.currentTab, viewNumber: 2)
.tag(Tab.View2)
CustomView(tab: $appModel.currentTab, viewNumber: 3)
.tag(Tab.View3)
CustomView(tab: $appModel.currentTab, viewNumber: 4)
.tag(Tab.View4)
CustomView(tab: $appModel.currentTab, viewNumber: 5)
.tag(Tab.View5)
}
.overlay(alignment: .bottom) {
CustomTabBar(currentTab: $appModel.currentTab)
}
}
}
}
class AppViewModel: ObservableObject {
#Published var currentTab: Tab = .View1
}
struct CustomView: View {
#Binding var tab: Tab
let viewNumber: Int
private let views: [Tab] = [.View1, .View2, .View3, .View4, .View5]
var body: some View {
Button("I'm view number \(viewNumber)") {
tab = views.randomElement()!
}
}
}
struct CustomTabBar: View {
#Binding var currentTab: Tab
var body: some View {
HStack(spacing: 25) {
TabButton(currentTab: $currentTab, name: "1.circle.fill", tab: .View1)
TabButton(currentTab: $currentTab, name: "2.circle.fill", tab: .View2)
TabButton(currentTab: $currentTab, name: "3.circle.fill", tab: .View3)
TabButton(currentTab: $currentTab, name: "4.circle.fill", tab: .View4)
TabButton(currentTab: $currentTab, name: "5.circle.fill", tab: .View5)
}
}
}
struct TabButton: View {
#Binding var currentTab: Tab
let name: String
let tab: Tab
var body: some View {
Image(systemName: name)
.resizable()
.frame(width: 25, height: 25)
.foregroundColor(currentTab == tab ? .red : .blue)
}
}

SwiftUI - NavigationLink cell in a Form stays highlighted after detail pop

In iOS 14, it appears that NavigationLinks do not become deselected after returning in a Form context.
This is also true for Form Pickers and anything else that causes the presentation of another View from a list (giving a highlight context to the presenting cell).
I didn't notice this behaviour in iOS 13.
Is there a way to 'deselect' the highlighted row once the other view is dismissed?
Example code:
struct ContentView: View {
var body: some View {
Form {
NavigationLink(destination: Text("Detail")) {
Text("Link")
}
}
}
}
(Different) Example visual:
In my case this behaviour appeared when using any Viewcontent (e.g. Text(), Image(), ...) between my NavigationView and List/Form.
var body: some View {
NavigationView {
VStack {
Text("This text DOES make problems.")
List {
NavigationLink(destination: Text("Doesn't work correct")) {
Text("Doesn't work correct")
}
}
}
}
}
Putting the Text() beneath the List does not make any problems:
var body: some View {
NavigationView {
VStack {
List {
NavigationLink(destination: Text("Does work correct")) {
Text("Does work correct")
}
}
Text("This text doesn't make problems.")
}
}
}
This is definitely a XCode 12 bug. As more people report this, as earlier it gets resolved.
I have also run into this issue and believed I found the root cause in my case.
In my case I had.a structure like the following:
struct Page1View: View {
var body: some View {
NavigationView {
List {
NavigationLink("Page 2", destination: Page2View())
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 1")
}
}
}
struct Page2View: View {
var body: some View {
List {
NavigationLink("Page 3", destination: Text("Page 3"))
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 2")
}
}
This issue would occur on the NavigationLink to Page 3. In the console output this error was showing when that link was used:
2021-02-13 16:41:00.599844+0000 App[59157:254215] [Assert] displayModeButtonItem is internally managed and not exposed for DoubleColumn style. Returning an empty, disconnected UIBarButtonItem to fulfill the non-null contract.
I discovered that I needed to apply .navigationViewStyle(StackNavigationViewStyle()) to the NavigationView and this solved the problem.
I.e.
struct Page1View: View {
var body: some View {
NavigationView {
List {
NavigationLink("Page 2", destination: Page2View())
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 1")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
Been fighting this issue half day today and came to this post that helped me to understand that issue appears if Text, Button or something else placed between NavigationView and in my case List. And I found solution that worked for me. Just add .zIndex() for the item. .zIndex() must be higher than for List Tried with Xcode 12.5.
var body: some View {
NavigationView {
VStack {
Text("This text DOES make problems.")
.zIndex(1.0)
List {
NavigationLink(destination: Text("Doesn't work correct")) {
Text("Doesn't work correct")
}
}
}
}
}
I did a bit more tinkering, it turns out this was caused due by having the UIHostingController being nested in a UINavigationController and using that navigation controller. Changing the navigation stack to use a SwiftUI NavigationView instead resolved this issue.
Similar to what #pawello2222 says in the question comments, I think the underlying cause is something to do with SwiftUI not understanding the proper navigation hierarchy when the external UINavigationController is used.
This is just one instance where this is fixed though, I'm still experiencing the issue in various other contexts depending on how my view is structured.
I've submitted an issue report FB8705430 to Apple, so hopefully this is fixed sometime soon.
Before (broken):
struct ContentView: View {
var body: some View {
Form {
NavigationLink(destination: Text("test")) {
Text("test")
}
}
}
}
// (UIKit presentation context)
let view = ContentView()
let host = UIHostingController(rootView: view)
let nav = UINavigationController(rootViewController: host)
present(nav, animated: true, completion: nil)
After (working):
struct ContentView: View {
var body: some View {
NavigationView {
Form {
NavigationLink(destination: Text("test")) {
Text("test")
}
}
}
}
}
// (UIKit presentation context)
let view = ContentView()
let host = UIHostingController(rootView: view)
present(host, animated: true, completion: nil)
This is definitely a bug in List, for now, my work-around is refreshing the List by changing the id, like this:
struct YourView: View {
#State private var selectedItem: String?
#State private var listViewId = UUID()
var body: some View {
List(items, id: \.id) {
NavigationLink(destination: Text($0.id),
tag: $0.id,
selection: $selectedItem) {
Text("Row \($0.id)")
}
}
.id(listViewId)
.onAppear {
if selectedItem != nil {
selectedItem = nil
listViewId = UUID()
}
}
}
}
I made a modifier based on this that you can use:
struct RefreshOnAppearModifier<Tag: Hashable>: ViewModifier {
#State private var viewId = UUID()
#Binding var selection: Tag?
func body(content: Content) -> some View {
content
.id(viewId)
.onAppear {
if selection != nil {
viewId = UUID()
selection = nil
}
}
}
}
extension View {
func refreshOnAppear<Tag: Hashable>(selection: Binding<Tag?>? = nil) -> some View {
modifier(RefreshOnAppearModifier(selection: selection ?? .constant(nil)))
}
}
use it like this:
List { ... }
.refreshOnAppear(selection: $selectedItem)
I managed to solve it by adding ids to the different components of the list, using binding and resetting the binding on .onDisappear
struct ContentView: View {
#State var selection: String? = nil
var body: some View {
NavigationView {
VStack {
Text("Hello, world!")
.padding()
List {
Section {
NavigationLink( destination: Text("Subscreen1"), tag: "link1", selection: $selection ) {
Text("Subscreen1")
}.onDisappear {
self.selection = nil
}
NavigationLink( destination: Text("Subscreen2"), tag: "link2", selection: $selection ) {
Text("Subscreen2")
}.onDisappear {
self.selection = nil
}
}.id("idSection1")
}
.id("idList")
}
}
}
}
I've also run into this issue and it seemed related to sheets as mentioned here.
My solution was to swizzle UITableView catch selections, and deselect the cell. The code for doing so is here. Hopefully this will be fixed in future iOS.
Adding .navigationViewStyle(StackNavigationViewStyle()) to NavigationView fixed it for me.
Suggested in this thread: https://developer.apple.com/forums/thread/660468
This is my solution to this issue.
// This in a stack in front of list, disables large navigation title from collapsing by disallowing list from scrolling on top of navigation title
public struct PreventCollapseView: View {
#State public var viewColor: Color?
public init(color: Color? = nil) {
self.viewColor = color
}
public var body: some View {
Rectangle()
.fill(viewColor ?? Color(UIColor(white: 0.0, alpha: 0.0005)))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 1)
}
}
// handy modifier..
extension List {
public func uncollapsing(_ viewUpdater: Bool) -> some View {
VStack(spacing: 0.0) {
Group {
PreventCollapseView()
self
}.id(viewUpdater)
}
}
}
struct TestView: View {
#State var updater: Bool = false
var body: some View {
List {
Text("Item one")
Text("Item two")
Text("Manually refresh")
.onTapGesture { DispatchQueue.main.async { updater.toggle() } }
.onAppear { print("List was refreshed") }
}
.uncollapsing(updater)
.clipped()
.onAppear { DispatchQueue.main.async { updater.toggle() }} // Manually refreshes list always when re-appearing/appearing
}
}
Add a NavigationView, configure for largeTitle, and embed TestView and it's all set. Toggle updater to refresh.
Having the same Problem. The weird thing is, that the exact same code worked in iOS13.
I'm having this issue with a simple list:
struct TestList: View {
let someArray = ["one", "two", "three", "four", "five"]
var body: some View {
List(someArray, id: \.self) { item in
NavigationLink(
destination: Text(item)) {
Text(item)
}.buttonStyle(PlainButtonStyle())
}.navigationBarTitle("testlist")
}
}
This is embedded in:
struct ListControllerView: View {
#State private var listPicker = 0
var body: some View {
NavigationView{
Group{
VStack{
Picker(selection: $listPicker, label: Text("Detailoverview")) {
Text("foo").tag(0)
Text("bar").tag(1)
Text("TestList").tag(2)
}
This is inside a Tabbar.
This is the workaround I've been using until this List issue gets fixed. Using the Introspect library, I save the List's UITableView.reloadData method and call it when it appears again.
import SwiftUI
import Introspect
struct MyView: View {
#State var reload: (() -> Void)? = nil
var body: some View {
NavigationView {
List {
NavigationLink("Next", destination: Text("Hello"))
}.introspectTableView { tv in
self.reload = tv.reloadData
}.onAppear {
self.reload?()
}
}
}
}

SwiftUI programmatic navigation from within list

I have a navigation requirement that looks something like this:
Each detail screen can navigation to the next and previous detail screen. At the same time, the "back" button should always go back to the main list (not the previous detail screen).
I'm struggling with how to accomplish this in SwiftUI?
Here is what I have so far:
struct ListView: View {
#State private var currentDetailShown: Int?
#State private var listItems: [Int] = Array(repeating: 0, count: 10)
func goToNext() {
if let idx = self.currentDetailShown {
self.currentDetailShown = min(self.listItems.count - 1, idx + 1)
}
}
func goToPrev() {
if let idx = self.currentDetailShown {
self.currentDetailShown = max(0, idx - 1)
}
}
var body: some View {
List {
ForEach(0..<listItems.count) { index in
NavigationLink(destination: DetailView(goToNext: self.goToNext, goToPrev: self.goToPrev),
tag: index,
selection: self.$currentDetailShown) {
ListItem(score: listItems[index])
}
.isDetailLink(false)
.onTapGesture {
self.currentDetailShown = index
}
}
}
}
}
What happens with this code is that from the first detail view, it'll move to the to the next detail view and then immediately jump back to the list view.
I feel like I'm overthinking this or missing something obvious...
Instead of navigating to each detail from your list, you can navigate to a detailView that can show each detail individually by using a published variable in an observable object. Here is an example
struct MainView: View{
#EnvironmentObject var viewModel: ViewModel
var body: some View{
NavigationView{
VStack{
ForEach(self.viewModel.details, id:\.self){ detail in
NavigationLink(destination: DetailView(detail: self.viewModel.details.firstIndex(of: detail)!).environmentObject(ViewModel())){
Text(detail)
}
}
}
}
}
}
class ViewModel: ObservableObject{
#Published var showingView = 0
#Published var details = ["detail1", "detail2", "detail3", "detail4", "detail5", "detail6"]
}
struct DetailView: View{
#EnvironmentObject var viewModel: ViewModel
#State var detail: Int
var body: some View{
VStack{
IndivisualDetailView(title: viewModel.details[detail])
Button(action: {
self.viewModel.showingView -= 1
}, label: {
Image(systemName: "chevron.left")
})
Button(action: {
self.viewModel.showingView += 1
print(self.viewModel.showingView)
}, label: {
Image(systemName: "chevron.right")
})
}
}
}
struct IndivisualDetailView: View{
#State var title: String
var body: some View{
Text(title)
}
}

SwiftUI Programmatically Select List Item

I have a SwiftUI app with a basic List/Detail structure. A new item is created from
a modal sheet. When I create a new item and save it I want THAT list item to be
selected. As it is, if no item is selected before an add, no item is selected after
an add. If an item is selected before an add, that same item is selected after the
add.
I'll include code for the ContentView, but this is really the simplest example of
List/Detail.
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addNewResort = false
#State private var coverDeletedDetail = false
#Environment(\.presentationMode) var presentationMode
var body: some View {
List {
ForEach(resortStore.resorts) { resort in
NavigationLink(destination: ResortView(resort: resort)) {
HStack(spacing: 20) {
Image("FlatheadLake1")
//bunch of modifiers
VStack(alignment: .leading, spacing: 10) {
//the cell contents
}
}
}
}
.onDelete { indexSet in
self.removeItems(at: [indexSet.first!])
self.coverDeletedDetail.toggle()
}
if UIDevice.current.userInterfaceIdiom == .pad {
NavigationLink(destination: WelcomeView(), isActive: self.$coverDeletedDetail) {
Text("")
}
}
}//list
.onAppear(perform: self.selectARow)
.navigationBarTitle("Resorts")
.navigationBarItems(leading:
//buttons
}//body
func removeItems(at offsets: IndexSet) {
resortStore.resorts.remove(atOffsets: offsets)
}
func selectARow() {
//nothing that I have tried works here
print("selectARow")
}
}//struct
And again - the add item modal is extremely basic:
struct AddNewResort: View {
//bunch of properties
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName, country: self.resortCountry, description: self.resortDescription, imageCredit: "Credit", price: Int(self.resortPriceString) ?? 0, size: Int(self.resortSizeString) ?? 0, snowDepth: 20, elevation: 3000, runs: 40, facilities: ["bar", "garage"])
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
To show the issue - The list with a selection:
The list after a new item created showing the previous selection:
Any guidance would be appreciated. Xcode 11.4
I tried to reconstitute your code as closely as could so that it builds. Here is what I have in the end. We have a list of resorts and when a new resort is saved in the AddNewResort sheet, if we are currently in split view (horizontalSizeClass is regular), we will select the new resort, otherwise just dismiss the sheet.
import SwiftUI
class ResortStore: ObservableObject {
#Published var resorts = [Resort(id: UUID(), name: "Resort 1")]
}
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addingNewResort = false
#State var selectedResortId: UUID? = nil
var navigationLink: NavigationLink<EmptyView, ResortView>? {
guard let selectedResortId = selectedResortId,
let selectedResort = resortStore.resorts.first(where: {$0.id == selectedResortId}) else {
return nil
}
return NavigationLink(
destination: ResortView(resort: selectedResort),
tag: selectedResortId,
selection: $selectedResortId
) {
EmptyView()
}
}
var body: some View {
NavigationView {
ZStack {
navigationLink
List {
ForEach(resortStore.resorts, id: \.self.id) { resort in
Button(action: {
self.selectedResortId = resort.id
}) {
Text(resort.name)
}
.listRowBackground(self.selectedResortId == resort.id ? Color.gray : Color(UIColor.systemBackground))
}
}
}
.navigationBarTitle("Resorts")
.navigationBarItems(trailing: Button("Add Resort") {
self.addingNewResort = true
})
.sheet(isPresented: $addingNewResort) {
AddNewResort(selectedResortId: self.$selectedResortId)
.environmentObject(self.resortStore)
}
WelcomeView()
}
}
}
struct ResortView: View {
let resort: Resort
var body: some View {
Text("Resort View for resort name: \(resort.name).")
}
}
struct AddNewResort: View {
//bunch of properties
#Binding var selectedResortId: UUID?
#State var resortName = ""
#Environment(\.presentationMode) var presentationMode
#Environment(\.horizontalSizeClass) var horizontalSizeClass
#EnvironmentObject var resortStore: ResortStore
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName)
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
if self.horizontalSizeClass == .regular {
self.selectedResortId = newResort.id
}
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
struct WelcomeView: View {
var body: some View {
Text("Welcome View")
}
}
struct Resort {
var id: UUID
var name: String
}
We need to keep track of the selectedResortId
We create an invisible NavigationLink that will programmatically navigate to the selected resort
We make our list row a Button, so that the user can select a resort by tapping on the row
I started writing a series of articles about navigation in SwiftUI List view, there are a lot of points to consider while implementing programmatic navigation.
Here is the one that describes this solution that I'm suggesting: SwiftUI Navigation in List View: Programmatic Navigation. This solution works at the moment on iOS 13.4.1. SwiftUI is changing rapidly, so we have to keep on checking.
And here is my previous article that explains why a more simple solution of adding a NavigationLink to each List row has some problems at the moment SwiftUI Navigation in List View: Exploring Available Options
Let me know if you have questions, I'd be happy to help where I can.

iOS SwiftUI: pop or dismiss view programmatically

I couldn't find any reference about any ways to make a pop or a dismiss programmatically of my presented view with SwiftUI.
Seems to me that the only way is to use the already integrated slide dow action for the modal(and what/how if I want to disable this feature?), and the back button for the navigation stack.
Does anyone know a solution?
Do you know if this is a bug or it will stays like this?
This example uses the new environment var documented in the Beta 5 Release Notes, which was using a value property. It was changed in a later beta to use a wrappedValue property. This example is now current for the GM version. This exact same concept works to dismiss Modal views presented with the .sheet modifier.
import SwiftUI
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(
"Here is Detail View. Tap to go back.",
action: { self.presentationMode.wrappedValue.dismiss() }
)
}
}
struct RootView: View {
var body: some View {
VStack {
NavigationLink(destination: DetailView())
{ Text("I am Root. Tap for Detail View.") }
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
RootView()
}
}
}
SwiftUI Xcode Beta 5
First, declare the #Environment which has a dismiss method which you can use anywhere to dismiss the view.
import SwiftUI
struct GameView: View {
#Environment(\.presentationMode) var presentation
var body: some View {
Button("Done") {
self.presentation.wrappedValue.dismiss()
}
}
}
iOS 15+
Starting from iOS 15 we can use a new #Environment(\.dismiss):
struct SheetView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
Text("Sheet")
.toolbar {
Button("Done") {
dismiss()
}
}
}
}
}
(There's no more need to use presentationMode.wrappedValue.dismiss().)
Useful links:
DismissAction
There is now a way to programmatically pop in a NavigationView, if you would like. This is in beta 5. Notice that you don't need the back button. You could programmatically trigger the showSelf property in the DetailView any way you like. And you don't have to display the "Push" text in the master. That could be an EmptyView(), thereby creating an invisible segue.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
MasterView()
}
}
}
struct MasterView: View {
#State private var showDetail = false
var body: some View {
VStack {
NavigationLink(destination: DetailView(showSelf: $showDetail), isActive: $showDetail) {
Text("Push")
}
}
}
}
struct DetailView: View {
#Binding var showSelf: Bool
var body: some View {
Button(action: {
self.showSelf = false
}) {
Text("Pop")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
I recently created an open source project called swiftui-navigation-stack (https://github.com/biobeats/swiftui-navigation-stack) that contains the NavigationStackView, an alternative navigation stack for SwiftUI. It offers several features described in the readme of the repo. For example, you can easily push and pop views programmatically. I'll show you how to do that with a simple example:
First of all embed your hierarchy in a NavigationStackVew:
struct RootView: View {
var body: some View {
NavigationStackView {
View1()
}
}
}
NavigationStackView gives your hierarchy access to a useful environment object called NavigationStack. You can use it to, for instance, pop views programmatically as asked in the question above:
struct View1: View {
var body: some View {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
VStack {
Text("VIEW 1")
Spacer()
PushView(destination: View2()) {
Text("PUSH TO VIEW 2")
}
}
}
}
}
struct View2: View {
#EnvironmentObject var navStack: NavigationStack
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.all)
VStack {
Text("VIEW 2")
Spacer()
Button(action: {
self.navStack.pop()
}, label: {
Text("PROGRAMMATICALLY POP TO VIEW 1")
})
}
}
}
}
In this example I use the PushView to trigger the push navigation with a tap. Then, in the View2 I use the environment object to programmatically come back.
Here is the complete example:
import SwiftUI
import NavigationStack
struct RootView: View {
var body: some View {
NavigationStackView {
View1()
}
}
}
struct View1: View {
var body: some View {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
VStack {
Text("VIEW 1")
Spacer()
PushView(destination: View2()) {
Text("PUSH TO VIEW 2")
}
}
}
}
}
struct View2: View {
#EnvironmentObject var navStack: NavigationStack
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.all)
VStack {
Text("VIEW 2")
Spacer()
Button(action: {
self.navStack.pop()
}, label: {
Text("PROGRAMMATICALLY POP TO VIEW 1")
})
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
RootView()
}
}
the result is:
Alternatively, if you don't want to do it programatically from a button, you can emit from the view model whenever you need to pop.
Subscribe to a #Published that changes the value whenever the saving is done.
struct ContentView: View {
#ObservedObject var viewModel: ContentViewModel
#Environment(\.presentationMode) var presentationMode
init(viewModel: ContentViewModel) {
self.viewModel = viewModel
}
var body: some View {
Form {
TextField("Name", text: $viewModel.name)
.textContentType(.name)
}
.onAppear {
self.viewModel.cancellable = self.viewModel
.$saved
.sink(receiveValue: { saved in
guard saved else { return }
self.presentationMode.wrappedValue.dismiss()
}
)
}
}
}
class ContentViewModel: ObservableObject {
#Published var saved = false // This can store any value.
#Published var name = ""
var cancellable: AnyCancellable? // You can use a cancellable set if you have multiple observers.
func onSave() {
// Do the save.
// Emit the new value.
saved = true
}
}
Please check Following Code it's so simple.
FirstView
struct StartUpVC: View {
#State var selection: Int? = nil
var body: some View {
NavigationView{
NavigationLink(destination: LoginView().hiddenNavigationBarStyle(), tag: 1, selection: $selection) {
Button(action: {
print("Signup tapped")
self.selection = 1
}) {
HStack {
Spacer()
Text("Sign up")
Spacer()
}
}
}
}
}
SecondView
struct LoginView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView{
Button(action: {
print("Login tapped")
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image("Back")
.resizable()
.frame(width: 20, height: 20)
.padding(.leading, 20)
}
}
}
}
}
You can try using a custom view and a Transition.
Here's a custom modal.
struct ModalView<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
if (!self.isShowing) {
self.content()
}
if (self.isShowing) {
self.content()
.disabled(true)
.blur(radius: 3)
VStack {
Text("Modal")
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.transition(.moveAndFade) // associated transition to the modal view
}
}
}
}
}
I reused the Transition.moveAndFade from the Animation Views and Transition tutorial.
It is defined like this:
extension AnyTransition {
static var moveAndFade: AnyTransition {
let insertion = AnyTransition.move(edge: .trailing)
.combined(with: .opacity)
let removal = AnyTransition.scale()
.combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
}
You can test it - in the simulator, not in the preview - like this:
struct ContentView: View {
#State var isShowingModal: Bool = false
func toggleModal() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation {
self.isShowingModal = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation {
self.isShowingModal = false
}
}
}
}
var body: some View {
ModalView(isShowing: $isShowingModal) {
NavigationView {
List(["1", "2", "3", "4", "5"].identified(by: \.self)) { row in
Text(row)
}.navigationBarTitle(Text("A List"), displayMode: .large)
}.onAppear { self.toggleModal() }
}
}
}
Thanks to that transition, you will see the modal sliding in from the trailing edge, and the it will zoom and fade out when it is dismissed.
The core concept of SwiftUI is to watch over the data flow.
You have to use a #State variable and mutate the value of this variable to control popping and dismissal.
struct MyView: View {
#State
var showsUp = false
var body: some View {
Button(action: { self.showsUp.toggle() }) {
Text("Pop")
}
.presentation(
showsUp ? Modal(
Button(action: { self.showsUp.toggle() }) {
Text("Dismiss")
}
) : nil
)
}
}
I experienced a compiler issue trying to call value on the presentationMode binding. Changing the property to wrappedValue fixed the issue for me. I'm assuming value -> wrappedValue is a language update. I think this note would be more appropriate as a comment on Chuck H's answer but don't have enough rep points to comment, I also suggested this change as and edit but my edit was rejected as being more appropriate as a comment or answer.
This will also dismiss the view
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
window?.rootViewController?.dismiss(animated: true, completion: {
print("dismissed")
})

Resources