From 5e09e5df00d40e75e7101118e90f93786374fd16 Mon Sep 17 00:00:00 2001 From: Eugene Berdnikov Date: Tue, 1 Sep 2020 06:41:38 -0700 Subject: [PATCH 01/10] NSBluetoothAlwaysUsageDescription key is added to iOS target Info.plist --- FlipperZero/iOS/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FlipperZero/iOS/Info.plist b/FlipperZero/iOS/Info.plist index cb3c1de93..bcfb1aa9f 100644 --- a/FlipperZero/iOS/Info.plist +++ b/FlipperZero/iOS/Info.plist @@ -20,6 +20,8 @@ $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS + NSBluetoothAlwaysUsageDescription + Bluetooth is required to connect to your Flipper device UIApplicationSceneManifest UIApplicationSupportsMultipleScenes From b81acd6e8b5532c6bf8c31192d28cb7bbd180468 Mon Sep 17 00:00:00 2001 From: Eugene Berdnikov Date: Tue, 1 Sep 2020 22:09:05 -0700 Subject: [PATCH 02/10] Simple DI container is added --- FlipperZero/Core/Container/Container.swift | 53 +++++++++++++++++++ .../Core/Container/ObservableResolver.swift | 20 +++++++ FlipperZero/Core/Container/Resolver.swift | 10 ++++ FlipperZero/Core/View/HomeView.swift | 2 + .../FlipperZero.xcodeproj/project.pbxproj | 20 +++++++ FlipperZero/Shared/FlipperZeroApp.swift | 4 +- 6 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 FlipperZero/Core/Container/Container.swift create mode 100644 FlipperZero/Core/Container/ObservableResolver.swift create mode 100644 FlipperZero/Core/Container/Resolver.swift diff --git a/FlipperZero/Core/Container/Container.swift b/FlipperZero/Core/Container/Container.swift new file mode 100644 index 000000000..a7eaddcc6 --- /dev/null +++ b/FlipperZero/Core/Container/Container.swift @@ -0,0 +1,53 @@ +// +// Container.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 8/23/20. +// + +// TODO: Replace with well-known DI container or extend to support resolving with dependencies +class Container: Resolver { + typealias Factory = () -> Any + + private struct Key: Hashable { + private let type: Any.Type + + init(_ type: Any.Type) { + self.type = type + } + + func hash(into hasher: inout Hasher) { + ObjectIdentifier(self.type).hash(into: &hasher) + } + + static func == (lhs: Container.Key, rhs: Container.Key) -> Bool { + lhs.type == rhs.type + } + } + + private var factories = [Key: Factory]() + + func register(_ service: Service) { + self.register(service, as: Service.self) + } + + func register(_ service: Service, as type: Service.Type) { + self.register({ service }, as: type) + } + + func register(_ factory: @escaping Factory, as type: Service.Type) { + self.factories[Key(type)] = factory + } + + func resolve(_ type: Service.Type) -> Service { + guard let factory = self.factories[Key(type)] else { + fatalError("Factory service for [\(type)] is not registered") + } + + guard let service = factory() as? Service else { + fatalError("Service returned by factory resolved for [\(type)] cannot be casted") + } + + return service + } +} diff --git a/FlipperZero/Core/Container/ObservableResolver.swift b/FlipperZero/Core/Container/ObservableResolver.swift new file mode 100644 index 000000000..c5ddc860f --- /dev/null +++ b/FlipperZero/Core/Container/ObservableResolver.swift @@ -0,0 +1,20 @@ +// +// ObservableResolver.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 8/23/20. +// + +import Combine + +public class ObservableResolver: Resolver, ObservableObject { + private let container: Container + + public init() { + self.container = Container() + } + + public func resolve(_ type: Service.Type) -> Service { + self.container.resolve(type) + } +} diff --git a/FlipperZero/Core/Container/Resolver.swift b/FlipperZero/Core/Container/Resolver.swift new file mode 100644 index 000000000..5f14e2f92 --- /dev/null +++ b/FlipperZero/Core/Container/Resolver.swift @@ -0,0 +1,10 @@ +// +// Resolver.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 8/23/20. +// + +public protocol Resolver { + func resolve(_ type: Service.Type) -> Service +} diff --git a/FlipperZero/Core/View/HomeView.swift b/FlipperZero/Core/View/HomeView.swift index 22619f8b7..4d6f13e65 100644 --- a/FlipperZero/Core/View/HomeView.swift +++ b/FlipperZero/Core/View/HomeView.swift @@ -8,6 +8,8 @@ import SwiftUI struct HomeView: View { + @EnvironmentObject var container: ObservableResolver + var body: some View { Text("Hello, Flipper users!") .padding() diff --git a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj index 3f0d89cce..ab12d1408 100644 --- a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj +++ b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj @@ -21,6 +21,9 @@ 4983062C25299C0600B04AFD /* Born2bSportyV2.ttf in CopyFiles */ = {isa = PBXBuildFile; fileRef = 498305C22527FEE900B04AFD /* Born2bSportyV2.ttf */; }; 4983062D25299C0600B04AFD /* HelvetiPixel.ttf in CopyFiles */ = {isa = PBXBuildFile; fileRef = 498305C12527FEE900B04AFD /* HelvetiPixel.ttf */; }; 49CE8F9E25262E2300B9CBE4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 49CE8F9D25262E2300B9CBE4 /* LaunchScreen.storyboard */; }; + B2FA88F43F8859CE0F038451 /* ObservableResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA84CE17FB8491B8891960 /* ObservableResolver.swift */; }; + B2FA8A35BF4A74EA20B234E0 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA80C7A7CD8772E74460E1 /* Resolver.swift */; }; + B2FA8BF96DD4BD3F143FCD43 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8169B16AE4C26E532D4A /* Container.swift */; }; F0DBFA4324EF2F9900EB2880 /* FlipperZeroApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DBFA1A24EF2F9600EB2880 /* FlipperZeroApp.swift */; }; F0DBFA4424EF2F9900EB2880 /* FlipperZeroApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DBFA1A24EF2F9600EB2880 /* FlipperZeroApp.swift */; }; F0DBFA4724EF2F9900EB2880 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F0DBFA1C24EF2F9900EB2880 /* Assets.xcassets */; }; @@ -103,6 +106,9 @@ 498305C22527FEE900B04AFD /* Born2bSportyV2.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Born2bSportyV2.ttf; sourceTree = ""; }; 49BE36C6253BE71200F727EF /* .xcccr.toml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .xcccr.toml; sourceTree = ""; }; 49CE8F9D25262E2300B9CBE4 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + B2FA80C7A7CD8772E74460E1 /* Resolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = ""; }; + B2FA8169B16AE4C26E532D4A /* Container.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = ""; }; + B2FA84CE17FB8491B8891960 /* ObservableResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObservableResolver.swift; sourceTree = ""; }; F0DBFA1A24EF2F9600EB2880 /* FlipperZeroApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlipperZeroApp.swift; sourceTree = ""; }; F0DBFA1C24EF2F9900EB2880 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F0DBFA2124EF2F9900EB2880 /* FlipperZero.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FlipperZero.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -162,6 +168,7 @@ 44A13C2724EF8D1B00617FEA /* View */, 44A5B59A24F05E74009EE7FB /* Core.h */, 44A5B59B24F05E74009EE7FB /* Info.plist */, + B2FA874723ECB425F97E632C /* Container */, ); path = Core; sourceTree = ""; @@ -191,6 +198,16 @@ path = Fonts; sourceTree = ""; }; + B2FA874723ECB425F97E632C /* Container */ = { + isa = PBXGroup; + children = ( + B2FA80C7A7CD8772E74460E1 /* Resolver.swift */, + B2FA8169B16AE4C26E532D4A /* Container.swift */, + B2FA84CE17FB8491B8891960 /* ObservableResolver.swift */, + ); + path = Container; + sourceTree = ""; + }; F0DBFA1424EF2F9600EB2880 = { isa = PBXGroup; children = ( @@ -464,6 +481,9 @@ files = ( 44A5B5AF24F05ECC009EE7FB /* RootView.swift in Sources */, 44A5B5B024F05ED0009EE7FB /* HomeView.swift in Sources */, + B2FA8A35BF4A74EA20B234E0 /* Resolver.swift in Sources */, + B2FA8BF96DD4BD3F143FCD43 /* Container.swift in Sources */, + B2FA88F43F8859CE0F038451 /* ObservableResolver.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/FlipperZero/Shared/FlipperZeroApp.swift b/FlipperZero/Shared/FlipperZeroApp.swift index b8cf94501..204aa131e 100644 --- a/FlipperZero/Shared/FlipperZeroApp.swift +++ b/FlipperZero/Shared/FlipperZeroApp.swift @@ -10,9 +10,11 @@ import SwiftUI @main struct FlipperZeroApp: App { + @StateObject private var resolver = ObservableResolver() + var body: some Scene { WindowGroup { - RootView() + RootView().environmentObject(resolver) } } } From 7593eeecd288aeb923ab3a2a1847695b438b417d Mon Sep 17 00:00:00 2001 From: Eugene Berdnikov Date: Wed, 2 Sep 2020 06:24:32 -0700 Subject: [PATCH 03/10] BluetoothService with device scan functionality is added --- .../Core/Container/ObservableResolver.swift | 1 + .../Model/Bluetooth/BluetoothStatus.swift | 18 ++++ .../Core/Model/Bluetooth/Peripheral.swift | 11 ++ FlipperZero/Core/Model/EquatableById.swift | 15 +++ .../Core/Service/BluetoothConnector.swift | 14 +++ .../Core/Service/BluetoothService.swift | 100 ++++++++++++++++++ .../Core/ViewModel/CombineExtensions.swift | 11 ++ .../FlipperZero.xcodeproj/project.pbxproj | 56 ++++++++++ 8 files changed, 226 insertions(+) create mode 100644 FlipperZero/Core/Model/Bluetooth/BluetoothStatus.swift create mode 100644 FlipperZero/Core/Model/Bluetooth/Peripheral.swift create mode 100644 FlipperZero/Core/Model/EquatableById.swift create mode 100644 FlipperZero/Core/Service/BluetoothConnector.swift create mode 100644 FlipperZero/Core/Service/BluetoothService.swift create mode 100644 FlipperZero/Core/ViewModel/CombineExtensions.swift diff --git a/FlipperZero/Core/Container/ObservableResolver.swift b/FlipperZero/Core/Container/ObservableResolver.swift index c5ddc860f..93b31e280 100644 --- a/FlipperZero/Core/Container/ObservableResolver.swift +++ b/FlipperZero/Core/Container/ObservableResolver.swift @@ -12,6 +12,7 @@ public class ObservableResolver: Resolver, ObservableObject { public init() { self.container = Container() + self.container.register(BluetoothService.init, as: BluetoothConnector.self) } public func resolve(_ type: Service.Type) -> Service { diff --git a/FlipperZero/Core/Model/Bluetooth/BluetoothStatus.swift b/FlipperZero/Core/Model/Bluetooth/BluetoothStatus.swift new file mode 100644 index 000000000..f32e86b17 --- /dev/null +++ b/FlipperZero/Core/Model/Bluetooth/BluetoothStatus.swift @@ -0,0 +1,18 @@ +// +// BluetoothStatus.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 8/22/20. +// + +enum BluetoothStatus: Equatable { + enum NotReadyReason: String { + case poweredOff + case preparing + case unauthorized + case unsupported + } + + case ready + case notReady(NotReadyReason) +} diff --git a/FlipperZero/Core/Model/Bluetooth/Peripheral.swift b/FlipperZero/Core/Model/Bluetooth/Peripheral.swift new file mode 100644 index 000000000..cb1f7a420 --- /dev/null +++ b/FlipperZero/Core/Model/Bluetooth/Peripheral.swift @@ -0,0 +1,11 @@ +// +// Peripheral.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 8/22/20. +// + +struct Peripheral: EquatableById, Identifiable { + let id: UUID + let name: String +} diff --git a/FlipperZero/Core/Model/EquatableById.swift b/FlipperZero/Core/Model/EquatableById.swift new file mode 100644 index 000000000..c99ec9dee --- /dev/null +++ b/FlipperZero/Core/Model/EquatableById.swift @@ -0,0 +1,15 @@ +// +// EquatableById.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 8/22/20. +// + +protocol EquatableById: Equatable { +} + +extension EquatableById where Self: Identifiable { + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } +} diff --git a/FlipperZero/Core/Service/BluetoothConnector.swift b/FlipperZero/Core/Service/BluetoothConnector.swift new file mode 100644 index 000000000..fa684cbd9 --- /dev/null +++ b/FlipperZero/Core/Service/BluetoothConnector.swift @@ -0,0 +1,14 @@ +// +// BluetoothConnector.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 8/22/20. +// + +protocol BluetoothConnector { + var peripherals: SafePublisher<[Peripheral]> { get } + var status: SafePublisher { get } + + func startScanForPeripherals() + func stopScanForPeripherals() +} diff --git a/FlipperZero/Core/Service/BluetoothService.swift b/FlipperZero/Core/Service/BluetoothService.swift new file mode 100644 index 000000000..0074f0583 --- /dev/null +++ b/FlipperZero/Core/Service/BluetoothService.swift @@ -0,0 +1,100 @@ +// +// BluetoothService.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 8/22/20. +// + +import CoreBluetooth + +class BluetoothService: NSObject, BluetoothConnector { + private let manager: CBCentralManager + private let peripheralsSubject = SafeSubject([Peripheral]()) + private let statusSubject = SafeSubject(BluetoothStatus.notReady(.preparing)) + + var peripherals: SafePublisher<[Peripheral]> { + self.peripheralsSubject.eraseToAnyPublisher() + } + + private var peripheralsMap = [UUID: CBPeripheral]() { + didSet { + self.peripheralsSubject.value = + self.peripheralsMap.values.compactMap(Peripheral.init).sorted { $0.name < $1.name } + } + } + + var status: SafePublisher { + self.statusSubject.eraseToAnyPublisher() + } + + override init() { + self.manager = CBCentralManager() + super.init() + self.manager.delegate = self + } + + func startScanForPeripherals() { + if self.statusSubject.value == .ready { + // TODO: Provide CBUUID relevant to Flipper devices + self.manager.scanForPeripherals(withServices: nil) + } + } + + func stopScanForPeripherals() { + if self.manager.isScanning { + self.peripheralsMap.removeAll() + self.manager.stopScan() + } + } +} + +extension BluetoothService: CBCentralManagerDelegate { + func centralManagerDidUpdateState(_ manager: CBCentralManager) { + let status = BluetoothStatus(manager.state) + self.statusSubject.value = status + if status != .ready { + self.peripheralsMap.removeAll() + } + } + + func centralManager( + _: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi: NSNumber + ) { + if self.peripheralsMap[peripheral.identifier] == nil, + let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? Bool, + isConnectable { + + self.peripheralsMap[peripheral.identifier] = peripheral + } + } +} + +fileprivate extension BluetoothStatus { + init(_ source: CBManagerState) { + switch source { + case .resetting, .unknown: + self = .notReady(.preparing) + case .unsupported: + self = .notReady(.unsupported) + case .unauthorized: + self = .notReady(.unauthorized) + case .poweredOff: + self = .notReady(.poweredOff) + case .poweredOn: + self = .ready + @unknown default: + self = .notReady(.unsupported) + } + } +} + +fileprivate extension Peripheral { + init?(_ source: CBPeripheral) { + guard let name = source.name else { + return nil + } + + self.id = source.identifier + self.name = name + } +} diff --git a/FlipperZero/Core/ViewModel/CombineExtensions.swift b/FlipperZero/Core/ViewModel/CombineExtensions.swift new file mode 100644 index 000000000..d67384c16 --- /dev/null +++ b/FlipperZero/Core/ViewModel/CombineExtensions.swift @@ -0,0 +1,11 @@ +// +// CombineExtensions.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 8/22/20. +// + +import Combine + +typealias SafePublisher = AnyPublisher +typealias SafeSubject = CurrentValueSubject diff --git a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj index ab12d1408..f3d5792e5 100644 --- a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj +++ b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj @@ -21,9 +21,15 @@ 4983062C25299C0600B04AFD /* Born2bSportyV2.ttf in CopyFiles */ = {isa = PBXBuildFile; fileRef = 498305C22527FEE900B04AFD /* Born2bSportyV2.ttf */; }; 4983062D25299C0600B04AFD /* HelvetiPixel.ttf in CopyFiles */ = {isa = PBXBuildFile; fileRef = 498305C12527FEE900B04AFD /* HelvetiPixel.ttf */; }; 49CE8F9E25262E2300B9CBE4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 49CE8F9D25262E2300B9CBE4 /* LaunchScreen.storyboard */; }; + B2FA842552A2630E9A81B0B5 /* BluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8A967D55B6E19EAB26F1 /* BluetoothService.swift */; }; + B2FA875A5249681A492A74D0 /* Peripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA871849FD3AEA2F11E6B4 /* Peripheral.swift */; }; B2FA88F43F8859CE0F038451 /* ObservableResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA84CE17FB8491B8891960 /* ObservableResolver.swift */; }; + B2FA899DF643F77F8C2EF7C6 /* EquatableById.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA80C09CFF768FF67E24D3 /* EquatableById.swift */; }; B2FA8A35BF4A74EA20B234E0 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA80C7A7CD8772E74460E1 /* Resolver.swift */; }; B2FA8BF96DD4BD3F143FCD43 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8169B16AE4C26E532D4A /* Container.swift */; }; + B2FA8D2EEB791271D0314B63 /* BluetoothStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA820DFEAA58C2703C5C95 /* BluetoothStatus.swift */; }; + B2FA8EE14865B67E25FCDF47 /* BluetoothConnector.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA833E55828477610B44D0 /* BluetoothConnector.swift */; }; + B2FA8F3E5178DFD55372FA8D /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8CB90070F1B5C37EBDA2 /* CombineExtensions.swift */; }; F0DBFA4324EF2F9900EB2880 /* FlipperZeroApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DBFA1A24EF2F9600EB2880 /* FlipperZeroApp.swift */; }; F0DBFA4424EF2F9900EB2880 /* FlipperZeroApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DBFA1A24EF2F9600EB2880 /* FlipperZeroApp.swift */; }; F0DBFA4724EF2F9900EB2880 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F0DBFA1C24EF2F9900EB2880 /* Assets.xcassets */; }; @@ -106,9 +112,15 @@ 498305C22527FEE900B04AFD /* Born2bSportyV2.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Born2bSportyV2.ttf; sourceTree = ""; }; 49BE36C6253BE71200F727EF /* .xcccr.toml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .xcccr.toml; sourceTree = ""; }; 49CE8F9D25262E2300B9CBE4 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + B2FA80C09CFF768FF67E24D3 /* EquatableById.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EquatableById.swift; sourceTree = ""; }; B2FA80C7A7CD8772E74460E1 /* Resolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = ""; }; B2FA8169B16AE4C26E532D4A /* Container.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = ""; }; + B2FA820DFEAA58C2703C5C95 /* BluetoothStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothStatus.swift; sourceTree = ""; }; + B2FA833E55828477610B44D0 /* BluetoothConnector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothConnector.swift; sourceTree = ""; }; B2FA84CE17FB8491B8891960 /* ObservableResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObservableResolver.swift; sourceTree = ""; }; + B2FA871849FD3AEA2F11E6B4 /* Peripheral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Peripheral.swift; sourceTree = ""; }; + B2FA8A967D55B6E19EAB26F1 /* BluetoothService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothService.swift; sourceTree = ""; }; + B2FA8CB90070F1B5C37EBDA2 /* CombineExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = ""; }; F0DBFA1A24EF2F9600EB2880 /* FlipperZeroApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlipperZeroApp.swift; sourceTree = ""; }; F0DBFA1C24EF2F9900EB2880 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F0DBFA2124EF2F9900EB2880 /* FlipperZero.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FlipperZero.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -169,6 +181,9 @@ 44A5B59A24F05E74009EE7FB /* Core.h */, 44A5B59B24F05E74009EE7FB /* Info.plist */, B2FA874723ECB425F97E632C /* Container */, + B2FA814364AB31F3059CF7E1 /* Service */, + B2FA8293F178CB04597E57BB /* Model */, + B2FA856B0F9FD57909122ECC /* ViewModel */, ); path = Core; sourceTree = ""; @@ -189,6 +204,32 @@ name = Frameworks; sourceTree = ""; }; + B2FA814364AB31F3059CF7E1 /* Service */ = { + isa = PBXGroup; + children = ( + B2FA833E55828477610B44D0 /* BluetoothConnector.swift */, + B2FA8A967D55B6E19EAB26F1 /* BluetoothService.swift */, + ); + path = Service; + sourceTree = ""; + }; + B2FA8293F178CB04597E57BB /* Model */ = { + isa = PBXGroup; + children = ( + B2FA80C09CFF768FF67E24D3 /* EquatableById.swift */, + B2FA8FE12FC96305D26AE9CE /* Bluetooth */, + ); + path = Model; + sourceTree = ""; + }; + B2FA856B0F9FD57909122ECC /* ViewModel */ = { + isa = PBXGroup; + children = ( + B2FA8CB90070F1B5C37EBDA2 /* CombineExtensions.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; 498305C02527FE9E00B04AFD /* Fonts */ = { isa = PBXGroup; children = ( @@ -208,6 +249,15 @@ path = Container; sourceTree = ""; }; + B2FA8FE12FC96305D26AE9CE /* Bluetooth */ = { + isa = PBXGroup; + children = ( + B2FA871849FD3AEA2F11E6B4 /* Peripheral.swift */, + B2FA820DFEAA58C2703C5C95 /* BluetoothStatus.swift */, + ); + path = Bluetooth; + sourceTree = ""; + }; F0DBFA1424EF2F9600EB2880 = { isa = PBXGroup; children = ( @@ -484,6 +534,12 @@ B2FA8A35BF4A74EA20B234E0 /* Resolver.swift in Sources */, B2FA8BF96DD4BD3F143FCD43 /* Container.swift in Sources */, B2FA88F43F8859CE0F038451 /* ObservableResolver.swift in Sources */, + B2FA899DF643F77F8C2EF7C6 /* EquatableById.swift in Sources */, + B2FA875A5249681A492A74D0 /* Peripheral.swift in Sources */, + B2FA8D2EEB791271D0314B63 /* BluetoothStatus.swift in Sources */, + B2FA8EE14865B67E25FCDF47 /* BluetoothConnector.swift in Sources */, + B2FA8F3E5178DFD55372FA8D /* CombineExtensions.swift in Sources */, + B2FA842552A2630E9A81B0B5 /* BluetoothService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 1ed0d59588810e728f44f9a71ceae31cc0b76460 Mon Sep 17 00:00:00 2001 From: Eugene Berdnikov Date: Wed, 2 Sep 2020 06:42:54 -0700 Subject: [PATCH 04/10] View and VM for Connections page are added --- FlipperZero/Core/View/ConnectionsView.swift | 100 ++++++++++++++++++ FlipperZero/Core/View/HomeView.swift | 22 +++- .../Core/ViewModel/CombineExtensions.swift | 1 + .../Core/ViewModel/ConnectionsViewModel.swift | 66 ++++++++++++ .../FlipperZero.xcodeproj/project.pbxproj | 8 ++ 5 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 FlipperZero/Core/View/ConnectionsView.swift create mode 100644 FlipperZero/Core/ViewModel/ConnectionsViewModel.swift diff --git a/FlipperZero/Core/View/ConnectionsView.swift b/FlipperZero/Core/View/ConnectionsView.swift new file mode 100644 index 000000000..039fc3e2b --- /dev/null +++ b/FlipperZero/Core/View/ConnectionsView.swift @@ -0,0 +1,100 @@ +// +// ConnectionsView.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 8/29/20. +// + +import Combine +import SwiftUI + +struct ConnectionsView: View { + @ObservedObject var viewModel: ConnectionsViewModel + + var body: some View { + VStack { + switch self.viewModel.state { + case .notReady(let reason): + Text(reason) + .multilineTextAlignment(.center) + .padding(.all) + case .scanning(let peripherals): + HStack { + Text("Scanning devices...") + .font(.title) + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .padding(.horizontal) + } + .padding(.all) + List(peripherals) { peripheral in + Text(peripheral.name) + } + } + } + } +} + +struct ConnectionsView_Previews: PreviewProvider { + static var previews: some View { + Group { + ConnectionsView(viewModel: ConnectionsViewModel(self.getContainer(.ready))) + ConnectionsView(viewModel: ConnectionsViewModel(self.getContainer(.notReady(.poweredOff)))) + ConnectionsView(viewModel: ConnectionsViewModel(self.getContainer(.notReady(.preparing)))) + ConnectionsView(viewModel: ConnectionsViewModel(self.getContainer(.notReady(.unauthorized)))) + ConnectionsView(viewModel: ConnectionsViewModel(self.getContainer(.notReady(.unsupported)))) + } + .previewLayout(.fixed(width: 480, height: 160)) + } + + private static func getContainer(_ status: BluetoothStatus) -> Resolver { + let container = Container() + container.register(TestConnector(status), as: BluetoothConnector.self) + return container + } +} + +private class TestConnector: BluetoothConnector { + private let peripheralsSubject = SafeSubject([Peripheral]()) + private let statusValue: BluetoothStatus + private var timer: Timer? + private let testDevices = Array(1...10).map { + Peripheral(id: UUID(), name: "Device \($0)") + } + + var peripherals: SafePublisher<[Peripheral]> { + self.peripheralsSubject.eraseToAnyPublisher() + } + + var status: SafePublisher { + Just(self.statusValue).eraseToAnyPublisher() + } + + init(_ status: BluetoothStatus) { + self.statusValue = status + if case .ready = status { + self.startScanForPeripherals() + } + } + + func startScanForPeripherals() { + self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + guard let `self` = self else { + return + } + + let index = Int.random(in: -self.peripheralsSubject.value.count.. = AnyPublisher typealias SafeSubject = CurrentValueSubject diff --git a/FlipperZero/Core/ViewModel/ConnectionsViewModel.swift b/FlipperZero/Core/ViewModel/ConnectionsViewModel.swift new file mode 100644 index 000000000..ac48e2bf2 --- /dev/null +++ b/FlipperZero/Core/ViewModel/ConnectionsViewModel.swift @@ -0,0 +1,66 @@ +// +// ConnectionsViewModel.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 8/29/20. +// + +class ConnectionsViewModel: ObservableObject { + enum State: Equatable { + case notReady(String) + case scanning([Peripheral]) + + init(_ notReadyReason: BluetoothStatus.NotReadyReason) { + self = .notReady(notReadyReason.description) + } + } + + private let connector: BluetoothConnector + private var disposeBag = DisposeBag() + + @Published private(set) var state: State = .init(.preparing) { + didSet { + let newValue = self.state + if case .notReady = oldValue, case .scanning = newValue { + self.connector.startScanForPeripherals() + } + } + } + + init(_ resolver: Resolver) { + self.connector = resolver.resolve(BluetoothConnector.self) + connector.status + .combineLatest(connector.peripherals) + .map { status, peripherals -> State in + switch status { + case .ready: + return .scanning(peripherals) + case .notReady(let reason): + return .notReady(reason.description) + } + }.removeDuplicates(by: ==).eraseToAnyPublisher() + .sink { [weak self] in + self?.state = $0 + }.store(in: &self.disposeBag) + } + + deinit { + self.connector.stopScanForPeripherals() + } +} + +fileprivate extension BluetoothStatus.NotReadyReason { + // TODO: support localizations here + var description: String { + switch self { + case .poweredOff: + return "Bluetooth is powered off" + case .preparing: + return "Bluetooth is not ready" + case .unauthorized: + return "The application is not authorized to use Bluetooth" + case .unsupported: + return "Bluetooth is not supported on this device" + } + } +} diff --git a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj index f3d5792e5..4b744ff26 100644 --- a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj +++ b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj @@ -21,8 +21,10 @@ 4983062C25299C0600B04AFD /* Born2bSportyV2.ttf in CopyFiles */ = {isa = PBXBuildFile; fileRef = 498305C22527FEE900B04AFD /* Born2bSportyV2.ttf */; }; 4983062D25299C0600B04AFD /* HelvetiPixel.ttf in CopyFiles */ = {isa = PBXBuildFile; fileRef = 498305C12527FEE900B04AFD /* HelvetiPixel.ttf */; }; 49CE8F9E25262E2300B9CBE4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 49CE8F9D25262E2300B9CBE4 /* LaunchScreen.storyboard */; }; + B2FA823F458ED6FC57954CA9 /* ConnectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8204BD4B6A42C9C5E88B /* ConnectionsView.swift */; }; B2FA842552A2630E9A81B0B5 /* BluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8A967D55B6E19EAB26F1 /* BluetoothService.swift */; }; B2FA875A5249681A492A74D0 /* Peripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA871849FD3AEA2F11E6B4 /* Peripheral.swift */; }; + B2FA88A253850BF9F2017806 /* ConnectionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8DB4A270E70FC69BA448 /* ConnectionsViewModel.swift */; }; B2FA88F43F8859CE0F038451 /* ObservableResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA84CE17FB8491B8891960 /* ObservableResolver.swift */; }; B2FA899DF643F77F8C2EF7C6 /* EquatableById.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA80C09CFF768FF67E24D3 /* EquatableById.swift */; }; B2FA8A35BF4A74EA20B234E0 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA80C7A7CD8772E74460E1 /* Resolver.swift */; }; @@ -115,12 +117,14 @@ B2FA80C09CFF768FF67E24D3 /* EquatableById.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EquatableById.swift; sourceTree = ""; }; B2FA80C7A7CD8772E74460E1 /* Resolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = ""; }; B2FA8169B16AE4C26E532D4A /* Container.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = ""; }; + B2FA8204BD4B6A42C9C5E88B /* ConnectionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionsView.swift; sourceTree = ""; }; B2FA820DFEAA58C2703C5C95 /* BluetoothStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothStatus.swift; sourceTree = ""; }; B2FA833E55828477610B44D0 /* BluetoothConnector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothConnector.swift; sourceTree = ""; }; B2FA84CE17FB8491B8891960 /* ObservableResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObservableResolver.swift; sourceTree = ""; }; B2FA871849FD3AEA2F11E6B4 /* Peripheral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Peripheral.swift; sourceTree = ""; }; B2FA8A967D55B6E19EAB26F1 /* BluetoothService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothService.swift; sourceTree = ""; }; B2FA8CB90070F1B5C37EBDA2 /* CombineExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = ""; }; + B2FA8DB4A270E70FC69BA448 /* ConnectionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionsViewModel.swift; sourceTree = ""; }; F0DBFA1A24EF2F9600EB2880 /* FlipperZeroApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlipperZeroApp.swift; sourceTree = ""; }; F0DBFA1C24EF2F9900EB2880 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F0DBFA2124EF2F9900EB2880 /* FlipperZero.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FlipperZero.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -170,6 +174,7 @@ children = ( 44A13C2824EF8D4100617FEA /* RootView.swift */, 44A13C2B24EF8DD700617FEA /* HomeView.swift */, + B2FA8204BD4B6A42C9C5E88B /* ConnectionsView.swift */, ); path = View; sourceTree = ""; @@ -226,6 +231,7 @@ isa = PBXGroup; children = ( B2FA8CB90070F1B5C37EBDA2 /* CombineExtensions.swift */, + B2FA8DB4A270E70FC69BA448 /* ConnectionsViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -540,6 +546,8 @@ B2FA8EE14865B67E25FCDF47 /* BluetoothConnector.swift in Sources */, B2FA8F3E5178DFD55372FA8D /* CombineExtensions.swift in Sources */, B2FA842552A2630E9A81B0B5 /* BluetoothService.swift in Sources */, + B2FA88A253850BF9F2017806 /* ConnectionsViewModel.swift in Sources */, + B2FA823F458ED6FC57954CA9 /* ConnectionsView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From f90b631dcd0096e24a188ed005b38f129fcad618 Mon Sep 17 00:00:00 2001 From: Eugene Berdnikov Date: Thu, 3 Sep 2020 23:37:28 -0700 Subject: [PATCH 05/10] Allow to register objects as singletons in the Container BluetoothService should be registered as a singleton to avoid errors on Connection View closing/reopening --- FlipperZero/Core/Container/Container.swift | 34 +++++++++------- .../Core/Container/ObservableResolver.swift | 2 +- .../Core/Container/ServiceFactory.swift | 40 +++++++++++++++++++ FlipperZero/Core/View/ConnectionsView.swift | 2 +- .../FlipperZero.xcodeproj/project.pbxproj | 4 ++ 5 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 FlipperZero/Core/Container/ServiceFactory.swift diff --git a/FlipperZero/Core/Container/Container.swift b/FlipperZero/Core/Container/Container.swift index a7eaddcc6..28ff9a850 100644 --- a/FlipperZero/Core/Container/Container.swift +++ b/FlipperZero/Core/Container/Container.swift @@ -7,8 +7,6 @@ // TODO: Replace with well-known DI container or extend to support resolving with dependencies class Container: Resolver { - typealias Factory = () -> Any - private struct Key: Hashable { private let type: Any.Type @@ -25,18 +23,10 @@ class Container: Resolver { } } - private var factories = [Key: Factory]() - - func register(_ service: Service) { - self.register(service, as: Service.self) - } - - func register(_ service: Service, as type: Service.Type) { - self.register({ service }, as: type) - } + private var factories = [Key: ServiceFactory]() - func register(_ factory: @escaping Factory, as type: Service.Type) { - self.factories[Key(type)] = factory + func register(_ builder: @escaping () -> Service, as type: Service.Type, isSingleton: Bool = false) { + self.factories[Key(type)] = isSingleton ? SingletonFactory(builder) : SingleUseFactory(builder) } func resolve(_ type: Service.Type) -> Service { @@ -44,10 +34,24 @@ class Container: Resolver { fatalError("Factory service for [\(type)] is not registered") } - guard let service = factory() as? Service else { - fatalError("Service returned by factory resolved for [\(type)] cannot be casted") + guard let service = factory.create() as? Service else { + fatalError("Service created by factory resolved for [\(type)] cannot be casted") } return service } } + +extension Container { + func register(instance: Service) { + self.register(instance: instance, as: Service.self) + } + + func register(instance: Service, as type: Service.Type) { + self.register({ instance }, as: type, isSingleton: true) + } + + func register(_ builder: @escaping () -> Service, isSingleton: Bool = false) { + self.register(builder, as: Service.self, isSingleton: isSingleton) + } +} diff --git a/FlipperZero/Core/Container/ObservableResolver.swift b/FlipperZero/Core/Container/ObservableResolver.swift index 93b31e280..8c44474f0 100644 --- a/FlipperZero/Core/Container/ObservableResolver.swift +++ b/FlipperZero/Core/Container/ObservableResolver.swift @@ -12,7 +12,7 @@ public class ObservableResolver: Resolver, ObservableObject { public init() { self.container = Container() - self.container.register(BluetoothService.init, as: BluetoothConnector.self) + self.container.register(BluetoothService.init, as: BluetoothConnector.self, isSingleton: true) } public func resolve(_ type: Service.Type) -> Service { diff --git a/FlipperZero/Core/Container/ServiceFactory.swift b/FlipperZero/Core/Container/ServiceFactory.swift new file mode 100644 index 000000000..f032becd8 --- /dev/null +++ b/FlipperZero/Core/Container/ServiceFactory.swift @@ -0,0 +1,40 @@ +// +// ServiceFactory.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 9/3/20. +// + +import Foundation + +protocol ServiceFactory { + func create() -> Any +} + +class SingletonFactory: ServiceFactory { + private let builder: () -> Any + + init(_ builder: @escaping () -> Any) { + self.builder = builder + } + + private lazy var value: Any = { + self.builder() + }() + + func create() -> Any { + self.value + } +} + +class SingleUseFactory: ServiceFactory { + private let builder: () -> Any + + init(_ builder: @escaping () -> Any) { + self.builder = builder + } + + func create() -> Any { + self.builder() + } +} diff --git a/FlipperZero/Core/View/ConnectionsView.swift b/FlipperZero/Core/View/ConnectionsView.swift index 039fc3e2b..97b57764a 100644 --- a/FlipperZero/Core/View/ConnectionsView.swift +++ b/FlipperZero/Core/View/ConnectionsView.swift @@ -49,7 +49,7 @@ struct ConnectionsView_Previews: PreviewProvider { private static func getContainer(_ status: BluetoothStatus) -> Resolver { let container = Container() - container.register(TestConnector(status), as: BluetoothConnector.self) + container.register(instance: TestConnector(status), as: BluetoothConnector.self) return container } } diff --git a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj index 4b744ff26..64026a968 100644 --- a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj +++ b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 4983062D25299C0600B04AFD /* HelvetiPixel.ttf in CopyFiles */ = {isa = PBXBuildFile; fileRef = 498305C12527FEE900B04AFD /* HelvetiPixel.ttf */; }; 49CE8F9E25262E2300B9CBE4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 49CE8F9D25262E2300B9CBE4 /* LaunchScreen.storyboard */; }; B2FA823F458ED6FC57954CA9 /* ConnectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8204BD4B6A42C9C5E88B /* ConnectionsView.swift */; }; + B2FA83690B577675E4761322 /* ServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8C80B37F033214EF18CC /* ServiceFactory.swift */; }; B2FA842552A2630E9A81B0B5 /* BluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8A967D55B6E19EAB26F1 /* BluetoothService.swift */; }; B2FA875A5249681A492A74D0 /* Peripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA871849FD3AEA2F11E6B4 /* Peripheral.swift */; }; B2FA88A253850BF9F2017806 /* ConnectionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8DB4A270E70FC69BA448 /* ConnectionsViewModel.swift */; }; @@ -123,6 +124,7 @@ B2FA84CE17FB8491B8891960 /* ObservableResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObservableResolver.swift; sourceTree = ""; }; B2FA871849FD3AEA2F11E6B4 /* Peripheral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Peripheral.swift; sourceTree = ""; }; B2FA8A967D55B6E19EAB26F1 /* BluetoothService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothService.swift; sourceTree = ""; }; + B2FA8C80B37F033214EF18CC /* ServiceFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceFactory.swift; sourceTree = ""; }; B2FA8CB90070F1B5C37EBDA2 /* CombineExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = ""; }; B2FA8DB4A270E70FC69BA448 /* ConnectionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionsViewModel.swift; sourceTree = ""; }; F0DBFA1A24EF2F9600EB2880 /* FlipperZeroApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlipperZeroApp.swift; sourceTree = ""; }; @@ -251,6 +253,7 @@ B2FA80C7A7CD8772E74460E1 /* Resolver.swift */, B2FA8169B16AE4C26E532D4A /* Container.swift */, B2FA84CE17FB8491B8891960 /* ObservableResolver.swift */, + B2FA8C80B37F033214EF18CC /* ServiceFactory.swift */, ); path = Container; sourceTree = ""; @@ -548,6 +551,7 @@ B2FA842552A2630E9A81B0B5 /* BluetoothService.swift in Sources */, B2FA88A253850BF9F2017806 /* ConnectionsViewModel.swift in Sources */, B2FA823F458ED6FC57954CA9 /* ConnectionsView.swift in Sources */, + B2FA83690B577675E4761322 /* ServiceFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 8c7fba50009a8989512a55bb1fc0a279b0220f1e Mon Sep 17 00:00:00 2001 From: Eugene Berdnikov Date: Fri, 4 Sep 2020 06:38:28 -0700 Subject: [PATCH 06/10] Tests coverage for ServiceFactory implementations --- .../Container/ServiceFactoryTests.swift | 56 +++++++++++++++++++ FlipperZero/CoreTests/CoreTests.swift | 18 ------ .../FlipperZero.xcodeproj/project.pbxproj | 16 ++++-- 3 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 FlipperZero/CoreTests/Container/ServiceFactoryTests.swift delete mode 100644 FlipperZero/CoreTests/CoreTests.swift diff --git a/FlipperZero/CoreTests/Container/ServiceFactoryTests.swift b/FlipperZero/CoreTests/Container/ServiceFactoryTests.swift new file mode 100644 index 000000000..2e8142ab0 --- /dev/null +++ b/FlipperZero/CoreTests/Container/ServiceFactoryTests.swift @@ -0,0 +1,56 @@ +// +// ServiceFactoryTests.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 9/3/20. +// +// + +@testable import Core +import XCTest + +class ServiceFactoryTests: XCTestCase { + func testSingletonFactoryDoesNotInvokeBuilderEarly() { + let builderIsInvoked = BoolWrapper() + let target = SingletonFactory { + XCTAssertFalse(builderIsInvoked.value, "Builder is invoked more than once") + builderIsInvoked.value = true + return builderIsInvoked + } + + XCTAssertFalse(builderIsInvoked.value, "Builder is invoked before `create` is called") + _ = target.create() + XCTAssert(builderIsInvoked.value, "Builder invocation was not tracked") + } + + func testSingletonFactoryReturnsTheSameValueOnSubsequentCall() { + let target = SingletonFactory(BoolWrapper.init) + let firstValue = target.create() as AnyObject + let secondValue = target.create() as AnyObject + XCTAssert(firstValue === secondValue) + } + + func testSingleUseFactoryDoesNotInvokeBuilderEarly() { + let builderIsInvoked = BoolWrapper() + let target = SingleUseFactory { + XCTAssertFalse(builderIsInvoked.value, "Builder is invoked more than once") + builderIsInvoked.value = true + return builderIsInvoked + } + + XCTAssertFalse(builderIsInvoked.value, "Builder is invoked before `create` is called") + _ = target.create() + XCTAssert(builderIsInvoked.value, "Builder invocation was not tracked") + } + + func testSingleUseFactoryReturnsDifferentValueOnSubsequentCall() { + let target = SingleUseFactory(BoolWrapper.init) + let firstValue = target.create() as AnyObject + let secondValue = target.create() as AnyObject + XCTAssert(firstValue !== secondValue) + } +} + +private class BoolWrapper { + var value = false +} diff --git a/FlipperZero/CoreTests/CoreTests.swift b/FlipperZero/CoreTests/CoreTests.swift deleted file mode 100644 index e51f39f58..000000000 --- a/FlipperZero/CoreTests/CoreTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// CoreTests.swift -// CoreTests -// -// Created by Eugene Berdnikov on 8/21/20. -// - -@testable import Core -import SwiftUI -import XCTest - -// TODO: remove this dummy test when more meaningful tests are added -class CoreTests: XCTestCase { - func testRootViewReturnsTabViewAsBody() throws { - let target = RootView() - XCTAssertEqual(target.homeTabTitle, "Home") - } -} diff --git a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj index 64026a968..bff4c28b6 100644 --- a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj +++ b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 44A5B5A124F05E75009EE7FB /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 44A5B59824F05E74009EE7FB /* Core.framework */; }; - 44A5B5A624F05E75009EE7FB /* CoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A5B5A524F05E75009EE7FB /* CoreTests.swift */; }; 44A5B5A824F05E75009EE7FB /* Core.h in Headers */ = {isa = PBXBuildFile; fileRef = 44A5B59A24F05E74009EE7FB /* Core.h */; settings = {ATTRIBUTES = (Public, ); }; }; 44A5B5AF24F05ECC009EE7FB /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A13C2824EF8D4100617FEA /* RootView.swift */; }; 44A5B5B024F05ED0009EE7FB /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A13C2B24EF8DD700617FEA /* HomeView.swift */; }; @@ -29,6 +28,7 @@ B2FA88F43F8859CE0F038451 /* ObservableResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA84CE17FB8491B8891960 /* ObservableResolver.swift */; }; B2FA899DF643F77F8C2EF7C6 /* EquatableById.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA80C09CFF768FF67E24D3 /* EquatableById.swift */; }; B2FA8A35BF4A74EA20B234E0 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA80C7A7CD8772E74460E1 /* Resolver.swift */; }; + B2FA8BDA1D9F39BBE33F7C00 /* ServiceFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8F2B542F29C99AB02B30 /* ServiceFactoryTests.swift */; }; B2FA8BF96DD4BD3F143FCD43 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8169B16AE4C26E532D4A /* Container.swift */; }; B2FA8D2EEB791271D0314B63 /* BluetoothStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA820DFEAA58C2703C5C95 /* BluetoothStatus.swift */; }; B2FA8EE14865B67E25FCDF47 /* BluetoothConnector.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA833E55828477610B44D0 /* BluetoothConnector.swift */; }; @@ -107,7 +107,6 @@ 44A5B59A24F05E74009EE7FB /* Core.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Core.h; sourceTree = ""; }; 44A5B59B24F05E74009EE7FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 44A5B5A024F05E74009EE7FB /* CoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 44A5B5A524F05E75009EE7FB /* CoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreTests.swift; sourceTree = ""; }; 44A5B5A724F05E75009EE7FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 497F0FC1252702B7000B2A86 /* perform_lint.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = perform_lint.sh; sourceTree = ""; }; 498305F62529569200B04AFD /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; @@ -127,6 +126,7 @@ B2FA8C80B37F033214EF18CC /* ServiceFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceFactory.swift; sourceTree = ""; }; B2FA8CB90070F1B5C37EBDA2 /* CombineExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = ""; }; B2FA8DB4A270E70FC69BA448 /* ConnectionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionsViewModel.swift; sourceTree = ""; }; + B2FA8F2B542F29C99AB02B30 /* ServiceFactoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceFactoryTests.swift; sourceTree = ""; }; F0DBFA1A24EF2F9600EB2880 /* FlipperZeroApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlipperZeroApp.swift; sourceTree = ""; }; F0DBFA1C24EF2F9900EB2880 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F0DBFA2124EF2F9900EB2880 /* FlipperZero.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FlipperZero.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -198,8 +198,8 @@ 44A5B5A424F05E75009EE7FB /* CoreTests */ = { isa = PBXGroup; children = ( - 44A5B5A524F05E75009EE7FB /* CoreTests.swift */, 44A5B5A724F05E75009EE7FB /* Info.plist */, + B2FA82E423A0E23496E26953 /* Container */, ); path = CoreTests; sourceTree = ""; @@ -229,6 +229,14 @@ path = Model; sourceTree = ""; }; + B2FA82E423A0E23496E26953 /* Container */ = { + isa = PBXGroup; + children = ( + B2FA8F2B542F29C99AB02B30 /* ServiceFactoryTests.swift */, + ); + path = Container; + sourceTree = ""; + }; B2FA856B0F9FD57909122ECC /* ViewModel */ = { isa = PBXGroup; children = ( @@ -559,7 +567,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 44A5B5A624F05E75009EE7FB /* CoreTests.swift in Sources */, + B2FA8BDA1D9F39BBE33F7C00 /* ServiceFactoryTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From a5fc74e9d00c5ca80401fa4c5f36c05d934d547e Mon Sep 17 00:00:00 2001 From: Eugene Berdnikov Date: Sun, 6 Sep 2020 06:52:46 -0700 Subject: [PATCH 07/10] Tests coverage for Container --- .../CoreTests/Container/ContainerTests.swift | 59 +++++++++++++++++++ .../FlipperZero.xcodeproj/project.pbxproj | 4 ++ 2 files changed, 63 insertions(+) create mode 100644 FlipperZero/CoreTests/Container/ContainerTests.swift diff --git a/FlipperZero/CoreTests/Container/ContainerTests.swift b/FlipperZero/CoreTests/Container/ContainerTests.swift new file mode 100644 index 000000000..34e581ad9 --- /dev/null +++ b/FlipperZero/CoreTests/Container/ContainerTests.swift @@ -0,0 +1,59 @@ +// +// ContainerTests.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 9/4/20. +// +// + +@testable import Core +import XCTest + +class ContainerTests: XCTestCase { + func testDefaultRegistrationResolvesDifferentValuesOnSubsequentCall() { + let target = Container() + target.register(TestImplementation.init) + let firstValue = target.resolve(TestImplementation.self) + let secondValue = target.resolve(TestImplementation.self) + XCTAssert(firstValue !== secondValue) + } + + func testInstanceRegistrationResolvesTheSameValue() { + let target = Container() + let registeredValue = TestImplementation() + target.register(instance: registeredValue, as: TestService.self) + let resolvedValue = target.resolve(TestService.self) + XCTAssert(registeredValue === resolvedValue) + } + + func testInstanceRegistrationAsSelfTypeResolvesTheSameValue() { + let target = Container() + let registeredValue = TestImplementation() + target.register(instance: registeredValue) + let resolvedValue = target.resolve(TestImplementation.self) + XCTAssert(registeredValue === resolvedValue) + } + + func testRegistrationAsSingletonResolvesTheSameValueOnSubsequentCall() { + let target = Container() + target.register(TestImplementation.init, as: TestService.self, isSingleton: true) + let firstValue = target.resolve(TestService.self) + let secondValue = target.resolve(TestService.self) + XCTAssert(firstValue === secondValue) + } + + func testSameTypeCouldBeRegisteredTwice() { + let target = Container() + target.register(TestImplementation.init, as: TestService.self, isSingleton: true) + target.register(TestImplementation.init, isSingleton: true) + let firstValue = target.resolve(TestService.self) + let secondValue: TestService = target.resolve(TestImplementation.self) + XCTAssert(firstValue !== secondValue) + } +} + +private protocol TestService: AnyObject { +} + +private class TestImplementation: TestService { +} diff --git a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj index bff4c28b6..c36f081ff 100644 --- a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj +++ b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ B2FA823F458ED6FC57954CA9 /* ConnectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8204BD4B6A42C9C5E88B /* ConnectionsView.swift */; }; B2FA83690B577675E4761322 /* ServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8C80B37F033214EF18CC /* ServiceFactory.swift */; }; B2FA842552A2630E9A81B0B5 /* BluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8A967D55B6E19EAB26F1 /* BluetoothService.swift */; }; + B2FA8688BED70F3D27647873 /* ContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8A53A5295C7744A7C858 /* ContainerTests.swift */; }; B2FA875A5249681A492A74D0 /* Peripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA871849FD3AEA2F11E6B4 /* Peripheral.swift */; }; B2FA88A253850BF9F2017806 /* ConnectionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8DB4A270E70FC69BA448 /* ConnectionsViewModel.swift */; }; B2FA88F43F8859CE0F038451 /* ObservableResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA84CE17FB8491B8891960 /* ObservableResolver.swift */; }; @@ -122,6 +123,7 @@ B2FA833E55828477610B44D0 /* BluetoothConnector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothConnector.swift; sourceTree = ""; }; B2FA84CE17FB8491B8891960 /* ObservableResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObservableResolver.swift; sourceTree = ""; }; B2FA871849FD3AEA2F11E6B4 /* Peripheral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Peripheral.swift; sourceTree = ""; }; + B2FA8A53A5295C7744A7C858 /* ContainerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerTests.swift; sourceTree = ""; }; B2FA8A967D55B6E19EAB26F1 /* BluetoothService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothService.swift; sourceTree = ""; }; B2FA8C80B37F033214EF18CC /* ServiceFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceFactory.swift; sourceTree = ""; }; B2FA8CB90070F1B5C37EBDA2 /* CombineExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = ""; }; @@ -233,6 +235,7 @@ isa = PBXGroup; children = ( B2FA8F2B542F29C99AB02B30 /* ServiceFactoryTests.swift */, + B2FA8A53A5295C7744A7C858 /* ContainerTests.swift */, ); path = Container; sourceTree = ""; @@ -568,6 +571,7 @@ buildActionMask = 2147483647; files = ( B2FA8BDA1D9F39BBE33F7C00 /* ServiceFactoryTests.swift in Sources */, + B2FA8688BED70F3D27647873 /* ContainerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 50a8a1d9d74a2a73d8654b76057ccb4ccd425a0b Mon Sep 17 00:00:00 2001 From: Eugene Berdnikov Date: Wed, 9 Sep 2020 22:03:50 -0700 Subject: [PATCH 08/10] Tests coverage for ConnectionsViewModel --- .../ViewModel/ConnectionsViewModelTests.swift | 106 ++++++++++++++++++ .../FlipperZero.xcodeproj/project.pbxproj | 12 ++ 2 files changed, 118 insertions(+) create mode 100644 FlipperZero/CoreTests/ViewModel/ConnectionsViewModelTests.swift diff --git a/FlipperZero/CoreTests/ViewModel/ConnectionsViewModelTests.swift b/FlipperZero/CoreTests/ViewModel/ConnectionsViewModelTests.swift new file mode 100644 index 000000000..0ad21eb5a --- /dev/null +++ b/FlipperZero/CoreTests/ViewModel/ConnectionsViewModelTests.swift @@ -0,0 +1,106 @@ +// +// ConnectionsViewModelTests.swift +// FlipperZero +// +// Created by Eugene Berdnikov on 9/6/20. +// + +@testable import Core +import XCTest + +class ConnectionsViewModelTests: XCTestCase { + func testStateWhenBluetoothIsPoweredOff() { + let connector = MockBluetoothConnector(initialState: .notReady(.poweredOff)) { + XCTFail("BluetoothConnector.startScanForPeripherals is called unexpectedly") + } + + let target = Self.createTarget(connector) + XCTAssertEqual(target.state, ConnectionsViewModel.State.notReady("Bluetooth is powered off")) + } + + func testStateWhenBluetoothIsUnauthorized() { + let connector = MockBluetoothConnector(initialState: .notReady(.unauthorized)) { + XCTFail("BluetoothConnector.startScanForPeripherals is called unexpectedly") + } + + let target = Self.createTarget(connector) + XCTAssertEqual( + target.state, ConnectionsViewModel.State.notReady("The application is not authorized to use Bluetooth")) + } + + func testStateWhenBluetoothIsUnsupported() { + let connector = MockBluetoothConnector(initialState: .notReady(.unsupported)) { + XCTFail("BluetoothConnector.startScanForPeripherals is called unexpectedly") + } + + let target = Self.createTarget(connector) + XCTAssertEqual(target.state, ConnectionsViewModel.State.notReady("Bluetooth is not supported on this device")) + } + + func testStateWhileScanningDevices() { + let startScanExpectation = self.expectation(description: "BluetoothConnector.startScanForPeripherals") + let connector = MockBluetoothConnector(onStartScanForPeripherals: startScanExpectation.fulfill) + + let target = Self.createTarget(connector) + XCTAssertEqual(target.state, ConnectionsViewModel.State.notReady("Bluetooth is not ready")) + connector.statusSubject.value = .ready + self.waitForExpectations(timeout: 0.1) + XCTAssertEqual(target.state, ConnectionsViewModel.State.scanning([])) + let peripheral = Peripheral(id: UUID(), name: "Device 42") + connector.peripheralsSubject.value.append(peripheral) + XCTAssertEqual(target.state, ConnectionsViewModel.State.scanning([peripheral])) + } + + func testStopScanIsCalledOnDeinit() { + let startScanExpectation = self.expectation(description: "BluetoothConnector.startScanForPeripherals") + let stopScanExpectation = self.expectation(description: "BluetoothConnector.stopScanForPeripherals") + let connector = MockBluetoothConnector( + initialState: .ready, + onStartScanForPeripherals: startScanExpectation.fulfill, + onStopScanForPeripherals: stopScanExpectation.fulfill) + + var target: ConnectionsViewModel? = Self.createTarget(connector) + XCTAssertEqual(target?.state, ConnectionsViewModel.State.scanning([])) + target = nil + self.waitForExpectations(timeout: 0.1) + } + + private static func createTarget(_ connector: BluetoothConnector) -> ConnectionsViewModel { + let container = Container() + container.register(instance: connector, as: BluetoothConnector.self) + return ConnectionsViewModel(container) + } +} + +private class MockBluetoothConnector: BluetoothConnector { + private let onStartScanForPeripherals: () -> Void + private let onStopScanForPeripherals: (() -> Void)? + let peripheralsSubject = SafeSubject([Peripheral]()) + let statusSubject: SafeSubject + + init( + initialState: BluetoothStatus = .notReady(.preparing), + onStartScanForPeripherals: @escaping () -> Void, + onStopScanForPeripherals: (() -> Void)? = nil + ) { + self.onStartScanForPeripherals = onStartScanForPeripherals + self.onStopScanForPeripherals = onStopScanForPeripherals + self.statusSubject = SafeSubject(initialState) + } + + var peripherals: SafePublisher<[Peripheral]> { + self.peripheralsSubject.eraseToAnyPublisher() + } + + var status: SafePublisher { + self.statusSubject.eraseToAnyPublisher() + } + + func startScanForPeripherals() { + self.onStartScanForPeripherals() + } + + func stopScanForPeripherals() { + self.onStopScanForPeripherals?() + } +} diff --git a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj index c36f081ff..45f8f8293 100644 --- a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj +++ b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ B2FA8BF96DD4BD3F143FCD43 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8169B16AE4C26E532D4A /* Container.swift */; }; B2FA8D2EEB791271D0314B63 /* BluetoothStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA820DFEAA58C2703C5C95 /* BluetoothStatus.swift */; }; B2FA8EE14865B67E25FCDF47 /* BluetoothConnector.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA833E55828477610B44D0 /* BluetoothConnector.swift */; }; + B2FA8F14ED1CCDF1F30AF84E /* ConnectionsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8B2103C59164121237BB /* ConnectionsViewModelTests.swift */; }; B2FA8F3E5178DFD55372FA8D /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8CB90070F1B5C37EBDA2 /* CombineExtensions.swift */; }; F0DBFA4324EF2F9900EB2880 /* FlipperZeroApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DBFA1A24EF2F9600EB2880 /* FlipperZeroApp.swift */; }; F0DBFA4424EF2F9900EB2880 /* FlipperZeroApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DBFA1A24EF2F9600EB2880 /* FlipperZeroApp.swift */; }; @@ -125,6 +126,7 @@ B2FA871849FD3AEA2F11E6B4 /* Peripheral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Peripheral.swift; sourceTree = ""; }; B2FA8A53A5295C7744A7C858 /* ContainerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerTests.swift; sourceTree = ""; }; B2FA8A967D55B6E19EAB26F1 /* BluetoothService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothService.swift; sourceTree = ""; }; + B2FA8B2103C59164121237BB /* ConnectionsViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionsViewModelTests.swift; sourceTree = ""; }; B2FA8C80B37F033214EF18CC /* ServiceFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceFactory.swift; sourceTree = ""; }; B2FA8CB90070F1B5C37EBDA2 /* CombineExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = ""; }; B2FA8DB4A270E70FC69BA448 /* ConnectionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionsViewModel.swift; sourceTree = ""; }; @@ -202,6 +204,7 @@ children = ( 44A5B5A724F05E75009EE7FB /* Info.plist */, B2FA82E423A0E23496E26953 /* Container */, + B2FA861D52EC55CA36264CF6 /* ViewModel */, ); path = CoreTests; sourceTree = ""; @@ -249,6 +252,14 @@ path = ViewModel; sourceTree = ""; }; + B2FA861D52EC55CA36264CF6 /* ViewModel */ = { + isa = PBXGroup; + children = ( + B2FA8B2103C59164121237BB /* ConnectionsViewModelTests.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; 498305C02527FE9E00B04AFD /* Fonts */ = { isa = PBXGroup; children = ( @@ -572,6 +583,7 @@ files = ( B2FA8BDA1D9F39BBE33F7C00 /* ServiceFactoryTests.swift in Sources */, B2FA8688BED70F3D27647873 /* ContainerTests.swift in Sources */, + B2FA8F14ED1CCDF1F30AF84E /* ConnectionsViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 6540c3556543c238237b7cefe7db22ef172d8af7 Mon Sep 17 00:00:00 2001 From: Eugene Berdnikov Date: Tue, 27 Oct 2020 06:37:06 -0700 Subject: [PATCH 09/10] BluetoothService is moved to the Platform group Platform group is intended to hold all platform-dependent services --- .../{ => Platform}/BluetoothService.swift | 0 .../FlipperZero.xcodeproj/project.pbxproj | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) rename FlipperZero/Core/Service/{ => Platform}/BluetoothService.swift (100%) diff --git a/FlipperZero/Core/Service/BluetoothService.swift b/FlipperZero/Core/Service/Platform/BluetoothService.swift similarity index 100% rename from FlipperZero/Core/Service/BluetoothService.swift rename to FlipperZero/Core/Service/Platform/BluetoothService.swift diff --git a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj index 45f8f8293..00170aff8 100644 --- a/FlipperZero/FlipperZero.xcodeproj/project.pbxproj +++ b/FlipperZero/FlipperZero.xcodeproj/project.pbxproj @@ -22,7 +22,6 @@ 49CE8F9E25262E2300B9CBE4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 49CE8F9D25262E2300B9CBE4 /* LaunchScreen.storyboard */; }; B2FA823F458ED6FC57954CA9 /* ConnectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8204BD4B6A42C9C5E88B /* ConnectionsView.swift */; }; B2FA83690B577675E4761322 /* ServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8C80B37F033214EF18CC /* ServiceFactory.swift */; }; - B2FA842552A2630E9A81B0B5 /* BluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8A967D55B6E19EAB26F1 /* BluetoothService.swift */; }; B2FA8688BED70F3D27647873 /* ContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8A53A5295C7744A7C858 /* ContainerTests.swift */; }; B2FA875A5249681A492A74D0 /* Peripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA871849FD3AEA2F11E6B4 /* Peripheral.swift */; }; B2FA88A253850BF9F2017806 /* ConnectionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8DB4A270E70FC69BA448 /* ConnectionsViewModel.swift */; }; @@ -31,6 +30,7 @@ B2FA8A35BF4A74EA20B234E0 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA80C7A7CD8772E74460E1 /* Resolver.swift */; }; B2FA8BDA1D9F39BBE33F7C00 /* ServiceFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8F2B542F29C99AB02B30 /* ServiceFactoryTests.swift */; }; B2FA8BF96DD4BD3F143FCD43 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8169B16AE4C26E532D4A /* Container.swift */; }; + B2FA8CEC58E76EF034FB5F69 /* BluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA83AA1F8EB1B7D3A17491 /* BluetoothService.swift */; }; B2FA8D2EEB791271D0314B63 /* BluetoothStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA820DFEAA58C2703C5C95 /* BluetoothStatus.swift */; }; B2FA8EE14865B67E25FCDF47 /* BluetoothConnector.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA833E55828477610B44D0 /* BluetoothConnector.swift */; }; B2FA8F14ED1CCDF1F30AF84E /* ConnectionsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FA8B2103C59164121237BB /* ConnectionsViewModelTests.swift */; }; @@ -122,10 +122,10 @@ B2FA8204BD4B6A42C9C5E88B /* ConnectionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionsView.swift; sourceTree = ""; }; B2FA820DFEAA58C2703C5C95 /* BluetoothStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothStatus.swift; sourceTree = ""; }; B2FA833E55828477610B44D0 /* BluetoothConnector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothConnector.swift; sourceTree = ""; }; + B2FA83AA1F8EB1B7D3A17491 /* BluetoothService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothService.swift; sourceTree = ""; }; B2FA84CE17FB8491B8891960 /* ObservableResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObservableResolver.swift; sourceTree = ""; }; B2FA871849FD3AEA2F11E6B4 /* Peripheral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Peripheral.swift; sourceTree = ""; }; B2FA8A53A5295C7744A7C858 /* ContainerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerTests.swift; sourceTree = ""; }; - B2FA8A967D55B6E19EAB26F1 /* BluetoothService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothService.swift; sourceTree = ""; }; B2FA8B2103C59164121237BB /* ConnectionsViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionsViewModelTests.swift; sourceTree = ""; }; B2FA8C80B37F033214EF18CC /* ServiceFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceFactory.swift; sourceTree = ""; }; B2FA8CB90070F1B5C37EBDA2 /* CombineExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = ""; }; @@ -220,7 +220,7 @@ isa = PBXGroup; children = ( B2FA833E55828477610B44D0 /* BluetoothConnector.swift */, - B2FA8A967D55B6E19EAB26F1 /* BluetoothService.swift */, + B2FA8E7D335C64A2BF92219F /* Platform */, ); path = Service; sourceTree = ""; @@ -280,6 +280,14 @@ path = Container; sourceTree = ""; }; + B2FA8E7D335C64A2BF92219F /* Platform */ = { + isa = PBXGroup; + children = ( + B2FA83AA1F8EB1B7D3A17491 /* BluetoothService.swift */, + ); + path = Platform; + sourceTree = ""; + }; B2FA8FE12FC96305D26AE9CE /* Bluetooth */ = { isa = PBXGroup; children = ( @@ -570,10 +578,10 @@ B2FA8D2EEB791271D0314B63 /* BluetoothStatus.swift in Sources */, B2FA8EE14865B67E25FCDF47 /* BluetoothConnector.swift in Sources */, B2FA8F3E5178DFD55372FA8D /* CombineExtensions.swift in Sources */, - B2FA842552A2630E9A81B0B5 /* BluetoothService.swift in Sources */, B2FA88A253850BF9F2017806 /* ConnectionsViewModel.swift in Sources */, B2FA823F458ED6FC57954CA9 /* ConnectionsView.swift in Sources */, B2FA83690B577675E4761322 /* ServiceFactory.swift in Sources */, + B2FA8CEC58E76EF034FB5F69 /* BluetoothService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 9ab40bcb02ec294f0116a2dc620c0e455396c5a1 Mon Sep 17 00:00:00 2001 From: Eugene Berdnikov Date: Tue, 27 Oct 2020 06:38:24 -0700 Subject: [PATCH 10/10] Coverage filter paths are updated --- FlipperZero/.xcccr.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/FlipperZero/.xcccr.toml b/FlipperZero/.xcccr.toml index e706c6700..f7a53793e 100644 --- a/FlipperZero/.xcccr.toml +++ b/FlipperZero/.xcccr.toml @@ -1,3 +1,7 @@ FilterTargets = ["CoreTests"] -FilterPaths = ["Core/View"] +FilterPaths = [ + "Core/Container/ObservableResolver.swift", + "Core/Service/Platform", + "Core/View" +] ZeroWarnOnly = true \ No newline at end of file