]> git.r.bdr.sh - rbdr/map/commitdiff
Fix performance and undo 1.2.0
authorRuben Beltran del Rio <redacted>
Fri, 5 Feb 2021 23:55:04 +0000 (00:55 +0100)
committerRuben Beltran del Rio <redacted>
Fri, 5 Feb 2021 23:55:04 +0000 (00:55 +0100)
Map.xcodeproj/project.pbxproj
Map/Debouncer.swift
Map/Info.plist
Map/MapParser/MapParser.swift
Map/State/AppState.swift
Map/Views/ContentView.swift
Map/Views/MapDetail.swift
Map/Views/MapRender.swift
Map/Views/MapTextEditor.swift
Map/Views/SlowMapRender.swift [new file with mode: 0644]

index 12a5d1cd1a8d87137cfe730c9498a950e91d20ee..b212de6ee8128558dc1ed803e815508ac46b9ade 100644 (file)
@@ -33,6 +33,7 @@
                B539516C25CB0C9300959F72 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539516B25CB0C9200959F72 /* Store.swift */; };
                B539517425CB0CA400959F72 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539517325CB0CA400959F72 /* AppState.swift */; };
                B539518125CB2D7A00959F72 /* MapTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539518025CB2D7A00959F72 /* MapTextEditor.swift */; };
+               B587BB6025CDCECB00F328ED /* SlowMapRender.swift in Sources */ = {isa = PBXBuildFile; fileRef = B587BB5F25CDCECB00F328ED /* SlowMapRender.swift */; };
                B5CF75C925CC19FC003BFF3D /* EvolutionPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */; };
                B5CF75CF25CC7965003BFF3D /* MapParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75CE25CC7965003BFF3D /* MapParser.swift */; };
                B5CF75D825CC79BC003BFF3D /* VertexParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75D725CC79BC003BFF3D /* VertexParserStrategy.swift */; };
@@ -94,6 +95,7 @@
                B539516B25CB0C9200959F72 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
                B539517325CB0CA400959F72 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
                B539518025CB2D7A00959F72 /* MapTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTextEditor.swift; sourceTree = "<group>"; };
+               B587BB5F25CDCECB00F328ED /* SlowMapRender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlowMapRender.swift; sourceTree = "<group>"; };
                B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvolutionPicker.swift; sourceTree = "<group>"; };
                B5CF75CE25CC7965003BFF3D /* MapParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapParser.swift; sourceTree = "<group>"; };
                B5CF75D725CC79BC003BFF3D /* VertexParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VertexParserStrategy.swift; sourceTree = "<group>"; };
                                B52625A525C876C3003E73B7 /* MapDetail.swift */,
                                B523C74A25C9C1BA00C44061 /* DefaultMapView.swift */,
                                B539518025CB2D7A00959F72 /* MapTextEditor.swift */,
+                               B587BB5F25CDCECB00F328ED /* SlowMapRender.swift */,
                                B52625AF25C87C14003E73B7 /* MapRender.swift */,
                                B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */,
                        );
                                B526257225C874F9003E73B7 /* MapApp.swift in Sources */,
                                B52625A625C876C3003E73B7 /* MapDetail.swift in Sources */,
                                B523C76225CA05A300C44061 /* MapStages.swift in Sources */,
+                               B587BB6025CDCECB00F328ED /* SlowMapRender.swift in Sources */,
                                B5CF75F725CC97CA003BFF3D /* Debouncer.swift in Sources */,
                                B523C76C25CA0DFA00C44061 /* MapEdges.swift in Sources */,
                        );
                                CODE_SIGN_ENTITLEMENTS = Map/Map.entitlements;
                                CODE_SIGN_STYLE = Automatic;
                                COMBINE_HIDPI_IMAGES = YES;
+                               CURRENT_PROJECT_VERSION = 3;
                                DEVELOPMENT_ASSET_PATHS = "\"Map/Preview Content\"";
                                DEVELOPMENT_TEAM = S68NHQVJXW;
                                ENABLE_HARDENED_RUNTIME = YES;
                                        "@executable_path/../Frameworks",
                                );
                                MACOSX_DEPLOYMENT_TARGET = 11;
