SwiftUI - show Alert from Button inside of ToolbarItem - ios

I want to do something similar to the code below, where it shows an alert when a user taps a navigation bar item button. However the code below doesn't work, the alerts do not display.
I can't add the alert modifiers to the NavigationView because my actual app is more complex and the VStack is a view in another file, also adding multiple alert modifiers to the same view doesn't work (only the last one added works).
import SwiftUI
struct SOQuestionView: View {
#State private var showAlert1 = false
#State private var showAlert2 = false
var body: some View {
NavigationView {
VStack {
Text("Click button in toolbar")
}
.navigationBarTitle(Text("Title"))
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
showAlert1 = true
}) {
Image(systemName: "square.and.arrow.up")
}
.alert(isPresented: $showAlert1) {
Alert(title: Text("Alert 1"))
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showAlert2 = true
}) {
Image(systemName: "square.and.arrow.up.fill")
}
.alert(isPresented: $showAlert2) {
Alert(title: Text("Alert 2"))
}
}
}
}
}
}

The solution is to use alert outside of toolbar and with different constructor. Tested with Xcode 12.1 / iOS 14.1.
struct SOQuestionView: View {
#State private var alertItem: String? // assuming item confirmed to Identifiable
var body: some View {
NavigationView {
VStack {
Text("Click button in toolbar")
}
.navigationBarTitle(Text("Title"))
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
alertItem = "Alert 1"
}) {
Image(systemName: "square.and.arrow.up")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
alertItem = "Alert 2"
}) {
Image(systemName: "square.and.arrow.up.fill")
}
}
}
.alert(item: $alertItem) { item in
Alert(title: Text(item))
}
}
}
}

Related

Presenting Sheet modally in SwiftUI

I am trying to present a modal sheet upon selecting the menu item in the navigation bar. But, the sheet is not displayed. Upon debugging I noticed that the state variable showSheet is not getting updated and I am sort of lost as to why it is not updating.
Any help is very much appreciated. Thank you!
There is another post (#State not updating in SwiftUI 2) that has a similar issue. Is this a bug in SwiftUI?
Below is a full sample
I have a fileprivate enum that defines two cases for the views - add and edit
fileprivate enum SheetView {
case add, edit
}
Below is the ContentView. The ContentView declares two #State variables that are set based on the menu item selected
The menu items (var actionItems) are on the NavigationView and has menu with 2 buttons - Add and Edit. Each button has an action set to toggle the showSheetView and the showSheet variables. The content is presented based on which item is selected. Content is built using #ViewBuilder
struct ContentView: View {
#State private var showSheetView = false
#State private var showSheet: SheetView? = nil
var body: some View {
GeometryReader { g in
NavigationView {
Text("Main Page")
.padding()
.navigationBarTitle("Main Page")
.navigationBarItems(trailing: actionItems)
}
}.sheet(isPresented: $showSheetView) {
content
}
}
var actionItems: some View {
Menu {
Button(action: {
showSheet = .add
showSheetView.toggle()
}) {
Label("Add Asset", systemImage: "plus")
}
Button(action: {
showSheet = .edit
showSheetView.toggle()
}) {
Label("Edit Asset", systemImage: "minus")
}
} label: {
Image(systemName: "dot.circle.and.cursorarrow").resizable()
}
}
#ViewBuilder
var content: some View {
if let currentView = showSheet {
switch currentView {
case .add:
AddAsset(showSheetView: $showSheetView)
case .edit:
EditAsset(showSheetView: $showSheetView)
}
}
}
}
Below are the two Views - AddAsset and EditAsset
struct AddAsset: View {
#Binding var showSheetView: Bool
var body: some View {
NavigationView {
Text("Add Asset")
.navigationBarTitle(Text("Add"), displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
print("Dismissing sheet view...")
self.showSheetView = false
}) {
Text("Done").bold()
})
}
}
}
struct EditAsset: View {
#Binding var showSheetView: Bool
var body: some View {
NavigationView {
Text("Edit Asset")
.navigationBarTitle(Text("Edit"), displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
print("Dismissing sheet view...")
self.showSheetView = false
}) {
Text("Done").bold()
})
}
}
}
The solution is to use sheet(item: variant.
Here is fixed code (there are many changes so all components included). Tested with Xcode 12.1 / iOS 14.1
enum SheetView: Identifiable {
var id: Self { self }
case add, edit
}
struct ContentView: View {
#State private var showSheet: SheetView? = nil
var body: some View {
GeometryReader { g in
NavigationView {
Text("Main Page")
.padding()
.navigationBarTitle("Main Page")
.navigationBarItems(trailing: actionItems)
}
}.sheet(item: $showSheet) { mode in
content(for: mode)
}
}
var actionItems: some View {
Menu {
Button(action: {
showSheet = .add
}) {
Label("Add Asset", systemImage: "plus")
}
Button(action: {
showSheet = .edit
}) {
Label("Edit Asset", systemImage: "minus")
}
} label: {
Image(systemName: "dot.circle.and.cursorarrow").resizable()
}
}
#ViewBuilder
func content(for mode: SheetView) -> some View {
switch mode {
case .add:
AddAsset(showSheet: $showSheet)
case .edit:
EditAsset(showSheet: $showSheet)
}
}
}
struct AddAsset: View {
#Binding var showSheet: SheetView?
var body: some View {
NavigationView {
Text("Add Asset")
.navigationBarTitle(Text("Add"), displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
print("Dismissing sheet view...")
self.showSheet = nil
}) {
Text("Done").bold()
})
}
}
}
struct EditAsset: View {
#Binding var showSheet: SheetView?
var body: some View {
NavigationView {
Text("Edit Asset")
.navigationBarTitle(Text("Edit"), displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
print("Dismissing sheet view...")
self.showSheet = nil
}) {
Text("Done").bold()
})
}
}
}

