Swift "Reference to property ... closure requires explicit use of 'self' to make capture semantics explicit" - swift

I'm coming from Python and JavaScript background, and I'm struggling to understand why I'm getting this error and how to fix it. "Reference to property 'eggMessage' in closure requires explicit use of 'self' to make capture semantics explicit". I'm just trying to update the text of the eggMessage label with the current countdown. (Swift5, xcode 13)
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var eggMessage: UILabel!
var softTime = 5
var mediumTime = 7
var hardTime = 12
func updateMessage(text: String) {
eggMessage.text = text
}
func createTimer(timeLeft: Int) {
var countdown = timeLeft
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) {
[weak self] timer in
DispatchQueue.main.async { [weak self] in
if countdown > 0 {
print(countdown, "<--- TIME LEFT")
eggMessage.text = String(countdown) // <---- ERROR!!
updateMessage(text: String(countdown)) // <---- ERROR!!
countdown -= 1
} else {
timer.invalidate()
}
}
}
DispatchQueue.main.asyncAfter(deadline: .now(), execute: {
timer.fire()
})
}
#IBAction func eggSelection(_ sender: UIButton) {
let hardness: String = sender.currentTitle!
switch hardness {
case "Soft":
print(softTime)
createTimer(timeLeft: softTime)
case "Medium":
print(mediumTime)
case "Hard":
print(hardTime)
default:
print("pass")
}
}
}

The error is due to the fact that your closure's lifetime is not tied to the lifetime of your ViewController. You need to tell the compiler the relationship you desire for the two lifetimes. In general, use [weak self] and self?.foo; this tells the compiler that you want the ViewController to be able to destruct at will (like when the app terminates), and you want the closure not to crash your app if it runs after the ViewController has destructed.
To get a solid grasp of the issue, read about retain cycles in the Swift Book.

Related

Async data loading swift

I got a function such as scrollViewDidScroll that can trigger many times. And I need to call function loadMoreDataFromRemoteServerIfNeed only single time. How could I do this more elegantly without using any "flag" variables. Maybe I should use DispathGroup|DispatchWorkItem?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if yOffset > offset {
loadMoreDataFromRemoteServerIfNeed()
}
}
func loadMoreDataFromRemoteServerIfNeed() {
DispatchQueue.global(qos: .background).async {
sleep(2)
DispatchQueue.main.async {
// <Insert New Data>
}
}
}
The thing that you are trying to describe — "Do this, but only if you are not told to do it again any time in the next 2 seconds" — has a name. It's called debouncing. This is a well-solved problem in iOS programming, so now that you know its name, you can do a search and find some of the solutions.
While I'm here telling you about this, here's a solution you might not know about. Debouncing is now built in to iOS functionality! Starting in iOS 13, it's part of the Combine framework. I'm now using Combine all over the place: instead of notifications, instead of GCD, instead of Timer objects, etc. It's great!
Here's a Combine-based solution to this type of problem. Instead of a scroll view, suppose we have a button hooked up to an action handler, and we don't want the action handler to do its task unless 2 seconds has elapsed since the last time the user tapped the button:
var pipeline : AnyCancellable?
let pipelineStart = PassthroughSubject<Void,Never>()
#IBAction func doButton(_ sender: Any) {
if self.pipeline == nil {
self.pipeline = pipelineStart
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { [weak self] _ in self?.doSomething() }
}
self.pipelineStart.send()
}
func doSomething() {
print("I did it!")
}
I'm sure you can readily see how to adapt that to your own use case:
var pipeline : AnyCancellable?
let pipelineStart = PassthroughSubject<Void,Never>()
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if yOffset > offset {
if self.pipeline == nil {
self.pipeline = pipelineStart
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { [weak self] _ in self?.loadMoreDataFromRemoteServerIfNeed()
}
self.pipelineStart.send()
}
}
func loadMoreDataFromRemoteServerIfNeed() {
// <Insert New Data>
}
You can create a flag from DispatchWorkItem to observe loading state e.g.:
var item: DispatchWorkItem?
func loadMoreDataFromRemoteServerIfNeed() {
assert(Thread.isMainThread)
guard item == nil else { return }
item = DispatchWorkItem {
print("loading items")
Thread.sleep(forTimeInterval: 2)
DispatchQueue.main.async {
item = nil
print("insert items")
}
}
DispatchQueue.global().async(execute: item!)
}
NOTE: to synchronise item var you must change its value on the same thread for instance the main thread.
Yes, you could use DispatchWorkItem, keep a reference to the old one, and cancel prior one if necessary. If you were going to do that, I might consider Operation, too, as that handles cancelation even more gracefully and has other advantages.
But that having been said, given that the work that you are dispatching is immediately sleeping for two seconds, this might suggest a completely different pattern, namely a Timer. You can schedule your timer, invalidating previously scheduled timers, if any:
weak var timer: Timer?
func loadMoreDataFromRemoteServerIfNeed() {
// cancel old timer if any
timer?.invalidate()
// schedule what you want to do in 2 seconds
timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
// <Insert New Data>
}
}
FWIW, if you ever find yourself sleeping, you should general consider either timers or asyncAfter. This avoids tying up the global queue’s worker thread. Sleeping is an inefficient pattern.
In this case, keeping a weak reference to the prior timer (if any) is probably the best pattern.

