From ed2232afde8283fefb5008c306a6bff8311d71ba Mon Sep 17 00:00:00 2001 From: Bill Booth Date: Thu, 1 Feb 2018 16:37:38 -0800 Subject: [PATCH] checkpoint --- PcVolumeControl.xcodeproj/project.pbxproj | 20 ++ PcVolumeControl/Base.lproj/Main.storyboard | 249 ++++++++++++-- PcVolumeControl/DiscoverViewController.swift | 167 ++++++++++ PcVolumeControl/ManualViewController.swift | 219 ++++++++++++ PcVolumeControl/MdnsViewController.swift | 160 +++++++++ PcVolumeControl/ServerCell.swift | 35 ++ PcVolumeControl/ServerController.swift | 138 ++++++++ PcVolumeControl/StreamController.swift | 3 + PcVolumeControl/ViewController.swift | 22 +- PcVolumeControl/ViewControllerStart.swift | 334 ++++++++++--------- 10 files changed, 1169 insertions(+), 178 deletions(-) create mode 100644 PcVolumeControl/DiscoverViewController.swift create mode 100644 PcVolumeControl/ManualViewController.swift create mode 100644 PcVolumeControl/MdnsViewController.swift create mode 100644 PcVolumeControl/ServerCell.swift create mode 100644 PcVolumeControl/ServerController.swift diff --git a/PcVolumeControl.xcodeproj/project.pbxproj b/PcVolumeControl.xcodeproj/project.pbxproj index 54007e8..3c675c6 100644 --- a/PcVolumeControl.xcodeproj/project.pbxproj +++ b/PcVolumeControl.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ A92F6CA020105AEA0045180F /* Icon-80.png in Resources */ = {isa = PBXBuildFile; fileRef = A92F6C8220105AEA0045180F /* Icon-80.png */; }; A92F6CA120105AEA0045180F /* Icon-87.png in Resources */ = {isa = PBXBuildFile; fileRef = A92F6C8320105AEA0045180F /* Icon-87.png */; }; A92F6CA220105AEA0045180F /* Icon-88.png in Resources */ = {isa = PBXBuildFile; fileRef = A92F6C8420105AEA0045180F /* Icon-88.png */; }; + A92F6CAF20199B7A0045180F /* MdnsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92F6CAE20199B7A0045180F /* MdnsViewController.swift */; }; A952C1661FE8D3A6002559B3 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = A952C1651FE8D3A6002559B3 /* Alert.swift */; }; A971F2471FF1DA5D00AD9167 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A971F2461FF1DA5D00AD9167 /* Launch Screen.storyboard */; }; A971F2491FF2ABBB00AD9167 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A971F2481FF2ABBB00AD9167 /* AboutViewController.swift */; }; @@ -55,6 +56,10 @@ A9AC32CA1FECEF1400D385A8 /* pcvg logo.png in Resources */ = {isa = PBXBuildFile; fileRef = A9AC32C91FECEF1400D385A8 /* pcvg logo.png */; }; A9AC32CC1FECF78400D385A8 /* pcvg logo square.png in Resources */ = {isa = PBXBuildFile; fileRef = A9AC32CB1FECF78400D385A8 /* pcvg logo square.png */; }; A9B66A231FECE1910051C4C5 /* ViewControllerStart.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B66A221FECE1910051C4C5 /* ViewControllerStart.swift */; }; + A9E20977201C2B5800D0DD26 /* DiscoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E20976201C2B5800D0DD26 /* DiscoverViewController.swift */; }; + A9E20979201C2B8500D0DD26 /* ManualViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E20978201C2B8500D0DD26 /* ManualViewController.swift */; }; + A9E2097B201C343F00D0DD26 /* ServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E2097A201C343F00D0DD26 /* ServerCell.swift */; }; + A9E2097D201C562000D0DD26 /* ServerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E2097C201C562000D0DD26 /* ServerController.swift */; }; A9FDBC6F1FECFCE300DAE6D7 /* pcvg logo square in Resources */ = {isa = PBXBuildFile; fileRef = A9FDBC6E1FECFCE300DAE6D7 /* pcvg logo square */; }; C52F283691D46C907B40E610 /* Pods_PcVolumeControlUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 44154FABEF460A95987B8D20 /* Pods_PcVolumeControlUITests.framework */; }; /* End PBXBuildFile section */ @@ -122,6 +127,7 @@ A92F6C8220105AEA0045180F /* Icon-80.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-80.png"; sourceTree = ""; }; A92F6C8320105AEA0045180F /* Icon-87.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-87.png"; sourceTree = ""; }; A92F6C8420105AEA0045180F /* Icon-88.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-88.png"; sourceTree = ""; }; + A92F6CAE20199B7A0045180F /* MdnsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MdnsViewController.swift; sourceTree = ""; }; A952C1651FE8D3A6002559B3 /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = ""; }; A971F2461FF1DA5D00AD9167 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; A971F2481FF2ABBB00AD9167 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; @@ -134,6 +140,10 @@ A9AC32C91FECEF1400D385A8 /* pcvg logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "pcvg logo.png"; path = "../../../../Desktop/pcvc_images/pcvg logo.png"; sourceTree = ""; }; A9AC32CB1FECF78400D385A8 /* pcvg logo square.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "pcvg logo square.png"; path = "../../../../Desktop/pcvc_images/pcvg logo square.png"; sourceTree = ""; }; A9B66A221FECE1910051C4C5 /* ViewControllerStart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerStart.swift; sourceTree = ""; }; + A9E20976201C2B5800D0DD26 /* DiscoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoverViewController.swift; sourceTree = ""; }; + A9E20978201C2B8500D0DD26 /* ManualViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualViewController.swift; sourceTree = ""; }; + A9E2097A201C343F00D0DD26 /* ServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerCell.swift; sourceTree = ""; }; + A9E2097C201C562000D0DD26 /* ServerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerController.swift; sourceTree = ""; }; A9FDBC6E1FECFCE300DAE6D7 /* pcvg logo square */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "pcvg logo square"; path = "../../../../Desktop/pcvg logo square"; sourceTree = ""; }; BC0C05349FA2A35B69E7936E /* Pods-PcVolumeControlUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PcVolumeControlUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PcVolumeControlUITests/Pods-PcVolumeControlUITests.debug.xcconfig"; sourceTree = ""; }; E497C089C2FB3C823939B7C5 /* Pods-PcVolumeControlTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PcVolumeControlTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PcVolumeControlTests/Pods-PcVolumeControlTests.debug.xcconfig"; sourceTree = ""; }; @@ -216,9 +226,14 @@ A92F6C6620105AEA0045180F /* iosicons */, A971F2461FF1DA5D00AD9167 /* Launch Screen.storyboard */, A927E14E1FC53AB10024D526 /* Main.storyboard */, + A92F6CAE20199B7A0045180F /* MdnsViewController.swift */, + A9E20976201C2B5800D0DD26 /* DiscoverViewController.swift */, + A9E20978201C2B8500D0DD26 /* ManualViewController.swift */, A9B66A221FECE1910051C4C5 /* ViewControllerStart.swift */, A927E14C1FC53AB10024D526 /* ViewController.swift */, A971F2481FF2ABBB00AD9167 /* AboutViewController.swift */, + A9E2097A201C343F00D0DD26 /* ServerCell.swift */, + A9E2097C201C562000D0DD26 /* ServerController.swift */, A9A25CCD1FEB61DD009C985B /* StreamController.swift */, A97414AA1FDF98BC00062954 /* SliderCell.swift */, A97414AC1FE629AC00062954 /* Session.swift */, @@ -622,14 +637,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A9E2097B201C343F00D0DD26 /* ServerCell.swift in Sources */, A952C1661FE8D3A6002559B3 /* Alert.swift in Sources */, + A92F6CAF20199B7A0045180F /* MdnsViewController.swift in Sources */, A9B66A231FECE1910051C4C5 /* ViewControllerStart.swift in Sources */, A9A25CCE1FEB61DD009C985B /* StreamController.swift in Sources */, A97414AD1FE629AC00062954 /* Session.swift in Sources */, A927E14D1FC53AB10024D526 /* ViewController.swift in Sources */, A97414AB1FDF98BC00062954 /* SliderCell.swift in Sources */, + A9E2097D201C562000D0DD26 /* ServerController.swift in Sources */, A971F2491FF2ABBB00AD9167 /* AboutViewController.swift in Sources */, + A9E20977201C2B5800D0DD26 /* DiscoverViewController.swift in Sources */, A971F24D1FF2BD4C00AD9167 /* DesignableSlider.swift in Sources */, + A9E20979201C2B8500D0DD26 /* ManualViewController.swift in Sources */, A927E14B1FC53AB10024D526 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/PcVolumeControl/Base.lproj/Main.storyboard b/PcVolumeControl/Base.lproj/Main.storyboard index ba786a9..b2ef471 100644 --- a/PcVolumeControl/Base.lproj/Main.storyboard +++ b/PcVolumeControl/Base.lproj/Main.storyboard @@ -1,5 +1,5 @@ - + @@ -10,6 +10,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -19,7 +70,7 @@ - + - + @@ -133,11 +177,11 @@ - + - + @@ -157,7 +201,7 @@ - + @@ -203,7 +247,7 @@ - + @@ -307,14 +351,14 @@ - + - + @@ -499,14 +543,177 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/PcVolumeControl/DiscoverViewController.swift b/PcVolumeControl/DiscoverViewController.swift new file mode 100644 index 0000000..8c8487e --- /dev/null +++ b/PcVolumeControl/DiscoverViewController.swift @@ -0,0 +1,167 @@ +// +// DiscoverViewController.swift +// PcVolumeControl +// +// Created by Bill Booth on 1/26/18. +// Copyright © 2018 PcVolumeControl. All rights reserved. +// + +import UIKit + +class DiscoverViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, MdnsServerDelegate { + + @IBOutlet weak var scanPercentage: UIProgressView! + @IBOutlet weak var discoverTableView: UITableView! + + var minValue = 0 + var maxValue = 100 + var barTimer = Timer() + var serverControl: ServerController? + var serverList: [(ip: String, port: Int32)] = [] + let asyncQueue = DispatchQueue(label: "asyncQueue", attributes: .concurrent) + var spinnerView: UIView? = nil + var SController: StreamController? = nil + + + override func viewDidLoad() { + super.viewDidLoad() + + + scanPercentage.progress = 0 + barTimer = Timer.scheduledTimer(timeInterval: 0.06, target: self, selector: (#selector(DiscoverViewController.progress)), userInfo: nil, repeats: true) + + discoverTableView.delegate = self + discoverTableView.dataSource = self + + serverControl = ServerController(delegate: self) + print("looking around for servers...") + serverControl?.listen() + + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "testSegue" { + let destVC = segue.destination as! ViewController + destVC.SController = self.SController // give them our stream controller + destVC.initialDraw = true // signal to viewDidLoad() to reload everything. + let backItem = UIBarButtonItem(title: "Servers", style: .plain, target: nil, action: nil) + self.navigationItem.backBarButtonItem? = backItem + } + } + + @objc func progress() { + if minValue != maxValue { + minValue += 1 + scanPercentage.progress = Float(minValue) / Float(maxValue) + } else { + minValue = 0 + } + } + + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return serverList.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "serverCell") as! ServerCell + cell.delegate = self + + let targetServer = serverList[indexPath.row] + cell.setCellParameter(ip: targetServer.0, port: targetServer.1) + return cell + } + + func didFindServer(ip: String, port: Int32) { + serverList.append((ip: ip, port: port)) + discoverTableView.reloadData() + } + + func serverUnregistered(ip: String, port: Int32) { + print("server was unregistered!") + // find the index of this object + if let idx = serverList.index(where: {$0 == (ip, port)}) { + serverList.remove(at: idx) + discoverTableView.reloadData() + } + } +} + +extension DiscoverViewController: ServerCellDelegate { + + func didClickConnect(ip: String, port: Int32) { + // load segue here. + SController = StreamController(address: ip, port: port, delegate: self) + SController?.delegate = self + + // Start looking for messages in the publish subject. + SController?.processMessages() + + // Make the initial server connection and get the first message. + asyncQueue.async { + self.SController?.connectNoSend() + } + + } + +} + +extension DiscoverViewController: StreamControllerDelegate { + func isAttemptingConnection() { + print("Connection is in progress...") + + asyncQueue.async { + DispatchQueue.main.async { + self.spinnerView = UIViewController.displaySpinner(onView: self.view) + } + } + } + func didConnectToServer() { + print("Server connection complete. Moving to main VC.") + asyncQueue.async { + DispatchQueue.main.async { + if let spinner = self.spinnerView { + UIViewController.removeSpinner(spinner: spinner) + } + // go to the next screen, pushing along the stream controller instance. + self.performSegue(withIdentifier: "testSegue", sender: self.SController) + } + } + } + + func failedToConnect() { + if let spinner = spinnerView { + UIViewController.removeSpinner(spinner: spinner) + DispatchQueue.main.async { + let _ = Alert.showBasic(title: "Connection Error", message: "Connection to the server failed.\n\nIs the IP or name correct?\nIs the port open?", vc: self) + } + } + } + func didGetServerUpdate() {} + func bailToConnectScreen() {} + func tearDownConnection() {} +} + +// This is the spinner shown when the socket is being set up. +extension UIViewController { + class func displaySpinner(onView : UIView) -> UIView { + + let spinnerView = UIView.init(frame: onView.bounds) + spinnerView.backgroundColor = UIColor.init(red: 0.5, green: 0.5, blue: 0.5, alpha: 0.5) + let ai = UIActivityIndicatorView.init(activityIndicatorStyle: .whiteLarge) + ai.startAnimating() + ai.center = spinnerView.center + + spinnerView.addSubview(ai) + onView.addSubview(spinnerView) + + return spinnerView + } + + class func removeSpinner(spinner :UIView) { + DispatchQueue.main.async { + spinner.removeFromSuperview() + } + } +} + diff --git a/PcVolumeControl/ManualViewController.swift b/PcVolumeControl/ManualViewController.swift new file mode 100644 index 0000000..add69e3 --- /dev/null +++ b/PcVolumeControl/ManualViewController.swift @@ -0,0 +1,219 @@ +// +// ManualViewController.swift +// PcVolumeControl +// +// Created by Bill Booth on 1/26/18. +// Copyright © 2018 PcVolumeControl. All rights reserved. +// + +import UIKit + +class ManualViewController: UIViewController, UITextFieldDelegate { + + let asyncQueue = DispatchQueue(label: "asyncQueue", attributes: .concurrent) + var defaults = UserDefaults.standard // to persist the IP/port entered previously + var SController: StreamController? = nil + var spinnerView: UIView? = nil + + @IBOutlet weak var Cbutton: UIButton! + @IBOutlet weak var ServerIPField: UITextField! + @IBOutlet weak var ServerPortField: UITextField! + + @IBAction func ConnectButtonClicked(_ sender: UIButton) { + // handoff to the main viewcontroller + guard let PortNum = Int32(self.ServerPortField.text!) else { + let _ = Alert.showBasic(title: "Error", message: "Bad port number specified.\nThe default is 3000. The port number needs to be between 1-65535.", vc: self) + return + } + guard let IPaddr = self.ServerIPField.text else { + return + } + + // Check if the IP field is blank. + if IPaddr.isEmpty { + let _ = Alert.showBasic(title: "Error", message: "A host name or IPv4 address is required in order to connect to your PCVolumeControl server.", vc: self) + return + } else { + // They entered *something* + if IPaddr.matches("^[0-9]") && IPaddr.matches("[0-9]$") { + // pretty sure it's an IP address, but is it valid? + if !isValidIP(s: IPaddr) { + let _ = Alert.showBasic(title: "Error", message: "The entry '\(IPaddr)' was not a valid IPv4 address.", vc: self) + return + } + } + } + + defaults.set(IPaddr, forKey: "IPaddr") + defaults.set(PortNum, forKey: "PortNum") + defaults.synchronize() + + SController = StreamController(address: IPaddr, port: PortNum, delegate: self) + SController?.delegate = self + + // Start looking for messages in the publish subject. + SController?.processMessages() + + // Make the initial server connection and get the first message. + asyncQueue.async { + self.SController?.connectNoSend() + } + + } + + + + override func viewDidLoad() { + super.viewDidLoad() + + ServerIPField.delegate = self + ServerPortField.delegate = self + ServerIPField.returnKeyType = UIReturnKeyType.next + ServerIPField.tag = 1 + ServerPortField.tag = 2 + ServerIPField.autocorrectionType = .no + ServerPortField.autocorrectionType = .no + ServerIPField.keyboardType = .numbersAndPunctuation + ServerPortField.keyboardType = .numberPad + + Cbutton.styleButton(cornerRadius: 8, borderWidth: 2, borderColor: UIColor.gray.cgColor) + addDoneButtonOnKeyboard() + + if let ipaddr = defaults.string(forKey: "IPaddr") { + ServerIPField.text = ipaddr + } + if let port = defaults.string(forKey: "PortNum") { + ServerPortField.text = port + } + + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool + { + if let nextField = textField.superview?.viewWithTag(textField.tag + 1) as? UITextField { + nextField.becomeFirstResponder() + } else { + textField.resignFirstResponder() + } + return false + } + + func isValidIP(s: String) -> Bool { + let parts = s.components(separatedBy: ".") + let nums = parts.flatMap { Int($0) } + return parts.count == 4 && nums.count == 4 && nums.filter { $0 >= 0 && $0 < 256}.count == 4 + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + func addDoneButtonOnKeyboard() + { + let doneToolbar: UIToolbar = UIToolbar() + doneToolbar.barStyle = UIBarStyle.default + + let flexSpace = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.flexibleSpace, target: nil, action: nil) + let done: UIBarButtonItem = UIBarButtonItem(title: "Done", style: UIBarButtonItemStyle.done, target: self, action: #selector(ManualViewController.doneButtonAction(_:))) + var items = [UIBarButtonItem]() + items.append(flexSpace) + items.append(done) + items.append(flexSpace) + + doneToolbar.items = items + doneToolbar.sizeToFit() + + ServerPortField.inputAccessoryView = doneToolbar + + } + + @objc func doneButtonAction(_ sender: UIBarButtonItem!) + { + view.endEditing(true) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "ManualConnectSegue" { + let destVC = segue.destination as! ViewController + destVC.SController = self.SController // give them our stream controller + destVC.initialDraw = true // signal to viewDidLoad() to reload everything. + navigationItem.backBarButtonItem?.title = "Disconnect" + } + } + +} + +extension ManualViewController: StreamControllerDelegate { + func isAttemptingConnection() { + print("Connection is in progress...") + + asyncQueue.async { + DispatchQueue.main.async { + self.spinnerView = UIViewController.displaySpinner(onView: self.view) + } + } + } + func didConnectToServer() { + print("Server connection complete. Moving to main VC.") + asyncQueue.async { + DispatchQueue.main.async { + if let spinner = self.spinnerView { + UIViewController.removeSpinner(spinner: spinner) + } + // go to the next screen, pushing along the stream controller instance. + self.performSegue(withIdentifier: "ManualConnectSegue", sender: self.SController) + } + } + } + + func failedToConnect() { + if let spinner = spinnerView { + UIViewController.removeSpinner(spinner: spinner) + DispatchQueue.main.async { + let _ = Alert.showBasic(title: "Connection Error", message: "Connection to the server failed.\n\nIs the IP or name correct?\nIs the port open?", vc: self) + } + } + } + func didGetServerUpdate() {} + func bailToConnectScreen() {} + func tearDownConnection() {} +} + +// This is the spinner shown when the socket is being set up. +extension ManualViewController { + func displaySpinner(onView : UIView) -> UIView { + + let spinnerView = UIView.init(frame: onView.bounds) + spinnerView.backgroundColor = UIColor.init(red: 0.5, green: 0.5, blue: 0.5, alpha: 0.5) + let ai = UIActivityIndicatorView.init(activityIndicatorStyle: .whiteLarge) + ai.startAnimating() + ai.center = spinnerView.center + + spinnerView.addSubview(ai) + onView.addSubview(spinnerView) + + return spinnerView + } + + func removeSpinner(spinner :UIView) { + DispatchQueue.main.async { + spinner.removeFromSuperview() + } + } +} + +// Customizing the Connect button a bit +extension UIButton { + func styleButton(cornerRadius: CGFloat, borderWidth: CGFloat, borderColor: CGColor) { + self.layer.cornerRadius = cornerRadius + self.layer.borderWidth = borderWidth + self.layer.borderColor = borderColor + } +} +// Extend String so we can allow regex matching on the domain name/IP entered. +extension String { + func matches(_ regex: String) -> Bool { + return self.range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil + } +} diff --git a/PcVolumeControl/MdnsViewController.swift b/PcVolumeControl/MdnsViewController.swift new file mode 100644 index 0000000..3e2e175 --- /dev/null +++ b/PcVolumeControl/MdnsViewController.swift @@ -0,0 +1,160 @@ +// +// MDNS.swift +// PcVolumeControl +// +// Created by Bill Booth on 1/24/18. +// Copyright © 2018 PcVolumeControl. All rights reserved. +// + +import UIKit + +class MdnsViewController: UIViewController, NetServiceBrowserDelegate, NetServiceDelegate, MdnsServerDelegate { + + @IBOutlet weak var discoverView: UIView! + @IBOutlet weak var manualView: UIView! + @IBOutlet weak var addressSegmentControl: UISegmentedControl! + @IBAction func segmentedControlClicked(_ sender: UISegmentedControl) { + switch addressSegmentControl.selectedSegmentIndex + { + case 0: + discoverView.isHidden = false + manualView.isHidden = true + case 1: + discoverView.isHidden = true + manualView.isHidden = false + default: + break; + } + } + +// var nsb : NetServiceBrowser! +// var services = [NetService]() + var serverDelegate: MdnsServerDelegate? + + + override func viewDidLoad() { + super.viewDidLoad() + self.view.sendSubview(toBack:manualView) + self.view.bringSubview(toFront: discoverView) + +// listen() + } + + +// func listen() { +// print("listening for services...") +// self.services.removeAll() +// self.nsb = NetServiceBrowser() +// self.nsb.delegate = self +// DispatchQueue.global(qos: .userInitiated).async { +// self.nsb.searchForServices(ofType: "_http._tcp.", inDomain: "local.") +// } +// } + +// func updateInterface () { +// for service in services { +// +// if service.port == -1 { +// print("service \(service.name) of type \(service.type)" + +// " not yet resolved") +// service.delegate = self +// service.resolve(withTimeout:10) +// } else { +// print("service \(service.name) of type \(service.type)," + +// "port \(service.port), addresses \(String(describing: service.addresses))") +// if service.name == "pcvolumecontrol" { +// let serverIP: String! +// let serverPort: Int32! +// print("Our service '\(service.name)' was located!") +// // now we know the IP and port of the server. +// if let addresses = service.addresses { +// guard let ip = getIPV4StringfromAddress(address: addresses) else { return } +// serverIP = ip +// serverPort = Int32(service.port) +// // segue to the main VC with the server and port now. +// didFindServer(ip: serverIP, port: serverPort) +// +// +// +//// print("Connecting to server \(serverIP):\(serverPort)") +//// // defaults.set(serverIP, forKey: "IPaddr") +//// // defaults.set(serverPort, forKey: "PortNum") +//// // defaults.synchronize() +//// +//// SController = StreamController(address: serverIP, port: serverPort, delegate: self) +//// SController?.delegate = self +//// +//// // Start looking for messages in the publish subject. +//// SController?.processMessages() +//// +//// // Make the initial server connection and get the first message. +//// asyncQueue.async { +//// self.SController?.connectNoSend() +//// } +// } +// } +// } +// } +// } +// +// // Used to get an IP address out of the array of data within service.addresses +// func getIPV4StringfromAddress(address: [Data]) -> String? { +// +// let data = address.first! as NSData; +// +// var ip1 = UInt8(0) +// data.getBytes(&ip1, range: NSMakeRange(4, 1)) +// +// var ip2 = UInt8(0) +// data.getBytes(&ip2, range: NSMakeRange(5, 1)) +// +// var ip3 = UInt8(0) +// data.getBytes(&ip3, range: NSMakeRange(6, 1)) +// +// var ip4 = UInt8(0) +// data.getBytes(&ip4, range: NSMakeRange(7, 1)) +// +// let ipStr = String(format: "%d.%d.%d.%d",ip1,ip2,ip3,ip4); +// +// return ipStr +// } +// +// func netServiceDidResolveAddress(_ sender: NetService) { +// self.updateInterface() +// } +// +// func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) { +// print("search starting.") +// } +// private func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch error: Error) { +// print("error!") +// } +// +// func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser) { +// print("search was stopped") +// } +// +// func netServiceBrowser(_ aNetServiceBrowser: NetServiceBrowser, didFind aNetService: NetService, moreComing: Bool) { +// print("adding a service") +// self.services.append(aNetService) +// if !moreComing { +// self.updateInterface() +// } +// } +// +// func netServiceBrowser(_ aNetServiceBrowser: NetServiceBrowser, didRemove aNetService: NetService, moreComing: Bool) { +// if let ix = self.services.index(of:aNetService) { +// self.services.remove(at:ix) +// print("removing a service") +// if !moreComing { +// self.updateInterface() +// } +// } +// } +// + func didFindServer(ip: String, port: Int32) { + } + func serverUnregistered(ip: String, port: Int32) { + + } +} diff --git a/PcVolumeControl/ServerCell.swift b/PcVolumeControl/ServerCell.swift new file mode 100644 index 0000000..5f785bd --- /dev/null +++ b/PcVolumeControl/ServerCell.swift @@ -0,0 +1,35 @@ +// +// serverCell.swift +// PcVolumeControl +// +// Created by Bill Booth on 1/26/18. +// Copyright © 2018 PcVolumeControl. All rights reserved. +// + +import UIKit + +protocol ServerCellDelegate { + func didClickConnect(ip: String, port: Int32) +} + +class ServerCell: UITableViewCell { + + @IBOutlet weak var serverNameLabel: UILabel! + @IBOutlet weak var serverConnectButton: UIButton! + + var delegate: ServerCellDelegate? + var serverIP: String! + var serverPort: Int32! + + func setCellParameter(ip: String, port: Int32) { + serverNameLabel.text = "\(ip):\(port)" + serverIP = ip + serverPort = port + } + + @IBAction func didClickConnect(_ sender: UIButton) { + print("User requested connection to server...") + delegate?.didClickConnect(ip: serverIP, port: serverPort) + } +} + diff --git a/PcVolumeControl/ServerController.swift b/PcVolumeControl/ServerController.swift new file mode 100644 index 0000000..227d764 --- /dev/null +++ b/PcVolumeControl/ServerController.swift @@ -0,0 +1,138 @@ +// +// ServerController.swift +// PcVolumeControl +// +// Created by Bill Booth on 1/26/18. +// Copyright © 2018 PcVolumeControl. All rights reserved. +// + +import Foundation +import RxSwift + + +protocol MdnsServerDelegate { + func didFindServer(ip: String, port: Int32) + func serverUnregistered(ip: String, port: Int32) +} + +class ServerController: NSObject, NetServiceBrowserDelegate, NetServiceDelegate { + + var delegate: MdnsServerDelegate? + var nsb : NetServiceBrowser! + var services = [NetService]() + + // Rx stuff + let bag = DisposeBag() + var lastServerSubject = PublishSubject<(String, Int32)>() // array of ip:port tuples + + init(delegate: MdnsServerDelegate) { + self.delegate = delegate + + } + + private func foundServer(ip: String, port: Int32) { + self.delegate?.didFindServer(ip: ip, port: port) + } + + func listen() { + print("listening for services...") + self.services.removeAll() + self.nsb = NetServiceBrowser() + self.nsb.delegate = self + DispatchQueue.global(qos: .userInitiated).async { + self.nsb.searchForServices(ofType: "_http._tcp.", inDomain: "local.") + } + } + func updateInterface () { + for service in services { + + if service.port == -1 { + print("service \(service.name) of type \(service.type)" + + " not yet resolved") + service.delegate = self + service.resolve(withTimeout:10) + } else { + print("service \(service.name) of type \(service.type)," + + "port \(service.port), addresses \(String(describing: service.addresses))") + if service.name == "pcvolumecontrol" { + let serverIP: String! + let serverPort: Int32! + print("Our service '\(service.name)' was located!") + // now we know the IP and port of the server. + if let addresses = service.addresses { + guard let ip = getIPV4StringfromAddress(address: addresses) else { return } + serverIP = ip + serverPort = Int32(service.port) + // segue to the main VC with the server and port now. + lastServerSubject.onNext((serverIP, serverPort)) + self.delegate?.didFindServer(ip: serverIP, port: serverPort) + } + } + } + } + } + + // Used to get an IP address out of the array of data within service.addresses + func getIPV4StringfromAddress(address: [Data]?) -> String? { + + guard let addr = address else { return "" } + let data = addr.first as! NSData + + var ip1 = UInt8(0) + data.getBytes(&ip1, range: NSMakeRange(4, 1)) + + var ip2 = UInt8(0) + data.getBytes(&ip2, range: NSMakeRange(5, 1)) + + var ip3 = UInt8(0) + data.getBytes(&ip3, range: NSMakeRange(6, 1)) + + var ip4 = UInt8(0) + data.getBytes(&ip4, range: NSMakeRange(7, 1)) + + let ipStr = String(format: "%d.%d.%d.%d",ip1,ip2,ip3,ip4); + + return ipStr + } + + func netServiceDidResolveAddress(_ sender: NetService) { + self.updateInterface() + } + + func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) { + print("search starting.") + } + private func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch error: Error) { + print("error!") + } + + func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser) { + print("search was stopped") + } + + func netServiceBrowser(_ aNetServiceBrowser: NetServiceBrowser, didFind aNetService: NetService, moreComing: Bool) { + print("adding a service") + self.services.append(aNetService) + if !moreComing { + self.updateInterface() + } + } + + func netServiceBrowser(_ aNetServiceBrowser: NetServiceBrowser, didRemove aNetService: NetService, moreComing: Bool) { + if let ix = self.services.index(of:aNetService) { + if let address = self.services[ix].addresses { + let v4Addr = self.getIPV4StringfromAddress(address: address) + self.delegate?.serverUnregistered(ip: v4Addr!, port: Int32(self.services[ix].port)) + } + self.services.remove(at:ix) + + print("removing a service") + if !moreComing { + self.updateInterface() + } + } + } + + func didFindServer(ip: String, port: Int32) { + } +} diff --git a/PcVolumeControl/StreamController.swift b/PcVolumeControl/StreamController.swift index 7403b15..cd608bc 100644 --- a/PcVolumeControl/StreamController.swift +++ b/PcVolumeControl/StreamController.swift @@ -73,6 +73,9 @@ class StreamController: NSObject { // Signal to delegates that the socket is open. self.delegate?.didConnectToServer() } + func didDiscovermDNSService() { + + } func disconnect() { if let cs = clientSocket { diff --git a/PcVolumeControl/ViewController.swift b/PcVolumeControl/ViewController.swift index 4c8f6ed..cde77e1 100644 --- a/PcVolumeControl/ViewController.swift +++ b/PcVolumeControl/ViewController.swift @@ -85,10 +85,11 @@ class ViewController: UIViewController, UITextFieldDelegate { @IBOutlet weak var disconnectButton: UIBarButtonItem! @IBOutlet weak var editCellsButton: UIBarButtonItem! @IBAction func disconnectButtonClicked(_ sender: UIBarButtonItem) { - print("Disconnect requested by user...") - disconnectRequested = true - SController?.disconnect() - bailToConnectScreen() + return +// print("Disconnect requested by user...") +// disconnectRequested = true +// SController?.disconnect() +// bailToConnectScreen() } @IBAction func editCellButtonClicked(_ sender: UIBarButtonItem) { @@ -103,6 +104,11 @@ class ViewController: UIViewController, UITextFieldDelegate { } } + override func viewWillAppear(_ animated: Bool) { + navigationItem.title = "192.168.2.78" + + } + override func viewDidLoad() { super.viewDidLoad() @@ -194,14 +200,16 @@ class ViewController: UIViewController, UITextFieldDelegate { // If the app is brought out of the background, we _always_ restart. print("App moved to foreground. Force reconnection...") if alreadySwitched == false { - bailToConnectScreen() +// bailToConnectScreen() + reconnect() alreadySwitched = true } } func appDidBecomeActive() { print("App moved from background selection screen to foreground.") if alreadySwitched == false { - bailToConnectScreen() +// bailToConnectScreen() + reconnect() } } @@ -513,7 +521,7 @@ extension ViewController: StreamControllerDelegate { func bailToConnectScreen() { // used if the TCP controller detects problems DispatchQueue.main.async { - self.performSegue(withIdentifier: "BackToStartSegue", sender: "abort") +// self.performSegue(withIdentifier: "BackToStartSegue", sender: "abort") } } func tearDownConnection() { diff --git a/PcVolumeControl/ViewControllerStart.swift b/PcVolumeControl/ViewControllerStart.swift index a4c0a8f..48fdaa0 100644 --- a/PcVolumeControl/ViewControllerStart.swift +++ b/PcVolumeControl/ViewControllerStart.swift @@ -14,10 +14,11 @@ class ViewControllerStart: UIViewController, UITextFieldDelegate { // This is controlling the initial connection screen. let asyncQueue = DispatchQueue(label: "asyncQueue", attributes: .concurrent) - var serverConnection: Bool? var spinnerView: UIView? = nil var SController: StreamController? = nil var defaults = UserDefaults.standard // to persist the IP/port entered previously + var serverIP: String! + var serverPort: Int32! @IBOutlet weak var Cbutton: UIButton! @IBOutlet weak var ServerIPField: UITextField! @@ -29,45 +30,34 @@ class ViewControllerStart: UIViewController, UITextFieldDelegate { } @IBAction func ConnectButtonClicked(_ sender: UIButton) { + // handoff to the main viewcontroller - guard let PortNum = Int32(self.ServerPortField.text!) else { - let _ = Alert.showBasic(title: "Error", message: "Bad port number specified.\nThe default is 3000. The port number needs to be between 1-65535.", vc: self) - return - } - guard let IPaddr = self.ServerIPField.text else { - return - } - - // Check if the IP field is blank. - if IPaddr.isEmpty { - let _ = Alert.showBasic(title: "Error", message: "A host name or IPv4 address is required in order to connect to your PCVolumeControl server.", vc: self) - return - } else { - // They entered *something* - if IPaddr.matches("^[0-9]") && IPaddr.matches("[0-9]$") { - // pretty sure it's an IP address, but is it valid? - if !isValidIP(s: IPaddr) { - let _ = Alert.showBasic(title: "Error", message: "The entry '\(IPaddr)' was not a valid IPv4 address.", vc: self) - return - } - } - } - - defaults.set(IPaddr, forKey: "IPaddr") - defaults.set(PortNum, forKey: "PortNum") - defaults.synchronize() +// guard let PortNum = Int32(self.ServerPortField.text!) else { +// let _ = Alert.showBasic(title: "Error", message: "Bad port number specified.\nThe default is 3000. The port number needs to be between 1-65535.", vc: self) +// return +// } +// guard let IPaddr = self.ServerIPField.text else { +// return +// } +// +// // Check if the IP field is blank. +// if IPaddr.isEmpty { +// let _ = Alert.showBasic(title: "Error", message: "A host name or IPv4 address is required in order to connect to your PCVolumeControl server.", vc: self) +// return +// } else { +// // They entered *something* +// if IPaddr.matches("^[0-9]") && IPaddr.matches("[0-9]$") { +// // pretty sure it's an IP address, but is it valid? +// if !isValidIP(s: IPaddr) { +// let _ = Alert.showBasic(title: "Error", message: "The entry '\(IPaddr)' was not a valid IPv4 address.", vc: self) +// return +// } +// } +// } - SController = StreamController(address: IPaddr, port: PortNum, delegate: self) - SController?.delegate = self - // Start looking for messages in the publish subject. - SController?.processMessages() - // Make the initial server connection and get the first message. - asyncQueue.async { - self.SController?.connectNoSend() - } } override func viewDidLoad() { @@ -88,26 +78,26 @@ class ViewControllerStart: UIViewController, UITextFieldDelegate { // connect button styling Cbutton.styleButton(cornerRadius: 8, borderWidth: 2, borderColor: UIColor.gray.cgColor) - addDoneButtonOnKeyboard() +// addDoneButtonOnKeyboard() if let ipaddr = defaults.string(forKey: "IPaddr") { - ServerIPField.text = ipaddr + ServerIPField.text = "remove me" } if let port = defaults.string(forKey: "PortNum") { - ServerPortField.text = port + ServerPortField.text = "remove me" } } // used to move between IP and Port fields - func textFieldShouldReturn(_ textField: UITextField) -> Bool - { - if let nextField = textField.superview?.viewWithTag(textField.tag + 1) as? UITextField { - nextField.becomeFirstResponder() - } else { - textField.resignFirstResponder() - } - return false - } +// func textFieldShouldReturn(_ textField: UITextField) -> Bool +// { +// if let nextField = textField.superview?.viewWithTag(textField.tag + 1) as? UITextField { +// nextField.becomeFirstResponder() +// } else { +// textField.resignFirstResponder() +// } +// return false +// } override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent @@ -117,114 +107,158 @@ class ViewControllerStart: UIViewController, UITextFieldDelegate { super.didReceiveMemoryWarning() } - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if segue.identifier == "ConnectSegue" { - let destVC = segue.destination as! ViewController - destVC.SController = self.SController // give them our stream controller - destVC.initialDraw = true // signal to viewDidLoad() to reload everything. - } - } - func addDoneButtonOnKeyboard() - { - let doneToolbar: UIToolbar = UIToolbar() - doneToolbar.barStyle = UIBarStyle.default - - let flexSpace = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.flexibleSpace, target: nil, action: nil) - let done: UIBarButtonItem = UIBarButtonItem(title: "Done", style: UIBarButtonItemStyle.done, target: self, action: #selector(ViewControllerStart.doneButtonAction(_:))) - var items = [UIBarButtonItem]() - items.append(flexSpace) - items.append(done) - items.append(flexSpace) - - doneToolbar.items = items - doneToolbar.sizeToFit() - - ServerPortField.inputAccessoryView = doneToolbar - - } - - @objc func doneButtonAction(_ sender: UIBarButtonItem!) - { - view.endEditing(true) - } - func isValidIP(s: String) -> Bool { - let parts = s.components(separatedBy: ".") - let nums = parts.flatMap { Int($0) } - return parts.count == 4 && nums.count == 4 && nums.filter { $0 >= 0 && $0 < 256}.count == 4 - } +// override func prepare(for segue: UIStoryboardSegue, sender: Any?) { +// if segue.identifier == "ConnectSegue" { +// let destVC = segue.destination as! ViewController +// destVC.SController = self.SController // give them our stream controller +// destVC.initialDraw = true // signal to viewDidLoad() to reload everything. +// } +// } +// func addDoneButtonOnKeyboard() +// { +// let doneToolbar: UIToolbar = UIToolbar() +// doneToolbar.barStyle = UIBarStyle.default +// +// let flexSpace = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.flexibleSpace, target: nil, action: nil) +// let done: UIBarButtonItem = UIBarButtonItem(title: "Done", style: UIBarButtonItemStyle.done, target: self, action: #selector(ViewControllerStart.doneButtonAction(_:))) +// var items = [UIBarButtonItem]() +// items.append(flexSpace) +// items.append(done) +// items.append(flexSpace) +// +// doneToolbar.items = items +// doneToolbar.sizeToFit() +// +// ServerPortField.inputAccessoryView = doneToolbar +// +// } +// +// @objc func doneButtonAction(_ sender: UIBarButtonItem!) +// { +// view.endEditing(true) +// } +// func isValidIP(s: String) -> Bool { +// let parts = s.components(separatedBy: ".") +// let nums = parts.flatMap { Int($0) } +// return parts.count == 4 && nums.count == 4 && nums.filter { $0 >= 0 && $0 < 256}.count == 4 +// } } -extension ViewControllerStart: StreamControllerDelegate { - func isAttemptingConnection() { - print("Connection is in progress...") - - asyncQueue.async { - DispatchQueue.main.async { - self.spinnerView = UIViewController.displaySpinner(onView: self.view) - } - } - } - func didConnectToServer() { - print("Server connection complete. Moving to main VC.") - asyncQueue.async { - DispatchQueue.main.async { - if let spinner = self.spinnerView { - UIViewController.removeSpinner(spinner: spinner) - } - // go to the next screen, pushing along the stream controller instance. - self.performSegue(withIdentifier: "ConnectSegue", sender: self.SController) - } - } - } - - func failedToConnect() { - if let spinner = spinnerView { - UIViewController.removeSpinner(spinner: spinner) - DispatchQueue.main.async { - let _ = Alert.showBasic(title: "Connection Error", message: "Connection to the server failed.\n\nIs the IP or name correct?\nIs the port open?", vc: self) - } - } - } - func didGetServerUpdate() {} - func bailToConnectScreen() {} - func tearDownConnection() {} -} +//extension ViewControllerStart: StreamControllerDelegate { +// func isAttemptingConnection() { +// print("Connection is in progress...") +// +// asyncQueue.async { +// DispatchQueue.main.async { +// self.spinnerView = UIViewController.displaySpinner(onView: self.view) +// } +// } +// } +// func didConnectToServer() { +// print("Server connection complete. Moving to main VC.") +// asyncQueue.async { +// DispatchQueue.main.async { +// if let spinner = self.spinnerView { +// UIViewController.removeSpinner(spinner: spinner) +// } +// // go to the next screen, pushing along the stream controller instance. +// self.performSegue(withIdentifier: "ConnectSegue", sender: self.SController) +// } +// } +// } +// +// func failedToConnect() { +// if let spinner = spinnerView { +// UIViewController.removeSpinner(spinner: spinner) +// DispatchQueue.main.async { +// let _ = Alert.showBasic(title: "Connection Error", message: "Connection to the server failed.\n\nIs the IP or name correct?\nIs the port open?", vc: self) +// } +// } +// } +// func didGetServerUpdate() {} +// func bailToConnectScreen() {} +// func tearDownConnection() {} +//} // This is the spinner shown when the socket is being set up. -extension UIViewController { - class func displaySpinner(onView : UIView) -> UIView { - - let spinnerView = UIView.init(frame: onView.bounds) - spinnerView.backgroundColor = UIColor.init(red: 0.5, green: 0.5, blue: 0.5, alpha: 0.5) - let ai = UIActivityIndicatorView.init(activityIndicatorStyle: .whiteLarge) - ai.startAnimating() - ai.center = spinnerView.center - - spinnerView.addSubview(ai) - onView.addSubview(spinnerView) +//extension UIViewController { +// class func displaySpinner(onView : UIView) -> UIView { +// +// let spinnerView = UIView.init(frame: onView.bounds) +// spinnerView.backgroundColor = UIColor.init(red: 0.5, green: 0.5, blue: 0.5, alpha: 0.5) +// let ai = UIActivityIndicatorView.init(activityIndicatorStyle: .whiteLarge) +// ai.startAnimating() +// ai.center = spinnerView.center +// +// spinnerView.addSubview(ai) +// onView.addSubview(spinnerView) +// +// return spinnerView +// } +// +// class func removeSpinner(spinner :UIView) { +// DispatchQueue.main.async { +// spinner.removeFromSuperview() +// } +// } +//} - return spinnerView - } - - class func removeSpinner(spinner :UIView) { - DispatchQueue.main.async { - spinner.removeFromSuperview() - } - } -} +//extension ViewControllerStart: NetServiceBrowserDelegate, NetServiceDelegate { +// // This is the mDNS extension on the start screen: auto-discovery of the server. +// +// func updateInterface () { +// for service in services { +// if service.port == -1 { +// print("service \(service.name) of type \(service.type)" + +// " not yet resolved") +// service.delegate = self +// service.resolve(withTimeout:10) +// } else { +// print("service \(service.name) of type \(service.type)," + +// "port \(service.port), addresses \(String(describing: service.addresses))") +// if service.name == "pcvolumecontrol" { +// print("Our service '\(service.name)' was located!") +// // now we know the IP and port of the server. +// if let addresses = service.addresses { +// guard let ip = getIPV4StringfromAddress(address: addresses) else { return } +// serverIP = ip +// serverPort = Int32(service.port) +// print("Connecting to server \(serverIP):\(serverPort)") +//// defaults.set(serverIP, forKey: "IPaddr") +//// defaults.set(serverPort, forKey: "PortNum") +//// defaults.synchronize() +// +// SController = StreamController(address: serverIP, port: serverPort, delegate: self) +// SController?.delegate = self +// +// // Start looking for messages in the publish subject. +// SController?.processMessages() +// +// // Make the initial server connection and get the first message. +// asyncQueue.async { +// self.SController?.connectNoSend() +// } +// } +// } +// } +// } +// } +// +//} // Customizing the Connect button a bit -extension UIButton { - func styleButton(cornerRadius: CGFloat, borderWidth: CGFloat, borderColor: CGColor) { - self.layer.cornerRadius = cornerRadius - self.layer.borderWidth = borderWidth - self.layer.borderColor = borderColor - } -} +//extension UIButton { +// func styleButton(cornerRadius: CGFloat, borderWidth: CGFloat, borderColor: CGColor) { +// self.layer.cornerRadius = cornerRadius +// self.layer.borderWidth = borderWidth +// self.layer.borderColor = borderColor +// } +//} + +//// Extend String so we can allow regex matching on the domain name/IP entered. +//extension String { +// func matches(_ regex: String) -> Bool { +// return self.range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil +// } +//} -// Extend String so we can allow regex matching on the domain name/IP entered. -extension String { - func matches(_ regex: String) -> Bool { - return self.range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil - } -}