Change foreground color when list item is highlighted (using SwiftUI on macOS) - macos

In a macOS app I have a List, the listed views (named MyItem here) are using the .primary and .secondary colors but also other colors like .orange or .blue.
The important thing here is: .primary and .secondary colors are automatically converted to white when the item is highlighted or selected, but the other colors are not.
Naively, I added a selected: Bool property to MyItem and when the list reports it as selected I manually convert the colors to white, something like this:
struct MyItem: View {
var selected: Bool
var body: some View {
Text("Hello").foregroundColor(.primary)
Text("World").foregroundColor(selected ? .white : .orange)
}
}
struct MyList: View {
#State private var selectedItemId: Int?
var body: some View {
List(items, selection: $selectedItemId) { item in
MyItem(selected: selectedItemId == item.id)
}
}
}
It works great when using the keyboard, but once you start selecting items with the mouse, you see this workaround doesn't work with highlighted items. Here's an example where I start by using the keyboard and next I show how the colors are not changed when highlighting the items with the mouse: https://www.youtube.com/watch?v=X07mB0OsTQk
I've searched everywhere, from SwiftUI's documentation to Cocoa's, including AppKit's, I couldn't find an easy way to change the color when the item is highlighted… Am I missing something?

Related

SwiftUI Table rowHeight on macOS

Looking for some advice trying to use Table on macOS using SwiftUI. Table was introduced in macOS 12, and I'm trying my darnedest to not step down into AppKit or replicate any existing functionality - I can't seem to find a solution to a SwiftUI version of NSTableView's rowHeight property.
There is a .tableStyle modifier but only allows for customization of insets and alternating row styling. Modifying the frame in the row views doesn't take effect, at least not the ways I've tried.
First, am I missing something obvious (or not obvious) and there is a way to do this? The underlying AppKit view is a SwiftUITableView that seems to inherit to NSTableView. I can adjust the rowHieght in the debugger, but only effects the table view's background. Second any recommendations on the way to approach this - other than using NSTableView and wrapping in a NSViewRepresentable, or manipulating the established NSView hierarchy using some SwiftUI/AppKit trickery?
Some elided code demonstrating the use of Table
struct ContentTable: View {
var items: [ContentItem]
#State var selection = Set<ContentItem.ID>()
var body: some View {
Table(selection: $selection) {
TableColumn("Name") {
Text($0.name)
.frame(height: 80) // Only way found to set height information
}.width(min: 200, ideal: 250)
TableColumn("Description", value: \.description)
} rows: {
ForEach(items) {
TableRow($0)
}
}
.tableStyle(.inset(alternatesRowBackgrounds: true))
}
}
Here's a side-by-side of SF Symbols (Left) and the SwiftUI Table I'm using. Both are using the inset/alternating row styles. Presentation wise I'd like to give the rows more space to breathe.
The way I do this is by utilizing the padding modifier in the content of declared TableColumns.
The important part is the table will adjust the row by the lowest padding value across all columns.
I would not try to approach this with tableStyle. There are no public configurations as of now.
It's important to note an undeclared padding defaults to 0.
struct ContentTable: View {
var items: [ContentItem]
#State var selection = Set<ContentItem.ID>()
var body: some View {
Table(selection: $selection) {
TableColumn("Name") {
Text($0.name).padding(.vertical, 8) // <--- THIS WILL TAKE PRECEDENCE.
}.width(min: 200, ideal: 250)
TableColumn("Description") {
Text("\($0.description)").padding(.vertical, 16) // <--- THIS WILL BE INEFFECTIVE.
}
} rows: {
ForEach(items) {
TableRow($0)
}
}
.tableStyle(.inset(alternatesRowBackgrounds: true))
}
}

SwiftUI is there any way of getting a disabled TextField to be grayed out on Mac OS

