Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hugo/feature/Add UIModel and action to OneToOne template #1654

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import GameEngineKit
import SwiftUI

// swiftlint:disable type_body_length

struct ContentView: View {
var body: some View {
NavigationStack {
Expand Down Expand Up @@ -349,6 +351,84 @@
}
}

HStack(spacing: 20) {
Text("Action Then DnD One To One")
.font(.title)
.padding()

NavigationLink("Observe Image Then Drag & Drop One To One", destination: {
let gameplay = NewGameplayFindTheRightOrder(choices: NewGameplayFindTheRightOrder.kDefaultImageChoicesWithZones)
let coordinator = DnDOneToOneCoordinatorFindTheRightOrder(gameplay: gameplay,
action: .ipad(type: .image("sport_dance_player_man")))
let viewModel = DnDOneToOneViewModel(coordinator: coordinator)

return DnDOneToOneView(viewModel: viewModel)
.navigationTitle("Observe Image Then Drag & Drop One To One")
.navigationBarTitleDisplayMode(.large)
})
.tint(.cyan)
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)

NavigationLink("Observe SFSymbol Then Drag & Drop One To One", destination: {
let gameplay = NewGameplayFindTheRightOrder(choices: NewGameplayFindTheRightOrder.kDefaultImageChoicesWithZones)
let coordinator = DnDOneToOneCoordinatorFindTheRightOrder(gameplay: gameplay,
action: .ipad(type: .sfsymbol("star")))
let viewModel = DnDOneToOneViewModel(coordinator: coordinator)

return DnDOneToOneView(viewModel: viewModel)
.navigationTitle("Observe SFSymbol Then Drag & Drop One To One")
.navigationBarTitleDisplayMode(.large)
})
.tint(.cyan)
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)

NavigationLink("Listen Then Drag & Drop One To One", destination: {
let gameplay = NewGameplayFindTheRightOrder(choices: NewGameplayFindTheRightOrder.kDefaultImageChoicesWithZones)
let coordinator = DnDOneToOneCoordinatorFindTheRightOrder(gameplay: gameplay,
action: .ipad(type: .audio("sound_animal_duck")))
let viewModel = DnDOneToOneViewModel(coordinator: coordinator)

return DnDOneToOneView(viewModel: viewModel)
.navigationTitle("Listen Then Drag & Drop One To One")
.navigationBarTitleDisplayMode(.large)
})
.tint(.cyan)
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)

NavigationLink("Listen Speech Then Drag & Drop One To One", destination: {
let gameplay = NewGameplayFindTheRightOrder(choices: NewGameplayFindTheRightOrder.kDefaultImageChoicesWithZones)
let coordinator = DnDOneToOneCoordinatorFindTheRightOrder(gameplay: gameplay,
action: .ipad(type: .speech("Correct answer")))
let viewModel = DnDOneToOneViewModel(coordinator: coordinator)

return DnDOneToOneView(viewModel: viewModel)
.navigationTitle("Listen Speech Then Drag & Drop One To One")
.navigationBarTitleDisplayMode(.large)
})
.tint(.cyan)
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)

NavigationLink("Robot Then Drag & Drop One To One", destination: {
let gameplay = NewGameplayFindTheRightOrder(choices: NewGameplayFindTheRightOrder.kDefaultImageChoicesWithZones)
let coordinator = DnDOneToOneCoordinatorFindTheRightOrder(gameplay: gameplay,
action: .robot(type: .color("red")))
let viewModel = DnDOneToOneViewModel(coordinator: coordinator)

return DnDOneToOneView(viewModel: viewModel)
.navigationTitle("Listen Robot Then Drag & Drop One To One")
.navigationBarTitleDisplayMode(.large)
})
.tint(.cyan)
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)

Spacer()
}

Text("Or choose a template")
.font(.largeTitle)
.padding()
Expand All @@ -358,6 +438,8 @@
}
}

// swiftlint:enable type_body_length

#Preview {
ContentView()
}

Check warning on line 445 in Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift

View workflow job for this annotation

GitHub Actions / swiftlint

File should contain 400 lines or less: currently contains 445 (file_length)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// SPDX-License-Identifier: Apache-2.0

