SwiftUI с SceneKit: как использовать действие кнопки из вида для управления базовой сценой - PullRequest
3 голосов
/ 04 октября 2019

Я пытаюсь реализовать очень простое приложение SwiftUI с базовой сценой / представлением SceneKit. Кнопки в представлении SwiftUI должны манипулировать содержимым сцены, и наоборот, содержимое сцены должно определять, являются ли кнопки активными или неактивными.

Да, я читал и смотрел сессии разработчиков Apple 226: поток данных через SwiftUI и 231: интеграция SwiftUI . И я прошел через учебники по Apple . Но я немного потерян здесь. Любые советы и указания приветствуются.

Вот код:

У меня есть MainView, который использует SceneKit SCNView с HUDView сверху:

import SwiftUI

struct MainView: View {
    var body: some View {
        ZStack {
            SceneView()

            HUDView()
        }
    }
}

SceneView включает SCNView через протокол UIViewRepresentable. Сцена имеет две функции для добавления и удаления рамки со сцены:

import SwiftUI
import SceneKit

struct SceneView: UIViewRepresentable {
    let scene = SCNScene()

    func makeUIView(context: Context) -> SCNView {

        // create a box
        scene.rootNode.addChildNode(createBox())

        // code for creating the camera and setting up lights is omitted for simplicity
        // …

        // retrieve the SCNView
        let scnView = SCNView()
    scnView.scene = scene
        return scnView
    }

    func updateUIView(_ scnView: SCNView, context: Context) {
        scnView.scene = scene

        // allows the user to manipulate the camera
        scnView.allowsCameraControl = true

        // configure the view
        scnView.backgroundColor = UIColor.gray

        // show statistics such as fps and timing information
        scnView.showsStatistics = true
        scnView.debugOptions = .showWireframe
    }

    func createBox() -> SCNNode {
        let boxGeometry = SCNBox(width: 20, height: 24, length: 40, chamferRadius: 0)
        let box = SCNNode(geometry: boxGeometry)
        box.name = "box"
        return box
    }

    func removeBox() {
        // check if box exists and remove it from the scene
        guard let box = scene.rootNode.childNode(withName: "box", recursively: true) else { return }
        box.removeFromParentNode()
    }

    func addBox() {
        // check if box is already present, no need to add one
        if scene.rootNode.childNode(withName: "box", recursively: true) != nil {
            return
        }
        scene.rootNode.addChildNode(createBox())
    }
}

* HUDView связывает кнопки с действиями для добавления и удаления рамки из базовой сцены. Если объект box находится в сцене, кнопка добавления должна быть неактивной, должна быть активна только кнопка удаления:

struct HUDView: View {
    var body: some View {
        VStack(alignment: .leading) {
            HStack(alignment: .center, spacing: 2) {
                Spacer()

                ButtonView(action: {}, icon: "plus.square.fill", isActive: false)
                ButtonView(action: {}, icon: "minus.square.fill")
                ButtonView(action: {}) // just a dummy button
            }
            .background(Color.white.opacity(0.2))

            Spacer()
        }
    }
}

Кнопки также довольно просты, они выполняют действие и, возможно, значок кака также их начальное активное / неактивное состояние:

struct ButtonView: View {
    let action: () -> Void
    var icon: String = "square"
    @State var isActive: Bool = true

    var body: some View {
        Button(action: action) {
            Image(systemName: icon)
                .font(.title)
                .accentColor(isActive ? Color.white : Color.white.opacity(0.5))
        }
        .frame(width: 44, height: 44)
    }
}

Полученное приложение довольно простое:

basic swift app with SceneKit scene

Сцена SceneKitотображается правильно, и я могу управлять камерой на экране.

Но как мне связать действия кнопок с соответствующими функциями сцены? Как разрешить сцене информировать HUDView о своем содержимом (поле присутствует или нет), чтобы оно могло устанавливать активное / неактивное состояние кнопок?

Каков наилучший подход к этой задаче? Должен ли я реализовать Coordinator в SceneView? Должен ли я использовать отдельный SceneViewController, реализованный по протоколу UIViewControllerRepresentable?

