3

I want to make Text View with dots that increase and decrease over time.

Now it looks like that: enter image description here

but it looks twitchy and smudged. And also the text itself is shifted every time. How to get rid of it?

Here is my code:

struct TextViewTest: View {
@State var dotsSwitcher = 0
var body: some View {
    Text("Loading\(dots)")
        .animation(.easeOut(duration: 0.1), value: dotsSwitcher)
        .onReceive(Timer.publish(every: 1, on: .main, in: .common)
            .autoconnect()) { _ in dotsAnimation() }
        .onAppear(perform: dotsAnimation)
}

var dots: String {
    switch dotsSwitcher {
    case 1: return "."
    case 2: return ".."
    case 3: return "..."
    default: return ""
    }
}

func dotsAnimation() {
    withAnimation {
        dotsSwitcher = 0
    }
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
        withAnimation {
            dotsSwitcher = 1
        }
    }
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
        withAnimation {
            dotsSwitcher = 2
        }
    }
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
        withAnimation {
            dotsSwitcher = 3
        }
    }
}

}

Dobry Siabar
  • 53
  • 1
  • 3

3 Answers3

1

Try sliding transition on text.

Also, you can do it without using GCD, here is a complete solution for text based animated loading indicator with some options:

struct AnimatedTextLoadingIndicatorView: View {
    @State private var text: String = ""
    @State private var dots: String = ""
    private let now: Date = .now
    private static let every: TimeInterval = 0.2
    private let timer = Timer.publish(every: Self.every, on: .main, in: .common).autoconnect()
    private let characters = Array("⣾⣽⣻⢿⡿⣟⣯⣷")

    var body: some View {
        VStack {
            Text(text)
                .font(.largeTitle)
                .transition(.slide)
            Text("Loading" + dots)
                .font(.footnote)
                .transition(.slide)
        }
        .onReceive(timer) { time in
            let tick: Int = .init(time.timeIntervalSince(now) / Self.every) - 1
            let index: Int = tick % characters.count
            text = "\(characters[index])"
            let count: Int = tick % 4
            dots = String(Array(repeating: ".", count: count))
        }
        .onDisappear {
            timer.upstream.connect().cancel()
        }
    }
}
Max Potapov
  • 1,267
  • 11
  • 17
1

I modified the view so that the number of dots does not change, I only change their transparency

struct LoadingText: View {
    var text: String
    var color: Color = .black

    @State var dotsCount = 0

    var body: some View {
        HStack(alignment: .bottom, spacing: 0) {
            Text(text)
                .foregroundColor(color) +
            Text(".")
                .foregroundColor(dotsCount > 0 ? color : .clear) +
            Text(".")
                .foregroundColor(dotsCount > 1 ? color : .clear) +
            Text(".")
                .foregroundColor(dotsCount > 2 ? color : .clear)
        }
        .animation(.easeOut(duration: 0.2), value: dotsCount)
        .onReceive(Timer.publish(every: 1, on: .main, in: .common)
            .autoconnect()) { _ in dotsAnimation() }
        .onAppear(perform: dotsAnimation)
    }

    func dotsAnimation() {
        withAnimation {
            dotsCount = 0
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            withAnimation {
                dotsCount = 1
            }
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
            withAnimation {
                dotsCount = 2
            }
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.9) {
            withAnimation {
                dotsCount = 3
            }
        }
    }
}
0

I agree with @MaxPotapov solution and have one additional code:

  Text("Loading")
                    .font(.headline)
                    .fontWeight(.heavy)
                    .foregroundColor(.theme.launchAccent)
                    .overlay {
                        GeometryReader { geo in
                            Text(dots)
                                .font(.headline)
                                .fontWeight(.heavy)
                                .foregroundColor(.theme.launchAccent)
                                .offset(x: geo.size.width + 2)
                        }
                    }

This approach allows you to control the size of Text("Loading") every time and add centered Text(dots) without a dynamic offset that looks like a BUG when adding dots.

PAULMAX
  • 87
  • 1
  • 11