UITextViewの一部のテキストに背景色を角丸でつける

UITextViewの一部のテキストに背景色を角丸でつける方法です。

### 背景色をつける

[UITextView](https://developer.apple.com/documentation/uikit/uitextview) のテキストの一部に背景色をつけたい場合は、[NSAttributedString](https://developer.apple.com/documentation/foundation/nsattributedstring) + [NSAttributedString.Key.backgroungColor](https://developer.apple.com/documentation/foundation/nsattributedstring/key/1528791-backgroundcolor) 属性を使います。

```swift
let text = "あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら、..."
let attributedText = NSMutableAttributedString(string: text)
attributedText.addAttributes([.backgroundColor: UIColor.cyan], range: NSRange(location: 2, length: 7))

let textView = UITextView()
textView.attributedText = attributedText
```

![スクリーンショット 2021-01-09 22.18.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/8501db22-9e04-b306-04d1-d17dfdd37b8b.png)

### 背景色を角丸でつける

この背景色の部分に角丸をつけたい場合は、[NSLayoutManager](https://developer.apple.com/documentation/uikit/nslayoutmanager) の [fillBackgroundRectArray](https://developer.apple.com/documentation/uikit/nslayoutmanager/1403161-fillbackgroundrectarray) メソッドをオーバーライドして背景を角丸で描画するカスタムクラスを作り、UITextViewに適用します。

```swift:TextLayout.swift
import UIKit

class LayoutManager: NSLayoutManager {

    var backgroundCornerRadius: CGFloat = 0
    
    override func fillBackgroundRectArray(_ rectArray: UnsafePointer<CGRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: UIColor) {
        guard let context = UIGraphicsGetCurrentContext() else {
            super.fillBackgroundRectArray(rectArray, count: rectCount, forCharacterRange: charRange, color: color)
            return
        }
        context.saveGState()
        defer { context.restoreGState() }
        context.setFillColor(color.cgColor)
        
        for rectIndex in 0..<rectCount {
            let rect = rectArray.advanced(by: rectIndex).pointee
            let path = UIBezierPath(roundedRect: rect, cornerRadius: backgroundCornerRadius)
            context.addPath(path.cgPath)
            context.fillPath()
        }
    }
}
```

```swift
let layoutManager = LayoutManager()
layoutManager.backgroundCornerRadius = 5

let textView = UITextView()
textView.textContainer.replaceLayoutManager(layoutManager)
```

![スクリーンショット 2021-01-09 21.33.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/c4d9289d-e5fe-f62f-7694-309c3ce0b76e.png)

### 背景色を角丸でつける（改行も考慮する）

上記の方法の場合、背景色のつくテキストが改行されると次のように折り返しの部分も角丸になってしまいます。

![スクリーンショット 2021-01-09 21.44.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/46a22697-e35a-8ead-d3ee-3b1c0c9e404c.png)

そこで、改行も考慮して fillBackgroundRectArray メソッドを実装してみます。実装のポイントは、

- fillBackgroundRectArray メソッドはテキスト内の backgroundColor 属性ごとに呼び出される
- 一つの backgroundColor 属性に対して、テキストが改行される場合は、入力の矩形 `rectArray`, `rectCount` が複数になる
- 複数の矩形がある場合は、最初の矩形は左側のみ角丸をつけ、最後の矩形は右側のみ角丸をつけ、それ以外は角丸をつけない

という感じです。

```swift:TextLayout.swift
class LayoutManager: NSLayoutManager {

    var backgroundCornerRadius: CGFloat = 0
    
    override func fillBackgroundRectArray(_ rectArray: UnsafePointer<CGRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: UIColor) {
        guard let context = UIGraphicsGetCurrentContext() else {
            super.fillBackgroundRectArray(rectArray, count: rectCount, forCharacterRange: charRange, color: color)
            return
        }
        context.saveGState()
        defer { context.restoreGState() }
        context.setFillColor(color.cgColor)
        
        let cornerRadii = CGSize(width: backgroundCornerRadius, height: backgroundCornerRadius)
        for rectIndex in 0..<rectCount {
            let rect = rectArray.advanced(by: rectIndex).pointee

            let corners: UIRectCorner
            if rectCount == 1 {
                corners = .allCorners
            } else if rectIndex == 0 {
                corners = [.topLeft, .bottomLeft]
            } else if rectIndex == rectCount - 1 {
                corners = [.topRight, .bottomRight]
            } else {
                corners = []
            }

            let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: cornerRadii)
            context.addPath(path.cgPath)
            context.fillPath()
        }
    }
}
``` 

![スクリーンショット 2021-01-09 21.51.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/6223aeff-6466-0639-6f0a-2ba28a83859f.png)

いい感じになりました :tada:

### :warning: 問題点 : UITextView のスクロールが無効の場合にクラッシュする

今回のようにカスタマイズした NSLayoutManager を適用した UITextView のスクロールが無効 (`isScrollEnabled = false`) になっていると、UITextView の描画時にクラッシュしてしまいます。

[rdar://14568899 (Turning off text view scrolling crashes layout manager)](http://www.openradar.appspot.com/14568899)

この問題の解消方法が今のところわかっていません :cry: