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

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)
}
}

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)
})
}
}

How to close a sheet using button shortcuts

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)

Managing Focus in an Autosuggestion Field in SwiftUI in a MacOS App

I want to build an "autosuggestion field" in SwiftUI in a MacOS App.
My first attempts work quite OK.
The problem I face is managing the focus, to enable smooth keyboard (only) handling.
If the users has entered more than 1 char in the textfield, a list with suggestions is displayed. At the moment the user can choose one with the mouse.
What I want is, that the user can still edit the textfield, navigate in the textfield with Cursor-Left and Cursor-Right (which is the case) and additionally navigate the list with Cursor-Up and Cursor-Down and select an entry with e.g. Space or Enter.
struct TestSearchField2: View {
var body: some View {
VStack{
SearchField2()
} .frame(width: 400, height: 400, alignment: .center)
}
}
enum SuggestionListStatus {
case off
case on
case accepted
}
struct SearchField2: View {
let allSuggestions = ["michael Schneider","thomas mueller","joachim Oster","Dennis mueller","Michael Carter","frank 1","frank 2","frank 3","frank 4","frank 5"]
#State var input = ""
#State var showList : SuggestionListStatus = .off
#State var selected = ""
var body: some View {
TextField("input Data", text: $input)
.cornerRadius(5.0)
.overlay(SuggestionList2(suggestionListStatus: $showList, selected: $input, suggestions: allSuggestions.filter {$0.contains(input)}))
.onChange(of: input, perform: { value in
if showList == .accepted {
showList = .off}
else if input.count >= 2 {
print(
allSuggestions.filter {$0.contains(input)})
showList = .on
} else {
showList = .off
}
})
}
}
struct SuggestionList2: View {
#Binding var suggestionListStatus : SuggestionListStatus
#Binding var selected : String
#State var selection : String? = "Michael"
var suggestions : [String]
var body: some View {
if suggestionListStatus == .on {
VStack{
List(suggestions, id: \.self, selection: $selection){ s in
Text(s).onTapGesture {
print("Pressed; \(s)")
selected = s
suggestionListStatus = .accepted
}
}
Text("Debug setected: \(selection ?? "nothing selected")").font(.footnote)
}
.frame(width: 200, height: 150, alignment: .leading)
.offset(x: 0, y: 100)
}
}
}
I wrapped a NSTextField in NSViewRepresentable and used the NSTextFieldDelegate. You can find my example of GitHub.
SuggestionsDemo Project on GitHub

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()
}
}

macOS SwiftUI Navigation for a Single View

