How to dismiss a Sheet and open a NavigationLink in a new View? - swiftui

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")
}
}

Related

Changing a TextField text for items in a foreach NavigationLink SwiftUI and saving it

There is a list in on the Main View that has navigation links that bring you to a an Edit Birthday View where the textFieldName is saved with the onAppear method. I need help in allowing the user to change the text in the text field on the Edit Birthday View and having it save when the user dismisses and returns to that particular item in the foreach list. I have tried onEditingChanged and on change method but they don't seem to work. (Also, in my view model i append birthday items when they are created in the Add Birthday View). If you would like to see more code i will make updates. Thank you.
/// MAIN VIEW
import SwiftUI
struct MainView: View {
#EnvironmentObject var vm: BirthdayViewModel
#State var nameTextField: String = ""
var body: some View {
VStack(spacing: 20) {
List {
ForEach(vm.searchableUsers, id: \.self) { birthday in
NavigationLink(destination: EditBirthdayView(birthday: birthday)) {
BirthdayRowView(birthday: birthday)
}
.listRowSeparator(.hidden)
}
.onDelete(perform: vm.deleteBirthday)
}
}
.toolbar {
ToolbarItem {
NavigationLink(destination: AddBirthdayView(textfieldName: $nameTextField)) {
Image(systemName: "plus.circle")
}
}
}
}
}
/// EDIT BIRTHDAY VIEW
import SwiftUI
import Combine
struct EditBirthdayView: View {
#EnvironmentObject var vm: BirthdayViewModel
#State var textfieldName: String = ""
#Environment(\.presentationMode) var presentationMode
var birthday: BirthdayModel
var body: some View {
NavigationView {
VStack {
TextField("Name...", text: $textfieldName)
}
Button {
saveButtonPressed()
} label: {
Text("Save")
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now()) {
textfieldName = birthday.name
}
}
}
}
func saveButtonPressed() {
vm.updateItem(birthday: birthday)
presentationMode.wrappedValue.dismiss()
}
func updateTextField() {
textfieldName = birthday.name
}
}
struct MainView: View {
#EnvironmentObject var store: BirthdayStore
var body: some View {
List {
ForEach($store.birthdays) { $birthday in
NavigationLink(destination: EditBirthdayView(birthday: $birthday)) {
BirthdayRowView(birthday: birthday)
}
}
.onDelete(perform: deleteBirthday)
}
.listRowSeparator(.hidden)
.toolbar {
ToolbarItem {
NavigationLink(destination: AddBirthdayView() {
Image(systemName: "plus.circle")
}
}
}
}
}
struct EditBirthdayView: View {
#EnvironmentObject var store: BirthdayStore
#Binding var birthday: Birthday
...
TextField("Name", text: $birthday.name)

SwiftUI Half-swipe back from navigation causes error

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

SwiftUI Picker problem after dismissing .fullScreenCover or .sheet

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

Refresh a SwiftUI View on Back Navigation

I have a MainView and DetailView. The MainView displays a list of items. From MainView you can go to DetailView using the push navigation. The DetailView allows to add the item. After adding the new item, I am trying to go back to the MainView and refresh the MainView. It goes back but it never displays the new item unless I restart the app.
I added onAppear on the MainView and I can see it is getting fired. But it still does not update the view.
Here is some code in the MainView:
var body: some View {
List {
ForEach(movieListVM.movies, id: \.id) { movie in
NavigationLink(
destination: AddUpdateMovieScreen(movieId: movie.id),
label: {
MovieCell(movie: movie)
})
}.onDelete(perform: deleteMovie)
}
.listStyle(PlainListStyle())
.navigationTitle("Movies")
.navigationBarItems(trailing: Button("Add Movie") {
isPresented = true
})
.sheet(isPresented: $isPresented, onDismiss: {
movieListVM.populateMovies()
}, content: {
AddUpdateMovieScreen()
})
.onAppear(perform: {
movieListVM.populateMovies()
})
.embedInNavigationView()
Here is the code in the ViewModel:
class MovieListViewModel: ObservableObject {
#Published var movies = [MovieViewModel]()
#Published var updated: Bool = false
func deleteMovie(movie: MovieViewModel) {
let movie = CoreDataManager.shared.getMovieById(id: movie.id)
if let movie = movie {
CoreDataManager.shared.deleteMovie(movie)
}
}
func populateMovies() {
let movies = CoreDataManager.shared.getAllMovies()
for movie in movies {
print(movie.title) // THIS PRINTS THE UPDATE OBJECTS
}
DispatchQueue.main.async {
self.movies = movies.map(MovieViewModel.init) // THIS POPULATES THE movies correctly.
}
}
}
Any ideas why the MainView is not updating, even though I am firing the populateMovies function of the MovieListViewModel.
import SwiftUI
import CoreData
struct AddUpdateMovieScreen: View {
#StateObject private var addMovieVM = AddUpdateMovieViewModel()
#Environment(\.presentationMode) var presentationMode
#State private var movieVS = MovieViewState()
var movieId: NSManagedObjectID?
private func saveOrUpdate() {
do {
if movieId != nil {
// UPDATE IS THE ISSUE I AM TRYING TO RESOLVE
try addMovieVM.update(movieVS)
} else {
addMovieVM.save(movieVS)
}
} catch {
print(error)
}
}
var body: some View {
Form {
TextField("Enter name", text: $movieVS.title)
TextField("Enter director", text: $movieVS.director)
HStack {
Text("Rating")
Spacer()
RatingView(rating: $movieVS.rating)
}
DatePicker("Release Date", selection: $movieVS.releaseDate)
HStack {
Spacer()
Button("Save") {
saveOrUpdate()
presentationMode.wrappedValue.dismiss()
}
Spacer()
}
}
.onAppear(perform: {
// if the movieId is not nil then fetch the movie information
if let movieId = movieId {
// fetch the movie
do {
let movieVM = try addMovieVM.getMovieById(movieId: movieId)
movieVS = MovieViewState.fromMovieViewModel(vm: movieVM)
} catch {
print(error)
}
}
})
.navigationTitle("Add Movie")
.embedInNavigationView()
}
}
struct AddMovieScreen_Previews: PreviewProvider {
static var previews: some View {
AddUpdateMovieScreen()
}
}
Since you don't show your code for "AddUpdateMovieScreen", here are my guesses:
if you are passing "movieListVM" to "AddUpdateMovieScreen" as ObservableObject, then use this:
.sheet(isPresented: $isPresented) {
AddUpdateMovieScreen(movieListVM: movieListVM)
}
and:
struct AddUpdateMovieScreen: View {
#ObservedObject var movieListVM: MovieListViewModel
...
if you are passing "movieListVM" to "AddUpdateMovieScreen" as EnvironmentObject, then use this:
.sheet(isPresented: $isPresented) {
AddUpdateMovieScreen().environment(movieListVM)
}
and:
struct AddUpdateMovieScreen: View {
#EnvironmentObject var movieListVM: MovieListViewModel
...
There is no need for "movieListVM.populateMovies()" in the sheet onDismiss.

.sheet: Shows only once and then never again

Working with Beta4, it seems that the bug is still existing. The following sequence of views (a list, where a tap on a list entry opens another list) allows to present the ListView exactly once; the onDisappear is never called, so the showModal flag changes, but does not triggers the redisplay of ListView when tapped again. So, for each GridCellBodyEntry, the .sheet presentation works exactly once, and then never again.
I tried around with several suggestions and workarounds, but none worked (e.g., encapsulating with a NavigationViewModel). I even tried to remove the List, because there was an assumption that the List causes that behaviour, but even this did not change anything.
Are there any ideas around?
The setup:
A GridCellBody with this view:
var body: some View {
GeometryReader { geometry in
VStack {
List {
Section(footer: self.footerView) {
ForEach(self.rawEntries) { rawEntry in
GridCellBodyEntry(entityType: rawEntry)
}
}
}
.background(Color.white)
}
}
}
A GridCellBodyEntry with this definition:
struct GridCellBodyEntry: View {
let entityType: EntityType
let viewModel: BaseViewModel
init(entityType: EntityType) {
self.entityType = entityType
self.viewModel = BaseViewModel(entityType: self.entityType)
}
#State var showModal = false {
didSet {
print("showModal: \(showModal)")
}
}
var body: some View {
Group {
Button(action: {
self.showModal.toggle()
},
label: {
Text(entityType.localizedPlural ?? "")
.foregroundColor(Color.black)
})
.sheet(isPresented: $showModal, content: {
ListView(showModal: self.$showModal,
viewModel: self.viewModel)
})
}.onAppear{
print("Profile appeared")
}.onDisappear{
print("Profile disappeared")
}
}
}
A ListView with this definition:
struct ListView: View {
// MARK: - Private properties
// MARK: - Public interface
#Binding var showModal: Bool
#ObjectBinding var viewModel: BaseViewModel
// MARK: - Main view
var body: some View {
NavigationView {
VStack {
List {
Section(footer: Text("\(viewModel.list.count) entries")) {
ForEach(viewModel.list, id: \.objectID) { item in
NavigationLink(destination: ItemView(),
label: {
Text("\(item.objectID)")
})
}
}
}
}
.navigationBarItems(leading:
Button(action: {
self.showModal = false
}, label: {
Text("Close")
}))
.navigationBarTitle(Text(viewModel.entityType.localizedPlural ?? ""))
}
}
}
The BaseViewModel (excerpt):
class BaseViewModel: BindableObject {
/// The binding support.
var willChange = PassthroughSubject<Void, Never>()
/// The context.
var context: NSManagedObjectContext
/// The current list of typed items.
var list: [NSManagedObject] = []
// ... other stuff ...
}
where willChange.send() is called whenever something changes (create, modify, delete operations).
This is a variant of swiftUI PresentaionLink does not work second time
The following simplified code exhibits the behavior you're experiencing (the sheet only displays once):
import SwiftUI
struct ContentView: View {
#State var isPresented = false
#State var whichPresented = -1
var body: some View {
NavigationView {
List {
ForEach(0 ..< 10) { i in
Button(action: {
self.whichPresented = i
self.isPresented.toggle()
})
{ Text("Button \(i)") }
}.sheet(isPresented: $isPresented, content: {
Text("Destination View \(self.whichPresented)") })
}
}
}
}
There appears to be a bug in SwiftUI when you put the .sheet inside a List or a ForEach. If you move the .sheet outside of the List, you should be able to get the correct behavior.
import SwiftUI
struct ContentView: View {
#State var isPresented = false
#State var whichPresented = -1
var body: some View {
NavigationView {
List {
ForEach(0 ..< 10) { i in
Button(action: {
self.whichPresented = i
self.isPresented.toggle()
})
{ Text("Button \(i)") }
}
}
}.sheet(isPresented: $isPresented, content: { Text("Destination View \(self.whichPresented)") })
}
}