Monitors added/removed devices again

This commit is contained in:
Casey Fleser 2023-03-18 09:45:45 -05:00
parent f6d596ad1d
commit baee85bcae
18 changed files with 428 additions and 335 deletions

View file

@ -32,8 +32,8 @@
C966877529B7641F007BB3F5 /* FilteredNodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876929B7641F007BB3F5 /* FilteredNodeView.swift */; };
C966877629B7641F007BB3F5 /* NodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876A29B7641F007BB3F5 /* NodeView.swift */; };
C966877729B7641F007BB3F5 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876B29B7641F007BB3F5 /* Node.swift */; };
C966877829B76533007BB3F5 /* SourceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE4F2861E9D000C2EF35 /* SourceState.swift */; };
C9779742284F6DE000706DFB /* ToolbarMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9779741284F6DE000706DFB /* ToolbarMenu.swift */; };
C98048FC29C5FC2A00DE0DBD /* NodeAB.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98048FB29C5FC2A00DE0DBD /* NodeAB.swift */; };
C982F859283B9F9000D491F4 /* SimDirsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F858283B9F9000D491F4 /* SimDirsApp.swift */; };
C982F85B283B9F9000D491F4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F85A283B9F9000D491F4 /* ContentView.swift */; };
C982F85D283B9F9200D491F4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C982F85C283B9F9200D491F4 /* Assets.xcassets */; };
@ -61,7 +61,6 @@
C90BCE492861DA6700C2EF35 /* RuntimeHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeHeader.swift; sourceTree = "<group>"; };
C90BCE4B2861E37900C2EF35 /* AppHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHeader.swift; sourceTree = "<group>"; };
C90BCE4D2861E4E400C2EF35 /* AppContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContent.swift; sourceTree = "<group>"; };
C90BCE4F2861E9D000C2EF35 /* SourceState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceState.swift; sourceTree = "<group>"; };
C90BCE512861EDBF00C2EF35 /* SourceFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFilter.swift; sourceTree = "<group>"; };
C90DCC132896AAAA0072E403 /* ContentHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentHeader.swift; sourceTree = "<group>"; };
C90DCC152896B0370072E403 /* AppearancePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePicker.swift; sourceTree = "<group>"; };
@ -82,6 +81,7 @@
C966876A29B7641F007BB3F5 /* NodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeView.swift; sourceTree = "<group>"; };
C966876B29B7641F007BB3F5 /* Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = "<group>"; };
C9779741284F6DE000706DFB /* ToolbarMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarMenu.swift; sourceTree = "<group>"; };
C98048FB29C5FC2A00DE0DBD /* NodeAB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAB.swift; sourceTree = "<group>"; };
C982F855283B9F9000D491F4 /* SimDirs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimDirs.app; sourceTree = BUILT_PRODUCTS_DIR; };
C982F858283B9F9000D491F4 /* SimDirsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimDirsApp.swift; sourceTree = "<group>"; };
C982F85A283B9F9000D491F4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@ -133,6 +133,7 @@
C966875E29B7641F007BB3F5 /* FilteredNode.swift */,
C966876629B7641F007BB3F5 /* NodeListBuilder.swift */,
C966876B29B7641F007BB3F5 /* Node.swift */,
C98048FB29C5FC2A00DE0DBD /* NodeAB.swift */,
);
path = Node;
sourceTree = "<group>";
@ -238,7 +239,6 @@
isa = PBXGroup;
children = (
C90BCE512861EDBF00C2EF35 /* SourceFilter.swift */,
C90BCE4F2861E9D000C2EF35 /* SourceState.swift */,
);
path = Presentation;
sourceTree = "<group>";
@ -342,7 +342,6 @@
C966877629B7641F007BB3F5 /* NodeView.swift in Sources */,
C966877129B7641F007BB3F5 /* SimRuntime+Node.swift in Sources */,
C90BCE4A2861DA6700C2EF35 /* RuntimeHeader.swift in Sources */,
C966877829B76533007BB3F5 /* SourceState.swift in Sources */,
C9EE0CD42847B79E00E9B97A /* SimApp.swift in Sources */,
C9DD54D22860A24B00D46AB3 /* RuntimeContent.swift in Sources */,
C9779742284F6DE000706DFB /* ToolbarMenu.swift in Sources */,
@ -360,6 +359,7 @@
C90BCE482861D70500C2EF35 /* ErrorView.swift in Sources */,
C982F877283D020C00D491F4 /* SimProductFamily.swift in Sources */,
C982F859283B9F9000D491F4 /* SimDirsApp.swift in Sources */,
C98048FC29C5FC2A00DE0DBD /* NodeAB.swift in Sources */,
C982F871283CE7B800D491F4 /* SimRuntime.swift in Sources */,
C95CC0FA28B2414900928FAE /* SystemIconButtonStyle.swift in Sources */,
C9EE0CD228478FDB00E9B97A /* PathRow.swift in Sources */,

