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...
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
Related
Normally, I would use an optional variable to hold my Timer reference, as it's nice to be able to invalidate and set it to nil before recreating.
I'm trying to use SwiftUI and want to make sure I'm correctly doing so...
I declare as:
#State var timer:Publishers.Autoconnect<Timer.TimerPublisher>? = nil
Later I:
self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
To drive a UI text control I use:
.onReceive(timer) { time in
print("The time is now \(time)")
}
What is the right way with this Combine typed Timer to invalidate and recreate?
I've read one should call:
self.timer.upstream.connect().cancel()
However, do I also need to invalidate or simply then nil out?
There is no need to throw away the TimerPublisher itself. Timer.publish creates a Timer.TimerPublisher instance, which like all other publishers, only starts emitting values when you create a subscription to it - and it stops emitting as soon as the subscription is closed.
So instead of recreating the TimerPublisher, you just need to recreate the subscription to it - when the need arises.
So assign the Timer.publish on declaration, but don't autoconnect() it. Whenever you want to start the timer, call connect on it and save the Cancellable in an instance property. Then whenever you want to stop the timer, call cancel on the Cancellable and set it to nil.
You can find below a fully working view with a preview that starts the timer after 5 seconds, updates the view every second and stops streaming after 30 seconds.
This can be improved further by storing the publisher and the subscription on a view model and just injecting that into the view.
struct TimerView: View {
#State private var text: String = "Not started"
private var timerSubscription: Cancellable?
private let timer = Timer.publish(every: 1, on: .main, in: .common)
var body: some View {
Text(text)
.onReceive(timer) {
self.text = "The time is now \($0)"
}
}
mutating func startTimer() {
timerSubscription = timer.connect()
}
mutating func stopTimer() {
timerSubscription?.cancel()
timerSubscription = nil
}
}
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
var timerView = TimerView()
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
timerView.startTimer()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
timerView.stopTimer()
}
return timerView
}
}
With a view model, you don't even need to expose a TimerPublisher (or any Publisher) to the view, but can simply update an #Published property and display that in the body of your view. This enables you to declare timer as autoconnect, which means you don't manually need to call cancel on it, you can simply nil out the subscription reference to stop the timer.
class TimerViewModel: ObservableObject {
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
private var timerSubscription: Cancellable?
#Published var time: Date = Date()
func startTimer() {
timerSubscription = timer.assign(to: \.time, on: self)
}
func stopTimer() {
timerSubscription = nil
}
}
struct TimerView: View {
#ObservedObject var viewModel: TimerViewModel
var body: some View {
Text(viewModel.time.description)
}
}
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
let viewModel = TimerViewModel()
let timerView = TimerView(viewModel: viewModel)
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
viewModel.startTimer()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
viewModel.stopTimer()
}
return timerView
}
}
I've just started with Swift and using MVVM with dependency injection.
In my ViewModel I have Timer that handles refreshing the data. I've simplified the code a little for clarity.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = ViewModel()
}
}
class ViewModel: NSObject {
private var timer: Timer?
override init() {
super.init()
setUpTimer()
}
func setUpTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true){_ in
self.refreshData()
}
}
func refreshData() {
//refresh data
print("refresh data")
}
}
I want to use dependency injection to pass the Timer into the ViewModel so that I can control the timer when doing unit tests and make it call immediately.
So passing the Timer is pretty simple. How can I pass a Timer in to ViewModel that has the ability to call the refreshData() belonging to ViewModel. Is there a trick in Swift that allows this?
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true){_ in
// call refreshData() from the class ViewModel
}
var viewModel = ViewModel(myTimer:timer)
}
}
class ViewModel: NSObject {
private var timer: Timer?
init(myTimer:Timer) {
super.init()
//setUpTimer()
timer = myTimer
}
func refreshData() {
//refresh data
print("refresh data")
}
}
I thought it might be possible using the scheduelTimer that takes a selector instead of a block but that would require using a #objc before the func refreshData() which seems clunky since I am using an Objective C feature in Swift.
Is there a nice way to achieve this?
Many Thanks,
Code
Conceptually, you want to decouple the implementation. So instead of having to pass Timer to the view model, you pass some other "control" object, which guarantees to perform the operation (of calling back after a delay)
If that doesn't shout protocol, I don't know what does...
typealias Ticker = () -> Void
protocol Refresher {
var isRunning: Bool { get }
func register(_ ticker: #escaping Ticker)
func start();
func stop();
}
So, pretty basic concept. It can start, stop and an observer can register itself to it and be notified when a "tick" occurs. The observer doesn't care "how" it works, so long as it guarantees to perform the specified operation.
A Timer based implementation then might look something like...
class TimerRefresher: Refresher {
private var timer: Timer? = nil
private var ticker: Ticker? = nil
var isRunning: Bool = false
func register(_ ticker: #escaping Ticker) {
self.ticker = ticker
guard timer == nil else {
return
}
}
func start() {
guard ticker != nil else {
return
}
stop()
isRunning = true
timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true, block: { (timer) in
self.tick()
})
}
func stop() {
guard let timer = timer else {
return
}
isRunning = false
timer.invalidate()
self.timer = nil
}
private func tick() {
guard let ticker = ticker else {
stop()
return
}
ticker()
}
}
This provides you the entry point for mocking the dependency injection, by replacing the implementation of the Refresher with one you can control manually (or use a different "delaying" action, depending on your needs)
This is just a conceptual example, your implementation/needs may differ and lead you to a slightly different design, but the idea remains the same, decouple the physical implementation in some way.
An alternative would require you to rethink your design, and instead of the view model performing it's own refresh, the view/controller would take over that responsibility instead. Since that's a significant design decision, you're really only the person who can make that decision, but it's another idea
If I understand you correctly, you want the model to refresh every 30 seconds when running in the app, but faster for test. If so, don't inject the Timer. Inject the refresh frequency.
class ViewModel: NSObject {
// We need something to observe and confirm that the data is fresh
#objc dynamic var lastRefreshed: Date?
private var timer: Timer!
// The default frequency is 30 seconds but users can adjust that
// The unit test uses it to inject dependency
init(refreshFrequency: TimeInterval = 30) {
super.init()
timer = Timer.scheduledTimer(timeInterval: refreshFrequency, target: self, selector: #selector(refreshData), userInfo: nil, repeats: true)
}
#objc func refreshData() {
lastRefreshed = Date()
print("refreshed on: \(lastRefreshed!)")
}
}
And your unit test:
func testModel() {
let startTime = Date()
let model = ViewModel(refreshFrequency: 5)
// Test first refresh: must be within 5 - 6 seconds from startTime
keyValueObservingExpectation(for: model, keyPath: #keyPath(ViewModel.lastRefreshed)) { (_, _) -> Bool in
if let duration = model.lastRefreshed?.timeIntervalSince(startTime), 5...6 ~= duration {
return true
} else {
return false
}
}
// Test second refresh: must be within 10 - 12 seconds from startTime
keyValueObservingExpectation(for: model, keyPath: #keyPath(ViewModel.lastRefreshed)) { (_, _) -> Bool in
if let duration = model.lastRefreshed?.timeIntervalSince(startTime), 10...12 ~= duration {
return true
} else {
return false
}
}
// Wait 12 seconds for both expectations to be fulfilled
waitForExpectations(timeout: 12, handler: nil)
}
Timer is not exact: it does not fire exactly every 5 seconds like you asked. Apple say Timer is accurate to about 50 - 100ms. Hence we cannot expect that the first refresh will happen 5 seconds from now. We must allow for some tolerances. The further out you go, the bigger this tolerance have to become.
Seemingly simple but I'm struggling...the code below crashes on the line setting the date of the workoutTimer. Also my WKInterfaceTimer isn't hooked up to an IBOutlet, does it need to be? I wanted to use it just for purposes of the time.
class InterfaceController {
var workoutTimer: WKInterfaceTimer!
var workoutStartTime: NSDate? = nil
func startWorkOutTimer() {
self.startWorkout()
if let test = self.workoutSecondsElapsed() {
print("timer seconds = \(test)")
}
}
func startWorkout() {
// To count up use 0.0 or less, otherwise the timer counts down.
workoutTimer.setDate(NSDate(timeIntervalSinceNow: 0.0) as Date)
workoutTimer.start()
self.workoutStartTime = NSDate()
}
func stopWorkout() {
workoutTimer.stop()
}
func workoutSecondsElapsed() -> TimeInterval? {
// If the timer hasn't been started then return nil
guard let startTime = self.workoutStartTime else {
return nil
}
// Time intervals from past dates are negative, so
// multiply by -1 to get the elapsed time.
return -1.0 * (self.workoutStartTime?.timeIntervalSinceNow)!
}
}
From Apple doc:
Do not subclass or create instances of this class yourself. Instead, define outlets in your interface controller class and connect them to the corresponding objects in your storyboard file.
Your app probably is crashing because your timer is nil, but for what you need you can use Timer class instead of WKInterfaceTimer.
All singleton functions are twice calls.
The "Prova" function, and the selector of the timer are twice calls.
class Timer {
static let sharedInstanceTimer = Timer()
var TimerCounter : NSTimer = NSTimer()
var Counter : Int = Int()
var TimerGameOver : Int = Int()
var TimerBonusMultipleCircle : Int = Int()
var TimerBonusBigCircle : Int = Int()
var TimerCounterInterval : NSTimeInterval = 1
init()
{
self.Counter = 60
self.TimerGameOver = 10
self.TimerBonusMultipleCircle = 5
self.TimerBonusBigCircle = 5
self.TimerCounterInterval = 1
}
func StartTimerCounter()
{
self.TimerCounter = NSTimer.scheduledTimerWithTimeInterval(self.TimerCounterInterval, target: Game.sharedInstanceGame, selector: #selector(Game.sharedInstanceGame.UpdateAllCounter), userInfo: nil, repeats: true)
}
func StopTimerCounter()
{
self.TimerCounter.invalidate()
}
}
And... In another file I call StartTimerCounter()
import UIKit
class FirstViewController: UIViewController {
static let sharedInstanceFirstViewController = FirstViewController()
override func viewDidLoad() {
super.viewDidLoad()
let backButton = UIBarButtonItem(title: "00:00:60", style: UIBarButtonItemStyle.Plain, target: self, action: nil)
backButton.tintColor = UIColor.whiteColor()
navigationItem.leftBarButtonItem = backButton
Circle.sharedInstanceCircle.CreateCircle()
view.layer.addSublayer(Circle.sharedInstanceCircle.PrincipalCircle)
Game.sharedInstanceGame.Play()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
func ReturnToMenu()
{
navigationController?.popViewControllerAnimated(true)
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
Leaving aside the various errors that I corrected as suggested below I have tried several solutions but I can not find the solution to this problem.
A couple of thoughts:
Your sharedInstanceTimer is instantiating Timer (which is, itself, a NSTimer). It should not subclass NSTimer.
You then initialize TimerCounter to a second timer which you never use.
The above will instantiate two timers. You then have StartTimerCounter that instantiates another NSTimer every time you call it. Assuming you want only one timer, you should should have StartTimerCounter invalidate any prior timer before starting a new one.
I'd call this class TimerManager, or something like that, to avoid clashes with Swift 3 type, Timer.
Method names should start with lowercase letters, as should properties. In the spirit of the Swift 3 API guidelines, you might want to shorten the method names, too.
Also, if you're going to define a singleton, I'd declare a private init() initializer, to prevent other classes from accidentally ever instantiating another TimerManager object.
So, that yields something like:
class TimerManager {
static let shared = TimerManager()
private var timer: NSTimer? // this certainly should be private
// whether you make these private or not, or constants vs properties, is up to you
private var tounter : Int = 60
private var timerGameOver : Int = 10
private var timerBonusMultipleCircle : Int = 5
private var timerBonusBigCircle : Int = 5
private var timerCounterInterval : NSTimeInterval = 1
// make sure no one else accidentally instantiates another TimerManager
private init() {
}
/// Start timer.
func startTimer() {
stopTimer() // cancel prior timer, if any
timer = NSTimer.scheduledTimerWithTimeInterval(timerCounterInterval, target: Game.sharedInstanceGame, selector: #selector(Game.sharedInstanceGame.updateAllCounter), userInfo: nil, repeats: true)
Game.sharedInstanceGame.prova()
}
/// Stop timer.
func stopTimer() {
timer?.invalidate()
timer = nil
}
}
An unrelated, deeper observation here: I'd suggest that you keep the timer object loosely coupled with respect to the Game object. So, I'd excise all "game" related stuff from this class:
class TimerManager {
static let shared = TimerManager()
private var timer: NSTimer? // this certainly should be private
private var timerHandler: (() -> ())?
// whether you make these private or not, or constants vs properties, is up to you
private var timerCounterInterval: NSTimeInterval = 1
// make sure no one else accidentally instantiates another TimerManager
private init() {
}
/// Start timer.
///
/// - parameter timerHandler: The closure that will be called once per second.
func startTimer(timerHandler: () -> ()) {
stopTimer() // cancel prior timer, if any
self.timerHandler = timerHandler
timer = NSTimer.scheduledTimerWithTimeInterval(timerCounterInterval, target: self, selector: #selector(handleTimer(_:)), userInfo: nil, repeats: true)
}
#objc func handleTimer(timer: NSTimer) {
timerHandler?()
}
/// Stop timer.
func stopTimer() {
timer?.invalidate()
timerHandler = nil
timer = nil
}
}
Then, when the Game object wants to start a timer, it might do:
TimerManager.shared.startTimer {
self.updateAllCounter()
}
prova()
Now, perhaps you simplified your timer object for the purpose of this question and perhaps there's more that's needed in this TimerManager object (as suggested by all these other properties that aren't otherwise referenced in your code snippet), but hopefully this illustrates the basic idea: The TimerManager shouldn't be involved in the business of calling any specific Game methods or the like. It should simply provide a mechanism by which the caller can simply supply a block of code that the timer should periodically invoke.
EDIT: I'm an idiot. I was trying to put the variable into NSTimer() and not NSTimeInterval for whatever silly reason. I guess my question is how can I wrap this all up in a separate class?
Ideally, I'd like all this wrapped up in a separate class (CountdownTimer), so I can create new instance of a timer but still retain all the functionality that NSTimer includes such as the ability to check timer.isValid. Psuedocode would look something like:
var timer = CountdownTimer(countDownFrom: 300)
timer.start()
timer.isValid()
My UIViewController class (not in viewDidLoad):
var totalCountDownTimeInterval = NSTimeInterval(480.0)
var startTime = NSDate()
var timer = NSTimer()
var isRunning = false
func updateTime() {
var elapsedTime : NSTimeInterval = NSDate().timeIntervalSinceDate(startTime)
var remainingTime : NSTimeInterval = totalCountDownTimeInterval - elapsedTime
if remainingTime <= 0.0 {
timer.invalidate()
}
let minutes = UInt8(remainingTime / 60.0)
remainingTime = remainingTime - (NSTimeInterval(minutes) * 60)
let seconds = UInt8(remainingTime)
println("The time is \(minutes) and \(seconds)")
}
#IBOutlet weak var TimerCount: UILabel!
#IBAction func StartButton(sender: AnyObject) {
if !timer.valid {
startTime = NSDate()
let aSelector : Selector = "updateTime"
timer = NSTimer.scheduledTimerWithTimeInterval(0.10, target: self, selector: aSelector, userInfo: nil, repeats: true)
}
}
#IBAction func StopButton(sender: AnyObject) {
timer.invalidate()
}
#IBAction func ResetButton(sender: AnyObject) {
timer.invalidate()
TimerCount.text = "00:00"
}
Make sure that you're actually passing in a double. I like to explicitly state the type of my variables when I declare them; it helps to avoid problems just like this.
You're most likely declaring doubleValue like this:
let doubleValue = 480
instead of like this:
let doubleValue = 480.0
or like this:
let doubleValue: Double = 480
If you've declared your variable correctly, this should work:
let timeInterval = NSTimeInterval(doubleValue)
If you are going to let the compiler infer the variable's type, just make sure that whatever's on the right side of your assignment operator evaluates to the type you're looking for. 480 evaluates to Int(480) while 480.0 evaluates to Double(480).
EDIT: Here's the answer to your second question: How can I wrap this [timer functionality] up in a separate class?
It's actually really simple. Assuming that all you want to do with the class is to be able to start it and check if it's still valid, here's how I would go about doing this:
class CountdownTimer
{
var time: NSTimeInterval
private var startTime: NSDate?
init(countDownFrom timeInSeconds: Int)
{
time = NSTimeInterval(timeInSeconds)
}
func start()
{
startTime = NSDate()
}
func isValid() -> Bool
{
if (startTime != nil)
{
let timePassed: NSTimeInterval = -(startTime!.timeIntervalSinceNow)
return timePassed < time
}
else
{
return false
}
}
}
Now, be warned, I barely tested this. Playground isn't complaining and from the looks of it, this should work. Now, just use the class like so:
var myCountdownTimer = CountdownTimer(countDownFrom: 300)
// and then whenever we want to start the countdown:
myCountdownTimer.start()
// and then whenever we want to check if the clock's still "ticking", so to speak:
myCountdownTimer.isValid()
// and if we want to restart the timer:
myCountdownTimer.time = NSTimeInterval(900) // we can change the time if we want
myCountdownTimer.start()
Essentially, all a CountdownTimer object does is save the exact time start() is called to a variable called startTime. Note that NSDate() by default is set to the time-and-date it's created. Then isValid() simply checks to see if the timePassed is less than whatever time the timer was set to count down from; if it is, then it returns true.
I tried this code on playground of Xcode 6.1, And It worked fine.
That's strange...
let someValue: Double = 60.0
var timeInterval = NSTimeInterval(someValue)
println(timeInterval)