Swift GCD Work Item Dispatch queue cancelling [duplicate]

This question already has answers here:
How to stop a DispatchWorkItem in GCD?
(3 answers)
Closed 3 years ago.
I have actions that must continue until the switch that initiated them is changed. I have tried to use GCD with a Work Item queue believing that the async action would allow the user to change the switch and consequently stop the actions. When the code runs it never sees or even triggers a switch change. Here is my code. Any idea what I am doing wrong?
#IBAction func trailerScanSwitchChange(_ sender: Any) {
let model = Model() //Instantiate the Model
gScan = trailerScanSwitch.isOn
//Set up work iten to read and parse data
let work1 = DispatchWorkItem {
while (gScan && gConnected){
model.parseValues()
print ("parsing values")
if gScan == false || gConnected == false{
break
}
model.convertVoltsToLights()
self.updateLights()
print ("updating Lights")
if gScan == false || gConnected == false{
break
}
}
}
if trailerScanSwitch.isOn == true{
print ("Scanning")
//perform on the current thread
work1.perform()
//perpform on the global queue
DispatchQueue.global(qos: .background).async(execute: work1) // concurrent actions
return
}
else { //Stop reading and displaying
gScan = false
work1.cancel()
}
}
You need to declare DispatchWorkItem variable at the top of class. In your code work1 variable is becoming inaccessible as soon as compiler is out of function. Every time you change your switch a new DispatchWorkItem variable initialize. Please go through below example for correct use of DispatchWorkItem with stop/resume functionality
#IBOutlet weak var button: UIButton!
#IBOutlet weak var label: UILabel!
var isStart = false
var work: DispatchWorkItem!
private func getCurrentTime() -> DispatchWorkItem {
work = DispatchWorkItem { [weak self] in
while true {
if (self?.work.isCancelled)!{break}
let date = Date()
let component = Calendar.current.dateComponents([.second], from: date)
DispatchQueue.main.async {
self?.label.text = "\(component.second!)"
}
}
}
return work
}
#IBAction func btnPressed() {
isStart = !isStart
button.setTitle(isStart ? "Stop" : "Start", for: .normal)
let workItem = getCurrentTime()
let globalQueue = DispatchQueue.global(qos: .background)
if isStart {
globalQueue.async(execute: workItem)
} else {
workItem.cancel()
}
}
This example will display current time seconds value in label. I have used while true, just to show an example.

I get "Unexpectedly found nil while implicitly unwrapping an Optional value" when trying to change the value of an NSTextField

