[UIKit/SwiftUI] UIViewControllerから内部のViewの@Stateとかには触らない方が良いかもしれない

ちょこちょこ忘れて詰まることがあるので、文書化しておくことにした。
ただ理由がわからないので、誰か知っている人いたら教えてください🙇

## 問題
UIViewControllerの内部にUIHostingControllerを使用して、SwiftUIのViewを埋め込み、そのViewの持つ＠Stateなどにアクセスすると正常に値が取れない。

悪いコード:

https://github.com/rikinyan/AccessListSelectionFromOutOfView/tree/bad_pattern

```swift

/// UIViewController側
import UIKit
import SwiftUI

class ViewController: UIViewController {
    
    var orderListView: OrderListView!

    override func viewDidLoad() {
        super.viewDidLoad()
        orderListView = OrderListView(
            orderList: OrderList(items: []),
            addAction: { [weak self] in
                guard let self else { return }
                self.orderListView.orderList = self.orderListView.orderList.add(self.randomOrder())
            },
            deleteAction: { [weak self] in
                guard let self else { return }
                self.orderListView.orderList = self.orderListView.orderList.delete(
                    self.orderListView.multiSelections
                )
            }
        )
        
        let hostingVc = UIHostingController(rootView: orderListView)
        hostingVc.view.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(hostingVc.view)
        
        let margin = view.layoutMarginsGuide
        hostingVc.view.topAnchor.constraint(equalTo: margin.topAnchor).isActive = true
        hostingVc.view.trailingAnchor.constraint(equalTo: margin.trailingAnchor).isActive = true
        hostingVc.view.bottomAnchor.constraint(equalTo: margin.bottomAnchor).isActive = true
        hostingVc.view.leadingAnchor.constraint(equalTo: margin.leadingAnchor).isActive = true
    }

    private func randomOrder() -> Order {
        let menuItem = MenuItem.allCases.shuffled().first!
        return Order(menuItem: menuItem)
    }
}

```

```swift
import SwiftUI

struct OrderListView: View {
    @State var orderList: OrderList
    @State var multiSelections = Set<UUID>()
    @State var editMode: EditMode = .inactive
    
    var addAction: (() -> Void)?
    var deleteAction: (() -> Void)?
    
    var body: some View {
        VStack {
            List(orderList.items, selection: $multiSelections) { item in
                HStack {
                    Text(item.menuItem.name)
                    Spacer()
                    Text("\(item.menuItem.priceString)")
                }
            }
            .listStyle(.plain)
            
            HStack {
                if editMode.isEditing == true {
                    Button(action: {
                        deleteAction?()
                        editMode = .inactive
                    }) {
                        Text("オーダー削除(\(multiSelections.count)件)")
                    }
                } else {
                    Button(action: {
                        addAction?()
                    }) {
                        Text("ランダム追加")
                    }
                }
                Spacer()
                EditButton()
            }
        }
        .environment(\.editMode, $editMode)
    }
}

```

コードを実行すると以下のような画面が出てくる。
Listにランダムなデータを追加して表示するだけのアプリである。

![スクリーンショット 2024-01-02 4.27.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65510/e058ba1a-c82a-a124-74f7-d50dfa578e08.png)

上記のコードでは、

- Orderを表示するためのOrderListView(SwiftUIのView)をUIHostingControllerを使用してUIViewControllerに埋め込んでいる。
- OrderListViewはListと、それを操作するためのButtonを含んでおり、ButtonのActionはUIViewControllerからClosureの形で渡している。
- 渡しているClosureの中で直接View内部の＠Stateの変数にアクセスしている。

（OrderやらOrderListやらはstructのモデル。詳しくはGithub参照。）

実際にこのコードを動かしてみると、「ランダム追加」を何度押してもOrderListにデータが追加されない。


## 理由
よくわからん。。。

## 解決法
改善コード：

https://github.com/rikinyan/AccessListSelectionFromOutOfView/tree/good_pattern

```swift
//ViewController側
import UIKit
import SwiftUI

class ViewController: UIViewController {
    
    var orderListView: OrderListView!

    override func viewDidLoad() {
        super.viewDidLoad()
        orderListView = OrderListView(
            orderList: OrderList(items: []),
            randomAddAction: { [weak self] orderList in
                guard let self else { return orderList }
                return orderList.add(self.randomOrder())
            },
            deleteAction: { orderList, deletedItemIds in
                orderList.delete(deletedItemIds)
            }
        )
        
        let hostingVc = UIHostingController(rootView: orderListView)
        hostingVc.view.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(hostingVc.view)
        
        let margin = view.layoutMarginsGuide
        hostingVc.view.topAnchor.constraint(equalTo: margin.topAnchor).isActive = true
        hostingVc.view.trailingAnchor.constraint(equalTo: margin.trailingAnchor).isActive = true
        hostingVc.view.bottomAnchor.constraint(equalTo: margin.bottomAnchor).isActive = true
        hostingVc.view.leadingAnchor.constraint(equalTo: margin.leadingAnchor).isActive = true
    }

    private func randomOrder() -> Order {
        let menuItem = MenuItem.allCases.shuffled().first!
        return Order(menuItem: menuItem)
    }
}
``` 

```swift

// view側

import SwiftUI

struct OrderListView: View {
    @State var orderList: OrderList
    @State var multiSelections = Set<UUID>()
    @State var editMode: EditMode = .inactive
    
    var randomAddAction: ((OrderList) -> OrderList)?
    var deleteAction: ((OrderList, Set<UUID>) -> OrderList)?
    
    var body: some View {
        VStack {
            List(orderList.items, selection: $multiSelections) { item in
                HStack {
                    Text(item.menuItem.name)
                    Spacer()
                    Text("\(item.menuItem.priceString)")
                }
            }
            .listStyle(.plain)
            
            HStack {
                if editMode.isEditing == true {
                    Button(action: {
                        guard let newOrderList = deleteAction?(orderList, multiSelections) else { return }
                        orderList = newOrderList
                        editMode = .inactive
                    }) {
                        Text("オーダー削除(\(multiSelections.count)件)")
                    }
                } else {
                    Button(action: {
                        guard let newOrderList = randomAddAction?(orderList) else { return }
                        orderList = newOrderList
                    }) {
                        Text("ランダム追加")
                    }
                }
                Spacer()
                EditButton()
            }
        }
        .environment(\.editMode, $editMode)
    }
}

```

端的にいうと **「UIViewController側からのView内部の＠Stateの変数への直接アクセス」** をやめた。

- View側に渡しているClosureの引数に@Stateのwrapped Valueの型を追加し、View側から＠Stateの値をClosureに渡すことで、コピーをいじるように修正した(OrderListはstructなので)。
- Closure内部で@Stateの変数を更新しようとしていたが、Viewのvar body内部でいじるように変更した。

このようにすると、Listへの追加・削除ができるようになった。

## 補)どうして@EnvironmentでeditModeを取得せず、自前でeditModeの@Stateを作成しているか？

なぜかそのように作成したらeditModeが常時Nilになってしまった。
なので、自前で＠Stateを作成して、副次的にprojectedValueとして作成されるBinding(コード内でいう$editMode)を代わりにEnvironmentの値として指定して代わりに使用している。

参考にしたサイト：https://stackoverflow.com/questions/68104918/how-to-check-editbutton-editmode-state-in-swiftui