Access a changing environment variable from within a scheduled timer closure

I have a timer program that works. It includes code that sets an environment boolean to true or false based on whether the timers window is the key window or not. What I would like to do is pause the timer when the boolean is false and restart it from where it was paused when the boolean is true. The print statement above the button always prints out the current value (true or false). The print statement inside the timer closure always prints true. I think if I could bind/tie the environment variable isKeyWindow to a @State variable I could solve my problem but am unable to figure out how to do so. Or maybe. there is a better way. Below is my code.

struct ContentView: View {
    @Environment(\.isKeyWindow) var isKeyWindow: Bool
    @State private var timerCount: Int = 0
    @State private var trimProgress: CGFloat = 0.0
    @State private var percentProgress: CGFloat = 0.0
    var body: some View {
        ZStack {
            Rectangle()
                .fill(Color.clear)
                .frame(width: 600, height: 400)
            Circle()
                .stroke(lineWidth: 15.0)
                .opacity(0.2)
                .foregroundColor(Color.gray)
                .frame(width: 200, height: 200)
                .rotationEffect(Angle(degrees: 270.0))
            Text(String(format: "%.0f%%", min(self.percentProgress,1) * 100))
                .font(.system(size: 50))
            Circle()
                .trim(from: 0, to: self.trimProgress)
                .stroke(style: StrokeStyle(lineWidth: 15.0, lineCap: .round, lineJoin: .round))
                .foregroundColor(Color.blue)
                .frame(width: 200, height: 200)
                .rotationEffect(Angle(degrees: 270.0))
                .animation(Animation.easeInOut)
            let _ = print("\(isKeyWindow)")
            Button("Download JSON Files") {
                Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { timer in
                    let _ = print("In timer \(isKeyWindow)")
                    self.timerCount += 1
                    if self.timerCount <= 20 {
                        self.trimProgress += 0.05
                    }
                    if self.timerCount >= 2 {
                        self.percentProgress += 0.05
                    }
                    if self.timerCount > 20 {
                        timer.invalidate()
                    }
                } // end timer
            }.offset(y: 150) // end button
        }.padding()  // end zstack
    }  // end var body
}  // end content view

See How to run some code when state changes using onChange() - a free SwiftUI by Example tutorial

Peter, once again you are on the money. I added onChange and in addition to get the functionality I wanted I removed the button and went to a different timer. Instead of pausing the timer I just ignore it when the timer window is not the key window. One odd thing. To make things work properly I had to set keyWindow to false when isKeyWindow is true in the onChange logic. I would have thought you would set keyWindow to true when isKeyWindow is true.
Below is my updated code.
Thanks again.

struct ContentView: View {
    
    @Environment(\.isKeyWindow) var isKeyWindow: Bool
    
    @State private var timerCount: Int = 0
    @State private var trimProgress: CGFloat = 0.0
    @State private var percentProgress: CGFloat = 0.0
    @State private var keyWindow: Bool = true
    
    let timer = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
    
    var body: some View {
        ZStack {
            Rectangle()
                .fill(Color.clear)
                .frame(width: 600, height: 400)
            Circle()
                .stroke(lineWidth: 15.0)
                .opacity(0.2)
                .foregroundColor(Color.gray)
                .frame(width: 200, height: 200)
                .rotationEffect(Angle(degrees: 270.0))
            Text(String(format: "%.0f%%", min(self.percentProgress,1) * 100))
                .font(.system(size: 50))
            Circle()
                .trim(from: 0, to: self.trimProgress)
                .stroke(style: StrokeStyle(lineWidth: 15.0, lineCap: .round, lineJoin: .round))
                .foregroundColor(Color.blue)
                .frame(width: 200, height: 200)
                .rotationEffect(Angle(degrees: 270.0))
                .animation(Animation.easeInOut)
            
                .onChange(of: isKeyWindow) { _ in
                    if isKeyWindow == true {
                        keyWindow = false
                    } else {
                        keyWindow = true
                    }
                } // end on change
            
                .onReceive(timer)  { time in
                    if keyWindow == true {
                        self.timerCount += 1
                        if self.timerCount <= 20 {
                            self.trimProgress += 0.05
                        }
                        if self.timerCount >= 2 {
                            self.percentProgress += 0.05
                        }
                        if self.timerCount > 20 {
                            self.timer.upstream.connect().cancel()
                        }
                    } // end of main if &&
                } // end of on receive
        } .padding()  // end zstack
    }  // end var body
}  // end content view


This just makes no sense. Remove this code. If @Environment(\.isKeyWindow) is true when it should be false and vice versa, then change the code for the environment value. You need to post the code for this custom environment value.

Below is the code. Thank you for taking a look.

typealias Window = NSWindow

struct HostingWindowFinder: NSViewRepresentable {
    var callback: (Window?) -> ()

    func makeNSView(context: Self.Context) -> NSView {
        let view = NSView()
        view.translatesAutoresizingMaskIntoConstraints = false
        DispatchQueue.main.async { [weak view] in
            self.callback(view?.window)
        }
        return view
    }
    func updateNSView(_ nsView: NSView, context: Context) {}
}

class WindowObserver: ObservableObject {
    
    @Published
    public private(set) var isKeyWindow: Bool = false
    
    private var becomeKeyobserver: NSObjectProtocol?
    private var resignKeyobserver: NSObjectProtocol?

    weak var window: Window? {
        didSet {
            self.isKeyWindow = window?.isKeyWindow ?? false
            guard let window = window else {
                self.becomeKeyobserver = nil
                self.resignKeyobserver = nil
                return
            }
            
            self.becomeKeyobserver = NotificationCenter.default.addObserver(
                forName: Window.didBecomeKeyNotification,
                object: window,
                queue: .main
            ) { (n) in
                self.isKeyWindow = true
            }
            
            self.resignKeyobserver = NotificationCenter.default.addObserver(
                forName: Window.didResignKeyNotification,
                object: window,
                queue: .main
            ) { (n) in
                self.isKeyWindow = false
            }
        }
    }
}

extension EnvironmentValues {
    struct IsKeyWindowKey: EnvironmentKey {
        static var defaultValue: Bool = false
        typealias Value = Bool
    }
    
    fileprivate(set) var isKeyWindow: Bool {
        get {
            self[IsKeyWindowKey.self]
        }
        set {
            self[IsKeyWindowKey.self] = newValue
        }
    }
}

struct WindowObservationModifier: ViewModifier {
    @StateObject
    var windowObserver: WindowObserver = WindowObserver()
    
    func body(content: Content) -> some View {
        content.background(
            HostingWindowFinder { [weak windowObserver] window in
                windowObserver?.window = window
            }
        ).environment(\.isKeyWindow, windowObserver.isKeyWindow)
    }
}

I tested the your code and it works just fine without the onChange modifier. In the .onReceive(timer) closure, use isKeyWindow instead and get rid of the keyWindow property entirely.

I have made your suggested changes to the code and it works perfectly. Thank you for your perseverance.

Terms of Service

Privacy Policy

Cookie Policy