MacOS: Commit TextField when clicked outside - swift

From a UX point, the goal is to commit the input when people click anywhere outside the TextField.
For example, if the input is for renaming an item, then when clicked outside, we store the input as the new item name, and replace the TextField with a Text to show the new item name.
I assume it is an expected standard behavior, but please let me know if it is against Apple's MacOS standards.
Is there a standard / conventional way to achieve it with SwiftUI, or with some AppKit workaround?
As I see, onCommit is only triggered when we hit the Return key, not when we click outside the TextField. So what I think what I need to figure out is how to detect clicking outside.
What I considered:
Some built-in view modifier on the TextField which activates this behavior, but I couldn't find any.
Detect focus loss or editingChanged of the TextField, but by default, when we click on the background / a button / a text, the TextField doesn't lose focus, nor is the editingChanged event triggered.
If the TextField is focused, add an overlay on the ContentView with an onTapGesture modifier, but then it would take two taps to trigger a button when a TextField is focused.
Add an onTapGesture modifier on the ContentView, which calls a focusOut method, but it doesn't receive the event when people tap on a child view that also has an onTapGesture on it.
Improving on 4, also call focusOut from the onTapGesture callback of all child views. So far this is the only viable option I see, but I'm not sure it is a good pattern to put extra code in all onTapGestures, just in order to customize TextField behavior.
Example code:
import SwiftUI
#main
struct app: App {
#State private var inputText: String = ""
var body: some Scene {
WindowGroup {
ZStack {
Color.secondary.onTapGesture { print("Background tapped") }
VStack {
Text("Some label")
Button("Some button") { print("Button clicked") }
TextField(
"Item rename input",
text: $inputText,
onCommit: { print("Item rename commit") }
)
}
}
}
}
}

Related

SwiftUI Add TextField to Menu

I have a menu and want to be able to input some data from the menu. This is what I've tried and it shows up in the menu, but doesn't allow any input. Is there a way to hack some input in SwiftUI Menus?
Menu("Award Users") {
TextField(awardedAmount, text: $awardedAmount)
Button("Send") {
}
}
SwiftUI will simplify the layout of the menu items, and if not possible, it may discard some of your items.
Not all views are suitable to work as a menu item and they will be silently ignored. These are some of the views that work: Menu, Text, Button, Link, Label, Divider or Image.
SwiftUI Menu gives us a dedicated view for showing popup button with the help of menus. Menu option is used to create variety of buttons to control what you want to appear in the menu. If you add a textfield, Menu View will consider it as button title and disable its action as shown in image below.
struct ContentView: View {
#State private var awardedAmount = "125"
var body: some View {
VStack{
Menu("Options") {
TextField(awardedAmount, text: $awardedAmount)
Button("Order Now", action: placeOrder)
Button("Adjust Order", action: adjustOrder)
Button("Cancel", action: cancelOrder)
}
}
}
func placeOrder() { }
func adjustOrder() { }
func cancelOrder() { }
}
So in short Menu is a dedicated view for buttons for selecting an option, not taking user inputs.

How do you change a Text field .onTapGesture (Swift5, Xcode12.4)?

