Custom Modifier Using Text's underline(_:color:)

I have a custom modifier which is only used on a Text view. I want to include an underline call in it, but get an error. My understanding is that this is because underline is unique to Text views whereas the ViewModifier protocol isn't.


Is there a way to specify this?


struct activeEquationString: ViewModifier {
    let termInd: Int
    @Binding var currentInd: Int
    
    func body(content: Content) -> some View {
        content
            .underline(currentInd == termInd) // <-- Value of type 'activeEquationString.Content' (aka '_ViewModifier_Content<activeEquationString>') has no member 'underline'
            .foregroundColor(currentInd == termInd ? Color.black : Color.gray)
    }
}
Answered by OOPer in 418732022

As far as I tried, I could not write a ViewModifier with its Content constrained to Text.

ViewModifier.Content is an opaque type and we do not have much control on it.


Instead, you can define your own TextModifier protocol.

protocol TextModifier {
    associatedtype Body: View
    
    func body(text: Text) -> Body
}
extension Text {
    func modifier<TM: TextModifier>(_ theModifier: TM) -> some View {
        return theModifier.body(text: self)
    }
}


And define a type conforming to the protocol.

struct ActiveEquationString: TextModifier {
    let termInd: Int
    @Binding var currentInd: Int
      
    func body(text: Text) -> some View {
        return text
            .underline(currentInd == termInd)
            .foregroundColor(currentInd == termInd ? Color.black : Color.gray)
    }
}


You can use it in a way very similar to ViewModifier.

struct ContentView: View {
    @State var ind: Int = 1
    
    var body: some View {
        VStack {
            Text("Hello!\(ind)")
                .modifier(ActiveEquationString(termInd: 0, currentInd: $ind))
            Button(action: {
                self.ind = (self.ind + 1) % 3
            }) {
                Text("Change ind")
            }
        }
    }
}


In a simple example as shown above, it works as expected.

Just a grain of salt…


Did you consider using ViewBuilder and test on condition of type of view ? Not sure it can work, but I would try some variation of it.

To make it clear, have a look here:

https://forums.swift.org/t/conditionally-apply-modifier-in-swiftui/32815/4

test the type of view and return underlined of not ?


@ViewBuilder

var myView: some View {

if condition {

ViewA()

} else {

ViewB()

}

}


Note: I've not found how to text the type of content…

Accepted Answer

As far as I tried, I could not write a ViewModifier with its Content constrained to Text.

ViewModifier.Content is an opaque type and we do not have much control on it.


Instead, you can define your own TextModifier protocol.

protocol TextModifier {
    associatedtype Body: View
    
    func body(text: Text) -> Body
}
extension Text {
    func modifier<TM: TextModifier>(_ theModifier: TM) -> some View {
        return theModifier.body(text: self)
    }
}


And define a type conforming to the protocol.

struct ActiveEquationString: TextModifier {
    let termInd: Int
    @Binding var currentInd: Int
      
    func body(text: Text) -> some View {
        return text
            .underline(currentInd == termInd)
            .foregroundColor(currentInd == termInd ? Color.black : Color.gray)
    }
}


You can use it in a way very similar to ViewModifier.

struct ContentView: View {
    @State var ind: Int = 1
    
    var body: some View {
        VStack {
            Text("Hello!\(ind)")
                .modifier(ActiveEquationString(termInd: 0, currentInd: $ind))
            Button(action: {
                self.ind = (self.ind + 1) % 3
            }) {
                Text("Change ind")
            }
        }
    }
}


In a simple example as shown above, it works as expected.

An interesting solution.


On the line

func modifier(_ theModifier: TM) -> some View { 

is TM supposed to be TextModifier?


If so, that causes the error "Protocol 'TextModifier' can only be used as a generic constraint because it has Self or associated type requirements"

Sorry, some parts lost when posting the code (due to a bug of this site).


It should be:

extension Text {
    func modifier<TM: TextModifier>(_ theModifier: TM) -> some View {
        return theModifier.body(text: self)
    }
}

Ah yes, an issue I am well acquainted with. I'd recommend updating your original post (which I've marked as the soln) in case others come here.

Personally, I went in a slightly different design direction using a rounded rectangle outline, but thank you all the same.

Thanks for a nice suggestion. Updated.


Can you share your solution? That may be better for others.

@OOPer

It seems like your solution doesn't work when trying to use state logic inside the modifier (which does work with ViewModifier).

An example:

struct HulkSmash: TextModifier {
  @State var isHulk: Bool = false

  func body(text: Text) -> some View {
    text.bold()
      .font(isHulk ? .system(.largeTitle, design: .rounded) : .body)
      .foregroundColor(isHulk ? .init(.systemGreen) : .init(.systemFill))
      .onTapGesture {
        print("tap triggered")
        isHulk.toggle()
      }
  }
}

Here the tap is triggered, however, the view does not update.
Do you have any idea why this doesn't work?

Custom Modifier Using Text's underline(_:color:)
 
 
Q