How can I make the Window change it's position on Screen of a mac with pure SwiftUI? - swift

Is there a pure SwiftUI code for changing the position of Window in macOS SwiftUI? I know there is a way via AppKit codes and using NSApp.mainWindow and then using window.isMovableByWindowBackground = true, but my approach is not about that way, I want to know if there is an approach using SwiftUI modifiers like offset or position or any other SwiftUI modifier to reach the goal?
This is my current code, it does not move Window:
import SwiftUI
struct ContentView: View {
#State private var offset: CGSize = .zero
var body: some View {
VStack {
Circle()
.frame(width: 100, height: 100)
}
.frame(width: 200, height: 200)
.background(Color.purple)
.offset(offset)
.gesture(gesture)
}
var gesture: some Gesture {
return DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged { dragValue in
offset = CGSize(width: dragValue.translation.width, height: -dragValue.translation.height)
}
.onEnded { _ in
offset = .zero
}
}
}

Related

How can I make a custom BlurMaterial using blur modifier in Swift?

I am trying to over come to making a custom BlurMaterial challenge, as you could read from question I am trying to make a custom BlurMaterial, the idea is simple, I am accessing the all available view and then trying to cut it and blur the cut part, but the issue happens when the blur radius goes up and up, the wired rectangular happens and it seems the blur got issue at edges the yellow rectangular, I think this issue could simply solved, as you can see the logic of codes works almost, need help to solve the wired rectangular issue and blur issue at edges the yellow rectangular when we increases the blur radius.
INFO: the issue of wired rectangular happens in macOS SwiftUI but the blur issue at edges the yellow rectangular happens in iOS and macOS SwiftUI. My target is macOS SwiftUI not just iOS SwiftUI.
struct ContentView: View {
#State private var radius: CGFloat = 8.0
#State private var opacity: CGFloat = 0.2
#State private var accentColorOfBlurMaterial: Color = Color.black.opacity(0.5)
#State private var backgroundColor: Color = Color.green
#State private var offset: CGSize = CGSize.zero
#State private var lastOffset: CGSize = CGSize.zero
#State private var blurSize: CGSize = CGSize(width: 200.0, height: 200.0)
var body: some View {
let baseView = VStack {
Button("Tap") { print("tapped!") }
swift
slider(label: "radius:", value: $radius, range: 0.0...50.0)
slider(label: "opacity:", value: $opacity, range: 0.0...1.0)
}
.padding()
.background(Color.yellow.cornerRadius(10.0))
.padding(50.0)
.background(backgroundColor)
.fixedSize()
return ZStack {
baseView
baseView
.blur(radius: radius)
.frame(width: blurSize.width, height: blurSize.height)
.offset(x: -offset.width, y: -offset.height)
.clipped()
.contentShape(Rectangle())
.overlay(accentColorOfBlurMaterial.opacity(opacity))
.border(Color.black, width: 5.0)
.offset(offset)
.gesture(gesture)
.onChange(of: radius, perform: { newValue in
print("blur radius =", newValue)
})
}
}
var swift: some View { return Image(systemName: "swift").resizable().scaledToFit().frame(width: 300.0).foregroundColor(Color.red) }
func slider(label: String, value: Binding<CGFloat>, range: ClosedRange<CGFloat>) -> some View {
return HStack { Text(label); Spacer(); Slider(value: value, in: range) }
}
private var gesture: some Gesture {
return DragGesture(minimumDistance: .zero, coordinateSpace: .global)
.onChanged { value in
offset = CGSize(width: lastOffset.width + value.translation.width, height: lastOffset.height + value.translation.height)
}
.onEnded { value in
lastOffset = CGSize(width: lastOffset.width + value.translation.width, height: lastOffset.height + value.translation.height)
offset = lastOffset
}
}
}

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)
}
}
}
}

Increase/Decrease the size of a view horizontally by dragging the edges of it

I've seen a few similar examples of this such as How to correctly do up an adjustable split view in SwiftUI? and How to resize UIView by dragging from its edges? but I can't find exactly what I'm looking for that correlates correctly across to SwiftUI
I have a view that I want the user to be able to adjust the width of via a 'grab bar' on the right of the view. When the user drags this bar left (decreases view width) and to the right (increases the view width). How can I go about doing this?
In the example, RedRectangle is my view that i'm trying to adjust which comprises of a Rectangle and the Resizer which is manipulated to adjust the size. What am I doing wrong here?
Additionally, there isn't a gradual animation/transition of the frame being increased/decreased and it just seems to jump. How can I achieve this?
Reproducible example linked here:
import SwiftUI
struct ContentView: View {
#State var resizedWidth: CGFloat?
var body: some View {
HStack(alignment: .center) {
Spacer()
RedRectangle(width: 175, resizedWidth: resizedWidth)
Resizer()
.gesture(
DragGesture()
.onChanged({ value in
resizedWidth = max(80, resizedWidth ?? 0 + value.translation.width)
})
)
Spacer()
}
}
}
struct RedRectangle: View {
let width: CGFloat
var resizedWidth: CGFloat?
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: resizedWidth != nil ? resizedWidth : width, height: 300)
.frame(minWidth: 80, maxWidth: 400)
}
}
struct Resizer: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 8, height: 75)
.cornerRadius(10)
}
}
import SwiftUI
struct ContentView: View {
let minWidth: CGFloat = 100
#State var width: CGFloat?
var body: some View {
HStack(alignment: .center) {
Spacer()
RedRectangle(width: width ?? minWidth)
Resizer()
.gesture(
DragGesture()
.onChanged { value in
width = max(minWidth, width! + value.translation.width)
}
)
Spacer()
}
.onAppear {
width = minWidth
}
}
}
struct RedRectangle: View {
let width: CGFloat
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: width, height: 100)
}
}
struct Resizer: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 8, height: 75)
.cornerRadius(10)
}
}

