How can I make a custom BlurMaterial using blur modifier in Swift? - 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
}
}
}

Related

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

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

How can I squeezing a Capsule while animation is active in SwiftUI

I have a Capsule which change it`s position on screen I want to make the Capsule get squeezed on both ends, but the animation does not allow me to do this, because it takes 2 View as start and end, and I just could manage left and right changing effect, so there is no room for squeezed effect to work! How can I do this as well? like apple done it.
struct CapsuleView: View {
#State private var startAnimation: Bool = Bool()
var body: some View {
return Capsule()
.fill(Color.secondary)
.frame(height: 5, alignment: .center)
.overlay(Capsule().fill(Color.green).frame(width: 100.0, height: 5, alignment: Alignment.center), alignment: startAnimation ? Alignment.trailing : Alignment.leading)
.onAppear() { startAnimation.toggle() }
.animation(Animation.easeInOut(duration: 1.0).repeatForever(autoreverses: true), value: startAnimation)
}
}
Quick and dirty code, but based on #Zaphod`s proposal try using a mask and put your capsule behind it. then animate the offset based on the frame sizes
struct SnakeLoading: View {
#State
private var animating: Bool = true
private let height: CGFloat = 10
private let capsuleWidth: CGFloat = 100
func leadingOffset(size: CGSize) -> CGFloat {
(-size.width - capsuleWidth) * 0.5 + height
}
func trailingOffset(size: CGSize) -> CGFloat {
(size.width + capsuleWidth) * 0.5 - height
}
var body: some View {
GeometryReader { geo in
ZStack {
// Background
Rectangle()
.fill(Color.gray)
// Capsule
Capsule()
.fill(Color.green)
.offset(x: animating ? leadingOffset(size: geo.size) : trailingOffset(size: geo.size))
.frame(width: capsuleWidth, height: height)
.onAppear() { animating.toggle() }
.animation(Animation.easeInOut(duration: 1.0).repeatForever(autoreverses: true), value: animating)
}
.mask(Capsule()
.frame(height: height)
)
}
.padding()
}
}

How to keep the initial animation running while a new one is performed?

I'm having a hard time figuring out this animation thing. The problem is, the .onAppear wobbly animation (which should be forever looped) stops and freezes as soon as the view is dragged (which has another animation so that it smoothly moves towards the drag location).
Is there a way to keep the onAppear animation running independently of the view being dragged around? Important to note is that I do need the position to be dependent on the array (hence drag gesture writes directly to data), and I don't need the wobble (size animation) to be tied to the array at all.
Illustration of the animation freezing on drag (undesirable - would like to circles to still wobble during and after being dragged)
Please see code:
var body: some View {
Circle()
// .resizable()
.frame(width:size.width, height: size.height) //self.size is a state var
.foregroundColor(color)
.position(pos) //pos is a normal var bound to an array row
.gesture(DragGesture()
.onChanged { gesture in
withAnimation(.default) {
ballStorage.balls[numberInArray].position = gesture.location
}
}
.onEnded { gesture in
ballStorage.checkOverlaps(for: numberInArray)
}
) .onAppear {
withAnimation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true)) {
size = CGSize(width: size.width+20, height: size.height+20)
}
}
}
}
You need to separate circle into own view with own animation, then internal animation will be independent of external animation.
Here is a demo of solution on somehow replicated code. Prepared with Xcode 12.1 / iOS 14.1.
struct Ball: Identifiable, Hashable {
let id = UUID()
var position = CGPoint.zero
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
struct BallView: View {
let ball: Ball
var color = Color.blue
#State private var size = CGSize(width: 40, height: 40)
var body: some View {
Circle()
.frame(width:size.width, height: size.height) //self.size is a state var
.animation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true), value: size)
.foregroundColor(color)
.onAppear {
size = CGSize(width: size.width+20, height: size.height+20)
}
}
}
struct ContentView: View {
#State private var balls = [Ball(position: CGPoint(x: 100, y: 100)), Ball(position: CGPoint(x: 100, y: 200))]
#State private var off = CGSize.zero
#State private var selected = -1
var body: some View {
ZStack {
ForEach (Array(balls.enumerated()), id: \.1) { i, ball in
BallView(ball: ball)
.offset(x: i == selected ? off.width : 0, y: i == selected ? off.height : 0)
.position(ball.position)
.gesture(DragGesture()
.onChanged { gesture in
self.selected = i
withAnimation(.default) {
self.off = gesture.translation
}
}
.onEnded { gesture in
let pt = balls[i].position
self.balls[i].position = CGPoint(x: pt.x + gesture.translation.width, y: pt.y + gesture.translation.height)
self.off = .zero
self.selected = -1
// ballStorage.checkOverlaps(for: numberInArray)
}
)
}
}
}
}

Limit rectangle to screen edge on drag gesture

I'm just getting started with SwiftUI and I was hoping for the best way to tackle the issue of keeping this rectangle in the bounds of a screen during a drag gesture. Right now it goes off the edge until it reaches the middle of the square (I think cause I'm using CGPoint).
I tried doing some math to limit the rectangle and it succeeds on the left side only but it seems like an awful way to go about this and doesn't account for varying screen sizes. Can anyone help?
struct ContentView: View {
#State private var pogPosition = CGPoint()
var body: some View {
PogSound()
.position(pogPosition)
.gesture(
DragGesture()
.onChanged { value in
self.pogPosition = value.location
// Poor solve
if(self.pogPosition.x < 36) {
self.pogPosition.x = 36
}
}
.onEnded { value in
print(value.location)
}
)
}
}
Here is a demo of possible approach (for any view, cause view frame is read dynamically).
Demo & tested with Xcode 12 / iOS 14
struct ViewSizeKey: PreferenceKey {
static var defaultValue = CGSize.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue()
}
}
struct ContentView: View {
#State private var pogPosition = CGPoint()
#State private var size = CGSize.zero
var body: some View {
GeometryReader { gp in
PogSound()
.background(GeometryReader {
Color.clear
.preference(key: ViewSizeKey.self, value: $0.frame(in: .local).size)
})
.onPreferenceChange(ViewSizeKey.self) {
self.size = $0
}
.position(pogPosition)
.gesture(
DragGesture()
.onChanged { value in
let rect = gp.frame(in: .local)
.inset(by: UIEdgeInsets(top: size.height / 2.0, left: size.width / 2.0, bottom: size.height / 2.0, right: size.width / 2.0))
if rect.contains(value.location) {
self.pogPosition = value.location
}
}
.onEnded { value in
print(value.location)
}
)
.onAppear {
let rect = gp.frame(in: .local)
self.pogPosition = CGPoint(x: rect.midX, y: rect.midY)
}
}.edgesIgnoringSafeArea(.all)
}
}

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