Skip to content

Instantly share code, notes, and snippets.

@jamesrochabrun
Last active September 30, 2023 05:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamesrochabrun/523a0ee19bfd2eb4854bb9748a697053 to your computer and use it in GitHub Desktop.
Save jamesrochabrun/523a0ee19bfd2eb4854bb9748a697053 to your computer and use it in GitHub Desktop.
An animated Trailing text
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
struct CenteredLoadingText: View {
private let mainText: String
private let dots = [".", ".", "."]
private let repeatInterval: TimeInterval
private let accessibilityLabel: String
private let loadingAnimation: Animation
private let spacing: CGFloat
/// https://www.hackingwithswift.com/books/ios-swiftui/triggering-events-repeatedly-using-a-timer
private let timer: Publishers.Autoconnect<Timer.TimerPublisher>
@State private var trailingText = ""
@State private var currentIndex = 0
init(
centerText: String,
accessibilityLabel: String,
repeatInterval: Double = 0.5,
loadingAnimation: Animation = Animation.default,
spacing: CGFloat = 0)
{
self.mainText = centerText
self.accessibilityLabel = accessibilityLabel
self.repeatInterval = repeatInterval
self.loadingAnimation = loadingAnimation
self.spacing = spacing
timer = Timer.publish(every: repeatInterval, on: .main, in: .default).autoconnect()
}
var body: some View {
FirstChildrenCenteredLayout(spacing: spacing) {
Text(mainText)
.border(.black)
Text(trailingText)
.border(.red)
}
.onReceive(timer) { _ in
withAnimation(loadingAnimation) {
if currentIndex < dots.count {
trailingText += dots[currentIndex]
currentIndex += 1
} else {
currentIndex = 0
trailingText = ""
}
}
}
.onDisappear {
timer.upstream.connect().cancel()
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(accessibilityLabel)
}
private struct FirstChildrenCenteredLayout: Layout {
// MARK: Lifecycle
init(spacing: CGFloat) {
self.spacing = spacing
}
// MARK: Public
public func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache _: inout Void)
-> CGSize
{
// Return a size.
assert(subviews.count == 2)
let width = proposal.width ?? 0
// We assume texts has the same Font and number of lines is 1, so the first height should be enough.
let height = subviews.first?.sizeThatFits(proposal).height ?? 0
return CGSize(width: width, height: height)
}
public func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache _: inout Void)
{
// Place child views.
assert(subviews.count == 2)
let height = subviews.first?.sizeThatFits(proposal).height ?? 0
let combinedWidth = subviews.map { $0.sizeThatFits(proposal) }.reduce(0.0) { (result, size) in
return result + size.width
}
let placementProposal = ProposedViewSize(width: combinedWidth, height: height)
let firstSubView = subviews[0]
let firstSubViewSize = firstSubView.sizeThatFits(proposal)
let firstSubViewPoint = CGPoint(x: bounds.midX, y: bounds.midY)
firstSubView.place(at: firstSubViewPoint, anchor: .center, proposal: placementProposal)
let secondSubView = subviews[1]
let secondSubViewPoint = CGPoint(x: firstSubViewPoint.x + firstSubViewSize.width / 2 + spacing, y: bounds.midY - firstSubView.sizeThatFits(proposal).height / 2)
secondSubView.place(at: secondSubViewPoint, proposal: placementProposal)
}
// MARK: Private
private let spacing: CGFloat
}
}
struct ContentView: View {
var body: some View {
CenteredLoadingText(
centerText: "Loading",
accessibilityLabel: "Loading")
}
}
#Preview {
ContentView()
}
@jamesrochabrun
Copy link
Author

Simulator Screen Recording - iPhone 15 Pro - 2023-09-29 at 22 33 15

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment