iPing button Title problem


#1

I have an issue trying to get the button to show the correct title. With the initial code from the chapter (with the usual corrections for Swift 4.1) when ‘Start Pinging’ was clicked the program ran as expected, the button title changed to the alternate title of ‘Stop Pinging’ and then back to ‘Start Pinging’ at the completion of the run. The problem started when clicking ‘Stop Pinging’ while iPing was running. It stopped the pinging, the button momentarily (maybe 1 second) showed ‘Start Pinging’ then reverted to ‘Stop Pinging’. The button still starts and stops the pinging, but the title remains ‘Stop Pinging’ until I let it run to the end, then it will change to ‘Start Pinging’.

So I added the following code to func togglePinging(sender: NSButton):

if startButton.state == NSControl.StateValue.off {
			startButton.state = NSControl.StateValue.on
		}

And had to change the startButton state to on instead of off in func receiveProcessTerminationNotification(notification: Notification).

import Cocoa

class ViewController: NSViewController {

@IBOutlet var outputView: NSTextView!
@IBOutlet weak var hostField: NSTextField!
@IBOutlet weak var startButton: NSButton!
var process: Process?
var pipe: Pipe?
var fileHandle: FileHandle?

@IBAction func togglePinging(sender: NSButton) {
	// Is there a running Process?
	if let process = process {
		// A process is running, stop it
		process.interrupt()
	} else {
		// A process is not running, start one
		// toggle button to display correct title/alternate
		if startButton.state == NSControl.StateValue.off {
			startButton.state = NSControl.StateValue.on
			switchTitle = true
		}
		// Create a new process
		let process = Process()
		process.launchPath = "/sbin/ping"
		process.arguments = ["-c10", hostField.stringValue]
		
		// Create a new pipe for standardOutput
		let pipe = Pipe()
		process.standardOutput = pipe
		
		// Grab the fileHandle
		let fileHandle = pipe.fileHandleForReading
		
		self.process = process
		self.pipe = pipe
		self.fileHandle = fileHandle
		
		let notificationCenter = NotificationCenter.default
		notificationCenter.removeObserver(self)
		notificationCenter.addObserver(self,
									   selector: #selector(receiveDataReadyNotification),
									   name: FileHandle.readCompletionNotification,
									   object: fileHandle)
		notificationCenter.addObserver(self,
									   selector: #selector(receiveProcessTerminationNotification),
									   name: Process.didTerminateNotification,
									   object: process)
	}
	process?.launch()
	
	outputView.string = ""
	
	fileHandle?.readInBackgroundAndNotify()
}

//--------------------------------------------------------------------------------------------------

func appendData(data: Data) {
	let string = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
	let textStorage = outputView.textStorage!
	let endRange = NSRange(location: (textStorage.length), length: 0)
	textStorage.replaceCharacters(in: endRange, with: string)
}

//--------------------------------------------------------------------------------------------------

@objc func receiveDataReadyNotification(notification: Notification) {
	let data = notification.userInfo![NSFileHandleNotificationDataItem] as! Data
	let length = data.count
	
	print("received data: \(length) bytes")
	
	if length > 0 {
		self.appendData(data: data)
	}
	
	// If the process is running, start reading again
	if let fileHandle = fileHandle {
		fileHandle.readInBackgroundAndNotify()
	}
}

//--------------------------------------------------------------------------------------------------

@objc func receiveProcessTerminationNotification(notification: Notification) {
	print("process terminated")
	
	process = nil
	pipe = nil
	fileHandle = nil
	
	startButton.state = NSControl.StateValue.on
  }

  //--------------------------------------------------------------------------------------------------
}

Now the button will change properly when clicked between the start and stop titles, however, because I had to change the button state to ‘on’ in func receiveProcessTerminationNotification(notification: Notification) (to get the button switching to work), now the button stays ‘Stop Pinging’ when allowed to complete a run.

Any ideas what I’ve messed up?


#2

Hint: To deal with problems like this effectively, use finite state machine techniques.


#3

I had a typo. I was trying your suggestion, Ibex10, when I noticed was getting an error message, and I think I was getting it all along. No explanation on my part about why I didn’t see it was an error message being printed. Turns out I was trying to launch a new process while one was running. I had a curly bracket ‘}’ in the wrong place.

I had it here:

....
notificationCenter.addObserver(self,
								   selector: #selector(receiveProcessTerminationNotification),
								   name: Process.didTerminateNotification,
								   object: process)
}
    process?.launch()

    outputView.string = ""

    fileHandle?.readInBackgroundAndNotify()
}

When it SHOULD have been here:

...
notificationCenter.addObserver(self,
									   selector: #selector(receiveProcessTerminationNotification),
									   name: Process.didTerminateNotification,
									   object: process)
		process.launch()
		
		outputView.string = ""
		
		fileHandle.readInBackgroundAndNotify()
	}
}

That was why I was getting the momentary flash of “Start Pinging” then reverting to “Stop Pinging”.

Now it works as it is supposed to with none of the additions I was trying to add in the first post.

Wonder how long it will take me to proofread better and to pay way more attention to what is int he terminal window!

Thanks again for your help Ibex10.