-                               MARKETING_VERSION = 1.1.0;
+                               MARKETING_VERSION = 1.2.0;
                                PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Map;
                                PRODUCT_NAME = "$(TARGET_NAME)";
                                SWIFT_VERSION = 5.0;
                                CODE_SIGN_ENTITLEMENTS = Map/Map.entitlements;
                                CODE_SIGN_STYLE = Automatic;
                                COMBINE_HIDPI_IMAGES = YES;
+                               CURRENT_PROJECT_VERSION = 3;
                                DEVELOPMENT_ASSET_PATHS = "\"Map/Preview Content\"";
                                DEVELOPMENT_TEAM = S68NHQVJXW;
                                ENABLE_HARDENED_RUNTIME = YES;
                                        "@executable_path/../Frameworks",
                                );
                                MACOSX_DEPLOYMENT_TARGET = 11;
-                               MARKETING_VERSION = 1.1.0;
+                               MARKETING_VERSION = 1.2.0;
                                PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Map;
                                PRODUCT_NAME = "$(TARGET_NAME)";
                                SWIFT_VERSION = 5.0;
index f119d8a5d1bce9f1f8e027ad635949a463c84f41..cd7960a8fd18322e02f3e9b4788bdaee6a563fcd 100644 (file)
@@ -3,7 +3,7 @@ import Foundation
 class Debouncer {
 
   // MARK: - Properties
-  private let queue = DispatchQueue.main
+  private let queue = DispatchQueue.global(qos: .utility)
   private var workItem = DispatchWorkItem(block: {})
   private var interval: TimeInterval
 
index 903c15d789e633eb6da9db189789bd88cbe5a350..6ba8cf0dcdc91e1ec37666bc341e3eeedc32ffba 100644 (file)
@@ -17,7 +17,7 @@
        <key>CFBundleShortVersionString</key>
        <string>$(MARKETING_VERSION)</string>
        <key>CFBundleVersion</key>
-       <string>1</string>
+       <string>$(CURRENT_PROJECT_VERSION)</string>
        <key>LSApplicationCategoryType</key>
        <string>public.app-category.productivity</string>
        <key>LSMinimumSystemVersion</key>
index 3528eb5d4d26375c2bda7a08b010b679628e8549..cce62514d8b29ff9303a759e12dccc5c1aed05a6 100644 (file)
@@ -25,6 +25,9 @@ struct ParsedMap {
   let blockers: [Blocker]
   let opportunities: [Opportunity]
   let stages: [CGFloat]
+
+  static let empty: ParsedMap = ParsedMap(
+    vertices: [], edges: [], blockers: [], opportunities: [], stages: defaultDimensions)
 }
 
 struct Vertex {
index f0620512c342a062761979b8f545e90e0b96a544..c261725fbae1eb22c893f9d8c886489ab0febab5 100644 (file)
@@ -38,7 +38,8 @@ func appStateReducer(state: inout AppState, action: AppAction) {
     window.makeKeyAndOrderFront(nil)
 
     let renderView = MapRenderView(
-      content: map.content ?? "", evolution: Stage.stages(state.selectedEvolution))
+      content: Binding.constant(map.content ?? ""),
+      evolution: Binding.constant(Stage.stages(state.selectedEvolution)))
 
     let view = NSHostingView(rootView: renderView)
     window.contentView = view
@@ -91,7 +92,7 @@ func appStateReducer(state: inout AppState, action: AppAction) {
         print("Cancel")
       }
     }
-  case .deleteMap(map: let map):
+  case .deleteMap(let map):
     let context = PersistenceController.shared.container.viewContext
     context.delete(map)
 
index f439026249988f669b78b118f7d9b4116db2ed10..534c14c41761453e6666776ae58100d51943b9ef 100644 (file)
@@ -25,7 +25,9 @@ struct ContentView: View {
           DefaultMapView()
         }
         ForEach(maps) { map in
-          NavigationLink(destination: MapDetailView(map: map)) {
+          NavigationLink(
+            destination: MapDetailView(map: map, title: map.title ?? "", content: map.content ?? "")
+          ) {
             HStack {
               Text(map.title ?? "Untitled Map")
               Spacer()
@@ -38,10 +40,12 @@ struct ContentView: View {
                 .cornerRadius(2.0)
             }.padding(.leading, 8.0)
           }.contextMenu {
-            Button(action: { store.send(.deleteMap(map: map))}) {
-              Image(systemName: "trash")
-              Text("Delete")
-            }
+            Button(
+              action: { store.send(.deleteMap(map: map)) },
+              label: {
+                Image(systemName: "trash")
+                Text("Delete")
+              })
           }
         }
         .onDelete(perform: deleteMaps)
@@ -56,7 +60,7 @@ struct ContentView: View {
             }
           }
         }
-        DefaultMapView()
+      DefaultMapView()
     }
   }
 
