LoginSignup
0
0

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

Last updated at Posted at 2024-01-01

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

問題

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にランダムなデータを追加して表示するだけのアプリである。

スクリーンショット 2024-01-02 4.27.41.png

上記のコードでは、

  • 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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0