Swiftui How Add Custom Modifier with Callback

SwiftUI how add custom modifier with callback

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()
}
}

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

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.

Add a onChanged callback to a SwiftUI struct for emitting values

Found this answer by @Asperi, which shows how to do it like I wanted. The trick is to actually return a copy of itself.

typealias OnChange = ((CGFloat) -> Void)?

struct WheelView: View {
var action: OnChange

func onChanged(perform action: OnChange) -> Self {
var copy = self
copy.action = action
return copy
}

var body: some View {
Circle()
.gesture(DragGesture()
.onChanged { value in
// Emit the angle change
if let action = self.action {
action(0.4)
}
})
}
}

Then we can use our component like this:

struct Usage: View {
var body: some View {
WheelView()
.onChanged { value in
print("Value is \(value)")
}
}
}

SwiftUI callback as parameter to a view

Below variant works. Tested with Xcode 11.4.

struct MainView: View {
var body: some View {
VStack {
MyContentView(doSomething: self.doSomething)
MenuView(doSomething: self.doSomething)
}
}

func doSomething() {
print("do something")
}
}

struct MyContentView : View {
var doSomething : () -> ()
var body: some View {
Button(action: { self.doSomething() }) { Text("do something") }
}
}

struct MenuView : View {
var doSomething : () -> ()
var body: some View {
Button(action: { self.doSomething() }) { Text("Menu do something") }
}
}

How can make a function which take View and returns custom result in SwiftUI?

The general idea is to have the view report its size using preference, and create a view modifier to capture that. But, like @RobNapier said, the struct has to be in the view hierarchy, and so within a rendering context, to be able to talk about sizes.

struct SizeReporter: ViewModifier {
@Binding var size: CGSize
func body(content: Content) -> some View {
content
.background(GeometryReader { geo in
Color.clear
.preference(key: SizePreferenceKey.self, value: geo.size)
})
.onPreferenceChange(SizePreferenceKey.self, perform: { value in
size = value
})
}
}

And we'd need to define SizePreferenceKey:

