Last active
September 30, 2023 05:58
-
-
Save jamesrochabrun/523a0ee19bfd2eb4854bb9748a697053 to your computer and use it in GitHub Desktop.
An animated Trailing text
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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() | |
} |
Author
jamesrochabrun
commented
Sep 30, 2023
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment