diff --git a/ExampleApp/ExampleApp.xcodeproj/project.pbxproj b/ExampleApp/ExampleApp.xcodeproj/project.pbxproj index 016784cb..1bf0db77 100644 --- a/ExampleApp/ExampleApp.xcodeproj/project.pbxproj +++ b/ExampleApp/ExampleApp.xcodeproj/project.pbxproj @@ -9,6 +9,10 @@ /* Begin PBXBuildFile section */ 8D4839A2254AAC0900266106 /* CharacteristicsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D4839A1254AAC0900266106 /* CharacteristicsViewController.swift */; }; 8D61CC8B254C321200952B2B /* CharacteristicInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D61CC8A254C321200952B2B /* CharacteristicInfo.swift */; }; + 8D61CC92254C479900952B2B /* PeripheralViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D61CC91254C479900952B2B /* PeripheralViewController.swift */; }; + 8D61CC96254C47BF00952B2B /* PeripheralView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D61CC95254C47BF00952B2B /* PeripheralView.swift */; }; + 8D61CC9A254C4A7800952B2B /* PeripheralReadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D61CC99254C4A7800952B2B /* PeripheralReadViewController.swift */; }; + 8D61CC9E254C4AA000952B2B /* PeripheralReadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D61CC9D254C4AA000952B2B /* PeripheralReadView.swift */; }; 8D72C58A2539C65000456D1A /* RxBluetoothKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8D72C5892539C65000456D1A /* RxBluetoothKit */; }; 8D72C58F2539C87300456D1A /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D72C58E2539C87300456D1A /* WelcomeView.swift */; }; 8D76F72B2546B5D200FF4DDB /* CentralServicesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D76F72A2546B5D200FF4DDB /* CentralServicesViewController.swift */; }; @@ -34,6 +38,10 @@ /* Begin PBXFileReference section */ 8D4839A1254AAC0900266106 /* CharacteristicsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacteristicsViewController.swift; sourceTree = ""; }; 8D61CC8A254C321200952B2B /* CharacteristicInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacteristicInfo.swift; sourceTree = ""; }; + 8D61CC91254C479900952B2B /* PeripheralViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralViewController.swift; sourceTree = ""; }; + 8D61CC95254C47BF00952B2B /* PeripheralView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralView.swift; sourceTree = ""; }; + 8D61CC99254C4A7800952B2B /* PeripheralReadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralReadViewController.swift; sourceTree = ""; }; + 8D61CC9D254C4AA000952B2B /* PeripheralReadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralReadView.swift; sourceTree = ""; }; 8D72C58E2539C87300456D1A /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; 8D76F72A2546B5D200FF4DDB /* CentralServicesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CentralServicesViewController.swift; sourceTree = ""; }; 8D76F7342546C4D900FF4DDB /* CentralServiceCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CentralServiceCell.swift; sourceTree = ""; }; @@ -79,6 +87,24 @@ path = CentralServiceChacacteristics; sourceTree = ""; }; + 8D61CC90254C478200952B2B /* Peripheral */ = { + isa = PBXGroup; + children = ( + 8D61CC91254C479900952B2B /* PeripheralViewController.swift */, + 8D61CC95254C47BF00952B2B /* PeripheralView.swift */, + ); + path = Peripheral; + sourceTree = ""; + }; + 8D61CC98254C4A6800952B2B /* PeripheralRead */ = { + isa = PBXGroup; + children = ( + 8D61CC99254C4A7800952B2B /* PeripheralReadViewController.swift */, + 8D61CC9D254C4AA000952B2B /* PeripheralReadView.swift */, + ); + path = PeripheralRead; + sourceTree = ""; + }; 8D72C5812539C32100456D1A /* Application */ = { isa = PBXGroup; children = ( @@ -102,11 +128,13 @@ 8D72C5832539C34400456D1A /* Screens */ = { isa = PBXGroup; children = ( + 8D61CC98254C4A6800952B2B /* PeripheralRead */, + 8D896D502542FFA900FD5FE5 /* Central */, + 8D896D492542FCB100FD5FE5 /* CentralList */, 8D4839A0254AABDC00266106 /* CentralServiceChacacteristics */, 8D76F7292546B5C500FF4DDB /* CentralServices */, - 8D896D502542FFA900FD5FE5 /* Central */, 8D72C5862539C3BD00456D1A /* CentralSpecific */, - 8D896D492542FCB100FD5FE5 /* CentralList */, + 8D61CC90254C478200952B2B /* Peripheral */, 8D72C5872539C3C700456D1A /* PeripheralUpdate */, 8D72C58D2539C86600456D1A /* Welcome */, ); @@ -287,10 +315,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8D61CC92254C479900952B2B /* PeripheralViewController.swift in Sources */, 8DF924C3254AE4F40027627D /* Error+Printable.swift in Sources */, 8D896D552542FFE000FD5FE5 /* CentralView.swift in Sources */, 8D61CC8B254C321200952B2B /* CharacteristicInfo.swift in Sources */, 8D4839A2254AAC0900266106 /* CharacteristicsViewController.swift in Sources */, + 8D61CC9E254C4AA000952B2B /* PeripheralReadView.swift in Sources */, + 8D61CC96254C47BF00952B2B /* PeripheralView.swift in Sources */, 8D76F7352546C4DA00FF4DDB /* CentralServiceCell.swift in Sources */, 8DB15AA9253D804A00DECF92 /* CentralSpecificView.swift in Sources */, 8DA476AA25399CFA00B79A0A /* WelcomeViewController.swift in Sources */, @@ -299,6 +330,7 @@ 8DA476A625399CFA00B79A0A /* AppDelegate.swift in Sources */, 8D896D522542FFB300FD5FE5 /* CentralViewController.swift in Sources */, 8D896D472542D7C500FD5FE5 /* AlertPresenter.swift in Sources */, + 8D61CC9A254C4A7800952B2B /* PeripheralReadViewController.swift in Sources */, 8D76F72B2546B5D200FF4DDB /* CentralServicesViewController.swift in Sources */, 8D896D5825430E7200FD5FE5 /* CentralListCell.swift in Sources */, 8D896D4B2542FCE400FD5FE5 /* CentralListViewController.swift in Sources */, diff --git a/ExampleApp/ExampleApp/Screens/Peripheral/PeripheralView.swift b/ExampleApp/ExampleApp/Screens/Peripheral/PeripheralView.swift new file mode 100644 index 00000000..04595f67 --- /dev/null +++ b/ExampleApp/ExampleApp/Screens/Peripheral/PeripheralView.swift @@ -0,0 +1,57 @@ +import UIKit + +class PeripheralView: UIView { + + init() { + super.init(frame: .zero) + backgroundColor = .systemBackground + setupLayout() + } + + required init?(coder: NSCoder) { nil } + + // MARK: - Subviews + + let updateButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Update", for: .normal) + button.setImage(UIImage(systemName: "sun.min"), for: .normal) + return button + }() + + let readButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Read", for: .normal) + button.setImage(UIImage(systemName: "phone.fill.arrow.up.right"), for: .normal) + return button + }() + + let writeButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Write", for: .normal) + button.setImage(UIImage(systemName: "phone.fill.arrow.down.left"), for: .normal) + return button + }() + + let stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 20.0 + return stackView + }() + + // MARK: - Private + + private func setupLayout() { + stackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(stackView) + [updateButton, readButton, writeButton].forEach(stackView.addArrangedSubview) + + NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: centerYAnchor), + stackView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor, constant: -32) + ]) + } + +} diff --git a/ExampleApp/ExampleApp/Screens/Peripheral/PeripheralViewController.swift b/ExampleApp/ExampleApp/Screens/Peripheral/PeripheralViewController.swift new file mode 100644 index 00000000..6ba1604e --- /dev/null +++ b/ExampleApp/ExampleApp/Screens/Peripheral/PeripheralViewController.swift @@ -0,0 +1,41 @@ +import UIKit + +class PeripheralViewController: UIViewController { + + init() { + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + // MARK: - View + + private(set) lazy var peripheralView = PeripheralView() + + override func loadView() { + view = peripheralView + } + + override func viewDidLoad() { + super.viewDidLoad() + + peripheralView.updateButton.addTarget(self, action: #selector(handleUpdateButton), for: .touchUpInside) + peripheralView.readButton.addTarget(self, action: #selector(handleReadButton), for: .touchUpInside) + peripheralView.writeButton.addTarget(self, action: #selector(handleWriteButton), for: .touchUpInside) + } + + // MARK: - Private + + @objc private func handleUpdateButton() { + let controller = PeripheralUpdateViewController() + navigationController?.pushViewController(controller, animated: true) + } + + @objc private func handleReadButton() { + let controller = PeripheralReadViewController() + navigationController?.pushViewController(controller, animated: true) + } + + @objc private func handleWriteButton() {} + +} diff --git a/ExampleApp/ExampleApp/Screens/PeripheralRead/PeripheralReadView.swift b/ExampleApp/ExampleApp/Screens/PeripheralRead/PeripheralReadView.swift new file mode 100644 index 00000000..b86eed51 --- /dev/null +++ b/ExampleApp/ExampleApp/Screens/PeripheralRead/PeripheralReadView.swift @@ -0,0 +1,61 @@ +import UIKit + +class PeripheralReadView: UIView { + + init() { + super.init(frame: .zero) + backgroundColor = .systemBackground + setupLayout() + } + + required init?(coder: NSCoder) { nil } + + // MARK: - Subviews + + let serviceUuidTextField: UITextField = { + let textField = UITextField() + textField.placeholder = "Service UUID" + return textField + }() + + let characteristicUuidTextField: UITextField = { + let textField = UITextField() + textField.placeholder = "Characteristic UUID" + return textField + }() + + let valueTextField: UITextField = { + let textField = UITextField() + textField.placeholder = "Value (String)" + return textField + }() + + let advertiseButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Advertise", for: .normal) + button.setImage(UIImage(systemName: "wave.3.right"), for: .normal) + return button + }() + + let stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 20.0 + return stackView + }() + + // MARK: - Private + + private func setupLayout() { + stackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(stackView) + [serviceUuidTextField, characteristicUuidTextField, valueTextField, advertiseButton].forEach(stackView.addArrangedSubview) + + NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: centerYAnchor), + stackView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor, constant: -32) + ]) + } + +} diff --git a/ExampleApp/ExampleApp/Screens/PeripheralRead/PeripheralReadViewController.swift b/ExampleApp/ExampleApp/Screens/PeripheralRead/PeripheralReadViewController.swift new file mode 100644 index 00000000..81d93493 --- /dev/null +++ b/ExampleApp/ExampleApp/Screens/PeripheralRead/PeripheralReadViewController.swift @@ -0,0 +1,98 @@ +import CoreBluetooth +import RxBluetoothKit +import RxSwift +import UIKit + +class PeripheralReadViewController: UIViewController { + + init() { + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + // MARK: - View + + private(set) lazy var peripheralReadView = PeripheralReadView() + + override func loadView() { + view = peripheralReadView + } + + override func viewDidLoad() { + super.viewDidLoad() + + peripheralReadView.advertiseButton.addTarget(self, action: #selector(handleAdvertiseButton), for: .touchUpInside) + } + + // MARK: - Private + + private let disposeBag = DisposeBag() + private lazy var manager = PeripheralManager() + private var characteristic: CBMutableCharacteristic? + private var advertisement: Disposable? + private var isAdvertising = false { + didSet { + let text = isAdvertising ? "Stop Advertising" : "Advertise" + peripheralReadView.advertiseButton.setTitle(text, for: .normal) + } + } + + @objc private func handleAdvertiseButton() { + isAdvertising ? handleAdvertisingStop() : handleAdvertisingStart() + } + + private func handleAdvertisingStart() { + guard let serviceUuidString = peripheralReadView.serviceUuidTextField.text, + let characteristicUuidString = peripheralReadView.characteristicUuidTextField.text, + let value = peripheralReadView.valueTextField.text else { return } + + let service = createService(uuidString: serviceUuidString) + let characteristic = createCharacteristic(uuidString: characteristicUuidString, value: value) + service.characteristics = [characteristic] + + startAdvertising(service: service) + self.characteristic = characteristic + } + + private func handleAdvertisingStop() { + advertisement?.dispose() + advertisement = nil + characteristic = nil + isAdvertising.toggle() + } + + private func createService(uuidString: String) -> CBMutableService { + let serviceUuid = CBUUID(string: uuidString) + return CBMutableService(type: serviceUuid, primary: true) + } + + private func createCharacteristic(uuidString: String, value: String) -> CBMutableCharacteristic { + let characteristicUuid = CBUUID(string: uuidString) + return CBMutableCharacteristic( + type: characteristicUuid, + properties: [.read], + value: value.data(using: .utf8), + permissions: [.readable] + ) + } + + private func startAdvertising(service: CBMutableService) { + let managerIsOn = manager.observeStateWithInitialValue() + .filter { $0 == .poweredOn } + + advertisement = Observable.combineLatest(managerIsOn, Observable.just(manager)) { $1 } + .flatMap { $0.add(service) } + .flatMap { [manager] in manager.startAdvertising($0.advertisingData) } + .subscribe( + onNext: { [weak self] in + print("advertising started! \($0)") + self?.isAdvertising.toggle() + }, + onError: { [weak self] in + AlertPresenter.presentError(with: $0.printable, on: self?.navigationController) + } + ) + } + +} diff --git a/ExampleApp/ExampleApp/Screens/PeripheralUpdate/PeripheralUpdateViewController.swift b/ExampleApp/ExampleApp/Screens/PeripheralUpdate/PeripheralUpdateViewController.swift index a856de38..a470bf36 100644 --- a/ExampleApp/ExampleApp/Screens/PeripheralUpdate/PeripheralUpdateViewController.swift +++ b/ExampleApp/ExampleApp/Screens/PeripheralUpdate/PeripheralUpdateViewController.swift @@ -13,18 +13,18 @@ class PeripheralUpdateViewController: UIViewController { // MARK: - View - private(set) lazy var peripheralView = PeripheralUpdateView() + private(set) lazy var peripheralUpdateView = PeripheralUpdateView() override func loadView() { - view = peripheralView + view = peripheralUpdateView } override func viewDidLoad() { super.viewDidLoad() setUpdate(enabled: false) - peripheralView.advertiseButton.addTarget(self, action: #selector(handleAdvertiseButton), for: .touchUpInside) - peripheralView.updateValueButton.addTarget(self, action: #selector(handleUpdateValueButton), for: .touchUpInside) + peripheralUpdateView.advertiseButton.addTarget(self, action: #selector(handleAdvertiseButton), for: .touchUpInside) + peripheralUpdateView.updateValueButton.addTarget(self, action: #selector(handleUpdateValueButton), for: .touchUpInside) } // MARK: - Private @@ -36,7 +36,7 @@ class PeripheralUpdateViewController: UIViewController { private var isAdvertising = false { didSet { let text = isAdvertising ? "Stop Advertising" : "Advertise" - peripheralView.advertiseButton.setTitle(text, for: .normal) + peripheralUpdateView.advertiseButton.setTitle(text, for: .normal) setUpdate(enabled: isAdvertising) } @@ -47,7 +47,7 @@ class PeripheralUpdateViewController: UIViewController { } @objc private func handleUpdateValueButton() { - guard let value = peripheralView.valueTextField.text, + guard let value = peripheralUpdateView.valueTextField.text, let data = value.data(using: .utf8), let characteristic = self.characteristic else { return } @@ -58,9 +58,9 @@ class PeripheralUpdateViewController: UIViewController { } private func handleAdvertisingStart() { - guard let serviceUuidString = peripheralView.serviceUuidTextField.text, - let characteristicUuidString = peripheralView.characteristicUuidTextField.text, - let value = peripheralView.valueTextField.text else { return } + guard let serviceUuidString = peripheralUpdateView.serviceUuidTextField.text, + let characteristicUuidString = peripheralUpdateView.characteristicUuidTextField.text, + let value = peripheralUpdateView.valueTextField.text else { return } let service = createService(uuidString: serviceUuidString) let characteristic = createCharacteristic(uuidString: characteristicUuidString, value: value) @@ -111,8 +111,8 @@ class PeripheralUpdateViewController: UIViewController { } private func setUpdate(enabled: Bool) { - peripheralView.valueTextField.isEnabled = enabled - peripheralView.updateValueButton.isEnabled = enabled + peripheralUpdateView.valueTextField.isEnabled = enabled + peripheralUpdateView.updateValueButton.isEnabled = enabled } } diff --git a/ExampleApp/ExampleApp/Screens/Welcome/WelcomeViewController.swift b/ExampleApp/ExampleApp/Screens/Welcome/WelcomeViewController.swift index dec0ec98..efed4d79 100644 --- a/ExampleApp/ExampleApp/Screens/Welcome/WelcomeViewController.swift +++ b/ExampleApp/ExampleApp/Screens/Welcome/WelcomeViewController.swift @@ -21,7 +21,7 @@ class WelcomeViewController: UIViewController { } @objc func handlePeripheralButtonTap() { - let peripheralController = PeripheralUpdateViewController() + let peripheralController = PeripheralViewController() navigationController?.pushViewController(peripheralController, animated: true) }