When a SwitfUI TextField is disabled, on Mac OS, there is no visual feedback that the field is not enterable (apart from not accepting focus click). I have searched high and low, it looks like simply setting .background(Color.whatever) works for IOS (from all the "how tos" that I have encountered). However for a Mac OS app, it only changes the color of the thin boundary of the textfield. I have futzed around and found that I can add opaque overlays to simulate the effect, but that seems overly complex for what I always took to be conventional standard of greying out of disabled fields. Which makes me think that I am missing something bleedingly obvious somewhere.
Has anyone a sample of a MacOS SwiftUI struct that greys the background of a disabled TextField ? My minimal example of what I am doing to see the issue is below.
struct ContentView: View {
#State var nameEditDisabled = true
#State var myText = "Fred"
var body: some View {
VStack {
Button("Change Name") {
nameEditDisabled.toggle()
}
TextField("hello", text: $myText)
.background(nameEditDisabled ? Color.gray: Color.yellow)
.disabled(nameEditDisabled)
}
}
}
it seems to be "fixed' in swiftUI 3.0, macos 12. I get a slightly darker shade of gray when disabled. When in focus, I get a blue border.
Edit:
struct ContentView: View {
#State var nameEditDisabled = false
#State var myText = "Fred"
var body: some View {
VStack {
Button("Change disabling") {
nameEditDisabled.toggle()
}
TextField("hello", text: $myText)
.colorMultiply(nameEditDisabled ? .gray: .yellow)
.disabled(nameEditDisabled)
}.frame(width: 444, height: 444)
}
}

SwiftUI NSTitlebarAccessoryViewController

I'd like to use the SwiftUI app lifecycle, but my app uses NSTitlebarAccessoryViewController to show a bar of tool options below the toolbar:
Specifically, I'm doing this:
let toolSettingsView = NSHostingView(rootView: ToolAccessoryView(model: model))
let vc = NSTitlebarAccessoryViewController()
vc.view = toolSettingsView
vc.fullScreenMinHeight = accessoryHeight // Ensure tool settings are visible in full screen.
toolSettingsView.frame.size = toolSettingsView.fittingSize
window?.addTitlebarAccessoryViewController(vc)
Is there a (practical) way I can mimic the control appearance (of the sliders, etc.) using pure SwiftUI? When use a SwiftUI view I get this:
Code looks like this:
struct MainView: View {
var model: DataModel
var undoManager: UndoManager
var body: some View {
VStack {
ToolAccessoryView(model: model)
SculptingView(model: model, undoManager: undoManager)
}
}
}
This is implemented as of macOS 13 by using a custom toolbar placement identifier as follows:
extension ToolbarItemPlacement {
static let toolOptionsBar = ToolbarItemPlacement(id: "com.companyname.toolOptions")
}
Then specifying the placement in the .toolbar:
ToolbarItem(placement: .toolOptionsBar) {
ToolAccessoryView()
}
Which in my case looks like this:
The colors on the sliders are a bit odd, which is likely their bug.
See also https://developer.apple.com/documentation/swiftui/toolbarplacement/init(id:) which has some example code.

Simple SwiftUI transition that doesn't seem to be working

