SwiftUI zIndex not updating correctly inside ForEach - swift

This issue occurs in Xcode 11 GM.
When a view is tapped the colour will change to blue and the zIndex should be updated to move it to the front.
With the top example this works fine and you will never see a green view overlap a blue view. In the bottom example (using the ForEach) it doesn't redraw correctly and the blue view stays behind the green.
Although strangely if you have the bottom example in a bad state and then touch any of the views in top then the bottom will suddenly correct itself.
Any ideas or solution?
struct ContentView: View {
var body: some View {
ZStack {
// First example, working
TestView().position(x: 100, y: 100)
TestView().position(x: 200, y: 200)
// Second example, not working
ForEach(1..<3) { i in
TestView().position(x: 100*CGFloat(i), y: 300 + 100*CGFloat(i))
}
}
}
}
struct TestView: View {
#State var isTapped = false
var body: some View {
return Text("Test Test Test")
.frame(width: 150, height: 150)
.background(isTapped ? Color.blue : Color.green)
.zIndex(isTapped ? 100 : 0)
.gesture(
TapGesture()
.onEnded {
self.isTapped.toggle()
}
)
}
}

Related

SwiftUI gestures in the toolbar area ignored

I'd like to implement a custom slider SwiftUI component and put it on the toolbar area of a SwiftUI Mac app. However the gesture of the control gets ignored as the system's window moving gesture takes priority. This problem does not occur for the system UI controls, like Slider or Button.
How to fix the code below so the slider works in the toolbar area as well, not just inside the window similar to the default SwiftUI components?
struct MySlider: View {
#State var offset: CGFloat = 0.0
var body: some View {
GeometryReader { gr in
let thumbSize = gr.size.height
let maxValue = (gr.size.width - thumbSize) / 2.0
let gesture = DragGesture(minimumDistance: 0).onChanged { v in
self.offset = max(min(v.translation.width, maxValue), -maxValue)
}
ZStack {
Capsule()
Circle()
.foregroundColor(Color.yellow)
.frame(width: thumbSize, height: thumbSize)
.offset(x: offset)
.highPriorityGesture(gesture)
}
}.frame(width: 100, height: 20)
}
}
struct ContentView: View {
#State var value = 0.5
var body: some View {
MySlider()
.toolbar {
MySlider()
Slider(value: $value).frame(width: 100, height: 20)
}.frame(width: 500, height: 100)
}
}
Looks like design limitation (or not implemented yet feature - Apple does not see such view as user interaction capable item).
A possible workaround is to wrap you active element into button style. The button as a container interpreted as user-interaction-able area but all drawing and handling is in your code.
Tested with Xcode 13.2 / macOS 12.2
Note: no changes in your slider logic
struct MySlider: View {
var body: some View {
Button("") {}.buttonStyle(SliderButtonStyle())
}
struct SliderButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
MySliderContent()
}
struct MySliderContent: View {
#State var offset: CGFloat = 0.0
var body: some View {
GeometryReader { gr in
let thumbSize = gr.size.height
let maxValue = (gr.size.width - thumbSize) / 2.0
let gesture = DragGesture(minimumDistance: 0).onChanged { v in
self.offset = max(min(v.translation.width, maxValue), -maxValue)
}
ZStack {
Capsule()
Circle()
.foregroundColor(Color.yellow)
.frame(width: thumbSize, height: thumbSize)
.offset(x: offset)
.highPriorityGesture(gesture)
}
}.frame(width: 100, height: 20)
}
}
}
}

Creating a drag handle inside of a SwiftUI view (for draggable windows and such)

