Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to fill a UIView with an alternating stripe pattern programmatically using Swift?

I can achieve an alternating stripe pattern fill using an image with UIColor(patternImage: UIImage(named: "pattern.png")).

However, how can I achieve an alternating stripe pattern fill drawing it programmatically with some simple compact code?

Here are two examples I'd like to achieve.

Question

Using code, how do I fill a UIView with different alternating color stripes using Swift?

(1) a two color alternating pattern running top to bottom (90 degrees)?

(2) a three color alternating pattern running top left to bottom right (45 degrees)?

enter image description here

like image 356
user4806509 Avatar asked Aug 27 '16 14:08

user4806509


3 Answers

Filling a view with a stripe pattern with two, three or more color stripes with adjustable stripe widths and rotation can be achieved with the below code. This code gives a three colour stripe pattern shown in the example image below.

Steps:

  1. Add the below code to ViewController.swift
  2. Add a UIView to Storyboard.
  3. Add new alignment contstraints to the UIView on the Storyboard; Horizontally in Container = 0, Vertically in Container = 0.
  4. In the Identity Inspector, set the UIView custom class to 'colorStripesView'.
  5. Connect the UIView on the Storyboard to the viewPattern IBOutlet in the code.

Code:

    ////   ViewController.swift

    //// 1. Add the below code to ViewController.swift
    //// 2. Add a UIView to Storyboard.
    //// 3. Add new alignment contstraints to the UIView on the Storyboard; Horizontally in Container = 0, Vertically in Container = 0.
    //// 4. In the Identity Inspector, set the UIView custom class to 'colorStripesView'.
    //// 5. Connect the UIView on the Storyboard to the viewPattern IBOutlet in the code.

    import UIKit

    class ViewController: UIViewController {
        @IBOutlet weak var viewPattern: UIView!

        override func viewDidLoad() {
            super.viewDidLoad()

            //// Extend width and height constraints by factor of 2 for viewPattern rotation.
            let widthConstraint = NSLayoutConstraint(item: viewPattern, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: (max(view.bounds.height, view.bounds.width)*2))
            viewPattern.addConstraint(widthConstraint)
            let heightConstraint = NSLayoutConstraint(item: viewPattern, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: (max(view.bounds.height, view.bounds.width)*2))
            viewPattern.addConstraint(heightConstraint)

            //// Rotate pattern 0 degrees - vertical.
            //viewPattern.transform = CGAffineTransformMakeRotation(CGFloat(M_PI*0/180))
            //// Rotate pattern 45 degrees - diagonal top right to bottom left.
            //viewPattern.transform = CGAffineTransformMakeRotation(CGFloat(M_PI*45/180))
            //// Rotate pattern 90 degrees - horizontal.
            //viewPattern.transform = CGAffineTransformMakeRotation(CGFloat(M_PI*90/180))
            //// Rotate pattern 135 degrees - diagonal top left to bottom right.
            viewPattern.transform = CGAffineTransformMakeRotation(CGFloat(M_PI*135/180))

            //// Set view color
            viewPattern.backgroundColor = UIColor.clearColor()
        }

        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
        }
    }

    class colorStripesView: UIView {
        override func drawRect(rect: CGRect) {
            //// Set pattern tile colors width and height; adjust the color width to adjust pattern.
            let color1 = UIColor(red: 255/255, green: 255/255, blue: 10/255, alpha: 1.0)
            let color1Width: CGFloat = 10
            let color1Height: CGFloat = 10

            let color2 = UIColor(red: 0/255, green: 0/255, blue: 254/255, alpha: 1.0)
            let color2Width: CGFloat = 10
            let color2Height: CGFloat = 10

            let color3 = UIColor(red: 0/255, green: 128/255, blue: 128/255, alpha: 1.0)
            let color3Width: CGFloat = 10
            let color3Height: CGFloat = 10

            //// Set pattern tile orientation vertical.
            let patternWidth: CGFloat = (color1Width + color2Width + color3Width)
            let patternHeight: CGFloat = min(color1Height, color2Height, color3Height)

            //// Set pattern tile size.
            let patternSize = CGSize(width: patternWidth, height: patternHeight)

            //// Draw pattern tile
            let context = UIGraphicsGetCurrentContext()
            UIGraphicsBeginImageContextWithOptions(patternSize, false, 0.0)

            let color1Path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: color1Width, height: color1Height))
            color1.setFill()
            color1Path.fill()

            let color2Path = UIBezierPath(rect: CGRect(x: color1Width, y: 0, width: color2Width, height: color2Height))
            color2.setFill()
            color2Path.fill()

            let color3Path = UIBezierPath(rect: CGRect(x: color1Width + color2Width, y: 0, width: color3Width, height: color3Height))
            color3.setFill()
            color3Path.fill()

            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()

            //// Draw pattern in view
            UIColor(patternImage: image).setFill()
            CGContextFillRect(context, rect)
        }
    }

Simulator:

enter image description here

like image 73
user4806509 Avatar answered Oct 25 '22 17:10

user4806509


For my application it was more helpful and performant to create a pattern UIColor that can be used as a background color.

Once I add this as an extension to UIColor I can easily put this anywhere in my code and it will always fill perfectly, even as views rotate, shrink and grow!

You use it like this:

view.backgroundColor = UIColor.red.patternStripes()
view.backgroundColor = UIColor.red.patternStripes(color2: .darkGray)
view.backgroundColor = UIColor.red.patternStripes(color2: .darkGray, barThickness: 25.0)

and get this beauty:

Square showing diagonal stripe pattern

Here is an extension to UIColor:

extension UIColor {
    
