トグルの値が変更された時に Toggle.OnValueChangedAsObservable を1度目は発火させず2度目以降は発火させる【UniRx / SetIsOnWithoutNotify】

Toggle.OnValueChange,初期値は発火させたくないけど,二度目以降は発火させるようにする奴です.

問題提起

MVPパターンで例えばModel側でboolの状態を管理し,View側でbool値を変更し,かつbool値の状態をUIに反映させる例を挙げます.

Model

using UniRx;
using UnityEngine;

public class SampleModel : MonoBehaviour
{
    private BoolReactiveProperty _isOn = new BoolReactiveProperty(false);
    public IReadOnlyReactiveProperty<bool> IsOn => _isOn;

    /// <summary>
    /// bool値の状態を変更する
    /// </summary>
    public void SwitchValue(bool isOn)
    {
        Debug.Log("Switched Value.");
        Debug.Log(isOn ? "ON." : "OFF.");
        _isOn.Value = isOn;
    }
}

Presenter

using UnityEngine;
using UniRx;

public class SamplePresenter : MonoBehaviour
{
    [SerializeField] private SampleView _view;
    [SerializeField] private SampleModel _model;

    private void Awake()
    {
        // Viewのトグルの値が変更されたら,Modelへ通知して,bool値の状態を変更する
        _view.OnToggleClicked.Subscribe(_model.SwitchValue).AddTo(this);

        // Modelのbool値の状態が変更されたら,Viewへ通知して,ToggleUIのオン・オフを表示を反映させる
        _model.IsOn.Subscribe(_view.ActivateToggle).AddTo(this);
    }
}

View

using System;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

public class SampleView : MonoBehaviour
{
    [SerializeField] private Toggle _toggle;

    private Subject<bool> _OnToggleClicked = new Subject<bool>();
    public IObservable<bool> OnToggleClicked => _OnToggleClicked;

    private void Awake() => _toggle.OnValueChangedAsObservable().Subscribe(_OnToggleClicked.OnNext).AddTo(this);

    /// <summary>
    /// トグルの表示状態を変更する
    /// </summary>
    public void ActivateToggle(bool isOn) => _toggle.isOn = isOn;
}

Toggle の isOn 初期値は true として,この状態でシーンを起動してみます.

gyazo.com

ReactiveProperty の特性上,初期値falseがOnNextで飛び,View側に通知されます.
ViewのActivateToggleで Toggle の isOn が true から false に変わるため,
ここで OnValueChangedAsObservable が発火し,Model側へ通知されます.
そして,SwitchValue の中身が呼び出されます.

ここで問題なのが,シーン実行から初期値が設定される際に OnValueChangedAsObservable が発火されるために,
不必要に 1度 SwitchValue が呼び出されるところです.コンソールに一度のDebug.Log が出てしまいますが,
これをやりたくないというのがモチベーションです.

解決方法 |SetIsOnWithoutNotify

Unity 2019.1 から追加された SetIsOnWithoutNotify というメソッドを利用します.
これは OnValueChanged を発火させずに,Toggle の isOn を設定することができるメソッドです.

docs.unity3d.com

これを利用した解決方法はこのようになります.

  • 1度目は Toggle.SetIsOnWithoutNotify(isOn) で Toggle の値を設定する
  • 2度目以降は Toggle.isOn = isOn で Toggle の値を設定する

以上を満足できるように View と Presenter 側の処理を書き換えます.

View

using System;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

public class SampleView : MonoBehaviour
{
    [SerializeField] private Toggle _toggle;

    private Subject<bool> _OnToggleClicked = new Subject<bool>();
    public IObservable<bool> OnToggleClicked => _OnToggleClicked;

    private void Awake() => _toggle.OnValueChangedAsObservable().Subscribe(_OnToggleClicked.OnNext).AddTo(this);

    /// <summary>
    /// トグルの表示状態を変更する
    /// </summary>
    public void ActivateToggle(bool isOn) => _toggle.isOn = isOn;

    /// <summary>
    /// OnValueChanged を発火させずにトグルの表示状態を変更する
    /// </summary>
    public void SetToggleValueWithoutNotify(bool isOn) => _toggle.SetIsOnWithoutNotify(isOn);
}

Presenter

using UnityEngine;
using UniRx;

public class SamplePresenter : MonoBehaviour
{
    [SerializeField] private SampleView _view;
    [SerializeField] private SampleModel _model;

    private void Awake()
    {
        // Viewのトグルの値が変更されたら,Modelへ通知して,bool値の状態を変更する
        _view.OnToggleClicked.Subscribe(_model.SwitchValue).AddTo(this);

        // Modelのbool値の状態が変更されたら,Viewへ通知して,ToggleUIのオン・オフを表示を反映させる
        // 1回目は OnValueChanged を発火させない
        _model.IsOn.First().Subscribe(_view.SetToggleValueWithoutNotify).AddTo(this);

        // 2回目以降は OnValueChanged を発火させる
        _model.IsOn.Skip(1).Subscribe(_view.ActivateToggle).AddTo(this);
    }
}

これで動作確認してみます.

gyazo.com

シーン実行時にログを吐かなくなり,UIでトグルの値を変更する時にだけ吐くようになりました.

参考資料

halcyonsystemblog.jp

Toggle以外のUIでも似た機能があります.SetValueWithoutNotify.

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

他に良い方法があればご教授頂けると幸いです.