I'm working on a search feature on a macOS SwiftUI app where I'm attempting to display a ProgressView() indicator while the searching algorithm is working. I am able to see the the spinner, but I cannot change the color of it.
Everywhere I have looked online, people have said to use the .tint(:) modifier on it, but that doesn't do anything, no matter what I set the tint to, the color remains the default white/grey. I've also tried changing the .foregroundStyle(:), the .accentColor(:) even though its deprecated, and using CircularProgressViewStyle(tint: .black). The only solution I've found is setting .preferredColorScheme(:) to .light, but doing that changes the color theme of the whole app, and when the Progress bar appears, the whole turns a little brighter, before dimming back to normal once the progress bar disappears. It baffles me how SwiftUI is developed by Apple but there doesn't seem to be a straightforward way of changing something as simple as the color of an indicator. If anyone has any suggestions that would be amazing. I'm on macOS Ventura 13.5 with a build target of macOS 12.4.
import SwiftUI
import AppKit
import Combine
struct SearchBar: View {
@ObservedObject private var windowState = WindowState.shared
@ObservedObject private var windowSize = WindowSize.shared
@Binding var showSearchBar: Bool
@State private var showSearchBarStroke: Bool = false
@StateObject private var searchText = DebouncedState(initialValue: "")
@FocusState var isFocused: Bool
@State private var isSearching: Bool = false
var body: some View {
HStack(spacing: 10) {
if isSearching {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .black))
.tint(.black)
.scaleEffect(0.75)
}
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(.clear)
.frame(width: showSearchBar ? windowSize.width * 0.5 : 27, height: 27)
.overlay(
RoundedRectangle(cornerRadius: 15, style: .continuous)
.stroke(showSearchBarStroke ? windowState.textTheme : .clear, lineWidth: 2)
)
TextField("", text: $searchText.currentValue)
.textFieldStyle(PlainTextFieldStyle())
.focused($isFocused)
.background(.clear)
.padding(.leading, 10)
.frame(width: showSearchBar ? (windowSize.width * 0.5) - 27 : 0, height: 30)
}
}
.onChange(of: showSearchBar) { newValue in
isFocused = newValue
if newValue {
showSearchBarStroke = true
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
showSearchBarStroke = false
}
}
}
.onChange(of: searchText.debouncedValue) { newValue in
performSearch(for: newValue)
}
.onChange(of: windowState.currentSpace) { _ in
searchText.currentValue = ""
showSearchBar = false
}
}
private func performSearch(for query: String) {
DispatchQueue.global(qos: .userInitiated).async {
let searchTokens: [String] = searchText.debouncedValue.components(separatedBy: " ").filter { !$0.isEmpty }
if searchTokens.isEmpty {
DispatchQueue.main.async {
windowState.searchResults = []
}
} else {
DispatchQueue.main.async {
windowState.searchResults = []
}
Forms.search(searchText: searchTokens, $isSearching)
}
}
}
}
private class DebouncedState<Value>: ObservableObject {
@Published var currentValue: Value
@Published var debouncedValue: Value
init(initialValue: Value, delay: Double = 0.3) {
_currentValue = Published(initialValue: initialValue)
_debouncedValue = Published(initialValue: initialValue)
$currentValue
.debounce(for: .seconds(delay), scheduler: RunLoop.main)
.assign(to: &$debouncedValue)
}
}
Since the progress spinner varies in opacity along its circle, you can use it as the mask of a Color, e.g.
Color.black.mask {
ProgressView()
}
Note that since this is a Color, it will expand to fill all the available space. You might want to use this as an overlay of another invisible ProgressView instead:
ProgressView()
.opacity(0)
.overlay {
Color.black.mask {
ProgressView()
}
}
This works reasonably well with .black, but other colors like .yellow looks quite bad.
You can also wrap your own NSProgressIndicator, and change its tint like this,
struct AppKitProgressView: NSViewRepresentable {
let tint: Color
@Environment(\.self) var env
func makeNSView(context: Context) -> NSProgressIndicator {
let v = NSProgressIndicator()
v.isIndeterminate = true
v.style = .spinning
v.startAnimation(nil)
return v
}
func updateNSView(_ nsView: NSProgressIndicator, context: Context) {
let color: CIColor
if #available(macOS 14, *) {
let colorResolved = tint.resolve(in: env)
color = CIColor(
red: CGFloat(colorResolved.red),
green: CGFloat(colorResolved.green),
blue: CGFloat(colorResolved.blue),
alpha: CGFloat(colorResolved.opacity)
)
} else if let cgColor = tint.cgColor {
color = CIColor(cgColor: cgColor)
} else {
color = CIColor(red: 1, green: 1, blue: 1)
}
let colorFilter = CIFilter(name: "CIFalseColor", parameters: [
"inputColor0": color,
"inputColor1": color
])!
nsView.contentFilters = [colorFilter]
}
}
// Usage:
AppKitProgressView(tint: .black)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With