Setting TextField focus in macOS SwiftUI - macos

I’ve built a simple macOS modal dialog in SwiftUI that takes some text from the user:
This is presented via Storyboard Segue from a menu item to an NSWindowController that contains an NSHostingController:
class
OpenLocationController: NSHostingController<OpenLocationView>
{
#objc
required
dynamic
init?(coder: NSCoder)
{
let view = OpenLocationView()
super.init(coder: coder, rootView: view)
}
}
struct
OpenLocationView : View
{
#State private var location: String = ""
var body: some View
{
VStack
{
HStack
{
Text("Location:")
TextField("https://", text: $location) { self.openLocation() }
}
HStack
{
Spacer()
Button("Cancel") { self.dismiss() }
Button("Open") { self.simulateClick() }
}
}
.padding()
.frame(minWidth: 500.0)
}
}
Screenshot of the Storyboard:
I’d like to automatically focus the text field and select all the text in it when the dialog is displayed. I’d also like the Tab key to focus to it (for some reason, that doesn't work either, although that would be moot if I could just focus it on display). How would I do this in SwiftUI?

Related

How to move focus to view which is not TextField

I have a MacOS app which has a lot of TextFields in many views; and one editor view which has to receive pressed keyboard shortcut, when cursor is above. But as I try, I cannot focus on a view which is not text enabled. I made a small app to show a problem:
#main
struct TestFocusApp: App {
var body: some Scene {
DocumentGroup(newDocument: TestFocusDocument()) { file in
ContentView(document: file.$document)
}
.commands {
CommandGroup(replacing: CommandGroupPlacement.textEditing) {
Button("Delete") {
deleteSelectedObject.send()
}
.keyboardShortcut(.delete, modifiers: [])
}
}
}
}
let deleteSelectedObject = PassthroughSubject<Void, Never>()
struct MysticView: View {
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.gray.opacity(0.3))
}.focusable()
.onReceive(deleteSelectedObject) { _ in
print ("received")
}
}
}
enum FocusableField {
case wordTwo
case view
case editor
case wordOne
}
struct ContentView: View {
#Binding var document: TestFocusDocument
#State var wordOne: String = ""
#State var wordTwo: String = ""
#FocusState var focus: FocusableField?
var body: some View {
VStack {
TextField("one", text: $wordOne)
.focused($focus, equals: .wordOne)
TextEditor(text: $document.text)
.focused($focus, equals: .editor)
///I want to receive DELETE in any way, in a MystickView or unfocus All another text views in App to not delete their contents
MysticView()
.focusable(true)
.focused($focus, equals: .view)
.onHover { inside in
focus = inside ? .view : nil
/// focus became ALWAYS nil, even set to `.view` here
}
.onTapGesture {
focus = .view
}
TextField("two", text: $wordTwo)
.focused($focus, equals: .wordTwo)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(document: .constant(TestFocusDocument()))
}
}
Only first TextField became focused when I click or hover over MysticView
I can assign nil to focus, but it will not unfocus fields from outside this one view.
Is it a bug, or I missed something? How to make View focusable? To Unfocus all textFields?

Is there a reliable workaround for onDisappear() not working within .sheet() or .popover() in SwiftUI on macOS?

I'm building an app that shares quite a bit of SwiftUI code between its iOS and macOS targets. On iOS, onDisappear seems to work reliably on Views. However, on macOS, onDisappear doesn't get called if the View is inside a sheet or popover.
The following code illustrates the concept:
import SwiftUI
struct ContentView: View {
#State private var textShown = true
#State private var showSheet = false
#State private var showPopover = false
var body: some View {
VStack {
Button("Toggle text") {
self.textShown.toggle()
}
if textShown {
Text("Text").onDisappear {
print("Text disappearing")
}
}
Button("Toggle sheet") {
self.showSheet.toggle()
}.sheet(isPresented: $showSheet, onDismiss: {
print("On dismiss")
}) {
VStack {
Button("Close sheet") {
self.showSheet = false
}
}.onDisappear {
print("Sheet disappearing")
}
}
Button("Toggle popover") {
self.showPopover.toggle()
}.popover(isPresented: $showPopover) {
VStack {
Text("popover")
}.onDisappear {
print("Popover disappearing")
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Note that onDisappear works fine on the Text component at the beginning of the VStack but the other two onDisappear calls don't get executed on macOS.
One workaround I've found is to attach an ObservableObject to the View and use deinit to call cleanup code. However, this isn't a great solution for two reasons:
1) With the popover example, there's a significant delay between the dismissal of the popover and the deist call (although it works quickly on sheets)
2) I haven't had any crashes on macOS with this approach, but on iOS, deinit have been unreliable in SwiftUI doing anything but trivial code -- holding references to my data store, app state, etc. have had crashes.
Here's the basic approach I used for the deinit strategy:
class DeinitObject : ObservableObject {
deinit {
print("Deinit obj")
}
}
struct ViewWithObservableObject : View {
#ObservedObject private var deinitObj = DeinitObject()
var body: some View {
Text("Deinit view")
}
}
Also, I would have thought I could use the onDismiss parameter of the sheet call, but that doesn't get called either on macOS. And, it's not an available parameter of popover.
All of this is using Xcode 11.4.1 and macOS 10.15.3.
Any solutions for good workarounds?

How can I display Touch Bar Buttons using SwiftUI?

I'm trying to add Touch Bar support for a SwiftUI View. There seems to be SwiftUI API for this using the .touchBar(content: () -> View) function on Views, but documentation is non existent and I can't get my Touch Bar to display anything.
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.touchBar {
Button(action: {
}) {
Text("do something")
}
}
}
}
This code does compile and run, but the Touch Bar remains empty. How can I get my touch bar to display content using SwiftUI (not catalyst)?
Using .focusable doesn't work without "Use keyboard navigation to move focus between controls" checked in System Preferences -> Keyboard -> Shortcuts. To work around that, I did this:
/// Bit of a hack to enable touch bar support.
class FocusNSView: NSView {
override var acceptsFirstResponder: Bool {
return true
}
}
/// Gets the keyboard focus if nothing else is focused.
struct FocusView: NSViewRepresentable {
func makeNSView(context: NSViewRepresentableContext<FocusView>) -> FocusNSView {
return FocusNSView()
}
func updateNSView(_ nsView: FocusNSView, context: Context) {
// Delay making the view the first responder to avoid SwiftUI errors.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
if let window = nsView.window {
// Only set the focus if nothing else is focused.
if let _ = window.firstResponder as? NSWindow {
window.makeFirstResponder(nsView)
}
}
}
}
}
Help from this How to use a SwiftUI touchbar with a NSWindow - Apple Developer Forums:
Use the focusable() modifier
The touch bar shows the text when you add the .focusable() modifier just before the .touchBar(content:) modifier.
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.focusable()
.touchBar {
Button(action: {
print("Perform some action")
}) {
Text("do something")
}
}
}
}