Show different views from NavigationBarItem menu in SwiftUI

I am trying to show a different view based on the option chosen in the NavigationBar menu. I am getting stuck on the best way to do this.
First, based on my current approach (I think it is not right!), I get a message in Xcode debugger when I press the menu item:
SideMenu[16587:1131441] [UILog] Called -[UIContextMenuInteraction
updateVisibleMenuWithBlock:] while no context menu is visible. This
won't do anything.
How do I fix this?
Second, When I select an option from the menu, how do I reset the bool so that it does not get executed unless it is chosen from the menu again. Trying to reset as self.showNewView = false within the if condition gives a compiler error
Here is a full executable sample code I am trying to work with. Appreciate any help in resolving this. Thank you!
struct ContentView: View {
#State var showNewView = false
#State var showAddView = false
#State var showEditView = false
#State var showDeleteView = false
var body: some View {
NavigationView {
GeometryReader { g in
VStack {
if self.showAddView {
AddView()
}
if self.showNewView {
NewView()
}
if self.showEditView {
EditView()
}
if self.showDeleteView {
DeleteView()
}
}.frame(width: g.size.width, height: g.size.height)
}
.navigationTitle("Title")
.navigationBarItems(leading: {
Menu {
Button(action: {showNewView.toggle()}) {
Label("New", systemImage: "pencil")
}
Button(action: {showEditView.toggle()}) {
Label("Edit", systemImage: "square.and.pencil")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}(), trailing: {
Menu {
Button(action: {showAddView.toggle()}) {
Label("Add", systemImage: "plus")
}
Button(action: {showDeleteView.toggle()}) {
Label("Delete", systemImage: "trash")
}
} label: {
Image(systemName: "plus")
}
}())
}
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct NewView: View {
var body: some View {
GeometryReader { g in
Text("This is New View")
}
.background(Color.red)
}
}
struct EditView: View {
var body: some View {
GeometryReader { g in
Text("This is Edit View")
}
.background(Color.green)
}
}
struct AddView: View {
var body: some View {
GeometryReader { g in
Text("This is Add View")
}
.background(Color.orange)
}
}
struct DeleteView: View {
var body: some View {
GeometryReader { g in
Text("This is Delete View")
}
.background(Color.purple)
}
}
Here is what I get when I select each of the menu items. I would like to be able to show only one menu item. Essentially dismiss the other one when a new menu item is selected
A possible solution is to use a dedicated enum for your current view (instead of four #State properties):
enum CurrentView {
case new, add, edit, delete
}
#State var currentView: CurrentView?
Note that you can also extract parts of code to computed properties.
Here is a full code:
enum CurrentView {
case new, add, edit, delete
}
struct ContentView: View {
#State var currentView: CurrentView?
var body: some View {
NavigationView {
GeometryReader { g in
content
.frame(width: g.size.width, height: g.size.height)
}
.navigationTitle("Title")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: leadingBarItems, trailing: trailingBarItems)
}
.navigationViewStyle(StackNavigationViewStyle())
}
#ViewBuilder
var content: some View {
if let currentView = currentView {
switch currentView {
case .add:
AddView()
case .new:
NewView()
case .edit:
EditView()
case .delete:
DeleteView()
}
}
}
var leadingBarItems: some View {
Menu {
Button(action: { currentView = .new }) {
Label("New", systemImage: "pencil")
}
Button(action: { currentView = .edit }) {
Label("Edit", systemImage: "square.and.pencil")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
var trailingBarItems: some View {
Menu {
Button(action: { currentView = .add }) {
Label("Add", systemImage: "plus")
}
Button(action: { currentView = .delete }) {
Label("Delete", systemImage: "trash")
}
} label: {
Image(systemName: "plus")
}
}
}

replace Tabbar with toolbar in SwiiftUI 2.0

I'm trying replicate the behavior of iOS Photos app.
Till now the thing I can't figure how could be done is the select mode, where when I press the button select how I can change the bottom bar?
Graphically, what I intend is, in this view:
When I pressed the button, the bottom bar changes to:
In the real project the views are embed inside a NavigationView
The code of the main view is similar to
struct ContentView: View {
var body: some View {
NavigationView{
TabView{
data()
.tabItem {
Text("Data")
}
data2()
.tabItem {
Text("Data")
}
}
}
}
I'm using Xcode 12 and swiftUI 2.0
First we need Conditional modifier like that https://stackoverflow.com/a/61253769/2715636
struct conditionalModifier: ViewModifier {
var isShowing: Bool
func body(content: Content) -> some View {
Group {
if self.isShowing {
content
.toolbar {
ToolbarItem(placement: .bottomBar, content: {
Button(action: {
}){
Image(systemName: "square.and.arrow.up")
}
})
}
.toolbar {
ToolbarItem(placement: .status, content: {
Text("Toolbar")
.fontWeight(.bold)
})
}
}
}
}}
I don't need else statement cause I only want to see Toolbar
else { content }
And here is my Tabbar inside ZStack. We're gonna overlay it with Text using Conditional modifier applied to Text
struct ContentView: View {
#State private var showToolbar: Bool = false
var body: some View {
Button(action: {
showToolbar.toggle()
}, label: {
Text(showToolbar ? "Show Tabbar" : "Show Toolbar")
}).padding()
ZStack {
TabView {
someView()
.tabItem {
Image(systemName: "placeholdertext.fill")
Text("Tab 1")
}
someView()
.tabItem {
Image(systemName: "placeholdertext.fill")
Text("Tab ")
}
someView()
.tabItem {
Image(systemName: "placeholdertext.fill")
Text("Tab 3")
}
}
Text("")
.modifier(conditionalModifier(isShowing: showToolbar))
}
}}
Final result
tabbar to toolbar
There's a new view modifier in iOS 16 that let you switch the tab bar and the bottom bar.
https://developer.apple.com/documentation/swiftui/view/toolbar(_:for:)
For example,
ContentView()
.toolbar(isSelecting ? .visible : .hidden, for: .bottomBar)
.toolbar(isSelecting ? .hidden : .visible, for: .tabBar)

SwiftUI toolbar not showing on a NavigationLink view

I'm trying to show a toolbar on a view that is inside to navigation links. When I navigate to the third view I get the following message:
2020-09-15 23:09:31.097289-0500 CountTime[35018:3542166] [Assert]
displayModeButtonItem is internally managed and not exposed for
DoubleColumn style. Returning an empty, disconnected UIBarButtonItem
to fulfill the non-null contract.
And the toolbar is not shown. This happens only on iPhone, not iPad. I'm using Xcode 12 GM.
Here is the code:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: SecondView(),
label: {
Text("Navigate")
})
}
}
}
struct SecondView: View {
var body: some View {
ZStack {
NavigationLink(
destination: Text("Destination")
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.bottomBar) {
Button(action: {
print("sharing")
}) {
Image(systemName: "square.and.arrow.up")
}
}
},
label: {
Text("Navigate")
})
}
}
}
displayModeButtonItem is internally managed and not exposed for
DoubleColumn style
In your case SwiftUI for some reason tries to present a NavigationView in a DoubleColumn style.
A possible solution is to specify the style explicitly:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: SecondView()) {
Text("Navigate")
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
I got the some issue.
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button(action: {
showEditView = true
}, label: {
Text("Edit")
})
}
}
does not work but the deprecated navigationBarItems works.
.navigationBarItems(trailing:
Button(action: {
showEditView = true
}, label: {
Text("Edit")
})
)

