I'm trying to create a menu in SwiftUI with the possibility of showing sheets, but when I try to display the menu the sheets doesn't display.
Here's the code:
import SwiftUI
enum ActiveSheet: Identifiable {
case first, second
var id: Int {
hashValue
}
}
struct ContentView: View {
#State var activeSheet: ActiveSheet?
#State private var showingConfirmation = false
var body: some View {
Menu("Actions") {
VStack {
Button {
activeSheet = .first
} label: {
Text("Activate first sheet")
}
Button {
activeSheet = .second
} label: {
Text("Activate second sheet")
}
}
.sheet(item: $activeSheet) { item in
switch item {
case .first:
FirstView()
case .second:
SecondView()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The actual result is this:
enter image description here
But without any possibility of access to the sheet pages I created.
Please let me know!
Move the .sheet to outside the Menu:
struct ContentView: View {
#State var activeSheet: ActiveSheet?
#State private var showingConfirmation = false
var body: some View {
Menu("Actions") {
VStack {
Button {
activeSheet = .first
} label: {
Text("Activate first sheet")
}
Button {
activeSheet = .second
} label: {
Text("Activate second sheet")
}
}
}
.sheet(item: $activeSheet) { item in
switch item {
case .first:
FirstView()
case .second:
SecondView()
}
}
}
}
Related
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")
}
}
When I press the Move button in the contextMenu, I change the isCopied and setOriginPath variables in the EnvironmentObject. When this change is made, the List view is cleared and I can't see anything on the screen. I don't have any problems when I don't use EnvironmentObject.
ContextMenu:
.contextMenu {
Button {
safeFileVM.hideSelectedFile(fileName: currentFile.fileName)
safeFileVM.takeArrayOfItems()
} label: {
HStack {
Text(!currentFile.isLock ? "Hide" : "Show")
Image(systemName: currentFile.isLock ? "eye" : "eye.slash")
}
}
Button {
safeFileClipboard.setOriginPath = URL(fileURLWithPath: currentFile.localPath)
safeFileClipboard.isCopied = true
} label: {
HStack {
Text("Move")
Image(systemName: "arrow.up.doc")
}
}
}
View:
struct DetailObjectView: View {
#ObservedObject var safeFileVM: SafeFileViewModel = SafeFileViewModel()
#EnvironmentObject var safeFileClipboard: SafeFileClipBoard
var currentFile: MyFile
var currentLocation = ""
var body: some View {
VStack {
.....
}
.contextMenu {
Button {
safeFileVM.hideSelectedFile(fileName: currentFile.fileName)
safeFileVM.takeArrayOfItems()
} label: {
HStack {
Text(!currentFile.isLock ? "Hide" : "Show")
Image(systemName: currentFile.isLock ? "eye" : "eye.slash")
}
}
Button {
safeFileClipboard.setOriginPath = URL(fileURLWithPath: currentFile.localPath)
safeFileClipboard.isCopied = true
} label: {
HStack {
Text("Move")
Image(systemName: "arrow.up.doc")
}
}
}
}
}
In the mini project below, when the EnvironmentObject value changes, navigation goes to the beginning. Why ? How can I fix this ?
Example Project:
Main:
#main
struct EnvironmentTestApp: App {
#StateObject var fooConfig: FooConfig = FooConfig()
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
.environmentObject(fooConfig)
}
}
}
}
ContentView:
struct ContentView: View {
#EnvironmentObject var fooConfig: FooConfig
private let numbers: [Number] = [.init(item: "1"), .init(item: "2"), .init(item: "3")]
var body: some View {
List {
ForEach(numbers, id: \.id) { item in
DetailView(itemNumber: item.item)
}
}
}
}
struct Number: Identifiable {
var id = UUID()
var item: String
}
DetailView:
struct DetailView: View {
#EnvironmentObject var fooConfig: FooConfig
var itemNumber: String = ""
var body: some View {
NavigationLink(destination: ContentView().environmentObject(fooConfig)) {
Text("\(itemNumber) - \(fooConfig.fooBool == true ? "On" : "Off")")
.environmentObject(fooConfig)
.contextMenu {
Button {
fooConfig.fooBool.toggle()
} label: {
HStack {
Text(fooConfig.fooBool != true ? "On" : "Off")
}
}
}
}
}
}
ObservableObject:
class FooConfig: ObservableObject {
#Published var fooBool: Bool = false
}
Move that from scene into ContentView, because scene is a bad place to update view hierarchy, it is better to do inside view hierarchy, so here
struct EnvironmentTestApp: App {
var body: some Scene {
WindowGroup {
ContentView() // only root view here !!
}
}
}
everything else is inside views, like
struct ContentView: View {
#StateObject private var foo = FooConfig()
var body: some View {
NavigationView {
MainView()
.environmentObject(foo) // << here !!
}
}
}
struct MainView: View {
#EnvironmentObject var fooConfig: FooConfig
private let numbers: [Number] = [.init(item: "1"), .init(item: "2"), .init(item: "3")]
var body: some View {
List {
ForEach(numbers, id: \.id) { item in
DetailView(itemNumber: item.item)
}
}
}
}
and so on...
Tested with Xcode 14 / iOS 16
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"
}
}
}
I'm trying to add different toolbars to each of my tabs but they are not displayed. The app will mostly be used on a landscape iPad and I can add the toolbars to the TabView itself and they display but then I don't know how to pass the button press down the navigation stack to the individual views/view-models to be handled locally.
I've tried adding new NavigationViews (including .stack navigationViewStyles) but this still just adds another column to the view.
This is some barebones, working code:
import SwiftUI
#main
struct NavTabTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
MasterView()
}
}
struct MasterView: View {
var body: some View {
NavigationView {
List {
ForEach(0..<20) { index in
NavigationLink(
destination: DetailView(index: index)
.navigationTitle("Row \(index)")
) {
Text("\(index) th row")
}.tag(index)
}
}.navigationTitle(Text("Ratty"))
}
}
}
struct DetailView: View {
var index: Int
#State var selectedTab = 1
var body: some View {
TabView(selection: $selectedTab) {
Tab1(index: index).tabItem { Label("Tab1", systemImage: "list.dash") }
Tab2(index: index).tabItem { Label("Tab2", systemImage: "aqi.medium") }
Tab3(index: index).tabItem { Label("Tab3", systemImage: "move.3d") }
}
}
}
struct Tab1: View {
var index: Int
var body: some View {
Text("This is \(index) in tab 1")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Bingo") { print("Bingo") }
}
}
}
}
struct Tab2: View {
var index: Int
var body: some View {
Text("This is \(index) in tab 2")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Bongo") { print("Bongo") }
}
}
}
}
struct Tab3: View {
var index: Int
var body: some View {
Text("This is \(index) in tab 3")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Banjo") { print("Banjo") }
}
}
}
}
I'm starting to wonder if this is even possible and whether it would be better to just implement my own view with buttons at the top of each tab.
EDIT:
Not sure if this will help but it does go over some interesting concepts with the toolbar in the nav view.
link: Stewart Lynch
You need to use only one level NavigationView. In other words, you should not nest NavigationViews. Have a look at this answer.
Only Back Button Visible on Custom Navigation Bar SwiftUI
Use a NavgationView for the MasterView as you have used.
Use a NavigationView for each of the Tab# veiws.
Switch between MasterView and DetailsView.
Use Button instead of NavigationLink in MasterView (You can customise it to look like a NavigationLink)
Use a custom back button in each of the Tab# veiws.
This controls which one of MasterView and DetailsView should be shown.
class BaseViewModel: ObservableObject {
#Published var userFlow: UserFlow = .masterView
init(){
userFlow = .masterView
}
enum UserFlow {
case masterView, detailsView
}
}
This view to be used either in ContentView or instead of it. When you are in MasterView and click on one of the list row, set appState.userFlow = .detailView. When you click the back buttons, set appState.userFlow = .masterView.
Doing so, you switch between the two views and one NavigationView is shown at a time.
As of now, it does not have animation. Use if you wish so
https://developer.apple.com/tutorials/swiftui/animating-views-and-transitions
struct BaseView: View {
#EnvironmentObject var appState: BaseViewModel
#State var index: Int = 0
var body: some View {
Group {
switch appState.userFlow {
case .masterView:
MasterView(index: $index)
default:
DetailView(index: index)
}
}
.edgesIgnoringSafeArea(.bottom)
}
}
Complete code
struct ContentView: View {
var body: some View {
BaseView().environmentObject(BaseViewModel())
}
}
class BaseViewModel: ObservableObject {
#Published var userFlow: UserFlow = .masterView
init(){
userFlow = .masterView
}
enum UserFlow {
case masterView, detailsView
}
}
struct BaseView: View {
#EnvironmentObject var appState: BaseViewModel
#State var index: Int = 0
var body: some View {
Group {
switch appState.userFlow {
case .masterView:
MasterView(index: $index)
default:
DetailView(index: index)
}
}
.edgesIgnoringSafeArea(.bottom)
}
}
struct MasterView: View {
#EnvironmentObject var appState: BaseViewModel
#Binding var index: Int
var body: some View {
NavigationView {
List {
ForEach(0..<20) { index in
Button(action: {
appState.userFlow = .detailsView
$index.wrappedValue = index
}, label: {
HStack {
Text("\(index) th row")
Spacer()
Image(systemName: "greaterthan")
}
})
.tag(index)
}
}.navigationTitle(Text("Ratty"))
}
}
}
struct DetailView: View {
var index: Int
#State var selectedTab = 1
var body: some View {
TabView(selection: $selectedTab) {
Tab1(index: index).tabItem { Label("Tab1", systemImage: "list.dash") }
Tab2(index: index).tabItem { Label("Tab2", systemImage: "aqi.medium") }
Tab3(index: index).tabItem { Label("Tab3", systemImage: "move.3d") }
}
}
}
struct Tab1: View {
#EnvironmentObject var appState: BaseViewModel
var index: Int
var body: some View {
NavigationView {
Text("This is \(index) in tab 1")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("back") { appState.userFlow = .masterView }
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Bingo") { print("Bingo") }
}
}
}
}
}
struct Tab2: View {
#EnvironmentObject var appState: BaseViewModel
var index: Int
var body: some View {
NavigationView {
Text("This is \(index) in tab 2")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("back") { appState.userFlow = .masterView }
}
ToolbarItem(placement: .primaryAction) {
Button("Bongo") { print("Bongo") }
}
}
}
}
}
struct Tab3: View {
#EnvironmentObject var appState: BaseViewModel
var index: Int
var body: some View {
NavigationView {
Text("This is \(index) in tab 3")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("back") { appState.userFlow = .masterView }
}
ToolbarItem(placement: .primaryAction) {
Button("Banjo") { print("Banjo") }
}
}
}
}
}
I have customised xtwistedx 's solution.
I'm trying to implement a dismiss button for my modal sheet as follows:
struct TestView: View {
#Environment(\.isPresented) var present
var body: some View {
Button("return") {
self.present?.value = false
}
}
}
struct DataTest : View {
#State var showModal: Bool = false
var modal: some View {
TestView()
}
var body: some View {
Button("Present") {
self.showModal = true
}.sheet(isPresented: $showModal) {
self.modal
}
}
}
But the return button when tapped does nothing. When the modal is displayed the following appears in the console:
[WindowServer] display_timer_callback: unexpected state (now:5fbd2efe5da4 < expected:5fbd2ff58e89)
If you force unwrap present you find that it is nil
How can I dismiss .sheet programmatically?
iOS 15+
Starting from iOS 15 we can use DismissAction that can be accessed as #Environment(\.dismiss).
There's no more need to use presentationMode.wrappedValue.dismiss().
struct SheetView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
Text("Sheet")
.toolbar {
Button("Done") {
dismiss()
}
}
}
}
}
Use presentationMode from the #Environment.
Beta 6
struct SomeView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Text("Ohay!")
Button("Close") {
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
For me, beta 4 broke this method - using the Environment variable isPresented - of using a dismiss button. Here's what I do nowadays:
struct ContentView: View {
#State var showingModal = false
var body: some View {
Button(action: {
self.showingModal.toggle()
}) {
Text("Show Modal")
}
.sheet(
isPresented: $showingModal,
content: { ModalPopup(showingModal: self.$showingModal) }
)
}
}
And in your modal view:
struct ModalPopup : View {
#Binding var showingModal:Bool
var body: some View {
Button(action: {
self.showingModal = false
}) {
Text("Dismiss").frame(height: 60)
}
}
}
Apple recommend (in WWDC 2020 Data Essentials in SwiftUI) using #State and #Binding for this. They also place the isEditorPresented boolean and the sheet's data in the same EditorConfig struct that is declared using #State so it can be mutated, as follows:
import SwiftUI
struct Item: Identifiable {
let id = UUID()
let title: String
}
struct EditorConfig {
var isEditorPresented = false
var title = ""
var needsSave = false
mutating func present() {
isEditorPresented = true
title = ""
needsSave = false
}
mutating func dismiss(save: Bool = false) {
isEditorPresented = false
needsSave = save
}
}
struct ContentView: View {
#State var items = [Item]()
#State private var editorConfig = EditorConfig()
var body: some View {
NavigationView {
Form {
ForEach(items) { item in
Text(item.title)
}
}
.navigationTitle("Items")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: presentEditor) {
Label("Add Item", systemImage: "plus")
}
}
}
.sheet(isPresented: $editorConfig.isEditorPresented, onDismiss: {
if(editorConfig.needsSave) {
items.append(Item(title: editorConfig.title))
}
}) {
EditorView(editorConfig: $editorConfig)
}
}
}
func presentEditor() {
editorConfig.present()
}
}
struct EditorView: View {
#Binding var editorConfig: EditorConfig
var body: some View {
NavigationView {
Form {
TextField("Title", text:$editorConfig.title)
}
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(action: save) {
Text("Save")
}
.disabled(editorConfig.title.count == 0)
}
ToolbarItem(placement: .cancellationAction) {
Button(action: dismiss) {
Text("Dismiss")
}
}
}
}
}
func save() {
editorConfig.dismiss(save: true)
}
func dismiss() {
editorConfig.dismiss()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(items: [Item(title: "Banana"), Item(title: "Orange")])
}
}