How does one use SwiftUI's ToolbarItemGroup? - swiftui

I clear don't understand the meaning of the definition syntax for SwiftUI because I can't figure out how one would use ToolbarItemGroup.
I can define a toolbar with toolbar items like this:
.toolbar {
ToolbarItem {
Button("200%", action: zoom200).foregroundColor(controller.scale == 2.0 ? selectedButtonColor : defaultButtonColor)
}
ToolbarItem {
Button("100%", action: zoom100).foregroundColor(controller.scale == 1.0 ? selectedButtonColor : defaultButtonColor)
}
}
But have been unable to get ToolbarItemGroup to work. Logically I would have expected something like this:
.toolbar {
ToolbarItemGroup {
ToolbarItem {
Button("200%", action: zoom200).foregroundColor(controller.scale == 2.0 ? selectedButtonColor : defaultButtonColor)
}
ToolbarItem {
Button("100%", action: zoom100).foregroundColor(controller.scale == 1.0 ? selectedButtonColor : defaultButtonColor)
}
}
ToolbarItemGroup {
ToolbarItem {
Button("Open", action: open)
}
ToolbarItem {
Button("Close", action: close)
}
}
}

ToolbarItemGroup is designed to group views in the same toolbar. It removes the need for explicit usage of ToolbarItem, as both conform to ToolbarContent.
e.g.
.toolbar {
ToolbarItemGroup {
Button("200%", action: zoom200)
.foregroundColor(controller.scale == 2.0 ? selectedButtonColor : defaultButtonColor)
Button("100%", action: zoom100)
.foregroundColor(controller.scale == 1.0 ? selectedButtonColor : defaultButtonColor)
}
ToolbarItemGroup(placement: .bottomBar) {
Spacer()
Button("Open", action: open)
Spacer()
Button("Close", action: close)
Spacer()
}
}
It's also the only way I know of to get Spacers to work, between toolbar items.

The ToolbarItemGroup is output entity, not input - as it is clear from the following toolbar builders:
/// Populates the toolbar or navigation bar with the specified items.
///
/// - Parameter items: The items representing the content of the toolbar.
public func toolbar<Items>(#ToolbarContentBuilder<Void> items: () -> ToolbarItemGroup<Void, Items>) -> some View
/// Populates the toolbar or navigation bar with the specified items,
/// allowing for user customization.
///
/// - Parameters:
/// - id: A unique identifier for this toolbar.
/// - items: The items representing the content of the toolbar.
public func toolbar<Items>(id: String, #ToolbarContentBuilder<String> items: () -> ToolbarItemGroup<String, Items>) -> some View
Thus groups are generated automatically by .toolbar based on toolbar items placement (in predefined order).
Here is example (tested with Xcode 12b / iOS 14)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {}) { Image(systemName: "book") }
}
ToolbarItem(placement: .primaryAction) {
Button(action: {}) { Image(systemName: "gear") }
}
ToolbarItem(placement: .principal) {
Button(action: {}) { Image(systemName: "car") }
}
ToolbarItem(placement: .principal) {
Button(action: {}) { Image(systemName: "gear") }
}
ToolbarItem(placement: .bottomBar) {
Button(action: {}) { Image(systemName: "1.square") }
}
ToolbarItem(placement: .bottomBar) {
Button(action: {}) { Image(systemName: "2.square") }
}
}

Related

How to build sort direction in menu like Files app?