Seemingly simple but I can't seem to find any results on this.
I have this code
struct ContentView: View {
var MinimalTimer: MinimalistTimer = MinimalistTimer()
var body: some View {
Text("\(MinimalTimer.fourteenMinutes)")
.onTapGesture {
MinimalTimer.decrementTimer()
}
}
}
and I want to tap the timer and display on the text the new time, e.g. 840 -> 839 -> 838.
How do you change the display text on tap?
Timer example
I dont know whether this will work in swiftUI though but try , MinimalTimer.addGestureRecognizer(<#T##gestureRecognizer: UIGestureRecognizer##UIGestureRecognizer#>) there is method something like this , you'll get it with autoComplete
Look for gesture Recogniser

SwiftUI onMoveCommand actions aren't executed

I'm making a macOS app with SwiftUI, and I would like to offset a SwiftUI view using the arrow keys on the built-in keyboard.
I couldn't find many resources online, but onMoveCommand() appears to be the event handler I need. Upon trying it out, I discovered that the action I specified for onMoveCommand() does not appear to be executed. Here's some code I wrote just to test it out:
struct ContentView: View {
var body: some View {
Text("Hello")
.onAppear() {
print("Appeared!")
}
.onMoveCommand() { (direction) in
print("Moved!")
}
.onTapGesture() {
print("Tapped!")
}
}
}
onMoveCommand() does not print "Moved!" when I press the arrow keys, instead I get the error alert sound played, and nothing is printed. onAppear() successfully prints the "Appeared!" message when the view appears, and onTapGesture() prints "Tapped!" correctly whenever I click the text. This seems to tell me that the basic syntax I got for these view events is correct, but I implemented onMoveCommand() incorrectly.
For now I only want my app to print something to the Xcode console when the arrow keys are pressed, and to be able to distinguish which arrow key was pressed. Can someone please explain what I did wrong?
Keyboard events are handled only by view in focus, so fix is
var body: some View {
Text("Hello")
.focusable() // << here !!
.onAppear() {
print("Appeared!")
}
.onMoveCommand() { (direction) in
print("Moved!")
}
.onTapGesture() {
print("Tapped!")
}
}
Tested with Xcode 11.4 / macOS 10.15.4. Make sure you have turned on keyboard navigation in System Preferences.

How to get accentColor background for menu items in SwiftUI with reduced transparency?

Using AppKit you can add NSMenuItems to an NSMenu. I saw that there is something similar to NSMenu in SwiftUI, namely MenuButton. But I cannot find any documentation on how it works.
I tried the following:
MenuButton("+") {
Button("New contact") { print("Create new contact") }
Button("New group") { print("Create new group") }
}
And that gives me this
It looks almost OK but when I enable the "Reduce transparency" in system preferences
The buttons have a different background color than the menu (notice the slightly lighter color above and beneath the menu items).
When I hover the menu items, their background color doesn't change like a normal macOS menu. See the image below:
I also tried to change the background color manually using the .background() modifier but that doesn't affect the full width of the menu item.
MenuButton("+") {
Button("New contact") { print("Create new contact") }
.background(Color.accentColor)
Button("New group") { print("Create new group") }
}
I suppose this is because I am placing Buttons inside the MenuButton while it is probably expecting some other SwiftUI element. What elements should I place inside MenuButtons to create a normal looking macOS menu like the one below?
[Update] macOS Big Sur
I also tried this out in Big Sur. While the background renders correctly, in Big Sur, the text color is messed up now. 🤯
I think I found a partial solution to this by configuring a ButtonStyle and applying it to the MenuButton generic structure. Note however that the conditional change of .foregroundColor isn't inherited by the individual's Button()'s Text(). Also the color ain't right.
Perhaps someone wants to improve on this.
struct DetectHover: ButtonStyle {
#State private var hovering: Bool = false
public func makeBody(configuration: DetectHover.Configuration) -> some View {
configuration.label
.foregroundColor(self.hovering ? Color.white : Color.primary)
.background(self.hovering ? Color.blue : Color.clear)
.onHover { hover in
self.hovering = hover
}
}
}
MenuButton(label: Image(nsImage: NSImage(named: NSImage.actionTemplateName)!)) {
// Buttons
}.buttonStyle(DetectHover())
I’ve got something that nearly looks right. I have created a custom button which does change appearance when hovered.
I am very new to both Swift and SwiftUI, so the following may be clumsy. I would welcome any improvements.
struct HoverButton: View {
var text = "Hover Button"
var action = {}
#State private var hovering = false
var body: some View {
Button(text, action: action )
.padding(.horizontal, 6)
.padding(.vertical, 2)
.buttonStyle(PlainButtonStyle())
.frame(minWidth: 0, maxWidth: .infinity)
.background(self.hovering ? Color(.selectedMenuItemColor) : Color(.clear))
.onHover { hover in
self.hovering = hover
}
}
}
// Usage:
MenuButton("Test") {
HoverButton(text: "Apple", action: {
print("Apple")
})
HoverButton(text: "Banana", action: {
print("Banana")
})
}
.font(.system(size: 14, /* weight: .heavy, */ design: .default))
.menuButtonStyle(BorderlessButtonMenuButtonStyle())
The whole point in creating the custom button is to have access to Self so that I can change its appearance. However, there are some serious shortcomings:
The main problem is that the buttons don’t take the full width of the menu body. I have no idea how to fix that.
The menu buttons are centered. I tried using a HStack with a Spacer() but that didn’t help.
I have no idea how to omit the first parameter name as for a real Button

SwiftUI - Making a View focusable and trigger an action on macOS

How is it possible to make a View on macOS focusable. I tried the it like below but the action is never triggered. I know that for NSView you have to implement acceptsFirstResponder to return true but can not find similar in SwiftUI.
Is this still a Beta related bug or a missing functionality for macOS ?
struct FocusableView: View {
var body: some View {
Group {
Text("Hello World!")
.padding(20)
.background(Color.blue.cornerRadius(8))
.focusable(true) { isFocused in
print("Focused", isFocused)
}
}
}
}
I think I found a work around for the problem to shift the focus to a SwiftUI view.
I am working on macOS Catalina, 10.15, Swift 5, Xcode 11.1
The problem is to shift the focus to a SwiftUI view. I could imagine that this has not been perfectly implemented, yet.
Only upon shifting the focus to the required SwiftUI view within the SwiftUI framework the onFocusChange closure of a focusable view will be called and the view will be focused.
My intention was: when I click into the SwiftUI view I want to execute the onFocusChange closure and I want it to be focused (for subsequent paste commands)
My SwiftUI view is built into the Window using an NSHostingView subclass - ImageHostingView.
When I add this ImageHostingView to the view hierarchy I define its previous key view:
theImageHostingView.superview.nextKeyView = theImageHostingView
I added a mouseDown handler to the ImageHostingView:
override dynamic func mouseDown(with event: NSEvent) {
if let theWindow = self.window, let theView = self.previousValidKeyView {
theWindow.selectKeyView(following:theView)
}
}
Calling the NSWindow's selectKeyView(following:) triggers within the SwiftUI framework a focus change to the desired view (the root view of ImageHostingView).
This is clearly a work-around and works only for final views which will be represented by a NSHostingView. But it highlights the problem (shifting the focus to a SwiftUI view upon a certain action) and it might be helpful in many cases.
The only way I've been able to get this to work is by checking the "Use keyboard navigation to move focus between controls" checkbox in Keyboard System Preferences