Skip to content

Commit

Permalink
Migrate to Observable and new #Preview
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg committed Nov 16, 2023
1 parent 52ac8f3 commit b2b8c07
Show file tree
Hide file tree
Showing 24 changed files with 159 additions and 143 deletions.
5 changes: 3 additions & 2 deletions NAMS/Bluetooth/BluetoothManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
import CoreBluetooth


class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate {
@Observable
class BluetoothManager: NSObject, CBCentralManagerDelegate {
private let bluetoothManager: CBCentralManager
private let dispatchQueue: DispatchQueue

@Published private(set) var bluetoothState: CBManagerState
private(set) var bluetoothState: CBManagerState

override init() {
// We use a separate dispatch queue, be aware that all delegate calls are not getting on the main thread.
Expand Down
6 changes: 2 additions & 4 deletions NAMS/Contacts/Contacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ struct Contacts: View {


#if DEBUG
struct Contacts_Previews: PreviewProvider {
static var previews: some View {
Contacts(presentingAccount: .constant(true))
}
#Preview {
Contacts(presentingAccount: .constant(true))
}
#endif
11 changes: 4 additions & 7 deletions NAMS/EEG/EEGChannelMark.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,9 @@ struct EEGChannelMark: ChartContent {


#if DEBUG
struct EEGChannelMark_Previews: PreviewProvider {
static let randomSamples = EEGMeasurementGenerator(sampleRate: 60)

static var previews: some View {
let generated = randomSamples.generateRecording(sampleTime: 5, recordingOffset: 10)
EEGChart(measurements: generated.data.suffix(from: 0), for: .af7, baseTime: generated.baseTime)
}
#Preview {
let randomSamples = EEGMeasurementGenerator(sampleRate: 60)
let generated = randomSamples.generateRecording(sampleTime: 5, recordingOffset: 10)
return EEGChart(measurements: generated.data.suffix(from: 0), for: .af7, baseTime: generated.baseTime)
}
#endif
15 changes: 8 additions & 7 deletions NAMS/EEG/EEGChart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,14 @@ struct EEGChart: View {


#if DEBUG
struct EEGChart_Previews: PreviewProvider {
static let randomSamples = EEGMeasurementGenerator(sampleRate: 60)
private let randomSamples = EEGMeasurementGenerator(sampleRate: 60)
private let generated = randomSamples.generateRecording(sampleTime: 5, recordingOffset: 10)

static var previews: some View {
let generated = randomSamples.generateRecording(sampleTime: 5, recordingOffset: 10)
EEGChart(measurements: generated.data.suffix(from: 0), for: .af7, baseTime: generated.baseTime)
EEGChart(measurements: generated.data.suffix(from: 0), for: .af8, baseTime: generated.baseTime)
}
#Preview {
EEGChart(measurements: generated.data.suffix(from: 0), for: .af7, baseTime: generated.baseTime)
}

#Preview {
EEGChart(measurements: generated.data.suffix(from: 0), for: .af8, baseTime: generated.baseTime)
}
#endif
28 changes: 11 additions & 17 deletions NAMS/EEG/EEGDeviceDetails.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct EEGDeviceDetails: View {
@Environment(\.locale)
private var locale

@ObservedObject private var device: ConnectedDevice
private let device: ConnectedDevice

var body: some View {
List {
Expand Down Expand Up @@ -146,27 +146,21 @@ struct EEGDeviceDetails: View {


#if DEBUG
struct EEGDeviceDetails_Previews: PreviewProvider {
@StateObject static var model = EEGViewModel(mock: MockEEGDevice(name: "Mock Device", model: "Mock", state: .connected))
#Preview {
let model = EEGViewModel(mock: MockEEGDevice(name: "Mock Device", model: "Mock", state: .connected))
return NavigationStack {
EEGDeviceDetails(device: model.activeDevice!) // swiftlint:disable:this force_unwrapping
}
}

@StateObject static var modelIntervention = EEGViewModel(mock: MockEEGDevice(
#Preview {
let modelIntervention = EEGViewModel(mock: MockEEGDevice(
name: "Mock Device",
model: "Mock",
state: .interventionRequired("INTERVENTION_MUSE_FIRMWARE")
))

static var previews: some View {
if let device = model.activeDevice {
NavigationStack {
EEGDeviceDetails(device: device)
}
}

if let device = modelIntervention.activeDevice {
NavigationStack {
EEGDeviceDetails(device: device)
}
}
return NavigationStack {
EEGDeviceDetails(device: modelIntervention.activeDevice!) // swiftlint:disable:this force_unwrapping
}
}
#endif
13 changes: 5 additions & 8 deletions NAMS/EEG/EEGDeviceList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SwiftUI


struct EEGDeviceList: View {
@ObservedObject private var eegModel: EEGViewModel
private let eegModel: EEGViewModel

var sortedDeviceList: [EEGDevice] {
eegModel.nearbyDevices.sorted { lhs, rhs in
Expand All @@ -32,13 +32,10 @@ struct EEGDeviceList: View {


#if DEBUG
struct MuseDeviceList_Previews: PreviewProvider {
@StateObject static var model = EEGViewModel(deviceManager: MockDeviceManager(immediate: true))

static var previews: some View {
List {
EEGDeviceList(eegModel: model)
}
#Preview {
let model = EEGViewModel(deviceManager: MockDeviceManager(immediate: true))
return List {
EEGDeviceList(eegModel: model)
}
}
#endif
64 changes: 33 additions & 31 deletions NAMS/EEG/EEGDeviceRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import SwiftUI

struct EEGDeviceRow: View {
private let device: EEGDevice

@ObservedObject private var eegModel: EEGViewModel
private let eegModel: EEGViewModel

var connectedDevice: ConnectedDevice? {
if let activeDevice = eegModel.activeDevice,
Expand All @@ -34,7 +33,9 @@ struct EEGDeviceRow: View {
EEGDeviceDetails(device: item)
}
.accessibilityRepresentation {
let button = Button(action: deviceButtonAction) {
let button = Button(action: {
deviceButtonAction()

Check warning on line 37 in NAMS/EEG/EEGDeviceRow.swift

View check run for this annotation

Codecov / codecov/patch

NAMS/EEG/EEGDeviceRow.swift#L37

Added line #L37 was not covered by tests
}) {
Text(verbatim: device.model) // currently, this order flows best for Muse device naming
Text(verbatim: device.name)
if device.connectionState.associatedConnection {
Expand All @@ -61,8 +62,11 @@ struct EEGDeviceRow: View {
}
}

@ViewBuilder private var deviceButton: some View {
Button(action: deviceButtonAction) {

@MainActor @ViewBuilder private var deviceButton: some View {
Button(action: {
deviceButtonAction()
}) {
HStack {
Text(verbatim: "\(device.model) - \(device.name)")
.foregroundColor(.primary)
Expand Down Expand Up @@ -106,6 +110,7 @@ struct EEGDeviceRow: View {
}


@MainActor
private func deviceButtonAction() {
eegModel.tapDevice(device)
}
Expand All @@ -117,42 +122,39 @@ struct EEGDeviceRow: View {


#if DEBUG
struct EEGDeviceRow_Previews: PreviewProvider {
struct StateRow: View {
static let device = MockEEGDevice(name: "Device", model: "Mock")
@StateObject var model = EEGViewModel(deviceManager: MockDeviceManager())
var body: some View {
EEGDeviceRow(eegModel: model, device: Self.device)
#Preview {
let device = MockEEGDevice(name: "Device", model: "Mock")
let model = EEGViewModel(deviceManager: MockDeviceManager())

return NavigationStack {
List {
EEGDeviceRow(eegModel: model, device: device) // tap to pair
}
}
}

static let devices = [
#Preview {
NavigationStack {
List {
EEGDeviceRow(
eegModel: EEGViewModel(deviceManager: MockDeviceManager()),
device: MockEEGDevice(name: "Nearby Device", model: "Mock")
)
}
}
}

#Preview {
let devices = [
MockEEGDevice(name: "Device 1", model: "Mock", state: .connecting),
MockEEGDevice(name: "Device 2", model: "Mock", state: .connected),
MockEEGDevice(name: "Device 3", model: "Mock", state: .interventionRequired("Firmware update required."))
]

static var previews: some View {
return ForEach(devices, id: \.macAddress) { device in
NavigationStack {
List {
StateRow() // tap to pair
}
}

NavigationStack {
List {
EEGDeviceRow(
eegModel: EEGViewModel(deviceManager: MockDeviceManager()),
device: MockEEGDevice(name: "Nearby Device", model: "Mock")
)
}
}

ForEach(devices, id: \.macAddress) { device in
NavigationStack {
List {
EEGDeviceRow(eegModel: EEGViewModel(mock: device), device: device)
}
EEGDeviceRow(eegModel: EEGViewModel(mock: device), device: device)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions NAMS/EEG/EEGRecording.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct EEGRecording: View {
@Environment(\.dismiss)
private var dismiss

@ObservedObject private var eegModel: EEGViewModel
private let eegModel: EEGViewModel
@Environment(PatientListModel.self)
private var patientList

Expand Down Expand Up @@ -110,8 +110,8 @@ struct EEGRecording: View {
struct EEGMeasurement_Previews: PreviewProvider {
static let connectedDevice = MockEEGDevice(name: "Device 1", model: "Mock", state: .connected)

@StateObject static var connectedModel = EEGViewModel(mock: connectedDevice)
@StateObject static var model = EEGViewModel(deviceManager: MockDeviceManager())
static let connectedModel = EEGViewModel(mock: connectedDevice)
static let model = EEGViewModel(deviceManager: MockDeviceManager())

static var previews: some View {
NavigationStack {
Expand Down
4 changes: 2 additions & 2 deletions NAMS/EEG/ListRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ struct ListRow<Value: View>: View {


#if DEBUG
struct ListRow_Previews: PreviewProvider {
static var previews: some View {
#Preview {
List {
ListRow("Hello") {
Text(verbatim: "World")
}
Expand Down
33 changes: 23 additions & 10 deletions NAMS/EEG/Model/ConnectedDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,41 @@ import Foundation
import OrderedCollections


class ConnectedDevice: ObservableObject {
@Observable
class ConnectedDevice {
let device: EEGDevice
var listener: DeviceConnectionListener?

@Published var state: ConnectionState = .unknown
var state: ConnectionState {
get {
access(keyPath: \.state)
return publishedState
}
set {
withMutation(keyPath: \.state) {
publishedState = newValue
}
}
}

@ObservationIgnored @Published var publishedState: ConnectionState = .unknown // current workaround to create a publisher for the view model

// artifacts muse supports
@Published var wearingHeadband = false
@Published var eyeBlink = false
@Published var jawClench = false
var wearingHeadband = false
var eyeBlink = false
var jawClench = false

/// Determines if the last second of data is considered good
@Published var isGood: (Bool, Bool, Bool, Bool) = (false, false, false, false) // swiftlint:disable:this large_tuple
var isGood: (Bool, Bool, Bool, Bool) = (false, false, false, false) // swiftlint:disable:this large_tuple
/// The current fit of the headband
@Published var fit = HeadbandFit(tp9Fit: .poor, af7Fit: .poor, af8Fit: .poor, tp10Fit: .poor)
var fit = HeadbandFit(tp9Fit: .poor, af7Fit: .poor, af8Fit: .poor, tp10Fit: .poor)

@Published var aboutInformation: OrderedDictionary<LocalizedStringResource, CustomStringConvertible> = [:]
var aboutInformation: OrderedDictionary<LocalizedStringResource, CustomStringConvertible> = [:]

/// Remaining battery percentage in percent [0.0;100.0]
@Published var remainingBatteryPercentage: Double?
var remainingBatteryPercentage: Double?

@Published var measurements: [EEGFrequency: [EEGSeries]] = [:]
var measurements: [EEGFrequency: [EEGSeries]] = [:]

init(device: EEGDevice) {
self.device = device
Expand Down
26 changes: 14 additions & 12 deletions NAMS/EEG/Model/EEGViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import Foundation
import OSLog


class EEGViewModel: ObservableObject {
@Observable
class EEGViewModel {
let logger = Logger(subsystem: "edu.stanford.NAMS", category: "MuseViewModel")

private let deviceManager: DeviceManager

@Published var nearbyDevices: [EEGDevice] = []
@Published var activeDevice: ConnectedDevice?
var nearbyDevices: [EEGDevice] = []
var activeDevice: ConnectedDevice?

private var deviceManagerCancelable: AnyCancellable?
private var activeDeviceCancelable: AnyCancellable?
Expand Down Expand Up @@ -67,9 +68,7 @@ class EEGViewModel: ObservableObject {
logger.info("Disconnecting previously connected device \(activeDevice.device.name)...")
// either we tapped on the same Muse or on another one, in any case disconnect the currently active Muse
activeDevice.disconnect()
activeDeviceCancelable?.cancel()
activeDeviceCancelable = nil
self.activeDevice = nil
clearActiveDevice()

Check warning on line 71 in NAMS/EEG/Model/EEGViewModel.swift

View check run for this annotation

Codecov / codecov/patch

NAMS/EEG/Model/EEGViewModel.swift#L71

Added line #L71 was not covered by tests


if activeDevice.device.macAddress == device.macAddress {
Expand All @@ -88,13 +87,16 @@ class EEGViewModel: ObservableObject {
}

func sinkActiveDevice(device: ConnectedDevice) {
activeDeviceCancelable = device.objectWillChange.sink { [weak self] in
self?.objectWillChange.send()
if case .disconnected = device.state {
self?.activeDeviceCancelable?.cancel()
self?.activeDeviceCancelable = nil
self?.activeDevice = nil
activeDeviceCancelable = device.$publishedState.sink { [weak self] state in
if case .disconnected = state {
self?.clearActiveDevice()
}
}
}

private func clearActiveDevice() {
activeDeviceCancelable?.cancel()
activeDeviceCancelable = nil
activeDevice = nil
}
}
Loading

0 comments on commit b2b8c07

Please sign in to comment.