I have prior experience in sequential programming in C, but it was in the mid 80's. I am new to OOP, Swift and multithread coding in general. I am writing a small program to better understand all 3. I was able to build a functional program that starts two threads that each count to 200 and then reset to 1 and restart counting in an endless loop. The value of each counter is printed to the console and I have a Start and stop button for each thread that allow me to control them separately. Everything works fine although I would admit that my code is far from perfect (I don't fully respect encapsulation, I have a few global variables that should be made local etc... My main problem is trying to output each thread counter value to a label instead of printing them to the console. When I try to change the content of any of my labels, I get "Unexpectedly found nil while implicitly unwrapping an Optional value"
When I use a similar line of code inside of a pushbutton function it works perfectly.
This is the content of my ViewController.swift file:
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//Call Async Task
startProgram()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBOutlet var threadAValueLabel: NSTextField!
#IBOutlet var threadBValueLabel: NSTextField!
#IBAction func threadAStartButton(_ sender: NSButtonCell) {
threadAGoNoGo = 1
self.threadAValueLabel.stringValue = "Start"
}
#IBAction func threadAStopButton(_ sender: NSButton) {
threadAGoNoGo = 0
self.threadAValueLabel.stringValue = "Stop"
}
#IBAction func threadBStartButton(_ sender: NSButton) {
threadBGoNoGo = 1
self.threadBValueLabel.stringValue = "Start"
}
#IBAction func threadBStopButton(_ sender: NSButton) {
threadBGoNoGo = 0
self.threadBValueLabel.stringValue = "Stop"
}
func changethreadALabel(_ message: String) {
self.threadAValueLabel.stringValue = message
}
func changethreadBLabel(_ message: String) {
self.threadBValueLabel.stringValue = message
}
The code creating the error is located in the last 2 methods:
self.threadAValueLabel.stringValue = message
and
self.threadBValueLabel.stringValue = message
While the following code inside of a pushbutton function, Works perfectly.
self.threadBValueLabel.stringValue = "Stop"
The code that creates the two threads is the following:
import Foundation
func startProgram(){
let myViewController: ViewController = ViewController (nibName:nil, bundle:nil)
// Start counting through 200 when Thread A start button is pressed and stop when Thread A Stop button is pressed. When reaching 200, go back to 0 and loop forever
DispatchQueue(label: "Start Thread A").async {
while true { // Loop Forever
var stepA:Int = 1
while stepA < 200{
for _ in 1...10000000{} // Delay loop
if threadAGoNoGo == 1{
print("Thread A \(stepA)")
myViewController.changethreadALabel("South \(stepA)") // Update Thread A value display label
stepA += 1
}
}
stepA = 1
}
}
// Start counting through 200 when Thread B start button is pressed and stop when Thread B Stop button is pressed. When reaching 200, go back to 0 and loop forever
DispatchQueue(label: "Start Thread B").async {
while true { // Loop Forever
var stepB:Int = 1
while stepB < 200{
for _ in 1...10000000{} // Delay loop
if threadBGoNoGo == 1{
print("Tread B \(stepB)")
myViewController.changethreadBLabel("South \(stepB)") // Update Thread B value display label
stepB += 1
}
}
stepB = 1
}
}
}
This is probably very simple to most of you, but I have spent four evenings trying to figure out by myself and searching through this forum, with no success.
New edit:
Thanks to Rob's answer, I was able to progress, but I am hitting another snag. if I move my code to the ViewController class, I seem to be able to access my stringValue variables self.threadAValueLabel.stringValue and self.threadBValueLabel.stringValue, but the below lines generate a "NSControl.stringValue must be used from main thread only" error message:
self.threadAValueLabel.stringValue = message
self.threadBValueLabel.stringValue = message
Although I understand what the error message means, I have tried to find a workaround to this problem for a few hours now and nothing seems to work.
Here is the full code
import Foundation
import Cocoa
public var threadAGoNoGo:Int = 0
public var threadBGoNoGo:Int = 0
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//Call Async Task
// Start counting through 200 when Thread A start button is pressed and stop when Thread A Stop button is pressed. When reaching 200, go back to 0 and loop forever
DispatchQueue(label: "Start Thread A").async {
while true { // Loop Forever
var stepA:Int = 1
while stepA < 200{
for _ in 1...10000000{} // Delay loop
if threadAGoNoGo == 1{
print("Thread A \(stepA)")
self.changethreadALabel("Thread A \(stepA)")
stepA += 1
}
}
stepA = 1
}
}
// Start counting through 200 when Thread B start button is pressed and stop when Thread B Stop button is pressed. When reaching 200, go back to 0 and loop forever
DispatchQueue(label: "Start Thread B").async {
while true { // Loop Forever
var stepB:Int = 1
while stepB < 200{
for _ in 1...10000000{} // Delay loop
if threadBGoNoGo == 1{
print("Tread B \(stepB)")
self.changethreadBLabel("Thread B \(stepB)")
stepB += 1
}
}
stepB = 1
}
}
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBOutlet var threadAValueLabel: NSTextField!
#IBOutlet var threadBValueLabel: NSTextField!
#IBAction func threadAStartButton(_ sender: NSButtonCell) {
threadAGoNoGo = 1
self.threadAValueLabel.stringValue = "Start"
}
#IBAction func threadAStopButton(_ sender: NSButton) {
threadAGoNoGo = 0
self.threadAValueLabel.stringValue = "Stop"
}
#IBAction func threadBStartButton(_ sender: NSButton) {
threadBGoNoGo = 1
self.threadBValueLabel.stringValue = "Start"
}
#IBAction func threadBStopButton(_ sender: NSButton) {
threadBGoNoGo = 0
self.threadBValueLabel.stringValue = "Stop"
}
func changethreadALabel(_ message:String) {
self.threadAValueLabel.stringValue = message
}
func changethreadBLabel(_ message: String) {
self.threadBValueLabel.stringValue = message
}
}
Your startProgram is instantiating a new, duplicative instance of the view controller which does not have any outlets hooked up. Hence, the outlets will be nil and attempts to reference them will generate the error you describe.
If you really wanted to make it a global function, then pass the view controller reference, e.g.:
func startProgram(on viewController: ViewController) {
// var myViewController = ...
// now use the `viewController` parameter rather than the `myViewController` local var
...
}
And then the view controller could pass reference to itself so that the global startProgram knew what instance of the view controller to update:
startProgram(on: self)
Or, better, (a) you shouldn’t have global methods at all; and (b) even if you did, nothing outside the view controller should be updating the view controller’s outlets, anyway. Here the simple solution is to, instead, make startProgram a method of the ViewController class, and then you can refer to the outlets directly.
You have edited your question to put startProgram in the view controller class, as advised above. You then encountered a second, different problem, where the debugger reported:
NSControl.stringValue must be used from main thread only
Yes, if you’re initiated a UI update from a background thread, you must dispatch that update back to the main thread, e.g.:
DispatchQueue.main.async {
self.changethreadALabel("Thread A \(stepA)")
}
See the Main Thread Checker for more information.

