17

Is there any way of using a gradient as foregroundColor of Text in SwiftUI?

Thanks for the answers in advance!

LuLuGaGa
  • 13,089
  • 6
  • 49
  • 57
wictorious
  • 851
  • 3
  • 10
  • 24

7 Answers7

46

I have updated my answer with new answer, you can try with that. Old one Answer is still available.

New Answer

import SwiftUI

struct GradientText: View {
    var body: some View {
        Text("Gradient foreground")
            .gradientForeground(colors: [.red, .blue])
            .padding(.horizontal, 20)
            .padding(.vertical)
            .background(Color.green)
            .cornerRadius(10)
            .font(.title)
       }
}

extension View {
    public func gradientForeground(colors: [Color]) -> some View {
        self.overlay(
            LinearGradient(
                colors: colors,
                startPoint: .topLeading,
                endPoint: .bottomTrailing)
        )
            .mask(self)
    }
}

Output

enter image description here


Old Answer

In SwiftUI You can also do it, as below using concept of Add gradient color to text

GradientView :

struct GradientView: View {
    var body: some View {
        VStack {
            GradientLabelWrapper(width: 150) //  you can give as you want
                .frame(width: 200, height: 200, alignment: .center) // set frame as you want
        }
    }
}

GradientLabelWrapper :

struct GradientLabelWrapper: UIViewRepresentable {

    var width: CGFloat
    var text: String?
    typealias UIViewType = UIView
    
    func makeUIView(context: UIViewRepresentableContext<GradientLabelWrapper>) -> UIView {
    
        let label = UILabel()
        label.lineBreakMode = .byWordWrapping
        label.numberOfLines = 0
        label.preferredMaxLayoutWidth = width
        label.text = text ?? ""
        label.font = UIFont.systemFont(ofSize: 25) //set as you need
        label.applyGradientWith(startColor: .red, endColor: .blue)
        return label
    }

    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<GradientLabelWrapper>) {
    }
} 

UILabel : Extension

extension UILabel {

    func applyGradientWith(startColor: UIColor, endColor: UIColor) {
        
        var startColorRed:CGFloat = 0
        var startColorGreen:CGFloat = 0
        var startColorBlue:CGFloat = 0
        var startAlpha:CGFloat = 0
        
        if !startColor.getRed(&startColorRed, green: &startColorGreen, blue: &startColorBlue, alpha: &startAlpha) {
            return
        }
        
        var endColorRed:CGFloat = 0
        var endColorGreen:CGFloat = 0
        var endColorBlue:CGFloat = 0
        var endAlpha:CGFloat = 0
        
        if !endColor.getRed(&endColorRed, green: &endColorGreen, blue: &endColorBlue, alpha: &endAlpha) {
            return
        }
        
        let gradientText = self.text ?? ""
        
        let textSize: CGSize = gradientText.size(withAttributes: [NSAttributedString.Key.font:self.font!])
        let width:CGFloat = textSize.width
        let height:CGFloat = textSize.height
        
        UIGraphicsBeginImageContext(CGSize(width: width, height: height))
        
        guard let context = UIGraphicsGetCurrentContext() else {
            UIGraphicsEndImageContext()
            return
        }
        
        UIGraphicsPushContext(context)
        
        let glossGradient:CGGradient?
        let rgbColorspace:CGColorSpace?
        let num_locations:size_t = 2
        let locations:[CGFloat] = [ 0.0, 1.0 ]
        let components:[CGFloat] = [startColorRed, startColorGreen, startColorBlue, startAlpha, endColorRed, endColorGreen, endColorBlue, endAlpha]
        rgbColorspace = CGColorSpaceCreateDeviceRGB()
        glossGradient = CGGradient(colorSpace: rgbColorspace!, colorComponents: components, locations: locations, count: num_locations)
        let topCenter = CGPoint.zero
        let bottomCenter = CGPoint(x: 0, y: textSize.height)
        context.drawLinearGradient(glossGradient!, start: topCenter, end: bottomCenter, options: CGGradientDrawingOptions.drawsBeforeStartLocation)
        
        UIGraphicsPopContext()
        
        guard let gradientImage = UIGraphicsGetImageFromCurrentImageContext() else {
            UIGraphicsEndImageContext()
            return
        }
        
        UIGraphicsEndImageContext()
        self.textColor = UIColor(patternImage: gradientImage)
    }
}
aheze
  • 24,434
  • 8
  • 68
  • 125
Rohit Makwana
  • 4,337
  • 1
  • 21
  • 29
41

This can be easily done in pure SwiftUI without using UIViewRepresentable. You need to mask a gradient with your text:

LinearGradient(gradient: Gradient(colors: [.pink, .blue]),
               startPoint: .top,
               endPoint: .bottom)
    .mask(Text("your text"))

enter image description here

LuLuGaGa
  • 13,089
  • 6
  • 49
  • 57
  • 9
    Unfortunately, perhaps, the `LinearGradient` view is greedy and uses all available space. That's why your sample shows it flush with the top of the screen and not in the center as a simple `Text` alone would be. Also, the greedy gradient runs the full height of the space it takes, but the mask is only using a bit at the top. That is why you don't see a full blue at the bottom. – Bob Peterson Jan 16 '20 at 20:06
18

I guess that should help. Works with text, images and any other views.

import SwiftUI

