How to implement custom callback action in SwiftUI? Similar to onAppear functionality - swift

I have custom ActionView with two buttons: Car and Bike. When these buttons tapped I need that in the MainView modifiers onCarTap/onBikeTap will be triggered.
With my current implementation here is error:
Argument passed to call that takes no arguments
Value of tuple type 'Void' has no member 'onBikeTap'
Source code:
struct ActionView: View {
// Callback for button taps
var onCarTap: (() -> Void)?
var onBikeTap: (() -> Void)?
var body: some View {
HStack {
Button(action: {
onCarTap?()
}, label: {
Text("Car")
})
Button(action: {
onBikeTap?()
}, label: {
Text("Bike")
})
}
}
}
I am looking for solution like this:
struct MainView: View {
var body: some View {
ActionView()
.onCarTap({})
.onBikeTap({ })
}
}
It is possible to implement in this way:
ActionView(onCarTap: {
print("on car tap")
}, onBikeTap: {
print("on bike tap")
})

Assuming you have the following View:
struct ActionView: View {
var onCarTapAction: (() -> Void)?
var onBikeTapAction: (() -> Void)?
var body: some View {
HStack {
Button(action: {
onCarTapAction?()
}, label: {
Text("Car")
})
Button(action: {
onBikeTapAction?()
}, label: {
Text("Bike")
})
}
}
}
You can create an extension:
extension ActionView {
func onCarTap(action: #escaping (() -> Void)) -> ActionView {
ActionView(onCarTapAction: action, onBikeTapAction: onBikeTapAction)
}
func onBikeTap(action: #escaping (() -> Void)) -> ActionView {
ActionView(onCarTapAction: onCarTapAction, onBikeTapAction: action)
}
}
and use it like this:
struct ContentView: View {
var body: some View {
ActionView()
.onCarTap {
print("onCarTap")
}
.onBikeTap {
print("onBikeTap")
}
}
}

You can declare a modifier for your purpose like the following.
extension ActionView {
func onCarTap(_ handler: #escaping () -> Void) -> ActionView {
var new = self
new.onCarTap = handler
return new
}
}
In addition, if you prefer to hide the handler property with private or fileprivate to prevent to be accessed directly, have to declare a designated init which accepts parameters for its properties except one for the handler.

Related

Custom Mouse Up/Down View Modifier Prevents View Hierarchy from Updating

I'm trying to implement a custom view modifier to detect mouse up/down events within a SwiftUI view hierarchy. This is my solution so far:
extension View {
func onMouseDown(action: #escaping (Bool) -> Void) -> some View {
MouseDownView(action: action) { self }
}
}
struct MouseDownView<Content: View>: View {
let action: (Bool) -> Void
let content: () -> Content
init(action: #escaping (Bool) -> Void, #ViewBuilder content: #escaping () -> Content) {
self.action = action
self.content = content
}
var body: some View {
MouseDownRepresentable(action: action, content: content())
}
}
struct MouseDownRepresentable<Content: View>: NSViewRepresentable {
let action: (Bool) -> Void
let content: Content
func makeNSView(context: Context) -> NSHostingView<Content> {
MouseDownHostingView(action: action, rootView: content)
}
func updateNSView(_ view: NSHostingView<Content>, context: Context) {
}
}
class MouseDownHostingView<Content: View>: NSHostingView<Content> {
let action: (Bool) -> Void
init(action: #escaping (Bool) -> Void, rootView: Content) {
self.action = action
super.init(rootView: rootView)
}
required init(rootView: Content) {
fatalError()
}
required init?(coder: NSCoder) {
fatalError()
}
override func mouseDown(with event: NSEvent) {
action(true)
}
override func mouseUp(with event: NSEvent) {
action(false)
}
}
It works, in the sense that the closure passed to onMouseDown is getting called whenever a mouse up/down event occurs within the the view that it was applied to (even taking non-rectangular shapes into account). Unfortunately though, there is an issue that prevents the view from updating when any of its #State variables get modified inside the passed closure.
Example usage:
struct PlayerView<Content: View>: View {
private let content: () -> Content
#State var isPlaying = true
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
ZStack {
content()
ZStack {
Circle()
.foregroundStyle(.secondary)
Image(systemName: isPlaying ? "play.fill" : "pause.fill")
.foregroundColor(.white)
.font(.system(size: 40))
}
.onMouseDown { isDown in
if isDown {
// ...
} else {
isPlaying.toggle()
}
}
.frame(width: 80, height: 80)
}
}
}
isPlaying gets toggled everytime the circular view is clicked, but the image never updates. How can this be? Does this configuration of an NSViewRepresentable inside of an NSHostingView somehow mean, that the modified view is no longer formally part of the original view hierarchy, and therefore not allowed to update it?
I tried wrapping isPlaying as a published property inside of an ObservableObject that gets attached to the view as a #StateObject, but it showed the same behavior.
Does anyone know what's going on here and/or how to work around it?
Yes, SwiftUI does not see dependency through NSView bridge anymore, so instead the content should remain in SwiftUI world, but modifier/handler be placed above it, like
extension View {
func onMouseDown(action: #escaping (Bool) -> Void) -> some View {
self.overlay(
MouseDownView(action: action) {
Color.clear.contentShape(Rectangle())
})
}
}
Tested with Xcode 13.4 / macOS 12.5

