Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Changing tint color on ProgressView in SwiftUI (macOS) does nothing

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)
    }
}
like image 845
Matias Carulli Avatar asked Oct 28 '25 20:10

Matias Carulli


1 Answers

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)
like image 140
Sweeper Avatar answered Oct 31 '25 12:10

Sweeper