diff --git a/BluetoothExplorer.xcodeproj/project.pbxproj b/BluetoothExplorer.xcodeproj/project.pbxproj index b00e877..79c6fa8 100644 --- a/BluetoothExplorer.xcodeproj/project.pbxproj +++ b/BluetoothExplorer.xcodeproj/project.pbxproj @@ -23,6 +23,9 @@ 353557D020E28CD200543440 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353557CF20E28CD200543440 /* String.swift */; }; 353557D620E2D15000543440 /* PnPIDCharacteristic.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 353557D520E2D15000543440 /* PnPIDCharacteristic.storyboard */; }; 353557DF20E2D17300543440 /* PnPIDCharacteristicViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353557DE20E2D17300543440 /* PnPIDCharacteristicViewController.swift */; }; + 35F5BB5A20E3DAC500AC5FF1 /* InputTextView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 35F5BB5920E3DAC500AC5FF1 /* InputTextView.xib */; }; + 35F5BB6320E3DB8A00AC5FF1 /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F5BB6220E3DB8A00AC5FF1 /* InputTextView.swift */; }; + 35F5BB6520E3DC0000AC5FF1 /* NibDesignable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F5BB6420E3DC0000AC5FF1 /* NibDesignable.swift */; }; 6E13E8CC20D9C7270007111B /* AdvertisementDataManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E13E8CB20D9C7270007111B /* AdvertisementDataManagedObject.swift */; }; 6E13E8E320D9D8290007111B /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E13E8E220D9D8290007111B /* TableViewController.swift */; }; 6E353A4620D7FDB200E94B73 /* CoreDataEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E353A4520D7FDB200E94B73 /* CoreDataEncodable.swift */; }; @@ -196,6 +199,9 @@ 353557CF20E28CD200543440 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 353557D520E2D15000543440 /* PnPIDCharacteristic.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PnPIDCharacteristic.storyboard; sourceTree = ""; }; 353557DE20E2D17300543440 /* PnPIDCharacteristicViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PnPIDCharacteristicViewController.swift; sourceTree = ""; }; + 35F5BB5920E3DAC500AC5FF1 /* InputTextView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InputTextView.xib; sourceTree = ""; }; + 35F5BB6220E3DB8A00AC5FF1 /* InputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = ""; }; + 35F5BB6420E3DC0000AC5FF1 /* NibDesignable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NibDesignable.swift; sourceTree = ""; }; 6E13E8CB20D9C7270007111B /* AdvertisementDataManagedObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvertisementDataManagedObject.swift; sourceTree = ""; }; 6E13E8E220D9D8290007111B /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 6E353A4520D7FDB200E94B73 /* CoreDataEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataEncodable.swift; sourceTree = ""; }; @@ -372,6 +378,9 @@ children = ( 6EC0D9C620DE076A005FB128 /* Appearance.swift */, 6EDE00D120DB47FD002E951A /* CustomButton.swift */, + 35F5BB5920E3DAC500AC5FF1 /* InputTextView.xib */, + 35F5BB6220E3DB8A00AC5FF1 /* InputTextView.swift */, + 35F5BB6420E3DC0000AC5FF1 /* NibDesignable.swift */, ); path = View; sourceTree = ""; @@ -610,6 +619,7 @@ 35335A1A20E14830006ABD4D /* ManufacturerNameStringCharacteristic.storyboard in Resources */, 35335A1220E13FC0006ABD4D /* FirmwareRevisionStringCharacteristic.storyboard in Resources */, 35335A1620E14353006ABD4D /* SoftwareRevisionStringCharacteristic.storyboard in Resources */, + 35F5BB5A20E3DAC500AC5FF1 /* InputTextView.xib in Resources */, 35335A2B20E16D4F006ABD4D /* DateTimeCharacteristic.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -642,6 +652,7 @@ 6E8C68E920DA02DA00232169 /* PresentPopover.swift in Sources */, 6EFB3E9520D3EFFB00364CA0 /* PeripheralManagedObject.swift in Sources */, 35335A1C20E14846006ABD4D /* ManufacturerNameStringCharacteristicViewController.swift in Sources */, + 35F5BB6320E3DB8A00AC5FF1 /* InputTextView.swift in Sources */, 6ECCCC6E20E091070054663C /* FDStackView.m in Sources */, 6E8C68F220DA340C00232169 /* PeripheralServicesViewController.swift in Sources */, 6E98B6982059B51000B6F016 /* PeripheralsViewController.swift in Sources */, @@ -674,6 +685,7 @@ 6ECCCC6C20E091070054663C /* FDTransformLayer.m in Sources */, 6E98B6962059B51000B6F016 /* AppDelegate.swift in Sources */, 353557DF20E2D17300543440 /* PnPIDCharacteristicViewController.swift in Sources */, + 35F5BB6520E3DC0000AC5FF1 /* NibDesignable.swift in Sources */, 6E8C68E520DA02B600232169 /* ErrorAlert.swift in Sources */, 6ECCCC7020E091070054663C /* FDStackViewAlignmentLayoutArrangement.m in Sources */, 6ECCCC7220E091070054663C /* FDStackViewLayoutArrangement.m in Sources */, diff --git a/BluetoothExplorer/Controller/GATT Characteristic Editors/SystemIDCharacteristicViewController.swift b/BluetoothExplorer/Controller/GATT Characteristic Editors/SystemIDCharacteristicViewController.swift index fbe69be..f66a24e 100644 --- a/BluetoothExplorer/Controller/GATT Characteristic Editors/SystemIDCharacteristicViewController.swift +++ b/BluetoothExplorer/Controller/GATT Characteristic Editors/SystemIDCharacteristicViewController.swift @@ -13,9 +13,9 @@ final class SystemIDCharacteristicViewController: UIViewController, Characterist // MARK: - IB Outlets - @IBOutlet private(set) var manufacturerIdentifierTextField: UITextField! + @IBOutlet weak var menufacturerIdentifierInputText: InputTextView! - @IBOutlet private(set) var organizationallyUniqueIdentifierTextField: UITextField! + @IBOutlet weak var organizationIdentifierInputText: InputTextView! // MARK: - Properties @@ -29,30 +29,30 @@ final class SystemIDCharacteristicViewController: UIViewController, Characterist super.viewDidLoad() configureView() - } - - // MARK: - Actions - - @IBAction func textFieldEditingChanged(_ sender: Any) { - - guard let manufacturerIdentifierText = manufacturerIdentifierTextField.text - else { showErrorAlert("Manufacturer Identifier is mandatory"); return } - - guard let organizationallyUniqueIdentifierText = organizationallyUniqueIdentifierTextField.text - else { showErrorAlert("Organization Identifier is mandatory"); return } - guard let manufacturerIdentifier = UInt64(manufacturerIdentifierText) - else { showErrorAlert("The entered number is too long"); return } + menufacturerIdentifierInputText.validator = { value in + + guard value.trim() != "" + else { return .none } + + // TODO: Use Bluetooth max value + guard let manufacturerIdValue = UInt64(value), manufacturerIdValue <= 1099511627775 + else { return .error("Maximum value is \(1099511627775)") } + + return .none + } - guard let organizationallyUniqueIdentifier = UInt32(organizationallyUniqueIdentifierText) - else { showErrorAlert("The entered number is too long"); return } - - guard let systemID = GATTSystemID(manufacturerIdentifier: manufacturerIdentifier, - organizationallyUniqueIdentifier: organizationallyUniqueIdentifier) - else { return } - - value = systemID - valueDidChange?(value) + organizationIdentifierInputText.validator = { value in + + guard value.trim() != "" + else { return .none } + + // TODO: Use Bluetooth max value + guard let organizationIdvalue = UInt32(value), organizationIdvalue <= 16777215 + else { return .error("Maximum value is \(16777215)") } + + return .none + } } // MARK: - Methods @@ -63,16 +63,16 @@ final class SystemIDCharacteristicViewController: UIViewController, Characterist updateText() - manufacturerIdentifierTextField.keyboardType = .numberPad - organizationallyUniqueIdentifierTextField.keyboardType = .numberPad + menufacturerIdentifierInputText.keyboardType = .numberPad + organizationIdentifierInputText.keyboardType = .numberPad } private func updateText() { - manufacturerIdentifierTextField.isEnabled = valueDidChange != nil - organizationallyUniqueIdentifierTextField.isEnabled = valueDidChange != nil - - manufacturerIdentifierTextField.text = "\(value.manufacturerIdentifier)" - organizationallyUniqueIdentifierTextField.text = "\(value.organizationallyUniqueIdentifier)" + menufacturerIdentifierInputText.isEnabled = valueDidChange != nil + organizationIdentifierInputText.isEnabled = valueDidChange != nil + + menufacturerIdentifierInputText.value = "\(value.manufacturerIdentifier)" + organizationIdentifierInputText.value = "\(value.organizationallyUniqueIdentifier)" } } diff --git a/BluetoothExplorer/Extensions/String.swift b/BluetoothExplorer/Extensions/String.swift index 9eaed1c..c10731e 100644 --- a/BluetoothExplorer/Extensions/String.swift +++ b/BluetoothExplorer/Extensions/String.swift @@ -11,7 +11,13 @@ import Foundation public extension String { func text(before text: String) -> String? { + guard let range = self.range(of: text) else { return nil } return String(self[self.startIndex.. String { + + return self.trimmingCharacters(in: CharacterSet.whitespaces) + } } diff --git a/BluetoothExplorer/SystemIDCharacteristic.storyboard b/BluetoothExplorer/SystemIDCharacteristic.storyboard index 1247535..2cf0a5d 100644 --- a/BluetoothExplorer/SystemIDCharacteristic.storyboard +++ b/BluetoothExplorer/SystemIDCharacteristic.storyboard @@ -21,51 +21,68 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - + - - - - - - + + + + - - + + - + diff --git a/BluetoothExplorer/View/InputTextView.swift b/BluetoothExplorer/View/InputTextView.swift new file mode 100644 index 0000000..2e96b0b --- /dev/null +++ b/BluetoothExplorer/View/InputTextView.swift @@ -0,0 +1,137 @@ +// +// InputTextView.swift +// BluetoothExplorer +// +// Created by Carlos Duclos on 6/27/18. +// Copyright © 2018 PureSwift. All rights reserved. +// + +import UIKit + +@IBDesignable +class InputTextView: NibDesignableView { + + typealias TextFieldEventClosure = (() -> Void)? + typealias ValidationClosure = ((String) -> Validation)? + + // MARK: - Properties + + @IBOutlet weak var fieldLabel: UILabel! + @IBOutlet weak var textField: UITextField! + @IBOutlet weak var messageLabel: UILabel! + + @IBInspectable + public var placeholder: String? { + + get { return textField.placeholder } + set { textField.placeholder = newValue } + } + + @IBInspectable + public var fieldLabelText: String? { + + get { return fieldLabel.text } + set { fieldLabel.text = newValue } + } + + @IBInspectable + public var value: String? { + + get { return textField.text } + set { textField.text = newValue } + } + + public var keyboardType: UIKeyboardType { + + get { return textField.keyboardType } + set { textField.keyboardType = newValue } + } + + public var isEnabled: Bool { + + get { return textField.isEnabled } + set { textField.isEnabled = newValue } + } + + private var validation: Validation = .none { + didSet { updateView() } + } + + var validator: ValidationClosure + + var onBeginEditing: TextFieldEventClosure + var onEndEditing: TextFieldEventClosure + var onPressReturn: TextFieldEventClosure + var onChange: TextFieldEventClosure + + public enum Validation { + case none + case error(String?) + } + + // MARK: - Initializers + + override public init(frame: CGRect) { + super.init(frame: frame) + self.setup() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.setup() + } + + // MARK: - Methods + + private func setup() { + + textField.delegate = self + textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged) + } + + private func updateView() { + + switch validation { + case .error(let message): + messageLabel.text = message + messageLabel.textColor = .red + + case .none: + messageLabel.text = "" + } + } + + private func updateValidation() { + + validation = validator?(textField.text ?? "") ?? .none + } + +} + +extension InputTextView: UITextFieldDelegate { + + func textFieldDidBeginEditing(_ textField: UITextField) { + + onBeginEditing?() + updateValidation() + } + + func textFieldDidEndEditing(_ textField: UITextField) { + + onEndEditing?() + updateValidation() + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + + onPressReturn?() + updateValidation() + return false + } + + @objc open func textFieldDidChange(_ textField: UITextField) { + + onChange?() + updateValidation() + } +} diff --git a/BluetoothExplorer/View/InputTextView.xib b/BluetoothExplorer/View/InputTextView.xib new file mode 100644 index 0000000..2df72a8 --- /dev/null +++ b/BluetoothExplorer/View/InputTextView.xib @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BluetoothExplorer/View/NibDesignable.swift b/BluetoothExplorer/View/NibDesignable.swift new file mode 100644 index 0000000..2977377 --- /dev/null +++ b/BluetoothExplorer/View/NibDesignable.swift @@ -0,0 +1,62 @@ +// +// NibDesignable.swift +// BluetoothExplorer +// +// Created by Carlos Duclos on 6/27/18. +// Copyright © 2018 PureSwift. All rights reserved. +// + +import UIKit + +public protocol NibDesignableProtocol: NSObjectProtocol { + + var nibContainerView: UIView { get } + + func loadNib() -> UIView + + func nibName() -> String +} + +extension NibDesignableProtocol { + + public func loadNib() -> UIView { + let bundle = Bundle(for: type(of: self)) + let nib = UINib(nibName: self.nibName(), bundle: bundle) + return nib.instantiate(withOwner: self, options: nil)[0] as! UIView // swiftlint:disable:this force_cast + } + + fileprivate func setupNib() { + let view = self.loadNib() + self.nibContainerView.addSubview(view) + view.translatesAutoresizingMaskIntoConstraints = false + let bindings = ["view": view] + self.nibContainerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|", options:[], metrics:nil, views: bindings)) + self.nibContainerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|", options:[], metrics:nil, views: bindings)) + } +} + +@objc +extension UIView { + + public var nibContainerView: UIView { + return self + } + + open func nibName() -> String { + return type(of: self).description().components(separatedBy: ".").last! + } +} + +@IBDesignable +open class NibDesignableView: UIView, NibDesignableProtocol { + + override public init(frame: CGRect) { + super.init(frame: frame) + self.setupNib() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.setupNib() + } +}