I have a picker that works fine until after showing and dismissing a fullScreenCover or a sheet. Does anyone know what the problem is with this sample code, or have a work-around?
I have tried dismissing the sheet using self.presentation.wrappedValue.dismiss() as well, but with the same result.
Example gif: https://i.stack.imgur.com/zmcmv.gif
Code:
import SwiftUI
struct ContentView: View {
#State var selectedFilterStatus = ActiveStatus.active
#State var showDetail = false
var body: some View {
NavigationView {
VStack {
Button(action: {
showDetail.toggle()
}, label: {
Text("Detail popup")
})
Picker("\(selectedFilterStatus.title)", selection: $selectedFilterStatus) {
Text(ActiveStatus.active.title).tag(ActiveStatus.active)
Text(ActiveStatus.inactive.title).tag(ActiveStatus.inactive)
}
}
.fullScreenCover(isPresented: $showDetail, content: {
MyDetailsView(presenting: $showDetail)
})
}
.navigationTitle("Main")
}
}
struct MyDetailsView: View {
#Binding var presenting: Bool
var body: some View {
VStack {
Text("Hello from details!")
Button(action: {
presenting.toggle()
}, label: {
HStack {
Image(systemName: "chevron.left")
Text("Back")
}
})
}
}
}
enum ActiveStatus: String, CaseIterable, Identifiable {
case active
case inactive
var id: String { self.rawValue }
}
extension ActiveStatus {
var title: String {
switch self {
case .active:
return "Active for sale"
case .inactive:
return "Inactive"
}
}
}
I totally agree there is a bug in the system. However, you can get around it.
This is the workaround that works for me, tested on ios-15 and macCatalyst (macos12.01) devices:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var selectedFilterStatus = ActiveStatus.active
#State var showDetail: ActiveStatus? // <-- here
var body: some View {
NavigationView {
VStack {
Button(action: {
showDetail = ActiveStatus.active // <-- here
}, label: { Text("Detail popup") })
Picker("\(selectedFilterStatus.title)", selection: $selectedFilterStatus) {
Text(ActiveStatus.active.title).tag(ActiveStatus.active)
Text(ActiveStatus.inactive.title).tag(ActiveStatus.inactive)
}.pickerStyle(.menu)
}
// -- here --
.fullScreenCover(item: $showDetail) { _ in
MyDetailsView()
}
}
.navigationViewStyle(.stack)
.navigationTitle("Main")
}
}
struct MyDetailsView: View {
#Environment(\.dismiss) var dismiss // <-- here
var body: some View {
VStack {
Text("Hello from details!")
Button(action: {
dismiss() // <-- here
}, label: {
HStack {
Image(systemName: "chevron.left")
Text("Back")
}
})
}
}
}
enum ActiveStatus: String, CaseIterable, Identifiable {
case active
case inactive
var id: String { self.rawValue }
}
extension ActiveStatus {
var title: String {
switch self {
case .active:
return "Active for sale"
case .inactive:
return "Inactive"
}
}
}
Related
I noticed issue in SwiftUI when using NavigationStack
Once I swipe-back on a half and revert it -> it stops working
Also I attached sample code if you want to try it
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
ListView()
}
}
}
struct ListView: View {
var body: some View {
List {
NavigationLink(destination: ViewA(viewModel: .init()), label: {
Text("A")
})
NavigationLink(destination: ViewB(), label: {
Text("B")
})
}
}
}
struct ViewA: View {
#StateObject var viewModel: Observed
var body: some View {
ZStack {
List {
Button(action: {
viewModel.action()
}, label: {
Text("label")
})
}
NavigationLink(isActive: $viewModel.shouldShowViewB, destination: {
ViewB()
}, label: {EmptyView()})
}
.navigationTitle("view a")
}
}
struct ViewB: View {
var body: some View {
List {
Button(action: {
print("actionb")
}, label: {
Text("labelb")
})
}
.navigationTitle("view b")
}
}
class Observed: ObservableObject {
#Published var shouldShowViewB = false
func action() {
print("action from model")
shouldShowViewB = true
}
}
Expected: whatever I do it should work as expected - when I tap it should open new view
Anyone else found this issue? How to fix it?
Issue 1 is you create the ObservedObject inside the NavigationLink with .init and then have a #StateObject declaration in the Subview ViewA(). That doesn't feel right. Create the Object with #StateObject in the parent view and pass it down.
Issue 2 is the new SwiftUI Navigation model, with NavigationLink)destination: label:) being deprecated. I adapted your code to the new navigation logic:
struct ContentView: View {
var body: some View {
NavigationStack {
ListView()
}
}
}
struct ListView: View {
#StateObject var viewModel = Observed() // create ObservedObject here
var body: some View {
List {
NavigationLink("A") {
ViewA(viewModel: viewModel) // pass down
}
NavigationLink("B") {
ViewB()
}
}
}
}
struct ViewA: View {
#ObservedObject var viewModel: Observed // passed down Object
var body: some View {
ZStack {
List {
Button(action: {
viewModel.action()
print(viewModel.shouldShowViewB)
}, label: {
Text("label")
})
}
.navigationDestination(isPresented: $viewModel.shouldShowViewB, destination: { ViewB() })
}
.navigationTitle("view a")
}
}
struct ViewB: View {
var body: some View {
List {
Button(action: {
print("actionb")
}, label: {
Text("labelb")
})
}
.navigationTitle("view b")
}
}
class Observed: ObservableObject {
#Published var shouldShowViewB = false
func action() {
print("action from model")
shouldShowViewB = true
}
}
I have a View with a search button in the toolbar. The search button presents a sheet to the user and when he clicks on a result I would like the sheet to be dismissed and a detailView to be opened rather than navigating to the detailView from inside the sheet. The dismiss part is easy, but how do I open the detailView in the NavigationStack relative to the original View that presented the Sheet?
I'm also getting an error on the navigationStack initialization.
HomeScreen:
struct CatCategoriesView: View {
#StateObject private var vm = CatCategoriesViewModel(service: Webservice())
#State var showingSearchView = false
#State var path: [CatDetailView] = []
var body: some View {
NavigationStack(path: $path) { <<-- Error here "No exact matches in call to initializer "
ZStack {
Theme.backgroundColor
.ignoresSafeArea()
ScrollView {
switch vm.state {
case .success(let cats):
LazyVStack {
ForEach(cats, id: \.id) { cat in
NavigationLink {
CatDetailView(cat: cat)
} label: {
CatCategoryCardView(cat: cat)
.padding()
}
}
}
case .loading:
ProgressView()
default:
EmptyView()
}
}
}
.navigationTitle("CatPedia")
.toolbar {
Button {
showingSearchView = true
} label: {
Label("Search", systemImage: "magnifyingglass")
}
}
}
.task {
await vm.getCatCategories()
}
.alert("Error", isPresented: $vm.hasError, presenting: vm.state) { detail in
Button("Retry") {
Task {
await vm.getCatCategories()
}
}
} message: { detail in
if case let .failed(error) = detail {
Text(error.localizedDescription)
}
}
.sheet(isPresented: $showingSearchView) {
SearchView(vm: vm, path: $path)
}
}
}
SearchView:
struct SearchView: View {
let vm: CatCategoriesViewModel
#Environment(\.dismiss) private var dismiss
#Binding var path: [CatDetailView]
#State private var searchText = ""
var body: some View {
NavigationStack {
List {
ForEach(vm.filteredCats, id: \.id) { cat in
Button(cat.name) {
dismiss()
path.append(CatDetailView(cat: cat))
}
}
}
.navigationTitle("Search")
.searchable(text: $searchText, prompt: "Find a cat..")
.onChange(of: searchText, perform: vm.search)
}
}
}
It can be a little tricky, but I'd suggest using a combination of Apple's documentation on "Control a presentation link programmatically" and shared state. To achieve the shared state, I passed a shared view model into the sheet.
I have simplified your example to get it working in a more generic way. Hope this will work for you!
ExampleParentView.swift
import SwiftUI
struct ExampleParentView: View {
#StateObject var viewModel = ExampleViewModel()
var body: some View {
NavigationStack(path: $viewModel.targetDestination) {
List {
NavigationLink("Destination A", value: TargetDestination.DestinationA)
NavigationLink("Destination B", value: TargetDestination.DestinationB)
}
.navigationDestination(for: TargetDestination.self) { target in
switch target {
case .DestinationA:
DestinationA()
case .DestinationB:
DestinationB()
}
}
.navigationTitle("Destinations")
Button(action: {
viewModel.showModal = true
}) {
Text("Click to open sheet")
}
}
.sheet(isPresented: $viewModel.showModal, content: {
ExampleSheetView(viewModel: viewModel)
.interactiveDismissDisabled()
})
}
}
ExampleViewModel.swift
import Foundation
import SwiftUI
class ExampleViewModel: ObservableObject {
#Published var showModal = false
#Published var targetDestination: [TargetDestination] = []
}
enum TargetDestination {
case DestinationA
case DestinationB
}
ExampleSheetView.swift
import SwiftUI
struct ExampleSheetView: View {
let viewModel: ExampleViewModel
var body: some View {
VStack {
Text("I am the sheet")
Button(action: {
viewModel.showModal = false
viewModel.targetDestination.append(.DestinationA)
}) {
Text("Close the sheet and navigate to `A`")
}
Button(action: {
viewModel.showModal = false
viewModel.targetDestination.append(.DestinationB)
}) {
Text("Close the sheet and navigate to `B`")
}
}
}
}
DestinationA.swift
import SwiftUI
struct DestinationA: View {
var body: some View {
Text("Destination A")
}
}
DestinationB.swift
import SwiftUI
struct DestinationB: View {
var body: some View {
Text("Destination B")
}
}
in iOS15, it is not working:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
Dest1().navigationTitle("Dest1")
} label: {
Text("to Destination 1")
}
}
}
}
struct Dest1: View {
#State var dest2Active: Bool = false
var body: some View {
NavigationLink(
destination: Button {
dest2Active = false // not working!!
} label: {Text("dismiss")} .navigationTitle("Dest2"),
isActive: $dest2Active
) {Text("to Destination 2")}
}
}
The dismiss button in Dest2 is not working!
I remember that in iOS14, this code works well.
How to resolve this?
Adding .isDetailLink(false) to the top level NavigationLink seems to solve the issue. Note that this works on iPhone iOS -- for iPad, you will need to use a StackNavigationStyle as #workingdog suggests in their answer.
The documentation is not clear on why this works (in fact, it refers specifically to multi-column navigation), but it seems to solve a number of NavigationLink-related issues. See, for example: https://developer.apple.com/forums/thread/667460
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
Dest1()
.navigationTitle("Dest1")
} label: {
Text("to Destination 1")
}.isDetailLink(false)
}
}
}
struct Dest1: View {
#State var dest2Active: Bool = false
var body: some View {
NavigationLink(isActive: $dest2Active) {
Dest2(dest2Active: $dest2Active)
} label: {
Text("to Destination 2")
}
}
}
struct Dest2: View {
#Binding var dest2Active : Bool
var body: some View {
Button {
dest2Active = false
} label: {
Text("Dismiss")
}.navigationTitle("Dest2")
}
}
You need to add .navigationViewStyle(.stack) to make it work.
Here is the test code that works for me.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
Dest1().navigationTitle("Dest1")
} label: {
Text("to Destination 1")
}
}.navigationViewStyle(.stack) // <-- here the important bit
}
}
struct Dest1: View {
#State var dest2Active: Bool = false
var body: some View {
NavigationLink(
destination: Button {
dest2Active = false // now working!!
} label: {Text("dismiss")} .navigationTitle("Dest2"),
isActive: $dest2Active
) {Text("to Destination 2")}
}
}
I’m seeing this issue where a Form / NavigationLink set up seems to stop passing through the data on changes.
Expected behavior: Checkmarks should update when you pick a different food.
Observed behavior: You can see the favorite food changing outside the NavigationLink Destination, but not inside.
This setup mirrors a dynamic application where a ForEach is used to display various NavigationLinks in the Form based on parent data. Weirdly enough, this works if you replace Form with VStack, so I’m curious why this isn’t updating.
I have attached two minimum-setup example projects that replicate this issue where the destination of a NavigationLink is not receiving an update when data is changing. One with Binding, one with simpler passed properties.
Sample Project #1 with Binding - Dropbox
Sample Project #2 without Binding - Dropbox
Code #1:
//
// ContentView.swift
// Form Updating Example
//
// Created by Sahand Nayebaziz on 12/10/20.
//
import SwiftUI
struct ContentView: View {
#State var isPresentingMainView = false
#State var favoriteFood: FoodType = .bagel
var body: some View {
VStack {
Button(action: { isPresentingMainView = true }, label: {
Text("Present Main View")
})
}
.fullScreenCover(isPresented: $isPresentingMainView) {
MainView(favoriteFood: $favoriteFood)
}
}
}
struct MainView: View {
#Binding var favoriteFood: FoodType
var body: some View {
NavigationView {
HStack {
Spacer()
Text(favoriteFood.emoji)
.font(.title)
.foregroundColor(.secondary)
Spacer()
NavigationView {
Form {
List {
ForEach(["SomethingRepresentingShowingFood"], id: \.self) { _ in
NavigationLink(
destination: makeDetail(),
label: {
Text("Food Randomizer")
})
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
.frame(maxWidth: 350)
}
.navigationTitle("Main")
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(StackNavigationViewStyle())
}
func makeDetail() -> some View {
Form {
ForEach(FoodType.allCases) { foodType in
Button(action: { favoriteFood = foodType }, label: {
HStack {
Text(foodType.emoji)
Spacer()
if favoriteFood == foodType {
Image(systemName: "checkmark")
}
}
})
}
}
}
}
enum FoodType: String, Identifiable, CaseIterable {
case bagel, pizza, broccoli
var id: String { rawValue }
var emoji: String {
switch self {
case .bagel: return "🥯"
case .pizza: return "🍕"
case .broccoli: return "🥦"
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
MainView(favoriteFood: .constant(.bagel))
MainView(favoriteFood: .constant(.bagel))
.makeDetail()
}
}
}
Code #2:
//
// ContentView.swift
// Form Updating Example
//
// Created by Sahand Nayebaziz on 12/10/20.
//
import SwiftUI
struct ContentView: View {
#State var isPresentingMainView = false
#State var favoriteFood: FoodType = .bagel
var body: some View {
VStack {
Button(action: { isPresentingMainView = true }, label: {
Text("Present Main View")
})
}
.fullScreenCover(isPresented: $isPresentingMainView) {
MainView(currentFavoriteFood: favoriteFood, onUpdateFavoriteFood: { favoriteFood = $0 })
}
}
}
struct MainView: View {
let currentFavoriteFood: FoodType
let onUpdateFavoriteFood: (FoodType) -> Void
var body: some View {
NavigationView {
HStack {
Spacer()
Text(currentFavoriteFood.emoji)
.font(.title)
.foregroundColor(.secondary)
Spacer()
NavigationView {
Form {
List {
ForEach(["SomethingRepresentingShowingFood"], id: \.self) { _ in
NavigationLink(
destination: makeDetail(),
label: {
Text("Food Randomizer")
})
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
.frame(maxWidth: 350)
}
.navigationTitle("Main")
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(StackNavigationViewStyle())
}
func makeDetail() -> some View {
Form {
ForEach(FoodType.allCases) { foodType in
Button(action: { onUpdateFavoriteFood(foodType) }, label: {
HStack {
Text(foodType.emoji)
Spacer()
if currentFavoriteFood == foodType {
Image(systemName: "checkmark")
}
}
})
}
}
}
}
enum FoodType: String, Identifiable, CaseIterable {
case bagel, pizza, broccoli
var id: String { rawValue }
var emoji: String {
switch self {
case .bagel: return "🥯"
case .pizza: return "🍕"
case .broccoli: return "🥦"
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
MainView(currentFavoriteFood: .bagel, onUpdateFavoriteFood: { _ in })
MainView(currentFavoriteFood: .bagel, onUpdateFavoriteFood: { _ in })
.makeDetail()
}
}
}
Not sure if I'm doing something wrong or if this is a bug. I'm running Xcode 12.1 and iOS 14.1. This wasn't happening on Xcode 11.
The code is as follows:
enum SheetView: Identifiable {
var id: SheetView { self }
case add
}
struct ContentView: View {
var items = ["Book", "Cat", "Dog"]
#State private var sheetView: SheetView?
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
}
.navigationTitle("Items")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
sheetView = .add
}, label: {
Text("Add")
})
}
}
.sheet(item: $sheetView, onDismiss: { sheetView = nil }) { sheet in
if sheet == .add {
Text("Add")
}
}
}
}
}