Maintain navigation item button alignment in SwiftUI as second button is added & removed

I have a scenario where there will be one or two trailing buttons based upon some criteria. I would like to have it such that the buttons always align to trailing for visual consistency, but so far, they appear to center align, regardless of what I do.
Below is a minimum example showing this:
import SwiftUI
struct ContentView: View {
#State private var isButtonShown = true
var body: some View {
NavigationView {
Button(action: {
self.isButtonShown.toggle()
}, label: {
Text(self.isButtonShown ? "Hide Button" : "Show Button")
})
.navigationBarItems(trailing:
HStack {
if self.isButtonShown {
Button(action: {
print("A tapped")
}, label: {
Text("A")
})
Spacer(minLength: 30)
}
Button(action: {
print("B tapped")
}, label: {
Text("B")
})
}
.frame(alignment: .trailing)
)
}
}
}
And a video that shows what happens when I select the button.
My goal is to have B remain in the same position, regardless of whether or not A is shown.
Finally, I tried a few other items:
Moved .frame(alignment: .trailing) to the NavigationView level
Added an else after self.isButtonShown that added a Spacer()
Applied .frame(alignment: .trailing) to the B Button
It is know issue in SwiftUI 1.0
SwiftUI 2.0
The solution based on new .toolbar does not have it. Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State private var isButtonShown = true
var body: some View {
NavigationView {
Button(action: {
self.isButtonShown.toggle()
}, label: {
Text(self.isButtonShown ? "Hide Button" : "Show Button")
})
.toolbar {
ToolbarItem(placement: .primaryAction) {
if self.isButtonShown {
Button(action: {
print("A tapped")
}, label: {
Text("A")
})
}
}
ToolbarItem(placement: .primaryAction) {
Button(action: {
print("B tapped")
}, label: {
Text("B")
})
}
}
}
}
}

Resources