From 2dd5c529301244c3f61250dedb2ca5e3f7f44b65 Mon Sep 17 00:00:00 2001 From: Ira Limitanei Date: Fri, 14 Jun 2024 18:00:24 +0900 Subject: [PATCH 01/15] Add new window for binding reactor demo --- Tests/Demo.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/Demo.swift b/Tests/Demo.swift index 212532a..6f9edae 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -55,6 +55,13 @@ struct Demo: App { .closeShortcut() .defaultSize(width: 400, height: 250) .title("Form Demo") + Window(id: "binding-reactor-demo", open: 0) { _ in + BindingReactorDemo.WindowContent() + } + .closeShortcut() + .defaultSize(width: 400, height: 250) + .title("Binding Reactor Demo") + Window(id: "navigation", open: 0) { _ in NavigationViewDemo.WindowContent() } From fa714a2331fd6aa821c1435308b549ee2b7dc6a3 Mon Sep 17 00:00:00 2001 From: Ira Limitanei Date: Fri, 14 Jun 2024 18:02:06 +0900 Subject: [PATCH 02/15] Add binding reactor demo page --- Tests/Page.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/Page.swift b/Tests/Page.swift index 04d5a7f..de296f7 100644 --- a/Tests/Page.swift +++ b/Tests/Page.swift @@ -25,6 +25,7 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible case carousel case viewSwitcher case form + case bindingReactor case popover case flowBox case navigationView @@ -45,6 +46,8 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible return "Navigation View" case .alertDialog: return "Alert Dialog" + case .bindingReactor: + return "Binding Reactor" default: return rawValue.capitalized } @@ -87,6 +90,8 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible return "Switch the window's view" case .form: return "Group controls used for data entry" + case .bindingReactor: + return "React to binding variable changes" case .popover: return "Present content in a bubble-like context popup" case .flowBox: @@ -130,6 +135,8 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible ViewSwitcherDemo(app: app) case .form: FormDemo(app: app) + case .bindingReactor: + BindingReactorDemo(app: app) case .popover: PopoverDemo() case .flowBox: From a4057884dabb379b1c2158722e6b70f1b48f61e5 Mon Sep 17 00:00:00 2001 From: Ira Limitanei Date: Fri, 14 Jun 2024 18:03:09 +0900 Subject: [PATCH 03/15] Add current state of binding reactor demo --- Tests/BindingReactorDemo.swift | 182 +++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 Tests/BindingReactorDemo.swift diff --git a/Tests/BindingReactorDemo.swift b/Tests/BindingReactorDemo.swift new file mode 100644 index 0000000..efca872 --- /dev/null +++ b/Tests/BindingReactorDemo.swift @@ -0,0 +1,182 @@ +// +// BindingReactorDemo.swift +// Adwaita +// +// Created by lambdaclan on 13.06.24. +// + +// swiftlint:disable missing_docs no_magic_numbers + +import Adwaita + +enum PasswordChecker: String, CaseIterable, CustomStringConvertible, Identifiable { + case length + case upper + + var id: String { + self.rawValue + } + + var label: String { + switch self { + case .length: + return "Password Length" + case .upper: + return "Password Uppercase Characters" + } + } + + var description: String { + switch self { + case .length: + return "Password needs to be greater than 8 characters long" + case .upper: + return "Password needs to contain at least one uppercase character" + } + } +} + +struct BindingReactorDemo: View { + + var app: GTUIApp + + var view: Body { + VStack { + Button("View Demo") { + app.showWindow("binding-reactor-demo") + } + .suggested() + .pill() + .frame(maxWidth: 100) + } + } + + struct WindowContent: View { + + @State private var password = "" + @State var checkStatus = PasswordChecker.allCases.enumerated().reduce([String: Bool]()) { dict, checker in + var dict = dict + dict[checker.element.rawValue] = false + return dict + } + + var view: Body { + VStack { + FormSection("Password Checker") { + Form { + EntryRow("Password", text: $password.onSet { _ in + Task { + let results = await checkPassword(content: password) + + Idle { + for result in results { + checkStatus[result.0] = result.1 + } + } + } + }) + } + } + .padding() + ForEach(PasswordChecker.allCases) { checker in + CheckerButton( + isValid: binding(for: checker.rawValue), + checkerName: checker.label, + checkerInfo: checker.description + ) + .padding() + } + } + .padding() + .frame(minWidth: 340, minHeight: 400) + .topToolbar { + HeaderBar.empty() + } + } + + private func binding(for key: String) -> Binding { + .init { + checkStatus[key] ?? false + } set: { newValue in + checkStatus[key] = newValue + } + } + + private func checkPassword(content password: String) async -> [(String, Bool)] { + var results: [(String, Bool)] = [] + + await withTaskGroup(of: (String, Bool).self) { group in + for checker in PasswordChecker.allCases { + group.addTask { + await check(password: password, checker: checker) + } + } + + for await result in group { + results.append(result) + } + } + + return results + } + + private func check(password: String, checker: PasswordChecker) async -> (String, Bool) { + switch checker { + case .length: + return (PasswordChecker.length.rawValue, password.count > 8) + case .upper: + let result = password.range(of: ".*[A-Z]+.*", options: .regularExpression) + return (PasswordChecker.upper.rawValue, result != nil ? true : false) + } + } + } + + private struct CheckerButton: View { + + @Binding var isValid: Bool + @State var isInfoVisible = false + + var checkerName: String + var checkerInfo: String + + var view: Body { + if isValid { + Button("") { + isInfoVisible = true + } + .child { + ButtonContent() + .iconName(Icon.DefaultIcon.emblemOk.string) + .label(checkerName) + .halign(.start) + } + .success() + .alertDialog( + visible: $isInfoVisible, + heading: checkerName, + body: checkerInfo + ) + .response("OK", role: .close) {} + } else { + Button("") { + isInfoVisible = true + } + .child { + ButtonContent() + .iconName(Icon.DefaultIcon.faceAngry.string) + .label(checkerName) + .halign(.start) + } + .destructive() + .alertDialog( + visible: $isInfoVisible, + heading: checkerName, + body: checkerInfo + ) + .response("OK", role: .close) {} + } + } + } +} + +// swiftlint:enable missing_docs no_magic_numbers From a93d230d244f97aa873e15a5ee7dfcf1bae2a56a Mon Sep 17 00:00:00 2001 From: Ira Limitanei Date: Fri, 14 Jun 2024 18:20:11 +0900 Subject: [PATCH 04/15] Add more password validation checks --- Tests/BindingReactorDemo.swift | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Tests/BindingReactorDemo.swift b/Tests/BindingReactorDemo.swift index efca872..497f5ba 100644 --- a/Tests/BindingReactorDemo.swift +++ b/Tests/BindingReactorDemo.swift @@ -12,6 +12,9 @@ import Adwaita enum PasswordChecker: String, CaseIterable, CustomStringConvertible, Identifiable { case length case upper + case lower + case special + case numeric var id: String { self.rawValue @@ -23,6 +26,12 @@ enum PasswordChecker: String, CaseIterable, CustomStringConvertible, Identifiabl return "Password Length" case .upper: return "Password Uppercase Characters" + case .lower: + return "Password Lowercase Characters" + case .special: + return "Password Special Characters" + case .numeric: + return "Password Numeric Characters" } } @@ -32,6 +41,12 @@ enum PasswordChecker: String, CaseIterable, CustomStringConvertible, Identifiabl return "Password needs to be greater than 8 characters long" case .upper: return "Password needs to contain at least one uppercase character" + case .lower: + return "Password needs to contain at least one lowercase character" + case .special: + return "Password needs to contain at least one special character `!&^%$#@()/`" + case .numeric: + return "Password needs to contain at least one numeric character" } } } @@ -127,6 +142,15 @@ struct BindingReactorDemo: View { case .upper: let result = password.range(of: ".*[A-Z]+.*", options: .regularExpression) return (PasswordChecker.upper.rawValue, result != nil ? true : false) + case .lower: + let result = password.range(of: ".*[a-z]+.*", options: .regularExpression) + return (PasswordChecker.lower.rawValue, result != nil ? true : false) + case .special: + let result = password.range(of: ".*[!&^%$#@()/]+.*", options: .regularExpression) + return (PasswordChecker.special.rawValue, result != nil ? true : false) + case .numeric: + let result = password.range(of: ".*[0-9]+.*", options: .regularExpression) + return (PasswordChecker.numeric.rawValue, result != nil ? true : false) } } } From ab831c727dd2cf11464a088e4784628ff1195cb0 Mon Sep 17 00:00:00 2001 From: Ira Limitanei Date: Mon, 17 Jun 2024 10:27:44 +0900 Subject: [PATCH 05/15] Update contributors file --- Contributors.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Contributors.md b/Contributors.md index a8d29d2..a97940e 100644 --- a/Contributors.md +++ b/Contributors.md @@ -5,3 +5,4 @@ - [Zev Eisenberg](https://github.com/ZevEisenberg) - [Jay Wren](https://github.com/jrwren) - [Amzd](https://github.com/amzd) +- [lambdaclan](https://github.com/lambdaclan) From 1fc0647d9084e0d2cb4876d90b1b8effb0e915d1 Mon Sep 17 00:00:00 2001 From: david-swift Date: Mon, 17 Jun 2024 17:23:09 +0200 Subject: [PATCH 06/15] Move from onSet using state to computed property --- Tests/BindingReactorDemo.swift | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/Tests/BindingReactorDemo.swift b/Tests/BindingReactorDemo.swift index 497f5ba..3d71686 100644 --- a/Tests/BindingReactorDemo.swift +++ b/Tests/BindingReactorDemo.swift @@ -69,33 +69,25 @@ struct BindingReactorDemo: View { struct WindowContent: View { @State private var password = "" - @State var checkStatus = PasswordChecker.allCases.enumerated().reduce([String: Bool]()) { dict, checker in - var dict = dict - dict[checker.element.rawValue] = false - return dict + var checkStatus: [String: Bool] { + PasswordChecker.allCases.enumerated().reduce([String: Bool]()) { dict, checker in + var dict = dict + dict[checker.element.rawValue] = check(password: password, checker: checker.element).1 + return dict + } } var view: Body { VStack { FormSection("Password Checker") { Form { - EntryRow("Password", text: $password.onSet { _ in - Task { - let results = await checkPassword(content: password) - - Idle { - for result in results { - checkStatus[result.0] = result.1 - } - } - } - }) + EntryRow("Password", text: $password) } } .padding() ForEach(PasswordChecker.allCases) { checker in CheckerButton( - isValid: binding(for: checker.rawValue), + isValid: .constant(checkStatus[checker.rawValue] ?? false), checkerName: checker.label, checkerInfo: checker.description ) @@ -109,14 +101,6 @@ struct BindingReactorDemo: View { } } - private func binding(for key: String) -> Binding { - .init { - checkStatus[key] ?? false - } set: { newValue in - checkStatus[key] = newValue - } - } - private func checkPassword(content password: String) async -> [(String, Bool)] { var results: [(String, Bool)] = [] @@ -135,7 +119,7 @@ struct BindingReactorDemo: View { return results } - private func check(password: String, checker: PasswordChecker) async -> (String, Bool) { + private func check(password: String, checker: PasswordChecker) -> (String, Bool) { switch checker { case .length: return (PasswordChecker.length.rawValue, password.count > 8) From 69ca6625dd6701c604fe5e586f17bb3d2f1c86dd Mon Sep 17 00:00:00 2001 From: Ira Limitanei Date: Thu, 4 Jul 2024 11:06:25 +0900 Subject: [PATCH 07/15] Remove unnecessary async processing --- Tests/BindingReactorDemo.swift | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Tests/BindingReactorDemo.swift b/Tests/BindingReactorDemo.swift index 3d71686..84e25dc 100644 --- a/Tests/BindingReactorDemo.swift +++ b/Tests/BindingReactorDemo.swift @@ -101,24 +101,6 @@ struct BindingReactorDemo: View { } } - private func checkPassword(content password: String) async -> [(String, Bool)] { - var results: [(String, Bool)] = [] - - await withTaskGroup(of: (String, Bool).self) { group in - for checker in PasswordChecker.allCases { - group.addTask { - await check(password: password, checker: checker) - } - } - - for await result in group { - results.append(result) - } - } - - return results - } - private func check(password: String, checker: PasswordChecker) -> (String, Bool) { switch checker { case .length: From fbd0f2a117e0eb5e766930c146b17f39daa3eef3 Mon Sep 17 00:00:00 2001 From: Ira Limitanei Date: Thu, 4 Jul 2024 11:07:13 +0900 Subject: [PATCH 08/15] Fix formatting --- Tests/BindingReactorDemo.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/BindingReactorDemo.swift b/Tests/BindingReactorDemo.swift index 84e25dc..e9163b2 100644 --- a/Tests/BindingReactorDemo.swift +++ b/Tests/BindingReactorDemo.swift @@ -40,13 +40,13 @@ enum PasswordChecker: String, CaseIterable, CustomStringConvertible, Identifiabl case .length: return "Password needs to be greater than 8 characters long" case .upper: - return "Password needs to contain at least one uppercase character" + return "Password needs to contain at least one uppercase character" case .lower: - return "Password needs to contain at least one lowercase character" + return "Password needs to contain at least one lowercase character" case .special: - return "Password needs to contain at least one special character `!&^%$#@()/`" + return "Password needs to contain at least one special character `!&^%$#@()/`" case .numeric: - return "Password needs to contain at least one numeric character" + return "Password needs to contain at least one numeric character" } } } @@ -135,7 +135,7 @@ struct BindingReactorDemo: View { isInfoVisible = true } .child { - ButtonContent() + ButtonContent() .iconName(Icon.DefaultIcon.emblemOk.string) .label(checkerName) .halign(.start) @@ -152,7 +152,7 @@ struct BindingReactorDemo: View { isInfoVisible = true } .child { - ButtonContent() + ButtonContent() .iconName(Icon.DefaultIcon.faceAngry.string) .label(checkerName) .halign(.start) From e26f1ea2050dfea4dffac59e27bf088cd2a0d9ca Mon Sep 17 00:00:00 2001 From: Ira Limitanei Date: Thu, 4 Jul 2024 11:29:10 +0900 Subject: [PATCH 09/15] Replace Binding Reactor with Password Checker --- Tests/Demo.swift | 2 +- Tests/Page.swift | 10 +++++----- ...dingReactorDemo.swift => PasswordCheckerDemo.swift} | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) rename Tests/{BindingReactorDemo.swift => PasswordCheckerDemo.swift} (98%) diff --git a/Tests/Demo.swift b/Tests/Demo.swift index 6f9edae..07432ee 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -56,7 +56,7 @@ struct Demo: App { .defaultSize(width: 400, height: 250) .title("Form Demo") Window(id: "binding-reactor-demo", open: 0) { _ in - BindingReactorDemo.WindowContent() + PasswordCheckerDemo.WindowContent() } .closeShortcut() .defaultSize(width: 400, height: 250) diff --git a/Tests/Page.swift b/Tests/Page.swift index de296f7..36aac4a 100644 --- a/Tests/Page.swift +++ b/Tests/Page.swift @@ -25,7 +25,7 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible case carousel case viewSwitcher case form - case bindingReactor + case passwordChecker case popover case flowBox case navigationView @@ -46,7 +46,7 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible return "Navigation View" case .alertDialog: return "Alert Dialog" - case .bindingReactor: + case .passwordChecker: return "Binding Reactor" default: return rawValue.capitalized @@ -90,7 +90,7 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible return "Switch the window's view" case .form: return "Group controls used for data entry" - case .bindingReactor: + case .passwordChecker: return "React to binding variable changes" case .popover: return "Present content in a bubble-like context popup" @@ -135,8 +135,8 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible ViewSwitcherDemo(app: app) case .form: FormDemo(app: app) - case .bindingReactor: - BindingReactorDemo(app: app) + case .passwordChecker: + PasswordCheckerDemo(app: app) case .popover: PopoverDemo() case .flowBox: diff --git a/Tests/BindingReactorDemo.swift b/Tests/PasswordCheckerDemo.swift similarity index 98% rename from Tests/BindingReactorDemo.swift rename to Tests/PasswordCheckerDemo.swift index e9163b2..313f206 100644 --- a/Tests/BindingReactorDemo.swift +++ b/Tests/PasswordCheckerDemo.swift @@ -1,5 +1,5 @@ // -// BindingReactorDemo.swift +// PasswordCheckerDemo.swift // Adwaita // // Created by lambdaclan on 13.06.24. @@ -51,7 +51,7 @@ enum PasswordChecker: String, CaseIterable, CustomStringConvertible, Identifiabl } } -struct BindingReactorDemo: View { +struct PasswordCheckerDemo: View { var app: GTUIApp From a84d94cca51211b915919cb48060f781969d93b1 Mon Sep 17 00:00:00 2001 From: Ira Limitanei Date: Thu, 4 Jul 2024 11:36:50 +0900 Subject: [PATCH 10/15] Update window id --- Tests/Demo.swift | 2 +- Tests/PasswordCheckerDemo.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Demo.swift b/Tests/Demo.swift index 07432ee..448d923 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -55,7 +55,7 @@ struct Demo: App { .closeShortcut() .defaultSize(width: 400, height: 250) .title("Form Demo") - Window(id: "binding-reactor-demo", open: 0) { _ in + Window(id: "password-checker-demo", open: 0) { _ in PasswordCheckerDemo.WindowContent() } .closeShortcut() diff --git a/Tests/PasswordCheckerDemo.swift b/Tests/PasswordCheckerDemo.swift index 313f206..9d4a46c 100644 --- a/Tests/PasswordCheckerDemo.swift +++ b/Tests/PasswordCheckerDemo.swift @@ -58,7 +58,7 @@ struct PasswordCheckerDemo: View { var view: Body { VStack { Button("View Demo") { - app.showWindow("binding-reactor-demo") + app.showWindow("password-checker-demo") } .suggested() .pill() From 61f985f0d006b71f4683ff543dc9da3422513940 Mon Sep 17 00:00:00 2001 From: Ira Limitanei Date: Thu, 4 Jul 2024 11:43:59 +0900 Subject: [PATCH 11/15] Update demo title and description --- Tests/Demo.swift | 2 +- Tests/Page.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/Demo.swift b/Tests/Demo.swift index 448d923..4cca82c 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -60,7 +60,7 @@ struct Demo: App { } .closeShortcut() .defaultSize(width: 400, height: 250) - .title("Binding Reactor Demo") + .title("Password Checker Demo") Window(id: "navigation", open: 0) { _ in NavigationViewDemo.WindowContent() diff --git a/Tests/Page.swift b/Tests/Page.swift index 36aac4a..f05b99a 100644 --- a/Tests/Page.swift +++ b/Tests/Page.swift @@ -47,7 +47,7 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible case .alertDialog: return "Alert Dialog" case .passwordChecker: - return "Binding Reactor" + return "Password Checker" default: return rawValue.capitalized } @@ -91,7 +91,7 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible case .form: return "Group controls used for data entry" case .passwordChecker: - return "React to binding variable changes" + return "Check the validity of a password" case .popover: return "Present content in a bubble-like context popup" case .flowBox: From 8011ceeaf313701ba235ac99166fb81bf4d0ce94 Mon Sep 17 00:00:00 2001 From: Ira Limitanei Date: Thu, 4 Jul 2024 11:47:32 +0900 Subject: [PATCH 12/15] Remove form section title --- Tests/PasswordCheckerDemo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/PasswordCheckerDemo.swift b/Tests/PasswordCheckerDemo.swift index 9d4a46c..5190883 100644 --- a/Tests/PasswordCheckerDemo.swift +++ b/Tests/PasswordCheckerDemo.swift @@ -79,7 +79,7 @@ struct PasswordCheckerDemo: View { var view: Body { VStack { - FormSection("Password Checker") { + FormSection("") { Form { EntryRow("Password", text: $password) } From 498610a0efca482f2d3d60ddcb74da84f4bf11de Mon Sep 17 00:00:00 2001 From: Ira Limitanei Date: Thu, 4 Jul 2024 12:00:16 +0900 Subject: [PATCH 13/15] Add copy and clear buttons --- Tests/PasswordCheckerDemo.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Tests/PasswordCheckerDemo.swift b/Tests/PasswordCheckerDemo.swift index 5190883..6a94034 100644 --- a/Tests/PasswordCheckerDemo.swift +++ b/Tests/PasswordCheckerDemo.swift @@ -82,6 +82,18 @@ struct PasswordCheckerDemo: View { FormSection("") { Form { EntryRow("Password", text: $password) + .suffix { + Button(icon: .default(icon: .editCopy)) { + State.copy(password) + } + .flat() + .verticalCenter() + Button(icon: .default(icon: .editClear)) { + password = "" + } + .flat() + .verticalCenter() + } } } .padding() From 8014a6ea4d4f9825cf05f162542f0bf663edffd7 Mon Sep 17 00:00:00 2001 From: Ira Limitanei Date: Thu, 4 Jul 2024 12:04:12 +0900 Subject: [PATCH 14/15] Update demo top comment --- Tests/PasswordCheckerDemo.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/PasswordCheckerDemo.swift b/Tests/PasswordCheckerDemo.swift index 6a94034..33fdaad 100644 --- a/Tests/PasswordCheckerDemo.swift +++ b/Tests/PasswordCheckerDemo.swift @@ -4,6 +4,7 @@ // // Created by lambdaclan on 13.06.24. // +// Adjusted by david-swift on 18.06.24. // swiftlint:disable missing_docs no_magic_numbers From c17730e89de68c196e13d516f78841ccdc92bf52 Mon Sep 17 00:00:00 2001 From: david-swift Date: Thu, 4 Jul 2024 21:45:13 +0200 Subject: [PATCH 15/15] Polish Password Checker Demo --- Tests/PasswordCheckerDemo.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Tests/PasswordCheckerDemo.swift b/Tests/PasswordCheckerDemo.swift index 33fdaad..2b2a1a5 100644 --- a/Tests/PasswordCheckerDemo.swift +++ b/Tests/PasswordCheckerDemo.swift @@ -70,6 +70,8 @@ struct PasswordCheckerDemo: View { struct WindowContent: View { @State private var password = "" + @State private var copied: Signal = .init() + var checkStatus: [String: Bool] { PasswordChecker.allCases.enumerated().reduce([String: Bool]()) { dict, checker in var dict = dict @@ -86,14 +88,17 @@ struct PasswordCheckerDemo: View { .suffix { Button(icon: .default(icon: .editCopy)) { State.copy(password) + copied.signal() } .flat() .verticalCenter() + .tooltip("Copy") Button(icon: .default(icon: .editClear)) { password = "" } .flat() .verticalCenter() + .tooltip("Clear") } } } @@ -108,6 +113,7 @@ struct PasswordCheckerDemo: View { } } .padding() + .toast("Copied to clipboard", signal: copied) .frame(minWidth: 340, minHeight: 400) .topToolbar { HeaderBar.empty()