SwiftUI: using RotationGesture with changeable blur effect - rotation

I am trying to combine the RotationGesture with changeable blur effect. So if you turn the ocular right, you get the picture more sharp like in binoculars. Turn left, it will be blurred again. The code below is basically doing it, but not very smoothly. Have you any ideas to improve the effect?
struct ContentView: View {
#State var blurring: CGFloat = 20
#State var sharping: CGFloat = 0
#State private var rotateState: Double = 0
var body: some View {
ZStack {
Image("cat")
.resizable()
.frame(width: 200, height: 200)
.blur(radius: blurring)
.clipShape(Circle())
Image("ocular_rk2")
.resizable()
.frame(width: 350, height: 350)
.rotationEffect(Angle(degrees: self.rotateState))
.gesture(RotationGesture()
.onChanged { value in
self.rotateState = value.degrees
self.changeBlur()
}
.onEnded { value in
self.rotateState = value.degrees
print(self.rotateState)
}
)
}
}
func changeBlur() {
switch rotateState {
case -10000...10000: sharping = 0.1
default:
print("# Mistake in Switch")
}
if rotateState < 0 {
blurring += sharping
} else {
blurring -= sharping
}
}
}

check this out (i tested it, it works)
maybe you should think about changing some values because you have to rotate a while until you see the text.
by the way: it is always a good manner to post compilable reproducable code (we do not have your images)...
struct ContentView: View {
#State var blurring: CGFloat = 20
#State var sharping: CGFloat = 0
#State private var rotateState: Double = 0
var body: some View {
ZStack {
Text("cat")
// .resizable()
.frame(width: 200, height: 200)
.clipShape(Circle())
.background(Color.yellow)
Text("ocular_rk2")
// .resizable()
.blur(radius: blurring)
.frame(width: 350, height: 350)
.background(Color.red)
.rotationEffect(Angle(degrees: self.rotateState))
.gesture(RotationGesture()
.onChanged { value in
self.rotateState = value.degrees
self.changeBlur()
}
.onEnded { value in
self.rotateState = value.degrees
print(self.rotateState)
}
)
}
}
func changeBlur() {
switch rotateState {
case -10000...10000: sharping = 0.1
default:
print("# Mistake in Switch")
}
if rotateState < 0 {
blurring = blurring + sharping
} else {
blurring = blurring - sharping
print(blurring)
}
}
}

Related

Tracking scroll position in a List SwiftUI

So in the past I have been tracking the scroll position using a scroll view but I've fallen into a situation where I need to track the position using a List. I am using a List because I want some of the built in real estate to create my views such as the default List styles.
I can get the value using PreferenceKeys, but the issue is when I scroll to far upwards, the PreferenceKey value will default back to its position 0, breaking my show shy header view logic.
This is the TrackableListView code
struct TrackableListView<Content: View>: View {
let offsetChanged: (CGPoint) -> Void
let content: Content
init(offsetChanged: #escaping (CGPoint) -> Void = { _ in }, #ViewBuilder content: () -> Content) {
self.offsetChanged = offsetChanged
self.content = content()
}
var body: some View {
List {
GeometryReader { geometry in
Color.clear.preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("ListView")).origin)
}
.frame(width: 0, height: 0)
content
.offset(y: -10)
}
.coordinateSpace(name: "ListView")
.onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: offsetChanged)
}
}
private struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { }
}
And this is my ContentView:
struct ContentView: View {
#State private var contentOffset = CGFloat(0)
#State private var offsetPositionValue: CGFloat = 0
#State private var isShyHeaderVisible = false
var body: some View {
NavigationView {
ZStack {
TrackableListView { offset in
withAnimation {
contentOffset = offset.y
}
} content: {
Text("\(contentOffset)")
}
.overlay(
ZStack {
HStack {
Text("Total points")
.foregroundColor(.white)
.lineLimit(1)
Spacer()
Text("20,000 pts")
.foregroundColor(.white)
.padding(.leading, 50)
}
.padding(.horizontal)
.padding(.vertical, 8)
.frame(width: UIScreen.main.bounds.width)
.background(Color.green)
.offset(y: contentOffset < 50 ? 0 : -5)
.opacity(contentOffset < 50 ? 1 : 0)
.transition(.move(edge: .top))
}
.frame(maxHeight: .infinity, alignment: .top)
)
}
.navigationTitle("Hello")
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarTitleDisplayMode(.inline)
.frame(maxHeight: .infinity, alignment: .top)
.background(AccountBackground())
}
}
}
The issue was that Other Views in my hierarchy (probably introduced by SwiftUI) could be sending the default value, which is why you start getting zero sometimes.
What I needed was a way to determine when to use or forward the new value, versus when to ignore it.
To do this I had to make my value optional GCPoint, with a nil default value, then in your reduce method when using PreferenceKeys you have to do:
if let nextValue = nextValue() {
value = nextValue
}
Then make sure your CGPoint is an optional value.

