SwiftUIのプロジェクトで作ったViewModelの値を独自のUIViewからもやり取りしたい

# SwiftUIで作っているプロジェクトの中に作ったUIView(UIViewRepresentable経由）で親ViewのViewModelを更新したい

## 履歴
2022年11月9日 初版
2022年11月9日 履歴にかかれていた初版の日付修正と、Githubへのリンク追加

## 開発環境
XCode Version 14.0 (14A309)
Swift version 5.7

## 準備
### 確認用CustomUIView
CustomUIViewという名前で以下のXibファイルを作る
制約は適当
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421964/db0669ef-0b05-b055-1e75-5ca35edb9449.png)

CustomClassは次のようにする。

```Swift
import UIKit

class CustomUIView: UIView {
    
    @IBOutlet private weak var messageLabel: UILabel! 
    var label: String {
        get {
            messageLabel?.text ?? ""
        }
        set {
            messageLabel?.text = newValue
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }
    
    func loadNib() {
        let nibName = String(describing: type(of: self))
        let bundle = Bundle(for: type(of: self))
        if let view = bundle.loadNibNamed(nibName, owner: self, options: nil)?.first as? UIView {
            view.frame = self.bounds
            self.addSubview(view)
        }
    }
    
    @IBAction private func tappedChangeMessage(_ sender: UIButton) {
        let formatter = DateFormatter()
        formatter.timeStyle = .medium
        formatter.dateStyle = .medium
        formatter.locale = Locale(identifier: "ja_JP")
        let now = Date()
        
        label = formatter.string(from: now)
    }
    
}
```

### 確認用UIViewRepresentable

```Swift
struct CustomView: UIViewRepresentable {
    typealias UIViewType = CustomUIView
    
    func makeUIView(context: Context) -> CustomUIView {
        let customView = CustomUIView()
        return customView
    }
    
    func updateUIView(_ uiView: CustomUIView, context: Context) {
        // 動作無し
    }
}
```

### 確認用ContentView

```Swift
struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            CustomView()
                .frame(height: 100)
        }
    }
}
```

### ここまでの動作を確認する
画面上に「Label」と表示されて、「時間」を押すと時間が表示される

## SwiftUI部分とViewModelを作る

```Swift
// ViewModel
class ContentViewModel: ObservableObject {
    @Published var displayingLabel: String = "hogeらっちょ"
}
```

ContentViewを全体的に修正する
```Swift
struct ContentView: View {
    @StateObject var viewModel = ContentViewModel()
    
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Spacer()
                Text("SwiftUI(のViewModelを変える）↓")
                Spacer()
            }
            HStack {
                Text(viewModel.displayingLabel)
                    .padding(.horizontal, 10)
                Spacer()
                Button {
                    viewModel.displayingLabel = "きえた"
                } label: {
                    Text("くりあ")
                        .frame(width: 75, height: 60)
                        .background(.blue)
                        .foregroundColor(Color.white)
                        .padding(.trailing, 20)
                }
            }
            
            HStack {
                Spacer()
                Text("UIViewからViewModelを変える↓")
                Spacer()
            }
            CustomView()
                .frame(height: 100)
        }
        .padding()
    }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421964/720cd842-3870-29e4-2e0f-853458b3cb2a.png)

「くりあ」を押すと「hogeらっちょ」は「きえた」に変わる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421964/1d4cf236-68bc-2da0-6035-256b4ccd5c44.png)

## UIViewの「時間」ボタンを押したら「hogeらっちょ」の値を変える
UIViewの値を変更したらViewModelの値を変更する

CustomViewにViewModelを渡すようにする

```diff_swift
struct CustomView: UIViewRepresentable {
    typealias UIViewType = CustomUIView
    
+   @ObservedObject var viewModel: ContentViewModel
    
    func makeUIView(context: Context) -> CustomUIView {
        let customView = CustomUIView()
+        customView.viewModel = viewModel
        return customView
    }
    
    func updateUIView(_ uiView: CustomUIView, context: Context) {
    }
}
```

ContentViewもViewModelを渡すようにする

```diff_swift
struct ContentView: View {
    @StateObject var viewModel = ContentViewModel()
    
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Spacer()
                Text("SwiftUI(のViewModelを変える）↓")
                Spacer()
            }
            HStack {
                Text(viewModel.displayingLabel)
                    .padding(.horizontal, 10)
                Spacer()
                Button {
                    viewModel.displayingLabel = "きえた"
                } label: {
                    Text("くりあ")
                        .frame(width: 75, height: 60)
                        .background(.blue)
                        .foregroundColor(Color.white)
                        .padding(.trailing, 20)
                }
            }
            
            HStack {
                Spacer()
                Text("UIViewからViewModelを変える↓")
                Spacer()
            }