SwiftUI: Remove 'Focus Ring' Highlight Border from macOS TextField

I used the below code to create a custom search bar in SwiftUI. It works great on iOS / Catalyst:
...but when running natively on macOS, the 'focus ring' highlighted border styling (when the user selects the text field) rather ruins the effect:
Using .textFieldStyle(PlainTextFieldStyle()) has removed most of the default styling from the underlying field (which I believe is an NSTextField), but not the focus ring.
Is there a way to remove this too? I tried creating a custom TextFieldStyle and applying that, but couldn't find any modifier to style that border.
public struct SearchTextView: View {
#Binding var searchText: String
#if !os(macOS)
private let backgroundColor = Color(UIColor.secondarySystemBackground)
#else
private let backgroundColor = Color(NSColor.controlBackgroundColor)
#endif
public var body: some View {
HStack {
Spacer()
#if !os(macOS)
Image(systemName: "magnifyingglass")
#else
Image("icons.general.magnifyingGlass")
#endif
TextField("Search", text: self.$searchText)
.textFieldStyle(PlainTextFieldStyle())
.foregroundColor(.primary)
.padding(8)
Spacer()
}
.foregroundColor(.secondary)
.background(backgroundColor)
.cornerRadius(12)
.padding()
}
public init(searchText: Binding<String>) {
self._searchText = searchText
}
}
As stated in an answer by Asperi to a similar question here, it's not (yet) possible to turn off the focus ring for a specific field using SwiftUI; however, the following workaround will disable the focus ring for all NSTextField instances in the app:
extension NSTextField {
open override var focusRingType: NSFocusRingType {
get { .none }
set { }
}
}
If you want to replace this with your own custom focus ring within the view, the onEditingChanged parameter can help you achieve this (see below example); however, it's unfortunately called on macOS when the user types the first letter, not when they first click on the field (which isn't ideal).
In theory, you could use the onFocusChange closure in the focusable modifier instead, but that doesn't appear to get called for these macOS text fields currently (as of macOS 10.15.3).
public struct SearchTextView: View {
#Binding var searchText: String
#State private var hasFocus = false
#if !os(macOS)
private var backgroundColor = Color(UIColor.secondarySystemBackground)
#else
private var backgroundColor = Color(NSColor.controlBackgroundColor)
#endif
public var body: some View {
HStack {
Spacer()
#if !os(macOS)
Image(systemName: "magnifyingglass")
#else
Image("icons.general.magnifyingGlass")
#endif
TextField("Search", text: self.$searchText, onEditingChanged: { currentlyEditing in
self.hasFocus = currentlyEditing // If the editing state has changed to be currently edited, update the view's state
})
.textFieldStyle(PlainTextFieldStyle())
.foregroundColor(.primary)
.padding(8)
Spacer()
}
.foregroundColor(.secondary)
.background(backgroundColor)
.cornerRadius(12)
.border(self.hasFocus ? Color.accentColor : Color.clear, width: self.hasFocus ? 3 : 0)
.padding()
}
public init(searchText: Binding<String>) {
self._searchText = searchText
}
}

