Resize cursors are missing in `NSCursor` - swift

I'm writing a resizable view in SwiftUI on macOS. Drag its edge can change its size. I'd like to show according cursor when dragging different edges. So I looked up Apple NSCursor documentation, however, I cannot find resize cursors like those generally used in system as shown below.
How can I get those cursors? I attached my code below in which drag the Divider() to resize the cyan rectangle. Hovering on that Divider() will change the cursor.
struct ContentView: View {
#AppStorage("InspectorHeight") var inspectorHeight = 200.0
var body: some View {
ZStack(alignment: .top) {
Color.cyan
.frame(width: 100)
.frame(height: CGFloat(inspectorHeight))
Divider()
.padding(.vertical, 2)
.onHover { inside in
if inside {
NSCursor.resizeUpDown.push()
} else {
NSCursor.pop()
}
}
.gesture(
DragGesture()
.onChanged { drag in
inspectorHeight = max(100, inspectorHeight - Double(drag.translation.height))
}
)
}
.frame(height: 600, alignment: .bottom)
.padding()
}
}

The system cursors you're looking for don't exist, so you'll have to create custom ones, which you should be able to approximate using SFSymbols, e.g.:
NSCursor(image: NSImage(systemSymbolName: "arrow.up.and.down", accessibilityDescription: nil)!, hotSpot: NSPoint(x: 8, y: 8)).push()

Related

How can make transition stay in own zIndex until ending the transition in SwiftUI?

I have code like this in below:
struct ContentView: View {
#State private var show: Bool = false
var body: some View {
ZStack {
Button("show") { show.toggle() }.foregroundColor(.black)
//.zIndex(1)
if (show) {
Circle()
.fill(.blue)
.frame(width: 100, height: 100)
.transition(AnyTransition.asymmetric(insertion: .offset(x: 0, y: 300), removal: .offset(x: 0, y: 300)))
.onTapGesture { show.toggle() }
//.zIndex(2)
}
}
.frame(width: 300, height: 300)
.padding()
.animation(.linear(duration: 1.5), value: show)
}
}
The issue with this code is that in insertion view stays in correct zIndex(in Top layer) but in removal goes to wrong zIndex(in Bottom layer), I can correct this issue with using direct zIndex but the goal of this question is to find a way without using zIndex modifier, I thing it is possible but not sure how, maybe it has something to do with transition.
This behavior seems like a bug.
I think this is a side effect of the fact that as soon as you remove the Circle() View, it is immediately gone, and the animation happens after the fact. So upon removal, there is instantly just one item in the ZStack and it is on top.
A workaround is to not completely remove a view from the ZStack. This can be accomplished by wrapping the if show { } with an HStack, VStack, or ZStack:
struct ContentView: View {
#State private var show: Bool = false
var body: some View {
ZStack {
Button("show") { show.toggle() }.foregroundColor(.black)
//.zIndex(1)
HStack { // Note: VStack and ZStack also work
if (show) {
Circle()
.fill(.blue)
.frame(width: 100, height: 100)
.transition(AnyTransition.asymmetric(insertion: .offset(x: 0, y: 300), removal: .offset(x: 0, y: 300)))
.onTapGesture { show.toggle() }
//.zIndex(2)
}
}
}
.frame(width: 300, height: 300)
.padding()
.animation(.linear(duration: 1.5), value: show)
}
}
In this case, while the Circle() View is immediately removed, the HStack to which it belongs remains, and it remains in the same position in the ZStack thus fixing the animation.
That said, I'm not sure why adding explicit .zIndex() modifiers helps.
Note: Wrapping if show() { } in a Group does not help because Group is not a View.

SwiftUI layout in VStack where one child is offered a maxHeight but can use less than that height