// MARK: - API
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension View {
    public func foreground<Overlay: View>(_ overlay: Overlay) -> some View {
        _CustomForeground(overlay: overlay, for: self)
    }
}

// MARK: - Implementation
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
private struct _CustomForeground<Content: View, Overlay: View>: View {
    let content: Content
    let overlay: Overlay
    
    internal init(overlay: Overlay, for content: Content) {
        self.content = content
        self.overlay = overlay
    }
    
    var body: some View {
        content.overlay(overlay).mask(content)
    }
}

Personaly I like that approach the most. But also you can combine it into:

import SwiftUI

// MARK: - API
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension View {
    public func foreground<Overlay: View>(_ overlay: Overlay) -> some View {
        self.overlay(overlay).mask(self)
    }
}

Usage example

// MARK: - Example
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
struct GradientTextDemo: View {
    var body: some View {
        Text("Gradient foreground")
            .foreground(makeGradient())
            .padding(.horizontal, 32)
            .padding(.vertical)
            .background(Color.black)
            .cornerRadius(12)
    }
    
    func makeGradient() -> some View {
        LinearGradient(
            gradient: .init(colors: [.red, .orange]),
            startPoint: .topLeading,
            endPoint: .bottomTrailing
        )
    }
}

enter image description here

See gist

maximkrouk
  • 249
  • 2
  • 6
  • 1
    This is so complex for such a simple task. SwiftUI already supports this and I think [A **simple** extension would be enough](https://stackoverflow.com/a/63933797/5623035) – Mojtaba Hosseini Sep 17 '20 at 08:08
  • 1
    Don't think my implementation is complicated and I still like the API Pros: Logic is isolated into a view with explicit namin some foreground view is returned, not just a ZStack; No sizing stuff, just masks involved; Clear naming for the method itself - `foreground`; `@available` attributes, so you can still support any OS; An extension version, which is as short as yours; Gist (i like gists). And I do not see cons But worth mentioning that our logic is similar, so any implementation is better than other top answers in this topic. But still not top liked... – maximkrouk Sep 17 '20 at 21:45
  • Reasonable, but the `extension` version (right above usage example) will work anyway – maximkrouk Sep 17 '20 at 21:49
  • Also as far as I understand you actually can use custom views, so even a `_CustomForegound` example should work https://developer.apple.com/videos/play/wwdc2020/10033/. Also, both our approaches use pure SwiftUI btw – maximkrouk Sep 17 '20 at 22:01
10

You can assign any gradient or other type of view as a self-size mask like:

Text("Gradient is on FIRE !!!")
    .selfSizeMask(
        LinearGradient(
            gradient: Gradient(colors: [.red, .yellow]),
            startPoint: .bottom,
            endPoint: .top)
    )

with this simple tiny extension:

extension View {
    func selfSizeMask<T: View>(_ mask: T) -> some View {
        ZStack {
            self.opacity(0)
            mask.mask(self)
        }.fixedSize()
    }
}

Demo


Bonus 1

You can apply it on any sort of view:

Bonus 1


Bonus 2

Also, you can apply all gradients or any sort of view on it:

Bonus 2

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
10

New in SwiftUI: modifier .foregroundStyle() (iOS15+)

Text with a gradient

Text(“Gradient”)
    .foregroundStyle(
        .linearGradient(
            colors: [.red, .blue],
            startPoint: .top,
            endPoint: .bottom
        )
    )
ramzesenok
  • 5,469
  • 4
  • 30
  • 41
2

It would make sense to create this as a TextStyle, like LabelStyle and ButtonStyle, but strangely SwiftUI left Text out of the styling modifiers for some reason. A custom one could be created going forward until SwiftUI releases an API (?):

protocol TextStyle: ViewModifier {}

extension View {
    func textStyle<T: TextStyle>(_ modifier: T) -> some View {
        self.modifier(modifier)
    }
}

With this in place, custom text styles can be created (overlay/mask technique inspired by accepted answer):

struct LinearGradientTextStyle: TextStyle {
    let colors: [Color]
    let startPoint: UnitPoint
    let endPoint: UnitPoint

    func body(content: Content) -> some View {
        content
            .overlay(
                LinearGradient(
                    colors: colors,
                    startPoint: .topLeading,
                    endPoint: .bottomTrailing
                )
            )
            .mask(content)
    }
}

extension TextStyle where Self == LinearGradientTextStyle {
    static func linearGradient(
        _ colors: [Color],
        startPoint: UnitPoint = .top,
        endPoint: UnitPoint = .bottom
    ) -> Self {
        LinearGradientTextStyle(
            colors: colors,
            startPoint: startPoint,
            endPoint: endPoint
        )
    }
}

Then you can use it the same way you would use LabelStyle or ButtonStyle:

Text("This has a custom linear gradient mask")
    .textStyle(.linearGradient([.red, .purple, .blue, .yellow]))

enter image description here

TruMan1
  • 33,665
  • 59
  • 184
  • 335
-3

You can use this to have gradient as foreground color of your Text:

Text("Hello World")
                .padding()
                .foregroundColor(.white)
                .background(LinearGradient(gradient: Gradient(colors: [.white, .black]), startPoint: .top, endPoint: .bottom))

Hope this helps :) you can also use this link for your reference: https://www.hackingwithswift.com/quick-start/swiftui/how-to-render-a-gradient

Aira Samson
  • 226
  • 2
  • 10