    /// make a diagonal striped pattern
    func patternStripes(color2: UIColor = .white, barThickness t: CGFloat = 25.0) -> UIColor {
        let dim: CGFloat = t * 2.0 * sqrt(2.0)

        let img = UIGraphicsImageRenderer(size: .init(width: dim, height: dim)).image { context in

            // rotate the context and shift up
            context.cgContext.rotate(by: CGFloat.pi / 4.0)
            context.cgContext.translateBy(x: 0.0, y: -2.0 * t)

            let bars: [(UIColor,UIBezierPath)] = [
                (self,  UIBezierPath(rect: .init(x: 0.0, y: 0.0, width: dim * 2.0, height: t))),
                (color2,UIBezierPath(rect: .init(x: 0.0, y: t, width: dim * 2.0, height: t)))
            ]

            bars.forEach {  $0.0.setFill(); $0.1.fill() }
            
            // move down and paint again
            context.cgContext.translateBy(x: 0.0, y: 2.0 * t)
            bars.forEach {  $0.0.setFill(); $0.1.fill() }
        }
        
        return UIColor(patternImage: img)
    }
}

Basically, I am creating a temporary drawing space for one little rectangle to paint. I paint it with a pattern that will match seamlessly on all sides when repeated. Then I snap an image of this and tell UIColor to use it as a pattern.

The trick I that you have to make your pattern image big enough to paint each bar two times. That turns out to be bar thickness * 2 * sqrt(2).

I know I could get beaten up a little about style. Here are my thoughts:

  1. I used one letter variables like t, which is not very descriptive. My defense: I think it makes the equations more readable and feel like it's okay when it's limited to only the next few lines.
  2. Tuples. Same as #1
  3. Using decimals ( 4.0 instead of just 4). I would prefer to not use the decimals but I have found better compile times with the decimal. On bigger projects it can make a huge difference. I will also admit that it may not read as pretty but being less ambiguous about type can even be helpful to humans.

3 Colors: I just realized that the original question was asking for 3 colors, so here is a version for that...

First, the math:

enter image description here

Updated code:

extension UIColor {
    
    /// make a diagonal striped pattern
    func pattern3Stripes(color2: UIColor, color3: UIColor, barThickness t: CGFloat = 25.0) -> UIColor {
        let sqrt2: CGFloat = sqrt(2.0)
        let dim: CGFloat = t * 3.0 * sqrt2
        let size: CGSize = .init(width: dim, height: dim)
        
        let img = UIGraphicsImageRenderer(size: size).image { context in
            
            // rotate the context and shift up
            context.cgContext.rotate(by: CGFloat.pi / 4.0)
            context.cgContext.translateBy(x: 0.0, y: -3.0 * t)
            
            let bars: [(UIColor,UIBezierPath)] = [
                (self,  UIBezierPath(rect: .init(x: 0.0, y: 0.0, width: dim * sqrt2, height: t))),
                (color2,UIBezierPath(rect: .init(x: 0.0, y: t, width: dim * sqrt2, height: t))),
                (color3,UIBezierPath(rect: .init(x: 0.0, y: 2.0 * t, width: dim * sqrt2, height: t)))
            ]
            
            bars.forEach {  $0.0.setFill(); $0.1.fill() }
            
            // move down and paint again
            context.cgContext.translateBy(x: 0.0, y: 3.0 * t)
            bars.forEach {  $0.0.setFill(); $0.1.fill() }
        }
        
        return UIColor(patternImage: img)
    }
}

Usage:

    view.backgroundColor = UIColor.green.pattern3Stripes(color2: .white, color3: .red, barThickness: 25.0)

Result:

enter image description here

like image 31
garafajon Avatar answered Oct 25 '22 16:10

garafajon


Sure you can.

You might be able to draw your stripes using a Core Image filter, or you could create a custom subclass of UIImage and implement the drawRect method.

EDIT

Using a CIFilter:

I have a sample project called CIFilterTest (link) on Github that lets you try out all different kinds of Core Image Filters. It interrogates the system for a list of available filters and then tries to build a UI for entering the different settings for each filter. It doesn't support some of the more special-purpose inputs, but it will let you try out the CIStripesGenerator, which is the filter that will create alternating stripes for you. The program is written in Objective-C but it should give you the general idea. Note that it looks like the CIStripesGenerator filter will only generate stripes in 2 alternating colors, so you wouldn't be able to use it for your 3-color case.

Using drawRect:

In drawRect you'd create a series of UIBezierPaths and fill them with alternating colors. For the vertical stripes you could use the simple bezierPathWithRect: init method. For the diagonal lines you'd either need to create the horizontal stripes using moveToPoint & lineToPoint, or apply a rotation transform to rectangle paths.

EDIT #2:

Here is a sample class that draws 2 colored rectangles inside itself:

class StripedView: UIView {
  
    override func drawRect(rect: CGRect) {
      
      //Create a rect for the lefthand stripe
      var rect = CGRectInset(bounds, 10, 10)
      rect.size.width /= 2
      var bezier = UIBezierPath(rect: rect) //Create a bezier for the left stripe
      
      //Set the fill color to blue for the first rect
      UIColor.blueColor().setFill()
      bezier.fill()
      
      //Shift the rect over for the righthand stripe
      rect.origin.x = rect.size.width
      bezier = UIBezierPath(rect: rect) //Create a bezier for the right stripe
      
      UIColor.redColor().setFill()
      bezier.fill()
  }
}

You'll need to expand on the code above to create a series of repeating stripes rather than 2 rectangles, and to use 3 rather than 2 colors.

Rotating the stripes 45 degrees is more complicated. Probably the simplest way to do that would be to create diagonal stripes by mapping out the corners of each stripe along the borders of the view and then creating a UIBezierPath for each stripe using moveToPoint and lineToPoint calls. That's beyond the scope of a forum post.

like image 32
Duncan C Avatar answered Oct 25 '22 17:10

Duncan C