[Swift]ホットリロードしつつ、UIKitで宣言的にレイアウトを組む

##  はじめに


SwiftUIが徐々に浸透していっている中、まだまだUIKitがメインなのが現状だと思います。

ただStoryboardもコンフリクトが起きやすいなどのデメリットが多いのも事実・・

UIKitでStoryboardを使わず、かつ宣言的で簡単にレイアウトを組むことができれば1番ですよね。

それを実現できる便利なライブラリを今回は紹介したいと思います。

https://github.com/sakiyamaK/DeclarativeUIKit


## 補足

ライブラリとは関係ないですが,ホットリロード機能が使えるようになるツールもご紹介します。

ホットリロードとは簡単にいうと,その都度ビルドしてUIを確認することなくソースコードの変更を即座にシュミレーターで確認できる便利機能です（flutterだと標準装備ですが・・）


まずは下記からツールをダウンロードしておきましょう↓
https://github.com/johnno1962/InjectionIII/releases/tag/4.3.2


ダウンロードしたらApplicationフォルダに入っていることを確認し、一度開きましょう。
上部バーに注射器アイコンが表示されていればOKです。


![スクリーンショット 2022-09-03 11.14.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112697/3c5b4a5f-a658-53a5-d0d6-3edfa62ac2ed.png)


## ライブラリ導入

次にSPMかPodsでライブラリを入れていきます。


```ruby: Swift Package Manager
dependencies: [
    .package(url: "https://github.com/sakiyamaK/DeclarativeUIKit", .upToNextMajor(from: "0.2"))
]
```

```ruby: CocoaPods
pod 'DeclarativeUIKit'

```


プロジェクトを作成したら、AppDelegateに下記を記述していきます。

ツールを入れた場所をpathで指定してます

```swift: AppDelegate.swift  
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
#if DEBUG
        Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
#endif
        return true
    }

```

つぎは　から初期画面を立ち上げるための処理です


```swift:   
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        let window = UIWindow(windowScene: windowScene)
        self.window = window
        
        let vc = DeclarativeViewController()
        window.rootViewController = vc
        window.makeKeyAndVisible()
    }

    func sceneDidDisconnect(_ scene: UIScene) {
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
    }

    func sceneWillResignActive(_ scene: UIScene) {
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
    }
}

```

これで初期画面が立ち上がります。

DeclarativeViewControllerのソースコードを見ていきましょう。


```swift:   
import UIKit
import DeclarativeUIKit


extension Notification.Name {
    static let injection = Notification.Name("INJECTION_BUNDLE_NOTIFICATION")
}

extension NotificationCenter {
    func addInjectionObserver(_ observer: Any, selector: Selector, object: Any?) {
        NotificationCenter.default.addObserver(observer, selector: selector, name: .injection, object: object)
    }
}


final class DeclarativeViewController: UIViewController {
    
    override func loadView() {
            super.loadView()
        NotificationCenter.default.addInjectionObserver(self, selector: #selector(setupLayout), object: nil)
        setupLayout()
    }
}

// ここでレイアウトを組んでいく
@objc private extension DeclarativeViewController {
    func setupLayout() {
        //初期時に画面を白にしておく
        self.view.backgroundColor = .white
        
        self.declarative {
           // ここに記述していく
       
        }
    }
}


```

loadViewdでレイアウト変更通知を受け取れるようにセットしておいて、レイアウトが変更されたら通知が送られるという感じです。

` self.view.backgroundColor = .white`と記述しないと画面が真っ黒なままになってしまうので注意です。


あとは`self.declarative {}`としてその中で宣言的に記述していけばいいだけ。


## ホットリロードの準備

- 上部バーの注射器アイコンをタップし、`Open Project`(1番上)を選択
- 作成したプロジェクトファイル（podで入れた場合は~.xcworkspaceのファイル）を選択

![スクリーンショット 2022-09-03 11.56.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112697/0c6a7f2c-15ec-7929-9f94-534a3b1360bf.png)


これで対象のプロジェクトのみにホットリロードを適用することができます！

一度ビルドすると、下記のようにデバッグコンソールに表示されたらOK
![スクリーンショット 2022-09-03 12.08.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112697/ce78716c-b09a-5920-b2ad-ff6d526c7d1e.png)

実際のホットリロード画面はこちら↓

![ezgif-1-11726da619.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112697/54ed5e03-7693-d5eb-76a6-c4fc6c67e3b9.gif)

感動...


## ソースコード

さて、本題であるDeclarativeUIKitの使い方を見ていきましょう。


```swift:   

self.declarative {
                UIStackView.vertical {
                    UIStackView.horizontal {}
                        .backgroundColor(.green)
                    UIStackView.horizontal {}
                        .backgroundColor(.red)
                    UIStackView.horizontal {}
                        .backgroundColor(.brown)
                }
                .distribution(.fillEqually)
        }

```

上のサンプルみたく、UIStackViewの中にさらにUIStackViewが３つ入ってる状態です。

下記のようにプロパティを並べていけるので直感的に分かりやすいです

```swift:   
self.declarative {
            UIStackView.vertical {
                UIStackView.horizontal {
                    UIView()
                        .size(width: 100, height: 100)
                        .backgroundColor(.red)
                        .cornerRadius(30)
                        .border(color: .blue, width: 10)
                }
            }
        }

```

また、宣言的に書くなかでも
「今までみたいに命令的に書きたい！」
って時に便利なのが`imperative`で

```swift:   
     self.declarative {
            UIStackView.vertical {
                UILabel()
                    .imperative { label in
                    let label = label as! UILabel
                    label.text = "hogehoge"
                }
                .backgroundColor(.gray)
                .height(100)
                
                UIView()
                    .imperative { view in
                        view.tintColor = .black
                        view.isUserInteractionEnabled = true
                    }
                UIView.spacer()
                    .backgroundColor(.brown)
            }
        }
```

こんな感じで今まで通りに書くことができます。

レイアウトは宣言的に書いて、その他の処理はimperativeの中にぶち込むなんてこともできちゃいますね

## おわりに

他にも色々使い方が満載なので、やりながら覚えていきたいと思います！

`ホットリロード＋DeclarativeUIKit`でレイアウト作りは爆速になりそうな予感ですね👀

一応サンプルのリポジトリを貼っておきます↓

https://github.com/taro-ken/DeclarativeUIKit-Sample

ぜひ試してみてください