-            CustomView()
+            CustomView(viewModel: viewModel)
                .frame(height: 100)
        }
        .padding()
    }
}
```

CustomUIViewもViewModelを受け取れるようにする

```diff_swift
import UIKit

class CustomUIView: UIView {
    
    @IBOutlet private weak var messageLabel: UILabel!
    
+    var viewModel: ContentViewModel?
    
    var label: String {
        get {
            messageLabel?.text ?? ""
        }
        set {
            messageLabel?.text = newValue
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }
    
    func loadNib() {
        let nibName = String(describing: type(of: self))
        let bundle = Bundle(for: type(of: self))
        if let view = bundle.loadNibNamed(nibName, owner: self, options: nil)?.first as? UIView {
            view.frame = self.bounds
            self.addSubview(view)
        }
    }
    
    @IBAction private func tappedChangeMessage(_ sender: UIButton) {
        let formatter = DateFormatter()
        
        formatter.timeStyle = .medium
        formatter.dateStyle = .medium
        formatter.locale = Locale(identifier: "ja_JP")
        let now = Date()
        
        label = formatter.string(from: now)
+        // viewModelの値を変更する
+        guard let viewModel else { return }
+        viewModel.displayingLabel = formatter.string(from: now)
    }
    
}
```

確認すると、時間を押すとHogeらっちょは変わることがわかる
ただ、「くりあ」を押してもUIView側の値は変わらない

![Screenshot_2022-11-09_at_13_53_18_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421964/35a92e5a-e74f-9b63-d056-8e47aa7a5f1e.gif)

## SwiftUI側からUIKit側の値を更新する

### 方法
1. updateUIViewメソッドを使う
1. Combineを使う


## updateUIViewメソッドを使う方法
これはかなり簡単
updateUIViewは画面描画のタイミングで呼ばれる
SwiftUI側で再描画が起きた時に自動で呼ばれるので修正が１行で足りる

```diff_swift
struct CustomView: UIViewRepresentable {
    typealias UIViewType = CustomUIView
    @ObservedObject var viewModel: ContentViewModel
    
    func makeUIView(context: Context) -> CustomUIView {
        let customView = CustomUIView()
        customView.viewModel = viewModel
        return customView
    }
    
    func updateUIView(_ uiView: CustomUIView, context: Context) {
+        uiView.label = viewModel.displayingLabel
    }
}
```

![Screenshot_2022-11-09_at_14_06_03_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421964/54b09473-9e0e-bec4-ad92-ec243c1481f4.gif)

## Combineを使う

Combine大好き！ って人でもあまり採用しなさそうな対応

```diff_swift
import UIKit
+import Combine

class CustomUIView: UIView {
    
    @IBOutlet private weak var messageLabel: UILabel!
+    private var cancellables = Set<AnyCancellable>()
 
-    var viewModel: ContentViewModel?    
+    var viewModel: ContentViewModel? {
+        didSet {
+            if oldValue !== viewModel {
+                guard let viewModel else { return }
+                viewModel.$displayingLabel
+                    .map { Optional($0) }
+                    .receive(on: DispatchQueue.main)
+                    .assign(to: \UILabel.text, on: messageLabel)
+                    .store(in: &cancellables)
+            }
+        }
+    }
    
-    var label: String {
-        get {
-            messageLabel?.text ?? ""
-        }
-        set {
-            messageLabel?.text = newValue
-        }
-    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }
    
    func loadNib() {
        let nibName = String(describing: type(of: self))
        let bundle = Bundle(for: type(of: self))
        if let view = bundle.loadNibNamed(nibName, owner: self, options: nil)?.first as? UIView {
            view.frame = self.bounds
            self.addSubview(view)
        }
    }
    
    @IBAction private func tappedChangeMessage(_ sender: UIButton) {
        let formatter = DateFormatter()
        
        formatter.timeStyle = .medium
        formatter.dateStyle = .medium
        formatter.locale = Locale(identifier: "ja_JP")
        let now = Date()
        
-        label = formatter.string(from: now)
        guard let viewModel else { return }
        viewModel.displayingLabel = formatter.string(from: now)
    }
    
}
```

`label = formatter.string(from: now)`消してしまうから、viewModelがnilの時困るけれども、
その場合は設計ミスということで。

![Screenshot_2022-11-09_at_14_14_29_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421964/9cfd76da-641d-6e65-4b9a-a12e757a9621.gif)


## 参考用
今回使ったソースが置かれているGithubへのリンク

https://github.com/MaskerDog/UsingPublisherToChangeUIViewValues