SwiftUI List on macOS: highlighting content in the selected row? - swift

In a SwiftUI List, when a row is selected, a blue selection is drawn in the selection, and foreground Text with the default primary color is automatically made white. For other views with custom colors, I'd like to be able to make sure they become tinted white to match the system apps. For example, see the blue dot on the selected row:
Example code:
List(selection: $selection) {
ForEach(0..<4) { index in
HStack {
Image(systemName: "circle.fill")
.foregroundColor(.blue)
Text("Test")
}
.tag(index)
}
}
Any tips on how to achieve the correct result here? 🙏
With NSTableCellView, there is the backgroundStyle property, but I couldn't find anything like this.
I've searched all of the available environment variables, and couldn't find anything appropriate.
I have also tried manually including an isSelected binding on a view for each row, but the selection binding is not updated by List until mouse-up, while the highlight is updated on mouse-down and drag, so this results in a flickery appearance and is not right either.
I'm also looking to customize not just a single SF Symbol image, but also a RoundedRectangle that is drawn with a custom foreground color that I want to become white upon selection.

Approach
Use a Label for the cell
Code
struct ContentView: View {
#State private var selection: Int?
var body: some View {
List(selection: $selection) {
ForEach(0..<4) { index in
Label("Test", systemImage: "circle.fill")
.tag(index)
}
}
}
}
Screenshot

Related

Applying padding to SwiftUI Picker with menu style is causing multiline text