SwiftUI odd animation behavior with systemImage

I was messing around with a fun animation in SwiftUI when I ran into a weird problem involving animating changes to SwiftUI's SF symbols. Basically, I want to animate a set of expanding circles that lose opacity as they get farther out. This works fine when I animate the circles using the Circle() shape, but throws a weird error when I use Image(systemName: "circle"). Namely, it throws No symbol named 'circle' found in system symbol set and I get the dreaded "purple" error in Xcode. Why does my animation work with shapes but not with SF symbols?
Animation Code with Shapes:
struct ContentView: View {
let timer = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
#State var firstIndex: Int = 0
#State var secondIndex: Int = 10
#State var thirdIndex: Int = 20
#State var fourthIndex: Int = 30
private func changeIndex(index: Int) -> Int {
if index == 40 {
return 0
} else {
return index + 1
}
}
var body: some View {
ZStack {
Circle()
.foregroundColor(.black)
.frame(width: 10, height: 10)
ExpandingCircle(index: firstIndex)
ExpandingCircle(index: secondIndex)
ExpandingCircle(index: thirdIndex)
ExpandingCircle(index: fourthIndex)
}
.onReceive(timer) { time in
withAnimation(.linear(duration: 0.25)) {
self.firstIndex = changeIndex(index: firstIndex)
self.secondIndex = changeIndex(index: secondIndex)
self.thirdIndex = changeIndex(index: thirdIndex)
self.fourthIndex = changeIndex(index: fourthIndex)
}
}
}
}
Where ExpandingCircle is defined as:
struct ExpandingCircle: View {
let index: Int
private func getSize() -> CGFloat {
return CGFloat(index * 2)
}
private func getOpacity() -> CGFloat {
if index == 0 || index == 40 {
return 0
} else {
return CGFloat(1 - (Double(index) * 0.025))
}
}
var body: some View {
Circle()
.strokeBorder(Color.red, lineWidth: 4)
.frame(width: getSize(), height: getSize())
.opacity(getOpacity())
}
}
To replicate the error, swap out ExpandingCircle in ContentView for ExpandingCircleImage:
struct ExpandingCircleImage: View {
let index: Int
private func getSize() -> CGFloat {
return CGFloat(index * 2)
}
private func getOpacity() -> CGFloat {
if index == 0 || index == 40 {
return 0
} else {
return CGFloat(1 - (Double(index) * 0.025))
}
}
var body: some View {
Image(systemName: "circle")
.foregroundColor(.red)
.font(.system(size: getSize()))
.opacity(getOpacity())
}
}
Your ExpandingCircleImage is choking because you can't have a system font of size 0, and you keep trying to feed 0 to your ExpandingCircleImage view. However, in addition to that, you don't need to use a timer to drive the animation. In fact, it makes the animation look weird because a timer is not exact. Next, your ExpandingCircle or ExpandingCircleImage should animate itself and be the complete effect.
The next issue you will encounter when you fix the font size = 0 issue, is that .font(.system(size:)) is not animatable as it is. You need to write an AnimatableModifier for it. That looks like this:
struct AnimatableSfSymbolFontModifier: AnimatableModifier {
var size: CGFloat
var animatableData: CGFloat {
get { size }
set { size = newValue }
}
func body(content: Content) -> some View {
content
.font(.system(size: size))
}
}
extension View {
func animateSfSymbol(size: CGFloat) -> some View {
self.modifier(AnimatableSfSymbolFontModifier(size: size))
}
}
The animatableData variable is the key. It teaches SwiftUI what to change to render the animation. In this case, we are animating the size of the font. The view extension is just a convenience so we can use . notation.
Another trick to animating a view like this is to have multiple animations that only go part of the way of the whole. In other words, if you use four circles, the first goes to 25%, the next from 25% to 50%, then 50% to 75%, lastly 75% to 100%. You also appear to have wanted the rings to fade as the expand, so I wrote that in as well. The code below will have two animating views, one made with a shape, and one with an SF Symbol.
struct ContentView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Circle()
.foregroundColor(.black)
.frame(width: 10, height: 10)
ExpandingCircle(maxSize: 100)
}
.frame(height: 100)
Spacer()
ZStack {
Circle()
.foregroundColor(.black)
.frame(width: 10, height: 10)
ExpandingCircleImage(maxSize: 100)
}
.frame(height: 100)
Spacer()
}
}
}
struct ExpandingCircle: View {
let maxSize: CGFloat
#State private var animate = false
var body: some View {
ZStack {
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0.75 : 1)
.scaleEffect(animate ? 0.25 : 0)
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0.5 : 0.75)
.scaleEffect(animate ? 0.5 : 0.25)
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0.25 : 0.5)
.scaleEffect(animate ? 0.75 : 0.5)
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0 : 0.25)
.scaleEffect(animate ? 1 : 0.75)
}
.frame(width: maxSize, height: maxSize)
.onAppear {
withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) {
animate = true
}
}
}
}
struct ExpandingCircleImage: View {
let maxSize: CGFloat
#State private var animate = false
var body: some View {
ZStack {
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize * 0.25) : 1)
.opacity(animate ? 0.75 : 1)
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize * 0.5) : (maxSize * 0.25))
.opacity(animate ? 0.5 : 0.75)
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize * 0.75) : (maxSize * 0.5))
.opacity(animate ? 0.25 : 0.5)
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize) : (maxSize * 0.75))
.opacity(animate ? 0 : 0.25)
}
.foregroundColor(.red)
.onAppear {
withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) {
animate = true
}
}
}
}
Remember to include the AnimatableModifier in your code.

