Presenting a sheet view with NavigationStack in SwiftUI - swiftui

I am trying to figure out if there is a way to open a sheet with the new NavigationStack in iOS 16, but can't seem to find a way.
So of cause, it's possible to open a sheet using:
.sheet(isPresented: $isShowing)
But with the new NavigationStack you have an array of a type which you present with:
.navigationDestination(for: SomeType.self, destination: { route in
And it would be awesome if I could somehow define that when a specific destination is presented it opens as a sheet instead of navigating with a modal.
Does anyone know if this is possible to achieve? :)

I believe what you're looking for is:
.navigationDestination(isPresented: $helloNewItem, destination: { HelloThereView() })
https://developer.apple.com/documentation/swiftui/view/navigationdestination(ispresented:destination:)
Does this match what you need?

This worked for me. I can push objects to my route from any view, and load it with a modal sheet.
class Route: ObservableObject {
#Published var presentedObject: [CarObject] = []
#Published var isPresented: Bool = false
}
struct NavigationWithSheet: View {
var body: some View {
#EnvironmentObject var route: Route
NavigationStack {
Button("Open sheet") {
route.isPresented = true
route.presentedObject.append(newCar)
}
.sheet(isPresented: $route.isPresented) {
let carObject = route.presentedObject.first
ViewWithObject(car: carObject!)
}
}
}
}

Related

Unable to RE-navigate through SwiftUI app

I’m writing a workout app for Apple Watch but am running into issues with navigation. I have a version which includes the behaviour I’m looking for but am trying to simplify the navigation through the app which is leading to issues.
The “working” version of the app (code below) has the user select a workout from a launch page. When the workout is started, a tabbed view is displayed which contains controls on one page and metrics on another, cut down here for the purpose of demonstration. When End on the controls page is pressed, a boolean is set to true forcing a summary view to be displayed. When Done is pressed here, the view is dismissed returning the user to the launch page. A new workout can then be initiated.
As mentioned above, I’m trying to simplify the app, doing away with the workout selection and immediately leading to a start page. From there the functionality is basically the same. However, when I return to the start page, I am unable to initiate another workout without re-launching the app.
I’m just starting out on my SwiftUI journey so can only presume I’m misusing the NavigationStack or misunderstanding the concepts behind it but have not been able to see the issue.
Any assistance would be greatly appreciated.
Thanks much.
struct ContentView: View {
#StateObject private var workoutManager = WorkoutManager()
var body: some View {
NavigationStack {
List(Workout.workouts) { workout in
NavigationLink(value: workout) {
Text(workout.shortName)
}
}
.navigationDestination(for: Workout.self) { workout in
WorkoutSetupView()
}
.navigationDestination(isPresented: $workoutManager.showingSummaryView) {
SummaryView()
}
}
.environmentObject(workoutManager)
}
}
struct Workout: Identifiable, Hashable {
var id: String
var shortName: String
static var workouts: [Workout] {
[
Workout(id: "WORKOUT1", shortName: "Workout 1"),
Workout(id: "WORKOUT2", shortName: "Workout 2")
]
}
}
class WorkoutManager: NSObject, ObservableObject {
#Published var showingSummaryView: Bool = false
func endWorkout() {
showingSummaryView = true
}
}
struct SummaryView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
ScrollView {
VStack{
Text("Results: 2")
Button("Done") {
dismiss()
}
}
}
.navigationTitle("Summary")
.navigationBarBackButtonHidden(true)
}
}
struct WorkoutSetupView: View {
var body: some View {
NavigationLink(
destination: SessionPagingView()) {
Image(systemName: "play")
}
}
}
struct SessionPagingView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#State private var selection: Tab = .session
enum Tab {
case controls, session
}
var body: some View {
TabView(selection: $selection) {
Button {
workoutManager.endWorkout()
} label: {
Text("End")
}.tag(Tab.controls)
Text("0:10").tag(Tab.session)
}
.navigationBarBackButtonHidden(true)
}
}
If I replace the NavigationStack block in ContentView with the code below, the app works in a simplified way as expected but a new workout cannot be initiated without relaunching the app.
NavigationStack {
NavigationLink(
destination: SessionPagingView()) {
Image(systemName: "play")
}
.navigationDestination(isPresented: $workoutManager.showingSummaryView) {
SummaryView()
}
}
I've tried resetting showingSummaryView to false on pressing Done in SummaryView and also tried using a NavigationPath but neither had any noticeable effect.
Thanks

SwiftUI Navigation popping back when modifying list binding property in a pushed view