I'm displaying sort options in my Menu, but I'd also like the user to control the sort direction in the way the Files app works: tapping a second time toggles the sort direction.
Here's what I have:
#State private var selectedSort: SortOption = .name
#State private var isSortAscending = true
enum SortOption {
case name
case number
case length
}
Menu {
Picker(selection: $selectedSort, label: Text("Sorting options")) {
Button {
isSortAscending.toggle()
} label: {
HStack {
Text("Name")
Spacer()
Image(systemName: isSortAscending ? "chevron.down" : "chevron.up")
}
}
.tag(SortOption.name)
Button {
isSortAscending.toggle()
} label: {
HStack {
Text("Number")
Spacer()
Image(systemName: isSortAscending ? "chevron.down" : "chevron.up")
}
}
.tag(SortOption.number)
Button {
isSortAscending.toggle()
} label: {
HStack {
Text("Length")
Spacer()
Image(systemName: isSortAscending ? "chevron.down" : "chevron.up")
}
}
.tag(SortOption.length)
}
}
Tapping doesn't toggle the sort state at all. Is there a better or more supported way to do this?
Item selected is handled internally, so we need selection side-effect. It is possible to do by injecting will-set side effect in computable binding.
Here is a possible approach tested with Xcode 13.4 / iOS 15.5
Main part:
var sorting: Binding<SortOption> { .init(
get: { self.selectedSort },
set: {
if self.selectedSort == $0 {
self.isSortAscending.toggle()
}
self.selectedSort = $0
}
)}
var body: some View {
Menu("Sort") {
Picker(selection: sorting, label: Text("Sorting options")) {
ForEach(SortOption.allCases) { option in
HStack {
Text(option.rawValue)
Spacer()
if selectedSort == option {
Image(systemName: isSortAscending ? "chevron.down" : "chevron.up")
}
Test module on GitHub

Navigation Title Issue

I have a navigation title for a list view. After navigating back and forth, my navigation title is missing. I am new to swiftui and unable to debug. Kindly help.
var body: some View {
NavigationView {
VStack {
TabView(selection: $choice,
content: {
OPListCell()
IPListCell()
})
.tabViewStyle(PageTabViewStyle())
}
.listStyle(PlainListStyle())
.navigationBarTitle("My Patients")
.toolbar {
ToolbarItem(placement: .principal) {
HStack {
Picker(selection: self.$choice, label: Text("")) {
ForEach(0 ..< self.choices.count) {
Text(self.choices[$0])
}
}
.frame(width: 175)
.pickerStyle(SegmentedPickerStyle())
.padding(.leading, 10)
}
}
}
}
}
}

How can I present Menu (Not context menu) by using a Button in SwiftUI?

struct MainMenuView: View {
var body: some View {
Menu("Actions") {
Button("Duplicate", action: duplicate)
Button("Rename", action: rename)
Button("Deleteā€¦", action: delete)
}
}
}
Button(action:{
MainMenuView()
}, label: { Text("More") })
I have a "Menu" view it's just a menu, and I want to present it in another view using button action closure, so when tapping the button the menu pops up, anyone can help me with this, please.
Try this Using toolbar as an example
.toolbar{
ToolbarItem(placement: .primaryAction) {
Menu {
Button(action: {//action}) {
Label("Bttn 1", systemImage: "text.badge.plus")
}
Button(action: {//action}) {
Label("Bttn 2", systemImage: "text.badge.plus")
}
}
label: {
Label("Add", systemImage: "plus")
.font(.title2)
.foregroundColor(.white)
}
}
}

Buttons inside ToolbarItem cannot dismiss sheet

In Xcode 12 Beta 6, dismissing a sheet doesn't work inside a button's action inside a ToolbarItem.
My sheet view looks like:
NavigationView {
Form {
Section {
TextField("Name", text: $name)
}
}
.navigationTitle("New Thing")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(action: {
self.presentation.wrappedValue.dismiss()
}, label: {
Text("Cancel")
})
}
ToolbarItem(placement: .confirmationAction) {
Button(action: {
do {
// some saving logic
try managedObjectContext.save()
self.presentation.wrappedValue.dismiss()
} catch {
print("didn't save due to \(error.localizedDescription)")
}
}, label: {
Text("Save")
})
}
}
}
EDIT: here's how I constructed the sheet
var body: some View {
List {
ForEach(results) { result in
HStack {
NavigationLink(destination: SingleResultView(result: result)) {
SingleResultRowView(result: result)
}
}
}
.onDelete(perform: deleteResult)
}
.navigationTitle("All Results")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
self.isNewResultSheetPresented.toggle()
}, label: {
Image(systemName: "plus.circle.fill")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
})
.sheet(isPresented: $isNewResultSheetPresented) {
NewResultView()
// ^ this contains the code above
.environment(\.managedObjectContext, self.managedObjectContext)
}
}
}
}
When the sheet is first presented, immediately a console log appears:
2020-09-13 20:52:02.333679-0700 MyApp[2710:89263]
[Presentation] Attempt to present <_TtGC7SwiftUI22SheetHostingControllerVS_7AnyView_: 0x1027b7890> on
<_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x10270d620>
(from <_TtGC7SwiftUIP10$194f39bd428DestinationHostingControllerVS_7AnyView_: 0x103605930>)
which is already presenting <_TtGC7SwiftUI22SheetHostingControllerVS_7AnyView_: 0x103606d60>.
I can dismiss the sheet only by swiping down.
For reference, I went back to an older commit where I used NavigationBarItems and it worked perfectly. But from what I understand, this is a situation where I'm supposed to be using ToolbarItem.
Does anybody know why the good old self.presentation.wrappedValue.dismiss() doesn't work here or why is the sheet being presented twice?
Move sheet out of toolbar, as
var body: some View {
List {
ForEach(results) { result in
HStack {
NavigationLink(destination: SingleResultView(result: result)) {
SingleResultRowView(result: result)
}
}
}
.onDelete(perform: deleteResult)
}
.navigationTitle("All Results")
.sheet(isPresented: $isNewResultSheetPresented) { // << here !!
NewResultView()
// ^ this contains the code above
.environment(\.managedObjectContext, self.managedObjectContext)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
self.isNewResultSheetPresented.toggle()
}, label: {
Image(systemName: "plus.circle.fill")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
})
}
}
}

SwiftUI - Form Picker & Button Not Co-Existing

I have created a reusable component that has a picker, textfield and button within a form. However, with the button present, a tap on the picker field does not go to the picker. Rather it executes the button code. The TextField works fine. If I remove the button code, the proper behavior will occur with the picker. So the question is how to have both elements within this component? Please note that the preview adds the Navigation and Form which would otherwise come from the parent view.
var body: some View {
HStack {
if !showFee {
Spacer()
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
self.showFee.toggle()
}
}) {
Image(systemName: "plus.circle")
.font(.largeTitle)
}
Spacer()
} else {
VStack(spacing:20) {
Picker(selection: $feeSelection, label: Text("Fee Type")) {
ForEach(0 ..< fees.count) {
Text(self.fees[$0])
}
}
TextField("Fee Amount: $", value: $feeAmount, formatter: NumberFormatter.currency)
.keyboardType(.decimalPad)
Divider()
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
self.showFee.toggle()
}
}) {
Image(systemName: "trash.circle.fill")
.font(.largeTitle)
.foregroundColor(.red)
}
}
}
}
.padding()
}
}
struct FeeCell_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
Form {
FeeCell()
}
}
}
}
What you can do is to apply the PlainButtonStyle to your button. This will stop the button's tap covering to the whole cell in the Form:
Button(action: {}) {
Text("Button")
}
.buttonStyle(PlainButtonStyle())