I'm trying to build a layout inside a VStack that contains two children. The first child should take up all available space unused by the second child. The second child has a preferred size based on its own contents. I'd like to limit the height of the second child to a maximum height, but it should be able to take less than the maximum (when its own contents cannot make use of all the height). This should all be responsive to the root view size, which is the parent of the VStack (because the device can rotate).
My attempt uses the .frame(maxHeight: n) modifier, which seems to unconditionally takes up the entire n points of height, even when the view being modified doesn't use it. This results in whitespace rendered above and below the VStack's second child. This problem is shown in the Portrait preview below - the hasIdealSizeView only has a height of 57.6pts, but the frame that wraps that view has a height of 75pts.
import SwiftUI
struct StackWithOneLimitedHeightChild: View {
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
fullyExpandingView
hasIdealSizeView
.frame(maxHeight: geometry.size.height / 4)
}
}
}
var fullyExpandingView: some View {
Rectangle()
.fill(Color.blue)
}
var hasIdealSizeView: some View {
HStack {
Rectangle()
.aspectRatio(5/3, contentMode: .fit)
Rectangle()
.aspectRatio(5/3, contentMode: .fit)
}
// the following modifier just prints out the resulting height of this view in the layout
.overlay(alignment: .center) {
GeometryReader { geometry in
Text("Height: \(geometry.size.height)")
.font(.system(size: 12.0))
.foregroundColor(.red)
}
}
}
}
struct StackWithOneLimitedHeightChild_Previews: PreviewProvider {
static var previews: some View {
Group {
StackWithOneLimitedHeightChild()
.previewDisplayName("Portrait")
.previewLayout(PreviewLayout.fixed(width: 200, height: 300))
StackWithOneLimitedHeightChild()
.previewDisplayName("Landscape")
.previewLayout(PreviewLayout.fixed(width: 300, height: 180))
}
}
}
This observed result is consistent with how the .frame(maxHeight: n) modifier is described in the docs and online blog posts (the flow chart here is extremely helpful). Nonetheless, I can't seem to find another way to build this type of layout.
Related question: what are the expected use cases for .frame(maxHeight: n)? It seems to do the opposite of what I'd expect by unconditionally wrapping the view in a frame that is at least n points in height. It seems no different than .frame(height: n), using an explicit value for the offered height.
The behavior of .minHeight in this example is strange and far from intuitive. But I found a solution using a slightly different route:
This defines the minHeight for the expanding view (to get the desired layout in portrait mode), but adds a .layoutPriority to the second, making it define itself first and then give the remaining space to the upper view.
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
fullyExpandingView
.frame(minHeight: geometry.size.height / 4 * 3)
hasIdealSizeView
.layoutPriority(1)
}
}
}
There's probably a really short way to go about this but in the meantime here is what I did.
Firstly I created a struct for your hasIdealSizeView and I made it return a GeometryProxy, and with that i could return the height of the HStack, in this case, the same height you were printing on to the Text View. then with that I used the return proxy to check if the height is greater than the maximum, and if it is, i set it to the maximum, otherwise, set the height to nil, which basically allows the native SwiftUI flexible height:
//
// ContentView.swift
// Test
//
// Created by Denzel Anderson on 3/16/22.
//
import SwiftUI
struct StackWithOneLimitedHeightChild: View {
#State var viewHeight: CGFloat = 0
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
fullyExpandingView
.overlay(Text("\(viewHeight)"))
// GeometryReader { geo in
hasIdealSizeView { proxy in
viewHeight = proxy.size.height
}
.frame(height: viewHeight > geometry.size.height / 4 ? geometry.size.height / 4:nil)
}
.background(Color.green)
}
}
var fullyExpandingView: some View {
Rectangle()
.fill(Color.blue)
}
}
struct hasIdealSizeView: View {
var height: (GeometryProxy)->()
var body: some View {
HStack {
Rectangle()
.fill(.white)
.aspectRatio(5/3, contentMode: .fit)
Rectangle()
.fill(.white)
.aspectRatio(5/3, contentMode: .fit)
}
// the following modifier just prints out the resulting height of this view in the layout
.overlay(alignment: .center) {
GeometryReader { geometry in
Text("Height: \(geometry.size.height)")
.font(.system(size: 12.0))
.foregroundColor(.red)
.onAppear {
height(geometry)
}
}
}
}
}

ScrollViewReader doesn't scroll to the correct position in a 2D ScrollView