Passing A Closure With Arguments To SwiftUI View

I am working on a SwiftUI project. I've created a custom button that I can pass a function to. This looks like the following.
Custom Button
struct CustomButton: View {
let buttonTitle: String
var function: () -> Void
var body: some View {
Button(action: {
self.function()
}, label: {
Text(self.buttonTitle)
}) // Button - Login
} // View
}
In the view that uses this I can do the following.
struct NewView: View {
var body: some View {
CustomButton(buttonTitle: "Custom Button", function: myFunc)
}
}
func myFunc() {
print("My Custom Button Tapped")
}
This works really well.
What I want to do now is pass a parameter to the function. And I am having trouble with this. I tried the following.
struct CustomButton: View {
let buttonTitle: String
var function: (String) -> Void
var body: some View {
Button(action: {
self.function() // I DON'T KNOW WHAT DO TO HERE.
}, label: {
Text(self.buttonTitle)
}) // Button - Login
} // View
}
struct NewView: View {
var body: some View {
CustomButton(buttonTitle: "Custom Button", function: myFunc(text: "Hello"))
}
}
func myFunc(text: String) {
print(text)
}
This does not work. When I call CustomButton I get the following error.
Cannot convert value of type '()' to expected argument type '() ->
Void'
I also do not know what parameter to add to the self.function() call in the Button action.
Any help would be greatly appreciated.
First, the simplest answer -- by enclosing myFunc(text: "Hello") in { }, you can turn it into a closure. Then, it can get passed to your original () -> Void declaration.
struct CustomButton: View {
let buttonTitle: String
let function : () -> Void
var body: some View {
Button(action: {
self.function()
}, label: {
Text(self.buttonTitle)
}) // Button - Login
} // View
}
struct NewView: View {
var body: some View {
CustomButton(buttonTitle: "Custom Button", function: {
myFunc(text: "Hello")
})
}
}
You could also use an #autoclosure to provide similar behavior without the { }, but you'd have to declare a custom init for your CustomButton:
struct CustomButton: View {
let buttonTitle: String
let function : () -> Void
init(buttonTitle: String, function: #autoclosure #escaping () -> Void) {
self.buttonTitle = buttonTitle
self.function = function
}
var body: some View {
Button(action: {
self.function()
}, label: {
Text(self.buttonTitle)
}) // Button - Login
} // View
}
struct NewView: View {
var body: some View {
CustomButton(buttonTitle: "Custom Button", function: myFunc(text:"Hello"))
}
}
Finally, another option (that I think there's unlikely to a use case for, but just in case it fits) would be to pass the string parameter separately:
struct CustomButton: View {
let buttonTitle: String
let stringParameter : String
let function : (String) -> Void
var body: some View {
Button(action: {
self.function(stringParameter)
}, label: {
Text(self.buttonTitle)
}) // Button - Login
} // View
}
struct NewView: View {
var body: some View {
CustomButton(buttonTitle: "Custom Button", stringParameter: "Hello", function: myFunc)
}
}
Here what you may looking for:
struct ContentView: View {
let action: (String) -> Void = { value in print(value) }
var body: some View {
CustomButtonView(string: "print", valueToSend: "Hello World!", action: action)
}
}
struct CustomButtonView: View {
let string: String
let valueToSend: String
let action: (String) -> Void
var body: some View {
Button(string) {
action(valueToSend)
}
}
}

Popover crash when update value using UIViewControllerRepresentable

I want to make popover on iPhone , I notice when using .popover in iPhone it will always show as sheet , but on iPad it will show as popover
so I decide to use UIKit version
everything is working fine until I tap on Button to update the view
it will crash with this error
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Application tried to present modally a view controller <_TtGC7SwiftUI19UIHostingControllerGVS_6HStackGVS_9TupleViewTGVS_6ButtonVS_4Text_S4_GS3_S4______: 0x7fd47b424df0> that is already being presented by <UIViewController: 0x7fd47b426200>.
My code :
struct PopoverViewModifier<PopoverContent>: ViewModifier where PopoverContent: View {
#Binding var isPresented: Bool
let onDismiss: (() -> Void)?
let content: () -> PopoverContent
func body(content: Content) -> some View {
content
.background(
Popover(
isPresented: self.$isPresented,
onDismiss: self.onDismiss,
content: self.content
)
)
}
}
extension View {
func popover<Content>(
isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
content: #escaping () -> Content
) -> some View where Content: View {
ModifiedContent(
content: self,
modifier: PopoverViewModifier(
isPresented: isPresented,
onDismiss: onDismiss,
content: content
)
)
}
}
struct Popover<Content: View> : UIViewControllerRepresentable {
#Binding var isPresented: Bool
let onDismiss: (() -> Void)?
#ViewBuilder let content: () -> Content
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self, content: self.content())
}
func makeUIViewController(context: Context) -> UIViewController {
return UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
let host = context.coordinator.host
if self.isPresented {
host.preferredContentSize = host.sizeThatFits(in: CGSize(width: Int.max , height: Int.max))
host.modalPresentationStyle = UIModalPresentationStyle.popover
host.popoverPresentationController?.delegate = context.coordinator
host.popoverPresentationController?.sourceView = uiViewController.view
host.popoverPresentationController?.sourceRect = uiViewController.view.bounds
uiViewController.present(host, animated: true, completion: nil)
}
else {
host.dismiss(animated: true, completion: nil)
}
}
class Coordinator: NSObject, UIPopoverPresentationControllerDelegate {
let host: UIHostingController<Content>
private let parent: Popover
init(parent: Popover, content: Content) {
self.parent = parent
self.host = UIHostingController(rootView: content)
}
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
self.parent.isPresented = false
if let onDismiss = self.parent.onDismiss {
onDismiss()
}
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
}
How I use it :
struct ContentView: View {
#State var openChangeFont = false
#State var currentFontSize = 0
var body: some View {
NavigationView {
Text("Test")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Text("Popover")
.popover(isPresented: $openChangeFont, content: {
HStack {
Button(action: {
DispatchQueue.main.async {
currentFontSize += 2
}
}, label: {
Text("Increase")
})
Text("\(currentFontSize)")
Button(action: {
currentFontSize -= 2
}, label: {
Text("Decrease")
})
}
})
.onTapGesture {
openChangeFont.toggle()
}
}
}
}
}
Put a breakpoint in Popover.updateUIViewController and I think you’ll catch the problem. updateUIViewController is called every time the SwiftUI view is updated, which means it may be called when isPresented is true and you the popover is already being presented.
If that’s the issue, then you need to track whether you’re already presenting the popover or not. You’re already implementing UIPopoverPresentationControllerDelegate so you can use that.

