SwiftUI: Creating a Pull-down menu - swift

in the HIG Apple writes: In iOS 14 and later, a button can display a pull-down menu that lists items or actions from which people can choose
This is exactly what I want for my project. This picture where they have a "more" bar button with a drop-down menu fits the bill perfectly. Does anyone have an example, though, of how to create a pull-down menu (not context-menu) from a button with SwiftUI?

You can simply use the Menu view that is new for iOS in iOS 14.
It acts as a button and when pressed presents the context menu. You can use a Label if you want an image and even nest different views, as shown in the
example in the documentation.
struct ContentView: View {
#State var text = "Hello World"
var body: some View {
NavigationView {
Text("Hello World")
.navigationTitle("Hello")
.navigationBarItems(trailing: {
Menu {
Button(action: { text = "Hello there" }) {
Label("Hello", systemImage: "pencil")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}())
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
Instead of navigationBarItems one should probably use toolbar, however I found that to be quite unreliable starting with Beta 4.

Related

SwiftUI Editable NavigationTitle

Is it possible to make the navigation title of SwiftUI editable?
Unfortunately the navigationTitle modifier only accepts Text views and not TextField views.
I want to do this instead of just using a text field below the navigation bar because I still want the nice default behaviour of having the modified title appear in the navigation bar inline when the user scrolls down and the navigation bar allocates space for the navigation title whether you define one or not.
iOS/iPadOS 16+, macOS 13+
The navigationTitle modifier now accepts a Binding<String> argument, as well as the more usual String-based initializer.
When using a bound value and the navigation bar is in its inline form, the title gains a drop-down menu with a Rename option. Tapping this allows the user to edit the view's title:
struct EditableTitleView: View {
#State private var title = "View Title"
var body: some View {
Text("Editable Title View")
.navigationTitle($title)
.navigationBarTitleDisplayMode(.inline)
}
}
This isn't exactly the same UX as always using a text field, but as it's a standard SwiftUI implementation it's a lot easier to implement.
For earlier versions of iOS
You can place a custom view in your NavigationView at the position where the title might be expected to go by specifying a ToolbarItem with a placement value of .principal, for example in the code below. I've added the RoundedBorderTextFieldStyle to make the text field more visible:
struct EditableTitleView: View {
#State private var editableTitle: String = "My Title"
var body: some View {
NavigationView {
Text("View with editable title")
.toolbar {
ToolbarItem(placement: .principal) {
TextField("Title", text: $editableTitle)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
}
}
Note that if you also add a navigationTitle modifier into your view, its default large style on iOS will still display beneath the toolbar, but if it scrolls off the page it while disappear while your principal item will remain on screen. If you set .navigationBarTitleDisplayMode(.inline) then the larger style title will never display.
I mention this because you should consider keeping a title for your view anyway, for a couple of reasons:
If a NavigationLink pushes a view on the stack, you want a meaningful name to appear in the back button and the list of stacked views that appear on long press.
I haven't checked what happens with VoiceOver when you have a navigation subview without a title. The more you override native behaviour, the more you need to consider whether you are making your app less accessible than the SwiftUI defaults provide.
Try a TextField inside of a ToolbarItem in the principal slot of the toolbar. Create a computed property for the destination and give it an editable navigation title, too.
struct TextFieldNavigationTitleView: View {
#State var mainTitle = "Main Menu"
#State var areaOneTitle = "Area One"
var body: some View {
NavigationView {
NavigationLink("App Area One", destination: areaOne)
.toolbar {
ToolbarItem(placement: .principal) {
TextField("Navigation Title", text: $mainTitle)
}
}
}
}
var areaOne : some View {
Text("AREA ONE")
.toolbar {
ToolbarItem(placement: .principal) {
TextField("Area One Title", text: $areaOneTitle)
}
}
}
}

SwiftUI - menu not appearing and double toolbar for NavigationView

I am creating a iOS app with XCode. All source code has been written and compiled.
The app runs in the iOS simulator.
The user interface was created in SwiftUI and it appears as expected.
The navigation seems to be working across the screens but I cannot have the menu as designed and I see a double toolbar where the back button appears.
The navigation happens by NavigationLinks associated to buttons.
If I navigate one screen deep I have a back button and a back icon. It seems that the back button is a menu in fact.
If I navigate two screens deep the back button shows a menu with two back options, one lets the user navigate back one level, the other lets the user navigate back two levels.
The main problem is that in the main screen no menu appears.
It doesn't depend on the content of the main view. Indeed if it is stripped down from the View still I do not have the menu, but I do not have the double toolbar either.
var body: some View {
NavigationView {
VStack{
Text("HELLO")
/*here was a sort of master-detail layout*/
}
}.navigationViewStyle(StackNavigationViewStyle()).navigationTitle("SwiftUI").toolbar {
ToolbarItem(placement: .primaryAction) {
Menu
{ //this is what the menu content is like but it never appeared
NavigationLink(destination:HelpView())
{ Label(help_menu_item, systemImage: "")
}
Button(action: {}) {
Label(liability_disclaimer_menu_item, systemImage: "")
}
Button(action: {}) {
Label(about_menu_item, systemImage: "")
}
//other buttons
}//menu
label: {
Label("Menu", systemImage: "ellipsis")
}
}
}
} //body
What changes or checks can be done? I want the menu and a single toolbar.
I also tried commenting things and trying step by step additions but even the simplest case does not work.
Put the toolbar modifier on the top-level view within the NavigationView, not on the NavigationView itself.

Docked Sidebar on iPad in portrait orientation with SwiftUI

SwiftUI helpfully gives you NavigationView which easily lets you define a sidebar and main content for iPad apps that automatically collapse for iPhones.
I have an app and everything works as expected except on iPad, in portrait mode, the sidebar is hidden by default and you are forced click a button to show it.
All I want is to force the sidebar to always be visible, even in portrait mode. And make it work the same way as the settings app.
I’m even willing to use a UIKit view wrapped for SwiftUI, but wrapping NavigationController seems very very challenging.
Is there a better way?
Without your specific code I can't be sure but I think you are describing the initial screen that shows on iPad. That is actually a "ThirdView". You can see it with the code below.
And I am answering the "better way" portion of your question. Which for me is a way of just "graciously" dealing with it.
struct VisibleSideBar2: View {
var body: some View {
NavigationView{
List(0..<10){ idx in
NavigationLink("SideBar \(idx)", destination: Text("Secondary \(idx)"))
}
Text("Welcome Screen")
}
}
}
It is even more apparent if you have a default selection in your "Secondary View" because now you have to click "Back" twice to get to the SideBar
struct VisibleSideBar1: View {
#State var selection: Int? = 1
var body: some View {
NavigationView{
List(0..<10){ idx in
NavigationLink(
destination: Text("Secondary \(idx)"),
tag: idx,
selection: $selection,
label: {Text("SideBar \(idx)")})
}
Text("Third View")
}
}
}
A lot of the "solutions" out there for this just turn the NavigationView into a Stack but then you can't get the double column.
One way of dealing with it is the what is depicted in VisibleSideBar2. You can make/embrace a nice "Welcome Screen" so the user isn't greeted with a blank screen and then the natural navigation instincts kick in. You only see the "Welcome Screen" on iPad Portrait and on Catalyst/MacOS where Stack is unavailable.
Or you can bypass the third screen by using isActive in a NavigationLink and using the Sidebar as a menu like View
struct VisibleSidebar3: View{
#State var mainIsPresented = true
var body: some View {
NavigationView {
ScrollView{
NavigationLink(
destination: Text("Main View").navigationTitle("Main"),
isActive: $mainIsPresented,
label: {
Text("Main View")
})
NavigationLink("List View", destination: ListView())
}.navigationTitle("Sidebar")
//Not visible anymore
Text("Welcome Screen")
}
}
}
struct ListView: View{
var body: some View {
List(0..<10){ idx in
NavigationLink("SideBar \(idx)", destination: Text("Secondary \(idx)"))
}.navigationTitle("List")
}
}
Like I said my answer isn't really a way of "fixing" the issue. Just dealing with it. To fix it we would have to somehow dismiss the "Third Screen/Welcome Screen". Then manipulate the remaining UISplitViewController (Several SO questions on this) to show both the SideBar/Master and the Detail View.
In UIKit it seems to have been done a lot, if you search SO, you will find a way to create a UISplitViewController that behaves like Settings.

SwiftUI Navigation on iPad - How to show master list

My app has simple navigation needs
List View (parent objects)
List View (child objects)
Detail View (child object)
I have this setup and working on iPhone, but when I run the app on iPad in portrait mode the master list is always hidden.
I'm using .isDetailLink(false) on the navigation link from the first list to the second, so both lists always stay in the master column. In iPad landscape everything works as expected but in portrait the detail view fills the screen. I can swipe in from the left side of the screen to show the list but I'd like to provide more clarity to the user.
I'd like to show or add the back button to show the master/list side (sort of like the Apple Notes app). On the iPhone I get the back button by default but on iPad in portrait mode there is nothing in its place.
This is what I see on iPhone
But this is what I see on iPad
Parent list
struct ParentList: View {
let firstList = ["Sample data 01", "Sample data 02", "Sample data 03", "Sample data 04", "Sample data 05"]
var body: some View {
NavigationView {
List{
ForEach(firstList, id: \.self) { item in
NavigationLink(destination: ChildList()){
Text(item)
}
.isDetailLink(false)
}
}
}
}
}
Child list
struct ChildList: View {
let secondList = ["More Sample data 01", "More Sample data 02", "More Sample data 03", "More Sample data 04", "More Sample data 05"]
var body: some View {
List{
ForEach(secondList, id: \.self) { item in
NavigationLink(destination: ChildDetail()){
Text(item)
}
}
}
}
}
Child detail
struct ChildDetail: View {
var body: some View {
Text("Child detail view")
}
}
Update: As of Oct 17, 2019 I have not found a way to get this to work. I decided to use .navigationViewStyle(StackNavigationViewStyle()) for the time being. Interestingly, this needs to go outside of the navigation view like a normal modifier, not inside it with the navigation title.
In portrait the default split view does not work. This may be fixed in future but it appears the current options are:
(a) change the navigation view style of your first list to .navigationViewStyle(StackNavigationViewStyle()) so the navigation will work like on iPhone and push each view.
(b) leave the style to default and only support landscape for iPad
(c) implement a UIKit split view controller
There also is a quite hacky workaround (see https://stackoverflow.com/a/57215664/3187762)
By adding .padding() to the NavigationView it seems to achieve the behaviour of always display the Master.
NavigationView {
MyMasterView()
DetailsView()
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
.padding()
Not sure if it is intended though. Might break in the future (works using Xcode 11.0, in simulator on iOS 13.0 and device with 13.1.2).
You should not rely on it. This comment seems to be the better answer: https://stackoverflow.com/a/57919024/3187762
in iOS 13.4, a "back to master view" button has been added to the iPad layout. From the release notes:
When using a NavigationView with multiple columns, the navigation bar now shows a control to toggle the columns. (49074511)
For example:
struct MyNavView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: Text("navigated"),
label: {Text("Go somwhere")}
)
.navigationBarTitle("Pick Item")
}
}
}
Has the following result:
Look this solution here
I hope this help
For my project I'm using this extension.
They will always use StackNavigationViewStyle for iPhone, iPad in a vertical orientation, and if you provide forceStackedStyle: true.
Otherwise DoubleColumnNavigationViewStyle will be used.
var body: some View {
NavigationView {
Text("Hello world")
}
.resolveNavigationViewStyle(forceStackedStyle: false)
}
extension View {
func resolveNavigationViewStyle(forceStackedStyle: Bool) -> some View {
if forceStackedStyle || UIDevice.current.userInterfaceIdiom == .phone {
return self.navigationViewStyle(StackNavigationViewStyle())
.eraseToAnyView()
} else {
return GeometryReader { p in
if p.size.height > p.size.width { self.navigationViewStyle(StackNavigationViewStyle())
} else {
self.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
.eraseToAnyView()
}
}
}

SwiftUI - Apple Watch Menu (Force Touch)

I'm trying to implement a menu on Apple Watch using SwiftUI but I can't find a way to do it. Even on the interface.storyboard, I can't drag/drop the menu.
Did you manage to make it work with SwiftUI? If yes, how?
I searched online but nothing so far.
Yes, this is possible. It's important to remember that unlike on iOS, a view can have only one single context menu, individual elements within the view can not have their own context menu.
Anyway, to implement a context menu (force touch menu) on Apple Watch with SwiftUI, add the .contextMenu() modifier to top-most view in your body
Example:
var body: some View {
Group {
Text("Hello Daymo")
}
.contextMenu(menuItems: {
Button(action: {
print("Refresh")
}, label: {
VStack{
Image(systemName: "arrow.clockwise")
.font(.title)
Text("Refresh view")
}
})
})
}
Edit the button (or add buttons) as you see fit.