Select SwiftUI cell in Popover

I am trying to make a popover in SwiftUI using a UIHostingController with a list that can be tapped. First, the user name and password should be filled in, and then the user role should be tapped in the list, and the popover should be dismissed when the save button is tapped.
Also, the save button in the navigation bar should be disabled until the user information has been verified.
The Xcode playground for this can be fetched from my GitHub repository https://github.com/imyrvold/Popover
To be able to use the AddUserView as a rootView in UIHostingController, I had to use an Xcode storyboard, and add it to the Resources in the Xcode Playground.
import SwiftUI
import Combine
public struct AddUserView : View {
#ObjectBinding public var loginInfo: LoginInfo
#EnvironmentObject var viewModel: RoleViewModel
#State var selectedRole: Role? = nil
#Environment(\.isPresented) var isPresented: Binding<Bool>?
public var body: some View {
NavigationView {
VStack {
TextField(self.$loginInfo.firstName, placeholder: Text("First Name"))
TextField(self.$loginInfo.lastName, placeholder: Text("Last Name"))
TextField(self.$loginInfo.email, placeholder: Text("Email"))
SecureField(self.$loginInfo.password, placeholder: Text("Password"))
Divider()
List(self.viewModel.roles) { role in
RoleCell(role: role).tapAction {
self.selectedRole = role
}
}
}
.padding()
.navigationBarTitle(Text("Add User"))
.navigationBarItems(trailing:
Button(action: {
self.saveAction()
self.isPresented?.value = false
}) {
Text("Save")
})//.disabled(!self.loginInfo.isValid)
}
}
// MARK:- Action methods
func saveAction() {
}
}
The first problem I have is that when I uncomment the disabled(!self.loginInfo.isValid), all the TextField's are also disabled. Not sure if that is a bug in SwiftUI?
I also want to have the rolecell set the checkmark on the cell when tapped, but so far I have been unable to figure out how to do that.
And how can I dismiss the Popover when the save button is tapped?
(When running the playground, have to click the start playground a second time to run properly, the first time the Save popover doesn't work).
Have you tried this
.navigationBarItems(trailing:
Button(action: {
self.saveAction()
self.isPresented?.value = false
}) {
Text("Save")
}.disabled(!self.loginInfo.isValid))

Resources