Skip to content

Commit

Permalink
x:sparkles: (GEK): Add Pairing views
Browse files Browse the repository at this point in the history
  • Loading branch information
HPezz committed Dec 20, 2023
1 parent 8d5cb4d commit 0f4fa5d
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Leka - iOS Monorepo
// Copyright 2023 APF France handicap
// SPDX-License-Identifier: Apache-2.0

import DesignKit
import SwiftUI

// MARK: - Action

extension PairingView {
struct ActionButton: View {
// MARK: Lifecycle

init(_ actionType: ActionType, text: String, hasStarted: Bool = false, action: @escaping () -> Void) {
self.actionType = actionType
self.text = text
self.hasStarted = hasStarted
self.action = action
}

// MARK: Internal

let actionType: ActionType
let text: String
let hasStarted: Bool
let action: () -> Void

var body: some View {
VStack {
Button {
self.action()
} label: {
self.actionType.icon(self.hasStarted)
}

.background(
Circle()
.fill(.white)
.shadow(color: .black.opacity(0.5), radius: 7, x: 0, y: 4)
)

Text(self.text)
.font(.body)
.foregroundColor(DesignKitAsset.Colors.lekaDarkGray.swiftUIColor)
.padding(.vertical, 10)
}
}
}
}

#Preview {
PairingView.ActionButton(.stop, text: "Stop", hasStarted: true) {
print("Button tapped !")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Leka - iOS Monorepo
// Copyright 2023 APF France handicap
// SPDX-License-Identifier: Apache-2.0

import DesignKit
import SwiftUI

extension PairingView {
enum ActionType {
case start
case pause
case stop

// MARK: Internal

func icon(_ isPlaying: Bool) -> some View {
let view = switch self {
case .start:
Image(systemName: "play.circle.fill")
.foregroundStyle(DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor)
.font(.system(size: 150))
case .pause:
Image(systemName: "pause.circle.fill")
.foregroundStyle(DesignKitAsset.Colors.btnDarkBlue.swiftUIColor)
.font(.system(size: 150))
case .stop:
Image(systemName: "stop.circle.fill")
.foregroundStyle(
isPlaying
? DesignKitAsset.Colors.lekaOrange.swiftUIColor
: DesignKitAsset.Colors.lekaDarkGray.swiftUIColor
)
.font(.system(size: 150))
}

return view
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Leka - iOS Monorepo
// Copyright 2023 APF France handicap
// SPDX-License-Identifier: Apache-2.0

import RobotKit
import SwiftUI

extension PairingView {
enum Animation: CaseIterable {
case quickMove
case spin
case headNod
case headShake
case reinforcer
case light

// MARK: Internal

func actions(robot: Robot) -> [(TimeInterval, () -> Void)] {
switch self {
case .quickMove:
let rotation = [Robot.Motion.Rotation.counterclockwise, Robot.Motion.Rotation.clockwise]
return [
(0.3, { robot.move(.spin(rotation.randomElement()!, speed: 0.6)) }),
(0.0, { robot.stopMotion() }),
]
case .spin:
let rotation = [Robot.Motion.Rotation.counterclockwise, Robot.Motion.Rotation.clockwise]
return [
(2.2, { robot.move(.spin(rotation.randomElement()!, speed: 0.6)) }),
(0.0, { robot.stopMotion() }),
]
case .headNod:
return [
(0.2, { robot.move(.forward(speed: 0.3)) }),
(0.3, { robot.move(.backward(speed: 0.3)) }),
(0.2, { robot.move(.forward(speed: 0.3)) }),
(0.0, { robot.stopMotion() }),
]
case .headShake:
return [
(0.1, { robot.move(.spin(.clockwise, speed: 0.5)) }),
(0.15, { robot.move(.spin(.counterclockwise, speed: 0.5)) }),
(0.1, { robot.move(.spin(.clockwise, speed: 0.5)) }),
(0.1, { robot.move(.spin(.counterclockwise, speed: 0.5)) }),
(0.0, { robot.stopMotion() }),
]
case .reinforcer:
let reinforcers: [Robot.Reinforcer] = [.fire, .rainbow, .sprinkles]
return [
(0.0, { robot.run(reinforcers.randomElement()!) }),
]
case .light:
let color: [Robot.Color] = [.blue, .green, .orange, .pink, .purple, .red, .yellow]
return [
(1.0, { robot.shine(.all(in: color.randomElement()!)) }),
(0.0, { robot.stopLights() }),
]
}
}

func duration() -> TimeInterval {
switch self {
case .quickMove:
0.5
case .spin:
1.5
case .headNod:
0.5
case .headShake:
0.5
case .reinforcer:
5
case .light:
1
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Leka - iOS Monorepo
// Copyright 2023 APF France handicap
// SPDX-License-Identifier: Apache-2.0

import Combine
import ContentKit
import RobotKit
import SwiftUI

extension PairingView {
class RobotManager {
// MARK: Internal

func startPairing() {
self.isAnimationRunning = true
self.runRandomAnimation()
}

func pausePairing() {
self.isAnimationRunning = false
self.robot.stopMotion()
}

func stopPairing() {
self.isAnimationRunning = false
self.robot.stop()
self.lightIntensity = 0.0
}

// MARK: Private

private var isAnimationRunning: Bool = false
private var isBreathing: Bool = false
private var breatheIn = true
private var lightIntensity: Float = 0.0
private var animationTime: TimeInterval = 0.0
private let lightIntensityChangeDuration = 0.05
private let robot = Robot.shared

private func runRandomAnimation() {
guard self.isAnimationRunning else { return }

let randomInterval = Double.random(in: 5.0...10.0)
self.isBreathing = true
self.breathe()

DispatchQueue.main.asyncAfter(deadline: .now() + randomInterval) {
guard self.isAnimationRunning else { return }
self.isBreathing = false
let currentAnimation = Animation.allCases.randomElement()!
self.play(currentAnimation)

DispatchQueue.main.asyncAfter(deadline: .now() + currentAnimation.duration()) {
guard self.isAnimationRunning else { return }
self.runRandomAnimation()
}
}
}

private func play(_ animation: Animation) {
let actions = animation.actions(robot: self.robot)
self.animationTime = 0.1

for (duration, action) in actions {
DispatchQueue.main.asyncAfter(deadline: .now() + self.animationTime) {
guard self.isAnimationRunning else { return }
action()
}
self.animationTime += duration
}
}

private func breathe() {
guard self.isAnimationRunning, self.isBreathing else { return }

self.updateLightIntensity()

DispatchQueue.main.asyncAfter(deadline: .now() + self.lightIntensityChangeDuration) {
let shadeOfColor: Robot.Color = .init(fromGradient: (.black, .lightBlue), at: self.lightIntensity)
self.robot.shine(.all(in: shadeOfColor))
self.breathe()
}
}

private func updateLightIntensity() {
if self.lightIntensity >= 1.0 {
self.breatheIn = false
} else if self.lightIntensity <= 0.0 {
self.breatheIn = true
}

self.lightIntensity += self.breatheIn ? 0.02 : -0.02
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Leka - iOS Monorepo
// Copyright 2023 APF France handicap
// SPDX-License-Identifier: Apache-2.0

import ContentKit
import SwiftUI

// MARK: - PairingView

struct PairingView: View {
// MARK: Lifecycle

init(instructions: Pairing.Payload.Instructions) {
self.instructions = instructions
self.shared = ExerciseSharedData()
}

init(exercise: Exercise, data: ExerciseSharedData? = nil) {
guard let payload = exercise.payload as? Pairing.Payload else {
fatalError("Exercise payload is not .selection")
}

self.instructions = payload.instructions
self.shared = data
}

// MARK: Internal

let instructions: Pairing.Payload.Instructions
let robotManager = RobotManager()
let shared: ExerciseSharedData?

var body: some View {
VStack {
Text(self.instructions.textMainInstructions)
.font(.body)
.foregroundColor(.primary)
.multilineTextAlignment(.center)

Spacer()

HStack(spacing: 180) {
if self.isPlaying {
ActionButton(.pause, text: self.instructions.textButtonPause) {
print("Pairing behavior is pausing")
self.robotManager.pausePairing()
self.isPlaying = false
}
} else {
ActionButton(.start, text: self.instructions.textButtonPlay) {
print("Pairing behavior is running")
self.robotManager.startPairing()
self.isPlaying = true
self.hasStarted = true
}
}

ActionButton(.stop, text: self.instructions.textButtonStop, hasStarted: self.hasStarted) {
print("Pairing behavior stopped")
self.robotManager.stopPairing()

self.isPlaying = false
self.hasStarted = false
}
.disabled(!self.hasStarted)
}

Spacer()
}
}

// MARK: Private

@State private var isPlaying: Bool = false
@State private var hasStarted: Bool = false
}

#Preview {
let instructions = Pairing.Payload.Instructions(
textMainInstructions: "Instructions principales",
textButtonPlay: "Jouer",
textButtonPause: "Pause",
textButtonStop: "Stop"
)

return PairingView(instructions: instructions)
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ public struct ActivityView: View {
exercise: self.viewModel.currentExercise,
data: self.viewModel.currentExerciseSharedData
)

case .pairing:
PairingView(
exercise: self.viewModel.currentExercise,
data: self.viewModel.currentExerciseSharedData
)
}
}
}
Expand Down

0 comments on commit 0f4fa5d

Please sign in to comment.