index 6ba92bb76f9fff10ad9f0ad20cea2c983f41dd1a..bebe3a12414a5e9be8f5a9ca46de107d4442c530 100644 (file)
@@ -5,9 +5,25 @@
 //  Created by Ruben Beltran del Rio on 2/1/21.
 //
 
+import Combine
 import CoreData
 import SwiftUI
 
+class SaveTimer {
+  let currentTimePublisher = Timer.TimerPublisher(interval: 1, runLoop: .main, mode: .default)
+  let cancellable: AnyCancellable?
+
+  init() {
+    self.cancellable = currentTimePublisher.connect() as? AnyCancellable
+  }
+
+  deinit {
+    self.cancellable?.cancel()
+  }
+}
+
+let timer = SaveTimer()
+
 struct MapDetailView: View {
   @Environment(\.managedObjectContext) private var viewContext
   @Environment(\.colorScheme) var colorScheme
@@ -20,23 +36,8 @@ struct MapDetailView: View {
     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 var title: String
+  @State var content: String
 
   var body: some View {
     if map.uuid != nil {
@@ -44,10 +45,7 @@ struct MapDetailView: View {
         VStack {
           HStack {
             TextField(
-              "Title", text: title,
-              onCommit: {
-                try? viewContext.save()
-              }
+              "Title", text: $title
             ).font(.title2).textFieldStyle(PlainTextFieldStyle()).padding(.vertical, 4.0).padding(
               .leading, 4.0)
             Button(action: saveText) {
@@ -60,25 +58,34 @@ struct MapDetailView: View {
           EvolutionPicker()
 
           ZStack(alignment: .topLeading) {
-            MapTextEditor(text: content, colorScheme: colorScheme).onChange(of: map.content) { _ in
-              try? viewContext.save()
-            }
-            .background(mapColor.background)
-            .foregroundColor(mapColor.foreground)
-            .frame(minHeight: 96.0)
+            MapTextEditor(text: $content, colorScheme: colorScheme)
+              .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(
-            content: content.wrappedValue, evolution: Stage.stages(store.state.selectedEvolution))
+          SlowMapRender(
+            content: content, evolution: Stage.stages(store.state.selectedEvolution),
+            colorScheme: colorScheme)
         }.background(mapColor.background)
+      }.onReceive(timer.currentTimePublisher) { _ in
+        saveModel()
+      }.onDisappear {
+        saveModel()
       }
     } else {
       DefaultMapView()
     }
   }
 
+  private func saveModel() {
+    map.content = content
+    map.title = title
+    try? viewContext.save()
+  }
+
   private func saveText() {
     store.send(.exportMapAsText(map: map))
   }
@@ -90,7 +97,7 @@ struct MapDetailView: View {
 
 struct MapDetailView_Previews: PreviewProvider {
   static var previews: some View {
-    MapDetailView(map: Map()).environment(
+    MapDetailView(map: Map(), title: "", content: "").environment(
       \.managedObjectContext, PersistenceController.preview.container.viewContext)
   }
 }
index 7623b6179a7504c6c845c191f54cf08abbedce1e..aa97e22ab12a883a247a5a259b8dae64dce5a8c4 100644 (file)
@@ -14,8 +14,10 @@ struct MapRenderView: View {
 
   @Environment(\.colorScheme) var colorScheme
 
-  var content: String
-  let evolution: Stage
+  @Binding var content: String
+  @Binding var evolution: Stage
+
+  @State var parsedMap: ParsedMap = ParsedMap.empty
 
   let mapSize = CGSize(width: 1300.0, height: 1000.0)
 
@@ -23,10 +25,6 @@ struct MapRenderView: View {
   let vertexSize = CGSize(width: 25.0, height: 25.0)
   let padding = CGFloat(30.0)
 
-  var parsedMap: ParsedMap {
-    return Map.parse(content: content)
-  }
-
   var body: some View {
     ZStack(alignment: .topLeading) {
 
@@ -50,13 +48,19 @@ struct MapRenderView: View {
     }.frame(
       width: mapSize.width,
       height: mapSize.height + 2 * padding, alignment: .topLeading
-    ).padding(padding)
+    ).onAppear {
+      self.parsedMap = Map.parse(content: content)
+    }.padding(padding).onChange(of: content) { newState in
+      self.parsedMap = Map.parse(content: newState)
+    }
   }
 }
 
 struct MapRenderView_Previews: PreviewProvider {
   static var previews: some View {
-    MapRenderView(content: "", evolution: Stage.stages(.general)).environment(
+    MapRenderView(
+      content: Binding.constant(""), evolution: Binding.constant(Stage.stages(.general))
+    ).environment(
       \.managedObjectContext, PersistenceController.preview.container.viewContext)
   }
 }
index e030d641e1bd7243c4ee174160859109e0bbacae..4fbff82f673b329f9b22e1a9b2130e0c6a46dbc4 100644 (file)
@@ -30,6 +30,7 @@ class MapTextEditorController: NSViewController {
 
     scrollView.translatesAutoresizingMaskIntoConstraints = false
 
+    textView.allowsUndo = true
     textView.delegate = self
     textView.textStorage?.delegate = self
     textView.string = self.text
diff --git a/Map/Views/SlowMapRender.swift b/Map/Views/SlowMapRender.swift
new file mode 100644 (file)
index 0000000..b7cd842
--- /dev/null
@@ -0,0 +1,90 @@
+import Cocoa
+import SwiftUI
+
+class SlowMapRenderController: NSViewController {
+
+  var content: String
+  private var contentBinding: Binding<String> {
+    Binding(
+      get: { self.content },
+      set: { content in
+        self.content = content
+      }
+    )
+  }
+
+  var evolution: Stage
+  private var evolutionBinding: Binding<Stage> {
+    Binding(
+      get: { self.evolution },
+      set: { evolution in
+        self.evolution = evolution
+      }
+    )
+  }
+  private var colorSchemeEnvironment: ObservableColorSchemeEnvironment
+
+  private let debouncer: Debouncer = Debouncer(seconds: 0.1)
+
+  init(content: String, evolution: Stage, colorScheme: ColorScheme) {
+
+    self.content = content
+    self.evolution = evolution
+    self.colorSchemeEnvironment = ObservableColorSchemeEnvironment(colorScheme: colorScheme)
+    super.init(nibName: nil, bundle: nil)
+  }
+
+  required init?(coder: NSCoder) {
+    fatalError("init(coder:) has not been implemented")
+  }
+
+  override func loadView() {
+
+    let renderView = MapRenderView(content: self.contentBinding, evolution: self.evolutionBinding)
+      .environmentObject(self.colorSchemeEnvironment)
+    let hostingView = NSHostingView(rootView: renderView)
+
+    self.view = hostingView
+  }
+
+  func update(content: String, evolution: Stage, colorScheme: ColorScheme) {
+    self.debouncer.debounce {
+      DispatchQueue.main.async {
+        print("Updating: START")
+        self.content = content
+        self.evolution = evolution
+        self.colorSchemeEnvironment.colorScheme = colorScheme
+        print("Updating: END")
+      }
+    }
+  }
+
+  private class ObservableColorSchemeEnvironment: ObservableObject {
+    @Published var colorScheme: ColorScheme
+
+    init(colorScheme: ColorScheme) {
+      self.colorScheme = colorScheme
+    }
+  }
+}
+
+struct SlowMapRender: NSViewControllerRepresentable {
+
+  var content: String
+  var evolution: Stage
+  let colorScheme: ColorScheme
+
+  func makeNSViewController(
+    context: NSViewControllerRepresentableContext<SlowMapRender>
+  ) -> SlowMapRenderController {
+    return SlowMapRenderController(content: content, evolution: evolution, colorScheme: colorScheme)
+  }
+
+  func updateNSViewController(
+    _ nsViewController: SlowMapRenderController,
+    context: NSViewControllerRepresentableContext<SlowMapRender>
+  ) {
+    nsViewController.update(
+      content: content, evolution: evolution, colorScheme: context.environment.colorScheme)
+  }
+}