Ответы [ 3 ]

1 голос
/ 06 октября 2019

Я нашел решение, используя @EnvironmentalObject, но я не совсем уверен, правильный ли это подход. Поэтому комментарии по этому поводу приветствуются.

Во-первых, я переместил сцену SCNSne в ее собственный класс и сделал ее OberservableObject:

class Scene: ObservableObject {
    @Published var scene: SCNScene

    init(_ scene: SCNScene = SCNScene()) {
        self.scene = scene
        self.scene = setup(scene: self.scene)
    }

    // code omitted which deals with setting the scene up and adding/removing the box

    // method used to determine if the box node is present in the scene -> used later on 
    func boxIsPresent() -> Bool {
        return scene.rootNode.childNode(withName: "box", recursively: true) != nil
    }
}

Я вставляю это Scene в приложение как.environmentalObject(), поэтому он доступен для всех представлений:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        // Create the SwiftUI view that provides the window contents.
        let sceneKit = Scene()
        let mainView = MainView().environmentObject(sceneKit)

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: mainView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}

MainView слегка изменен для вызова SceneView (a UIViewRepresentable) с отдельным Scene для среды:

struct MainView: View {
    @EnvironmentObject var scene: Scene

    var body: some View {
        ZStack {
            SceneView(scene: self.scene.scene)
            HUDView()
        }
    }
}

Затем я делаю Scene доступным для HUDView как @EnvironmentalObject, чтобы я мог ссылаться на сцену и ее методы и вызывать их из действия Button. Другой эффект заключается в том, что я могу запросить вспомогательный метод Scene, чтобы определить, должна ли кнопка быть активной или нет:

struct HUDView: View {
    @EnvironmentObject var scene: Scene
    @State private var canAddBox: Bool = false
    @State private var canRemoveBox: Bool = true

    var body: some View {
        VStack {
            HStack(alignment: .center, spacing: 0) {
                Spacer ()

                ButtonView(
                    action: {
                        self.scene.addBox()
                        if self.scene.boxIsPresent() {
                            self.canAddBox = false
                            self.canRemoveBox = true
                        }
                    },
                    icon: "plus.square.fill",
                    isActive: $canAddBox
                )

                ButtonView(
                    action: {
                        self.scene.removeBox()
                        if !self.scene.boxIsPresent() {
                            self.canRemoveBox = false
                            self.canAddBox = true
                        }
                    },
                    icon: "minus.square.fill",
                    isActive: $canRemoveBox
                )

            }
            .background(Color.white.opacity(0.2))

            Spacer()
        }
    }
}

Вот код ButtonView, который использовал @Binding для установки его активногосостояние (не уверен в правильном порядке для этого с @State property in HUDView`):

struct ButtonView: View {
    let action: () -> Void
    var icon: String = "square"
    @Binding var isActive: Bool

    var body: some View {
        Button(action: action) {
            Image(systemName: icon)
                .font(.title)
                .accentColor(self.isActive ? Color.white : Color.white.opacity(0.5))
        }
        .frame(width: 44, height: 44)
        .disabled(self.isActive ? false: true)

    }
}

В любом случае, код работает сейчас. Есть мысли по этому поводу?

1 голос
/ 07 октября 2019

Есть много способов сделать это, и вы, очевидно, решили это - поздравляю!

Вы можете проверить этот пост 58103566 на макете дизайна игры. Я уверен, что найдутся эксперты, которые не согласны, что хорошо, и я не хочу спорить, просто подумал, что это может сэкономить вам несколько шагов в будущем.

Из нескольких ВК я часто манипулирую игровыми объектами и кнопками, обычно без чрезмерного загрязнения водой.

Надеюсь, это поможет.

0 голосов
/ 04 октября 2019

Я ответил на очень похожий вопрос здесь . Идея состоит в том, что вы создаете PassthroughSubject, что ваш родительский вид (тот, которому принадлежат кнопки) будет отправлять события вниз, а ваш UIViewRespresentable будет подписываться на эти события.

...