import Combine
import ContentKit
import SpriteKit
import SwiftUI
import UtilsKit
Expand All @@ -12,18 +13,19 @@ import UtilsKit
public class DnDOneToOneCoordinatorFindTheRightOrder: DnDOneToOneGameplayCoordinatorProtocol {
// MARK: Lifecycle

public init(gameplay: NewGameplayFindTheRightOrder) {
public init(gameplay: NewGameplayFindTheRightOrder, action: Exercise.Action? = nil) {
self.gameplay = gameplay

self.uiChoices.value.choices = gameplay.orderedChoices.map { choice in
DnDAnswerNode(id: choice.id, value: choice.value, type: choice.type, size: self.uiChoices.value.choiceSize(for: gameplay.orderedChoices.count))
self.uiModel.value.action = action
self.uiModel.value.choices = gameplay.orderedChoices.map { choice in
DnDAnswerNode(id: choice.id, value: choice.value, type: choice.type, size: self.uiModel.value.choiceSize(for: gameplay.orderedChoices.count))
}

self.uiDropZones = self.uiChoices.value.choices.map { node in
self.uiDropZones = self.uiModel.value.choices.map { node in
DnDDropZoneNode(node: node)
}

self.uiChoices.value.choices.shuffle()
self.uiModel.value.choices.shuffle()

self.currentOrderedChoices = Array(repeating: .zero, count: gameplay.orderedChoices.count)
self.alreadyValidatedChoices = Array(repeating: .zero, count: gameplay.orderedChoices.count)
Expand All @@ -32,7 +34,7 @@ public class DnDOneToOneCoordinatorFindTheRightOrder: DnDOneToOneGameplayCoordin
// MARK: Public

public private(set) var uiDropZones: [DnDDropZoneNode] = []
public private(set) var uiChoices = CurrentValueSubject<DnDUIChoices, Never>(.zero)
public private(set) var uiModel = CurrentValueSubject<DnDOneToOneUIModel, Never>(.zero)

public func setAlreadyOrderedNodes() {
self.gameplay.orderedChoices.forEach { choice in
Expand Down Expand Up @@ -90,9 +92,9 @@ public class DnDOneToOneCoordinatorFindTheRightOrder: DnDOneToOneGameplayCoordin
}

private func updateChoiceState(for choice: NewGameplayFindTheRightOrderChoice, to state: State) {
guard let index = self.uiChoices.value.choices.firstIndex(where: { $0.id == choice.id }) else { return }
guard let index = self.uiModel.value.choices.firstIndex(where: { $0.id == choice.id }) else { return }

self.updateUINodeState(node: self.uiChoices.value.choices[index], state: state)
self.updateUINodeState(node: self.uiModel.value.choices[index], state: state)
}

private func choiceAlreadySelected(choice: NewGameplayFindTheRightOrderChoice) -> Bool {
Expand Down Expand Up @@ -130,7 +132,7 @@ public class DnDOneToOneCoordinatorFindTheRightOrder: DnDOneToOneGameplayCoordin
}

private func isMovable(choice: NewGameplayFindTheRightOrderChoice) -> Bool {
self.uiChoices.value.choices.first(where: { choice.id == $0.id })!.isDraggable
self.uiModel.value.choices.first(where: { choice.id == $0.id })!.isDraggable
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
import Combine
import Foundation

// swiftlint:disable:next type_name
public protocol DnDOneToOneGameplayCoordinatorProtocol {
var uiChoices: CurrentValueSubject<DnDUIChoices, Never> { get }
var uiModel: CurrentValueSubject<DnDOneToOneUIModel, Never> { get }
var uiDropZones: [DnDDropZoneNode] { get }
func setAlreadyOrderedNodes()
func onTouch(_ event: DnDTouchEvent, choice: DnDAnswerNode, destination: DnDDropZoneNode?)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Leka - iOS Monorepo
// Copyright APF France handicap
// SPDX-License-Identifier: Apache-2.0

import ContentKit
import SpriteKit
import SwiftUI

// swiftlint:disable cyclomatic_complexity

// MARK: - DnDOneToOneUIModel

public struct DnDOneToOneUIModel {
static let zero = DnDOneToOneUIModel(action: nil, choices: [])

var action: Exercise.Action?
var choices: [DnDAnswerNode]

func choiceSize(for numberOfChoices: Int) -> CGSize {
switch self.action {
case .ipad(type: .image),
.ipad(type: .sfsymbol):
switch numberOfChoices {
case 1:
CGSize(width: 200, height: 200)
case 2:
CGSize(width: 160, height: 160)
case 3:
CGSize(width: 130, height: 130)
case 4:
CGSize(width: 110, height: 110)
case 5:
CGSize(width: 90, height: 90)
default:
CGSize(width: 75, height: 75)
}
case .none:
switch numberOfChoices {
case 1...3:
CGSize(width: 220, height: 220)
case 4:
CGSize(width: 200, height: 200)
case 5:
CGSize(width: 170, height: 170)
default:
CGSize(width: 150, height: 150)
}
case .ipad(type: .audio),
.ipad(type: .speech),
.robot:
switch numberOfChoices {
case 1...2:
CGSize(width: 200, height: 200)
case 3:
CGSize(width: 180, height: 180)
case 4:
CGSize(width: 160, height: 160)
case 5:
CGSize(width: 130, height: 130)
default:
CGSize(width: 110, height: 110)
}
default:
CGSize(width: 75, height: 75)
}
}
}

// swiftlint:enable cyclomatic_complexity
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,44 @@ public struct DnDOneToOneView: View {
// MARK: Public

public var body: some View {
GeometryReader { proxy in
SpriteView(scene: self.makeScene(size: proxy.size), options: [.allowsTransparency])
.frame(width: proxy.size.width, height: proxy.size.height)
.onAppear {
self.scene = DnDOneToOneBaseScene(viewModel: self.viewModel)
HStack(spacing: 0) {
if let action = self.viewModel.action {
Button {
// nothing to do
}
label: {
ActionButtonView(action: action)
.padding(20)
}
.simultaneousGesture(
TapGesture()
.onEnded { _ in
withAnimation {
self.viewModel.isActionTriggered = true
}
}
)

Divider()
.opacity(0.4)
.frame(maxHeight: 500)
.padding(.vertical, 20)
}

Spacer()

GeometryReader { proxy in
SpriteView(scene: self.makeScene(size: proxy.size), options: [.allowsTransparency])
.frame(width: proxy.size.width, height: proxy.size.height)
.onAppear {
self.scene = DnDOneToOneBaseScene(viewModel: self.viewModel)
}
}
.colorMultiply(self.viewModel.isActionTriggered ? .white : .gray.opacity(0.4))
.animation(.easeOut(duration: 0.3), value: self.viewModel.isActionTriggered)
.allowsHitTesting(self.viewModel.isActionTriggered)

Spacer()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// SPDX-License-Identifier: Apache-2.0

import Combine
import ContentKit
import SwiftUI

// MARK: - DnDOneToOneViewModel
Expand All @@ -11,22 +12,27 @@ public class DnDOneToOneViewModel: ObservableObject {
// MARK: Lifecycle

public init(coordinator: DnDOneToOneGameplayCoordinatorProtocol) {
self.choices = coordinator.uiChoices.value.choices
self.choices = coordinator.uiModel.value.choices
self.dropzones = coordinator.uiDropZones
self.action = coordinator.uiModel.value.action
self.isActionTriggered = (self.action == nil) ? true : false
self.coordinator = coordinator
self.coordinator.uiChoices
self.coordinator.uiModel
.receive(on: DispatchQueue.main)
.sink { [weak self] choices in
self?.choices = choices.choices
.sink { [weak self] model in
self?.choices = model.choices
}
.store(in: &self.cancellables)
}

// MARK: Internal

@Published var isActionTriggered = false
@Published var choices: [DnDAnswerNode] = []
@Published var dropzones: [DnDDropZoneNode] = []

let action: Exercise.Action?

func setAlreadyOrderedNodes() {
self.coordinator.setAlreadyOrderedNodes()
}
Expand Down
Loading
Loading