extension SizeReporter {
private struct SizePreferenceKey: PreferenceKey {
static let defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
}

You could create a convenience method on View:

extension View {
func getSize(_ size: Binding<CGSize>) -> some View {
self.modifier(SizeReporter(size: size))
}
}

and use it like so:

@State var size: CGSize = .zero

var body: some View {
Text("hello").getSize($size)
}

How to write function in SwiftUI that takes callback (function type) from user of the view so that user can chain the function calls?

Here is possible approach (recalling that View is just a value)...

struct MyResuableView: View {
var event1Callback: () -> Void = {}
var event2Callback: () -> Void = {}

var body: some View {
Text("Resuable View")
}

func onEvent1(action: @escaping () -> Void) -> Self {
var newOne = self
newOne.event1Callback = action
return newOne
}

func onEvent2(action: @escaping () -> Void) -> Self {
var newOne = self
newOne.event2Callback = action
return newOne
}
}

How can I change this custom modifier to set input for its functions?

To get the index of ForEach passed to singleTapAction and doubleTapAction, you shouldn't change the view modifier, because then the view modifier is responsible for passing the index to the closures, but the view modifier doesn't know about the ForEach, does it? The view modifier is not even applied to the ForEach.

Just change the method declarations:

func singleTapAction(index: Int) {

print("single tapped index \(index)")

}
func doubleTapAction(index: Int) {

print("double tapped index \(index)")

}

and then change how you call the view modifier. Instead of passing the methods themselves, pass a closure that calls the methods with the index of the ForEach as argument:

.tapRecognizer(
tapSensitivity: 0.2,
singleTapAction: { singleTapAction(index: index) },
doubleTapAction: { doubleTapAction(index: index) }
)

Adding a view modifier inside .onChange

First of all, thanks for the help. Neither of the answers helped in my situation, since I couldn't get the modifier to update with the variable change. But with some Googling and trying out different solutions I figured out a working solution for updating the status bar colors.

I needed to update the style variable in the HostingViewController, and then update accordingly. So I added the HostingViewController as a @StateObject and updated the style variable inside the .onChange(). Not quite the solution I was going with to start out, but it does work.

The code:

import SwiftUI
import Introspect

struct ContentView: View {
@ObservedObject var viewModel: TestViewModel
@StateObject var hostingViewController: HostingViewController = .init(rootViewController: nil, style: .default)

var body: some View {
ZStack {
Rectangle().foregroundColor(.mint)
VStack(alignment: .center, spacing: 25) {
Text("Test text \(viewModel.publishedValue)")
.onChange(of: viewModel.publishedValue) { newValue in
// Change status bar color
if viewModel.publishedValue % 2 == 0 {
hostingViewController.style = .lightContent
} else {
hostingViewController.style = .darkContent
}

}
Button("Increment") {
viewModel.publishedValue += 1
}
}
}
.ignoresSafeArea()
.introspectViewController { viewController in
let window = viewController.view.window
guard let rootViewController = window?.rootViewController else { return }
hostingViewController.rootViewController = rootViewController
window?.rootViewController = hostingViewController
}
}
}

class TestViewModel: ObservableObject {
@Published var publishedValue: Int

init(publishedValue: Int) {
self.publishedValue = publishedValue
}
}

class HostingViewController: UIViewController, ObservableObject {
var rootViewController: UIViewController?

var style: UIStatusBarStyle = .default {
didSet {
self.rootViewController?.setNeedsStatusBarAppearanceUpdate()
}
}

init(rootViewController: UIViewController?, style: UIStatusBarStyle) {
self.rootViewController = rootViewController
self.style = style
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
super.init(coder: coder)
}

override func viewDidLoad() {
super.viewDidLoad()
guard let child = rootViewController else { return }
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}

override var preferredStatusBarStyle: UIStatusBarStyle {
return style
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
setNeedsStatusBarAppearanceUpdate()
}
}

Big shoutout to this answer for giving me all that I needed to implement the solution. Note: You can override the rootViewController however you like, I used SwiftUI-Introspect as we are already using it in our project anyway.

GitHub branch

Implementing a custom ViewModifier where output is conditional on concrete View type (SwiftUI)

You're right, there are some code smells in that implementation, starting with the fact that you need to write type checks to accomplish the goal. Whenever you start writing is or as? along with concrete types, you should think about abstracting to a protocol.

In your case, you need an abstraction to give you the background color, so a simple protocol like:

protocol CustomModifiable: View {
var customProp: Color { get }
}

extension Text: CustomModifiable {
var customProp: Color { .red }
}

extension TextField: CustomModifiable {
var customProp: Color { .blue }
}

, should be the way to go, and the modifier should be simplifiable along the lines of:

struct CustomModifier: ViewModifier {
@ViewBuilder func body(content: Content) -> some View {
if let customModifiable = content as? CustomModifiable {
content.background(customModifiable.customProp)
} else {
content
}
}
}

The problem is that this idiomatic approach doesn't work with SwiftUI modifiers, as the content received as an argument to the body() function is some internal type of SwiftUI that wraps the original view. This means that you can't (easily) access the actual view the modifier is applied to.

And this is why the is checks always failed, as the compiler correctly said.

Not all is lost, however, as we can work around this limitation via static properties and generics.

protocol CustomModifiable: View {
static var customProp: Color { get }
}

extension Text: CustomModifiable {
static var customProp: Color { .red }
}

extension TextField: CustomModifiable {
static var customProp: Color { .blue }
}

struct CustomModifier<T: CustomModifiable>: ViewModifier {
@ViewBuilder func body(content: Content) -> some View {
content.background(T.customProp)
}
}

extension View {
func customModifier() -> some View where Self: CustomModifiable {
modifier(CustomModifier<Self>())
}
}

The above implementation comes with a compile time benefit, as only Text and TextField are allowed to be modified with the custom modifier. If the developer tries to apply the modifier on a non-accepted type of view, they-ll get a nice Instance method 'customModifier()' requires that 'MyView' conform to 'CustomModifiable', which IMO is better than deceiving about the behaviour of the modifier (i.e. does nothing of some views).

And if you need to support more views in the future, simply add extensions that conform to your protocol.



Related Topics



Leave a reply



Submit