feat(task-0006): implement menu restart button and signal coordination

- Update MenuUIViewModel with IMenuRestartSignal dependency and Restart() method
- Add RestartButton to MenuUIView with listener management in Initialize/Release
- Connect MenuUIView click handler to ViewModel.Restart() callback
- Fix race condition in MenuRestartSignal.RequestRestart() by nulling completion source first
- Wrap MenuState.WaitAsync() in try-catch for proper view cleanup on cancellation
- Update TASK-0006 status to Ready

Выполнена задача TASK-0006: реализована кнопка Restart и координация сигналов

- Обновлён MenuUIViewModel с зависимостью IMenuRestartSignal и методом Restart()
- Добавлена кнопка RestartButton в MenuUIView с управлением слушателями в Initialize/Release
- Подключен обработчик кликов MenuUIView к колбэку ViewModel.Restart()
- Исправлено состояние гонки в MenuRestartSignal.RequestRestart() путем обнуления completion source
- Обёрнут WaitAsync в MenuState в try-catch для корректной очистки view при отмене
- Обновлён статус TASK-0006 до Ready
This commit is contained in:
2026-05-27 04:35:05 +07:00
parent fda094dd44
commit 6601c8ea22
6 changed files with 109 additions and 3 deletions
+4
View File
@@ -1,5 +1,9 @@
# TASK-0006: MenuState и Restart # TASK-0006: MenuState и Restart
## Статус
Ready
## Цель ## Цель
Реализовать меню с кнопкой `Restart`, которое завершает `MenuState` и возвращает flow к загрузке. Реализовать меню с кнопкой `Restart`, которое завершает `MenuState` и возвращает flow к загрузке.
+46
View File
@@ -381,6 +381,7 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 1000000302} - component: {fileID: 1000000302}
- component: {fileID: 1000000303} - component: {fileID: 1000000303}
- component: {fileID: 1000000304}
m_Layer: 0 m_Layer: 0
m_Name: MenuView m_Name: MenuView
m_TagString: Untagged m_TagString: Untagged
@@ -414,3 +415,48 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: b3333333333333333333333333333333, type: 3} m_Script: {fileID: 11500000, guid: b3333333333333333333333333333333, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
<RestartButton>k__BackingField: {fileID: 1000000304}
--- !u!114 &1000000304
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1000000301}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.88235295, g: 0.88235295, b: 0.88235295, a: 1}
m_PressedColor: {r: 0.69803923, g: 0.69803923, b: 0.69803923, a: 1}
m_SelectedColor: {r: 0.88235295, g: 0.88235295, b: 0.88235295, a: 1}
m_DisabledColor: {r: 0.52156866, g: 0.52156866, b: 0.52156866, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 0}
m_OnClick:
m_PersistentCalls:
m_Calls: []
@@ -17,7 +17,9 @@ namespace QuizPleaseTest.Boot.Flow
public void RequestRestart() public void RequestRestart()
{ {
_restartCompletionSource?.TrySetResult(); UniTaskCompletionSource restartCompletionSource = _restartCompletionSource;
_restartCompletionSource = null;
restartCompletionSource?.TrySetResult();
} }
} }
} }
+11 -1
View File
@@ -1,3 +1,4 @@
using System;
using System.Threading; using System.Threading;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using QuizPleaseTest.Boot.Flow; using QuizPleaseTest.Boot.Flow;
@@ -19,10 +20,19 @@ namespace QuizPleaseTest.Boot.States
public async UniTask EnterAsync(CancellationToken ct) public async UniTask EnterAsync(CancellationToken ct)
{ {
_view.Bind(new MenuUIViewModel()); _view.Bind(new MenuUIViewModel(_restartSignal));
_view.Initialize(); _view.Initialize();
try
{
await _restartSignal.WaitAsync(ct); await _restartSignal.WaitAsync(ct);
} }
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
_view.Release();
throw;
}
}
public UniTask ExitAsync(CancellationToken ct) public UniTask ExitAsync(CancellationToken ct)
{ {
+31
View File
@@ -1,8 +1,39 @@
using QuizPleaseTest.Common.UI; using QuizPleaseTest.Common.UI;
using UnityEngine;
using UnityEngine.UI;
namespace QuizPleaseTest.Boot.UI namespace QuizPleaseTest.Boot.UI
{ {
public class MenuUIView : UIView<MenuUIViewModel> public class MenuUIView : UIView<MenuUIViewModel>
{ {
[field: SerializeField] public Button RestartButton { get; private set; }
public override void Initialize()
{
base.Initialize();
if (RestartButton == null)
{
return;
}
RestartButton.onClick.RemoveListener(OnRestartClicked);
RestartButton.onClick.AddListener(OnRestartClicked);
}
public override void Release()
{
if (RestartButton != null)
{
RestartButton.onClick.RemoveListener(OnRestartClicked);
}
base.Release();
}
private void OnRestartClicked()
{
ViewModel.Restart();
}
} }
} }
+13
View File
@@ -1,8 +1,21 @@
using System;
using QuizPleaseTest.Boot.Flow;
using QuizPleaseTest.Common.UI; using QuizPleaseTest.Common.UI;
namespace QuizPleaseTest.Boot.UI namespace QuizPleaseTest.Boot.UI
{ {
public class MenuUIViewModel : IUIViewModel public class MenuUIViewModel : IUIViewModel
{ {
private readonly IMenuRestartSignal _restartSignal;
public MenuUIViewModel(IMenuRestartSignal restartSignal)
{
_restartSignal = restartSignal ?? throw new ArgumentNullException(nameof(restartSignal));
}
public void Restart()
{
_restartSignal.RequestRestart();
}
} }
} }