How to drag a working slider using SwiftUI

I would like to drag a slider and of course have it slide as well.
I can do one or the other, but I cannot do both.
How can I drag and have a working slider?
I also tried to find a way to remove a gesture, but I could not find a way to do that.
Also tried the "Sequenced Gesture States" code from Apple "Composing SwiftUI Gestures" docs,
and introduce a flag to turn the dragging on/off with same results, drag or slide not both.
I also tried to put the slider in a Container (VStack) and attach the drag gesture to that,
but that did not work either.
import SwiftUI
struct ContentView: View {
#State var pos = CGSize.zero
#State var acc = CGSize.zero
#State var value = 0.0
var body: some View {
let drag = DragGesture()
.onChanged { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
}
.onEnded { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
self.acc = self.pos
}
return Slider(value: $value, in: 0...100, step: 1)
.frame(width: 250, height: 40, alignment: .center)
.overlay(RoundedRectangle(cornerRadius: 25).stroke(lineWidth: 2).foregroundColor(Color.black))
.offset(x: self.pos.width, y: self.pos.height)
.simultaneousGesture(drag, including: .all) // tried .none .gesture, .subviews
// also tried .gesture(flag ? nil : drag)
}
}
With "simultaneousGesture" I was expecting to have both gestures operating at the same time.
This is working. Basically I needed to set the flag in an outside observable object for it to update the view so that it could take effect. When the value changes the flag is set to false, but then after a tenth of a second it becomes draggable. Working pretty seamlessly.
struct ContentView: View {
#State var pos = CGSize.zero
#State var acc = CGSize.zero
#State var value = 0.0
#ObservedObject var model = Model()
var body: some View {
let drag = DragGesture()
.onChanged { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
}
.onEnded { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
self.acc = self.pos
}
return VStack {
Slider(value: $value, in: 0...100, step: 1) { _ in
self.model.flag = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.model.flag = true
}
}
}
.frame(width: 250, height: 40, alignment: .center)
.overlay(RoundedRectangle(cornerRadius: 25).stroke(lineWidth: 2).foregroundColor(Color.black))
.offset(x: self.pos.width, y: self.pos.height)
.gesture(model.flag == true ? drag : nil)
}
}
class Model: ObservableObject {
#Published var flag = false
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI: Gesture and Offset Are Not Working As Intended

I am using offset and gesture modifiers to move a circle around the screen. When I use this code, everything works as expected:
import SwiftUI
struct MovingCircle: View {
#State private var dragged = CGSize.zero
var body: some View {
Circle()
.offset(x: self.dragged.width)
.frame(width: 20, height: 20)
.gesture(DragGesture()
.onChanged{ value in
self.dragged = value.translation
}
.onEnded{ value in
self.dragged = CGSize.zero
}
)
}
}
However, I do not want to have the circle reset to the original position onEnded. I would like it to remain in place and then be moved again on dragging. When I use the following code, I lose the ability to move the circle again upon re-dragging and it remains in place:
import SwiftUI
struct MovingCircle: View {
#State private var dragged = CGSize.zero
var body: some View {
Circle()
.offset(x: self.dragged.width)
.frame(width: 20, height: 20)
.gesture(DragGesture()
.onChanged{ value in
self.dragged = value.translation
}
.onEnded{ value in
self.dragged = value.translation
}
)
}
}
What is the cause of this, have I encountered some bug or have I coded it incorrectly?
First, to understand the problem, add a .border(Color.red) to the .frame() modifier:
.frame(width: 20, height: 20).border(Color.red)
You'll see that when the dot is moved, its frame remains in place. That is why later, it won't respond to gestures. The "tappable" area no longer matches the dot. And because the content area is now empty, it is no longer "tappable".
To make the frame move with the dot, invert the order. The .offset() should come later:
.frame(width: 20, height: 20).border(Color.red)
.offset(x: self.dragged.width)
Finally, you will see that after each .onEnded(), the whole thing resets back. One way to solve it, is by accumulating how much you dragged in previous gestures:
struct MovingCircle: View {
#State private var dragged = CGSize.zero
#State private var accumulated = CGSize.zero
var body: some View {
Circle()
.frame(width: 20, height: 20).border(Color.red)
.offset(x: self.dragged.width)
.gesture(DragGesture()
.onChanged{ value in
self.dragged = CGSize(width: value.translation.width + self.accumulated.width, height: value.translation.height + self.accumulated.height)
}
.onEnded{ value in
self.dragged = CGSize(width: value.translation.width + self.accumulated.width, height: value.translation.height + self.accumulated.height)
self.accumulated = self.dragged
}
)
}
}
Swift 5.x iOS 13
A Less elegant solution, if you don't care to see the object dragged; you just want to be able to drag it; maybe makes more sense if you got a lot going on and you're looking to save CPU.
Obviously you need to find a better starting position for it then absolute zero.
import SwiftUI
var intern:CGPoint = CGPoint(x:0,y:0)
enum DragState {
case inactive
case dragging(translation: CGSize)
}
struct ContentView: View {
#State var position:CGPoint = CGPoint(x:0,y:0)
#GestureState private var dragState = DragState.inactive
var body: some View {
ZStack {
Circle()
.frame(width: 24, height: 24, alignment: .center)
}.offset(x: self.dragOffset.width, y: self.dragOffset.height)
.gesture(DragGesture(coordinateSpace: .global)
.updating($dragState, body: { (drag, state, translation)
intern = drag.location
})
.onEnded { ( value ) in
self.position = intern
}
).position(position)
}
}