-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathViewController.swift
More file actions
556 lines (472 loc) · 22.1 KB
/
Copy pathViewController.swift
File metadata and controls
556 lines (472 loc) · 22.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
//
// ViewController.swift
// PcVolumeControl
//
// Created by Bill Booth on 11/21/17.
// Copyright © 2017 PcVolumeControl. All rights reserved.
//
import UIKit
import Foundation
@objcMembers
class ViewController: UIViewController, UITextFieldDelegate {
let protocolVersion = 7
var SController: StreamController?
var alreadySwitched: Bool?
var selectedDefaultDevice: (String, String)?
var allSessions = [Session]() // Array used to build slider table
var IPaddr: String!
var PortNum: UInt32?
var disconnectRequested: Bool = false
var initialDraw: Bool = false
var defaults = UserDefaults.standard
@IBOutlet weak var defaultDeviceView: UIView!
@IBOutlet weak var pickerTextField: UITextField!
@IBOutlet weak var masterVolumeSlider: DesignableSlider!
@IBOutlet weak var masterMuteButton: UISwitch!
@IBAction func masterMuteSwitch(_ sender: UISwitch) {
let id = SController?.fullState?.defaultDevice.deviceId
var defaultDevId: AMasterChannelUpdate.adflDevice?
// Volume is not changing.
guard let masterVolume = SController?.fullState?.defaultDevice.masterVolume else { return }
if sender.isOn {
// Unmute the master.
defaultDevId = AMasterChannelUpdate.adflDevice(deviceId: id!, masterMuted: false, masterVolume: Float(masterVolume))
}
else
{
// Mute the master.
defaultDevId = AMasterChannelUpdate.adflDevice(deviceId: id!, masterMuted: true, masterVolume: Float(masterVolume))
}
let data = AMasterChannelUpdate(protocolVersion: protocolVersion, defaultDevice: (defaultDevId!))
let encoder = JSONEncoder()
let dataAsBytes = try! encoder.encode(data)
dump(dataAsBytes)
// The data is supposed to be an array of Uint8.
guard let dataAsString = String(bytes: dataAsBytes, encoding: .utf8) else { return }
let dataWithNewline = dataAsString + "\n"
SController?.sendString(input: dataWithNewline)
}
@IBAction func masterVolumeChanged(_ sender: UISlider) {
guard let id = SController?.fullState?.defaultDevice.deviceId else { reloadTheWorld(); return }
var defaultDevId: AMasterChannelUpdate.adflDevice?
// mute value is not changing.
guard let masterMuted = SController?.fullState?.defaultDevice.masterMuted else { return }
let volumeValue = sender.value
defaultDevId = AMasterChannelUpdate.adflDevice(deviceId: id, masterMuted: masterMuted, masterVolume: volumeValue)
let data = AMasterChannelUpdate(protocolVersion: protocolVersion, defaultDevice: (defaultDevId)!)
let encoder = JSONEncoder()
let dataAsBytes = try! encoder.encode(data)
dump(dataAsBytes)
// The data is supposed to be an array of Uint8.
guard let dataAsString = String(bytes: dataAsBytes, encoding: .utf8) else { return }
let dataWithNewline = dataAsString + "\n"
SController?.sendString(input: dataWithNewline)
}
// bottom sliders for sessions
@IBOutlet weak var sliderTableView: UITableView!
// bottom toolbar buttons
@IBOutlet weak var bottomToolbar: UIToolbar!
// Very bottom toolbar with disconnect button
@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()
}
@IBAction func editCellButtonClicked(_ sender: UIBarButtonItem) {
// Toggle editing of the cells - reordering or deleting.
self.sliderTableView.isEditing = !self.sliderTableView.isEditing
if self.sliderTableView.isEditing {
sender.title = "Done"
sender.tintColor = .white
} else {
sender.title = "Reorder Sliders"
sender.tintColor = .none
}
}
override func viewDidLoad() {
super.viewDidLoad()
if initialDraw == true {
reloadTheWorld()
initialDraw = false
}
setNeedsStatusBarAppearanceUpdate() // white top status bar
disconnectRequested = false
// Detection that the app was minimized so we can close TCP connections
let notificationCenter = NotificationCenter.default
// If the app is backgrounded with the home button, tear down the TCP connection.
notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.willResignActiveNotification, object: nil)
// When the app is brough back to foreground, go to the initial connection screen.
notificationCenter.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
// table view stuff for the slider screen
sliderTableView.delegate = self
sliderTableView.dataSource = self
sliderTableView.tableFooterView = UIView(frame: CGRect.zero) // remove footer
sliderTableView.estimatedRowHeight = 140.0
sliderTableView.rowHeight = UITableView.automaticDimension
constructPicker()
alreadySwitched = false
// Make this a delegate for the TCP Stream Controller class.
SController?.delegate = self
if SController?.fullState?.defaultDevice == nil {
view.setNeedsDisplay()
}
}
// When going back to the start screen, make sure the modal here takes up the entire screen.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "BackToStartSegue" {
let destVC = segue.destination as UIViewController
destVC.modalPresentationStyle = .fullScreen
destVC.modalTransitionStyle = .crossDissolve
}
}
// white top carrier/battery bar
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
struct ADefaultDeviceUpdate : Codable {
struct adflDevice : Codable {
let deviceId: String
}
let protocolVersion: Int
let defaultDevice: adflDevice
}
struct AMasterChannelUpdate : Codable {
struct adflDevice : Codable {
let deviceId: String
let masterMuted: Bool
let masterVolume: Float
}
let protocolVersion: Int
let defaultDevice: adflDevice
}
struct ASessionUpdate : Codable {
struct adflDevice : Codable {
let sessions: [OneSession]
let deviceId: String
}
let protocolVersion: Int
let defaultDevice: adflDevice
}
struct OneSession : Codable {
let name: String
let id: String
let volume: Float
let muted: Bool
}
func appMovedToBackground() {
// Tear down the TCP connection any time they minimize or exit the app.
print("App moved to background. TCP Connection should be torn down now...")
if SController?.clientSocket?.isConnected == true {
disconnectRequested = true
SController?.disconnect()
}
}
func appWillEnterForeground() {
// If the app is brought out of the background, we _always_ restart.
print("App moved to foreground. Force reconnection...")
if alreadySwitched == false {
bailToConnectScreen()
alreadySwitched = true
}
}
func appDidBecomeActive() {
print("App moved from background selection screen to foreground.")
if alreadySwitched == false {
bailToConnectScreen()
}
}
func createDisconnectAlert(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Quit", style: UIAlertAction.Style.default, handler: { (action) in alert.dismiss(animated:true, completion: nil); self.bailToConnectScreen()}))
self.present(alert, animated: true, completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func initSessions() {
// This finds all sessions and overwrites the global session state.
print("Sessions are being re-intialized...")
allSessions.removeAll()
guard let sessions = SController?.fullState?.defaultDevice.sessions else {
createDisconnectAlert(title: "Whoops", message: "Full state sent from the server was not loaded for some reason.")
return
}
for x : FullState.Session in sessions {
allSessions.append(Session(id: x.id, muted: x.muted, name: x.name, volume: Double(x.volume)))
}
// always sort alphabetically for a consistent view
allSessions.sort {
$0.name.localizedCaseInsensitiveCompare($1.name) == ComparisonResult.orderedAscending
}
// While we are reloading here, initialize the title showing in the initial picker view.
guard let state = SController?.fullState?.defaultDevice.name else { return }
DispatchQueue.main.async {
self.pickerTextField.text = state
self.pickerTextField.tintColor = .clear
}
/*
of all the sessions here, if we have already reordered the stack, move our cells around
so the prioritized ones are up top and everything else appears below.
*/
if let dfl = defaults.object(forKey: "customOrderedCells") as? [String] {
// We found a customized ordering.
if dfl.count > allSessions.count {
// TODO: This is kinda hacky. There has to be a better way.
defaults.set([], forKey: "customOrderedCells")
defaults.synchronize()
return
}
for item in allSessions {
if dfl.contains(item.id) {
// The ID we are looking at has a custom ordering.
if let cIdx = dfl.firstIndex(of: item.id) {
// allSessions is mutated in this loop. Get the new index.
if let aIdx = allSessions.firstIndex(where: {$0.id == item.id}) {
allSessions.remove(at: aIdx)
allSessions.insert(item, at: cIdx)
}
}
}
}
}
}
func findDeviceId(longName: String) -> String {
// Look through the device IDs to get the short-form device ID.
// This takes in the long-form session ID as input.
var deviceName = "Unknown"
if let ids = SController?.fullState?.deviceIds {
for (shortId, _) in ids {
if longName.contains(shortId) {
deviceName = shortId
}
}
}
return deviceName
}
func reloadTheWorld() {
// Reload everything! All the things!
// bail if the client and server protocols mismatch.
if SController?.fullState?.protocolVersion != protocolVersion {
createDisconnectAlert(title: "Error", message: "Client and server protocols mismatch.")
}
guard let masterMuteState = SController?.fullState?.defaultDevice.masterMuted else {
print("Master mute state could not be set!")
return
}
masterMuteButton.isOn = !masterMuteState
let masterVolume = SController?.fullState?.defaultDevice.masterVolume ?? 50.0
masterVolumeSlider?.value = masterVolume
// Bottom slider table stack with all sessions
// re-populate the array of current sessions and reload the sliders.
initSessions()
sliderTableView.reloadData()
}
}
/*
This controls the picker view for the master/default device.
*/
extension ViewController: UIPickerViewDelegate, UIPickerViewDataSource {
func constructPicker() {
let devicePicker = UIPickerView()
devicePicker.delegate = self
pickerTextField.inputView = devicePicker
pickerTextField.text = selectedDefaultDevice?.1
// TODO: change the default selected item to be the current default.
createToolbar() // done button
}
func createToolbar() {
// make a toolbar with a 'done' button for the picker.
let toolBar = UIToolbar()
toolBar.sizeToFit()
let doneButton = UIBarButtonItem(title: "Set Output Device", style: .plain, target: self,
action: #selector(ViewController.outputDeviceSelected))
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) // shift it right
toolBar.setItems([spacer, doneButton, spacer], animated: false)
toolBar.isUserInteractionEnabled = true
pickerTextField.inputAccessoryView = toolBar
toolBar.barTintColor = .gray
toolBar.tintColor = .white
}
func getDeviceIds() -> [(String, String)] {
// return an array of tuples showing all available device IDs and pretty names
var y = [(String, String)]()
if SController == nil {
print("full state is nil. bailing...")
bailToConnectScreen()
}
if let deviceIDs = SController?.fullState?.deviceIds {
for (shortId, prettyName) in deviceIDs {
y.append((shortId, prettyName))
}
}
return y
}
func outputDeviceSelected() {
view.endEditing(true)
// When they select the default, we need to update state and send a new master device to the server.
guard let id = selectedDefaultDevice?.0 else {
// They didn't change anything. They just hit 'Done'.
return
}
let defaultDevId = ADefaultDeviceUpdate.adflDevice(deviceId: id)
let data = ADefaultDeviceUpdate(protocolVersion: protocolVersion, defaultDevice: defaultDevId)
let encoder = JSONEncoder()
let dataAsBytes = try! encoder.encode(data)
dump(dataAsBytes)
// The data is supposed to be an array of Uint8.
let dataAsString = String(bytes: dataAsBytes, encoding: .utf8)
let dataWithNewline = dataAsString! + "\n"
SController?.sendString(input: dataWithNewline)
}
//picker view overrides
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
let deviceids = getDeviceIds()
return deviceids[row].1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
let deviceids = getDeviceIds()
return deviceids.count
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
selectedDefaultDevice = getDeviceIds()[row]
pickerTextField.text = getDeviceIds()[row].1
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
// This code controls the tableView rows the sliders live in.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return allSessions.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "sliderCell") as! SliderCell
cell.delegate = self
let targetSession = allSessions[indexPath.row]
print("Updating cell index path: \(indexPath.row), target: \(targetSession.name)")
cell.setSessionParameter(session: targetSession)
// Customize the reordercontrol/hamburger icon background
cell.backgroundColor = .gray
// TODO: We really should disable the sliders/switches in each cell during editing.
return cell
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
// return .delete to get a delete button on the left side of the cell.
return .none
}
func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
// indentation on the left side of the cell
return false
}
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let movedObject = self.allSessions[sourceIndexPath.row]
allSessions.remove(at: sourceIndexPath.row)
allSessions.insert(movedObject, at: destinationIndexPath.row)
NSLog("%@", "\(sourceIndexPath.row) => \(destinationIndexPath.row) \(allSessions)")
// The sessions have been customized. Write the state/order into defaults so initsessions can read them.
var customOrdering = [String]()
for (index, session) in allSessions.enumerated() {
customOrdering.append(session.id)
print("Session '\(session.name)' custom-ordered to index: \(index)")
}
defaults.set(customOrdering, forKey: "customOrderedCells")
defaults.synchronize()
}
}
/*
The cells where the sessions show up are custom UITableViewCells.
*/
extension ViewController: SliderCellDelegate {
func didChangeVolume(id: String, newvalue: Double, name: String) {
print("\n\nVolume changed on \(id) to: \(newvalue)")
let defaultDeviceShortId = findDeviceId(longName: id)
for session in (SController?.fullState?.defaultDevice.sessions)! {
if session.id == id {
// Pull the current mute value for this session.
let currentMuteValue = session.muted
let encoder = JSONEncoder()
// TODO: return current muted state and use that to make the onesession instance.
let onesession = OneSession(name: name, id: id, volume: Float(newvalue), muted: currentMuteValue)
let adefault = ASessionUpdate.adflDevice(sessions: [onesession], deviceId: defaultDeviceShortId)
let data = ASessionUpdate(protocolVersion: protocolVersion, defaultDevice: adefault)
let dataAsBytes = try! encoder.encode(data)
let dataAsString = String(bytes: dataAsBytes, encoding: .utf8)
let dataWithNewline = dataAsString! + "\n"
SController?.sendString(input: dataWithNewline)
break
}
}
}
func didToggleMute(id: String, muted: Bool, name: String) {
print("\n\nMute button hit on session \(id) to value \(muted)")
let defaultDeviceShortId = findDeviceId(longName: id)
for session in (SController?.fullState?.defaultDevice.sessions)! {
if session.id == id {
// Pull the current volume value for this session.
let currentVolumeValue = session.volume
let encoder = JSONEncoder()
// TODO: return current muted state and use that to make the onesession instance.
let onesession = OneSession(name: name, id: id, volume: Float(currentVolumeValue), muted: !muted)
let adefault = ASessionUpdate.adflDevice(sessions: [onesession], deviceId: defaultDeviceShortId)
let data = ASessionUpdate(protocolVersion: protocolVersion, defaultDevice: adefault)
let dataAsBytes = try! encoder.encode(data)
let dataAsString = String(bytes: dataAsBytes, encoding: .utf8)
let dataWithNewline = dataAsString! + "\n"
SController?.sendString(input: dataWithNewline)
break
}
}
}
}
/*
The streamcontroller is used to keep track of the TCP socket to the server.
It also handles validation/coding of the JSON strings going to/from the server.
This view controller should not try to look for status on the stream controller
object here. Instead, delegates from the stream controller should be used to
signal important events to this view controller.
*/
extension ViewController: StreamControllerDelegate {
func didGetServerUpdate() {
// This is the top of everything. The entire UI is reloaded if this executes.
// It's only executed when the streamcontroller parses a valid JSON server message.
print("Server update detected. Reloading...\n")
DispatchQueue.main.async {
self.reloadTheWorld()
}
}
func bailToConnectScreen() {
// used if the TCP controller detects problems
DispatchQueue.main.async {
self.performSegue(withIdentifier: "BackToStartSegue", sender: "abort")
}
}
func tearDownConnection() {
// Check to see if the disconnect button took us here.
if disconnectRequested == true {
bailToConnectScreen()
return
}
// Something went wrong with the socket open to the server.
let alert = UIAlertController(title: "Error", message: "The server connection was lost.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { action in
self.bailToConnectScreen()
}))
DispatchQueue.main.async {
self.present(alert, animated: true)
}
}
func didConnectToServer() {}
func isAttemptingConnection() {}
func failedToConnect() {}
func reconnect() {
// This should tear down what we have, then cause a reload of everything.
SController?.disconnect()
SController?.connectNoSend()
}
}