How to close a sheet using button shortcuts - macos

I'm presenting a sheet to let the user enter a new message on a macOS app. I have a cancel and a save buttons, and I have assigned the .cancelAction and another shortcut to them. The idea is that if the user presses ESC, then the sheet closes without saving, and if the user presses CMD+Return then it will save it.
When the sheet is displaying, I can't get those button shortcuts to work.
The sheet code is as follow:
struct ComposeMessage: View {
#Binding var showComposeMessage: Bool
#State var text: String = ""
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
HStack {
TextEditor(text: $text).font(.body)
.disableAutocorrection(false)
Spacer()
}
.padding()
Divider()
HStack {
// cancel button
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark.circle")
}
.help("Cancel")
.buttonStyle(.plain)
.keyboardShortcut(.cancelAction)
Spacer()
// save button
Button {
if text == "" {return} // prevent saving empty message
print("Save message")
//showComposeMessage = false <- I tried using this too
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "checkmark.circle")
}
.keyboardShortcut(.return, modifiers: [.command])
.help("Save (⌘ Return)")
.buttonStyle(.plain)
}
.font(.title2)
.padding()
}
.frame(width: 400, height: 256)
}
}
I'm calling it with:
sheet(isPresented: $showComposeMessage) {
ComposeMessage(showComposeWindow: $showComposeWindow).background(Color(NSColor.textBackgroundColor))
}
If I use .popover instead, they do work. What am I missing?
Thanks for the help!

The answer is to use:
.buttonStyle(.borderless)

Related

SwiftUI - MacOS - TextField within Alert not receiving focus

I've added a TextField within an Alert in MacOS. However, the TextField doesn't receive focus, when the alert shows. Here's the code I've tested with. Is this even possible? If this is a bug, then please suggest other workarounds.
import SwiftUI
struct ContentView: View {
#State private var presented = false
#State private var username = ""
#FocusState private var focused: Bool
var body: some View {
VStack {
Button(action: {
focused = true
presented = true
}, label: {
Text("Click to show Alert")
})
}
.alert("", isPresented: $presented, actions: {
VStack {
TextField("User name (email address)", text: $username)
.focusable()
.focused($focused)
Button(action: {}, label: { Text("OK") })
}
.onAppear() {
// Try setting focus with a delay.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
focused = true
})
}
}, message: {
Text("The textField in this alert doesn't get the focus. Therefore, a manual click is needed to set focus and start typing.")
})
}
}
I can confirm that focus doesn't work inside Alert. A possible workaround is using a custom Sheet:
struct ContentView: View {
#State private var presented = false
#State private var username = ""
#FocusState private var focused: Bool
var body: some View {
VStack {
Button(action: {
presented = true
focused = true
}, label: {
Text("Click to show Sheet")
})
}
.sheet(isPresented: $presented, content: {
VStack {
// App Icon placeholder
RoundedRectangle(cornerRadius: 8)
.fill(.secondary)
.frame(width: 48, height: 48)
.padding(12)
Text("The textField in this alert doesn't get the focus. Therefore, a manual click is needed to set focus and start typing.")
.font(.caption)
.multilineTextAlignment(.center)
TextField("User name (email address)", text: $username)
.focused($focused)
.padding(.vertical)
Button(action: {
presented = false
}, label: {
Text("OK")
.frame(maxWidth: .infinity)
})
.buttonStyle(.borderedProminent)
}
.padding()
.frame(width: 260)
})
}
}

macOS: How to prevent that showing an alert dismisses the Popover it comes from

In a macOS App I use .popovers to show and edit cell details. If the user deletes specific content I want to show a warning .alert. But showing the alert always dismisses the Popover it originated from. How can I prevent that?
This should work as Apple is using it e.g. in Calendar, when you delete an attachment in a calendar entry.
Here is a simple demo code showing the issue:
struct ContentView: View {
#State private var showPopover = false
#State private var showAlert = false
var body: some View {
VStack {
Button("Show popover") { showPopover = true }
.popover(isPresented: $showPopover, arrowEdge: .leading) {
VStack {
Text("Popover")
Button("Delete someting") { showAlert = true}
}
.padding()
.frame(minWidth: 100, minHeight: 100)
}
.alert("Really delete?", isPresented: $showAlert) { }
// ^ dismisses the popover immediately
}
.frame(minWidth: 600, minHeight: 400)
}
}

