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

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

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

Fading Text ViewModifier to simulate truncationMode .byWordWrapping and apply fading mask

I am trying to create a ViewModifier called .fade that can be used on SwiftUI.Text and simulates a fading effect with a truncation mode of .byWordWrapping known from UIKit. In addition I apply a fading mask to the last line of the Text View.
In my example code for a static View everything works as expected, but for some dynamic changes the modifier creates a wrong layout or does not update properly.
My actual problems are:
changing the lineLimit dynamically
Everything works if going from a larger lineLimit to a smaller one, but the other way going from smaller to larger values is not working
Changing the size of the parent Stack does not resize the view if the view is already faded. Meaning the view exceeds the parent bounds of width: 300 (Can be simulated in the TestView by showing the image while a text is faded.)
Helper ViewModifer to determine the size of a view:
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
public extension View {
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
The FadingViewModifer itself:
private struct FadingTextViewModifier: ViewModifier {
#State private var intrinsicSize = CGSize.zero
#State private var truncatedSize = CGSize.zero
#State private var isTruncated = false
let lineLimit: Int?
let font: UIFont
let useMaxWidth: Bool
let alignment: Alignment
let isTruncatedAction: ((Bool) -> Void)?
var maxWidth: CGFloat? { useMaxWidth ? .infinity : nil }
func body(content: Content) -> some View {
let truncatedView = content
.font(Font(font))
.lineLimit(lineLimit)
.readSize { size in
truncatedSize = size
isTruncated = truncatedSize != intrinsicSize
isTruncatedAction?(isTruncated)
}
let intrinsicView = content
.font(Font(font))
.fixedSize(horizontal: false, vertical: true)
.readSize { size in
intrinsicSize = size
isTruncated = truncatedSize != intrinsicSize
isTruncatedAction?(isTruncated)
}
let view = content
.font(Font(font))
.frame(maxWidth: maxWidth, alignment: alignment)
.background(truncatedView.hidden(), alignment: alignment)
.background(intrinsicView.hidden(), alignment: alignment)
Group {
if isTruncated {
content
.font(Font(font))
.fixedSize(horizontal: lineLimit == 1, vertical: true)
.frame(width: truncatedSize.width, height: truncatedSize.height, alignment: alignment)
.frame(maxWidth: maxWidth, alignment: alignment)
.clipped()
.mask(maskView)
.background(Color.yellow)
.background(view.hidden())
} else {
view
}
}
.background(Color.purple)
}
}
Extension to create the maskView that is used for the fading:
private extension FadingTextViewModifier {
var ellipsisWidth: CGFloat { lineLimit == 1 ? 0 : 15 }
var fadeOutWidth: CGFloat { 50 }
var maskStart: UnitPoint {
let width = truncatedSize.width
let visibleWidth = width - ellipsisWidth - fadeOutWidth
let startX: CGFloat = width > 0 && visibleWidth > 0 ? visibleWidth / width : 0.8
return .init(x: startX, y: 0.5)
}
var maskEnd: UnitPoint {
let width = truncatedSize.width
let endX: CGFloat = width > 0 && width > ellipsisWidth ? (width - ellipsisWidth) / width : 1
return .init(x: endX, y: 0.5)
}
// Only apllied to the last line.
var maskView: some View {
VStack(spacing: 0) {
Rectangle()
LinearGradient(colors: [.white, .clear], startPoint: maskStart, endPoint: maskEnd)
.frame(height: font.lineHeight)
}
}
}
The actual ViewModifier extension:
extension Text {
/// Fades the `Text` if needed.
/// - Parameters:
/// - lineLimit: Defines the lineLimit of your text. Modifies by default the lineLimit to 1 instead of nil
/// - font: Needs an UIFont to determine the lineHeight for the masking effect. Directly applies the font to the Text.
/// - useMaxWidth: Only relevant for lineLimit of 1. By default the text view uses maxWidth .infinity to avoid a fading effect, although it would look like space is available.
/// - alignment: Specifies the text alignment inside the frame
/// - isTruncatedAction: Returns if the text is truncated or not. Use this to customize your view. e.g. a "show more"-button
func fade(lineLimit: Int? = 1,
font: UIFont = UIFont.systemFont(ofSize: 16),
useMaxWidth: Bool = true,
alignment: Alignment = .topLeading,
isTruncatedAction: ((Bool) -> Void)? = nil) -> some View {
modifier(FadingTextViewModifier(lineLimit: lineLimit,
font: font,
useMaxWidth: useMaxWidth,
alignment: alignment,
isTruncatedAction: isTruncatedAction))
}
}
A testView to simulate the problems:
struct FadingTextTest: View {
#State var withImage = false
#State var useLongText = false
#State var lineLimit = 1
let shortText = "This is a short text!"
let longText = "This is a much much much much much much much much much much longer text!"
var text: String {
useLongText ? longText : shortText
}
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Button("activate image") { withImage.toggle() }
Button("use long text") { useLongText.toggle() }
Stepper("Line Limit \(lineLimit)", value: $lineLimit)
.padding(.bottom, 50)
// Single Fading Text that does not need to adapt to size changes of the parent dynamically
Text(text)
.fade(lineLimit: lineLimit)
// Text that needs to adapt dynamically to width changes inside a Stack
HStack(spacing: 0) {
Text(text)
.fade(lineLimit: lineLimit)
if withImage {
Image(systemName: "star")
.resizable()
.frame(width: 20, height: 20)
}
}
// Text that fades at intrinsic width
Text(text)
.fade(lineLimit: lineLimit, useMaxWidth: false)
// Text without fading, but with lineLimit
Text(text)
.font(Font(UIFont.systemFont(ofSize: 16)))
.lineLimit(lineLimit)
.frame(maxWidth: .infinity, alignment: .topLeading)
// Text without any effect
Text(text)
.font(Font(UIFont.systemFont(ofSize: 16)))
.frame(maxWidth: .infinity, alignment: .topLeading)
Spacer()
}
.frame(maxHeight: .infinity)
.background(Color.green)
.frame(width: 300)
}
}

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

