키보드 탭 내비게이션은 키보드 사용자가 UI 요소를 효율적으로 탐색하고 상호작용할 수 있도록 도와주는 접근성 기능입니다. 이 기능을 구현하고, 쉽게 설정할 수 있는 도구를 함께 제작하여 정리하였습니다.

 

📺 미리 보기

 

📖 구현 내용

  • Tab을 누르면 다음 입력필드로, Shift + Tab을 누르면 이전 입력필드로 이동합니다.
  • 특정한 입력필드(마지막 입력필드)에서 설정한 키를 누르면 버튼 이벤트를 호출합니다.
  • 입력필드 설정을 쉽고 간편하게 하기위해 자동으로 설정해주는 에디터 Tool을 구현하였습니다.

 

⚒️ 구현

  • 에디터 기능과 런타임 도중 실행될 두개의 스크립트가 있습니다.

 

· UiTabNavi

  • 런타임도중 실행되어 키보드의 입력을 확인하여 탭 내비게이션을 수행합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;

[RequireComponent(typeof(TMP_InputField))]
public class UiTabNavi : MonoBehaviour
{
    /// <summary>
    /// 키보드를 이용한 포커싱 이동이 활성화 되어있는가?
    /// </summary>
    public static bool IsTabEnabled = true;

    private TMP_InputField mCurrentInputField;

    [Header("이전 단계의 입력필드(nullable)")]
    [SerializeField] TMP_InputField? mPrevInputField = null;

    [Header("다음 단계의 입력필드(nullable)")]
    [SerializeField] TMP_InputField? mNextInputField = null;

    [Space(50)]
    [Header("버튼 누름 이벤트를 발생시킬 KeyCode")]
    [SerializeField] private KeyCode mEventButtonKeyCode = KeyCode.Return;
    [Header("Key Code 이벤트 발생 시 누를 버튼")]
    [SerializeField] Button? mEventButton = null;


    private void Awake()
    {
        mCurrentInputField = GetComponent<TMP_InputField>();

        // 만약 KeyCode가 엔터(Return)라면, onSubmit에 이벤트를 추가
        if (mEventButtonKeyCode == KeyCode.Return)
            mCurrentInputField.onSubmit.AddListener((text) =>
            {
                mEventButton?.onClick.Invoke();
            });
    }

    // Update is called once per frame
    void Update()
    {
        // 현재 이 입력필드가 포커싱(활성화) 되어있는가?
        if (mCurrentInputField.isFocused)
            // 탭 내비게이션이 활성화 되어있는가?
            if (IsTabEnabled)
                // 탭키를 눌렀다면?
                if (Input.GetKeyDown(KeyCode.Tab))
                {
                    // 탭키를 누른 상태에서 LeftShift를 눌렀다면?
                    if (Input.GetKey(KeyCode.LeftShift))
                    {
                        // 이전 입력필드로 포커싱
                        mPrevInputField?.ActivateInputField();
                    }
                    // 탭키만 누른 상태라면?
                    else
                    {
                        // 다음 입력필드로 포커싱
                        mNextInputField?.ActivateInputField();
                    }
                }
                // 이벤트 코드를 눌렀다면?
                else if (Input.GetKeyDown(mEventButtonKeyCode))
                {
                    // 연결된 버튼을 클릭
                    mEventButton?.onClick.Invoke();
                }
    }

#if UNITY_EDITOR

    /// <summary>
    /// Tools/Bonnate/Ui Tab Navigation Simple Setting에서 호출되어 자동으로 탭 내비게이션을 설정합니다.
    /// </summary>
    /// <param name="prev"></param>
    /// <param name="next"></param>
    public void InitFromEditor(TMP_InputField prev, TMP_InputField next)
    {
        mPrevInputField = prev;
        mNextInputField = next;
    }
#endif
}

 

[Header("이전 단계의 입력필드(nullable)")]
[SerializeField] TMP_InputField? mPrevInputField = null;

[Header("다음 단계의 입력필드(nullable)")]
[SerializeField] TMP_InputField? mNextInputField = null;
  • 이전, 다음 단계의 입력 필드를 인스펙터에서 지정합니다.
  • 또는 자동 설정 도구를 이용하여 지정할 수 있습니다.

 

[Space(50)]
[Header("버튼 누름 이벤트를 발생시킬 KeyCode")]
[SerializeField] private KeyCode mEventButtonKeyCode = KeyCode.Return;
[Header("Key Code 이벤트 발생 시 누를 버튼")]
[SerializeField] Button? mEventButton = null;
  • 현재 입력필드가 활성화 되어 있는 상태에서 mEventButtonKeyCode에 해당하는 키보드 이벤트가 발생하면 mEventButton에 있는 버튼이 클릭되는 이벤트가 처리됩니다.

 

private void Awake()
{
    mCurrentInputField = GetComponent<TMP_InputField>();

    // 만약 KeyCode가 엔터(Return)라면, onSubmit에 이벤트를 추가
    if (mEventButtonKeyCode == KeyCode.Return)
        mCurrentInputField.onSubmit.AddListener((text) =>
        {
            mEventButton?.onClick.Invoke();
        });
}
  • 입력필드에서 엔터를 입력했을 때 포커싱이 풀리기 때문에 onSubmit 이벤트에 버튼 이벤트를 추가합니다.

 