Here is a simple SwiftUI code that builds a large grid which is put inside a 2D ScrollView. Clicking each item will force the ScrollView to scroll to another item whose id is 50 more than the clicked item's. The ScrollView did scroll but it never scrolled to the correct one. I tried it on macOS and iOS, and neither worked. I also tried using VStack and HStack instead of LazyVGrid, and still it didn't work. What is the correct way to achieve this?
struct ContentView: View {
let columns = Array.init(repeating: GridItem(.fixed(80)), count: 20)
var body: some View {
ScrollView([.horizontal, .vertical]) {
ScrollViewReader { proxy in
LazyVGrid(columns: columns, spacing: 20) {
ForEach(0..<500) { i in
ZStack {
Circle()
.fill(Color.green)
.frame(width: 30, height: 30)
Text("\(i)")
}
.id("\(i)")
.onTapGesture {
proxy.scrollTo("\(i + 50)")
}
}
}
.padding(.horizontal)
}
}
.frame(minWidth: 500, minHeight: 300)
}
}

SwiftUI Center Content alignment without supporting frames

I'm having trouble trying to center a single element to emulate the navigation modal with a close button.
I would like to center content without using a supporting Rectangle on the sides or spacers.
What i'm trying to achieve is whenever the text grow, if it reaches the left sides where there is the close xmark button it should try to push itself on the right where there is available space until it reaches the right border and after wrap itself if there are no available space on the both sides.
here are some pictures:
expected result 1
expected result 2
current solution short text
current solution long text
i tried using long and short text to test the content behaviour
Currently this is the start of the code and basically i would like to avoid to add the blue rectangle (that would be usually clear)
struct TestAlignmentSwiftUIView: View {
var body: some View {
HStack(spacing: 0) {
Rectangle().fill(Color.blue).frame(width: 44, height: 44)
Text("aaa eee aaa")
.background(Color.red)
.padding(5)
Button(action: {}, label: {
Image(systemName: "xmark")
.padding(15)
.frame(width: 44, height: 44)
.background(Color.yellow)
})
}
.background(Color.green)
}
}
What i've tried so far but doesn't resolve the issue if the code inside the text component grow:
Using a zstack where i place the text and the close button one on
top of each other but the button is pushed to the side using a spacer. It will work for small text or content but is not scalable if the text grows
var body: some View {
ZStack {
HStack {
Spacer()
Button(action: {}, label: {
Image(systemName: "xmark")
.padding(15)
.frame(width: 44, height: 44)
.background(Color.yellow)
})
}
Text("aaa eee aaa random long very long text that should wrap without overlapping. long text")
.background(Color.red)
.frame(maxWidth: .infinity, alignment: .center)
.padding(5)
.opacity(0.7)
}
.background(Color.green)
}
Using alignment guides :
i would create my own center alignment guide, then use this custom alignment on a vstack where i place my content plus a fake filler rectangle that should center the elements on the content side.
the problem is that with swiftui , as far i know, you can only align one descendant element, and doesn't support multiple custom alignments on the stack of elements. so i would have only the text centered or the side button aligned not both aligned one to the center and the other to the trailing edge. and if i put a spacer between them it will just mess the alignment created. If I try with small text they will be both attached.
Heres the code. try to comment the button and you will see that it will center itself or add spacer between them.
extension HorizontalAlignment {
private enum MyAlignment: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[HorizontalAlignment.center]
}
}
static let myAlignment = HorizontalAlignment(MyAlignment.self)
}
var body: some View {
VStack(alignment: .myAlignment, spacing: 0) {
HStack {
Text("aaa eee aaa random ")
.background(Color.red)
.frame(maxWidth: .infinity, alignment: .center)
.padding(5)
.alignmentGuide(.myhAlignment, computeValue: { dimension in
dimension[HorizontalAlignment.center]
})
Button(action: {}, label: {
Image(systemName: "xmark")
.padding(15)
.frame(width: 44, height: 44)
.background(Color.yellow)
})
}
.background(Color.green)
Rectangle()
.fill(Color.purple)
.frame(width: 10, height: 10, alignment: .center)
.alignmentGuide(.myhAlignment, computeValue: { dimension in
dimension[HorizontalAlignment.center]
})
}
}
Tried with a combination of geometry reader and/or anchor preferences to read with sizes of the text content and side button width and apply the appropriate center offset manually, but it seems too hacky and it never worked as expected without good results
If you're familiar with uikit this problem would be resolved using a
centerX on the container with a minor layout priority and a right constraint from the center to the
close button, and call it a day. But on swiftui it seems soo hard to
handle this simple cases.
So far i haven't found a solution without using a supporting fixed frame on the side that would work with both long and short text. that space is clearly visibile if you try to use long text. and it will leave the user to wonder why is not used.
¯\ (ツ)/¯
EDIT: added possible solution in the answers
From the #Yrb suggestion in the comments, here's what i came up that shrink the blue size so it will center on the available space.
I added a fake text underneath and tracked the size. and if it's over the available space i will take the difference and shrink the blu rectangle.
One thing to keep in mind is that the hidden content if contains some text should have linelimit 1, otherwise it will get a smaller size from wrapping itself.
And i just assume that i know the size of the close button (or at least one side) for center alignment, and even if i don't know it at compile time, i could probably use a preference key to get the size at run time, and have it dynamic.
But for the moment i think it's fine the result that i got.
but honestly i hope to find something more easier in the future.
#State var text: String = "aaa eee aaa"
#State private var fillerWidth: CGFloat = 44
// i assume i know the max size of the close button or at least one side
private let kCloseButtonWidth: CGFloat = 44
private struct FakeSizeTitlteContentKey: PreferenceKey {
static var defaultValue: CGFloat { .zero }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
GeometryReader { parentGeometry in
titleContent
.lineLimit(1) // hidden text must not wrap
.overlay(GeometryReader { proxyFake in
Color.clear.border(Color.black, width: 0.3)
.preference(key: FakeSizeTitlteContentKey.self, value: proxyFake.frame(in: .local).width
.onPreferenceChange(FakeSizeTitlteContentKey.self) { value in
let availableW = parentGeometry.frame(in: .local).width
let fillSpace = availableW - value - kCloseButtonWidth * 2
fillerWidth = min(kCloseButtonWidth, max(0, fillSpace))
}
})
}
.hidden()
VStack {
HStack(spacing: 0) {
Rectangle()
.fill(Color.blue)
.frame(width: fillerWidth, height: 44)
titleContent
.background(Color.green)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, alignment: .center)
Button(action: {}, label: {
Image(systemName: "xmark")
.padding(15)
.frame(width: kCloseButtonWidth, height: kCloseButtonWidth)
.background(Color.yellow)
})
}
.coordinateSpace(name: "fullCont")
.background(Color.green)
TextEditor(text: $text)
.frame(maxHeight: 150, alignment: .center)
.border(Color.black, width: 1)
.padding(15)
Spacer()
}
}
}
#ViewBuilder var titleContent: some View {
HStack(spacing: 0) {
Text(text)
.background(Color.red)
.padding(.horizontal, 5)
}
}

