SwitchesViewController: Modernize switches collection view controller

This switches to using the more modern collection view diffable data source API.
Also I moved the action cells into their own section so we don't need to do
arithmetic to determine whether or not a cell is an action cell or a device cell.
This commit is contained in:
2020-03-13 19:36:44 -07:00
parent 5b521541fe
commit f1f86ced29
3 changed files with 82 additions and 102 deletions
@@ -21,20 +21,25 @@ extension SwitchesViewControllerDelegate
}
class SwitchesViewController: UIViewController,
UICollectionViewDataSource,
UICollectionViewDelegateFlowLayout
{
weak var delegate: SwitchesViewControllerDelegate?
weak var delegate: SwitchesViewControllerDelegate?
fileprivate var _collectionView: UICollectionView = UICollectionView(frame: CGRect.zero,
collectionViewLayout: UICollectionViewFlowLayout())
fileprivate var _currentDevicesHash: Int = 0
fileprivate var _collectionView: UICollectionView = UICollectionView(frame: CGRect.zero,
collectionViewLayout: UICollectionViewFlowLayout())
fileprivate var _collectionViewDataSource: UICollectionViewDiffableDataSource<Int, Int>!
fileprivate var _currentDevicesHash: Int = 0
static fileprivate let collectionViewDeviceSwitchCellReuseIdentifier = "DeviceSwitchReuseID"
static fileprivate let collectionViewActionCellReuseIdentifier = "ActionCellReuseID"
static fileprivate let collectionViewCellsSpacing: CGFloat = 5.0
fileprivate enum ActionCell: Int
static fileprivate let actionCellsSectionIdentifier = 0
static fileprivate let switchCellsSectionIdentifier = 1
fileprivate enum ActionCell: Int, CaseIterable
{
case allOn
case allOff
@@ -49,13 +54,37 @@ class SwitchesViewController: UIViewController,
}
}
static let count: Int = {
var max = 0
while let _ = ActionCell(rawValue: max) { max += 1 }
return max
}()
static let count: Int = { return ActionCell.allCases.count }()
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
{
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
_collectionViewDataSource = UICollectionViewDiffableDataSource(collectionView: _collectionView, cellProvider: { (collectionView, indexPath, identifier) -> UICollectionViewCell? in
return self.collectionView(collectionView, cellForItemAt: indexPath, identifier: identifier)
})
// Initialize data source
var snapshot = _collectionViewDataSource.snapshot()
// Sections
snapshot.appendSections([
SwitchesViewController.actionCellsSectionIdentifier,
SwitchesViewController.switchCellsSectionIdentifier,
])
// Action cells
snapshot.appendItems([
SwitchesViewController.ActionCell.allOn.rawValue,
SwitchesViewController.ActionCell.allOff.rawValue
], toSection: SwitchesViewController.actionCellsSectionIdentifier)
_collectionViewDataSource.apply(snapshot, animatingDifferences: false)
}
required init?(coder: NSCoder) { fatalError("Unimplemented") }
override func viewDidLoad()
{
super.viewDidLoad()
@@ -69,7 +98,7 @@ class SwitchesViewController: UIViewController,
_collectionView.backgroundColor = UIColor.black
_collectionView.delegate = self
_collectionView.dataSource = self
_collectionView.dataSource = _collectionViewDataSource
_collectionView.register(WemoDeviceCellView.self, forCellWithReuseIdentifier: deviceCellReuseID)
_collectionView.register(WemoActionCellView.self, forCellWithReuseIdentifier: actionCellReuseID)
self.view.addSubview(_collectionView)
@@ -97,87 +126,37 @@ class SwitchesViewController: UIViewController,
return (d1.name.compare(d2.name, options: .caseInsensitive, range: nil, locale: nil) == .orderedAscending)
})
let hash = self.devices.reduce(0, {$0 ^ $1.hashValue})
if (hash != _currentDevicesHash) {
let previousSet = NSOrderedSet(array: oldValue)
let newSet = NSOrderedSet(array: self.devices)
var insertedIndexPaths: [IndexPath] = []
var updatedIndexPaths: [IndexPath] = []
var deletedIndexPaths: [IndexPath] = []
// if we have devices now and we didn't before, or vice versa,
// we need to update the action cells
if ((oldValue.count == 0 && self.devices.count != 0) || (self.devices.count == 0 && oldValue.count != 0)) {
for actionCellIdx in 0 ..< ActionCell.count {
let actionCellIndexPath = IndexPath(item: actionCellIdx, section: 0)
updatedIndexPaths.append(actionCellIndexPath)
}
}
// find deletes and updates
for (idx, device) in previousSet.enumerated() {
let itemIndex = idx + ActionCell.count
let curIndexPath = IndexPath(item: itemIndex, section: 0)
if (!newSet.contains(device)) {
deletedIndexPaths.append(curIndexPath)
} else if (idx < newSet.count) {
let deviceInNewSet = newSet.object(at: idx) as! WemoDevice
if (deviceInNewSet != (device as! WemoDevice)) {
updatedIndexPaths.append(curIndexPath)
}
}
}
// find insertions
for (idx, device) in newSet.enumerated() {
if (!previousSet.contains(device)) {
let itemIndex = idx + ActionCell.count
let insertedIndexPath = IndexPath(item: itemIndex, section: 0)
insertedIndexPaths.append(insertedIndexPath)
}
}
UIView.performWithoutAnimation { () -> Void in
self._collectionView.performBatchUpdates({ () -> Void in
self._collectionView.deleteItems(at: deletedIndexPaths)
self._collectionView.reloadItems(at: updatedIndexPaths)
self._collectionView.insertItems(at: insertedIndexPaths)
}, completion: nil)
}
_currentDevicesHash = hash
}
var snapshot = _collectionViewDataSource.snapshot()
snapshot.appendItems(self.devices.map { $0.hashValue }, toSection: SwitchesViewController.switchCellsSectionIdentifier)
_collectionViewDataSource.apply(snapshot, animatingDifferences: false)
}
}
// MARK: UICollectionView
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, identifier: Int) -> UICollectionViewCell?
{
return self.devices.count + ActionCell.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
if (indexPath.item < ActionCell.count) {
if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) {
let reuseID = SwitchesViewController.collectionViewActionCellReuseIdentifier
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseID, for: indexPath) as! WemoActionCellView
cell.textLabel.text = ActionCell(rawValue: indexPath.item)?.name().uppercased()
cell.textLabel.text = ActionCell(rawValue: identifier)?.name().uppercased()
cell.enabled = (self.devices.count > 0)
return cell
} else {
} else if (indexPath.section == SwitchesViewController.switchCellsSectionIdentifier) {
let reuseID = SwitchesViewController.collectionViewDeviceSwitchCellReuseIdentifier
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseID, for: indexPath) as! WemoDeviceCellView
let device = _deviceAtIndexPath(indexPath)
cell.deviceName = device.name
cell.toggled = (device.state == .on)
cell.ordinal = indexPath.item - ActionCell.count + 1
if let device = (self.devices.first { $0.hashValue == identifier }) {
cell.deviceName = device.name
cell.toggled = (device.state == .on)
cell.ordinal = indexPath.row
}
return cell
}
return nil
}
func collectionView(_ collectionView: UICollectionView,
@@ -206,28 +185,33 @@ class SwitchesViewController: UIViewController,
}
let dimensions = floor((collectionView.bounds.size.width / cellsPerRow) - ((spacing * (cellsPerRow - 1.0)) / cellsPerRow))
if (indexPath.item < ActionCell.count) {
if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) {
return CGSize(width: collectionView.bounds.size.width, height: rint(dimensions / 1.5))
} else {
return CGSize(width: dimensions, height: dimensions)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
{
let spacing = SwitchesViewController.collectionViewCellsSpacing
return UIEdgeInsets(top: spacing, left: 0, bottom: 0, right: 0)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
if (indexPath.item < ActionCell.count) {
if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) {
let tappedActionCell = ActionCell(rawValue: indexPath.item)
var currentDelay: TimeInterval = 0.0
for i in ActionCell.count ..< collectionView.numberOfItems(inSection: indexPath.section) {
if let cell = collectionView.cellForItem(at: IndexPath(item: i, section: indexPath.section)) as? WemoDeviceCellView {
for cell in collectionView.visibleCells {
if collectionView.indexPath(for: cell)?.section == SwitchesViewController.switchCellsSectionIdentifier {
let switchCell = cell as! WemoDeviceCellView
let animOptions = UIView.AnimationOptions([.allowUserInteraction])
UIView.animate(withDuration: 0.3, delay: currentDelay, options: animOptions, animations: {
cell.toggled = (tappedActionCell == .allOn)
let delay: TimeInterval = Double(switchCell.ordinal) * 0.05
UIView.animate(withDuration: 0.3, delay: delay, options: animOptions, animations: {
switchCell.toggled = (tappedActionCell == .allOn)
}, completion: nil)
currentDelay += 0.05
}
}
@@ -236,11 +220,11 @@ class SwitchesViewController: UIViewController,
}
self.delegate?.switchesViewControllerDidToggleDevices(self, devices: self.devices)
} else {
} else if (indexPath.section == SwitchesViewController.switchCellsSectionIdentifier) {
let cell = collectionView.cellForItem(at: indexPath) as! WemoDeviceCellView
cell.toggled = !cell.toggled
let device = _deviceAtIndexPath(indexPath)
let device = self.devices[indexPath.row]
device.state = (cell.toggled ? .on : .off)
self.delegate?.switchesViewControllerDidToggleDevices(self, devices: [device])
@@ -249,28 +233,15 @@ class SwitchesViewController: UIViewController,
func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool
{
if (indexPath.item < ActionCell.count) {
return (self.devices.count > 0)
} else {
return true
}
return self.collectionView(collectionView, shouldSelectItemAt: indexPath)
}
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool
{
if (indexPath.item < ActionCell.count) {
if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) {
return (self.devices.count > 0)
} else {
return true
}
}
// MARK: Internal
internal func _deviceAtIndexPath(_ indexPath: IndexPath) -> WemoDevice
{
let deviceIdx = indexPath.item - ActionCell.count
let device = self.devices[deviceIdx]
return device
}
}
@@ -1,215 +0,0 @@
//
// WemoServer.swift
// XIONControlPanel
//
// Created by Charles Magahern on 12/31/15.
// Copyright © 2015 XION. All rights reserved.
//
import Foundation
enum ConnectionStatus
{
case disconnected
case connecting
case connected
case error
}
enum ConnectionError : Error
{
case unknown
case serverUnavailable
}
class WemoServer
{
fileprivate(set) var baseURL: URL
fileprivate(set) var connected: Bool = false
fileprivate var _urlSession: URLSession
fileprivate var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream()
fileprivate var _operationQueue: OperationQueue = OperationQueue()
init(_ url: URL)
{
self.baseURL = url
let config = URLSessionConfiguration.default
_urlSession = URLSession(configuration: config)
_operationQueue.maxConcurrentOperationCount = 1
}
func connect(_ completion: @escaping (Error?) -> Void)
{
if (!self.connected) {
let op = ConnectOperation(baseURL: self.baseURL, session: _urlSession)
weak var weakOp = op
op.completionBlock = {
guard let strongOp = weakOp else { completion(nil) ; return }
if let error = strongOp.error {
self._logError("Error connecting to server", error: error)
} else {
self.connected = true
}
completion(strongOp.error)
}
_operationQueue.addOperation(op)
} else {
completion(nil)
}
}
func disconnect(_ completion: (Error?) -> Void)
{
self.connected = false
completion(nil)
}
func fetchDevices(_ completion: @escaping ([WemoDevice], Error?) -> Void)
{
if (self.connected) {
let op = FetchDevicesOperation(baseURL: self.baseURL, session: _urlSession)
weak var weakOp = op
op.completionBlock = {
guard let strongOp = weakOp else { completion([], nil) ; return }
if let error = strongOp.error {
self._logError("Error fetching devices", error: error)
}
completion(strongOp.devices, strongOp.error)
}
_operationQueue.addOperation(op)
} else {
let err = ConnectionError.serverUnavailable
completion([], err)
}
}
func toggleDevice(_ device: WemoDevice, state: WemoDevice.State, completion: @escaping (Error?) -> Void)
{
if (self.connected) {
let op = ToggleDeviceOperation(baseURL: self.baseURL, session: _urlSession, device: device, state: state)
weak var weakOp = op
op.completionBlock = {
guard let strongOp = weakOp else { completion(nil) ; return }
if let error = strongOp.error {
self._logError("Error toggling device", error: error)
}
completion(strongOp.error)
}
_operationQueue.addOperation(op)
} else {
let err = ConnectionError.serverUnavailable
completion(err)
}
}
// MARK: Internal
internal func _logError(_ description: String, error: Error)
{
print("ERROR: \(description) \(error)", to: &_errorStream)
}
}
internal class WemoOperation : Operation
{
var baseURL: URL
var session: URLSession
fileprivate(set) var error: Error?
init(baseURL: URL, session: URLSession)
{
self.baseURL = baseURL
self.session = session
}
}
internal class ConnectOperation : WemoOperation
{
override func main()
{
let semaphore = Semaphore(value: 0)
let url = self.baseURL.appendingPathComponent("api/environment")
var request = URLRequest(url: url)
request.httpMethod = "POST"
let task = self.session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
self.error = error
semaphore.signal()
})
task.resume()
semaphore.wait()
}
}
internal class FetchDevicesOperation : WemoOperation
{
private(set) var devices: [WemoDevice] = []
override func main()
{
let semaphore = Semaphore(value: 0)
let url = self.baseURL.appendingPathComponent("api/environment")
let task = self.session.dataTask(with: url, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
if (data != nil) {
self.devices = self._parseDevices(data!)
} else {
self.error = error
}
semaphore.signal()
})
task.resume()
semaphore.wait()
}
internal func _parseDevices(_ data: Data) -> [WemoDevice]
{
var devices: [WemoDevice] = []
if let responseDict = try? JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary {
for responseObj in responseDict.allValues {
if let deviceDict = responseObj as? NSDictionary {
let device = WemoDevice(deviceDict)
devices.append(device)
}
}
}
return devices
}
}
internal class ToggleDeviceOperation : WemoOperation
{
var device: WemoDevice
var state: WemoDevice.State
init(baseURL: URL, session: URLSession, device: WemoDevice, state: WemoDevice.State)
{
self.device = device
self.state = state
super.init(baseURL: baseURL, session: session)
}
override func main()
{
let semaphore = Semaphore(value: 0)
let stateArg = (self.state == .on ? "on" : "off")
let url = self.baseURL.appendingPathComponent("api/device/\(self.device.name)").URLByAppendingRequestParameters(["state" : stateArg])
var request = URLRequest(url: url!)
request.httpMethod = "POST"
let task = self.session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
self.error = error
semaphore.signal()
}
task.resume()
semaphore.wait()
}
}