Gradient as foreground color of Text in SwiftUI
Asked Answered
D

7

18

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

Thanks for the answers in advance!

Disappear answered 22/11, 2019 at 9:35 Comment(1)
R
11

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

Text with a gradient

Text(“Gradient”)
    .foregroundStyle(
        .linearGradient(
            colors: [.red, .blue],
            startPoint: .top,
            endPoint: .bottom
        )
    )
Rubinrubina answered 27/12, 2021 at 13:27 Comment(1)
Only for iOS 15 & above.Catenary
P
47

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)
    }
}
Pilfer answered 22/11, 2019 at 12:27 Comment(4)
Not working in widgets. Widgets only work with pure Swift.Puton
@MojtabaHosseini. I updated my ans can you please check. let me know works or not ?Pilfer
No, Widgets work ONLY with pure swift. It turns white if when you use UIViewRepresentablePuton
Nice update and works great, love the extension!Denial
S
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

Synergetic answered 22/11, 2019 at 21:46 Comment(1)
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.Eastsoutheast
E
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

Etom answered 22/1, 2020 at 10:54 Comment(4)
This is so complex for such a simple task. SwiftUI already supports this and I think A simple extension would be enoughPuton
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...Etom
Reasonable, but the extension version (right above usage example) will work anyway 🙂Etom
Also as far as I understand you actually can use custom views, so even a _CustomForegound example should work developer.apple.com/videos/play/wwdc2020/10033. Also, both our approaches use pure SwiftUI btw 🌚Etom
R
11

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

Text with a gradient

Text(“Gradient”)
    .foregroundStyle(
        .linearGradient(
            colors: [.red, .blue],
            startPoint: .top,
            endPoint: .bottom
        )
    )
Rubinrubina answered 27/12, 2021 at 13:27 Comment(1)
Only for iOS 15 & above.Catenary
P
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

Puton answered 17/9, 2020 at 8:5 Comment(0)
D
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

Denial answered 19/11, 2021 at 5:46 Comment(0)
L
-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

Lynettelynn answered 22/11, 2019 at 10:9 Comment(1)
Sadly, I've tried it before and it just gives you an error, but thanks for the effort!Disappear

© 2022 - 2024 — McMap. All rights reserved.