Can you please help updating the code to move the green image text "One thing is for sure ....." just above its original position only once the first animation has terminated ?
(for instance just below the "Toggle" text)
Thanks,
Olivier
Can you please help updating the code to move the green image text "One thing is for sure ....." just above its original position only once the first animation has terminated ?
(for instance just below the "Toggle" text)
import SwiftUI
struct ContentView : View {
// #EnvironmentObject var showBack: Bool
#State var showBack = false
var body : some View {
VStack() {
ContentViewTest()
Spacer()
Text(String(self.showBack))
}
}
}
struct ContentViewTest : View {
#State var showBack = false
let sample1 = "If you know you have an unpleasant nature and dislike people, this is no obstacle to work."
let sample2 = "One thing is for sure – a sheep is not a creature of the air."
var body : some View {
let front = CardFace(text: sample1, background: Color.yellow)
let back = CardFace(text: sample2, background: Color.green)
let resetBackButton = Button(action: { self.showBack = true }) { Text("Back")}.disabled(showBack == true)
let resetFrontButton = Button(action: { self.showBack = false }) { Text("Front")}.disabled(showBack == false)
let animatedToggle = Button(action: {
withAnimation(Animation.linear(duration: 0.8)) {
self.showBack.toggle()
}
}) { Text("Toggle")}
return
VStack() {
HStack() {
resetFrontButton
Spacer()
animatedToggle
Spacer()
resetBackButton
}.padding()
Spacer()
Spacer()
Spacer()
Spacer()
FlipView(front: front, back: back, showBack: $showBack)
}
}
}
struct FlipView<SomeTypeOfViewA : View, SomeTypeOfViewB : View> : View {
var front : SomeTypeOfViewA
var back : SomeTypeOfViewB
#State private var flipped = false
#Binding var showBack : Bool
var body: some View {
return VStack {
Spacer()
ZStack() {
front.opacity(flipped ? 0.0 : 1.0)
back.opacity(flipped ? 1.0 : 0.0)
}
.modifier(FlipEffect(flipped: $flipped, angle: showBack ? 180 : 0, axis: (x: 1, y: 0)))
.onTapGesture {
withAnimation(Animation.linear(duration: 0.8)) {
self.showBack.toggle()
}
}
Spacer()
}
}
}
struct CardFace<SomeTypeOfView : View> : View {
var text : String
var background: SomeTypeOfView
var body: some View {
Text(text)
.multilineTextAlignment(.center)
.padding(5).frame(width: 250, height: 150).background(background)
}
}
struct FlipEffect: GeometryEffect {
var animatableData: Double {
get { angle }
set { angle = newValue }
}
#Binding var flipped: Bool
var angle: Double
let axis: (x: CGFloat, y: CGFloat)
func effectValue(size: CGSize) -> ProjectionTransform {
DispatchQueue.main.async {
self.flipped = self.angle >= 90 && self.angle < 270
}
let tweakedAngle = flipped ? -180 + angle : angle
let a = CGFloat(Angle(degrees: tweakedAngle).radians)
var transform3d = CATransform3DIdentity;
transform3d.m34 = -1/max(size.width, size.height)
transform3d = CATransform3DRotate(transform3d, a, axis.x, axis.y, 0)
transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)
let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))
return ProjectionTransform(transform3d).concatenating(affineTransform)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Related
What I expect to happen
I have a meditation view that has an animation subview with a binding property inhaling that should appear when a button is pressed.
When the animation subview appears, it should start the animation from the beginning. It's the Apple meditation breathing animation basically: it starts as a small ball and gets bigger as inhaling is true, and then smaller as inhaling is false.
When the user presses the button again, the animation should disappear.
When the user then again presses the button, a second time, it should start the animation subview with a binding clean. Meaning the subview is a small ball and gets big again. Like the first time.
struct Meditation: View {
#State var startBreathingAnimation = false
#State var inhaling = false
#State var infoText = "Start a mediation"
var body: some View {
VStack(spacing: 20) {
ZStack {
if startBreathingAnimation {
BreathAnimation(inhaling: $inhaling)
.onChange(of: inhaling) { newValue in
if newValue {
infoText = "Breath in..."
} else {
infoText = "Breath out..."
} }
.onDisappear {
infoText = "Start your meditation" // Never executed?
}
} else {
Circle()
.frame(height: 100)
.foregroundColor(.blue)
}
}
Text(infoText)
Button("Toggle") {
startBreathingAnimation.toggle()
}
}
.padding()
}
}
What actually happens
The animation subview with a binding is not reset, newly initialized, but starts just where it left off after being "dismissed" with the button press.
When I don't add a binding property into the subview, it actually works as expected: it resets every time and gives me a "fresh" subview. But I do actually need to observe changes to the animation subview property inhaling in order to update the infoText property in the main view.
Reproducible example code, ready to copy into Xcode
Any help is greatly appreciated!
// Can be copied to Xcode directly
struct Meditation: View {
#State var startBreathingAnimation = false
#State var inhaling = false
#State var infoText = "Start a mediation"
var body: some View {
VStack(spacing: 20) {
ZStack {
if startBreathingAnimation {
BreathAnimation(inhaling: $inhaling)
.onChange(of: inhaling) { newValue in
if newValue {
infoText = "Breath in..."
} else {
infoText = "Breath out..."
} }
.onDisappear {
infoText = "Start your meditation" // Never executed?
}
} else {
Circle()
.frame(height: 100)
.foregroundColor(.blue)
}
}
Text(infoText)
Button("Toggle") {
startBreathingAnimation.toggle()
}
}
.padding()
}
}
private let gradientStart = Color.accentColor.opacity(0.9)
private let gradientEnd = Color.accentColor.opacity(1.0)
private let gradient = LinearGradient(gradient: Gradient(colors: [gradientStart, gradientEnd]), startPoint: .top, endPoint: .bottom)
private let maskGradient = LinearGradient(gradient: Gradient(colors: [.black]), startPoint: .top, endPoint: .bottom)
private let maxSize: CGFloat = 150
private let minSize: CGFloat = 30
private let inhaleTime: Double = 8
private let exhaleTime: Double = 8
private let pauseTime: Double = 1.5
private let numberOfPetals = 4
private let bigAngle = 360 / numberOfPetals
private let smallAngle = bigAngle / 2
private let ghostMaxSize: CGFloat = maxSize * 0.99
private let ghostMinSize: CGFloat = maxSize * 0.95
private struct Petals: View {
let size: CGFloat
let inhaling: Bool
var isMask = false
var body: some View {
let petalsGradient = isMask ? maskGradient : gradient
ZStack {
ForEach(0..<numberOfPetals) { index in
petalsGradient
.frame(maxWidth: .infinity, maxHeight: .infinity)
.mask(
Circle()
.frame(width: size, height: size)
.offset(x: inhaling ? size * 0.5 : 0)
.rotationEffect(.degrees(Double(bigAngle * index)))
)
.blendMode(isMask ? .normal : .screen)
}
}
}
}
struct BreathAnimation: View {
#State private var size = minSize
#Binding var inhaling: Bool
#State private var ghostSize = ghostMaxSize
#State private var ghostBlur: CGFloat = 0
#State private var ghostOpacity: Double = 0
var body: some View {
ZStack {
// Color.black
// .edgesIgnoringSafeArea(.all)
ZStack {
// ghosting for exhaling
Petals(size: ghostSize, inhaling: inhaling)
.blur(radius: ghostBlur)
.opacity(ghostOpacity)
// the mask is important, otherwise there is a color
// 'jump' when exhaling
Petals(size: size, inhaling: inhaling, isMask: true)
// overlapping petals
Petals(size: size, inhaling: inhaling)
Petals(size: size, inhaling: inhaling)
.rotationEffect(.degrees(Double(smallAngle)))
.opacity(inhaling ? 0.8 : 0.6)
}
.rotationEffect(.degrees(Double(inhaling ? bigAngle : -smallAngle)))
.drawingGroup()
}
.onAppear {
performAnimations()
}
.onDisappear {
size = minSize
inhaling = false
ghostSize = ghostMaxSize
ghostBlur = 0
ghostOpacity = 0
}
}
func performAnimations() {
withAnimation(.easeInOut(duration: inhaleTime)) {
inhaling = true
size = maxSize
}
Timer.scheduledTimer(withTimeInterval: inhaleTime + pauseTime, repeats: false) { _ in
ghostSize = ghostMaxSize
ghostBlur = 0
ghostOpacity = 0.8
Timer.scheduledTimer(withTimeInterval: exhaleTime * 0.2, repeats: false) { _ in
withAnimation(.easeOut(duration: exhaleTime * 0.6)) {
ghostBlur = 30
ghostOpacity = 0
}
}
withAnimation(.easeInOut(duration: exhaleTime)) {
inhaling = false
size = minSize
ghostSize = ghostMinSize
}
}
Timer.scheduledTimer(withTimeInterval: inhaleTime + pauseTime + exhaleTime + pauseTime, repeats: false) { _ in
// endless animation!
performAnimations()
}
}
private func performAnimations2() {
withAnimation(.easeInOut(duration: inhaleTime)) {
inhaling = true
size = maxSize
}
Timer.scheduledTimer(withTimeInterval: inhaleTime + pauseTime, repeats: false) { _ in
ghostSize = ghostMaxSize
ghostBlur = 0
ghostOpacity = 0.8
Timer.scheduledTimer(withTimeInterval: exhaleTime * 0.2, repeats: false) { _ in
withAnimation(.easeOut(duration: exhaleTime * 0.6)) {
ghostBlur = 30
ghostOpacity = 0
}
}
withAnimation(.easeInOut(duration: exhaleTime)) {
inhaling = false
size = minSize
ghostSize = ghostMinSize
}
}
Timer.scheduledTimer(withTimeInterval: inhaleTime + pauseTime + exhaleTime + pauseTime, repeats: false) { _ in
// endless animation!
performAnimations()
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
Meditation()
}
}
Here is a possible approach by setting a specific .id for the view and changing it on reset, forcing a redraw of the subview:
struct ContentView: View {
#State var startBreathingAnimation = false
#State var inhaling = false
#State var infoText = "Start a mediation"
#State var viewID = UUID()
var body: some View {
VStack(spacing: 20) {
ZStack {
if startBreathingAnimation {
BreathAnimation(inhaling: $inhaling)
.id(viewID) // here
.onChange(of: inhaling) { newValue in
if newValue {
infoText = "Breath in..."
} else {
infoText = "Breath out..."
}
}
} else {
Circle()
.frame(height: 100)
.foregroundColor(.blue)
}
}
Text(infoText)
Button("Toggle") {
if startBreathingAnimation {
startBreathingAnimation = false
infoText = "Start your meditation"
inhaling = false
} else {
startBreathingAnimation = true
viewID = UUID()
}
}
}
.padding()
}
}
As an extension to ChrisR's answer, which helped give me a fresh subview, but created the problem of out-of-sync animation property values, I used the help of PreferenceKeys. PreferenceKeys are apparently not that known among many intermediate SwiftUI devs, so I thought I'd share it here briefly.
Swiftful Thinking has a great video on them: link to video
A binding to a subview and its parent creates a way to strong connection for my case. I only want to observe the inhaling property of BreathAnimation() on my MainView().
That's when PreferenceKeys come into play.
Here is the code that helped me solve my issue.
Create a property that can be accessed from all views if needed:
struct InhalingPreferenceKey: PreferenceKey {
static var defaultValue: Bool = false
static func reduce(value: inout Bool, nextValue: () -> Bool) {
value = nextValue()
}
}
// Housekeeping that lets us update the preference key in our childview
extension View {
func updateInhalingPreferenceKey(_ isInhaling: Bool) -> some View {
preference(key: InhalingPreferenceKey.self, value: isInhaling)
}
}
Add this to the childview and connect it to the BreathAnimation property inhaling:
var body: some View {
VStack {
// Content of child view
}
.updateInhalingPreferenceKey(inhaling)
}
And finally, we can access the childview property by using this:
.onPreferenceChange(InhalingPreferenceKey.self, perform: { inhaling in
self.inhaling = inhaling
}) // self.inhaling is the parentview property
This together with ChrisR's solution for fresh child views helped me achieve what I wanted. Hope this might help someone else as well!
Tried several way to make it bubble effect from bottom to top moving.
I able to move it from bottom to top. but I can not make it like bubble effect.
#State private var bouncing = true
var body: some View {
Image("bubble").resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 40)
.frame(maxHeight: .infinity, alignment: bouncing ? .bottom : .top)
.animation(Animation.easeInOut(duration: 5.0).repeatForever(autoreverses: false))
.onAppear {
self.bouncing.toggle()
}
}
Here is simple bubble animation what I looking for.
import SwiftUI
struct MyParentView: View {
#State var replay: Bool = false
var body: some View {
ZStack{
Color.blue.opacity(0.8)
BubbleEffectView(replay: $replay)
VStack{
Spacer()
Button(action: {
replay.toggle()
}, label: {Text("replay")}).foregroundColor(.red)
}
}
}
}
struct BubbleEffectView: View {
#StateObject var viewModel: BubbleEffectViewModel = BubbleEffectViewModel()
#Binding var replay: Bool
var body: some View {
GeometryReader{ geo in
ZStack{
//Show bubble views for each bubble
ForEach(viewModel.bubbles){bubble in
BubbleView(bubble: bubble)
}
}.onChange(of: replay, perform: { _ in
viewModel.addBubbles(frameSize: geo.size)
})
.onAppear(){
//Set the initial position from frame size
viewModel.viewBottom = geo.size.height
viewModel.addBubbles(frameSize: geo.size)
}
}
}
}
class BubbleEffectViewModel: ObservableObject{
#Published var viewBottom: CGFloat = CGFloat.zero
#Published var bubbles: [BubbleViewModel] = []
private var timer: Timer?
private var timerCount: Int = 0
#Published var bubbleCount: Int = 50
func addBubbles(frameSize: CGSize){
let lifetime: TimeInterval = 2
//Start timer
timerCount = 0
if timer != nil{
timer?.invalidate()
}
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (timer) in
let bubble = BubbleViewModel(height: 10, width: 10, x: frameSize.width/2, y: self.viewBottom, color: .white, lifetime: lifetime)
//Add to array
self.bubbles.append(bubble)
//Get rid if the bubble at the end of its lifetime
Timer.scheduledTimer(withTimeInterval: bubble.lifetime, repeats: false, block: {_ in
self.bubbles.removeAll(where: {
$0.id == bubble.id
})
})
if self.timerCount >= self.bubbleCount {
//Stop when the bubbles will get cut off by screen
timer.invalidate()
self.timer = nil
}else{
self.timerCount += 1
}
}
}
}
struct BubbleView: View {
//If you want to change the bubble's variables you need to observe it
#ObservedObject var bubble: BubbleViewModel
#State var opacity: Double = 0
var body: some View {
Circle()
.foregroundColor(bubble.color)
.opacity(opacity)
.frame(width: bubble.width, height: bubble.height)
.position(x: bubble.x, y: bubble.y)
.onAppear {
withAnimation(.linear(duration: bubble.lifetime)){
//Go up
self.bubble.y = -bubble.height
//Go sideways
self.bubble.x += bubble.xFinalValue()
//Change size
let width = bubble.yFinalValue()
self.bubble.width = width
self.bubble.height = width
}
//Change the opacity faded to full to faded
//It is separate because it is half the duration
DispatchQueue.main.asyncAfter(deadline: .now()) {
withAnimation(.linear(duration: bubble.lifetime/2).repeatForever()) {
self.opacity = 1
}
}
DispatchQueue.main.asyncAfter(deadline: .now()) {
withAnimation(Animation.linear(duration: bubble.lifetime/4).repeatForever()) {
//Go sideways
//bubble.x += bubble.xFinalValue()
}
}
}
}
}
class BubbleViewModel: Identifiable, ObservableObject{
let id: UUID = UUID()
#Published var x: CGFloat
#Published var y: CGFloat
#Published var color: Color
#Published var width: CGFloat
#Published var height: CGFloat
#Published var lifetime: TimeInterval = 0
init(height: CGFloat, width: CGFloat, x: CGFloat, y: CGFloat, color: Color, lifetime: TimeInterval){
self.height = height
self.width = width
self.color = color
self.x = x
self.y = y
self.lifetime = lifetime
}
func xFinalValue() -> CGFloat {
return CGFloat.random(in:-width*CGFloat(lifetime*2.5)...width*CGFloat(lifetime*2.5))
}
func yFinalValue() -> CGFloat {
return CGFloat.random(in:0...width*CGFloat(lifetime*2.5))
}
}
struct MyParentView_Previews: PreviewProvider {
static var previews: some View {
MyParentView()
}
}
I am trying to animate individual items on mouseover. The issue I am having is that every item gets animated on mouseover of an item instead of just that specific item.
Here is what I have:
struct ContentView : View {
#State var hovered = false
var body: some View {
VStack(spacing: 90) {
ForEach(0..<2) {_ in
HStack(spacing: 90) {
ForEach(0..<4) {_ in
Circle().fill(Color.red).frame(width: 50, height: 50)
.scaleEffect(self.hovered ? 2.0 : 1.0)
.animation(.default)
.onHover { hover in
print("Mouse hover: \(hover)")
self.hovered.toggle()
}
}
}
}
}
.frame(minWidth:300,maxWidth:.infinity,minHeight:300,maxHeight:.infinity)
}
}
It needs to change onHover view on per-view base, ie. store some identifier of hovered view.
Here is possible solution. Tested with Xcode 11.4.
struct TestOnHoverInList : View {
#State var hovered: (Int, Int) = (-1, -1)
var body: some View {
VStack(spacing: 90) {
ForEach(0..<2) {i in
HStack(spacing: 90) {
ForEach(0..<4) {j in
Circle().fill(Color.red).frame(width: 50, height: 50)
.scaleEffect(self.hovered == (i,j) ? 2.0 : 1.0)
.animation(.default)
.onHover { hover in
print("Mouse hover: \(hover)")
if hover {
self.hovered = (i, j) // << here !!
} else {
self.hovered = (-1, -1) // reset
}
}
}
}
}
}
.frame(minWidth:300,maxWidth:.infinity,minHeight:300,maxHeight:.infinity)
}
}
Every item currently gets animated because they are all relying on hovered to see if the Circle is hovered over. To fix that, we can make every circle have their own hovered state.
struct CircleView: View {
#State var hovered = false
var body: some View {
Circle().fill(Color.red).frame(width: 50, height: 50)
.scaleEffect(self.hovered ? 2.0 : 1.0)
.animation(.default)
.onHover { hover in
print("Mouse hover: \(hover)")
self.hovered.toggle()
}
}
}
and in the ForEach we can just call the new CircleView where every Circle has their own source of truth.
struct ContentView : View {
var body: some View {
VStack(spacing: 90) {
ForEach(0..<2) { _ in
HStack(spacing: 90) {
ForEach(0..<4) { _ in
CircleView()
}
}
}
}
.frame(minWidth:300,maxWidth:.infinity,minHeight:300,maxHeight:.infinity)
}
}
Alternatively, you can create a modifier that allows you to change the View in question when it's hovered:
extension View {
func onHover<Content: View>(#ViewBuilder _ modify: #escaping (Self) -> Content) -> some View {
modifier(HoverModifier { modify(self) })
}
}
private struct HoverModifier<Result: View>: ViewModifier {
#ViewBuilder let modifier: () -> Result
#State private var isHovering = false
func body(content: Content) -> AnyView {
(isHovering ? modifier().eraseToAnyView() : content.eraseToAnyView())
.onHover { self.isHovering = $0 }
.eraseToAnyView()
}
}
Then each Circle on your example would go something like:
Circle().fill(Color.red).frame(width: 50, height: 50)
.animation(.default)
.onHover { view in
_ = print("Mouse hover")
view.scaleEffect(2.0)
}
I have text but it's not fit. I want use marquee when text not fit in my default frame.
Text(self.viewModel.soundTrack.title)
.font(.custom("Avenir Next Regular", size: 24))
.multilineTextAlignment(.trailing)
.lineLimit(1)
.foregroundColor(.white)
.fixedSize(horizontal: false, vertical: true)
//.frame(width: 200.0, height: 30.0)
Try below code....
In MarqueeText.swift
import SwiftUI
struct MarqueeText: View {
#State private var leftMost = false
#State private var w: CGFloat = 0
#State private var previousText: String = ""
#State private var contentViewWidth: CGFloat = 0
#State private var animationDuration: Double = 5
#Binding var text : String
var body: some View {
let baseAnimation = Animation.linear(duration: self.animationDuration)//Animation duration
let repeated = baseAnimation.repeatForever(autoreverses: false)
return VStack(alignment:.center, spacing: 0) {
GeometryReader { geometry in//geometry.size.width will provide container/superView width
Text(self.text).font(.system(size: 24)).lineLimit(1).foregroundColor(.clear).fixedSize(horizontal: true, vertical: false).background(TextGeometry()).onPreferenceChange(WidthPreferenceKey.self, perform: {
self.w = $0
print("textWidth:\(self.w)")
print("geometry:\(geometry.size.width)")
self.contentViewWidth = geometry.size.width
if self.text.count != self.previousText.count && self.contentViewWidth < self.w {
let duration = self.w/50
print("duration:\(duration)")
self.animationDuration = Double(duration)
self.leftMost = true
} else {
self.animationDuration = 0.0
}
self.previousText = self.text
}).fixedSize(horizontal: false, vertical: true)// This Text is temp, will not be displayed in UI. Used to identify the width of the text.
if self.animationDuration > 0.0 {
Text(self.text).font(.system(size: 24)).lineLimit(nil).foregroundColor(.green).fixedSize(horizontal: true, vertical: false).background(TextGeometry()).onPreferenceChange(WidthPreferenceKey.self, perform: { _ in
if self.text.count != self.previousText.count && self.contentViewWidth < self.w {
} else {
self.leftMost = false
}
self.previousText = self.text
}).modifier(self.makeSlidingEffect().ignoredByLayout()).animation(repeated, value: self.leftMost).clipped(antialiased: true).offset(y: -8)//Text with animation
}
else {
Text(self.text).font(.system(size: 24)).lineLimit(1).foregroundColor(.blue).fixedSize(horizontal: true, vertical: false).background(TextGeometry()).fixedSize(horizontal: false, vertical: true).frame(maxWidth: .infinity, alignment: .center).offset(y: -8)//Text without animation
}
}
}.fixedSize(horizontal: false, vertical: true).layoutPriority(1).frame(maxHeight: 50, alignment: .center).clipped()
}
func makeSlidingEffect() -> some GeometryEffect {
return SlidingEffect(
xPosition: self.leftMost ? -self.w : self.w,
yPosition: 0).ignoredByLayout()
}
}
struct MarqueeText_Previews: PreviewProvider {
#State static var myCoolText = "myCoolText"
static var previews: some View {
MarqueeText(text: $myCoolText)
}
}
struct SlidingEffect: GeometryEffect {
var xPosition: CGFloat = 0
var yPosition: CGFloat = 0
var animatableData: CGFloat {
get { return xPosition }
set { xPosition = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let pt = CGPoint(
x: xPosition,
y: yPosition)
return ProjectionTransform(CGAffineTransform(translationX: pt.x, y: pt.y)).inverted()
}
}
struct TextGeometry: View {
var body: some View {
GeometryReader { geometry in
return Rectangle().fill(Color.clear).preference(key: WidthPreferenceKey.self, value: geometry.size.width)
}
}
}
struct WidthPreferenceKey: PreferenceKey {
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
typealias Value = CGFloat
}
struct MagicStuff: ViewModifier {
func body(content: Content) -> some View {
Group {
content.alignmentGuide(.underlineLeading) { d in
return d[.leading]
}
}
}
}
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
In your existing SwiftUI struct.
(The below sample code will check 3 cases 1.Empty string, 2.Short string that doesn't need to marquee, 3.Lengthy marquee string)
#State var value = ""
#State var counter = 0
var body: some View {
VStack {
Spacer(minLength: 0)
Text("Monday").background(Color.yellow)
HStack {
Spacer()
VStack {
Text("One").background(Color.blue)
}
VStack {
MarqueeText(text: $value).background(Color.red).padding(.horizontal, 8).clipped()
}
VStack {
Text("Two").background(Color.green)
}
Spacer()
}
Text("Tuesday").background(Color.gray)
Spacer(minLength: 0)
Button(action: {
self.counter = self.counter + 1
if (self.counter % 2 == 0) {
self.value = "1Hello World! Hello World! Hello World! Hello World! Hello World!"
} else {
self.value = "1Hello World! Hello"
}
}) {
Text("Button")
}
Spacer()
}
}
Install https://github.com/SwiftUIKit/Marquee 0.2.0 above
with Swift Package Manager and try below code....
struct ContentView: View {
var body: some View {
Marquee {
Text("Hello World!")
.font(.system(size: 40))
}
// This is the key point.
.marqueeWhenNotFit(true)
}
}
When you keep increasing the length of the text until it exceeds the width of the marquee, the marquee animation will automatically start.
I was looking for the same thing, but every solution I tried either did not meet my specifications or caused layout/rendering issues, especially when the text changed or the parent view was refreshed. I ended up just writing something from scratch. It is quite hack-y, but it seems to be working now. I would welcome any suggestions on how it can be improved!
import SwiftUI
struct Marquee: View {
#ObservedObject var controller:MarqueeController
var body: some View {
VStack {
if controller.changing {
Text("")
.font(Font(controller.font))
} else {
if !controller.shouldAnimate {
Text(controller.text)
.font(Font(controller.font))
} else {
AnimatedText(controller: controller)
}
}
}
.onAppear() {
self.controller.checkForAnimation()
}
.onReceive(controller.$text) {_ in
self.controller.checkForAnimation()
}
}
}
struct AnimatedText: View {
#ObservedObject var controller:MarqueeController
var body: some View {
Text(controller.text)
.font(Font(controller.font))
.lineLimit(1)
.fixedSize()
.offset(x: controller.animate ? controller.initialOffset - controller.offset : controller.initialOffset)
.frame(width:controller.maxWidth)
.mask(Rectangle())
}
}
class MarqueeController:ObservableObject {
#Published var text:String
#Published var animate = false
#Published var changing = true
#Published var offset:CGFloat = 0
#Published var initialOffset:CGFloat = 0
var shouldAnimate:Bool {text.widthOfString(usingFont: font) > maxWidth}
let font:UIFont
var maxWidth:CGFloat
var textDoubled = false
let delay:Double
let duration:Double
init(text:String, maxWidth:CGFloat, font:UIFont = UIFont.systemFont(ofSize: 12), delay:Double = 1, duration:Double = 3) {
self.text = text
self.maxWidth = maxWidth
self.font = font
self.delay = delay
self.duration = duration
}
func checkForAnimation() {
if shouldAnimate {
let spacer = " "
if !textDoubled {
self.text += (spacer + self.text)
self.textDoubled = true
}
let textWidth = self.text.widthOfString(usingFont: font)
self.initialOffset = (textWidth - maxWidth) / 2
self.offset = (textWidth + spacer.widthOfString(usingFont: font)) / 2
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.changing = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
withAnimation(Animation.linear(duration:self.duration).delay(self.delay).repeatForever(autoreverses: false)) {
self.animate = self.shouldAnimate
}
}
}
}
}
struct Flashcard: View {
#State var tangoID = randomNum
#State var refreshToggle = false
#State var showingSheet = false
#State var bookmarked = tangoArray[randomNum].bookmark
#State public var showingNoMoreCardsSheet = false
#State private var showResults: Bool = false
#State private var fullRotation: Bool = false
var body: some View {
let zstack = ZStack {
Frontside(id: $tangoID, sheet: $showingSheet, rotate: $fullRotation)
.rotation3DEffect(.degrees(self.showResults ? 180.0 : 0.0), axis: (x: 0.0, y: 1.0, z: 0.0))
.rotation3DEffect(.degrees(self.fullRotation ? 360.0 : 0.0), axis: (x: 0.0, y: 1.0, z: 0.0))
.zIndex(self.showResults ? 0 : 1)
Backside(id: $tangoID, sheet: $showingSheet, bookmark: $bookmarked, results: $showResults, rotate: $fullRotation)
.rotation3DEffect(.degrees(self.showResults ? 0.0 : 180.0), axis: (x: 0.0, y: -1.0, z: 0.0))
.rotation3DEffect(.degrees(self.fullRotation ? 360.0 : 0.0), axis: (x: 0.0, y: 1.0, z: 0.0))
.zIndex(self.showResults ? 1 : 0)
}
.actionSheet(isPresented: $showingSheet) {
ActionSheet(title: Text("Finished 終わり"), message: Text("お疲れさま! But feel free to keep going."), buttons: [.default(Text("はい"))]);
}
.onTapGesture {
self.handleFlipViewTap()
}
.navigationBarTitle("Study")
.contextMenu(menuItems: {Button(action: {
tangoArray[randomNum].bookmark.toggle()
database.updateUserData(tango: tangoArray[randomNum])
self.fullRotation.toggle()
}, label: {
VStack{
Image(systemName: tangoArray[randomNum].bookmark ? "bookmark" : "bookmark.fill")
.font(.title)
Text(tangoArray[randomNum].bookmark ? "Remove bookmark" : "Bookmark")
}
})
})
return zstack
}
private func handleFlipViewTap() -> Void
{
withAnimation(.linear(duration: 0.25))
{
self.showResults.toggle()
}
}
}
public struct Frontside: View
{
#Binding public var id: Int
public var body: some View
{
ZStack{
RoundedRectangle(cornerRadius: 8, style: .continuous)
.frame(width: 140, height: 149)
.zIndex(0)
VStack {
Text(tangoArray[self.id].kanji)
.font(.system(size: 24))
.fontWeight(.regular)
.padding(25)
.lineLimit(3)
.zIndex(1)
Spacer()
}
VStack {
Spacer()
HStack {
Button(action: {
incorrect(i: self.id)
checkAttempts()
self.id = nextCard()
}) {
Image(systemName: "xmark")
.font(.headline)
.opacity(0.4)
}
Button(action: {
correct(i: self.id)
checkAttempts()
self.id = nextCard()
}) {
Image(systemName: "circle")
.font(.headline)
.opacity(0.4)
}
}
}
.zIndex(2)
}
}
}
I have a view which is a flashcard. When the user taps on the incorrect button I want the flash card to slide to the left of the screen, and when the user taps on the correct button I want the flash card to transition/slide to the right of the watch screen. How do I do that?
When the user taps on the incorrect button I want the flash card to
slide to the left of the screen, and when the user taps on the correct
button I want the flash card to transition/slide to the right of the
watch screen
I created a minimum viable example to show you a possible solution. Take a look and tell me if I can help you more.
struct ContentView: View {
private let objWidth = CGFloat(100)
private let objHeight = CGFloat(200)
private let screenWidth = UIScreen.main.bounds.size.width;
private let screenHeight = UIScreen.main.bounds.size.height;
#State private var objOffset = CGFloat(50)
var body: some View {
VStack {
Rectangle()
.frame(width: objWidth, height: objHeight)
.background(Color.black)
.position(x: objOffset, y: (screenHeight-objHeight)/2.0)
Button(action: {
withAnimation{
self.move()
}
}) {
Text("TAP")
}
}
}
private func move() {
if objOffset > screenWidth/2.0 {
objOffset = objWidth/2.0
} else {
objOffset = screenWidth-objWidth/2.0
}
}
}