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 action to DnDGrid in NewGEK #1649

Merged
merged 8 commits into from
Dec 17, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,80 @@ struct ContentView: View {

Spacer()
}

HStack(spacing: 20) {
Text("Action Then DnDGrid")
.font(.title)
.padding()

NavigationLink("Observe Image Then Drag & Drop Categories", destination: {
let gameplay = NewGameplayAssociateCategories(choices: NewGameplayAssociateCategories.kDefaultChoices)
let coordinator = DnDGridCoordinatorAssociateCategories(gameplay: gameplay,
action: .ipad(type: .image("sport_dance_player_man")))
let viewModel = DnDGridViewModel(coordinator: coordinator)

return DnDGridView(viewModel: viewModel)
.navigationTitle("Observe Image Then Drag & Drop Categories")
.navigationBarTitleDisplayMode(.large)
})
.tint(.pink)
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)

NavigationLink("Observe SFSymbol Then Drag & Drop Categories", destination: {
let gameplay = NewGameplayAssociateCategories(choices: NewGameplayAssociateCategories.kDefaultChoices)
let coordinator = DnDGridCoordinatorAssociateCategories(gameplay: gameplay, action: .ipad(type: .sfsymbol("star")))
let viewModel = DnDGridViewModel(coordinator: coordinator)

return DnDGridView(viewModel: viewModel)
.navigationTitle("Observe SFSymbol Then Drag & Drop Categories")
.navigationBarTitleDisplayMode(.large)
})
.tint(.pink)
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)

NavigationLink("Listen Then Drag & Drop Categories", destination: {
let gameplay = NewGameplayAssociateCategories(choices: NewGameplayAssociateCategories.kDefaultChoices)
let coordinator = DnDGridCoordinatorAssociateCategories(gameplay: gameplay, action: .ipad(type: .audio("sound_animal_duck")))
let viewModel = DnDGridViewModel(coordinator: coordinator)

return DnDGridView(viewModel: viewModel)
.navigationTitle("Listen Then Drag & Drop Categories")
.navigationBarTitleDisplayMode(.large)
})
.tint(.pink)
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)

NavigationLink("Listen Speech Then Drag & Drop Categories", destination: {
let gameplay = NewGameplayAssociateCategories(choices: NewGameplayAssociateCategories.kDefaultImageChoices)
let coordinator = DnDGridCoordinatorAssociateCategories(gameplay: gameplay, action: .ipad(type: .speech("Correct answer")))
let viewModel = DnDGridViewModel(coordinator: coordinator)

return DnDGridView(viewModel: viewModel)
.navigationTitle("Listen Speech Then Drag & Drop Categories")
.navigationBarTitleDisplayMode(.large)
})
.tint(.pink)
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)

NavigationLink("Robot Then Drag & Drop Categories", destination: {
let gameplay = NewGameplayAssociateCategories(choices: NewGameplayAssociateCategories.kDefaultChoices)
let coordinator = DnDGridCoordinatorAssociateCategories(gameplay: gameplay, action: .robot(type: .color("red")))
let viewModel = DnDGridViewModel(coordinator: coordinator)

return DnDGridView(viewModel: viewModel)
.navigationTitle("Listen Speech Then Drag & Drop Categories")
.navigationBarTitleDisplayMode(.large)
})
.tint(.pink)
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)

Spacer()
}
}

Text("Or choose a template")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ public extension NewGameplayAssociateCategories {
NewGameplayAssociateCategoriesChoice(value: "Maison", category: nil, type: .text),
]

static let kDefaultImageChoices: [NewGameplayAssociateCategoriesChoice] = [
NewGameplayAssociateCategoriesChoice(value: "pictograms-weather-sun_yellow-0106", category: .categoryA, type: .image),
NewGameplayAssociateCategoriesChoice(value: "pictograms-animals-arctic-penguin_yellow-0088", category: .categoryB, type: .image),
NewGameplayAssociateCategoriesChoice(value: "pictograms-weather-sun_yellow-0106", category: .categoryA, type: .image),
NewGameplayAssociateCategoriesChoice(value: "pictograms-animals-arctic-penguin_yellow-0088", category: .categoryB, type: .image),
NewGameplayAssociateCategoriesChoice(value: "pictograms-weather-sun_yellow-0106", category: .categoryA, type: .image),
NewGameplayAssociateCategoriesChoice(value: "Maison", category: nil, type: .text),
]