KVO listener issues in Swift 4

I am using ViewModel class and want to setup observer if any changes into loginResponse variable.
#objcMembers class ViewModel: NSObject {
var count = 300
#objc dynamic var loginResponse :String
override init() {
loginResponse = "1"
super.init()
setupTimer()
}
func setupTimer(){
_ = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector:#selector(callTimer), userInfo: nil, repeats: true)
}
func callTimer(){
let minutes = String(count / 60)
let seconds = String(count % 60)
loginResponse = minutes + ":" + seconds
count = count - 1
}
}
View controller code:
override func viewDidLoad() {
super.viewDidLoad()
_ = viewModel.observe(\ViewModel.loginResponse) { (model, changes) in
print(changes)
}
}
I want to listen to any change into loginResponse variable in my Viewcontroller but it's not getting the callback. What am I doing wrong here?
The .observe(_:options:changeHandler:) function returns a NSKeyValueObservation object that is used to control the lifetime of the observation. When it is deinited or invalidated, the observation will stop.
Since your view controller doesn't keep a reference to the returned "observation" it goes out of scope at the end of viewDidLoad and thus stops observing.
To continue observing for the lifetime of the view controller, store the returned observation in a property. If you're "done" observing before that, you can call invalidate on the observation or set the property to nil.

Listening for NSWorkspace Notifications in Swift 4

The simple Swift 4 example below should stop when the computer's display goes to sleep.
class Observer {
var asleep = false
func addDNC () {
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.screensDidSleepNotification, object: nil, queue: nil, using: notificationRecieved)
}
func notificationRecieved (n: Notification) {
asleep = true
}
}
let observer = Observer ()
observer.addDNC ()
while (!observer.asleep) {}
print ("zzzz")
However, the program gets stuck in the while loop. What am I doing wrong, and what is the proper way to wait for a Notification?
I have tried using a selector (#selector (notificationRecieved), with #objc in the function declaration, of course), to no avail.
Start a template app in Xcode and modify the ViewController.swift to do this:
import Cocoa
class Observer {
var asleep = false
func addDNC () {
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.screensDidSleepNotification, object: nil, queue: nil, using: notificationRecieved)
}
func notificationRecieved (n: Notification) {
print("got sleep notification!")
asleep = true
}
}
class ViewController: NSViewController {
let observer = Observer ()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
observer.addDNC ()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
The difference between your code and mine is that I'm not doing the wacky sleepy polling thing you're doing (that's going to lead to a spinning pizza cursor), and I'm also setting observer to be a property off the ViewController object, so the observer property sticks around as long as the view controller does.