ちょこちょこ忘れて詰まることがあるので、文書化しておくことにした。
ただ理由がわからないので、誰か知っている人いたら教えてください🙇
問題
UIViewControllerの内部にUIHostingControllerを使用して、SwiftUIのViewを埋め込み、そのViewの持つ@Stateなどにアクセスすると正常に値が取れない。
悪いコード:
/// 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)
}
}
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にランダムなデータを追加して表示するだけのアプリである。
上記のコードでは、
- Orderを表示するためのOrderListView(SwiftUIのView)をUIHostingControllerを使用してUIViewControllerに埋め込んでいる。
- OrderListViewはListと、それを操作するためのButtonを含んでおり、ButtonのActionはUIViewControllerからClosureの形で渡している。
- 渡しているClosureの中で直接View内部の@Stateの変数にアクセスしている。
(OrderやらOrderListやらはstructのモデル。詳しくはGithub参照。)
実際にこのコードを動かしてみると、「ランダム追加」を何度押してもOrderListにデータが追加されない。
理由
よくわからん。。。
解決法
改善コード:
//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)
}
}
// 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