I'm trying to learn SwiftUI, and I had a question about how to make a component that has a handle on it which you can use to drag around. There are several tutorials online about how to make a draggable component, but none of them exactly answer the question I have, so I thought I would seek the wisdom of you fine people.
Lets say you have a view that's like a window with a title bar. For simplicity's sake, lets make it like this:
struct WindowView: View {
var body: some View {
VStack(spacing:0) {
Color.red
.frame(height:25)
Color.blue
}
}
}
I.e. the red part at the top is the title bar, and the main body of the component is the blue area. Now, this window view is contained inside another view, and you can drag it around. The way I've read it, you should do something like this (very simplified):
struct ContainerView: View {
#State private var loc = CGPoint(x:150, y:150);
var body: some View {
ZStack {
WindowView()
.frame(width:100, height:100)
.position(loc)
.gesture(DragGesture()
.onChanged { value in
loc = value.location
}
)
}
}
}
and that indeed lets you drag the component around (ignore for now that we're always dragging by the center of the image, it's not really the point):
However, this is not what I want: I don't want you to be able to drag the component around by just dragging inside the window, I only want to drag it around by dragging the red title bar. But the red title-bar is hidden somewhere inside of WindowView. I don't want to move the #State variable containing the position to inside the WindowView, it seems to me much more logical to have that inside ContainerView. But then I need to somehow forward the gesture into the embedded title bar.
I imagine the best way would be for the ContainerView to look something like this:
struct ContainerView: View {
#State private var loc = CGPoint(x:150, y:150);
var body: some View {
ZStack {
WindowView()
.frame(width:100, height:100)
.position(loc)
.titleBarGesture(DragGesture()
.onChanged { value in
loc = value.location
}
)
}
}
}
but I don't know how you would implement that .titleBarGesture in the correct way (or if this is even the proper way to do it. should the gesture be an argument to the WindowView constructor?). Can anyone help me out, give me some pointers?
Thanks in advance!
You can get smooth translation of the window using the offset from the drag, and then disable touch on the background element to prevent content from dragging.
Buttons still work in the content area.
import SwiftUI
struct WindowBar: View {
#Binding var location: CGPoint
var body: some View {
ZStack {
Color.red
.frame(height:25)
Text(String(format: "%.1f: %.1f", location.x, location.y))
}
}
}
struct WindowContent: View {
var body: some View {
ZStack {
Color.blue
.allowsHitTesting(false) // background prevents interaction
Button("Press Me") {
print("Tap")
}
}
}
}
struct WindowView: View, Identifiable {
#State var location: CGPoint // The views current center position
let id = UUID()
/// Keep track of total translation so that we don't jump on finger drag
/// SwiftUI doesn't have an onBegin callback like UIKit's gestures
#State private var startDragLocation = CGPoint.zero
#State private var isBeginDrag = true
init(location: CGPoint = .zero) {
_location = .init(initialValue: location)
}
var body: some View {
VStack(spacing:0) {
WindowBar(location: $location)
WindowContent()
}
.frame(width: 100, height: 100)
.position(location)
.gesture(DragGesture()
.onChanged({ value in
if isBeginDrag {
isBeginDrag = false
startDragLocation = location
}
// In UIKit we can reset translation to zero, but we can't in SwiftUI
// So we do book keeping to track startLocation of gesture and adjust by
// total translation
location = CGPoint(x: startDragLocation.x + value.translation.width,
y: startDragLocation.y + value.translation.height)
})
.onEnded({ value in
isBeginDrag = true /// reset for next drag
}))
}
}
struct ContainerView: View {
#State private var windows = [
WindowView(location: CGPoint(x: 50, y: 100)),
WindowView(location: CGPoint(x: 190, y: 75)),
WindowView(location: CGPoint(x: 250, y: 50))
]
var body: some View {
ZStack {
ForEach(windows) { window in
window
}
}
.frame(width: 600, height: 480)
}
}
struct ContentView: View {
var body: some View {
ContainerView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can just use .allowsHitTesting(false) on the blue view, which will ignore the touch gesture on that view. Hence, you can only drag on the red View and still have the DragGesture outside that view.
struct WindowView: View {
var body: some View {
VStack(spacing:0) {
Color.red
.frame(height:25)
Color.blue
.allowsHitTesting(false)
}
}
}
You can wrap the DragGesture into a ViewModifier:
struct MovableByBar: ViewModifier {
static let barHeight: CGFloat = 14
#State private var loc: CGPoint!
#State private var transition: CGSize = .zero
func body(content: Content) -> some View {
if loc == nil { // Get the original position
content.padding(.top, MovableByBar.barHeight)
.overlay {
GeometryReader { geo -> Color in
DispatchQueue.main.async {
let frame = geo.frame(in: .local)
loc = CGPoint(x: frame.midX, y: frame.midY)
}
return Color.clear
}
}
} else {
VStack(spacing: 0) {
Rectangle()
.fill(.secondary)
.frame(height: MovableByBar.barHeight)
.offset(x: transition.width, y: transition.height)
.gesture (
DragGesture()
.onChanged { value in
transition = value.translation
}
.onEnded { value in
loc.x += transition.width
loc.y += transition.height
transition = .zero
}
)
content
.offset(x: transition.width,
y: transition.height)
}
.position(loc)
}
}
}
And use modifier like this:
WindowView()
.modifier(MovableByBar())
.frame(width: 80, height: 60) // `frame()` after it

How to reliably retrieve a window's background color on macOS with SwiftUI?

Is there a Swifty way to detect the background color of a window in SwiftUI on macOS, that would work reliably regardless of the current theme (Dark Mode or Light Mode)?
For example, if one were to make a solid rectangle that "blends in" with the window's background, which color would they use?
This answer suggests the use of NSColor.xxxBackgroundColor:
SwiftUI: Get the Dynamic Background Color (Dark Mode or Light Mode)
However, this doesn't quite work for me. Here's some test code (Xcode 12.5, Swift 5.4) that makes three rectangles of various NSColors. I am looking for the one that blends in with the background.
struct TestView: View {
var body: some View {
VStack(spacing: 20) {
Text("This text is on the default background")
HStack(spacing: 30) {
Text("windowBackgroundColor")
.frame(width: 200, height: 100)
.background(Color(NSColor.windowBackgroundColor))
Text("underPageBackgroundColor")
.frame(width: 200, height: 100)
.background(Color(NSColor.underPageBackgroundColor))
Text("textBackgroundColor")
.frame(width: 200, height: 100)
.background(Color(NSColor.textBackgroundColor))
}
}
.padding(20)
}
}
In Dark mode, it seems NSColor.underPageBackgroundColor matches the window's background.
But in Light mode, nothing matches:
Swifty way to get Window Background
The window's background is not composed by a color, is a NSVisualEffectView with .windowBackground as material.
You can achieve that with this code:
struct EffectView: NSViewRepresentable {
#State var material: NSVisualEffectView.Material = .headerView
#State var blendingMode: NSVisualEffectView.BlendingMode = .withinWindow
func makeNSView(context: Context) -> NSVisualEffectView {
let view = NSVisualEffectView()
view.material = material
view.blendingMode = blendingMode
return view
}
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
nsView.material = material
nsView.blendingMode = blendingMode
}
}
And apply that to your view:
struct ContentView: View {
var body: some View {
EffectView(material: .windowBackground)
.overlay(Text("Window Real Background"))
.padding(30)
}
}
The output is this (nothing to see because it mimetizes with background):
The reason why the background is an NSVisualEffectView is because the Window Tinting of macOS Big Sur, that changes the background according to wallpaper predominant color:
You can use #Environment variables to get the ColorScheme that is being produced. In iOS I often use it like this, however it should translate to MacOS as well. There is no way to GET a view's color dynamically, because it is a set value that is not accessible. The best you can do is set the view, in a known state, and then pass that color around as needed. In my example I just used Color.black and Color.white but you can easily assign any color to a variable and pass it around so that it is known.
#Environment(\.colorScheme) var colorScheme
struct TestView: View {
var body: some View {
VStack(spacing: 20) {
Text("This text is on the default background")
HStack(spacing: 30) {
Text("windowBackgroundColor")
.frame(width: 200, height: 100)
.background(colorScheme == .dark ? Color.black : Color.white)
Text("underPageBackgroundColor")
.frame(width: 200, height: 100)
.background(colorScheme == .dark ? Color.black : Color.white)
Text("textBackgroundColor")
.frame(width: 200, height: 100)
.background(colorScheme == .dark ? Color.black : Color.white)
}
}
.background(colorScheme == .dark ? Color.black : Color.white)
.padding(20)
}
}
In your case you can alternate the colors to the system colors that you're wanting to use. Setting based on the colorScheme for the particular color you want on that view. Eg...
var windowBGColor = NSColor.windowBackgroundColor
var underPageBGColor = NSColor.underPageBackgroundColor
var textBGColor = NSColor.textBackgroundColor
var sampleColor = colorScheme == .dark ? windowBGColor : textBGColor
The windowBackground is a real window's background color, you just compare it not with real window background, but with theme view below hosting controller view
Here is a hierarchy (you can verify it yourself in view debug mode)

How to make a picture appear in a cell after pressing a button

In SwiftUI I have a button in a View:
Button(action: {self.isMatrimonioOn.toggle()}) {
Image(!isMatrimonioOn ? "imageA" : "imageA_color")
}
When the user presses the button it changes color (imageA-> imageA_color) and at the same time an image (imageA_Color itself) must appear in the cell in another view. How should I set the function?
Thanks but I still haven't succeeded. I put it, I think, but when the button is pressed it does not change the image.
#Binding var isMatrimonioOn : Bool
var body: some View {
ZStack{
Image("sfondo")
.resizable()
.frame(width: 350, height: 180)
CellBackground()
.offset(x: 93, y: 29)
HStack {
VStack(alignment: .leading) {
if isNataleOn == true {
Image("imageA").offset(x: 3, y: 15)
} else {
Image("none")
}
Assuming "another view" here is a child view of this view and isMatrimonioOn is a #State
The child view can have a var with attribute #Binding to which you can pass $isMatrimonioOn.
You can then use logic like in your question or if statement wrapping the image.

SwiftUI - setting the frame of view

When I learn new stuff like SwiftUI (beta 6)
I want to start from the basic.
I just want to set frame to subview like in UIKit.
What I'm missing here ? (this is from Simulator)
1. the subview is not in 0,0 position.
2. why at least the start of the word is not inside the border ?
UPDATE :
how to set the text view in 0,0 position ? (just like on UIKit)
I thought my question is very clear, but for some reason, it's not.
I think it's important to understand why your solution doesn't work because at a first glance it seems correct and it seems that SwiftUI works in some weird ways (because, of course, we are all used to UIKit).
You tried:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello World")
.position(CGPoint(x: 0, y: 0))
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
}
}
}
And you got:
First of all, the position modifier says:
Fixes the center of the view at the specified point in its parent’s
coordinate space.
Two things are important here:
The view is moved based on its centre, not based on its top-left corner
The view is moved in the parent's coordinate space
But who is the Text's parent? A view modifier in SwiftUI is something that applies to a View and returns a View. Modifiers are applied from the last one to the first one (in reverse order respect to how you see them). In your case:
So: The centre of the Text is positioned at (0,0) respect to a Frame 50x100 with a red Border. The resulting View is placed in the centre of the screen because of the VStack (it's the VStack default behaviour). In other words: the position's parent (position returns a View, every modifier returns a View) is the Frame 50x100 placed in the centre of the screen.
If you want to position the top-left corner of the Text at (0,0) in the Frame coordinate space you should use the Spacer modifier this way:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello World")
Spacer()
}
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
}
}
And you'll get:
If you want, instead, the top-left corner of the Frame to be at (0,0) respect to the whole View I think the simplest way is:
struct ContentView: View {
var body: some View {
HStack {
VStack {
Text("Hello World")
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
Spacer()
}
Spacer()
}
.edgesIgnoringSafeArea(.all)
}
}
And you'll get:
Do Like this way
struct ContentView: View {
var body: some View {
VStack{
Text("Hello World")
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
.padding()
Spacer()
}
}
}
Below is output
if you want to remove space on top add .edgesIgnoringSafeArea(.top) for your view like below
struct ContentView: View {
var body: some View {
VStack{
Text("Hello World")
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
.padding()
Spacer()
}
.edgesIgnoringSafeArea(.top)
}
}