void Update()
{
	...
}
  • 키보드 입력에 따라 이벤트를 처리합니다.

 

#if UNITY_EDITOR

    /// <summary>
    /// Tools/Bonnate/Ui Tab Navigation Simple Setting에서 호출되어 자동으로 탭 내비게이션을 설정합니다.
    /// </summary>
    /// <param name="prev"></param>
    /// <param name="next"></param>
    public void InitFromEditor(TMP_InputField prev, TMP_InputField next)
    {
        mPrevInputField = prev;
        mNextInputField = next;
    }
#endif
  • 유니티 에디터에서 사용할 함수로, 입력필드 자동지정 Tool을 사용하면 호출됩니다.
  • prev, next를 자동으로 설정해주어 편리하게 사용이 가능합니다.

 

· UiTabNavigationSimpleSetting

#if UNITY_EDITOR

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine.UI;
using UnityEditor;
using UnityEngine;

public class UiTabNavigationSimpleSetting : EditorWindow
{
    [MenuItem("Tools/Bonnate/Ui Tab Navigation Simple Setting")]
    private static void OpenWindow()
    {
        // 현재 선택된 게임 오브젝트 가져오기
        GameObject selectedObject = Selection.activeGameObject;

        if (selectedObject == null)
        {
            Debug.LogWarning("게임 오브젝트를 선택해주세요.");
            return;
        }

        // 선택된 게임 오브젝트의 1단계 자식 오브젝트들 중 TMP_InputField 찾기
        List<TMP_InputField> inputFields = new List<TMP_InputField>();
        for (int i = 0; i < selectedObject.transform.childCount; ++i)
        {
            TMP_InputField? inputField = null;

            if (selectedObject.transform.GetChild(i).TryGetComponent<TMP_InputField>(out inputField))
                inputFields.Add(inputField);
        }


        if (inputFields.Count == 0)
        {
            Debug.Log("TMP_InputField를 찾을 수 없습니다.");
            return;
        }

        if (inputFields.Count == 1)
        {
            Debug.Log("TMP_InputField가 하나밖에 없습니다.");
            return;
        }

        // 찾은 입력필드가 두개인경우
        if (inputFields.Count == 2)
        {
            UiTabNavi? prevUiTabNavi = null, nextUiTabNavi = null;

            if(inputFields[0].TryGetComponent<UiTabNavi>(out prevUiTabNavi) == false)
                prevUiTabNavi = inputFields[0].gameObject.AddComponent<UiTabNavi>();

            if(inputFields[1].TryGetComponent<UiTabNavi>(out nextUiTabNavi) == false)
                nextUiTabNavi = inputFields[1].gameObject.AddComponent<UiTabNavi>();                

            prevUiTabNavi.InitFromEditor(null, inputFields[1]);
            nextUiTabNavi.InitFromEditor(inputFields[0], null);
        }

        for (int i = 0; i < inputFields.Count; ++i)
        {
            UiTabNavi? uiTabNavi = null;

            if (inputFields[i].TryGetComponent<UiTabNavi>(out uiTabNavi) == false)
                uiTabNavi = inputFields[i].gameObject.AddComponent<UiTabNavi>();

            uiTabNavi.InitFromEditor(i == 0 ? null : inputFields[i - 1], i == inputFields.Count - 1 ? null : inputFields[i + 1]);
        }
    }
}

#endif

 

[MenuItem("Tools/Bonnate/Ui Tab Navigation Simple Setting")]
  • Tools에서 이 스크립트를 호출할 도구를 찾을 수 있습니다.

 

if (inputFields.Count == 0)
{
    Debug.Log("TMP_InputField를 찾을 수 없습니다.");
    return;
}

if (inputFields.Count == 1)
{
    Debug.Log("TMP_InputField가 하나밖에 없습니다.");
    return;
}
  • 현재 선택한 오브젝트의 자식 오브젝트가 한개 이하인경우, 이 기능을 사용할 수 없기에 로그를 찍으며 리턴합니다.

 

✅ 사용 방법

  • 자동 탭 네비게이션을 사용하기위해서는 입력필드(TMP_INPUTFIELD)가 있는 오브젝트의 부모 오브젝트를 선택합니다.
  • Tools/Bonnate/Ui Tab Navigation Simple Setting(기본값)을 선택합니다.
  • 입력필드 오브젝트를 클릭하면 UiTabNavi 스크립트가 추가되어 자동으로 값이 설정되어 있습니다.

자동으로 설정된 컴포넌트

 

🕹️ Unity Affiliate

  • Unity Affiliate Program 파트너로서 아래의 배너를 통해 접속하신 경우 수수료를 받을 수 있습니다.
  • 아래 배너의 에셋들은 '실시간 무료 에셋 랭킹'을 나타냅니다.
bonnate