Focus on a TextField using a keyboard shortcut

I have a macOS Monterrey app that has a TextField on the toolbar. I use this to search for text on my app. Now, I'm trying to add a keyboard shortcut to focus on the TextField. I've tried the code below, adding button with a shortcut as a way to test whether this is doable, but I can't get it to work. The .focused() doesn't do anything.
Beyond that, I have added a new menu item Find and set the keyboard shortcut to cmd-L but I don't know either how to send the focus to the TextField.
What am I missing?
struct AllData: View {
#FocusState private var searchFieldIsFocused: Bool
#State var searchText: String = ""
var body: some View {
NavigationView {
List(data.notes.filter { searchText.isEmpty ? true : $0.text.localizedCaseInsensitiveContains(searchText) }) {
//...
}
}
.navigationTitle("A Title")
.toolbar {
ToolbarItem(placement: .automatic) {
TextField("Search...", text: $searchText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(minWidth: 200)
.focused($searchFieldIsFocused)
}
//test for the focus
ToolbarItem(placement: .automatic) {
Button(action: {
print("Plus pressed")
searchFieldIsFocused = true
}) {
Image(systemName: "plus")
}
.keyboardShortcut("e", modifiers: [.command])
}
}
}
}
Edit after Yrb comments
It seems .focusable will not work with a TextField on a toolbar, so he suggested using .searchable.
Trying with .searchable
struct AllData: View {
#FocusState private var searchFieldIsFocused: Bool
#State var searchText: String = ""
var body: some View {
NavigationView {
List(data.notes.filter { searchText.isEmpty ? true : $0.text.localizedCaseInsensitiveContains(searchText) }) {
//...
}
.searchable(
text: $searchText,
placement: .toolbar,
prompt: "Search..."
)
}
.navigationTitle("A Title")
.toolbar {
//test for the focus
ToolbarItem(placement: .automatic) {
Button(action: {
print("Plus pressed")
searchFieldIsFocused = true
}) {
Image(systemName: "plus")
}
.keyboardShortcut("e", modifiers: [.command])
}
}
}
}
Observations:
I have no control where the search field will appear, it seems to be the last item on the toolbar, which is not what I want, but OK.
There is no way that I can find to add a .focusable so I can jump to the search with a keyboard shortcut, which is the reason for this question
When you search, the list get's filtered, but when you try to navigate the list with the arrows (keyboard), upon selecting the next item, the focus returns to the search field, it doesn't remain on the list, making navigation very slow and painful.
I'm sure I'm missing something, and probably my code is wrong. Any clues?
Edit #2
It seems this is not possible with a .searchable:
.searchable modifier with a keyboard shortcut
I ended up getting the following to work on macOS using the .searchable() modifier (only tested on macOS 12.3):
import SwiftUI
#main
struct MyApp: App {
#State var searchText = ""
var items = ["Item 1", "Item 2", "Item 3"]
var searchResults: [String] {
if searchText.isEmpty {
return items
} else {
return items.filter { $0.contains(searchText) }
}
}
var body: some Scene {
WindowGroup {
List(self.searchResults, id: \.self) { item in
Text(item)
}.searchable(text: self.$searchText)
}.commands {
CommandMenu("Find") {
Button("Find") {
if let toolbar = NSApp.keyWindow?.toolbar,
let search = toolbar.items.first(where: { $0.itemIdentifier.rawValue == "com.apple.SwiftUI.search" }) as? NSSearchToolbarItem {
search.beginSearchInteraction()
}
}.keyboardShortcut("f", modifiers: .command)
}
}
}
}

Trying to move to a new view in swiftUI after a button press

I am trying to create an application which has the ability to create and view assignments. I have a page called AddAssignment.swift and ViewAssignment.swift, and I am trying to move from the AddAssignment page to the ViewAssignment page on a button press.
The user should enter the details in the text boxes, and then when the button is pressed, I want to save the information in the text box and move to the View Assignments screen. So far, I am not able to get the button to work correctly.
Here is my code:
import SwiftUI
struct AddAssignment: View {
// Properties
#State var taskName = ""
#State var dueDate = ""
#State var subject = ""
#State var weighting = ""
#State var totalMarks = ""
// Methods
func saveTask(a:String,b:String,c:String,d:String,e:String) -> [String] {
var newTask: [String] = []
newTask.append(a);
newTask.append(b);
newTask.append(c);
newTask.append(d);
newTask.append(e);
return newTask
}
// Main UI
var body: some View {
VStack {
Text("Add New Task")
.font(.headline)
.padding()
.scaleEffect(/*#START_MENU_TOKEN#*/2.0/*#END_MENU_TOKEN#*/)
Spacer()
Image(systemName: "plus.app.fill")
.foregroundColor(Color.orange)
.scaleEffect(/*#START_MENU_TOKEN#*/5.0/*#END_MENU_TOKEN#*/)
Spacer()
Group { // Text Fields
TextField("Enter assignment name", text: $taskName)
.padding([.top, .leading, .bottom])
TextField("Enter due date", text: $dueDate)
.padding([.top, .leading, .bottom])
TextField("Enter subject", text: $subject)
.padding([.top, .leading, .bottom])
TextField("Enter percent weighting", text: $weighting)
.padding([.top, .leading, .bottom])
TextField("Enter total marks", text: $totalMarks)
.padding([.top, .leading, .bottom])
}
Spacer()
Button("Create New Task", action: {
let task: [String] = [taskName, dueDate, subject, weighting, totalMarks]
print(task)
ViewAssignment()
})
Spacer()
}
}
}
struct AddAssignment_Previews: PreviewProvider {
static var previews: some View {
AddAssignment()
}
}
The console is able to return the values of the textbooks and store this information, I'm just not sure how to bring that across to another struct in the ViewAssignment.swift file.
to make the "move work", add this to "AddAssignment":
struct AddAssignment: View {
#State var toAssignment: Int? = nil
replace your Button("Create New Task", action: {...}, with
NavigationLink(destination: ViewAssignment(), tag: 1, selection: $toAssignment) {
Button("Create New Task") {
let task: [String] = [taskName, dueDate, subject, weighting, totalMarks]
print(task)
self.toAssignment = 1
}
}
make sure you wrap the whole thing in "NavigationView { ...}"

SwiftUI macOS sheet does not dismiss sometimes

I have implemented a sheet to edit the values of a client.
It's normally possible to edit the client and close the sheet after pressing the OK-Button. But if the sheet is open for a longer time it is not possible to dismiss the sheet. Nothing happens and they only way to proceed is to quit the program.
Does anyone have an idea why this happens sometimes?
struct ContentView: View {
#State private var showingEditClient = false
var body: some View {
VStack{
HStack {
Button(action: showEditClientSheet) {
Text("Edit Client")
}
.sheet(isPresented: $showingEditClient) {
EditClientSheet()
}
}
}
.frame(minWidth: 400, minHeight: 400)
}
func showEditClientSheet(){
showingEditClient.toggle()
}
}
struct EditClientSheet: View {
#Environment(\.presentationMode) var presentationMode
#State private var name = "Max"
var body: some View {
VStack {
Form {
TextField("Name", text: $name)
}
HStack{
Button(action: cancel) {
Text("Abbrechen")
}
Button(action: editClient) {
Text("Ok")
}
}
}
.frame(minWidth: 200, minHeight: 200)
}
func editClient() {
NSApp.keyWindow?.makeFirstResponder(nil)
//Check if content is correct to save
if name != "" {
//store the changes
self.presentationMode.wrappedValue.dismiss()
}else {
//show Alert
}
}
func cancel() {
self.presentationMode.wrappedValue.dismiss()
}
}

Resources