static let kDefaultChoicesWithZones: [NewGameplayAssociateCategoriesChoice] = [
NewGameplayAssociateCategoriesChoice(value: "sun", category: .categoryA, type: .text),
NewGameplayAssociateCategoriesChoice(value: "car", category: .categoryB, type: .text),
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 SpriteKit
import SwiftUI
import UtilsKit
Expand All @@ -12,17 +13,18 @@ import UtilsKit
public class DnDGridCoordinatorAssociateCategories: DnDGridGameplayCoordinatorProtocol {
// MARK: Lifecycle

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

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

// MARK: Public

public private(set) var uiChoices = CurrentValueSubject<DnDUIChoices, Never>(.zero)
public private(set) var uiModel = CurrentValueSubject<DnDGridUIModel, Never>(.zero)

public func onTouch(_ event: DnDTouchEvent, choice: DnDAnswerNode, destination: DnDAnswerNode? = nil) {
switch event {
Expand Down Expand Up @@ -76,7 +78,7 @@ public class DnDGridCoordinatorAssociateCategories: DnDGridGameplayCoordinatorPr
private func updateChoiceState(for choice: NewGameplayAssociateCategoriesChoice, to state: State) {
guard let index = self.gameplay.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: NewGameplayAssociateCategoriesChoice) -> Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ import Combine
import Foundation

public protocol DnDGridGameplayCoordinatorProtocol {
var uiChoices: CurrentValueSubject<DnDUIChoices, Never> { get }
var uiModel: CurrentValueSubject<DnDGridUIModel, Never> { get }
func onTouch(_ event: DnDTouchEvent, choice: DnDAnswerNode, destination: DnDAnswerNode?)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Leka - iOS Monorepo
// Copyright APF France handicap
// SPDX-License-Identifier: Apache-2.0

import ContentKit
import SpriteKit
import SwiftUI

// MARK: - DnDGridViewUIChoicesWrapper

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

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

// swiftlint:disable cyclomatic_complexity

func choiceSize(for numberOfChoices: Int) -> CGSize {
switch self.action {
case .ipad(type: .image),
.ipad(type: .sfsymbol):
switch numberOfChoices {
case 1...4:
CGSize(width: 180, height: 180)
case 5...6:
CGSize(width: 150, height: 150)
default:
CGSize(width: 150, height: 150)
}
case .none:
switch numberOfChoices {
case 1...2:
CGSize(width: 300, height: 300)
case 3...4:
CGSize(width: 240, height: 240)
case 5...6:
CGSize(width: 200, height: 200)
default:
CGSize(width: 200, height: 200)
}
default:
switch numberOfChoices {
case 1...2:
CGSize(width: 220, height: 220)
case 3...4:
CGSize(width: 200, height: 200)
case 5:
CGSize(width: 160, height: 160)
case 6:
CGSize(width: 150, height: 150)
default:
CGSize(width: 150, height: 150)
}
}
}

// swiftlint:enable cyclomatic_complexity
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,44 @@ public struct DnDGridView: 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 = self.getScene(for: self.viewModel.choices.count, size: proxy.size)
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 = self.getScene(for: self.viewModel.choices.count, size: proxy.size)
}
}
.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: - DnDGridViewModel
Expand All @@ -11,12 +12,14 @@ public class DnDGridViewModel: ObservableObject {
// MARK: Lifecycle

public init(coordinator: DnDGridGameplayCoordinatorProtocol) {
self.choices = coordinator.uiChoices.value.choices
self.choices = coordinator.uiModel.value.choices
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)
}
Expand All @@ -29,8 +32,11 @@ public class DnDGridViewModel: ObservableObject {

// MARK: Internal

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

let action: Exercise.Action?

// MARK: Private

private let coordinator: DnDGridGameplayCoordinatorProtocol
Expand Down
Loading