SwiftUI Keyboard Shortcuts Not Working In Toolbar Or Menus - swift

SwiftUI keyboard shortcuts do not appear to work at all when the button is in a Menu and they only work in certain ToolbarItem Placements such as bottomBar.
I've put together this demo app to show the current behavior.
Unfortunately I think this is a SwiftUI bug but can anyone figure out a way to get these working, especially the buttons in Menus.
This is tested with Xcode 12.2 and iOS 14.2.
Feedback number: FB8895536
struct ContentView: View{
#State private var tabSelection = Tab.navigationBarItemTest
enum Tab {
case navigationBarItemTest
case navigationBarMenuTest
case toolbarItemTest
case ToolbarItemMenuTest
}
var body: some View{
TabView(selection: $tabSelection) {
NavigationBarItemTest()
.tabItem {
Label("Nav Bar", systemImage: "1.circle")
}
.tag(Tab.navigationBarItemTest)
NavigationBarMenuTest()
.tabItem {
Label("Nav Bar Menu", systemImage: "2.circle")
}
.tag(Tab.navigationBarMenuTest)
ToolbarItemTest()
.tabItem {
Label("Toolbar Bar", systemImage: "3.circle")
}
.tag(Tab.toolbarItemTest)
ToolbarItemMenuTest()
.tabItem {
Label("Toolbar Bar Menu", systemImage: "4.circle")
}
.tag(Tab.ToolbarItemMenuTest)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct NavigationBarItemTest: View{
#State private var show1 = false
var body: some View{
NavigationView{
Color.blue
.frame(width: 200, height: 200)
.cornerRadius(10)
.navigationBarTitle("1")
.frame(width: 200, height: 200)
.cornerRadius(10)
.sheet(isPresented: $show1, content: {
Color.blue
})
.navigationBarItems(trailing:
Button("Show"){
show1.toggle()
}.keyboardShortcut("1", modifiers: .command)
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct NavigationBarMenuTest: View{
#State private var show2 = false
var body: some View{
NavigationView{
Color.red
.frame(width: 200, height: 200)
.cornerRadius(10)
.navigationBarTitle("2")
.frame(width: 200, height: 200)
.cornerRadius(10)
.sheet(isPresented: $show2, content: {
Color.red
})
.navigationBarItems(trailing:
Menu{
Button("Show"){
show2.toggle()
}.keyboardShortcut("2", modifiers: .command)
} label: {
Image(systemName: "plus")
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ToolbarItemTest: View{
#State private var show3 = false
var body: some View{
NavigationView{
Color.green
.frame(width: 200, height: 200)
.cornerRadius(10)
.navigationBarTitle("3")
.frame(width: 200, height: 200)
.cornerRadius(10)
.sheet(isPresented: $show3, content: {
Color.green
})
.toolbar{
ToolbarItem(placement: .navigationBarTrailing) {
Button("Show"){
show3.toggle()
}.keyboardShortcut("3", modifiers: .command)
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ToolbarItemMenuTest: View{
#State private var show4 = false
var body: some View{
NavigationView{
Color.yellow
.frame(width: 200, height: 200)
.cornerRadius(10)
.navigationBarTitle("4")
.frame(width: 200, height: 200)
.cornerRadius(10)
.sheet(isPresented: $show4, content: {
Color.yellow
})
.toolbar{
ToolbarItem(placement: .navigationBarTrailing) {
Menu{
Button("Show"){
show4.toggle()
}.keyboardShortcut("4", modifiers: .command)
} label: {
Image(systemName: "plus")
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}

Related

How can i remove this space from a SwiftUI custom CustomTabBar

You can see the issue in the image below there is a space above the Rectangle with a teal Color that I cant remove so that code can match the design.
You can replicate the issue using the code below:
import SwiftUI
#main
struct DatemeApp: App {
var body: some Scene {
WindowGroup {
RootView()
}
}
}
and
import SwiftUI
struct RootView: View {
#StateObject var viewRouter = ViewRouter()
var body: some View {
CustomTabBar(viewRouter: viewRouter)
}
}
struct RootView_Previews: PreviewProvider {
static var previews: some View {
RootView()
}
}
struct HomeView: View {
var body: some View {
ScrollView(.vertical) {
SectionView()
SectionView()
SectionView()
SectionView()
SectionView()
}
}
}
struct SectionView: View {
var body: some View {
VStack {
ScrollView(.horizontal) {
LazyHStack( pinnedViews: .sectionHeaders) {
ForEach(0..<8) { _ in
Rectangle()
.foregroundColor(.gray)
.frame(width: 132, height: 168)
}
}
}
}.background(Color.green)
}
}
class ViewRouter: ObservableObject {
#Published var currentPage: Page = .home
}
enum Page {
case home
case liked
case records
case user
}
struct CustomTabBar: View {
#StateObject var viewRouter: ViewRouter
var body: some View {
GeometryReader { geometry in
VStack {
Spacer()
switch viewRouter.currentPage {
case .home:
HomeView()
.background(Color.green)
case .liked:
Text("Msg")
case .records:
Text("Cal")
case .user:
Text("People")
}
Spacer()
Rectangle()
.fill(Color.teal)
.frame(height: 2)
ZStack {
HStack {
TabBarIcon(viewRouter: viewRouter, assignedPage: .home, width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "house.fill", tabName: "Home")
TabBarIcon(viewRouter: viewRouter, assignedPage: .liked, width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "ellipsis.message.fill", tabName: "Msg")
Image(systemName: "plus.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.symbolRenderingMode(.palette)
.foregroundStyle(Color.yellow, Color.teal)
.offset(y: -geometry.size.height / 13.5)
.onTapGesture {
print("Plus Button Tapped")
}
TabBarIcon(viewRouter: viewRouter, assignedPage: .records, width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "calendar", tabName: "Cal")
TabBarIcon(viewRouter: viewRouter, assignedPage: .user, width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "person", tabName: "People")
}
.frame(width: geometry.size.width, height: geometry.size.height/8)
}
}
.edgesIgnoringSafeArea(.bottom)
}
}
}
struct TabBarIcon: View {
#StateObject var viewRouter: ViewRouter
let assignedPage: Page
let width, height: CGFloat
let systemIconName, tabName: String
var body: some View {
VStack {
Image(systemName: systemIconName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: width, height: height)
// .padding(.top, 10)
Text(tabName)
.font(.footnote)
Spacer()
}
.padding(.horizontal, -4)
.onTapGesture {
viewRouter.currentPage = assignedPage
}
.foregroundColor(viewRouter.currentPage == assignedPage ? Color(UIColor(hue:0.713, saturation:0.532, brightness:0.393, alpha:1.000)) : Color(UIColor(hue:0.648, saturation:0.421, brightness:0.705, alpha:1.000)))
}
}
You have 2 elemnts in your CustomTabBars VStack that cause this. Remove:
Spacer()
Rectangle()
.fill(Color.teal)
.frame(height: 2)
A spacer has a minimum height, also the Rectange has an explicit height of 2.

Why does my custom tab bar get pushed up everytime the on screen keyboard pops up?

I've made a custom tab bar and I'm trying to add a search bar on one of my views. For some reason half of the tab gets pushed up when the onscreen keyboard appears. Ive tried " .ignoresSafeArea(.keyboard)" literally everywhere and the only thing that happens is my icons for the tab disappear but the top of the tab still stays there. I've been trying to fix it for the last 24 hours but I'm getting nowhere, can somebody please help me with this. Thanks!
Code for Custom Tab Bar:
import SwiftUI
enum Tabs: Int {
case home = 0
case hot = 1
case favourites = 2
case settings = 3
}
struct TabBar: View {
#Binding var selectedTab : Tabs
var body: some View {
HStack (alignment: .center){
Button {
//switch to home
selectedTab = .home
} label: {
GeometryReader { geo in
if selectedTab == .home {
Rectangle()
// .ignoresSafeArea(.keyboard)
.foregroundColor(.red)
.frame(width: geo.size.width/2, height: 5)
.padding(.leading, geo.size.width/4)
}
VStack (alignment: .center){
Image(systemName: "house")
// .ignoresSafeArea(.keyboard)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.padding(.top, 40.0)
}
// .ignoresSafeArea(.keyboard)
.frame(width: geo.size.width, height: geo.size.height)
}
//.ignoresSafeArea(.keyboard)
.offset(y: -35)
//.ignoresSafeArea(.keyboard)
}
// .ignoresSafeArea(.keyboard)
.tint(Color.black)
Button {
//Switch views
selectedTab = .hot
} label: {
GeometryReader { geo in
if selectedTab == .hot {
Rectangle()
//.ignoresSafeArea(.keyboard)
.foregroundColor(.red)
.frame(width: geo.size.width/2, height: 5)
.padding(.leading, geo.size.width/4)
}
VStack (alignment: .center){
Image(systemName: "flame")
//.ignoresSafeArea(.keyboard)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.padding(.top, 40.0)
}
//.ignoresSafeArea(.keyboard)
.frame(width: geo.size.width, height: geo.size.height)
}
//.ignoresSafeArea(.keyboard)
.offset(y: -35)
}
//.ignoresSafeArea(.keyboard)
.tint(Color.black)
Button {
//Switch views
selectedTab = .favourites
} label: {
GeometryReader { geo in
if selectedTab == .favourites {
Rectangle()
//.ignoresSafeArea(.keyboard)
.foregroundColor(.red)
.frame(width: geo.size.width/2, height: 5)
.padding(.leading, geo.size.width/4)
}
VStack (alignment: .center){
Image(systemName: "star")
//.ignoresSafeArea(.keyboard)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.padding(.top, 40.0)
}//.ignoresSafeArea(.keyboard)
.frame(width: geo.size.width, height: geo.size.height)
}
//.ignoresSafeArea(.keyboard)
.offset(y: -35)
}
//.ignoresSafeArea(.keyboard)
.tint(Color.black)
Button {
//Switch views
selectedTab = .settings
} label: {
GeometryReader { geo in
if selectedTab == .settings {
Rectangle()
//.ignoresSafeArea(.keyboard)
.foregroundColor(.red)
.frame(width: geo.size.width/2, height: 5)
.padding(.leading, geo.size.width/4)
}
VStack (alignment: .center){
Image(systemName: "gear")
//.ignoresSafeArea(.keyboard)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.padding(.top, 40.0)
}//.ignoresSafeArea(.keyboard)
.frame(width: geo.size.width, height: geo.size.height)
}
//.ignoresSafeArea(.keyboard)
.offset(y: -35)
}
//.ignoresSafeArea(.keyboard)
.tint(Color.black)
}
//.ignoresSafeArea(.keyboard, edges: .all)
.frame(height: 20)
}
}
struct TabBar_Previews: PreviewProvider {
static var previews: some View {
TabBar(selectedTab: .constant(.home))
.ignoresSafeArea(.keyboard)
}
}
ignoresSafeArea is commented in all the places I tried to put it.
Here is also the code for the search bar, maybe I need to input ignoreSafeArea somewhere here?
import SwiftUI
struct SearchBarView: View {
#StateObject var im = SearchBarContents()
//#State var selectedTabs: SearchB = .search
#State private var query = ""
var body: some View {
NavigationView {
List {
ForEach(im.filteredData) { item in
HStack{
NavigationLink(destination: ItemView(item: item))
{
SBarView(item: item)
}
}
}
}
.navigationTitle("Items")
.searchable(text: $query,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Find an Item") {
}
.onSubmit(of: .search) {
im.search(with: query)
}
.onChange(of: query) { newQuery in
im.search(with: newQuery)
}
.onAppear {
im.search()
}
}
}
}
struct SearchBarView_previews: PreviewProvider {
static var previews: some View {
SearchBarView()
}
}
If there is anything missing please let me know.
Imgur link to see exactly what I mean
https://imgur.com/fv79bKh
https://imgur.com/a/Rx9Ki6c (after I add ignoresafearea)
Depends on how you have set up your project. If you have a RootView which is controlling the View to be displayed based on the selection of your Tab Bar and is also the location in the project that you would add the Custom Tab Bar, then on that root View add the modifier .ignoresSafeArea(.keyboard)
For example:
enum Tabs: Int {
case home = 0
case hot = 1
case favourites = 2
case settings = 3
}
struct RootView: View {
#State private var selectedTab = Tabs.home
var body: some View {
VStack {
switch selectedTab {
case .home:
HomeView()
case .hot:
HotView()
case .favourites:
FavouritesView()
case .settings:
SettingsView()
}
Spacer()
CustomTabBar(selectedTab: $selectedTab)
}
.ignoresSafeArea(.keyboard)
}
}

Using TextField hides the ScrollView beneath it in VStack

This view hold a list of pdf names which when tapped open webviews of pdf links.
The view has a search bar above the list which when tapped causes the scrollview to disappear.
struct AllPdfListView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#ObservedObject var pdfsFetcher = PDFsFetcher()
#State var searchString = ""
#State var backButtonHidden: Bool = false
#State private var width: CGFloat?
var body: some View {
GeometryReader { geo in
VStack(alignment: .leading, spacing: 1) {
HStack(alignment: .center) {
Image(systemName: "chevron.left")
Text("All PDFs")
.font(.largeTitle)
Spacer()
}
.padding(.leading)
.frame(width: geo.size.width, height: geo.size.height / 10, alignment: .leading)
.background(Color(uiColor: UIColor.systemGray4))
.onTapGesture {
self.mode.wrappedValue.dismiss()
}
HStack(alignment: .center) {
Image(systemName: "magnifyingglass")
.padding([.leading, .top, .bottom])
TextField ("Search All Documents", text: $searchString)
.textFieldStyle(PlainTextFieldStyle())
.autocapitalization(.none)
Image(systemName: "slider.horizontal.3")
.padding(.trailing)
}
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.black, lineWidth: 1))
.padding([.leading, .top, .bottom])
.frame(width: geo.size.width / 1.05 )
ScrollView {
ForEach($searchString.wrappedValue == "" ? pdfsFetcher.pdfs :
pdfsFetcher.pdfs.filter({ pdf in
pdf.internalName.contains($searchString.wrappedValue.lowercased())
})
, id: \._id) { pdf in
if let parsedString = pdf.file?.split(separator: "-") {
let request = URLRequest(url: URL(string: "https://mylink/\(parsedString[1]).pdf")!)
NavigationLink(destination: WebView(request: request)
.navigationBarBackButtonHidden(backButtonHidden)
.navigationBarHidden(backButtonHidden)
.onTapGesture(perform: {
backButtonHidden.toggle()
})) {
HStack(alignment: .center) {
Image(systemName: "doc")
.padding()
.frame(width: width, alignment: .leading)
.lineLimit(1)
.alignmentGuide(.leading, computeValue: { dimension in
self.width = max(self.width ?? 0, dimension.width)
return dimension[.leading]
})
Text(pdf.internalName)
.padding()
.multilineTextAlignment(.leading)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
.padding(.leading)
}
}
}
.navigationBarHidden(true)
}
.accentColor(Color.black)
.onAppear{
pdfsFetcher.pdfs == [] ? pdfsFetcher.fetchPDFs() : nil
}
}
}
}
}
Pdf list and Searchbar.
The same view on Searchbar focus.
I would like the search string to filter the list of pdfs while maintaining the visibility of the list.
I was able to fix this by making my #ObservableObject an #EnvironmentObject in my App :
#main
struct MyApp: App {
#ObservedObject var pdfsFetcher = PDFsFetcher()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(pdfsFetcher)
}
}
}
struct AllPdfListView: View {
#EnvironmentObject var pdfsFetcher: PDFsFetcher
}

SwiftUI - how to increase Toggle's target touch size?

I want to increase Toggle's target size for usability purposes.
It's easy with buttons using .frame(width: 200, height: 200) . But I don't know how to apply the same for toggles.
Here is my code. I have marked where it works and where it doesn't.
import SwiftUI
struct ContentView: View {
#State private var test = true
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Button(action: {
print("button pressed")
}) {
Image(systemName: "checkmark.circle.fill")
.font(.largeTitle)
// This works for Button
.frame(width: 200, height: 200)
}
Toggle(isOn: Binding<Bool>(
get: { test },
set: {
test = $0
})) {
Text("Done")
}
// This doesn't work for Toggle
.frame(width: 200, height: 200)
.labelsHidden()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can try something like this:
Toggle(isOn: Binding<Bool>(
get: { test },
set: {
test = $0
})) {
Text("Done")
}
.frame(width: 200, height: 200)
.labelsHidden()
.contentShape(Rectangle())
.onTapGesture {
withAnimation {
test.toggle()
}
}

Collapse sidebar in SwiftUI (Xcode 12)

I'm trying to do a simple application in SwiftUI taking advantage of SwiftUI 2.0's new multiplatform project template, and I wish to add the option to collapse the sidebar as many other apps do.
I tried adding a boolean state variable that controls whether the sidebar should show or not, but that doesn't work because then the main view is turned translucent (I guess this is because macOS thinks the sidebar is now the main view).
Is there a way to natively achieve this in SwiftUI?
Please note I'm using macOS Big Sur, Xcode 12, and SwiftUI 2.0
Thanks in advance.
My code
ContentView.swift
import SwiftUI
struct ContentView: View {
#if os(iOS)
#Environment(\.horizontalSizeClass) private var horizontalSizeClass
#endif
#ViewBuilder var body: some View {
#if os(iOS)
if horizontalSizeClass == .compact {
TabController()
} else {
SidebarNavigation()
}
#else
SidebarNavigation()
.frame(minWidth: 900, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity)
#endif
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.sizeThatFits)
}
}
Sidebar.swift
import SwiftUI
struct SidebarNavigation: View {
enum HyperspaceViews {
case home
case localTimeline
case publicTimeline
case messages
case announcements
case community
case recommended
case profile
}
#State var selection: Set<HyperspaceViews> = [.home]
#State var searchText: String = ""
#State var showComposeTootView: Bool = false
#State var showNotifications: Bool = false
#State private var showCancelButton: Bool = false
var sidebar: some View {
VStack {
HStack {
TextField("Search...", text: $searchText)
.cornerRadius(4)
}
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
List(selection: self.$selection) {
Group {
NavigationLink(destination: Timeline().container.frame(maxWidth: .infinity, maxHeight: .infinity)) {
Label("Home", systemImage: "house")
}
.accessibility(label: Text("Home"))
.tag(HyperspaceViews.home)
NavigationLink(destination: Text("Local").frame(maxWidth: .infinity, maxHeight: .infinity)) {
Label("Local", systemImage: "person.2")
}
.accessibility(label: Text("Local"))
.tag(HyperspaceViews.localTimeline)
NavigationLink(destination: Text("Public").frame(maxWidth: .infinity, maxHeight: .infinity)) {
Label("Public", systemImage: "globe")
}
.accessibility(label: Text("Public"))
.tag(HyperspaceViews.localTimeline)
NavigationLink(destination: Text("Messages").frame(maxWidth: .infinity, maxHeight: .infinity)) {
Label("Messages", systemImage: "bubble.right")
}
.accessibility(label: Text("Message"))
.tag(HyperspaceViews.localTimeline)
Divider()
NavigationLink(destination: Text("Announcements").frame(maxWidth: .infinity, maxHeight: .infinity)) {
Label("Announcements", systemImage: "megaphone")
}
.accessibility(label: Text("Announcements"))
.tag(HyperspaceViews.announcements)
NavigationLink(destination: Text("Community").frame(maxWidth: .infinity, maxHeight: .infinity)) {
Label("Community", systemImage: "flame")
}
.accessibility(label: Text("Community"))
.tag(HyperspaceViews.community)
NavigationLink(destination: Text("Recommended").frame(maxWidth: .infinity, maxHeight: .infinity)) {
Label("Recommended", systemImage: "star")
}
.accessibility(label: Text("Community"))
.tag(HyperspaceViews.recommended)
Divider()
NavigationLink(destination: Text("Recommended").frame(maxWidth: .infinity, maxHeight: .infinity)) {
Label("hyperspacedev", systemImage: "tag")
}
.accessibility(label: Text("Community"))
.tag(HyperspaceViews.recommended)
}
}
.overlay(self.profileButton, alignment: .bottom)
.listStyle(SidebarListStyle())
}
}
var profileButton: some View {
VStack(alignment: .leading, spacing: 0) {
Divider()
NavigationLink(destination: ProfileView().container.frame(maxWidth: .infinity, maxHeight: .infinity)) {
HStack {
Image("amodrono")
.resizable()
.clipShape(Circle())
.frame(width: 25, height: 25)
Text("amodrono")
.font(.headline)
}
.contentShape(Rectangle())
}
.accessibility(label: Text("Your profile"))
.padding(.vertical, 8)
.padding(.horizontal, 16)
.buttonStyle(PlainButtonStyle())
}
.tag(HyperspaceViews.profile)
}
var body: some View {
NavigationView {
#if os(macOS)
sidebar.frame(minWidth: 100, idealWidth: 180, maxWidth: 200, maxHeight: .infinity)
#else
sidebar
#endif
Text("Content List")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.sheet(isPresented: self.$showComposeTootView) {
ComposeTootView(showComposeTootView: self.$showComposeTootView)
.frame(minWidth: 400, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity)
}
.toolbar {
ToolbarItem {
Button(action: {
self.showNotifications.toggle()
}) {
Image(systemName: "bell")
}
.popover(
isPresented: self.$showNotifications,
arrowEdge: .bottom
) {
LazyVStack {
ForEach(0 ..< 10 ) { i in
Label("#\(i) liked your post!", systemImage: "hand.thumbsup")
.padding()
Divider()
}
}
}
}
ToolbarItem {
Button(action: {
self.showComposeTootView.toggle()
}) {
Image(systemName: "square.and.pencil")
}
}
}
}
}
struct SidebarNavigation_Previews: PreviewProvider {
static var previews: some View {
SidebarNavigation()
}
}
This worked for me -
https://developer.apple.com/forums/thread/651807
struct SwiftUIView: View {
var body: some View {
NavigationView{
}.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: toggleSidebar, label: {
Image(systemName: "sidebar.left")
})
}
}
}
}
func toggleSidebar() {
NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
It shows in the views on iOS so you will need some conditions for macOS only.
Apple provide NSToolbarToggleSidebarItemIdentifier which perform the toggleSidebar(_:) method.
.onAppear {
NSApp.keyWindow?.toolbar?.insertItem(withItemIdentifier: .toggleSidebar, at: 0)
}
This way will appear a button in your toolbar which action will invoke the sidebar toggle and it works perfectly.