SwiftUI: magnification gesture that changes value between min and max

I want to make the same thing that is possible to do with a slider:
#State itemSize: CGFloat = 55.60
....
Text("ItemSize: \(itemSize)")
Slider(value: $itemSize, in: 45...120)
but with magnification gesture.
I have created modifier for this:
import SwiftUI
#available(OSX 11.0, *)
public extension View {
func magnificationZoomer(value: Binding<CGFloat>,min: CGFloat, max: CGFloat) -> some View
{
self.modifier( MagnificationZoomerMod(minZoom: min, maxZoom: max, zoom: value) )
}
}
#available(OSX 11.0, *)
public struct MagnificationZoomerMod: ViewModifier {
let minZoom: CGFloat
let maxZoom: CGFloat
#Binding var zoom: CGFloat
public func body (content: Content) -> some View
{
content
.gesture(
MagnificationGesture()
.onChanged() { val in
let magnification = (val - 1) * 2.2
print("magnification = \(magnification)")
zoom = max(min(zoom + magnification, maxZoom), minZoom)
}
)
}
}
sample of usage:
#State itemSize: CGFloat = 55.60
var body: some View {
HStack {
VStack {
Text("ItemSize: \(itemSize)")
Slider(value: $itemSize, in: 45...120)
}
}
.frame(width: 500, height: 500)
.magnificationZoomer(value: $itemSize, min: 45, max: 120)
}
But I have a few problems with this code:
It slide value by a strange way -- sometimes with correct speed, it does not change it
after some time of usage it stop to work at all
What did I do wrong?
It works almost like a magic, you can change the size/scale of Circle via Slider or your finger on MagnificationGesture, both working together and sinked together.
import SwiftUI
struct ContentView: View {
let minZoom: CGFloat = 0.5
let maxZoom: CGFloat = 1.5
#State private var scale: CGFloat = 1.0
#State private var lastScale: CGFloat = 1.0
#State private var magnitudeIsActive: Bool = Bool()
var body: some View {
ZStack {
Circle()
.fill(Color.red)
.scaleEffect(scale)
.gesture(magnificationGesture.simultaneously(with: dragGesture)) // <<: Here: adding unneeded dragGesture! on macOS! no need on iOS!
VStack {
Spacer()
Text("scale: " + scale.rounded)
HStack {
Button("min") { scale = minZoom; lastScale = scale }
Slider(value: Binding.init(get: { () -> CGFloat in return scale },
set: { (newValue) in if !magnitudeIsActive { scale = newValue; lastScale = newValue } }), in: minZoom...maxZoom)
Button("max") { scale = maxZoom; lastScale = scale }
}
}
}
.padding()
.compositingGroup()
.shadow(radius: 10.0)
.animation(Animation.easeInOut(duration: 0.2), value: scale)
}
var magnificationGesture: some Gesture {
MagnificationGesture(minimumScaleDelta: 0.0)
.onChanged { value in
if !magnitudeIsActive { magnitudeIsActive = true }
let magnification = (lastScale + value.magnitude - 1.0)
if (magnification >= minZoom && magnification <= maxZoom) {
scale = magnification
}
else if (magnification < minZoom) {
scale = minZoom
}
else if (magnification > maxZoom) {
scale = maxZoom
}
}
.onEnded { value in
let magnification = (lastScale + value.magnitude - 1.0)
if (magnification >= minZoom && magnification <= maxZoom) {
lastScale = magnification
}
else if (magnification < minZoom) {
lastScale = minZoom
}
else if (magnification > maxZoom) {
lastScale = maxZoom
}
scale = lastScale
magnitudeIsActive = false
}
}
var dragGesture: some Gesture { DragGesture(minimumDistance: 0.0) } // <<: Here: this Extra un-needed gesture would keep magnificationGesture alive! And Stop it to get killed in macOS! We do not need this line of code in iOS!
}
extension CGFloat { var rounded: String { get { return String(Double(self*100.0).rounded()/100.0) } } }