How to create macOS Big Sur-style Search Box with exterior focus ring in SwiftUI?

In macOS Big Sur, all the built-in search box controls have a glow around their exterior to show focus. How can I achieve this effect in my SwiftUI app?
I built a search bar that looks similar, but it still has a rectangular focus ring which looks really bad. I can turn the focus ring off entirely, but that's not what I want either. I want the exterior glow.
This is my current SwiftUI code. Notice the NSTextField focusRingType override. With that, I am able to turn off the focus ring, but that's not what I want. Default gives me the rectangular, bad focus. It seems like exterior would be the ticket, but it produces the same result as default. I'm wondering if that might just be a bug in the current Big Sur/Xcode/SwiftUI developer beta.
import SwiftUI
struct SearchBarView: View {
#Binding var searchText: String
var body: some View {
HStack {
TextField("Search ...", text: self.$searchText)
.textFieldStyle(PlainTextFieldStyle())
.padding(7)
.padding(.horizontal, 25)
.background(Color("systemGray6"))
.cornerRadius(8)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 10)
}
)
.padding(.horizontal, 10)
}
.padding()
}
}
struct SearchBarView_Previews: PreviewProvider {
static var previews: some View {
VStack {
SearchBarView(searchText: .constant(""))
.previewLayout(.fixed(width: 35, height: 50))
.environment(\.colorScheme, .dark)
SearchBarView(searchText: .constant(""))
.previewLayout(.fixed(width: 35, height: 50))
.environment(\.colorScheme, .light)
}
}
}
extension NSTextField {
open override var focusRingType: NSFocusRingType {
//get { .none }
get { .default }
// get { .exterior } // same as .default, could just be bug in Big Sur Dev Beta 1&2?
set { }
}
}
I was able to figure this out! This style of search bar is provided by NSToolbar using NSSearchToolbarItem!