[Objective-C] CGGradientを使って動的にグラデーションを作る


軽く調べてみたところ、CGGradientを使う方法が基本的なのかな、という印象です。
（単純にレイヤーに表示するだけなら`CAGradientLayer`を使うのが簡単そうですが）

今回調べたかったのは、すでに存在する画像と合成する目的でグラデーションを作りたかったのでCGGradientで調べました。
っていうところまで書いて、`CAGradientLayer`をUIImageViewなりに重ねて、さらにそれをキャプチャしてやればよかったんじゃ、とか思いましたが低レイヤーでの作り方はやっぱり知っておいたほうがいいと思うのでまとめておこうと思います。


## コード

なにはともあれ、コードを見てもらったほうが早いと思うのでコードを。

```objc:create-gradient-using-cg-interface
// インターフェース
- (UIImage *)createGradientImage:(CGSize)size colors:(NSArray *)colors locations:(NSArray *)locations isVertical:(BOOL)isVertical;
```

```objc:create-gradient-using-cg
// Color spaceを作る
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

// 成分ごとのビット数
size_t bitsPerComponent = 8;

// 行ごとのバイト数
size_t bytesPerRow      = size.width * 4;

// BitmapContextとしてコンテキストを生成
CGContextRef context = CGBitmapContextCreate(NULL, size.width, size.height, bitsPerComponent, bytesPerRow, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedFirst);

// 引数で渡されたロケーション数
const size_t length     = locations.count;

// 色の成分数
const size_t components = 4;

// ロケーション配列用にメモリを確保
CGFloat *locationsRef = malloc(sizeof(CGFloat) * length);

// カラー配列用にメモリを確保
CGFloat *colorsRef    = malloc(sizeof(CGFloat) * length * components);

// 引数で渡されたロケーションを`CGFloat`の配列に保持
for (NSInteger i = 0; i < length; i++) {
    locationsRef[i] = [locations[i] floatValue];
}

// 引数で渡されたカラー情報を`CGFloat`の配列で保持
for (NSInteger i = 0; i < length; i++) {
    // 渡されたUIColorから色成分を取り出す
    const CGFloat *colorComponents = CGColorGetComponents([colors[i] CGColor]);
    
    colorsRef[(i * components) + 0] = colorComponents[0];
    colorsRef[(i * components) + 1] = colorComponents[1];
    colorsRef[(i * components) + 2] = colorComponents[2];
    colorsRef[(i * components) + 3] = colorComponents[3];
}

// グラデーションのスタート／エンド位置を決める
CGPoint startPoint = CGPointZero;
CGPoint endPoint   = CGPointZero;

if (isVertical) {
	// 座標空間が逆？のため、下から上を指定すると意図通りに描画される
    const CGFloat halfWidth = size.width / 2.0;
    startPoint = CGPointMake(halfWidth, size.height);
    endPoint   = CGPointMake(halfWidth, 0.0);
}
else {
    const CGFloat halfHeight = size.height / 2.0;
    startPoint = CGPointMake(0.0,        halfHeight);
    endPoint   = CGPointMake(size.width, halfHeight);
}

// セットアップした情報でグラデーションを描画
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, colorsRef, locationsRef, length);
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
CGImageRef cgImage = CGBitmapContextCreateImage(context);
UIImage *image = [UIImage imageWithCGImage:cgImage];

// 解放
CGImageRelease(cgImage);
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
free(colorsRef);
free(locationsRef);

return image;
```

実際書いてみると、Objective-Cらしさはほぼありませんｗ
できるだけマジックナンバーを避けて書いたので、コードを見るだけである程度分かるかと思います。

### 使い方

```objc:how-to-use
UIColor *startColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.5];
UIColor *endColor   = [UIColor colorWithRed:0.0 green:0.0 blue:1.0 alpha:0.1];
UIImage *gradient = [self createGradientImage:CGSizeMake(200, 300)
                                       colors:@[startColor, endColor]
                                    locations:@[@0.0, @1.0]
                                   isVertical:YES];
UIImageView *view = [[UIImageView alloc] initWithImage:gradient];
self.view.backgroundColor = UIColor.blackColor;
```


### レンダリング結果

実際に上記を実行すると以下のような見た目になります。

![スクリーンショット](https://qiita-image-store.s3.amazonaws.com/0/5616/ec85cfd8-b744-e1fe-1987-1fd082e3438f.png "スクリーンショット")