I'm attempting to create a settings view for my macOS SwiftUI status bar app. My implementation so far has been using a NavigationView, and NavigationLink, but this solution produces a half view as the settings view pushes the parent view to the side. Screenshot and code example below.
Navigation Sidebar
struct ContentView: View {
var body: some View {
VStack{
NavigationView{
NavigationLink(destination: SecondView()){
Text("Go to next view")
}}
}.frame(width: 800, height: 600, alignment: .center)}
}
struct SecondView: View {
var body: some View {
VStack{
Text("This is the second view")
}.frame(width: 800, height: 600, alignment: .center)
}
}
The little information I can find suggests that this is unavoidable using SwiftUI on macOS, because the 'full screen' NavigationView on iOS (StackNavigationViewStyle) is not available on macOS.
Is there a simple or even complex way of implementing a transition to a settings view that takes up the whole frame in SwiftUI for macOS? And if not, is it possible to use AppKit to call a View object written in SwiftUI?
Also a Swift newbie - please be gentle.
Here is a simple demo of possible approach for custom navigation-like solution. Tested with Xcode 11.4 / macOS 10.15.4
Note: background colors are used for better visibility.
struct ContentView: View {
#State private var show = false
var body: some View {
VStack{
if !show {
RootView(show: $show)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
.transition(AnyTransition.move(edge: .leading)).animation(.default)
}
if show {
NextView(show: $show)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
.transition(AnyTransition.move(edge: .trailing)).animation(.default)
}
}
}
}
struct RootView: View {
#Binding var show: Bool
var body: some View {
VStack{
Button("Next") { self.show = true }
Text("This is the first view")
}
}
}
struct NextView: View {
#Binding var show: Bool
var body: some View {
VStack{
Button("Back") { self.show = false }
Text("This is the second view")
}
}
}
I've expanded upon Asperi's great suggestion and created a generic, reusable StackNavigationView for macOS (or even iOS, if you want). Some highlights:
It supports any number of subviews (in any layout).
It automatically adds a 'Back' button for each subview (just text for now, but you can swap in an icon if using macOS 11+).
Swift v5.2:
struct StackNavigationView<RootContent, SubviewContent>: View where RootContent: View, SubviewContent: View {
#Binding var currentSubviewIndex: Int
#Binding var showingSubview: Bool
let subviewByIndex: (Int) -> SubviewContent
let rootView: () -> RootContent
var body: some View {
VStack {
VStack{
if !showingSubview { // Root view
rootView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(AnyTransition.move(edge: .leading)).animation(.default)
}
if showingSubview { // Correct subview for current index
StackNavigationSubview(isVisible: self.$showingSubview) {
self.subviewByIndex(self.currentSubviewIndex)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(AnyTransition.move(edge: .trailing)).animation(.default)
}
}
}
}
init(currentSubviewIndex: Binding<Int>, showingSubview: Binding<Bool>, #ViewBuilder subviewByIndex: #escaping (Int) -> SubviewContent, #ViewBuilder rootView: #escaping () -> RootContent) {
self._currentSubviewIndex = currentSubviewIndex
self._showingSubview = showingSubview
self.subviewByIndex = subviewByIndex
self.rootView = rootView
}
private struct StackNavigationSubview<Content>: View where Content: View {
#Binding var isVisible: Bool
let contentView: () -> Content
var body: some View {
VStack {
HStack { // Back button
Button(action: {
self.isVisible = false
}) {
Text("< Back")
}.buttonStyle(BorderlessButtonStyle())
Spacer()
}
.padding(.horizontal).padding(.vertical, 4)
contentView() // Main view content
}
}
}
}
More info on #ViewBuilder and generics used can be found here.
Here's a basic example of it in use. The parent view tracks current selection and display status (using #State), allowing anything inside its subviews to trigger state changes.
struct ExampleView: View {
#State private var currentSubviewIndex = 0
#State private var showingSubview = false
var body: some View {
StackNavigationView(
currentSubviewIndex: self.$currentSubviewIndex,
showingSubview: self.$showingSubview,
subviewByIndex: { index in
self.subView(forIndex: index)
}
) {
VStack {
Button(action: { self.showSubview(withIndex: 0) }) {
Text("Show View 1")
}
Button(action: { self.showSubview(withIndex: 1) }) {
Text("Show View 2")
}
Button(action: { self.showSubview(withIndex: 2) }) {
Text("Show View 3")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
}
}
private func subView(forIndex index: Int) -> AnyView {
switch index {
case 0: return AnyView(Text("I'm View One").frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.green))
case 1: return AnyView(Text("I'm View Two").frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.yellow))
case 2: return AnyView(VStack {
Text("And I'm...")
Text("View Three")
}.frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.orange))
default: return AnyView(Text("Inavlid Selection").frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.red))
}
}
private func showSubview(withIndex index: Int) {
currentSubviewIndex = index
showingSubview = true
}
}
Note: Generics like this require all subviews to be of the same type. If that's not so, you can wrap them in AnyView, like I've done here. The AnyView wrapper isn't required if you're using a consistent type for all subviews (the root view’s type doesn’t need to match).
Heyo, so a problem I had is that I wanted to have multiple navigationView-layers, I'm not sure if that's also your attempt, but if it is: MacOS DOES NOT inherit the NavigationView.
Meaning, you need to provide your DetailView (or SecondView in your case) with it's own NavigationView. So, just embedding like [...], destination: NavigationView { SecondView() }) [...] should do the trick.
But, careful! Doing the same for iOS targets will result in unexpected behaviour. So, if you target both make sure you use #if os(macOS)!
However, when making a settings view, I'd recommend you also look into the Settings Scene provided by Apple.
Seems this didn't get fixed in Xcode 13.
Tested on Xcode 13 Big Sur, not on Monterrey though...
You can get full screen navigation with
.navigationViewStyle(StackNavigationViewStyle())

Resources