1

I am building a SwiftUI App and have all my UI related stuff within a global observable class UILogic. That class itself has a @Published variable called bp which is of type BoxParameters (struct).

My SwiftUI View observes this published variable, which has a lot of components: aspectRatio, frameWidth, xOffset, yOffset, etc. If I want my View to be wider for example, I just call the setWidth() function like this:

struct BoxParameters {
    private(set) var frameWidth: CGFloat = 175
    
    mutating func setWidth(newWidth: Double) {
         self.frameWidth = newWidth
    }
}
    
class UILogic: ObservableObject {
    @Published var bp = BoxParameters
    
    func doubleWidth() { 
        bp.setWidth(bp.frameWidth * 2) 
    } 
}

This works fine: because it’s mutating, it creates a new struct instance, which triggers @Published to send an update and the view changes with the new width.

What I'm struggling to do is to change the frameWidth (or any other struct variable) with a timer. So let’s say I don’t want to change the value instantly, but want to change it by incrementing the value 10 times every second.

My first guess was to use timer directly:

mutating func setWidth(newWidth: Double, slow: Bool = false) {
    if !slow {
        self.frameWidth = newWidth
    } else {        
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            self.frameWidth += 1
            if self.frameWidth >= newWidth  {
                self.frameWidth = newWidth
                timer.invalidate()
            }
        }
    }
}

This code doesn't compile and throws an error: Escaping closure captures mutating 'self' parameter

This has already made me scratch my head a bit, so I started digging around for solutions: https://stackoverflow.com/a/47173607/12596719\ https://developer.apple.com/forums/thread/652094\

Those two threads sparked a hope that my problem might be finally solved didn’t change anything: the compiler was still complaining.

What seemed to solve my problem was this thread, so I tried to adapt it in my code (just to test out it is a void function just increases the frameWidth by 50):

struct BoxParameters {
...
    var timerLogic: TimerLogic!

    class TimerLogic {
        var structRef: BoxParameters!
        var timer: Timer!

        init(_ structRef: BoxParameters){
            self.structRef = structRef;
            self.timer = Timer.scheduledTimer(
                timeInterval: 0.1,
                target: self,
                selector: #selector(timerTicked),
                userInfo: nil,
                repeats: true)
        }

        func stopTimer(){
            self.timer?.invalidate()
            self.structRef = nil
        }

        @objc private func timerTicked(){
            self.structRef.timerTicked()
        }
    }

    mutating func startTimer(){
        print("Start Timer")
        self.timerLogic = TimerLogic(self)
    }

    mutating func stopTimer() {
        print("Stop Timer")
        self.timerLogic.stopTimer()
        self.timerLogic = nil
    }
    
    mutating func timerTicked(){
        self.frameWidth += 50
        print("Timer: new frame width: \(self.frameWidth)")
    }
}

Expected behavior: it increased the frameWidth by 50
What happens: it prints that the frame width has been increased by 50 (printing value is correct), but nothing changes. BUT: if I call the function timerTicked manually, the frameWidth changes by 50 as expected! ugh!

What I think is happening is that the timer is changing the frameWidth of a copy of the struct without changing the real struct, but then again, the timerTicked function should change the parent struct itself. (because of self.)

Anyone knows a way to solve this issue? Changing the struct to an observed class would've been an option but due to Swift's design, a change of a @Published variable inside a @Published class doesn’t notify SwiftUI of a change...

VladislavS
  • 370
  • 2
  • 12
  • Using a mutating function is really fighting the immutable nature of structs. The function that "animates" your struct change should be outside it, in `UILogic`, for example. Even the name`UILogic`, while just a name, hints that you may need to rethink your use of the MVVM architecture. The fact that is a global, doubly so. – Paulw11 Dec 21 '21 at 19:53

1 Answers1

0

Why are you putting classes and any code logic in a struct? I think you need to work the logic into classes and just use the struct for simple variable usage.

A struct is better used to call variables around the app .

struct AllstructFittings {
    static var collectedWorks: Bool = false
    static var collected: String = "notcollected"
    static var failclicked: Bool = false
}

https://www.appypie.com/struct-vs-class-swift-how-to

Kingsley Mitchell
  • 2,412
  • 2
  • 18
  • 25