UIViewControllerの一部だけをSwiftUIで構築した話

# 前書き

[ACCESS Advent Calender](https://qiita.com/advent-calendar/2021/access) 今年の1日目は@tonionagauzziです。

1月に娘が生まれて、コロナ禍での育児1年生でしたが、会社や周囲の手厚いサポートによって無事に過ごせました。この場を借りて、1年間お世話になった皆さんにお礼を申し上げます。

そしてようやく念願叶い、両家に顔見せに行った帰りの飛行機で、明後日投稿日じゃん！って気づいてスマホで記事書いてます。便利な時代になりましたね！

# やったこと

UIKitで書かれた歴史の長いiOSアプリがあります。最近、その Deployment Target が13.0に上がったので、**UIViewControllerの一部だけ**にSwiftUIを導入しました。

また、SwiftUIで、URLは青下線表示で押したらSafariが開くようにしました。

![名称未設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277107/4cc0282b-b615-3df7-4a21-a19097d9d430.png)
このような Key-Delimiter-Value 形式のViewです。

SwiftUIを導入した狙いは、

1. 要素数（縦の行数）が不定なので、SwiftUIの`ForEach`で手軽に可変にしたい
2. UIKitと比べて20%以下のコード量で同じ内容を記述できるので、開発効率を上げたい
3. 単純なViewでデザイン制約がないので、SwiftUI導入のきっかけ作りをしたい

でした。

上に貼ったスクショはプレビューなので、実際は上下に従来のUIKitのパーツが並んでいる想定です。

# 説明

## 1. UIViewController上の一部だけをSwiftUIにする

まず、URLは置いといて、UIViewControllerにSwiftUIを埋め込む部分のコードを載せます。

ポイントは3つです。

1. `UIHostingController`にSwiftUI Viewを紐付け、UIKitのViewControllerに`addChild`する
2. SwiftUI Viewを空のUIViewに`addSubView`する
3. 2の親子が密接するようConstraintを設定する

こう書くと面倒くさそうですがコード量は大したことないです。

```Swift:Item.swift
struct Item: Hashable {
    var name: String
    var value: String
}
```

```Swift:ExtraView.siwft
import SwiftUI

let ITEM_LIST_FOR_PREVIEW = [
    Item(name: "名前", value: "tonionagauzzi"),
    Item(name: "SNS", value: "https://twitter.com/tonionagauzzi"),
    Item(name: "Motto", value: "You can decide you're happy or not.")
]

struct ExtraView: View {
    var itemList: [Item] = []
    
    @ViewBuilder
    var body: some View {
        GeometryReader { geometry in
            VStack {
                ForEach(itemList, id: \.self) { item in
                    HStack(alignment: .top) {
                        Text(item.name)
                            .frame(maxWidth: geometry.size.width * 0.12, alignment: .leading)
                        Text("：")
                        Text(item.value)
                            .frame(maxWidth: .infinity, alignment: .leading)
                    }.padding(.bottom, 1)
                }
            }
            .padding(.horizontal, 10)
        }
    }
}

struct ExtraView_Previews: PreviewProvider {
    static var previews: some View {
        ExtraView(
            itemList: ITEM_LIST_FOR_PREVIEW
        )
    }
}
```

```Swift:MyViewController.swift
import SwiftUI

class MyViewController: UIViewController {
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        // 省略

        let extraViewController: UIHostingController<ExtraView> = UIHostingController(
            rootView: ExtraView(
                itemList: itemList
            )
        )
        addChild(extraViewController)

        // extraView は、あらかじめ Xib/Storyboard で空の UIView として Auto Layout 配置しておく
        extraView.addSubview(extraViewController.view)
        extraViewController.didMove(toParent: self)

        extraViewController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            extraViewController.view.widthAnchor.constraint(
                equalTo: extraView.widthAnchor,
                multiplier: 1
            ),
            extraViewController.view.heightAnchor.constraint(
                equalTo: extraView.heightAnchor,
                multiplier: 1
            ),
            extraViewController.view.centerXAnchor.constraint(
                equalTo: extraView.centerXAnchor
            ),
            extraViewController.view.centerYAnchor.constraint(
                equalTo: extraView.centerYAnchor
            )
        ])
    }
)
```

`didMove`は忘れがちですが、処理の終了を通知するもので、しないと`viewWillAppear`などライフサイクルに関係するメソッドが呼ばれなくなる可能性があるので、忘れないようにしましょう。

## 2. URLはClickableにする

そしてURL対応。リンクを検出したら青文字・下線付きにして押せるようにしたいので、ExtraViewを以下のように作り変えました。

```Swift:ExtraView.swift
struct ExtraView: View {
    var itemList: [Item] = []
    
    @ViewBuilder
    var body: some View {
        GeometryReader { geometry in
            VStack {
                ForEach(itemList, id: \.self) { item in
                    HStack(alignment: .top) {
                        Text(item.name)
                            .frame(maxWidth: geometry.size.width * 0.12, alignment: .leading)
                        Text("：")
                        if item.isUrl {
                            Button(action: item.actionUrl) {
                                Text(item.value)
                                    .underline()
                                    .foregroundColor(Color.blue)
                            }
                            .buttonStyle(PlainButtonStyle())
                            .frame(maxWidth: .infinity, alignment: .leading)
                        } else {
                            Text(item.value)
                                .frame(maxWidth: .infinity, alignment: .leading)
                        }
                    }.padding(.bottom, 1)
                }
            }
            .padding(.horizontal, 10)
        }
    }
}

struct ExtraView_Previews: PreviewProvider {
    static var previews: some View {
        ExtraView(
            itemList: ITEM_LIST_FOR_PREVIEW
        )
    }
}

extension Item {
    private var url: URL? {
        return URL(string: self.value)
    }
    fileprivate var isUrl: Bool {
        if let url = url {
            return UIApplication.shared.canOpenURL(url)
        }
        return false
    }
    fileprivate var actionUrl: () -> () {
        return {
            if let url = url {
                UIApplication.shared.open(url)
            }
        }
    }
}
```

変えたのは、`if item.isUrl`で開けるURLかどうかを判定する部分と、必要なExtensionの追加です。

ちなみに`.underline()`は1つ目に書かないと`Value of type 'some View' has no member 'underline'`というエラーに嵌ってしまいます。

# おわりに

そろそろ東京の大地が見えてきたので、書き終わります。

このように、SwiftUIはView1つから簡単に置き換えることが可能なので、大規模プロジェクトでもできるところから導入しましょう！