civil-and-structural-engineering
Creating a Dynamic News Ticker with Swiftui and Combine
Table of Contents
Introduction: Why a Dynamic News Ticker Matters
A dynamic news ticker is one of those subtle UI touches that can dramatically improve user engagement. Whether you are building a financial app, a live event tracker, or a social feed, the ability to glide breaking headlines across the screen keeps users informed without demanding their full attention. With SwiftUI and Combine, iOS developers can craft a performant, reactive ticker that updates seamlessly and feels native. In this article we will expand a basic ticker implementation into a production-ready component covering data flow, animation, error handling, and performance tuning.
Understanding SwiftUI and Combine
SwiftUI provides a declarative syntax that lets you describe what your interface should do, while Combine handles asynchronous events like timer firing, network responses, and data streams. Together they form a powerful pair: SwiftUI automatically redraws views when @Published properties change, and Combine supplies publishers that feed those changes. This eliminates manual wiring and reduces boilerplate. For a news ticker, Combine can efficiently manage periodic polling, network requests, and reactive updates without blocking the main thread.
It is important to understand that Combine is not just for networking. It excels at composing multiple streams – for example merging a timer stream with a user‑refresh action – and controlling backpressure. We will leverage its operators later to avoid common pitfalls like duplicate requests or stale data.
Designing the News Ticker
A well‑designed ticker has three layers: a data model, a service that publishes news items over time, and a view that animates the presentation. The data layer should be agnostic to how items are fetched (local cache, network, or mock). The Combine service acts as the bridge, transforming raw events into a stream of NewsItem objects. The view then observes this stream and renders the ticker.
Step 1: A Robust Data Model
Beyond a simple title, real tickers require metadata. A NewsItem should include a unique identifier, source, timestamp, and an optional URL for more detail. Using Identifiable is necessary for ForEach, and Hashable helps with diffing in animations.
struct NewsItem: Identifiable, Hashable {
let id = UUID()
let title: String
let source: String
let timestamp: Date
let url: URL?
init(title: String, source: String = "NewsAPI", url: URL? = nil) {
self.title = title
self.source = source
self.timestamp = Date()
self.url = url
}
}
Step 2: Fetching News with Combine – Beyond the Basics
The original example used a simple Timer.publish. In production you will need to combine a timer with an actual network call. Use URLSession.dataTaskPublisher to fetch from a REST endpoint, then map the response into an array of NewsItem. Wrap the publisher in a class that conforms to ObservableObject.
import Combine
import Foundation
class NewsViewModel: ObservableObject {
@Published var items: [NewsItem] = []
@Published var error: Error?
private var cancellables = Set<AnyCancellable>()
private let baseURL = URL(string: "https://api.example.com/headlines")!
init() {
startPolling(every: 30) // seconds
}
func startPolling(every interval: TimeInterval) {
Timer.publish(every: interval, on: .main, in: .default)
.autoconnect()
.prepend(Date()) // fetch immediately on init
.flatMap { _ in
URLSession.shared.dataTaskPublisher(for: self.baseURL)
.map(\.data)
.decode(type: [NewsItem].self, decoder: JSONDecoder())
.catch { [weak self] error -> Just<[NewsItem]> in
print("Fetch failed: \(error.localizedDescription)")
return Just([])
}
}
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
// Handle terminal error if needed
}, receiveValue: { [weak self] newItems in
self?.items = newItems
})
.store(in: &cancellables)
}
deinit {
cancellables.removeAll()
}
}
Notice the use of .flatMap to switch from the timer to the network publisher. The .prepend(Date()) ensures the first fetch happens as soon as the view appears. Error handling is done with .catch – returning an empty array prevents the ticker from stopping on transient failures.
Step 3: Advanced Combine Operators for a Smoother Ticker
In a real app you might want to .removeDuplicates on the timer stream to avoid identical network calls, or use .debounce if the user can manually refresh. You can also throttle the network publisher to ensure you never hammer the API faster than once per 10 seconds.
Timer.publish(every: interval, on: .main, in: .default)
.autoconnect()
.removeDuplicates()
.throttle(for: .seconds(10), scheduler: DispatchQueue.main, latest: true)
.flatMap { _ in ... }
This ensures that even if the timer fires earlier (due to manual refresh or app state restoration), the network call is spaced out.
Building the News Ticker View
The original view used a simple ScrollView with a horizontal HStack. For a true ticker effect you want continuous auto‑scrolling. The user should see items glide left (or right) smoothly, pause on tap, and resume. We will implement that with a ScrollViewReader and a custom ScrollView driven by an Animation publisher.
Horizontal Auto‑Scrolling Ticker
Here is an enhanced version that uses a background timer to update an offset, combined with ScrollViewReader to snap to the latest item when new news arrives. The auto‑scroll runs at a constant speed regardless of item count.
import SwiftUI
struct NewsTickerView: View {
@StateObject private var viewModel = NewsViewModel()
@State private var scrollOffset: CGFloat = 0
@State private var isPaused = false
let tickerSpeed: CGFloat = 50 // points per second
var body: some View {
GeometryReader { geo in
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(viewModel.items) { item in
Text("\(item.source): \(item.title)")
.lineLimit(1)
.font(.subheadline)
.id(item.id)
}
}
.offset(x: scrollOffset)
}
.frame(height: 44)
.background(Color(.secondarySystemBackground))
.onTapGesture {
withAnimation { isPaused.toggle() }
}
.onAppear {
startAutoScroll(in: geo.size.width)
}
.onChange(of: viewModel.items.count) { _ in
// Shift scroll to see new items if needed
withAnimation {
proxy.scrollTo(viewModel.items.last?.id, anchor: .trailing)
}
}
}
}
}
private func startAutoScroll(in viewWidth: CGFloat) {
Timer.scheduledTimer(withTimeInterval: 1/60, repeats: true) { timer in
guard !isPaused else { return }
var offset = scrollOffset - tickerSpeed / 60
// Reset when all items have scrolled past
let totalWidth = CGFloat(viewModel.items.count) * 200 // approximate
if offset < -totalWidth - viewWidth {
offset = 0
}
scrollOffset = offset
}
}
}
For production, consider using CADisplayLink wrapped in a Combine publisher for precise frame timing. The above timer runs at 60 fps and decrements the offset. When the offset reaches a certain threshold, we reset it to create an infinite loop effect.
Vertical Ticker Alternative
Some apps prefer a vertical ticker that scrolls upwards like a stock market feed. That is achieved by swapping the axis and using VStack with a similar offset mechanic. You can even combine both directions depending on the content.
Handling User Interaction
Users often want to tap a headline to open the full story. Attach a TapGesture to each Text that calls UIApplication.shared.open(item.url) if the URL is valid. Ensure that tapping does not conflict with the pause gesture – a good pattern is to disable scroll‑pause when tapping an item.
Text(item.title)
.onTapGesture {
guard let url = item.url else { return }
UIApplication.shared.open(url)
}
Performance Considerations
Running a timer at 60 fps and redrawing the ticker can be expensive if you hold many items. Here are key optimisations:
- Limit item count: Keep only the last 20‑30 items in the array. Older items can be discarded or cached separately.
- Lazy loading previews: If items contain images, load them asynchronously and display a placeholder until ready.
- Use
LazyHStackorLazyVStack: Even though the ticker is small, lazy stacks prevent SwiftUI from creating views for items that are not in the visible area. - Avoid
@ObservedObjectin deep subviews: If the ticker is part of a larger view, consider using a single@StateObjectat the top and pass only the required array. - Profile with Instruments: Watch for excessive time‑updates or large diff calculations. Use the SwiftUI Instruments template to detect re‑renders.
Error Handling and State Management
Network errors, empty responses, and slow connections must be surfaced gracefully. The NewsViewModel should expose an error property (already @Published). The view can show a warning banner (red ticker) or fall back to cached data.
struct ErrorBanner: View {
let message: String
var body: some View {
Text("⚠️ \(message)")
.foregroundColor(.white)
.padding(8)
.background(Color.red)
.cornerRadius(8)
}
}
// In NewsTickerView:
if let error = viewModel.error {
ErrorBanner(message: error.localizedDescription)
}
Also, implement a manual refresh button: viewModel.startPolling(every: ...) can be called again, but use a CurrentValueSubject to signal a refresh event and combine it with the timer.
Customization and Theming
A production ticker should respect Dark Mode, dynamic type, and app theming. Expose properties like background color, text style, scroll speed, and pause behavior via a configuration struct. This makes the component reusable across different apps or screens.
struct TickerConfiguration {
var backgroundColor: Color = .secondarySystemBackground
var textColor: Color = .primary
var scrollSpeed: CGFloat = 50
var font: Font = .subheadline
var showsError: Bool = true
}
Inject it via environment or as a parameter. This also simplifies testing.
Conclusion
Building a dynamic news ticker with SwiftUI and Combine goes beyond a simple timer and scroll view. By leveraging Combine’s operators for networking, error handling, and throttling, you create a robust data pipeline. The ticker view can be made smooth with offset‑based animation and proper state management. Remember to profile performance, handle errors gracefully, and design for configurability. The result is a production‑ready component that keeps users engaged without overwhelming them.
For further reading, explore the official Combine documentation and the SwiftUI ScrollView guide. A practical example using Timer.publish is shown in Apple’s animating state article. Additionally, the Combine: Asynchronous Programming with Swift book offers deep insights into reactive patterns.