View file

@ -6,26 +6,61 @@
//
import SwiftUI
import Combine
struct ContentView: View {
@ObservedObject var state : SourceState
@State var filter = SourceFilter.restore()
enum Style: Int, CaseIterable, Identifiable {
case placeholder
case byDevice
case byRuntime
var id : Int { rawValue }
var visible : Bool { self != .placeholder }
var title : String {
switch self {
case .placeholder: return "Placeholder"
case .byDevice: return "By Device"
case .byRuntime: return "By Runtime"
}
}
}
@State var filter = SourceFilter.restore()
@State var viewID = UUID().uuidString
@State var style = Style.byDevice
let model : SimModel
init(model: SimModel) {
state = SourceState(model: model)
self.model = model
}
var body: some View {
VStack {
NavigationView {
FilteredNodeView(filter: $filter) { state.items }
.id(state.style)
.toolbar { ToolbarItem { ToolbarMenu(state: state, filter: $filter) } }
FilteredNodeView(filter: $filter) { items }
.id(viewID)
.toolbar { ToolbarItem { ToolbarMenu(style: $style, filter: $filter) } }
.frame(minWidth: 200)
Image("Icon-256") // Initial View
}
}
.onChange(of: style) { _ in resetView() }
.environment(\.deviceUpdates, model.deviceUpdates)
}
@NodeListBuilder
var items: [some Node] {
switch style {
case .placeholder: [] as [LeafNode]
case .byDevice: SimProductFamily.presentation.map { $0.linked(from: model) }
case .byRuntime: SimPlatform.presentation.map { $0.linked(from: model) }
}
}
func resetView() {
viewID = UUID().uuidString
}
}
@ -39,3 +74,14 @@ struct ContentView_Previews: PreviewProvider {
.preferredColorScheme(.light)
}
}
private struct DeviceUpdatesKey: EnvironmentKey {
static let defaultValue = PassthroughSubject<SimModel.Update, Never>()
}
extension EnvironmentValues {
var deviceUpdates: PassthroughSubject<SimModel.Update, Never> {
get { self[DeviceUpdatesKey.self] }
set { self[DeviceUpdatesKey.self] = newValue }
}
}

View file