How can I make a function which returns some View and accepts a closure in SwiftUI?

I want build a function for View which accepts a closure like onChange or onPreferenceChange here is my code, with this code I can read value or the change of value, but I cannot send it back to my ContentView, because I want use print(newValue) in ContentView. How can I do this? thanks for your help and time.
import SwiftUI
struct ContentView: View {
#State private var stringOfText: String = "Hello, world!"
var body: some View {
Text(stringOfText)
.perform(value: stringOfText) // <<: Here: I want get back data as: { newValue in print(newValue) }
Button("update") { stringOfText += " updated!" }.padding()
}
}
extension View {
func perform(value: String) -> some View {
return self
.preference(key: CustomPreferenceKey.self, value: value)
.onPreferenceChange(CustomPreferenceKey.self) { newValue in
print(newValue)
// I want send back newValue to ContentView that perform used!
}
}
}
struct CustomPreferenceKey: PreferenceKey {
static var defaultValue: String { get { return String() } }
static func reduce(value: inout String, nextValue: () -> String) { value = nextValue() }
}
so we only need to add callback closure parameter in perform function. like this
func perform(value: String,callback : #escaping (_ value : String ) -> Void) -> some View
so now we have a callback closure in which we can send the value back.
struct ContentView: View {
#State private var stringOfText: String = "Hello, world!"
var body: some View {
Text(stringOfText)
.perform(value: stringOfText) {
newValue in
print(newValue)
}
Button("update") { stringOfText += " updated!" }.padding()
}
}
extension View {
func perform(value: String,callback : #escaping (_ value : String ) -> Void) -> some View {
return self
.preference(key: CustomPreferenceKey.self, value: value)
.onPreferenceChange(CustomPreferenceKey.self) { newValue in
callback(newValue)
}
}
}
struct CustomPreferenceKey: PreferenceKey {
static var defaultValue: String { get { return String() } }
static func reduce(value: inout String, nextValue: () -> String) { value = nextValue() }
}

Handle single click and double click while updating the view

I'm trying to implement a single and double click for grid view items on macOS. On the first click it should highlight the item. On the second click it should open a detail view.
struct MyGridView: View {
var items: [String]
#State var selectedItem: String?
var body: some View {
LazyVGrid(columns: [
GridItem(.adaptive(minimum: 200, maximum: 270))
], alignment: .center, spacing: 8, pinnedViews: []) {
ForEach(items, id: \.self) { item in
Text("Test")
.gesture(TapGesture(count: 2).onEnded {
print("double clicked")
})
.simultaneousGesture(TapGesture().onEnded {
self.selectedItem = item
})
.background(self.selectedItem == item ? Color.red : .blue)
}
}
}
Problem
The highlighting works as expected without delay. But because the first click updates the view, the TapGesture for the double click is ignored. Only once the item was selected before, the double click also works, because it doesn't need to update the view again.
I was following https://stackoverflow.com/a/59992192/1752496 but it doesn't consider view updates after the first click.
You can use a custom click handler like this:
class TapState {
static var taps: [String: Date] = [:]
static func isDoubleTap<Content: View>(for view: Content) -> Bool {
let key = "\(view)"
let date = taps[key]
taps[key] = Date()
if let date = date, date.timeIntervalSince1970 >= Date().timeIntervalSince1970 - 0.4 {
return true
} else {
return false
}
}
}
extension View {
public func onTapGesture(firstTap: #escaping () -> Void, secondTap: #escaping () -> Void) -> some View {
onTapGesture(count: 1) {
if TapState.isDoubleTap(for: self) {
secondTap()
} else {
firstTap()
}
}
}
}
And implement it like this:
struct MyView: View {
#State var isSelected: Bool = false
#State var isShowingDetail: Bool = false
var body: some View {
Text("Text")
.background(isSelected ? Color.green : Color.blue)
.onTapGesture(firstTap: {
isSelected = true
}, secondTap: {
isShowingDetail = true
})
.sheet(isPresented: $isShowingDetail) {
Text("Detail")
}
}
}
Swift Package
I've also created a small package you can use: https://github.com/lukaswuerzburger/DoubleTap
As far as I know there is no build in function or something like that to understand deference between singleTap or doubleTap, but we have endless freedom to build what we want, I found this way:
You can apply tapRecognizer to anything you want, will work the same! for showing the use case I just applied to a Circle().
import SwiftUI
struct ContentView: View {
var body: some View {
Circle()
.fill(Color.red)
.frame(width: 100, height: 100, alignment: .center)
.tapRecognizer(tapSensitivity: 0.2, singleTapAction: singleTapAction, doubleTapAction: doubleTapAction)
}
func singleTapAction() { print("singleTapAction") }
func doubleTapAction() { print("doubleTapAction") }
}
struct TapRecognizerViewModifier: ViewModifier {
#State private var singleTapIsTaped: Bool = Bool()
var tapSensitivity: Double
var singleTapAction: () -> Void
var doubleTapAction: () -> Void
init(tapSensitivity: Double, singleTapAction: #escaping () -> Void, doubleTapAction: #escaping () -> Void) {
self.tapSensitivity = tapSensitivity
self.singleTapAction = singleTapAction
self.doubleTapAction = doubleTapAction
}
func body(content: Content) -> some View {
return content
.gesture(simultaneouslyGesture)
}
private var singleTapGesture: some Gesture { TapGesture(count: 1).onEnded{
singleTapIsTaped = true
DispatchQueue.main.asyncAfter(deadline: .now() + tapSensitivity) { if singleTapIsTaped { singleTapAction() } }
} }
private var doubleTapGesture: some Gesture { TapGesture(count: 2).onEnded{ singleTapIsTaped = false; doubleTapAction() } }
private var simultaneouslyGesture: some Gesture { singleTapGesture.simultaneously(with: doubleTapGesture) }
}
extension View {
func tapRecognizer(tapSensitivity: Double, singleTapAction: #escaping () -> Void, doubleTapAction: #escaping () -> Void) -> some View {
return self.modifier(TapRecognizerViewModifier(tapSensitivity: tapSensitivity, singleTapAction: singleTapAction, doubleTapAction: doubleTapAction))
}
}