From 09971c1ad28d79c527e874cd66c6c328b1612ee2 Mon Sep 17 00:00:00 2001 From: Christoffer Winterkvist Date: Thu, 22 Jun 2023 22:57:12 +0200 Subject: [PATCH 1/5] Various performance optimizations - Remove linear gradient commands, it comes with a hefty performance penalty to render a gradient layer with blended colors - Remove the shadow from the command views - Add debouncing when editing application names - Add new `AppCheckbox` & `AppToggle` - Replace all use of `Toggle` with wither `AppCheckbox` or `AppToggle` - Fix warnings about using `var` instead of `let` - Refactor performance data generator in `ContentStore` --- App/Sources/Core/Stores/ContentStore.swift | 26 ++-- App/Sources/UI/Views/CommandView.swift | 12 +- .../Commands/ApplicationCommandView.swift | 30 ++--- .../Views/Commands/CommandContainerView.swift | 21 ++- .../UI/Views/Components/AppCheckbox.swift | 105 +++++++++++++++ .../UI/Views/Components/AppToggle.swift | 126 ++++++++++++++++++ .../UI/Views/KeyboardTriggerView.swift | 9 +- .../NewCommandApplicationView.swift | 15 ++- .../WorkflowApplicationTriggerItemView.swift | 10 +- App/Sources/UI/Views/WorkflowInfoView.swift | 8 +- .../UI/Views/WorkflowTriggerListView.swift | 2 +- Project.swift | 2 +- 12 files changed, 295 insertions(+), 71 deletions(-) create mode 100644 App/Sources/UI/Views/Components/AppCheckbox.swift create mode 100644 App/Sources/UI/Views/Components/AppToggle.swift diff --git a/App/Sources/Core/Stores/ContentStore.swift b/App/Sources/Core/Stores/ContentStore.swift index fb3aa3fb..4d3b04fc 100644 --- a/App/Sources/Core/Stores/ContentStore.swift +++ b/App/Sources/Core/Stores/ContentStore.swift @@ -131,18 +131,26 @@ final class ContentStore: ObservableObject { } private func generatePerformanceData() { - for kind in Command.CodingKeys.allCases { - var group = WorkflowGroup(name: "Group:\(kind.rawValue)") - for x in 0..<100 { - var workflow = Workflow(name: "Workflow:\(kind.rawValue):\(x + 1)") - for y in 0..<100 { - var command = Command.empty(kind) - command.name = "Command:\(kind.rawValue):\(y+1)" + var updatedGroups = Set() + updatedGroups.reserveCapacity(groupStore.groups.count) + + for group in groupStore.groups { + var copy = group + let appsCount = applicationStore.applications.count + (0..<5).forEach { x in + var workflow = Workflow(name: "Performance workflow \(x)") + (0..<50).forEach { y in + let randomApp = applicationStore.applications[Int.random(in: 0.. + @EnvironmentObject var applicationStore: ApplicationStore private let onAction: (Action) -> Void @@ -31,6 +33,9 @@ struct ApplicationCommandView: View { _metaData = .init(initialValue: metaData) _model = .init(initialValue: model) self.onAction = onAction + self.debounce = DebounceManager(for: .milliseconds(500)) { newName in + onAction(.updateName(newName: newName)) + } } var body: some View { @@ -66,24 +71,17 @@ struct ApplicationCommandView: View { TextField("", text: $metaData.name) .textFieldStyle(AppTextFieldStyle()) - .onChange(of: metaData.name, perform: { - onAction(.updateName(newName: $0)) - }) + .onChange(of: metaData.name, perform: { debounce.send($0) }) } }, subContent: { _ in - HStack { - Toggle("In background", isOn: $model.inBackground) - .onChange(of: model.inBackground) { newValue in - onAction(.changeApplicationModifier(modifier: .background, newValue: newValue)) - } - Toggle("Hide when opening", isOn: $model.hideWhenRunning) - .onChange(of: model.hideWhenRunning) { newValue in - onAction(.changeApplicationModifier(modifier: .hidden, newValue: newValue)) - } - Toggle("If not running", isOn: $model.ifNotRunning) - .onChange(of: model.ifNotRunning) { newValue in - onAction(.changeApplicationModifier(modifier: .onlyIfNotRunning, newValue: newValue)) - } + AppCheckbox("In background", style: .small, isOn: $model.inBackground) { newValue in + onAction(.changeApplicationModifier(modifier: .background, newValue: newValue)) + } + AppCheckbox("Hide when opening", style: .small, isOn: $model.hideWhenRunning) { newValue in + onAction(.changeApplicationModifier(modifier: .hidden, newValue: newValue)) + } + AppCheckbox("If not running", style: .small, isOn: $model.ifNotRunning) { newValue in + onAction(.changeApplicationModifier(modifier: .onlyIfNotRunning, newValue: newValue)) } }, onAction: { onAction(.commandAction($0)) }) diff --git a/App/Sources/UI/Views/Commands/CommandContainerView.swift b/App/Sources/UI/Views/Commands/CommandContainerView.swift index 9361620d..b0fc6b11 100644 --- a/App/Sources/UI/Views/Commands/CommandContainerView.swift +++ b/App/Sources/UI/Views/Commands/CommandContainerView.swift @@ -46,22 +46,19 @@ struct CommandContainerView: View where IconCo .frame(minHeight: 30) } .padding([.top, .leading], 8) + .padding(.bottom, 4) HStack(spacing: 0) { - Toggle(isOn: $metaData.isEnabled) { } - .onChange(of: metaData.isEnabled, perform: { - onAction(.toggleIsEnabled($0)) - }) - .toggleStyle(.switch) - .tint(.green) - .compositingGroup() - .scaleEffect(0.65) + AppToggle("", onColor: Color(.systemGreen), style: .small, isOn: $metaData.isEnabled) { + onAction(.toggleIsEnabled($0)) + } + .padding(.leading, 5) + .padding(.trailing, 5) HStack { - Toggle("Notify", isOn: $metaData.notification) - .onChange(of: metaData.notification) { newValue in - onAction(.toggleNotify(newValue)) - } + AppCheckbox("Notify", style: .small, isOn: $metaData.notification) { + onAction(.toggleNotify($0)) + } if detailPublisher.data.execution == .serial { Button { diff --git a/App/Sources/UI/Views/Components/AppCheckbox.swift b/App/Sources/UI/Views/Components/AppCheckbox.swift new file mode 100644 index 00000000..ebe559d5 --- /dev/null +++ b/App/Sources/UI/Views/Components/AppCheckbox.swift @@ -0,0 +1,105 @@ +import SwiftUI + +struct AppCheckbox: View { + enum Style { + case regular + case small + + var size: CGSize { + switch self { + case .regular: + CGSize(width: 16, height: 16) + case .small: + CGSize(width: 12, height: 12) + } + } + + var fontSize: CGFloat { + switch self { + case .regular: + return 10 + case .small: + return 8 + } + } + } + + @Binding private var isOn: Bool + private let style: Style + private let titleKey: String + private let onChange: (Bool) -> Void + + init(_ titleKey: String, + style: Style = .regular, + isOn: Binding, + onChange: @escaping (Bool) -> Void = { _ in }) { + _isOn = isOn + self.style = style + self.titleKey = titleKey + self.onChange = onChange + } + + var body: some View { + HStack(spacing: 4) { + Button(action: { + isOn.toggle() + onChange(isOn) + }, label: { + RoundedRectangle(cornerRadius: 4, style: .continuous) + .fill(Color(nsColor: .controlColor)) + .overlay(content: { + Image(systemName: "checkmark") + .font(Font.system(size: style.fontSize, weight: .heavy)) + .opacity(isOn ? 1 : 0) + }) + .frame(width: style.size.width, height: style.size.height) + + }) + .buttonStyle(.plain) + Text(titleKey) + } + } +} + +struct AppCheckbox_Previews: PreviewProvider { + static var systemToggles: some View { + VStack { + Toggle(isOn: .constant(true), label: { + Text("Default on") + }) + Toggle(isOn: .constant(false), label: { + Text("Default off") + }) + } + } + + static var previews: some View { + VStack(alignment: .leading) { + HStack(alignment: .top, spacing: 32) { + VStack(alignment: .leading) { + Text("System") + .font(.headline) + systemToggles + .toggleStyle(.switch) + } + + VStack(alignment: .leading) { + Text("Regular") + .font(.headline) + AppToggle("Default on", isOn: .constant(true)) { _ in } + AppToggle("Default off", isOn: .constant(false)) { _ in } + } + + VStack(alignment: .leading) { + Text("Small") + .font(.headline) + AppToggle("Default on", style: .small, isOn: .constant(true)) { _ in } + .font(.caption) + AppToggle("Default off", style: .small, isOn: .constant(false)) { _ in } + .font(.caption) + } + } + } + .padding() + } +} diff --git a/App/Sources/UI/Views/Components/AppToggle.swift b/App/Sources/UI/Views/Components/AppToggle.swift new file mode 100644 index 00000000..bd2895ec --- /dev/null +++ b/App/Sources/UI/Views/Components/AppToggle.swift @@ -0,0 +1,126 @@ +import SwiftUI + +struct AppToggle: View { + enum Style { + case regular + case small + + var size: CGSize { + switch self { + case .regular: + CGSize(width: 38, height: 20) + case .small: + CGSize(width: 22, height: 12) + } + } + + var circle: CGSize { + switch self { + case .regular: + CGSize(width: 19, height: 19) + case .small: + CGSize(width: 11, height: 11) + } + } + } + + @Environment(\.controlActiveState) var controlActiveState + @Binding private var isOn: Bool + private let onColor: Color + private let style: Style + private let titleKey: String + private let onChange: (Bool) -> Void + + init(_ titleKey: String, + onColor: Color = Color(nsColor: .controlColor), + style: Style = .regular, + isOn: Binding, + onChange: @escaping (Bool) -> Void = { _ in }) { + _isOn = isOn + self.onColor = onColor + self.style = style + self.titleKey = titleKey + self.onChange = onChange + } + + var body: some View { + HStack(spacing: 4) { + Text(titleKey) + Button(action: { + isOn.toggle() + onChange(isOn) + }, label: { + Capsule() + .fill( + controlActiveState == .key + ? isOn ? onColor : Color(nsColor: .controlColor) + : Color(nsColor: .controlColor) + ) + .overlay(alignment: isOn ? .trailing : .leading, content: { + Circle() + .frame(width: style.circle.width, height: style.circle.height) + .overlay( + Circle() + .stroke(Color(nsColor: .gray), lineWidth: 0.5) + ) + }) + .animation(.default, value: isOn) + .background( + Capsule() + .strokeBorder(Color(nsColor: .windowBackgroundColor), lineWidth: 1) + ) + .frame(width: style.size.width, height: style.size.height) + }) + .buttonStyle(.plain) + } + } +} + + + +struct AppToggle_Previews: PreviewProvider { + static var systemToggles: some View { + VStack { + Toggle(isOn: .constant(true), label: { + Text("Default on") + }) + .tint(Color(.systemGreen)) + Toggle(isOn: .constant(false), label: { + Text("Default off") + }) + } + } + + static var previews: some View { + VStack(alignment: .leading) { + HStack(alignment: .top, spacing: 32) { + VStack(alignment: .leading) { + Text("System") + .font(.headline) + systemToggles + .toggleStyle(.switch) + } + + VStack(alignment: .leading) { + Text("Regular") + .font(.headline) + AppToggle("Default on", + onColor: Color(.systemGreen), + style: .small, + isOn: .constant(true)) { _ in } + AppToggle("Default off", + style: .small, + isOn: .constant(false)) { _ in } + } + + VStack(alignment: .leading) { + Text("Small") + .font(.headline) + AppToggle("Default on", style: .small, isOn: .constant(true)) { _ in } + AppToggle("Default off", style: .small, isOn: .constant(false)) { _ in } + } + } + } + .padding() + } +} diff --git a/App/Sources/UI/Views/KeyboardTriggerView.swift b/App/Sources/UI/Views/KeyboardTriggerView.swift index 45fb8005..16606443 100644 --- a/App/Sources/UI/Views/KeyboardTriggerView.swift +++ b/App/Sources/UI/Views/KeyboardTriggerView.swift @@ -35,11 +35,10 @@ struct KeyboardTriggerView: View { Label("Keyboard Shortcuts sequence:", image: "") .padding(.trailing, 12) Spacer() - Toggle("Passthrough", isOn: $passthrough) - .font(.caption) - .onChange(of: passthrough) { newValue in - onAction(.togglePassthrough(workflowId: data.id, newValue: newValue)) - } + AppToggle("Passthrough", isOn: $passthrough) { newValue in + onAction(.togglePassthrough(workflowId: data.id, newValue: newValue)) + } + .font(.caption) } .padding([.leading, .trailing], 8) diff --git a/App/Sources/UI/Views/NewCommand/NewCommandApplicationView.swift b/App/Sources/UI/Views/NewCommand/NewCommandApplicationView.swift index e3417c92..04bf39b6 100644 --- a/App/Sources/UI/Views/NewCommand/NewCommandApplicationView.swift +++ b/App/Sources/UI/Views/NewCommand/NewCommandApplicationView.swift @@ -90,13 +90,16 @@ struct NewCommandApplicationView: View { Divider() HStack { - Toggle("In background", isOn: $inBackground) - Toggle("Hide when opening", isOn: $hideWhenRunning) - Toggle("If not running", isOn: $ifNotRunning) + AppCheckbox("In background", isOn: $inBackground) { _ in + updateAndValidatePayload() + } + AppCheckbox("Hide when opening", isOn: $hideWhenRunning) { _ in + updateAndValidatePayload() + } + AppCheckbox("If not running", isOn: $ifNotRunning) { _ in + updateAndValidatePayload() + } } - .onChange(of: inBackground, perform: { _ in updateAndValidatePayload() }) - .onChange(of: hideWhenRunning, perform: { _ in updateAndValidatePayload() }) - .onChange(of: ifNotRunning, perform: { _ in updateAndValidatePayload() }) .onChange(of: validation) { newValue in guard newValue == .needsValidation else { return } validation = updateAndValidatePayload() diff --git a/App/Sources/UI/Views/WorkflowApplicationTriggerItemView.swift b/App/Sources/UI/Views/WorkflowApplicationTriggerItemView.swift index baae992d..28d0c181 100644 --- a/App/Sources/UI/Views/WorkflowApplicationTriggerItemView.swift +++ b/App/Sources/UI/Views/WorkflowApplicationTriggerItemView.swift @@ -27,7 +27,7 @@ struct WorkflowApplicationTriggerItemView: View { Text(element.name) HStack { ForEach(DetailViewModel.ApplicationTrigger.Context.allCases) { context in - Toggle(context.displayValue, isOn: Binding(get: { + AppCheckbox(context.displayValue, style: .small, isOn: Binding(get: { element.contexts.contains(context) }, set: { newValue in if newValue { @@ -37,8 +37,12 @@ struct WorkflowApplicationTriggerItemView: View { } onAction(.updateApplicationTriggerContext(element)) - })) - .font(.caption) + })) { _ in } + .lineLimit(1) + .allowsTightening(true) + .truncationMode(.tail) + .font(.caption) + } } } diff --git a/App/Sources/UI/Views/WorkflowInfoView.swift b/App/Sources/UI/Views/WorkflowInfoView.swift index abeb4d73..597e6021 100644 --- a/App/Sources/UI/Views/WorkflowInfoView.swift +++ b/App/Sources/UI/Views/WorkflowInfoView.swift @@ -45,13 +45,7 @@ struct WorkflowInfoView: View { .onChange(of: workflowName) { debounce.send($0) } Spacer() - Toggle("", isOn: $isEnabled) - .toggleStyle(SwitchToggleStyle()) - .tint(Color.green) - .font(.callout) - .onChange(of: isEnabled) { newValue in - onAction(.setIsEnabled(isEnabled: newValue)) - } + AppToggle("", onColor: Color(.systemGreen), isOn: $isEnabled) { onAction(.setIsEnabled(isEnabled: $0)) } } .debugEdit() } diff --git a/App/Sources/UI/Views/WorkflowTriggerListView.swift b/App/Sources/UI/Views/WorkflowTriggerListView.swift index 4a89e603..7d0422c2 100644 --- a/App/Sources/UI/Views/WorkflowTriggerListView.swift +++ b/App/Sources/UI/Views/WorkflowTriggerListView.swift @@ -27,7 +27,7 @@ struct WorkflowTriggerListView: View { var body: some View { Group { switch data.trigger { - case .keyboardShortcuts(var trigger): + case .keyboardShortcuts(let trigger): KeyboardTriggerView(namespace: namespace, focus: focus, data: data, trigger: trigger, keyboardShortcutSelectionManager: keyboardShortcutSelectionManager, onAction: onAction) diff --git a/Project.swift b/Project.swift index 835cce7c..2d01d104 100644 --- a/Project.swift +++ b/Project.swift @@ -53,7 +53,7 @@ let project = Project( "ASSETCATALOG_COMPILER_APPICON_NAME": "AppIcon", "CODE_SIGN_IDENTITY": "Apple Development", "CODE_SIGN_STYLE": "Automatic", - "CURRENT_PROJECT_VERSION": "170", + "CURRENT_PROJECT_VERSION": "175", "DEVELOPMENT_TEAM": env["TEAM_ID"], "ENABLE_HARDENED_RUNTIME": true, "MARKETING_VERSION": "3.7.0", From e2593904413feb5dd5018ae1f51e9ce3454ee67e Mon Sep 17 00:00:00 2001 From: Christoffer Winterkvist Date: Thu, 22 Jun 2023 23:07:27 +0200 Subject: [PATCH 2/5] Fix the AppCheckbox preview --- App/Sources/UI/Views/Components/AppCheckbox.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/App/Sources/UI/Views/Components/AppCheckbox.swift b/App/Sources/UI/Views/Components/AppCheckbox.swift index ebe559d5..11e04d7f 100644 --- a/App/Sources/UI/Views/Components/AppCheckbox.swift +++ b/App/Sources/UI/Views/Components/AppCheckbox.swift @@ -80,22 +80,21 @@ struct AppCheckbox_Previews: PreviewProvider { Text("System") .font(.headline) systemToggles - .toggleStyle(.switch) } VStack(alignment: .leading) { Text("Regular") .font(.headline) - AppToggle("Default on", isOn: .constant(true)) { _ in } - AppToggle("Default off", isOn: .constant(false)) { _ in } + AppCheckbox("Default on", isOn: .constant(true)) { _ in } + AppCheckbox("Default off", isOn: .constant(false)) { _ in } } VStack(alignment: .leading) { Text("Small") .font(.headline) - AppToggle("Default on", style: .small, isOn: .constant(true)) { _ in } + AppCheckbox("Default on", style: .small, isOn: .constant(true)) { _ in } .font(.caption) - AppToggle("Default off", style: .small, isOn: .constant(false)) { _ in } + AppCheckbox("Default off", style: .small, isOn: .constant(false)) { _ in } .font(.caption) } } From 9c4aa0e44a8ae0b9a5251c4b851c197de9e3324e Mon Sep 17 00:00:00 2001 From: Christoffer Winterkvist Date: Thu, 22 Jun 2023 23:11:36 +0200 Subject: [PATCH 3/5] Check passthrough to use a checkbox instead of a toggle --- App/Sources/UI/Views/KeyboardTriggerView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/Sources/UI/Views/KeyboardTriggerView.swift b/App/Sources/UI/Views/KeyboardTriggerView.swift index 16606443..eeff0874 100644 --- a/App/Sources/UI/Views/KeyboardTriggerView.swift +++ b/App/Sources/UI/Views/KeyboardTriggerView.swift @@ -35,7 +35,7 @@ struct KeyboardTriggerView: View { Label("Keyboard Shortcuts sequence:", image: "") .padding(.trailing, 12) Spacer() - AppToggle("Passthrough", isOn: $passthrough) { newValue in + AppCheckbox("Passthrough", isOn: $passthrough) { newValue in onAction(.togglePassthrough(workflowId: data.id, newValue: newValue)) } .font(.caption) From d3477f92f1ef664dd0b82bf3a495251f04118360 Mon Sep 17 00:00:00 2001 From: Christoffer Winterkvist Date: Thu, 22 Jun 2023 23:13:02 +0200 Subject: [PATCH 4/5] Add missing return to switch --- App/Sources/UI/Views/Components/AppCheckbox.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/App/Sources/UI/Views/Components/AppCheckbox.swift b/App/Sources/UI/Views/Components/AppCheckbox.swift index 11e04d7f..ed8fff31 100644 --- a/App/Sources/UI/Views/Components/AppCheckbox.swift +++ b/App/Sources/UI/Views/Components/AppCheckbox.swift @@ -8,9 +8,9 @@ struct AppCheckbox: View { var size: CGSize { switch self { case .regular: - CGSize(width: 16, height: 16) + return CGSize(width: 16, height: 16) case .small: - CGSize(width: 12, height: 12) + return CGSize(width: 12, height: 12) } } @@ -85,16 +85,16 @@ struct AppCheckbox_Previews: PreviewProvider { VStack(alignment: .leading) { Text("Regular") .font(.headline) - AppCheckbox("Default on", isOn: .constant(true)) { _ in } - AppCheckbox("Default off", isOn: .constant(false)) { _ in } + AppCheckbox("Default on", isOn: .constant(true)) + AppCheckbox("Default off", isOn: .constant(false)) } VStack(alignment: .leading) { Text("Small") .font(.headline) - AppCheckbox("Default on", style: .small, isOn: .constant(true)) { _ in } + AppCheckbox("Default on", style: .small, isOn: .constant(true)) .font(.caption) - AppCheckbox("Default off", style: .small, isOn: .constant(false)) { _ in } + AppCheckbox("Default off", style: .small, isOn: .constant(false)) .font(.caption) } } From 75434a711934b53a26e31d00d4c43d37bed3920c Mon Sep 17 00:00:00 2001 From: Christoffer Winterkvist Date: Thu, 22 Jun 2023 23:24:12 +0200 Subject: [PATCH 5/5] Add missing returns --- App/Sources/UI/Views/Components/AppToggle.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/App/Sources/UI/Views/Components/AppToggle.swift b/App/Sources/UI/Views/Components/AppToggle.swift index bd2895ec..c3c90577 100644 --- a/App/Sources/UI/Views/Components/AppToggle.swift +++ b/App/Sources/UI/Views/Components/AppToggle.swift @@ -8,18 +8,18 @@ struct AppToggle: View { var size: CGSize { switch self { case .regular: - CGSize(width: 38, height: 20) + return CGSize(width: 38, height: 20) case .small: - CGSize(width: 22, height: 12) + return CGSize(width: 22, height: 12) } } var circle: CGSize { switch self { case .regular: - CGSize(width: 19, height: 19) + return CGSize(width: 19, height: 19) case .small: - CGSize(width: 11, height: 11) + return CGSize(width: 11, height: 11) } } }