I have a Picker with menu style presenting a label with an icon and text. When applying a padding to any views containing the picker, the picker will behave as though there is not enough space and run to multiple lines even though there is enough space.
Sample Code
struct ContentView: View {
let options = [
"Some Long Text"
]
private var selectedOption: Binding<String> =
.constant("Some Long Text")
var body: some View {
HStack {
Text("Some Text")
Picker("Work Type", selection: selectedOption) {
ForEach(self.options, id: \.self) {
Label($0, systemImage: "pencil.tip.crop.circle")
}
}.pickerStyle(.menu)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
VStack {
ContentView()
ContentView()
.padding()
}
}
}
This might be a layout bug in SwiftUI. There is clearly enough space to put it all in one line, even with the padding. You may want to file a bug report to Apple.
However, you can easily fix this by applying the fixedSize() modifier to your Picker:
Picker("Work Type", selection: selectedOption) {
// ...
}
.pickerStyle(.menu)
.fixedSize(horizontal: true, vertical: false)
This will make sure that the Picker doesn't concern itself with external sizing requirements, ignores the proposed with and just uses renders with its ideal size.
Please note that this might lead to unintended behavior. For example, when the user chooses a bigger font size in accessibility settings, the text in the Picker will never break and might be rendered off-screen. So make sure to think through all your use cases.
PS: You can also use a Menu instead of a Picker with .menu style. Of course, you won't get the selection behavior for free and have to do it manually, but it doesn't have this layout bug.
This seems like intended behavior to me. Because of the padding applied around the HStack (specifically at the horizontal edges) there is not enough space to render the form label on one line. If you were to change the size of the screen (for example using an iPad simulator or using landscape mode) the label would have enough room and be displayed on one line. I tested this code on an iPad simulator and it worked as expected.
If you want to force the label text to be rendered on one line, use fixedSize(horizontal:vertical:). Though this can lead to unexpected behavior.
Picker("Work Type", selection: selectedOption) {
ForEach(self.options, id: \.self) {
Label($0, systemImage: "pencil.tip.crop.circle")
}
}
.pickerStyle(.menu)
.fixedSize(horizontal: true, vertical: false)

SwiftUI Navigate up down not working in Grid

Here is my simple tvOS 16.0 app with the following code:
import SwiftUI
struct ContentView: View {
var sixColumnGrid: [GridItem] = Array(repeating: .init(.flexible()), count: 6)
let colors: [Color] = [.red, .green, .blue, .yellow, .cyan, .pink, .gray, .indigo, .brown]
var body: some View {
NavigationStack {
LazyVGrid(columns: sixColumnGrid) {
ForEach(colors, id: \.self) { color in
NavigationLink {
//MyView()
} label: {
Text(color.description.capitalized)
.padding()
.background(color)
}
.padding(20)
.buttonStyle(.card)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The code is pretty simple where I have a LazyVGrid with six column. The grid is populated with 9 different colors. Each item of the grid is a NavigationLink
This is how it renders:
The issue I am facing is that one can navigate with up and down keys between two rows there only if there is any item directly below or above the source item from where we are pressing the up/down key.
For instance, in the above code example, if we press up key when we are on Brown color it takes us to Blue color, and if we press down key when we are on Blue color, we can navigate to Brown Color.
But the issue is that when you are on Yellow color, you can't go down to the bottom row via Down key, you need to go left first, towards the Blue color and then you can go down to the second row via Down key.
Can you share how we can make navigation to go up/down even if the element is not directly above or down below that same item in the grid.
My goal is to have multiple grids like that in my view but should be able to go up and down between those grids even if the item is not directly the above item from where we are pressing up/down keys.
Is there any limitation with NavigationStack/LazyVGrid/NavigationLink that one can navigate with up/down keys only if any item is directly present in down/up direction of that specific item?
How can we navigate to different sections of the pages via up/down keys if there is such limitation? It is not practical to have all the items aligned vertically on a page to make them navigate through up and down keys.

SwiftUI: Editable TextFields in Lists

*** NOTE: This question concerns macOS, not iOS ***
Context:
I have a list of items that serves as a "Master" view in a standard Master-Detail arrangement. You select an item from the list, and the detail view updates:
In SwiftUI, this is powered by a List like this:
struct RuleListView: View
{
#State var rules: [Rule]
#State var selectedRuleUUID: UUID?
var body: some View
{
List(rules, id: \.uuid, selection: $selectedRuleUUID) { rule in
RuleListRow(rule: rule, isSelected: (selectedRuleUUID == rule.uuid))
}
}
}
The Problem:
The name of each item in the list is user-editable. In AppKit, when using NSTableView or NSOutlineView for the list, the first click on a row selects that row and the second click would then begin editing the NSTextField that contains the name.
In SwiftUI, things have apparently gotten dumber. Clicking on ANY text in the list immediately begins editing that text, which makes selecting a row VERY difficult—you have to click on the 2 pixels above or below the TextField in each row.
To combat this, I've stashed a clear Button() on top of every row that's not selected. This button intercepts the click before it reaches the TextField() and selects the row. When the row is selected, the Button() is no longer included and subsequent clicks then select the TextField():
struct RuleListRow: View
{
var rule: Rule
var isSelected: Bool
var body: some View
{
ZStack
{
Label {
TextField("", text: $rule.name)
.labelsHidden()
} icon: {
Image(systemName: "lasso.sparkles")
}
if !isSelected
{
Button {
// No-op
} label: {
EmptyView()
}
.buttonStyle(.plain)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.clear)
.contentShape(Rectangle())
}
}
}
}
Question:
The above works, but...this can't be correct, right? I've missed something basic that makes List behave properly when it contains TextField rows, right? There's some magic viewModifier I'm supposed to stick somewhere, I'm sure.
What is the correct, canonical way to solve this issue using Swift 5.5 and targeting macOS 11.0+?
Note:
My first approach was to simply disable the TextField when the row isn't selected, but that caused the text to appear "dimmed" and I couldn't find a way to override the text color when the TextField is disabled.

Why does SwiftUI View background extend into safe area?

Here's a view that navigates to a 2nd view:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
SecondView()
} label: {
Text("Go to 2nd view")
}
}
}
}
struct SecondView: View {
var body: some View {
ZStack {
Color.red
VStack {
Text("This is a test")
.background(.green)
//Spacer() // <--If you add this, it pushes the Text to the top, but its background no longer respects the safe area--why is this?
}
}
}
}
On the 2nd screen, the green background of the Text view only extends to its border, which makes sense because the Text is a pull-in view:
Now, if you add Spacer() after the Text(), the Text view pushes to the top of the VStack, which makes sense, but why does its green background suddenly push into the safe area?
In the Apple documentation here, it says:
By default, SwiftUI sizes and positions views to avoid system defined
safe areas to ensure that system content or the edges of the device
won’t obstruct your views. If your design calls for the background to
extend to the screen edges, use the ignoresSafeArea(_:edges:) modifier
to override the default.
However, in this case, I'm not using ignoresSafeArea, so why is it acting as if I did, rather than perform the default like Apple says, which is to avoid the safe areas?
You think that you use old background modifier version.
But actually, the above code uses a new one, introduced in iOS 15 version of background modifier which by default ignores all safe area edges:
func background<S>(_ style: S, ignoresSafeAreaEdges edges: Edge.Set = .all) -> some View where S : ShapeStyle
To fix the issue just replace .background(.green) with .background(Color.green) if your app deployment target < iOS 15.0, otherwise use a new modifier: .background { Color.green }.

SwiftUI Label text and image vertically misaligned

I'm using SwiftUI's brand new Label View, running Xcode 12 beta on Big Sur.
As image I use SF Symbol and found an image named "play". But I've noticed the same problem with custom images without any bordering pixels (i.e. spacing is not caused by the image), e.g. PDF icons, so it is probably not related to the image.
In demos by Apple the Text and the image should just automatically align properly, but I do not see that.
struct ContentView: View {
var body: some View {
Label("Play", systemImage: "play")
}
}
Results in this:
Any ideas why the image (icon) and the text is vertically misaligned?
If we give the Button a background color we see more precisely the misalignment:
Label("Play", systemImage: "play")
.background(Color.red)
Results in this:
Probably a bug, so worth submitting feedback to Apple. Meanwhile here is working solution based on custom label style.
Tested with Xcode 12b
struct CenteredLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.icon
configuration.title
}
}
}
struct TestLabelMisalignment: View {
var body: some View {
Label("Play", systemImage: "play")
.labelStyle(CenteredLabelStyle())
}
}
#Sajjon You can add a custom View as a workaround and use Image with Text inside a HStack