How can I detect keyboard events in a SwiftUI view on macOS?
I want to be able to use key strokes to control items on a particular screen but it's not clear how I detect keyboard events, which is usually done by overriding the keyDown(_ event: NSEvent) in NSView.
New in SwiftUI bundled with Xcode 12 is the commands modifier, which allows us to declare key input with keyboardShortcut view modifier. You then need some way of forwarding the key inputs to your child views. Below is a solution using a Subject, but since it is not a reference type it cannot be passed using environmentObject - which is really what we wanna do, so I've made a small wrapper, conforming to ObservableObject and for conveninece Subject itself (forwarding via the subject).
Using some additional convenience sugar methods, I can just write like this:
.commands {
CommandMenu("Input") {
keyInput(.leftArrow)
keyInput(.rightArrow)
keyInput(.upArrow)
keyInput(.downArrow)
keyInput(.space)
}
}
And forward key inputs to all subviews like this:
.environmentObject(keyInputSubject)
And then a child view, here GameView can listen to the events with onReceive, like so:
struct GameView: View {
#EnvironmentObject private var keyInputSubjectWrapper: KeyInputSubjectWrapper
#StateObject var game: Game
var body: some View {
HStack {
board
info
}.onReceive(keyInputSubjectWrapper) {
game.keyInput($0)
}
}
}
The keyInput method used to declare the keys inside CommandMenu builder is just this:
private extension ItsRainingPolygonsApp {
func keyInput(_ key: KeyEquivalent, modifiers: EventModifiers = .none) -> some View {
keyboardShortcut(key, sender: keyInputSubject, modifiers: modifiers)
}
}
Full Code
extension KeyEquivalent: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.character == rhs.character
}
}
public typealias KeyInputSubject = PassthroughSubject<KeyEquivalent, Never>
public final class KeyInputSubjectWrapper: ObservableObject, Subject {
public func send(_ value: Output) {
objectWillChange.send(value)
}
public func send(completion: Subscribers.Completion<Failure>) {
objectWillChange.send(completion: completion)
}
public func send(subscription: Subscription) {
objectWillChange.send(subscription: subscription)
}
public typealias ObjectWillChangePublisher = KeyInputSubject
public let objectWillChange: ObjectWillChangePublisher
public init(subject: ObjectWillChangePublisher = .init()) {
objectWillChange = subject
}
}
// MARK: Publisher Conformance
public extension KeyInputSubjectWrapper {
typealias Output = KeyInputSubject.Output
typealias Failure = KeyInputSubject.Failure
func receive<S>(subscriber: S) where S : Subscriber, S.Failure == Failure, S.Input == Output {
objectWillChange.receive(subscriber: subscriber)
}
}
#main
struct ItsRainingPolygonsApp: App {
private let keyInputSubject = KeyInputSubjectWrapper()
var body: some Scene {
WindowGroup {
#if os(macOS)
ContentView()
.frame(idealWidth: .infinity, idealHeight: .infinity)
.onReceive(keyInputSubject) {
print("Key pressed: \($0)")
}
.environmentObject(keyInputSubject)
#else
ContentView()
#endif
}
.commands {
CommandMenu("Input") {
keyInput(.leftArrow)
keyInput(.rightArrow)
keyInput(.upArrow)
keyInput(.downArrow)
keyInput(.space)
}
}
}
}
private extension ItsRainingPolygonsApp {
func keyInput(_ key: KeyEquivalent, modifiers: EventModifiers = .none) -> some View {
keyboardShortcut(key, sender: keyInputSubject, modifiers: modifiers)
}
}
public func keyboardShortcut<Sender, Label>(
_ key: KeyEquivalent,
sender: Sender,
modifiers: EventModifiers = .none,
#ViewBuilder label: () -> Label
) -> some View where Label: View, Sender: Subject, Sender.Output == KeyEquivalent {
Button(action: { sender.send(key) }, label: label)
.keyboardShortcut(key, modifiers: modifiers)
}
public func keyboardShortcut<Sender>(
_ key: KeyEquivalent,
sender: Sender,
modifiers: EventModifiers = .none
) -> some View where Sender: Subject, Sender.Output == KeyEquivalent {
guard let nameFromKey = key.name else {
return AnyView(EmptyView())
}
return AnyView(keyboardShortcut(key, sender: sender, modifiers: modifiers) {
Text("\(nameFromKey)")
})
}
extension KeyEquivalent {
var lowerCaseName: String? {
switch self {
case .space: return "space"
case .clear: return "clear"
case .delete: return "delete"
case .deleteForward: return "delete forward"
case .downArrow: return "down arrow"
case .end: return "end"
case .escape: return "escape"
case .home: return "home"
case .leftArrow: return "left arrow"
case .pageDown: return "page down"
case .pageUp: return "page up"
case .return: return "return"
case .rightArrow: return "right arrow"
case .space: return "space"
case .tab: return "tab"
case .upArrow: return "up arrow"
default: return nil
}
}
var name: String? {
lowerCaseName?.capitalizingFirstLetter()
}
}
public extension EventModifiers {
static let none = Self()
}
extension String {
func capitalizingFirstLetter() -> String {
return prefix(1).uppercased() + self.lowercased().dropFirst()
}
mutating func capitalizeFirstLetter() {
self = self.capitalizingFirstLetter()
}
}
extension KeyEquivalent: CustomStringConvertible {
public var description: String {
name ?? "\(character)"
}
}
There is no built-in native SwiftUI API for this, so far.
Here is just a demo of a possible approach. Tested with Xcode 11.4 / macOS 10.15.4
struct KeyEventHandling: NSViewRepresentable {
class KeyView: NSView {
override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
print(">> key \(event.charactersIgnoringModifiers ?? "")")
}
}
func makeNSView(context: Context) -> NSView {
let view = KeyView()
DispatchQueue.main.async { // wait till next event cycle
view.window?.makeFirstResponder(view)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
}
}
struct TestKeyboardEventHandling: View {
var body: some View {
Text("Hello, World!")
.background(KeyEventHandling())
}
}
Output:
There's another solution that is very simple but only works for particular types of keys - you'd have to experiment. Just create Buttons with .keyboardShortcut modifier, but hide them visually.
Group {
Button(action: { goAway() }) {}
.keyboardShortcut(.escape, modifiers: [])
Button(action: { goLeft() }) {}
.keyboardShortcut(.upArrow, modifiers: [])
Button(action: { goDown() }) {}
.keyboardShortcut(.downArrow, modifiers: [])
}.opacity(0)
Related
I have been struggling to get an app to work with CloudKit and record sharing. I have
created several apps that sync Core Data records among devices for one user. I have not
been able to get the UICloudSharingController to work for sharing records. I can display
the Sharing View Controller, but tapping on Mail or Message displays a keyboard but no
address field and no way to dismiss the view. I have been so frustrated by it that I
decided to try the Apple "Sharing" sample app to start from the basics. However, the
sample app does not work for me either.
Here's the link to the sample app:
https://github.com/apple/cloudkit-sample-sharing/tree/swift-concurrency
The code below is pretty much straight from the sample app.
This is the ContentView file:
import SwiftUI
import CloudKit
struct ContentView: View {
#EnvironmentObject private var vm: ViewModel
#State private var isAddingContact = false
#State private var isSharing = false
#State private var isProcessingShare = false
#State private var showShareView = false
#State private var showIntermediateView = false
#State private var activeShare: CKShare?
#State private var activeContainer: CKContainer?
var body: some View {
NavigationView {
contentView
.navigationTitle("Contacts")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button { Task.init { try await vm.refresh() } } label: { Image(systemName: "arrow.clockwise") }
}
ToolbarItem(placement: .navigationBarLeading) {
progressView
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { isAddingContact = true }) { Image(systemName: "plus") }
}
}
}
.onAppear {
Task.init {
try await vm.initialize()
try await vm.refresh()
}
}
.sheet(isPresented: $isAddingContact, content: {
AddContactView(onAdd: addContact, onCancel: { isAddingContact = false })
})
}
/// This progress view will display when either the ViewModel is loading, or a share is processing.
var progressView: some View {
let showProgress: Bool = {
if case .loading = vm.state {
return true
} else if isProcessingShare {
return true
}
return false
}()
return Group {
if showProgress {
ProgressView()
}
}
}
/// Dynamic view built from ViewModel state.
private var contentView: some View {
Group {
switch vm.state {
case let .loaded(privateContacts, sharedContacts):
List {
Section(header: Text("Private")) {
ForEach(privateContacts) { contactRowView(for: $0) }
}
Section(header: Text("Shared")) {
ForEach(sharedContacts) { contactRowView(for: $0, shareable: false) }
}
}.listStyle(GroupedListStyle())
case .error(let error):
VStack {
Text("An error occurred: \(error.localizedDescription)").padding()
Spacer()
}
case .loading:
VStack { EmptyView() }
}
}
}
/// Builds a `CloudSharingView` with state after processing a share.
private func shareView() -> CloudSharingView? {
guard let share = activeShare, let container = activeContainer else {
return nil
}
return CloudSharingView(container: container, share: share)
}
/// Builds a Contact row view for display contact information in a List.
private func contactRowView(for contact: Contact, shareable: Bool = true) -> some View {
HStack {
VStack(alignment: .leading) {
Text(contact.name)
Text(contact.phoneNumber)
.textContentType(.telephoneNumber)
.font(.footnote)
}
if shareable {
Spacer()
Button(action: { Task.init { try? await shareContact(contact) }
}, label: { Image(systemName: "square.and.arrow.up") }).buttonStyle(BorderlessButtonStyle())
.sheet(isPresented: $isSharing, content: { shareView() })
}//if sharable
}//h
}//contact row view
// MARK: - Actions
private func addContact(name: String, phoneNumber: String) async throws {
try await vm.addContact(name: name, phoneNumber: phoneNumber)
try await vm.refresh()
isAddingContact = false
}
private func shareContact(_ contact: Contact) async throws {
isProcessingShare = true
do {
let (share, container) = try await vm.createShare(contact: contact)
isProcessingShare = false
activeShare = share
activeContainer = container
isSharing = true
} catch {
debugPrint("Error sharing contact record: \(error)")
}
}
}
And the UIViewControllerRepresentable file for the sharing view:
import Foundation
import SwiftUI
import UIKit
import CloudKit
/// This struct wraps a `UIImagePickerController` for use in SwiftUI.
struct CloudSharingView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
let container: CKContainer
let share: CKShare
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
func makeUIViewController(context: Context) -> some UIViewController {
let sharingController = UICloudSharingController(share: share, container: container)
sharingController.availablePermissions = [.allowReadWrite, .allowPrivate]
sharingController.delegate = context.coordinator
return sharingController
}
func makeCoordinator() -> CloudSharingView.Coordinator {
Coordinator()
}
final class Coordinator: NSObject, UICloudSharingControllerDelegate {
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
debugPrint("Error saving share: \(error)")
}
func itemTitle(for csc: UICloudSharingController) -> String? {
"Sharing Example"
}
}
}
This is the presented screen when tapping to share a record. This all looks as expected:
This is the screen after tapping Messages (same result for Mail). I don't see any way to influence the second screen presentation - it seems that the view controller representable is not working with this version of Xcode:
Any guidance would be appreciated. Xcode 13.1, iOS 15 and I am on macOS Monterrey.
I had the same issue and fixed it by changing makeUIViewController() in CloudSharingView.swift:
func makeUIViewController(context: Context) -> some UIViewController {
share[CKShare.SystemFieldKey.title] = "Boom"
let sharingController = UICloudSharingController(share: share, container: container)
sharingController.availablePermissions = [.allowReadWrite, .allowPrivate]
sharingController.delegate = context.coordinator
-->>> sharingController.modalPresentationStyle = .none
return sharingController
}
It seems like some value work, some don't. Not sure why.
I'm new at Swift and currently, I'm implementing UI for verification code but I have no idea to do I have found something that similar to my requirement on StackOverflow I just copy and pass into my project, and then as you can see in VerificationView_Previews we need to pass the method handler back i don't know how to pass it help, please
//
// VerificationView.swift
// UpdateHistory
//
// Created by Admin on 4/21/21.
//
import SwiftUI
public struct VerificationView: View {
var maxDigits: Int = 6
var label = "Enter One Time Password"
#State var pin: String = ""
#State var showPin = true
var handler: (String, (Bool) -> Void) -> Void
public var body: some View {
VStack {
Text(label).font(.title)
ZStack {
pinDots
backgroundField
}
}
}
private var pinDots: some View {
HStack {
Spacer()
ForEach(0..<maxDigits) { index in
Image(systemName: self.getImageName(at: index))
.font(.system(size: 60, weight: .thin, design: .default))
Spacer()
}
}
}
private func getImageName(at index: Int) -> String {
if index >= self.pin.count {
return "square"
}
if self.showPin {
return self.pin.digits[index].numberString + ".square"
}
return "square"
}
private var backgroundField: some View {
let boundPin = Binding<String>(get: { self.pin }, set: { newValue in
self.pin = newValue
self.submitPin()
})
return TextField("", text: boundPin, onCommit: submitPin)
.accentColor(.clear)
.foregroundColor(.clear)
.keyboardType(.numberPad)
}
private var showPinButton: some View {
Button(action: {
self.showPin.toggle()
}, label: {
self.showPin ?
Image(systemName: "eye.slash.fill").foregroundColor(.primary) :
Image(systemName: "eye.fill").foregroundColor(.primary)
})
}
private func submitPin() {
if pin.count == maxDigits {
handler(pin) { isSuccess in
if isSuccess {
print("pin matched, go to next page, no action to perfrom here")
} else {
pin = ""
print("this has to called after showing toast why is the failure")
}
}
}
}
}
extension String {
var digits: [Int] {
var result = [Int]()
for char in self {
if let number = Int(String(char)) {
result.append(number)
}
}
return result
}
}
extension Int {
var numberString: String {
guard self < 10 else { return "0" }
return String(self)
}
}
struct VerificationView_Previews: PreviewProvider {
static var previews: some View {
VerificationView() // need to pass method handler
}
}
For viewing purpose you can just simply use like this. No need second param for preview
struct VerificationView_Previews: PreviewProvider {
static var previews: some View {
VerificationView { (pin, _) in
print(pin)
}
}
}
You can also use like this
struct VerificationView_Previews: PreviewProvider {
var successClosure: (Bool) -> Void
static var previews: some View {
VerificationView { (pin, successClosure) in
}
}
}
This question is similar to this unanswered question from the Apple Developer Forums, but with a slightly different scenario:
I have a view with a #FetchRequest property of type FetchedResults<Metric>
Within the view, I display the list of those objects
I can tap on one item to select it, storing that selection in a #State var selection: Metric? = nil property.
Here's the properties I defined for my #FocusedValue:
struct FocusedMetricValue: FocusedValueKey {
typealias Value = Metric?
}
extension FocusedValues {
var metricValue: FocusedMetricValue.Value? {
get { self[FocusedMetricValue.self] }
set { self[FocusedMetricValue.self] = newValue }
}
}
Here's how I set the focusedValue from my list view:
.focusedValue(\.metricValue, selection)
And here's how I'm using the #FocusedValue on my Commands struct:
struct MacOSCommands: Commands {
#FocusedValue(\.metricValue) var metric
var body: some Commands {
CommandMenu("Metric") {
Button("Test") {
print(metric??.name ?? "-")
}
.disabled(metric == nil)
}
}
}
The code builds successfully, but when I run the app and select a Metric from the list, the app freezes. If I pause the program execution in Xcode, this is the stack trace I get:
So, how can I make #FocusedValue work in this scenario, with an optional object from a list?
I ran into the same issue. Below is a View extension and ViewModifier that present a version of focusedValue which accepts an Binding to an optional. Not sure why this wasn't included in the framework as it corresponds more naturally to a selection situation in which there can be none...
extension View{
func focusedValue<T>(_ keypath: WritableKeyPath<FocusedValues, Binding<T>?>, selection: Binding<T?>) -> some View{
self.modifier(FocusModifier(keypath: keypath, optionalBinding: selection))
}
}
struct FocusModifier<T>: ViewModifier{
var keypath: WritableKeyPath<FocusedValues, Binding<T>?>
var optionalBinding: Binding<T?>
func body(content: Content) -> some View{
Group{
if optionalBinding.wrappedValue == nil{
content
}
else if let binding = Binding(optionalBinding){
content
.focusedValue(keypath, binding)
}
else{
content
}
}
}
}
In your car usage would look like:
.focusedValue(\.metricValue, selection: $selection)
I have also found that the placement of this statement is finicky. I can only make things work when I place this on the NavigationView itself as opposed to one of its descendants (eg List).
// 1 CoreData optional managedObject in #State var selection
#State var selection: Metric?
// 2 modifiers on View who own the list with the selection
.focusedValue(\.metricValue, $selection)
.focusedValue(\.deleteMetricAction) {
if let metric = selection {
print("Delete \(metric.name ?? "unknown metric")?")
}
}
// 3
extension FocusedValues {
private struct SelectedMetricKey: FocusedValueKey {
typealias Value = Binding<Metric?>
}
private struct DeleteMetricActionKey: FocusedValueKey {
typealias Value = () -> Void
}
var metricValue: Binding<Metric?>? {
get { self[SelectedMetricKey.self] }
set { self[SelectedMetricKey.self] = newValue}
}
var deleteMetricAction: (() -> Void)? {
get { self[DeleteMetricActionKey.self] }
set { self[DeleteMetricActionKey.self] = newValue }
}
}
// 4 Use in macOS Monterey MenuCommands
struct MetricsCommands: Commands {
#FocusedValue(\.metricValue) var selectedMetric
#FocusedValue(\.deleteMetricAction) var deleteMetricAction
var body: some Commands {
CommandMenu("Type") {
Button { deleteMetricAction?() } label: { Text("Delete \(selectedMetric.name ?? "unknown")") }.disabled(selectedMetric?.wrappedValue == nil || deleteMetricAction == nil )
}
}
}
// 5 In macOS #main App struct
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
}
.commands {
SidebarCommands()
MetricsCommands()
}
}
Use of #FocusedValues in Apple WWDC21 example
Use of FocusedValues in SwiftUI with Majid page
Use of #FocusedSceneValue with example of action value passed, in Apple documentation
For SwiftUI 3 macOS Table who support multiple selections
// 1 Properties
#Environment(\.managedObjectContext) var context
var type: Type
var fetchRequest: FetchRequest<Propriete>
var proprietes: FetchedResults<Propriete> { fetchRequest.wrappedValue }
#State private var selectedProprietes = Set<Propriete.ID>()
// 2 Init from Type managedObject who own Propriete managedObjects
// #FecthRequest required to have updates in Table (when delete for example)
init(type: Type) {
self.type = type
fetchRequest = FetchRequest<Propriete>(entity: Propriete.entity(),
sortDescriptors: [ NSSortDescriptor(key: "nom",
ascending: true,
selector: #selector(NSString.localizedCaseInsensitiveCompare(_:))) ],
predicate: NSPredicate(format: "type == %#", type))
}
// 3 Table view
VStack {
Table(proprietes, selection: $selectedProprietes) {
TableColumn("Propriétés :", value: \.wrappedNom)
}
.tableStyle(BorderedTableStyle())
.focusedSceneValue(\.selectedProprietes, $selectedProprietes)
.focusedSceneValue(\.deleteProprietesAction) {
deleteProprietes(selectedProprietes)
}
}
// 4 FocusedValues
extension FocusedValues {
private struct FocusedProprietesSelectionKey: FocusedValueKey {
typealias Value = Binding<Set<Propriete.ID>>
}
var selectedProprietes: Binding<Set<Propriete.ID>>? {
get { self[FocusedProprietesSelectionKey.self] }
set { self[FocusedProprietesSelectionKey.self] = newValue }
}
}
// 5 Delete (for example) in Table View
private func deleteProprietes(_ proprietesToDelete: Set<Propriete.ID>) {
var arrayToDelete = [Propriete]()
for (index, propriete) in proprietes.enumerated() {
if proprietesToDelete.contains(propriete.id) {
let propriete = proprietes[index]
arrayToDelete.append(propriete)
}
}
if arrayToDelete.count > 0 {
print("array to delete: \(arrayToDelete)")
for item in arrayToDelete {
context.delete(item)
print("\(item.wrappedNom) deleted!")
}
try? context.save()
}
}
How to manage selection in Table
I am following the KMM tutorial and was able to successfully run the app on Android side. Now I would like to test the iOS part. Everything seems to be fine except the compilation error below. I suppose this must be something trivial, but as I have zero experience with iOS/Swift, I am struggling fixing it.
My first attempt was to make RocketLaunchRow extend Identifiable, but then I run into other issues... Would appreciate any help.
xcode version: 12.1
Full source:
import SwiftUI
import shared
func greet() -> String {
return Greeting().greeting()
}
struct RocketLaunchRow: View {
var rocketLaunch: RocketLaunch
var body: some View {
HStack() {
VStack(alignment: .leading, spacing: 10.0) {
Text("Launch name: \(rocketLaunch.missionName)")
Text(launchText).foregroundColor(launchColor)
Text("Launch year: \(String(rocketLaunch.launchYear))")
Text("Launch details: \(rocketLaunch.details ?? "")")
}
Spacer()
}
}
}
extension RocketLaunchRow {
private var launchText: String {
if let isSuccess = rocketLaunch.launchSuccess {
return isSuccess.boolValue ? "Successful" : "Unsuccessful"
} else {
return "No data"
}
}
private var launchColor: Color {
if let isSuccess = rocketLaunch.launchSuccess {
return isSuccess.boolValue ? Color.green : Color.red
} else {
return Color.gray
}
}
}
extension ContentView {
enum LoadableLaunches {
case loading
case result([RocketLaunch])
case error(String)
}
class ViewModel: ObservableObject {
let sdk: SpaceXSDK
#Published var launches = LoadableLaunches.loading
init(sdk: SpaceXSDK) {
self.sdk = sdk
self.loadLaunches(forceReload: false)
}
func loadLaunches(forceReload: Bool) {
self.launches = .loading
sdk.getLaunches(forceReload: forceReload, completionHandler: { launches, error in
if let launches = launches {
self.launches = .result(launches)
} else {
self.launches = .error(error?.localizedDescription ?? "error")
}
})
}
}
}
struct ContentView: View {
#ObservedObject private(set) var viewModel: ViewModel
var body: some View {
NavigationView {
listView()
.navigationBarTitle("SpaceX Launches")
.navigationBarItems(trailing:
Button("Reload") {
self.viewModel.loadLaunches(forceReload: true)
})
}
}
private func listView() -> AnyView {
switch viewModel.launches {
case .loading:
return AnyView(Text("Loading...").multilineTextAlignment(.center))
case .result(let launches):
return AnyView(List(launches) { launch in
RocketLaunchRow(rocketLaunch: launch)
})
case .error(let description):
return AnyView(Text(description).multilineTextAlignment(.center))
}
}
}
using a List or ForEach on primitive types that don’t conform to the Identifiable protocol, such as an array of strings or integers. In this situation, you should use id: .self as the second parameter to your List or ForEach
From the above, we can see that you need to do this on that line where your error occurs:
return AnyView(List(launches, id: \.self) { launch in
I think that should eliminate your error.
In SwiftUI you can wrote code like this:
List {
ForEach(users, id: \.self) { user in
Text(user)
}
.onDelete(perform: delete)
}
I try to add functionality with .onDelete syntax method to my custom component:
struct MyComponen: View {
#Binding var alert: String
.
.
.
}
I try to add this ability with extension:
extension MyComponent {
func foo() -> Self {
var copy = self
copy.alert = "Hohoho"
return copy
}
func onDelete() -> Void {
}
}
How can I change state (or callback function with):
struct ContentView: View {
var body: some View {
Group {
MyComponent() //-> with alert = "state 1"
MyComponent().foo() //-> with alert = "state 2"
MyComponent().foo(action: actionFunction) //-> how do this?
}
}
}
Continuing your approach this might look like below. As alternate it is possible to use ViewModifier protocol.
struct MyComponen: View {
#Binding var alert: String
var action: (() -> Void)?
var body: some View {
VStack {
Text("Alert: \(alert)")
if nil != action {
Button(action: action!) {
Text("Action")
}
}
}
}
}
extension MyComponen {
func foo(perform action: #escaping () -> Void ) -> Self {
var copy = self
copy.action = action
return copy
}
}
struct TestCustomModifier: View {
#State var message = "state 2"
var body: some View {
VStack {
MyComponen(alert: .constant("state 1"))
MyComponen(alert: $message).foo(perform: {
print(">> got action")
})
}
}
}
struct TestCustomModifier_Previews: PreviewProvider {
static var previews: some View {
TestCustomModifier()
}
}