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")
}
}
}
}
Related
I’m trying to build a custom sidebar menu that animates out when a button in it is tapped. For debugging purposes the animation is deliberately slow at 2.0 seconds. As you can see the animation does not work properly:
I suspect there are two parts to this problem:
The background of the newly selected button is moving out faster than the menu. I think this is rooted in the default system animation of Button.
When I replace Button with Text and use an .onTapGesture, there is still the fading animation, so I assume there is something structurally wrong in the way I’m setting selected in FeatureButton.
Sorry the example code is a bit long, tried to simplify my app architecture as much as possible. The reason for using MenuState as an EnvironmentObject is to be able to the change its properties from various places throughout the app.
Here’s the code:
class MenuState: ObservableObject {
#Published var currentFeature: Feature = .featureA
#Published var menuOffset: CGFloat = 0
}
enum Feature: String, CaseIterable {
case featureA = "Feature A"
case featureB = "Feature B"
case featureC = "Feature C"
}
extension Feature: Identifiable {
var id: RawValue { rawValue }
}
struct ContentView: View {
#StateObject var menuState = MenuState()
var body: some View {
ZStack(alignment: .leading) {
content
menu
}
.environmentObject(menuState)
.animation(.easeOut(duration: 2.0), value: menuState.menuOffset)
}
var menu: some View {
VStack {
ForEach(Feature.allCases) { feature in
FeatureButton(feature: feature)
}
}
.frame(maxHeight: .infinity)
.frame(width: 200)
.background(.thinMaterial)
.offset(x: menuState.menuOffset)
}
var content: some View {
VStack {
Button("Show Menu") {
menuState.menuOffset = 0
}
Text(menuState.currentFeature.rawValue)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
struct FeatureButton: View {
#EnvironmentObject var menuState: MenuState
let feature: Feature
var selected: Bool {
return menuState.currentFeature == feature
}
var body: some View {
Button(feature.rawValue) {
menuState.currentFeature = feature
menuState.menuOffset = -200
}
.buttonStyle(FeatureButtonStyle(selected: selected))
}
}
struct FeatureButtonStyle: ButtonStyle {
#EnvironmentObject var menuState: MenuState
var selected: Bool
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(maxWidth: .infinity, minHeight: 44)
.foregroundColor(selected ? .blue : .primary)
.background(Color.gray.opacity(selected ? 0.4 : 0))
.contentShape(Rectangle())
}
}
EDIT:
For some reason making the animation explicit solves the issue, see answer below.
The problem can be solved by making the animation explicit instead of using the .animation modifier:
Button(feature.rawValue) {
menuState.currentFeature = feature
withAnimation(.easeOut) {
menuState.menuOffset = -200
}
}
.buttonStyle(FeatureButtonStyle(selected: selected))
I don't understand why it only works like this though.
I'm assuming I should probably file this as a feedback report with Apple, but posting here in case I am missing something - or if there is new guidance with latest SwiftUI.
This code works as expected in Xcode 13, but in Xcode 14 beta 2, the navigation bar and "Cancel" button are missing. Is this ProgressView with deferred content loading somehow a technique that doesn't work anymore?
import SwiftUI
struct ContentView: View {
#State private var isFlowDetermined = false
var body: some View {
NavigationView {
//NestedView()
if self.isFlowDetermined {
NestedView()
} else {
ProgressView()
.task {
await self.determineFlow()
}
}
}
}
private func determineFlow() async {
self.isFlowDetermined = true
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct NestedView: View {
var body: some View {
ScrollView {
Text("Where is the \"Cancel\" button?")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
#if !os(macOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
#if !os(macOS)
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
print("got here")
}
}
#endif
}
}
}
UPDATE: Xcode 14 beta 4 appears to resolve this issue. The suggested workaround below is no longer needed.
It seems they optimised toolbar construction (and don't assume it is changed). Anyway I see only one workaround for now:
NavigationView {
// .. content
}
.id(isFlowDetermined) // << here !!
Tested with Xcode 14b2 / iOS 16
*Note: NavigationView is deprecated since iOS 16
I want to make a list view which contents could be dragged for reordering and tapped for detail.
But when I add the onTapGesture() action in the content view, the onMove() action stop working.
Example code:
import SwiftUI
struct TestTapAndMove: View {
#State private var users = ["Paul", "Taylor", "Adele"]
var body: some View {
List {
ForEach(users, id: \.self) { user in
VStack {
Text("user: \(user)").font(.headline)
Divider()
}.background(Color.gray)
// The TapGesture will result in onMove action not working.
.onTapGesture(perform: {
print("tap!")
})
}
.onMove(perform: move)
}
}
func move(from source: IndexSet, to destination: Int) {
print("onMove!")
users.move(fromOffsets: source, toOffset: destination)
}
}
Is there any solution that could make the tap and move action work together?
As workaround you can make some specific part of row clickable, like text in below example (or some added custom shape or plain button)
VStack {
Text("user: \(user)").font(.headline)
.onTapGesture(perform: {
print("tap!")
})
Divider()
}.background(Color.gray)
Alternate: you can add helper custom view as row overlay which would handle tap/mouseDown action for view (and does not break drug)
class TapHandlerView: NSView {
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event) // keep this to allow drug !!
print("tap")
}
}
struct TapHandler: NSViewRepresentable {
func makeNSView(context: Context) -> TapHandlerView {
TapHandlerView()
}
func updateNSView(_ nsView: TapHandlerView, context: Context) {
}
}
and use it
VStack {
Text("user: \(user)").font(.headline)
Divider()
}.background(Color.gray)
.overlay(TapHandler()) // << here !!
Tested with Xcode 12.0 / macOS 10.15.6
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?
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?