When I update a binding property from an array in a pushed view 2+ layers down, the navigation pops back instantly after a change to the property.
Xcode 13.3 beta, iOS 15.
I created a simple demo and code is below.
Shopping Lists
List Edit
List section Edit
Updating the list title (one view deep) is fine, navigation stack stays same, and changes are published if I return. But when adjusting a section title (two deep) the navigation pops back as soon as I make a single change to the property.
I have a feeling I'm missing basic fundamentals here, and I have a feeling it must be related to the lists id? but I'm struggling to figure it out or work around it.
GIF
Code:
Models:
struct ShoppingList {
let id: String = UUID().uuidString
var title: String
var sections: [ShoppingListSection]
}
struct ShoppingListSection {
let id: String = UUID().uuidString
var title: String
}
View Model:
final class ShoppingListsViewModel: ObservableObject {
#Published var shoppingLists: [ShoppingList] = [
.init(
title: "Shopping List 01",
sections: [
.init(title: "Fresh food")
]
)
]
}
Content View:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}
}
}
ShoppingListsView
struct ShoppingListsView: View {
#StateObject private var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
ShoppingListEditView
struct ShoppingListEditView: View {
#Binding var shoppingList: ShoppingList
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("Title", text: $shoppingList.title)
}
Section(header: Text("Sections")) {
List($shoppingList.sections, id: \.id) { $section in
NavigationLink(destination: ShoppingListSectionEditView(section: $section)) {
Text(section.title)
}
}
}
}
.navigationBarTitle("Edit list")
}
}
ShoppingListSectionEditView
struct ShoppingListSectionEditView: View {
#Binding var section: ShoppingListSection
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("title", text: $section.title)
}
}
.navigationBarTitle("Edit section")
}
}
try this, works for me:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}.navigationViewStyle(.stack) // <--- here
}
}
Try to make you object confirm to Identifiable and return value which unique and stable, for your case is ShoppingList.
Detail view seems will pop when object id changed.
The reason your stack is popping back to the root ShoppingListsView is that the change in the list is published and the root ShoppingListsView is registered to listen for updates to the #StateObject.
Therefore, any change to the list is listened to by ShoppingListsView, causing that view to be re-rendered and for all new views on the stack to be popped in order to render the root ShoppingListsView, which is listening for updates on the #StateObject.
The solution to this is to change the #StateObject to #EnvironmentObject
Please refactor your code to change ShoppingListsViewModel to use an #EnvironmentObject wrapper instead of a #StateObject wrapper
You may pass the environment object in to all your child views and also add a boolean #Published flag to track any updates to the data.
Then your ShoppingListView would look as below
struct ShoppingListsView: View {
#EnvironmentObject var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
Don't forget to pass the viewModel in to all your child views.
That should fix your problem.

SwiftUI - How to pass data then initialise and edit data

I'm downloading data from Firebase and trying to edit it. It works, but with an issue. I am currently passing data to my EditViewModel with the .onAppear() method of my view. And reading the data from EditViewModel within my view.
class EditViewModel: ObservableObject {
#Published var title: String = ""
}
struct EditView: View {
#State var selected_item: ItemModel
#StateObject var editViewModel = EditViewModel()
var body: some View {
VStack {
TextField("Name of item", text: self.$editViewModel.title)
Divider()
}.onAppear {
DispatchQueue.main.async {
editViewModel.title = selected_item.title
}
}
}
}
I have given you the extremely short-hand version as it's much easier to follow.
However, I push to another view to select options from a list and pop back. As a result, everything is reset due to using the onAppear method. I have spent hours trying to use init() but I am struggling to get my application to even compile, getting errors in the process. I understand it's due to using the .onAppear method, but how can I use init() for this particular view/view-model?
I've search online but I've found the answers to not be useful, or different from what I wish to achieve.
Thank you.
You don't need to use State for input property - it is only for internal view usage. So as far as I understood your scenario, here is a possible solution:
struct EditView: View {
private var selected_item: ItemModel
#StateObject var editViewModel = EditViewModel()
init(selectedItem: ItemModel) {
selected_item = selectedItem
editViewModel.title = selectedItem.title
}
var body: some View {
VStack {
TextField("Name of item", text: self.$editViewModel.title)
Divider()
}.onAppear {
DispatchQueue.main.async {
editViewModel.title = selected_item.title
}
}
}
}

SwiftUI: Navigate from Sheet to a new View

How can I push a new View on the navigation stack from within a Sheet. I want to display a list of Lessons. When tabbing on one of the lessons, a sheet should open showing details about the lesson. From within the Sheet one should be able to start the lesson in a new fullscreen view.
import SwiftUI
struct ContentView: View {
var lessons = [Lesson(id:"1"), Lesson(id:"2"), Lesson(id:"3"), Lesson(id:"4"), Lesson(id:"5"), Lesson(id:"6"), Lesson(id:"7"), Lesson(id:"8"), Lesson(id:"9")]
var body: some View {
NavigationView(){
Form{
List(lessons){ lesson in
LessonButton(lesson: lesson)
}
}
}
}
}
struct LessonButton:View{
#State var showSheet = false
var lesson:Lesson
var body: some View {
Button(action:{self.showSheet = true}){
Text(lesson.name)
}.sheet(isPresented:$showSheet){
NavigationLink(destination: Text("reached")){
Text("start")
}
}
}
}
struct Lesson: Identifiable{
var id:String
var name: String{
"Lesson \(self.id)"
}
}
However the NavigationLink is not working. I guess, this is because the Sheet is not a ChildView of Content View. That's probably why it does not work. But how can it be achieved?
A bit late, but this question came up while solving this. Your sheet acts like its own view controller stack. You can't navigate the parent through the sheet overlay, nor should you. It does seem like you're asking what I was looking for, which is to emulate other apple apps that navigate in sheets. You simply need an additional NavigationView within your sheet. This will give you a navigation stack to push other sheet styled views to the navigation controller within your first sheet.
(SwiftUI beginner, verbiage is likely wrong)
import SwiftUI
struct NavigateFromSheet: View {
var lessons = [Lesson(id:"1"), Lesson(id:"2"), Lesson(id:"3"), Lesson(id:"4"), Lesson(id:"5"), Lesson(id:"6"), Lesson(id:"7"), Lesson(id:"8"), Lesson(id:"9")]
var body: some View {
NavigationView(){
Form {
List(lessons){ lesson in
LessonButton(lesson: lesson)
}
}
}
}
}
struct LessonButton:View{
#State var showSheet = false
var lesson:Lesson
var body: some View {
Button(action:{self.showSheet = true}){
Text(lesson.name)
}.sheet(isPresented: $showSheet){
NavigationView {
VStack {
Text("My First Sheet")
NavigationLink(destination: Text("reached")){
Text("My Second Sheet")
}
}
}
}
}
}
struct Lesson: Identifiable{
var id:String
var name: String{
"Lesson \(self.id)"
}
}
struct NavigateFromSheet_Previews: PreviewProvider {
static var previews: some View {
NavigateFromSheet()
}
}
Sheet is modal view mode, you can enter in it and return back from it.
Actually I can't understand why do you need a sheet in described scenario. As you described it is expected:
List -> Details -> Lesson,
so use consequently two navigation links, one in List, one in Details. This is a native Apple design for NavigationView/NavigationLink usage - navigation from view to view.

Disable drag to dismiss in SwiftUI Modal

I've presented a modal view but I would like the user to go through some steps before it can be dismissed.
Currently the view can be dragged to dismiss.
Is there a way to stop this from being possible?
I've watched the WWDC Session videos and they mention it but I can't seem to put my finger on the exact code I'd need.
struct OnboardingView2 : View {
#Binding
var dismissFlag: Bool
var body: some View {
VStack {
Text("Onboarding here! 🙌🏼")
Button(action: {
self.dismissFlag.toggle()
}) {
Text("Dismiss")
}
}
}
}
I currently have some text and a button I'm going to use at a later date to dismiss the view.
iOS 15+
Starting from iOS 15 we can use interactiveDismissDisabled:
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View
We just need to attach it to the sheet. Here is an example from the documentation:
struct PresentingView: View {
#Binding var showTerms: Bool
var body: some View {
AppContents()
.sheet(isPresented: $showTerms) {
Sheet()
}
}
}
struct Sheet: View {
#State private var acceptedTerms = false
var body: some View {
Form {
Button("Accept Terms") {
acceptedTerms = true
}
}
.interactiveDismissDisabled(!acceptedTerms)
}
}
It is easy if you use the 3rd party lib Introspect, which is very useful as it access the corresponding UIKit component easily. In this case, the property in UIViewController:
VStack { ... }
.introspectViewController {
$0.isModalInPresentation = true
}
Not sure this helps or even the method to show the modal you are using but when you present a SwiftUI view from a UIViewController using UIHostingController
let vc = UIHostingController(rootView: <#your swiftUI view#>(<#your parameters #>))
you can set a modalPresentationStyle. You may have to decide which of the styles suits your needs but .currentContext prevents the dragging to dismiss.
Side note:I don't know how to dismiss a view presented from a UIHostingController though which is why I've asked a Q myself on here to find out 😂
I had a similar question here
struct Start : View {
let destinationView = SetUp()
.navigationBarItem(title: Text("Set Up View"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Set Up")
}
}
}
}
The main thing here is that it is hiding the back button. This turns off the back button and makes it so the user can't swipe back ether.
For the setup portion of your app you could create a new SwiftUI file and add a similar thing to get home, while also incorporating your own setup code.
struct SetUp : View {
let destinationView = Text("Your App Here")
.navigationBarItem(title: Text("Your all set up!"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Done")
}
}
}
}
There is an extension to make controlling the modal dismission effortless, at https://gist.github.com/mobilinked/9b6086b3760bcf1e5432932dad0813c0
A temporary solution before the official solution released by Apple.
/// Example:
struct ContentView: View {
#State private var presenting = false
var body: some View {
VStack {
Button {
presenting = true
} label: {
Text("Present")
}
}
.sheet(isPresented: $presenting) {
ModalContent()
.allowAutoDismiss { false }
// or
// .allowAutoDismiss(false)
}
}
}