0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Result BuilderでSwiftUIのようなオリジナル文法を作成しよう

Posted at

Result Builderは、元となる子要素を組み合わせ、何か一つの親要素を作り上げるのに非常に便利な文法です。
この方法を用いれば、SwiftUIのような書き方で様々な子要素の組み合わせコンポーネントを作成できるようになります。ここではそのメリット、具体的な使い方について説明します。

Result Builderのメリット

例えば、このような画面を表示したいとします。

今はSwiftUIで便利に実装できる部分もありますが、これをNSAttributeStringで丁寧に実装しようとすると、以下のようなコードを書く必要があるかと思われます。

private var richText2: NSAttributedString {
    let formerAttributeString = NSMutableAttributedString(string: "We like ")
    let latterAttributedString = NSMutableAttributedString(string: "swift")
    latterAttributedString.addAttribute(NSAttributedString.Key.underlineStyle,
                                        value: 1,
                                        range: .init(location: 0,
                                                     length: latterAttributedString.length))
    formerAttributeString.append(latterAttributedString)
    return formerAttributeString
}

これがもし、resultBuilderを使った場合、このように書くことができます。

@RichTextBuilder
private var richText: NSAttributedString {
    RichText(string: "We like ")
    RichText(string: "swift")
        .lined()
}

このように可読性が非常に高いカスタムコードが書けることこそ、ResultBuilderの最大のメリットです。

Result Builderを使ってみる

Result Builderは以下のように、enum@resultBuilder修飾子をつけることで使用することが出来るようになります

@resultBuilder
enum RichTextBuilder {
    static func buildBlock(_ components: NSAttributedString...) -> NSAttributedString {
        let attributedString = NSMutableAttributedString()

        for component in components {
            attributedString.append(component)
        }

        return attributedString
    }
}

こちらを見ていただくと、個数制限のないcomponentを引数に取り、最終的に一つのアウトプットを出すようなコードになっていることがわかると思います。
さらにここから可読性を上げていくため、typealiasextensionメソッドを駆使して、さらにSwiftUIのようなシンプルで可読性の高いコードに仕上げていきます。

typealias RichText = NSMutableAttributedString

private extension NSMutableAttributedString {
    func lined() -> NSMutableAttributedString {
        self.addAttribute(NSAttributedString.Key.underlineStyle,
                          value: 1,
                          range: .init(location: 0,
                                       length: self.length))
        return self
    }
}

以上の二つのコードを追加することで、上記に例示したテキストを、以下のようなコードで表現できるようになります。

@RichTextBuilder
private var richText: NSAttributedString {
    RichText(string: "We like ")
    RichText(string: "swift")
        .lined()
}

Result Builderの応用編

例えば、配列をループさせて値を代入し、特定の場合のみ下線を引く、のような処理を実現したい場合を考えてみます。

完成予想図

まず、配列をループさせるために、result builderに新たな関数を追加します。

private let langs = ["Ruby", "PHP", "Golang", "Swift"]
@RichTextBuilder
private var richText: NSAttributedString {
    for lang in langs {
        RichText(string: "We like ")
        RichText(string: lang)
            .lined()
    }
}

@resultBuilder
enum RichTextBuilder {
    ...
    // 追加
    static func buildArray(_ components: [NSAttributedString]) -> NSAttributedString {
        let attributedString = NSMutableAttributedString()

        for component in components {
            attributedString.append(component)
        }

        return attributedString
    }
}

result builder中にarrayが入った場合の処理をここでは記述しています。
さらに、swiftの場合でのみ下線を引く、のような条件分岐をする際には、以下のようなコードを記述します。

private let langs = ["Ruby", "PHP", "Golang", "Swift"]
@RichTextBuilder
private var richText: NSAttributedString {
    for lang in langs {
        RichText(string: "We like ")
        if lang == "Swift" {
            RichText(string: lang)
                .lined()
        } else {
            RichText(string: lang)
        }
    }
}

@resultBuilder
enum RichTextBuilder {
    ...
    // 二つの関数を追加
    static func buildEither(first component: NSAttributedString) -> NSAttributedString {
        return component
    }

    static func buildEither(second component: NSAttributedString) -> NSAttributedString {
        return component
    }
}

ここで関数を二つ追加しているのは、firstがif-elseのifがtrueの場合、secondがif-elseのifがfalseの場合(つまり、elseに行く場合)の処理を示しており、その両方に対して対処できるようにするためです。

これでほぼ完成ですが、このままだと全ての文章が繋がって見えてしまうため、適切に改行やカンマ、ピリオドを打っていきたいと思います。

句読点を意味する、Punctuationsを以下のように定義します。

enum Punctuations {
    case period
    case comma
}

そして、こちらをNSAttributeStringへ変換できるようにするため、最終的に以下のようなコードを追加します。

private let langs = ["Ruby", "PHP", "Golang", "Swift"]
@RichTextBuilder
private var richText: NSAttributedString {
    for lang in langs {
        RichText(string: "We like ")
        if lang == "Swift" {
            RichText(string: lang)
                .lined()
            Punctuations.period
        } else {
            RichText(string: lang)
            Punctuations.comma
        }
    }
}

@resultBuilder
enum RichTextBuilder {
    ...
    // 追加
    static func buildExpression(_ expression: Punctuations) -> NSAttributedString {
        switch expression {
            case .period:
                return RichText(".\n")
            case .comma:
                return RichText(",\n")
        }
    }

    // 追加(上記を実装すると、builder内の全てのコンポーネントがPunctuationsとして扱われてしまうため、
    // ここでNSMutableAttributeStringも処理対象であることを明示する。)
    static func buildExpression(_ expression: NSMutableAttributedString) -> NSAttributedString {
        return expression
    }
}

これで、上記の例に示したようなレイアウトを、SwiftUIのような書き方で書けるようになりました。

まとめ

  • Result Builderによってカスタムの言語仕様を作ることが出来るようになる。
  • 完成したResult Builderはcomputed property、関数、クロージャに使うことができる。
  • buildBlock()を定義することで、子要素を組み合わせたコンポーネントを可読性高く作成できる。
  • typealiasextensionを作成することでSwiftUIのような書き方が可能になり、さらに可読性が上げられる。
  • buildEither(first:)/buildEither(second:), buildArray(), buildExpression()を用いてループや条件分岐、型に対応することが出来るようになる。

最後に

こちらは私が書籍で学んだ学習内容をアウトプットしたものです。
わかりにくい点、間違っている点等ございましたら是非ご指摘お願いいたします。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?