Search code examples
iosswiftuitextviewcagradientlayer

Adding a gradient background to a scrollable UITextView


When adding gradients as backgrounds to views in iOS, I employ this method:

let gradient = CAGradientLayer()
// gradient settings...
view.layer.insertSublayer(gradient, at: 0)

However, when doing so on a scrollable UITextView the gradient is drawn only within the visible bounds which then scrolls up and out of view when the text view scrolls up.

Furthermore, as the UITextView has no fixed content size, it's also not possible to set the gradient to that, although that's not desirable either.

One workaround is to place another view behind the scroll view and apply the gradient there, then placing the text view over that and making its background transparent. But this is not a preferred solution.

Q: Is there any way to fix the gradient layer position so that it doesn't scroll with the text layer?


EDIT 1: additional code:

let view = UITextView(frame: CGRect(x: 20, y: 20, width: 200, height: 200))
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = view.bounds
gradient.colors = [gradientColors]  // pre-set array of 2 CGColors
let alpha: Float = angle / 360  // pre-set between 0-90
let startPointX = powf(sinf(2 * Float.pi * ((alpha + 0.75) / 2)), 2)
let startPointY = powf(sinf(2 * Float.pi * ((alpha + 0) / 2)), 2)
let endPointX = powf(sinf(2 * Float.pi * ((alpha + 0.25) / 2)), 2)
let endPointY = powf(sinf(2 * Float.pi * ((alpha + 0.5) / 2)), 2)
gradient.endPoint = CGPoint(x: CGFloat(endPointX), y: CGFloat(endPointY))
gradient.startPoint = CGPoint(x: CGFloat(startPointX), y: CGFloat(startPointY))
view.layer.insertSublayer(gradient, at: 0)

EDIT 2: workaround (working, but gradient visible only after text view receives focus):

extension UITextView {      
  func addGradient(gradient: CAGradientLayer) {
    backgroundColor = UIColor.clear
    let gradientContainer = UIView(frame:
      CGRect(x: frame.minX, y: frame.minY,
             width: frame.width, height: frame.height))
    gradientContainer.isUserInteractionEnabled = false
    gradientContainer.layer.insertSublayer(gradient, at: 0)
    superview?.addSubview(gradientContainer)
  }
}

let view = UITextView(frame: CGRect(x: 20, y: 20, width: 200, height: 200))
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = view.bounds
gradient.colors = [gradientColors]  // pre-set array of 2 CGColors
let alpha: Float = angle / 360  // pre-set between 0-90
let startPointX = powf(sinf(2 * Float.pi * ((alpha + 0.75) / 2)), 2)
let startPointY = powf(sinf(2 * Float.pi * ((alpha + 0) / 2)), 2)
let endPointX = powf(sinf(2 * Float.pi * ((alpha + 0.25) / 2)), 2)
let endPointY = powf(sinf(2 * Float.pi * ((alpha + 0.5) / 2)), 2)
gradient.endPoint = CGPoint(x: CGFloat(endPointX), y: CGFloat(endPointY))
gradient.startPoint = CGPoint(x: CGFloat(startPointX), y: CGFloat(startPointY))

view.addGradient(gradient)

EDIT 3: creative workaround by Carpsen90 (thank you!):

class ViewController: UIViewController, UITextViewDelegate {

  @IBOutlet weak var myTxtView: UITextView!
  var gradient = CAGradientLayer()

  override func viewDidLoad() {
    super.viewDidLoad()
    myTxtView.delegate = self
    gradient.colors = [UIColor.red.cgColor, UIColor.yellow.cgColor]
    updateGradientFrame()
    myTxtView.textInputView.layer.insertSublayer(gradient, at: 0)
  }

  func textViewDidChange(_ textView: UITextView) {
    updateGradientFrame()
  }

  func updateGradientFrame() {
    gradient.frame = myTxtView.textInputView.bounds
  }

}

This works quite well except:

i. slight flickering caused by the gradient-refresh when typing

ii. carriage returns past the bottom of the text view sometimes cause the gradient to be displaced upwards resulting in a blank space at the bottom of the text view

iii. after typing past the bounds and having scrolled down beyond the text view bounds, the gradient remains affixed at the bottom of the contentsize area when scrolling up to the top of the text view; apparently scrolling does not trigger the textViewDidChange() delegate method.


Solution

  • Here is a workaround: Make the VC observe the changes in the textView, and update the gradient frame accordingly:

    class ViewController: UIViewController, UITextViewDelegate {
    
        @IBOutlet weak var myTxtView: UITextView!
    
        var gradient = CAGradientLayer()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            //set the delegate to self
            myTxtView.delegate = self
            //set up the gradient
            gradient.colors = [UIColor.red.cgColor, UIColor.yellow.cgColor]
            updateGradientFrame()
            //insert the gradient in the textInputView
            myTxtView.textInputView.layer.insertSublayer(gradient, at: 0)
        }
    
        //The textView delegate method
        func textViewDidChange(_ textView: UITextView) {
            updateGradientFrame()
        }
    
        func updateGradientFrame() {
            gradient.frame = myTxtView.textInputView.bounds
        }
    }