Add View as a Parameter to a Custom Viewmodifier

Add view as a parameter to a custom ViewModifier

You can use generics, i.e., make innerView returning some view:

struct SampleModifier<V>: ViewModifier where V: View {
let param1: CGFloat
let innerView: (CGFloat, CGFloat) -> V

...
}

Then you don't need AnyView anymore:

struct SampleView: View {
var body: some View {
Rectangle()
.modifier(SampleModifier(param1: 150, innerView: { param1, param2 in
Text("Inner View: \(param1) - \(param2)")
}))
}
}

Alternatively, you can create a View extension:

extension View {
func sampleModifier<V>(
param1: CGFloat,
innerView: @escaping (CGFloat, CGFloat) -> V
) -> some View where V: View {
modifier(SampleModifier(param1: param1, innerView: innerView))
}
}

and use it like this:

struct SampleView: View {
var body: some View {
Rectangle()
.sampleModifier(param1: 150, innerView: { param1, param2 in
Text("Inner View: \(param1) - \(param2)")
})
}
}

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.

How to create custom Modifier to manipulate custom View in SwiftUI?

A custom modifier is possible but is not needed in this case as would introduced unneeded complexity, instead your extension can look like

extension View where Self: TitleView {
func setTitle(_ title: String) -> some View {
var newView = self
newView.title = title
return newView
}
}

Tested with Xcode 13.3

SwiftUI passing ViewModifier as parameter

If I understood correctly, you can make your MySuperTextField accept a generic parameter:

struct MySuperTextField<V>: View where V: ViewModifier {
private let placeholder: String
@Binding private var text: String
private let vm: V

init(_ placeholder: String, text: Binding<String>, vm: V) {
self.placeholder = placeholder
self._text = text
self.vm = vm
}

var body: some View {
TextField(placeholder, text: $text)
.modifier(vm)
}
}

Then, you can pass some ViewModifier as the parameter:

struct ContentView: View {
@State private var text: String = "Test"

var body: some View {
MySuperTextField("Last Name", text: $text, vm: LastNameModifier())
}
}

If you need a way to skip the vm parameter when creating MySuperTextField:

MySuperTextField("Last Name", text: $text)

you can create an extension:

extension MySuperTextField where V == EmptyModifier {
init(_ placeholder: String, text: Binding<String>) {
self.placeholder = placeholder
self._text = text
self.vm = EmptyModifier()
}
}

SwiftUI ViewModifier for custom View

It is your view and modifiers are just functions that generate another, modified, view, so... here is some possible simple way to achieve what you want.

Tested with Xcode 12 / iOS 14

demo

struct ChildView: View {
var theText = ""

@State private var color = Color(.purple)

var body: some View {
HStack {
if theText.isEmpty { // If there's no theText, a Circle is created
Circle()
.foregroundColor(color)
.frame(width: 100, height: 100)
} else { // If theText is provided, a Text is created
Text(theText)
.padding()
.background(RoundedRectangle(cornerRadius: 25.0)
.foregroundColor(color))
.foregroundColor(.white)
}
}
}

// simply modify self, as self is just a value
public func someModifierOrTheLike(color: Color) -> some View {
var view = self
view._color = State(initialValue: color)
return view.id(UUID())
}
}

How to implement a custom view modifier

This is possible approach, the only small correction to your modifier

extension NavBarView {
func trailingItem(@ViewBuilder _ item: @escaping () -> Item) -> some View {
var view = self // make modifiable
view.item = item()
return view
}
}

and so you don't need to have it as @State, just declare it as

fileprivate var item: Item?

Tested with Xcode 11.4

Implementing Trailing Closure Syntax In A View Modifier That Wraps Another View

Each of your item parameters is defined as (V) -> V -- this means a closure that takes V as a parameter and returns V. In fact, all you want is () -> V (a closure that takes no parameters and returns V). That gets you the trailing closure you want -- then, you can pass just the V to the view modifiers.

@available(iOS 14, *)
struct ToolbarAvailable14ViewModifier<V>: ViewModifier where V: View {
var placement: ToolbarItemPlacement
var item: V

func body(content: Content) -> some View {
content
.toolbar {
ToolbarItemGroup(placement: placement) {
item
}
}
}
}

@available(iOS 15, *)
struct ToolbarAvailable15ViewModifier<V>: ViewModifier where V: View {
var placement: ToolbarItemPlacement
var item: V

func body(content: Content) -> some View {
content
.toolbar {
ToolbarItemGroup(placement: placement) {
item
}
}
}
}

extension View {
@ViewBuilder
func toolbarIfAvailable<V>(placement: ToolbarItemPlacement, _ item: @escaping () -> V) -> some View where V: View {
if #available(iOS 14, *) {
self
.modifier(ToolbarAvailable14ViewModifier(placement: placement, item: item()))
}
if #available(iOS 15, *) {
self
.modifier(ToolbarAvailable15ViewModifier(placement: placement, item: item()))
}
else {
self
}
}
}

This also takes care of your _ in issue since the closure no longer takes an argument/parameter.



Related Topics



Leave a reply



Submit