I've been trying to get a comprehensive understanding of how Animations and Transitions work in SwiftUI.
I've been experimenting with different transitions and animations all day but one transition I want isn't working. I'll first show the code and then explain what sort of transition I want.
struct Test: View {
#State private var pressed = false // Controls whether the tower is shown or not.
var body: some View {
VStack {
Button(pressed ? "Press me to hide tower" : "Press me to show tower") { // Controls truth value of the "pressed" variable above.
withAnimation(.easeInOut(duration: 5)) { // I've set the duration to 5 because I want to see the animation in slow-motion.
self.pressed.toggle() // Toggles truth value of "pressed" from true to false or vice-versa.
}
}
if pressed { // Displays the Tower when "pressed" is true.
Tower() // Tower struct is provided below.
}
}
}
}
And this is the Tower struct:
struct Tower: View {
var body: some View {
VStack {
Text("Level 3").transition(.move(edge: .leading))
Text("Level 2")
Text("Level 1").transition(.move(edge: .trailing))
}
}
}
The transition I want to achieve is pretty straightforward - I want Level 3 to fly in from the left, Level 1 to fly in from the right, and Level 2 to just fade in and out. With this code however, Levels 1, 2 and 3, all just fade in and out together. The .move(edge: .trailing) transition seems to not work for some reason.
The catch is that I definitely want the Tower struct and the Test struct to be separate at all times. (I don't want to copy-paste any of the code that's within the Tower struct inside of the Test struct)
If you can show me how I can make the upper and lower levels fly in from different sides please let me know (if you can provide a code sample as well it'll help a ton).
Transition is an engine to present/remove a view in/from view hierarchy (with animation if animation is specified). It is applied to view as a whole, directly, and is not passed-into view's subviews. So if you try to add view into view hierarchy that does not have own transition it just appears, immediately, if there is animation then by default fade-in/out transition is applied (again, to view as a whole).
But you want to transition view's internals from outside. So here is possible solution.
Tested with Xcode 11.4 / iOS 13.4 (you can play with animations by yourself)
struct Test: View {
#State private var pressed = false
var body: some View {
VStack {
Button(pressed ? "Press me to hide tower" : "Press me to show tower") {
self.pressed.toggle()
}
Tower(show: $pressed)
}.animation(.easeInOut)
}
}
struct Tower: View {
#Binding var show: Bool
var body: some View {
VStack {
if show {
Text("Level 3").transition(.move(edge: .leading))
Text("Level 2")
Text("Level 1").transition(.move(edge: .trailing))
}
}
.animation(.easeInOut)
}
}

Animating Text in SwiftUI

SwiftUI has wonderful animation features, but the way it handles changes in Text View content is problematic. It animates the change of the text frame but changes the text immediately without animation. As a result, when the content of a Text View is made longer, animating the transition causes an ellipsis (…) to appear until the text frame reaches its full width. For example, in this little app, pressing the Toggle button switches between shorter and longer text:
Here's the code:
import SwiftUI
struct ContentView: View {
#State var shortString = true
var body: some View {
VStack {
Text(shortString ? "This is short." : "This is considerably longer.").font(.title)
.animation(.easeInOut(duration:1.0))
Button(action: {self.shortString.toggle()}) {
Text("Toggle").padding()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The question is: how to avoid the ellipsis? When animating a one character string into a two character string the situation is even worse, because the short string is completely replaced by the ellipsis while it animates into the longer string.
One possibility is to assign a separate id to the view in one state or another by adding the modifier, for instance, .id(self.shortString ? 0 : 1) and then adding a .transition() modifier. That will treat the Text as two different Views, before and after. Unfortunately, in my case I need to move text location during the change, and different ids makes animating that impossible.
I guess the solution is a creative use of AnimatableData. Any ideas?
Here is a demo of possible approach (scratchy - you can redesign it to extension, modifier, or separate view)
Tested with Xcode 11.4 / iOS 13.4
struct ContentView: View {
#State var shortString = true
var body: some View {
VStack {
if shortString {
Text("This is short.").font(.title).fixedSize()
.transition(AnyTransition.opacity.animation(.easeInOut(duration:1.0)))
}
if !shortString {
Text("This is considerably longer.").font(.title).fixedSize()
.transition(AnyTransition.opacity.animation(.easeInOut(duration:1.0)))
}
Button(action: {self.shortString.toggle()}) {
Text("Toggle").padding()
}
}
}
}
Any suggestions for shrinking an animated gif's dimensions?
I use this way:
- decrease zoom of Preview to 75% (or resize window of Simulator)
- use QuickTimePlayer region-based Screen Recording
- use https://ezgif.com/video-to-gif for converting to GIF
If you add .animation(nil) to the Text object definition then the contents will change directly between values, avoiding ellipsis.
However, this may prevent the animation of the text location, which you also mention wanting to do simultaneously.
You can add one by one character into a string with animation after 0.1 seconds additional, but remember to disable the button toggle while the characters being added, like below:
Code:
public struct TextAnimation: View {
public init(){ }
#State var text: String = ""
#State var toggle = false
public var body: some View {
VStack{
Text(text).animation(.spring())
HStack {
Button {
toggle.toggle()
} label: {
Text("Toggle")
}
}.padding()
}.onChange(of: toggle) { toggle in
if toggle {
text = ""
"This is considerably longer.".enumerated().forEach { index, character in
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) {
text += String(character)
}
}
} else {
text = "This is short."
}
}
}
}

Resources