How to change color of a shape after a time x?

Sorry if it's a simple fix, I am a still bit new to swift/swiftui
I have been trying to accomplish a change of color after a second.
struct ContentView: View {
#State private var hasTimeElapsed = false
var colorCircle: [Color] = [.red, .green]
var body: some View {
Circle()
.fill(self.colorCircle.randomElement()!)
.frame(width: 100,
height: 100)
.onAppear(perform: delayCircle)
}
private func delayCircle() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.hasTimeElapsed = true
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
There's no need to use the hasTimeElapsed variable. Instead you can add a #State variable to store your current colour and set it in delayCircle():
struct ContentView: View {
var colorCircle: [Color] = [.red, .green]
#State var currentColor = Color.red
var body: some View {
Circle()
.fill(currentColor)
.frame(width: 100,
height: 100)
.onAppear(perform: delayCircle)
}
private func delayCircle() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.currentColor = self.colorCircle.randomElement()!
}
}
}
If you want to start with a random colour (instead of #State var currentColor = Color.red) you can set it in init:
#State var currentColor: Color
init() {
_currentColor = .init(initialValue: self.colorCircle.randomElement()!)
}
UPDATE due to OP's comment
If you want to set your circle colour in a loop you can use a Timer:
struct ContentView: View {
var colorCircle: [Color] = [.red, .green]
#State var currentColor: Color = .red
// firing every 1 second
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Circle()
.fill(currentColor)
.frame(width: 100,
height: 100)
Button("Stop timer") {
self.timer.upstream.connect().cancel() // <- stop the timer
}
}
.onReceive(timer) { _ in // <- when the timer fires perfom some action (here `randomizeCircleColor()`)
self.randomizeCircleColor()
}
}
private func randomizeCircleColor() {
self.currentColor = self.colorCircle.randomElement()!
}
}

Animate the width of a Rectangle over time

In SwiftUI on WatchOS, how can I animate the width of a Rectangle (or any View for that matter) so that it starts at a certain value and over a specified time animates to a different value?
Specifically, I want to animate a Rectangle to indicate the time left to the next full minute or the next 30 seconds after a minute.
All the examples I've seen are based on Timer.scheduledTimer firing at relatively high speed and setting a #State variable, but my understanding is that especially on WatchOS this should be avoided. Is there a better way?
This is the timer/state based code I have but I feel like there should be a more efficient way:
import SwiftUI
func percentage() -> CGFloat {
1 - CGFloat(fmod(Date().timeIntervalSince1970, 30) / 30)
}
struct ContentView: View {
#State var ratio: CGFloat = percentage()
let timer = Timer.publish(every: 1 / 60, on:.main, in:.common).autoconnect()
var body: some View {
GeometryReader { geometry in
ZStack {
Rectangle()
.foregroundColor(Color.gray)
.frame(width:geometry.size.width, height:5)
HStack {
Rectangle()
.foregroundColor(Color.red)
.frame(width:geometry.size.width * self.ratio, height:5)
Spacer()
}
}
}.onReceive(self.timer) { _ in
self.ratio = percentage()
}
}
}
I think a "more efficient way" to use animation:
struct AnimationRectangle: View {
struct AnimationRectangle: View {
#State private var percentage: CGFloat = 0.0
// count, how much time left to nearest 30 seconds
#State private var animationDuration = 30 - Double(fmod(Date().timeIntervalSince1970, 30))
private var repeatedAnimationFor30Seconds: Animation {
return Animation.easeInOut(duration: 30)
.repeatForever(autoreverses: false)
}
var body: some View {
VStack {
// just showing duration of current animation
Text("\(self.animationDuration)")
ZStack {
Rectangle()
.foregroundColor(.gray)
GeometryReader { geometry in
HStack {
Rectangle()
.foregroundColor(.green)
.frame(width: geometry.size.width * self.percentage)
Spacer()
}
}
}
.frame(height: 5)
.onAppear() {
// first animation without repeating
withAnimation(Animation.easeInOut(duration: self.animationDuration)) {
self.percentage = 1.0
}
// other repeated animations
DispatchQueue.main.asyncAfter(deadline: .now() + self.animationDuration) {
self.percentage = 0.0
self.animationDuration = 30.0
withAnimation(self.repeatedAnimationFor30Seconds) {
self.percentage = 1.0
}
}
}
}
}
}
struct AnimationRectangle_Previews: PreviewProvider {
static var previews: some View {
AnimationRectangle()
}
}

Resources