@ -40,7 +40,6 @@ class SimDevice: ObservableObject, Decodable {
let dataPathSize : Int
let logPath : String
let deviceTypeIdentifier : String
var deviceType : SimDeviceType?
var deviceModel : String?
var apps = [SimApp]()
var dataURL : URL { URL(fileURLWithPath: dataPath) }
@ -315,16 +314,6 @@ extension SimDevice {
}
extension Array where Element == SimDevice {
func linkingDeviceType(_ deviceType: SimDeviceType) -> Self {
let devices = filter { $0.isDeviceOfType(deviceType) }
for device in devices {
device.deviceType = deviceType
}
return devices
}
func of(deviceType: SimDeviceType) -> Self {
filter { $0.isDeviceOfType(deviceType) }
}

View file

@ -13,13 +13,13 @@ enum SimError: Error {
case invalidApp
}
struct SimDevicesUpdates {
let runtime : SimRuntime
var additions : [SimDevice]
var removals : [SimDevice]
}
class SimModel {
struct Update {
let runtime : SimRuntime
var additions : [SimDevice]
var removals : [SimDevice]
}
var deviceTypes : [SimDeviceType]
var runtimes : [SimRuntime]
var monitor : Cancellable?
@ -28,7 +28,7 @@ class SimModel {
var devices : [SimDevice] { runtimes.flatMap { $0.devices } }
var apps : [SimApp] { devices.flatMap { $0.apps } }
var deviceUpdates = PassthroughSubject<SimDevicesUpdates, Never>()
var deviceUpdates = PassthroughSubject<SimModel.Update, Never>()
init() {
let simctl = SimCtl()
@ -70,25 +70,13 @@ class SimModel {
}
.receive(on: DispatchQueue.main)
.sink { [weak self] runtimeDevs in
guard let this = self else { return }
guard let this = self else { return }
for (runtimeID, curDevices) in runtimeDevs {
guard let runtime = this.runtimes.first(where: { $0.identifier == runtimeID }) else { print("missing runtime: \(runtimeID)"); continue }
let curDevIDs = curDevices.map { $0.udid }
let lastDevID = runtime.devices.map { $0.udid }
let updates = SimDevicesUpdates(
runtime: runtime,
additions: curDevices.filter { !lastDevID.contains($0.udid) },
removals: runtime.devices.filter { !curDevIDs.contains($0.udid) })
if !updates.removals.isEmpty || !updates.additions.isEmpty {
let idsToRemove = updates.removals.map { $0.udid }
updates.additions.completeSetup(with: this.deviceTypes)
runtime.devices.removeAll(where: { idsToRemove.contains($0.udid) })
runtime.devices.append(contentsOf: updates.additions)
this.deviceUpdates.send(updates)
if let changes = runtime.reconcileDevices(curDevices, forTypes: this.deviceTypes) {
this.deviceUpdates.send(changes)
}
for srcDevice in curDevices {

View file

@ -112,6 +112,26 @@ class SimRuntime: ObservableObject, Comparable, Decodable {
}
}
}
func reconcileDevices(_ curDevices: [SimDevice], forTypes deviceTypes: [SimDeviceType]) -> SimModel.Update? {
let curDevIDs = curDevices.map { $0.udid }
let ourDevIDs = devices.map { $0.udid }
let additions = curDevices.filter { !ourDevIDs.contains($0.udid) }
let removals = devices.filter { !curDevIDs.contains($0.udid) }
var result : SimModel.Update? = nil
if !additions.isEmpty || !removals.isEmpty {
let idsToRemove = removals.map { $0.udid }
additions.completeSetup(with: deviceTypes)
devices.removeAll(where: { idsToRemove.contains($0.udid) })
devices.append(contentsOf: additions)
result = SimModel.Update(runtime: self, additions: additions, removals: removals)
}
return result
}
}
extension Array where Element == SimRuntime {

View file

@ -7,25 +7,38 @@
import SwiftUI
extension SimDevice: Node {
var title : String { return name }
var headerTitle : String { "Device: \(title)" }
var header : some View { DeviceHeader(device: self) }
var content : some View { DeviceContent(self) }
// SimDevice requires a wrapper to simulate Node conformance because its
// icon is provided by a SimDeviceType
// var isEnabled : Bool { isBooted }
var iconName : String { deviceType?.productFamily.symbolName ?? "questionmark.circle" }
struct SimDeviceNode: Node {
let device : SimDevice
var iconName : String
var title : String { device.name }
var headerTitle : String { "Device: \(title)" }
var header : some View { DeviceHeader(device) }
var content : some View { DeviceContent(device) }
var items : [SimApp]? {
get { apps }
set { apps = newValue ?? [] }
get { device.apps }
set { device.apps = newValue ?? [] }
}
init(_ device: SimDevice, iconName: String) {
self.device = device
self.iconName = iconName
}
func icon(forHeader: Bool) -> some View {
symbolIcon(iconName, color: isAvailable ? .green : .red, forHeader: forHeader)
symbolIcon(iconName, color: device.isAvailable ? .green : .red, forHeader: forHeader)
}
func matchedFilterOptions() -> SourceFilter.Options {
return !apps.isEmpty ? .withApps : []
return !device.apps.isEmpty ? .withApps : []
}
}
extension Array where Element == SimDevice {
func nodesFor(deviceType: SimDeviceType) -> [SimDeviceNode] {
filter({ $0.isDeviceOfType(deviceType) }).map({ SimDeviceNode($0, iconName: deviceType.productFamily.symbolName) })
}
}

View file

@ -17,4 +17,29 @@ extension SimDeviceType: Node {
func icon(forHeader: Bool) -> some View {
symbolIcon(productFamily.symbolName, forHeader: forHeader)
}
func linkedForDeviceStyle(from model: SimModel) -> some Node {
NodeLink(self) {
model.runtimes.supporting(deviceType: self).map { runtime in
runtime.linkedForDeviceStyle(from: model, deviceType: self)
}
}
}
func linkedForRuntimeStyle(from model: SimModel, runtime: SimRuntime) -> some Node {
var node = NodeLink(self, items: runtime.devices.nodesFor(deviceType: self))
return node.onUpdate { update in
guard let runtime = model.runtimes.supporting(deviceType: self).first(where: { $0 == update.runtime }) else { return nil }
let ourAdditions = update.additions.filter({ $0.deviceTypeIdentifier == identifier })
let ourRemovals = update.removals.filter({ $0.deviceTypeIdentifier == identifier })
if !ourAdditions.isEmpty || !ourRemovals.isEmpty {
return runtime.devices.nodesFor(deviceType: self)
}
else {
return nil
}
}
}
}

View file

@ -17,5 +17,12 @@ extension SimPlatform: Node {
func icon(forHeader: Bool) -> some View {
symbolIcon(symbolName, forHeader: forHeader)
}
}
func linked(from model: SimModel) -> some Node {
NodeLink(self) {
model.runtimes.supporting(platform: self).map { runtime in
runtime.linkedForRuntimeStyle(from: model)
}
}
}
}

View file

@ -17,5 +17,12 @@ extension SimProductFamily: Node {
func icon(forHeader: Bool) -> some View {
symbolIcon(symbolName, forHeader: forHeader)
}
func linked(from model: SimModel) -> some Node {
NodeLink(self) {
model.deviceTypes.supporting(productFamily: self).map { deviceType in
deviceType.linkedForDeviceStyle(from: model)
}
}
}
}

View file

@ -21,4 +21,30 @@ extension SimRuntime: Node {
func matchedFilterOptions() -> SourceFilter.Options {
return isAvailable ? .runtimeInstalled : []
}
func linkedForDeviceStyle(from model: SimModel, deviceType: SimDeviceType) -> some Node {
var node = NodeLink(self) { devices.nodesFor(deviceType: deviceType) }
return node.onUpdate { [weak self] update in
guard let this = self else { return nil }
guard update.runtime == this else { return nil }
let ourAdditions = update.additions.filter({ $0.deviceTypeIdentifier == deviceType.identifier })
let ourRemovals = update.removals.filter({ $0.deviceTypeIdentifier == deviceType.identifier })
if !ourAdditions.isEmpty || !ourRemovals.isEmpty {
return this.devices.nodesFor(deviceType: deviceType)
}
else {
return nil
}
}
}
func linkedForRuntimeStyle(from model: SimModel) -> some Node {
NodeLink(self) {
model.deviceTypes.supporting(runtime: self).map { devType in
devType.linkedForRuntimeStyle(from: model, runtime: self)
}
}
}
}

View file

@ -9,17 +9,17 @@ import SwiftUI
import Combine
class FilteredNode<T: Node>: Node, ObservableObject {
typealias FilteredList = [FilteredNode<T.List.Element>]
typealias FilteredList = [FilteredNode<T.Child>]
@Published var filtered : Bool
@Published var isExpanded = false
@Published var items : FilteredList?
var wrappedNode : T
var title : String { wrappedNode.title }
var headerTitle : String { wrappedNode.headerTitle }
var header : some View { wrappedNode.header }
var content : some View { wrappedNode.content }
var items : FilteredList?
var children : FilteredList { items ?? [] }
init(_ node: T) {
@ -63,9 +63,24 @@ class FilteredNode<T: Node>: Node, ObservableObject {
return nodeMatch
}
@discardableResult
func processUpdate(_ update: SimModel.Update) -> Bool {
if wrappedNode.processUpdate(update) {
items = wrappedNode.items?.asFilteredNodes()
}
if let items {
for node in items {
node.processUpdate(update)
}
}
return false
}
}
extension NodeList {
extension Array where Element: Node {
func asFilteredNodes() -> [FilteredNode<Element>] {
self.map { FilteredNode($0) }
}

View file

@ -7,11 +7,14 @@
import SwiftUI
protocol Node: NodeSource {
protocol Node {
associatedtype Icon: View
associatedtype Header: View
associatedtype Content: View
associatedtype Child: Node
var items : [Child]? { get set }
var title : String { get }
var headerTitle : String { get }
@ -23,14 +26,13 @@ protocol Node: NodeSource {
func matchedFilterOptions() -> SourceFilter.Options
func matchesFilter(_ filter: SourceFilter, inherited options: SourceFilter.Options) -> Bool
@discardableResult
mutating func processUpdate(_ update: SimModel.Update) -> Bool
}
extension Node {
var items : [LeafNode]? {
get { nil }
set { }
}
var items : [LeafNode]? { get { nil } set { } }
@ViewBuilder
func symbolIcon(_ systemName: String, color: Color? = nil, forHeader: Bool) -> some View {
if forHeader {
@ -66,14 +68,11 @@ extension Node {
func matchesFilter(_ filter: SourceFilter, inherited options: SourceFilter.Options) -> Bool {
filter.options.isSubset(of: options) && matchesTerm(filter.searchTerm)
}
}
/// Indicates a type that owns a list of Nodes
protocol NodeSource {
associatedtype List: NodeList
var items : List? { get set }
@discardableResult
mutating func processUpdate(_ update: SimModel.Update) -> Bool {
return false
}
}
/// Defines the requirements of a collection that can serve as a `NodeList`.
@ -82,14 +81,10 @@ protocol NodeList: RandomAccessCollection where Self.Element: Node, Index: Hasha
extension NodeList {
@NodeListBuilder
func linkEachTo<Item: Node>(emptyIsNil: Bool = false, @NodeListBuilder items: (Element) -> [Item]) -> some NodeList {
func linkEachTo<Item: Node>(emptyIsNil: Bool = false, @NodeListBuilder items: (Element) -> [Item]) -> [some Node] {
map { item in
item.link(emptyIsNil: emptyIsNil, to: { items(item) })
}
// Makes compiler unhappy. resultBuilder probably incorrect
// for item in self {
// item.link(to: { items(item) })
// }
}
}
@ -124,17 +119,22 @@ struct RootNode<Item: Node>: Node {
self.items = items()
}
func icon(forHeader: Bool) -> some View { symbolIcon("tree", forHeader: forHeader) }
func icon(forHeader: Bool) -> some View {
symbolIcon("tree", forHeader: forHeader)
}
}
struct NodeLink<Base: Node, Item: Node>: Node {
var base : Base
var items : [Item]?
var title : String { base.title }
var headerTitle : String { base.headerTitle }
var header : Base.Header { base.header }
var content : Base.Content { base.content }
typealias UpdateHandler = (SimModel.Update) -> [Item]??
var base : Base
var items : [Item]?
var title : String { base.title }
var headerTitle : String { base.headerTitle }
var header : Base.Header { base.header }
var content : Base.Content { base.content }
var updaterHandler : UpdateHandler? = nil
init(_ base: Base, emptyIsNil: Bool = false, @NodeListBuilder items: () -> [Item]) {
let list = items()
@ -142,10 +142,9 @@ struct NodeLink<Base: Node, Item: Node>: Node {
self.items = emptyIsNil ? (list.isEmpty ? nil : list) : list
}
@available(*, deprecated, message: "Consider using Root { items } instead")
init(@NodeListBuilder _ items: () -> [Item]) where Base == RootNode<Item> {
self.base = RootNode()
self.items = items()
init(_ base: Base, emptyIsNil: Bool = false, items: [Item]) {
self.base = base
self.items = emptyIsNil ? (items.isEmpty ? nil : items) : items
}
func icon(forHeader: Bool) -> some View {
@ -155,4 +154,19 @@ struct NodeLink<Base: Node, Item: Node>: Node {
func matchedFilterOptions() -> SourceFilter.Options {
return base.matchedFilterOptions()
}
mutating func onUpdate(_ handler: @escaping UpdateHandler) -> Self {
updaterHandler = handler
return self
}
@discardableResult
mutating func processUpdate(_ update: SimModel.Update) -> Bool {
guard let newItems = updaterHandler?(update) else { return false }
self.items = newItems
return true
}
}

119
SimDirs/Node/NodeAB.swift Normal file
View file

@ -0,0 +1,119 @@
//
// NodeAB.swift
// SimDirs
//
// Created by Casey Fleser on 3/18/23.
//
import SwiftUI
enum NodeAB<A: Node, B: Node>: Node, CustomStringConvertible {
case a(A)
case b(B)
var title : String {
switch self {
case .a(let node): return node.title
case .b(let node): return node.title
}
}
var headerTitle : String {
switch self {
case .a(let node): return node.headerTitle
case .b(let node): return node.headerTitle
}
}
@ViewBuilder
var header : some View {
switch self {
case .a(let node): node.header
case .b(let node): node.header
}
}
@ViewBuilder
var content : some View {
switch self {
case .a(let node): node.content
case .b(let node): node.content
}
}
var items : [NodeAB<A.Child, B.Child>]? {
get {
switch self {
case .a(let node): return node.items?.map { .a($0) }
case .b(let node): return node.items?.map { .b($0) }
}
}
set {
switch self {
case .a(var node):
let items : [A.Child]? = newValue?.compactMap({ ab in
guard case .a(let a) = ab else { return nil }
return a
})
node.items = items
self = .a(node)
case .b(var node):
let items : [B.Child]? = newValue?.compactMap({ ab in
guard case .b(let b) = ab else { return nil }
return b
})
node.items = items
self = .b(node)
}
}
}
var description : String {
let valueDesc : String
switch self {
case .a(let node): valueDesc = ".a: \(String(describing: node))"
case .b(let node): valueDesc = ".b: \(String(describing: node))"
}
return "NodeAB<A-\(A.self), B-\(B.self)>: \(valueDesc)"
}
func icon(forHeader: Bool) -> some View {
switch self {
case .a(let node): node.icon(forHeader: forHeader)
case .b(let node): node.icon(forHeader: forHeader)
}
}
func matchedFilterOptions() -> SourceFilter.Options {
switch self {
case .a(let node): return node.matchedFilterOptions()
case .b(let node): return node.matchedFilterOptions()
}
}
@discardableResult
mutating func processUpdate(_ update: SimModel.Update) -> Bool {
switch self {
case .a(var node):
let result = node.processUpdate(update)
self = .a(node)
return result
case .b(var node):
let result = node.processUpdate(update)
self = .b(node)
return result
}
}
}

View file

@ -9,123 +9,65 @@ import SwiftUI
@resultBuilder struct NodeListBuilder {
typealias P = Node
typealias OneOf = NodeAB
enum OneOf<A: P, B: P>: P, CustomStringConvertible {
typealias List = [NodeListBuilder.OneOf<A.List.Element, B.List.Element>]
case a(A)
case b(B)
var title : String {
switch self {
case .a(let node): return node.title
case .b(let node): return node.title
}
}
var headerTitle : String {
switch self {
case .a(let node): return node.headerTitle
case .b(let node): return node.headerTitle
}
}
@ViewBuilder
var header : some View {
switch self {
case .a(let node): node.header
case .b(let node): node.header
}
}
@ViewBuilder
var content : some View {
switch self {
case .a(let node): node.content
case .b(let node): node.content
}
}
var items : List? {
get {
switch self {
case .a(let node): return node.items?.map { .a($0) }
case .b(let node): return node.items?.map { .b($0) }
}
}
set { }
}
var description : String {
let valueDesc : String
switch self {
case .a(let node): valueDesc = ".a: \(String(describing: node))"
case .b(let node): valueDesc = ".b: \(String(describing: node))"
}
return "OneOf<A - \(A.self), B - \(B.self)>: \(valueDesc)"
}
func icon(forHeader: Bool) -> some View {
switch self {
case .a(let node): node.icon(forHeader: forHeader)
case .b(let node): node.icon(forHeader: forHeader)
}
}
func matchedFilterOptions() -> SourceFilter.Options {
switch self {
case .a(let node): return node.matchedFilterOptions()
case .b(let node): return node.matchedFilterOptions()
}
}
}
static func buildBlock<C: P>(_ c: [C]) -> [C] {
static func buildPartialBlock<C: P>(first c: [C]) -> [C] {
c
}
static func buildBlock<C0: P, C1: P> (
_ c0: [C0], _ c1: [C1]) -> [OneOf<C0, C1>]
{
[buildEither(first: c0), buildEither(second: c1)].flatMap { $0 }
// matching types
static func buildPartialBlock<C: P>(accumulated c0: [C], next c1: [C]) -> [C] {
c0 + c1
}
static func buildBlock<C0: P, C1: P, C2: P> (
_ c0: [C0], _ c1: [C1], _ c2: [C2]) -> [OneOf<OneOf<C0, C1>, C2>]
{
[buildEither(first: buildBlock(c0, c1)), buildEither(second: c2)].flatMap { $0 }
// matches A of OneOf<A, B>
static func buildPartialBlock<A: P, B: P>(accumulated ab: [OneOf<A, B>], next a: [A]) -> [OneOf<A, B>] {
ab + a.map { .a($0) }
}
// matches B of OneOf<A, B>
static func buildPartialBlock<A: P, B: P>(accumulated ab: [OneOf<A, B>], next b: [B]) -> [OneOf<A, B>] {
ab + b.map { .b($0) }
}
// matches A of OneOf<OneOf<A, B>, C>
static func buildPartialBlock<A: P, B: P, C: P>(accumulated abc: [OneOf<OneOf<A, B>, C>], next a: [A]) -> [OneOf<OneOf<A, B>, C>] {
buildPartialBlock(accumulated: [] as [OneOf<A, B>], next: a).map { .a($0) }
}
// matches B of OneOf<OneOf<A, B>, C>
static func buildPartialBlock<A: P, B: P, C: P>(accumulated abc: [OneOf<OneOf<A, B>, C>], next b: [B]) -> [OneOf<OneOf<A, B>, C>] {
buildPartialBlock(accumulated: [] as [OneOf<A, B>], next: b).map { .a($0) }
}
// matches C of OneOf<OneOf<A, B>, C>
static func buildPartialBlock<A: P, B: P, C: P>(accumulated abc: [OneOf<OneOf<A, B>, C>], next c: [C]) -> [OneOf<OneOf<A, B>, C>] {
abc + c.map { .b($0) }
}
// matches A of OneOf<OneOf<A, B>, OneOf<C, D>>
static func buildPartialBlock<A: P, B: P, C: P, D: P>(accumulated abcd: [OneOf<OneOf<A, B>, OneOf<C, D>>], next a: [A]) -> [OneOf<OneOf<A, B>, OneOf<C, D>>] {
buildPartialBlock(accumulated: [] as [OneOf<A, B>], next: a).map { .a($0) }
}
// matches B of OneOf<OneOf<A, B>, OneOf<C, D>>
static func buildPartialBlock<A: P, B: P, C: P, D: P>(accumulated abcd: [OneOf<OneOf<A, B>, OneOf<C, D>>], next b: [B]) -> [OneOf<OneOf<A, B>, OneOf<C, D>>] {
buildPartialBlock(accumulated: [] as [OneOf<A, B>], next: b).map { .a($0) }
}
static func buildBlock<C0: P, C1: P, C2: P, C3: P> (
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3]) -> [OneOf<OneOf<C0, C1>, OneOf<C2, C3>>]
{
[buildEither(first: buildBlock(c0, c1)), buildEither(second: buildBlock(c2, c3))].flatMap { $0 }
// matches C of OneOf<OneOf<A, B>, OneOf<C, D>>
static func buildPartialBlock<A: P, B: P, C: P, D: P>(accumulated abcd: [OneOf<OneOf<A, B>, OneOf<C, D>>], next c: [C]) -> [OneOf<OneOf<A, B>, OneOf<C, D>>] {
buildPartialBlock(accumulated: [] as [OneOf<C, D>], next: c).map { .b($0) }
}
static func buildBlock<C0: P, C1: P, C2: P, C3: P, C4: P> (
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3], _ c4: [C4]) -> [OneOf<OneOf<OneOf<C0, C1>, OneOf<C2, C3>>, C4>]
{
[buildEither(first: buildBlock(c0, c1, c2, c3)), buildEither(second: c4)].flatMap { $0 }
// matches D of OneOf<OneOf<A, B>, OneOf<C, D>>
static func buildPartialBlock<A: P, B: P, C: P, D: P>(accumulated abcd: [OneOf<OneOf<A, B>, OneOf<C, D>>], next d: [D]) -> [OneOf<OneOf<A, B>, OneOf<C, D>>] {
buildPartialBlock(accumulated: [] as [OneOf<C, D>], next: d).map { .b($0) }
}
static func buildBlock<C0: P, C1: P, C2: P, C3: P, C4: P, C5: P> (
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3], _ c4: [C4], _ c5: [C5]) -> [OneOf<OneOf<OneOf<C0, C1>, OneOf<C2, C3>>, OneOf<C4, C5>>]
{
[buildEither(first: buildBlock(c0, c1, c2, c3)), buildEither(second: buildBlock(c4, c5))].flatMap { $0 }
}
static func buildBlock<C0: P, C1: P, C2: P, C3: P, C4: P, C5: P, C6: P> (
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3], _ c4: [C4], _ c5: [C5], _ c6: [C6]) -> [OneOf<OneOf<OneOf<C0, C1>, OneOf<C2, C3>>, OneOf<OneOf<C4, C5>, C6>>]
{
[buildEither(first: buildBlock(c0, c1, c2, c3)), buildEither(second: buildBlock(c4, c5, c6))].flatMap { $0 }
}
static func buildBlock<C0: P, C1: P, C2: P, C3: P, C4: P, C5: P, C6: P, C7: P> (
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3], _ c4: [C4], _ c5: [C5], _ c6: [C6], _ c7: [C7]) -> [OneOf<OneOf<OneOf<C0, C1>, OneOf<C2, C3>>, OneOf<OneOf<C4, C5>, OneOf<C6, C7>>>]
{
[buildEither(first: buildBlock(c0, c1, c2, c3)), buildEither(second: buildBlock(c4, c5, c6, c7))].flatMap { $0 }
// non-matching types
static func buildPartialBlock<C0: P, C1: P>(accumulated c0: [C0], next c1: [C1]) -> [OneOf<C0, C1>] {
c0.map({ OneOf<C0, C1>.a($0) }) + c1.map({ OneOf<C0, C1>.b($0) })
}
// static func buildBlock<C: Node>(_ c: [C]...) -> [C] {
@ -159,15 +101,11 @@ import SwiftUI
}
static func buildExpression<N: Node>(_ node: N) -> [N] {
[node]
return [node]
}
static func buildExpression<NL: NodeList>(_ nodeList: NL) -> [NL.Element] {
Array(nodeList)
}
static func buildExpression<NS: NodeSource>(_ nodeSource: NS) -> [NS.List.Element] {
nodeSource.items.map({ buildExpression($0) }) ?? []
return Array(nodeList)
}
}

View file

@ -6,8 +6,10 @@
//
import SwiftUI
import Combine
struct FilteredNodeView<T: Node>: View {
@Environment(\.deviceUpdates) var deviceUpdates
@StateObject var node : FilteredNode<T>
@Binding var filter : SourceFilter
@ -25,6 +27,10 @@ struct FilteredNodeView<T: Node>: View {
.searchable(text: $filter.searchTerm, placement: .sidebar)
.onAppear { node.applyFilter(filter) }
.onChange(of: filter) { node.applyFilter($0) }
.onReceive(deviceUpdates) { update in
node.processUpdate(update)
node.applyFilter(filter)
}
}
}
@ -32,7 +38,7 @@ extension FilteredNodeView {
struct Root: View {
@ObservedObject var node : FilteredNode<T>
var visibleItems : FilteredNode<T>.List { node.items.map { $0.filter { !$0.filtered} } ?? [] }
var visibleItems : [FilteredNode<T.Child>] { node.items.map { $0.filter { !$0.filtered} } ?? [] }
var body: some View {
let items = visibleItems

View file

@ -1,124 +0,0 @@
//
// SourceState.swift
// SimDirs
//
// Created by Casey Fleser on 6/21/22.
//
import Foundation
import Combine
class SourceState: ObservableObject {
enum Style: Int, CaseIterable, Identifiable {
case placeholder
case byDevice
case byRuntime
var id : Int { rawValue }
var title : String {
switch self {
case .placeholder: return "Placeholder"
case .byDevice: return "By Device"
case .byRuntime: return "By Runtime"
}
}
var visible : Bool {
switch self {
case .placeholder: return false
default: return true
}
}
}
@Published var style = Style.placeholder // { didSet { rebuildBase() } }
@Published var selection : UUID?
var model : SimModel
var deviceUpdates : Cancellable?
init(model: SimModel) {
self.style = .byDevice
self.model = model
deviceUpdates = model.deviceUpdates.sink(receiveValue: applyDeviceUpdates)
}
#warning("TODO: still need to apply updates")
func applyDeviceUpdates(_ updates: SimDevicesUpdates) {
#if false
switch base {
case .placeholder:
break
case let .device(_, item):
for prodFamily in item.children {
for devType in prodFamily.children {
for runtime in devType.children {
guard updates.runtime.identifier == runtime.data.identifier else { continue }
let devTypeDevices = updates.additions.filter { $0.isDeviceOfType(devType.data) }
runtime.children = runtime.children.filter { device in !updates.removals.contains { $0.udid == device.data.udid } }
runtime.children.append(contentsOf: devTypeDevices.map { device in
let imageDesc = devType.imageDesc.withColor(device.isAvailable ? .green : .red)
return Device(data: device, children: device.apps.map { app in App(data: app, children: []) }, customImgDesc: imageDesc)
})
}
}
}
case let .runtime(_, item):
for platform in item.children {
for runtime in platform.children {
guard updates.runtime.identifier == runtime.data.identifier else { continue }
for devType in runtime.children {
let devTypeDevices = updates.additions.filter { $0.isDeviceOfType(devType.data) }
devType.children = devType.children.filter { device in !updates.removals.contains { $0.udid == device.data.udid } }
devType.children.append(contentsOf: devTypeDevices.map { device in
let imageDesc = devType.imageDesc.withColor(device.isAvailable ? .green : .red)
return Device(data: device, children: device.apps.map { app in App(data: app, children: []) }, customImgDesc: imageDesc)
})
}
}
}
}
applyFilter()
#endif
}
@NodeListBuilder
var items: some NodeList {
switch style {
case .placeholder: [] as [LeafNode]
case .byDevice: deviceStyleItems
case .byRuntime: runtimeStyleItems
}
}
@NodeListBuilder
var deviceStyleItems: some NodeList {
SimProductFamily.presentation.linkEachTo { family in
model.deviceTypes.supporting(productFamily: family).linkEachTo(emptyIsNil: true) { devType in
model.runtimes.supporting(deviceType: devType).linkEachTo(emptyIsNil: true) { runtime in
runtime.devices.linkingDeviceType(devType)
}
}
}
}
@NodeListBuilder
var runtimeStyleItems: some NodeList {
SimPlatform.presentation.linkEachTo(emptyIsNil: true) { platform in
model.runtimes.supporting(platform: platform).linkEachTo(emptyIsNil: true) { runtime in
model.deviceTypes.supporting(runtime: runtime).linkEachTo(emptyIsNil: true) { devType in
runtime.devices.linkingDeviceType(devType)
}
}
}
}
}

View file

@ -10,6 +10,10 @@ import SwiftUI
struct DeviceHeader: View {
@ObservedObject var device : SimDevice
init(_ device: SimDevice) {
self.device = device
}
var body: some View {
VStack(alignment: .leading, spacing: 3.0) {
HStack(spacing: 8.0) {
@ -36,8 +40,8 @@ struct DeviceHeader_Previews: PreviewProvider {
static var previews: some View {
if !devices.isEmpty {
DeviceHeader(device: devices[0])
DeviceHeader(device: devices.randomElement() ?? devices[1])
DeviceHeader(devices[0])
DeviceHeader(devices.randomElement() ?? devices[1])
}
else {
Text("No devices")

View file

@ -8,13 +8,13 @@
import SwiftUI
struct ToolbarMenu: View {
@ObservedObject var state : SourceState
@Binding var style : ContentView.Style
@Binding var filter : SourceFilter
var body: some View {
Menu {
Picker("Style", selection: $state.style) {
ForEach(SourceState.Style.allCases) { style in
Picker("Style", selection: $style) {
ForEach(ContentView.Style.allCases) { style in
if style.visible {
Text(style.title).tag(style)
}
@ -30,10 +30,10 @@ struct ToolbarMenu: View {
}
struct ToolbarMenu_Previews: PreviewProvider {
static var state = SourceState(model: SimModel())
@State static var style = ContentView.Style.byDevice
@State static var filter = SourceFilter.restore()
static var previews: some View {
ToolbarMenu(state: state, filter: $filter)
ToolbarMenu(style: $style, filter: $filter)
}
}