SwiftUI view animates at unexpected path

I made the following PulsatingView:
struct PulsatingView: View {
#State private var opacity = 0.7
#State private var scale: CGFloat = 0.5
var body: some View {
VStack {
ZStack {
Circle()
.fill(Color.black.opacity(opacity))
.animation(.easeInOut(duration: 2).delay(1.5).repeatForever(autoreverses: false))
.frame(width: 7, height: 7)
.scaleEffect(scale)
.animation(.easeIn(duration: 2.5).delay(1).repeatForever(autoreverses: false))
Circle()
.fill(Color.black)
.frame(width: 7, height: 7)
}
}
.onAppear {
opacity = 0
scale = 5
}
}
}
It produces the following result: (click to see animation; it's a .gif)
However, when I add it to a view, inside a VStack for example, this happens: (click to see animation; it's a .gif)
I have no idea why this happens or how I can fix it. I've already tried adding modifiers such as .fixedSize(horizontal: true, vertical: true) or .frame(width: 50, height: 50) to it but that doesn't help.
Does anyone know what's going on?
Try using animation with joined value, like
Circle()
.fill(Color.black.opacity(opacity))
.animation(.easeInOut(duration: 2).delay(1.5).repeatForever(autoreverses: false), value: opacity) // << here !!
.frame(width: 7, height: 7)
.scaleEffect(scale)
.animation(.easeIn(duration: 2.5).delay(1).repeatForever(autoreverses: false), value: scale) // << here !!
Important: if it is started in NavigationView then animatable state should be activated postponed. The reason and investigation details was provided in https://stackoverflow.com/a/66643096/12299030.
This issue is not about your code it is because NavigationView to fix it we should use DispatchQueue.main.async I wanted refactor your code as well, hope help you, also you can avoid hard coded value as much as possible, for example you can apply the frame in outside of view:
struct ContentView: View {
var body: some View {
CircleView(lineWidth: 0.5, color: .red, scale: 5.0)
.frame(width: 7, height: 7)
}
}
struct CircleView: View {
let lineWidth: CGFloat
let color: Color
let scale: CGFloat
#State private var startAnimation: Bool = Bool()
var body: some View {
Circle()
.strokeBorder(style: .init(lineWidth: startAnimation ? .zero : lineWidth)).opacity(startAnimation ? .zero : 1.0)
.scaleEffect(startAnimation ? scale : 1.0)
.overlay( Circle() )
.foregroundColor(color)
.onAppear() { DispatchQueue.main.async { startAnimation.toggle() } }
.animation(.easeIn(duration: 2.5).delay(1).repeatForever(autoreverses: false), value: startAnimation)
}
}
I finally managed to find a solution to this on my own project - I found the only solution to get the animated view working within a NavigationView was to move the animation inside the onAppear call, like this:
Circle()
.fill(Color.black.opacity(opacity))
.onAppear {
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: false)) {
opacity = 0
}
}
Hope this can help someone else if they get stuck on the same issue I ended up having!

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