diff options
Diffstat (limited to 'Map/Views')
| -rw-r--r-- | Map/Views/ContentView.swift | 104 | ||||
| -rw-r--r-- | Map/Views/DefaultMapView.swift | 23 | ||||
| -rw-r--r-- | Map/Views/MapApp.swift | 21 | ||||
| -rw-r--r-- | Map/Views/MapDetail.swift | 101 | ||||
| -rw-r--r-- | Map/Views/MapRender.swift | 61 | ||||
| -rw-r--r-- | Map/Views/MapTextEditor.swift | 71 | ||||
| -rw-r--r-- | Map/Views/Stage.swift | 161 |
7 files changed, 542 insertions, 0 deletions
diff --git a/Map/Views/ContentView.swift b/Map/Views/ContentView.swift new file mode 100644 index 0000000..4f55c27 --- /dev/null +++ b/Map/Views/ContentView.swift @@ -0,0 +1,104 @@ +// +// ContentView.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/1/21. +// + +import CoreData +import SwiftUI + +struct ContentView: View { + @Environment(\.managedObjectContext) private var viewContext + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Map.createdAt, ascending: true)], + animation: .default) + private var maps: FetchedResults<Map> + + var body: some View { + NavigationView { + List { + if maps.count == 0 { + DefaultMapView() + } + ForEach(maps) { map in + NavigationLink(destination: MapDetailView(map: map)) { + HStack { + Text(map.title ?? "Untitled Map") + Spacer() + Text(mapFormatter.string(from: (map.createdAt ?? Date()))) + .font(.caption) + .padding(.vertical, 2.0) + .padding(.horizontal, 4.0) + .background(Color.accentColor) + .foregroundColor(Color.black) + .cornerRadius(2.0) + }.padding(.leading, 8.0) + } + } + .onDelete(perform: deleteMaps) + }.frame(minWidth: 250.0, alignment: .leading) + .toolbar { + HStack { + Button(action: toggleSidebar) { + Label("Toggle Sidebar", systemImage: "sidebar.left") + } + Button(action: addMap) { + Label("Add Map", systemImage: "plus") + } + } + } + } + } + + private func toggleSidebar() { + NSApp.keyWindow?.firstResponder?.tryToPerform( + #selector(NSSplitViewController.toggleSidebar(_:)), with: nil) + } + + private func addMap() { + withAnimation { + let newMap = Map(context: viewContext) + newMap.uuid = UUID() + newMap.createdAt = Date() + newMap.title = "Map \(newMap.createdAt!.format())" + newMap.content = "" + + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } + + private func deleteMaps(offsets: IndexSet) { + + withAnimation { + offsets.map { maps[$0] }.forEach(viewContext.delete) + + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } +} + +private let mapFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .none + return formatter +}() + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView().environment( + \.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Map/Views/DefaultMapView.swift b/Map/Views/DefaultMapView.swift new file mode 100644 index 0000000..66914f1 --- /dev/null +++ b/Map/Views/DefaultMapView.swift @@ -0,0 +1,23 @@ +// +// ContentView.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/1/21. +// + +import CoreData +import SwiftUI + +struct DefaultMapView: View { + + var body: some View { + Text("Select a map from the left hand side, or click on + to create one.") + } +} + +struct DefaultMapView_Previews: PreviewProvider { + static var previews: some View { + MapDetailView(map: Map()).environment( + \.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Map/Views/MapApp.swift b/Map/Views/MapApp.swift new file mode 100644 index 0000000..0f8ae64 --- /dev/null +++ b/Map/Views/MapApp.swift @@ -0,0 +1,21 @@ +// +// MapApp.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/1/21. +// + +import SwiftUI + +@main +struct MapApp: App { + let persistenceController = PersistenceController.shared + + var body: some Scene { + WindowGroup { + ContentView() + .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environmentObject(AppStore(initialState: AppState(), reducer: appStateReducer)) + }.windowStyle(HiddenTitleBarWindowStyle()) + } +} diff --git a/Map/Views/MapDetail.swift b/Map/Views/MapDetail.swift new file mode 100644 index 0000000..a8ea3d1 --- /dev/null +++ b/Map/Views/MapDetail.swift @@ -0,0 +1,101 @@ +// +// ContentView.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/1/21. +// + +import CoreData +import SwiftUI + +struct MapDetailView: View { + @Environment(\.managedObjectContext) private var viewContext + @Environment(\.colorScheme) var colorScheme + + @EnvironmentObject var store: AppStore + + @ObservedObject var map: Map + + private var mapColor: MapColor { + MapColor.colorForScheme(colorScheme) + } + + private var title: Binding<String> { + Binding( + get: { map.title ?? "" }, + set: { title in + map.title = title + } + ) + } + + private var content: Binding<String> { + Binding( + get: { map.content ?? "" }, + set: { content in + map.content = content + } + ) + } + + @State private var selectedEvolution = StageType.general + + var body: some View { + if map.uuid != nil { + VSplitView { + VStack { + HStack { + TextField( + "Title", text: title, + onCommit: { + try? viewContext.save() + } + ).font(.title2).textFieldStyle(PlainTextFieldStyle()).padding(.vertical, 4.0).padding( + .leading, 4.0) + Button(action: saveText) { + Image(systemName: "doc.text") + }.padding(.vertical, 4.0).padding(.leading, 4.0) + Button(action: saveImage) { + Image(systemName: "photo") + }.padding(.vertical, 4.0).padding(.leading, 4.0).padding(.trailing, 8.0) + } + Picker("Evolution", selection: $selectedEvolution) { + ForEach(StageType.allCases) { stage in + Text(Stage.title(stage)).tag(stage).padding(4.0) + } + }.padding(.horizontal, 8.0).padding(.vertical, 4.0) + + ZStack(alignment: .topLeading) { + MapTextEditor(text: content).onChange(of: map.content) { _ in + try? viewContext.save() + } + .background(mapColor.background) + .foregroundColor(mapColor.foreground) + .frame(minHeight: 96.0) + }.padding(.top, 8.0).padding(.leading, 8.0).background(mapColor.background).cornerRadius( + 5.0) + }.padding(.horizontal, 8.0) + ScrollView([.horizontal, .vertical]) { + MapRenderView(map: map, evolution: Stage.stages(selectedEvolution)) + }.background(mapColor.background) + } + } else { + DefaultMapView() + } + } + + private func saveText() { + store.send(.exportMapAsText(map: map)) + } + + private func saveImage() { + store.send(.exportMapAsImage(map: map, evolution: selectedEvolution)) + } +} + +struct MapDetailView_Previews: PreviewProvider { + static var previews: some View { + MapDetailView(map: Map()).environment( + \.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Map/Views/MapRender.swift b/Map/Views/MapRender.swift new file mode 100644 index 0000000..b35b724 --- /dev/null +++ b/Map/Views/MapRender.swift @@ -0,0 +1,61 @@ +// +// ContentView.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/1/21. +// + +import CoreData +import CoreGraphics +import SwiftUI + +struct MapRenderView: View { + + @Environment(\.colorScheme) var colorScheme + + @ObservedObject var map: Map + let evolution: Stage + + let mapSize = CGSize(width: 1300.0, height: 1000.0) + + let lineWidth = CGFloat(1.0) + let vertexSize = CGSize(width: 25.0, height: 25.0) + let padding = CGFloat(30.0) + + var parsedMap: ParsedMap { + return map.parse() + } + + var body: some View { + ZStack(alignment: .topLeading) { + + Path { path in + path.addRect( + CGRect( + x: -padding, y: -padding, width: mapSize.width + padding * 2, + height: mapSize.height + padding * 2)) + }.fill(MapColor.colorForScheme(colorScheme).background) + + MapStages(mapSize: mapSize, lineWidth: lineWidth, stages: parsedMap.stages) + MapAxes( + mapSize: mapSize, lineWidth: lineWidth, evolution: evolution, stages: parsedMap.stages) + MapOpportunities( + mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, + opportunities: parsedMap.opportunities) + MapBlockers(mapSize: mapSize, vertexSize: vertexSize, blockers: parsedMap.blockers) + MapVertices(mapSize: mapSize, vertexSize: vertexSize, vertices: parsedMap.vertices) + MapEdges( + mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: parsedMap.edges) + }.frame( + width: mapSize.width, + height: mapSize.height, alignment: .topLeading + ).padding(padding) + } +} + +struct MapRenderView_Previews: PreviewProvider { + static var previews: some View { + MapDetailView(map: Map()).environment( + \.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Map/Views/MapTextEditor.swift b/Map/Views/MapTextEditor.swift new file mode 100644 index 0000000..7251c59 --- /dev/null +++ b/Map/Views/MapTextEditor.swift @@ -0,0 +1,71 @@ +import Cocoa +import SwiftUI + +class MapTextEditorController: NSViewController { + + @Binding var text: String + + init(text: Binding<String>) { + self._text = text + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + let scrollView = NSTextView.scrollableTextView() + let textView = scrollView.documentView as! NSTextView + + scrollView.translatesAutoresizingMaskIntoConstraints = false + + textView.delegate = self + textView.string = self.text + textView.isEditable = true + textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular) + self.view = scrollView + } + + override func viewDidAppear() { + self.view.window?.makeFirstResponder(self.view) + } +} + +extension MapTextEditorController: NSTextViewDelegate { + + func textDidChange(_ obj: Notification) { + if let textField = obj.object as? NSTextView { + self.text = textField.string + } + } + + func textView(_ view: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool + { + let range = Range(shouldChangeTextIn, in: view.string) + let target = view.string[range!] + + if target == "--" { + return false + } + + return true + } +} + +struct MapTextEditor: NSViewControllerRepresentable { + + @Binding var text: String + + func makeNSViewController( + context: NSViewControllerRepresentableContext<MapTextEditor> + ) -> MapTextEditorController { + return MapTextEditorController(text: $text) + } + + func updateNSViewController( + _ nsViewController: MapTextEditorController, + context: NSViewControllerRepresentableContext<MapTextEditor> + ) { + } +} diff --git a/Map/Views/Stage.swift b/Map/Views/Stage.swift new file mode 100644 index 0000000..23432ad --- /dev/null +++ b/Map/Views/Stage.swift @@ -0,0 +1,161 @@ +struct Stage { + let i: String + let ii: String + let iii: String + let iv: String + + static func stages(_ type: StageType) -> Stage { + switch type { + case .general: + return Stage( + i: "Genesis", ii: "Custom Built", iii: "Product (+rental)", iv: "Commodity (+utility)") + case .practice: + return Stage( + i: "Novel", ii: "Emerging", iii: "Good", iv: "Best") + case .data: + return Stage( + i: "Unmodelled", ii: "Divergent", iii: "Convergent", iv: "Modelled") + case .knowledge: + return Stage( + i: "Concept", ii: "Hypothesis", iii: "Theory", iv: "Accepted") + case .ubiquity: + return Stage( + i: "Rare", ii: "Slowly Increasing Consumption", iii: "Rapidly Increasing Consumption", + iv: "Widespread and stabilising") + case .certainty: + return Stage( + i: "Poorly Understood", ii: "Rapid Increase In Learning", + iii: "Rapid Increase in Use / fit for purpose", iv: "Commonly understood (in terms of use)") + case .publicationTypes: + return Stage( + i: "Normally describing the wonder of the thing", + ii: "Build / construct / awareness and learning", + iii: "Maintenance / operations / installation / feature", iv: "Focused on use") + case .market: + return Stage( + i: "Undefined Market", ii: "Forming Market", iii: "Growing Market", iv: "Mature Market") + case .knowledgeManagement: + return Stage( + i: "Uncertain", ii: "Learning on use", iii: "Learning on operation", iv: "Known / accepted") + case .marketPerception: + return Stage( + i: "Chaotic (non-linear)", ii: "Domain of experts", iii: "Increasing expectation of use", + iv: "Ordered (appearance of being trivial) / trivial") + case .userPerception: + return Stage( + i: "Different / confusing / exciting / surprising", ii: "Leading edge / emerging", + iii: "Increasingly common / disappointed if not used", iv: "Standard / expected") + case .perceptionInIndustry: + return Stage( + i: "Competitive advantage / unpredictable / unknown", + ii: "Competitive advantage / ROI / case examples", + iii: "Advantage through implementation / features", iv: "Cost of doing business") + case .focusOfValue: + return Stage( + i: "High future worth", ii: "Seeking profit / ROI", iii: "High profitability", + iv: "High volume / reducing margin") + case .understanding: + return Stage( + i: "Poorly Understood / unpredictable", + ii: "Increasing understanding / development of measures", + iii: "Increasing education / constant refinement of needs / measures", + iv: "Believed to be well defined / stable / measurable") + case .comparison: + return Stage( + i: "Constantly changing / a differential / unstable", + ii: "Learning from others / testing the water / some evidential support", + iii: "Feature difference", iv: "Essential / operational advantage") + case .failure: + return Stage( + i: "High / tolerated / assumed", ii: "Moderate / unsurprising but disappointed", + iii: "Not tolerated, focus on constant improvement", + iv: "Operational efficiency and surprised by failure") + case .marketAction: + return Stage( + i: "Gambling / driven by gut", ii: "Exploring a \"found\" value", + iii: "Market analysis / listening to customers", iv: "Metric driven / build what is needed") + case .efficiency: + return Stage( + i: "Reducing the cost of change (experimentation)", ii: "Reducing cost of waste (Learning)", + iii: "Reducing cost of waste (Learning)", iv: "Reducing cost of deviation (Volume)") + case .decisionDrivers: + return Stage( + i: "Heritage / culture", ii: "Analyses & synthesis", iii: "Analyses & synthesis", + iv: "Previous Experience") + case .behavior: + return Stage( + i: "Uncertain when to use", ii: "Learning when to use", iii: "Learning through use", + iv: "Known / common usage") + } + } + + static func title(_ type: StageType) -> String { + switch type { + case .general: + return "Activities" + case .practice: + return "Practice" + case .data: + return "Data" + case .knowledge: + return "Knowledge" + case .ubiquity: + return "Ubiquity" + case .certainty: + return "Certainty" + case .publicationTypes: + return "Publication Types" + case .market: + return "Market" + case .knowledgeManagement: + return "Knowledge Management" + case .marketPerception: + return "Market Perception" + case .userPerception: + return "User Perception" + case .perceptionInIndustry: + return "Perception In Industry" + case .focusOfValue: + return "Focus Of Value" + case .understanding: + return "Understanding" + case .comparison: + return "Comparison" + case .failure: + return "Failure" + case .marketAction: + return "Market Action" + case .efficiency: + return "Efficiency" + case .decisionDrivers: + return "Decision Drivers" + case .behavior: + return "Behavior" + } + } +} + +enum StageType: String, CaseIterable, Identifiable { + case general + case practice + case data + case knowledge + case ubiquity + case certainty + case publicationTypes + case market + case knowledgeManagement + case marketPerception + case userPerception + case perceptionInIndustry + case focusOfValue + case understanding + case comparison + case failure + case marketAction + case efficiency + case decisionDrivers + case behavior + + var id: String { self.rawValue } +} |