From: Ruben Beltran del Rio Date: Fri, 5 Feb 2021 23:55:04 +0000 (+0100) Subject: Fix performance and undo X-Git-Tag: 1.2.0 X-Git-Url: https://git.r.bdr.sh/rbdr/map/commitdiff_plain/75a0e4509a70055851b085f3f7293ae1cf48164c?ds=inline;hp=91fd86189477e6c690c6487d51d80bc58c8ecb63 Fix performance and undo --- diff --git a/Map.xcodeproj/project.pbxproj b/Map.xcodeproj/project.pbxproj index 12a5d1c..b212de6 100644 --- a/Map.xcodeproj/project.pbxproj +++ b/Map.xcodeproj/project.pbxproj @@ -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 = ""; }; B539517325CB0CA400959F72 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; B539518025CB2D7A00959F72 /* MapTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTextEditor.swift; sourceTree = ""; }; + B587BB5F25CDCECB00F328ED /* SlowMapRender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlowMapRender.swift; sourceTree = ""; }; B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvolutionPicker.swift; sourceTree = ""; }; B5CF75CE25CC7965003BFF3D /* MapParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapParser.swift; sourceTree = ""; }; B5CF75D725CC79BC003BFF3D /* VertexParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VertexParserStrategy.swift; sourceTree = ""; }; @@ -246,6 +248,7 @@ B52625A525C876C3003E73B7 /* MapDetail.swift */, B523C74A25C9C1BA00C44061 /* DefaultMapView.swift */, B539518025CB2D7A00959F72 /* MapTextEditor.swift */, + B587BB5F25CDCECB00F328ED /* SlowMapRender.swift */, B52625AF25C87C14003E73B7 /* MapRender.swift */, B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */, ); @@ -429,6 +432,7 @@ 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 */, ); @@ -589,6 +593,7 @@ 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; @@ -599,7 +604,7 @@ "@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; @@ -614,6 +619,7 @@ 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; @@ -624,7 +630,7 @@ "@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; diff --git a/Map/Debouncer.swift b/Map/Debouncer.swift index f119d8a..cd7960a 100644 --- a/Map/Debouncer.swift +++ b/Map/Debouncer.swift @@ -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 diff --git a/Map/Info.plist b/Map/Info.plist index 903c15d..6ba8cf0 100644 --- a/Map/Info.plist +++ b/Map/Info.plist @@ -17,7 +17,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 1 + $(CURRENT_PROJECT_VERSION) LSApplicationCategoryType public.app-category.productivity LSMinimumSystemVersion diff --git a/Map/MapParser/MapParser.swift b/Map/MapParser/MapParser.swift index 3528eb5..cce6251 100644 --- a/Map/MapParser/MapParser.swift +++ b/Map/MapParser/MapParser.swift @@ -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 { diff --git a/Map/State/AppState.swift b/Map/State/AppState.swift index f062051..c261725 100644 --- a/Map/State/AppState.swift +++ b/Map/State/AppState.swift @@ -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) diff --git a/Map/Views/ContentView.swift b/Map/Views/ContentView.swift index f439026..534c14c 100644 --- a/Map/Views/ContentView.swift +++ b/Map/Views/ContentView.swift @@ -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() } } diff --git a/Map/Views/MapDetail.swift b/Map/Views/MapDetail.swift index 6ba92bb..bebe3a1 100644 --- a/Map/Views/MapDetail.swift +++ b/Map/Views/MapDetail.swift @@ -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 { - Binding( - get: { map.title ?? "" }, - set: { title in - map.title = title - } - ) - } - - private var content: Binding { - 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) } } diff --git a/Map/Views/MapRender.swift b/Map/Views/MapRender.swift index 7623b61..aa97e22 100644 --- a/Map/Views/MapRender.swift +++ b/Map/Views/MapRender.swift @@ -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) } } diff --git a/Map/Views/MapTextEditor.swift b/Map/Views/MapTextEditor.swift index e030d64..4fbff82 100644 --- a/Map/Views/MapTextEditor.swift +++ b/Map/Views/MapTextEditor.swift @@ -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 index 0000000..b7cd842 --- /dev/null +++ b/Map/Views/SlowMapRender.swift @@ -0,0 +1,90 @@ +import Cocoa +import SwiftUI + +class SlowMapRenderController: NSViewController { + + var content: String + private var contentBinding: Binding { + Binding( + get: { self.content }, + set: { content in + self.content = content + } + ) + } + + var evolution: Stage + private var evolutionBinding: Binding { + 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 + ) -> SlowMapRenderController { + return SlowMapRenderController(content: content, evolution: evolution, colorScheme: colorScheme) + } + + func updateNSViewController( + _ nsViewController: SlowMapRenderController, + context: NSViewControllerRepresentableContext + ) { + nsViewController.update( + content: content, evolution: evolution, colorScheme: context.environment.colorScheme) + } +}