SwiftUIで縦書きText

# SwiftUIで縦書きを実現するTategakiText

## 動作イメージ
* textを渡すと縦書き表示されます。
* カギかっこや、ビックリやハテナ記号、３点リーダーなども縦書きに合わせた表示になっています。

<img width="1072" alt="スクリーンショット 2020-04-18 19.12.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/73d22ca5-d8b1-416f-0d42-51d39c6b676c.png">

## ソースコード
https://github.com/fuziki/UIKittoView/blob/master/UIKittoView/CoreText/TategakiView.swift

## SwiftUIで電子書籍っぽい動作

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">swiftuiに移植終わった<br>とてもいい感じ <a href="https://t.co/jILnpdbHYp">pic.twitter.com/jILnpdbHYp</a></p>&mdash; ふじき (@fzkqi) <a href="https://twitter.com/fzkqi/status/1251414299139923968?ref_src=twsrc%5Etfw">April 18, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# TategakiText
## TategakiText
* TategakiTextViewのswiftuiのwrapper
* UIViewRepresentableを使って、UIViewのサブクラスをswiftuiで使っている

```swift
public struct TategakiText: UIViewRepresentable {
    public var text: String?

    public func makeUIView(context: Context) -> TategakiTextView {
        let uiView = TategakiTextView()
        uiView.isOpaque = false
        uiView.text = text
        return uiView
    }

    public func updateUIView(_ uiView: TategakiTextView, context: Context) {
        uiView.text = text
    }
}
```

## TategakiTextView
* UIViewのサブクラス。
* CoreTextを使って縦書きでテキストを表示する。

### CoreTextでテキストを表示する
* CTFrameが上下反転表示されるので、CGContextで上下反転させて表示する。

```swift
public class TategakiTextView: UIView {    
    override public func draw(_ rect: CGRect) {
        guard let context:CGContext = UIGraphicsGetCurrentContext() else {
            return
        }
        context.scaleBy(x: 1, y: -1)
        context.translateBy(x: 0, y: -rect.height)

        ctFrame = textからCTFrameを作成する諸々
        
        CTFrameDraw(ctFrame, context)
    }
}
```

### textからCTFrameを作成する諸々
* verticalGlyphFormを有効にしたCTFramesetterを作成する。

```swift
        let baseAttributes: [NSAttributedString.Key : Any] = [
            .verticalGlyphForm: true,
        ]
        let attributedText = NSMutableAttributedString(string: text ?? "", attributes: baseAttributes)
        let setter = CTFramesetterCreateWithAttributedString(attributedText)
```

* CTFrameProgression.rightToLeftの形式でCTFrameを作成する

```swift
        let path = CGPath(rect: rect, transform: nil)
        let frameAttrs = [
            kCTFrameProgressionAttributeName: CTFrameProgression.rightToLeft.rawValue,
        ]
        ctFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, 0), path, frameAttrs as CFDictionary)
```

# 全文

```swift
import UIKit
import CoreText
import SwiftUI

struct TategakiText_Previews: PreviewProvider {
    static var previews: some View {
        TategakiText(text: """
こんにちは、
わたしの名前はふじきです。
これはswiftuiで実装されています。
「どうやって実装してるのか」って？
それはQiitaの記事を読むとわかりますよ！
""").padding(40)
    }
}

public struct TategakiText: UIViewRepresentable {
    public var text: String?

    public func makeUIView(context: Context) -> TategakiTextView {
        let uiView = TategakiTextView()
        uiView.isOpaque = false
        uiView.text = text
        return uiView
    }

    public func updateUIView(_ uiView: TategakiTextView, context: Context) {
        uiView.text = text
    }
}

public class TategakiTextView: UIView {
    public var text: String? = nil {
        didSet {
            ctFrame = nil
        }
    }
    private var ctFrame: CTFrame? = nil
    
    override public func draw(_ rect: CGRect) {
        guard let context:CGContext = UIGraphicsGetCurrentContext() else {
            return
        }
        context.scaleBy(x: 1, y: -1)
        context.translateBy(x: 0, y: -rect.height)
        
        let baseAttributes: [NSAttributedString.Key : Any] = [
            .verticalGlyphForm: true,
            NSAttributedString.Key.font: UIFont.hiraMinProN_W6(size: 25),
        ]
        let attributedText = NSMutableAttributedString(string: text ?? "", attributes: baseAttributes)
        let setter = CTFramesetterCreateWithAttributedString(attributedText)
        let path = CGPath(rect: rect, transform: nil)
        let frameAttrs = [
            kCTFrameProgressionAttributeName: CTFrameProgression.rightToLeft.rawValue,
        ]
        let ct = CTFramesetterCreateFrame(setter, CFRangeMake(0, 0), path, frameAttrs as CFDictionary)
        ctFrame = ct
        
        CTFrameDraw(ct, context)
    }
}
```

# 終わりに
SwiftUIは、SwiftUIを書いていたはずが、気付いたらUIKitを書いていて、複雑な気持ちになりがちです。