Implementing a Custom Viewmodifier Where Output Is Conditional on Concrete View Type (Swiftui)

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.

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 treat if-let-else as a single view in SwiftUI?

Make it closure argument a view builder, like

extension View {
func statusBar<V: View>(@ViewBuilder statusBar: () -> V) -> some View {
self.modifier(StatusBarView(statusBar: statusBar))
}
}

the same can be done in init of modifier, but not required specifically for this case of usage.

Tested with Xcode 13.4 / iOS 15.5

TextField ViewModifier not conforming to ViewModifier?

You likely have a struct/class in your project named Content

If you have Xcode's standard dark theme the "mint"/"greenish" means it is "Project" defined.

Sample Image

When you are using Apple's definition it is pinkish/purple like ViewModifier, View, and String in your screenshot.

Sample Image

Search for struct Content, class Content, enum Content, etc. In your project, You will find the duplicate and then just change the name of the duplicate.

It could also be a generic <Content: SomeProtocol> or <Content> or typealias Content

You can confirm the duplicate by being more specific

Sample Image

Swift Generic ViewModifier

It is difficult—probably impossible—to write this as a type conforming to ViewModifier. You give this type for then:

var then: (_: Content) -> Content

However, you're usually going to want then to return a different type than it was given, because that's how modifiers generally work. For example, Text("hello") has type Text, but Text("hello").background(Color.red) has type ModifiedContent<Text, _BackgroundStyleModifier<Color>>.

We can try making the modifier generic over the type returned by then:

struct If<Then: View>: ViewModifier {
var test: Bool

@ViewBuilder
var then: (_: Content) -> Then

@ViewBuilder
func body(content: Content) -> some View {
if test {
then(content)
} else {
content
}
}
}

But then we run into a problem when we try to use the modifier:

struct ExampleView: View {
var isActive: Bool
var body: some View {
HStack {
}.modifier(If(test: isActive, then: { $0.background(.red) }))
// ^ Generic parameter 'Then' could not be inferred
}
}

Why can't it infer the type? The type would be ModifiedContent<Content, _BackgroundStyleModifier<Color>>. But Content is actually a type alias for _ViewModifier_Content<If>… except that we made If generic over Then, so Content is a type alias for _ViewModifier_Content<If<ModifiedContent<Content, _BackgroundStyleModifier<Color>>>>. Oops, we just put Content back in there. The Content type alias and the Then type are mutually recursive, and Swift cannot handle an infinitely-nested generic type.

It is, however, possible to achieve a similar effect by extending View with a modifier method. You can find lots of web pages with examples by searching for “swiftui conditional modifier”. It typically looks like this:

extension View {
@ViewBuilder
func `if`<Content: View>(
_ condition: Bool,
@ViewBuilder transform: (Self) -> Content
) -> some View {
if (condition) {
transform(self)
} else {
self
}
}
}

and you use it like this:

struct ExampleView: View {
var isActive: Bool
var body: some View {
HStack {
}.if(isActive) { $0.background(.red) }
}
}

Note, however, that a modifier like this can cause animations and transitions to misbehave. The problem is that SwiftUI generally considers transform(self) and self to have different identities because they're in separate branches of the if statement. This affects how SwiftUI animates them. This may or may not be a problem for you, but it is why the general advice from Apple is to use “inert” forms of modifiers directly, instead of introducing an if-style modifier. That is, Apple would recommend this instead, for the example above:

struct ExampleView: View {
var isActive: Bool
var body: some View {
HStack {
}.background(isActive ? .red : .clear)
}
}

Here, there's no if statement, so SwiftUI will recognize the HStack as being the same across changes to isActive, for animation purposes.

Value of type 'some View' has no member 'baselineOffset'

The baselineOffset modifier is available only for Text, but content in ViewModifier is opaque generic view.

Instead you can just make direct extension for Text, like

extension Text {
func heading(
color: Color = .white,
size: CGFloat = 42,
align: TextAlignment = .center
) -> some View {
self.font(Font.custom("HunterHeartFreeFont", size: size))
.baselineOffset(-14)
.foregroundColor(color)
.multilineTextAlignment(align)
}
}


Related Topics



Leave a reply



Submit