본문 바로가기

카테고리 없음

[troubleShooting] flutter inappwebview - 안드로이드, ios환경에따라 뒤로가기, 스와이프 제어하기

flutter inappwebview환경에서 특정상황(ex. 웹에서 모달이 열였을 때, 홈화면일 때 등등)에서 ios의 뒤로 가기 스와이프를 막는 설정에 대해서 알아보자

 

우선 최초 버그내용으로는 다음과같았다

"모달을 띄운 후 페이지를 뒤로 갔을 때 모달이 그대로 유지됩니다" 

이는 확인해 본 결과 모달을 전역적으로 유지하고 있었기 때문에 발생한 문제였고, 모달을 해당 페이지컴포넌트 내부로 이동하여 페이지가 원마운트될 때 자연스럽게 사라지도록 하여 버그를 해결했었다

 

근데 문제는 추가 구현사항이었다

1. android: 모달이 열려있을 때 디바이스의 뒤로가기에해당하는 물리버튼을 눌렀을때 모달 닫힘

2. ios: 모달이 열려있을때 스와이프 제스처 막기

 

처음봤을 땐 둘중 하나의 요구사항을 충족하면 나머지하나는 자연스럽게 해결되는것만 같았다 

하지만 내비게이션 관점에서 봤을땐 android환경에선 하나의 history stack을 더 쌓아야만 하고, ios환경에서는 history back을 아예 막아줘야 하는 별개의 요구사항으로 바라봐야 했다(지라 티켓을 별게로 관리해야 했다;;)

 

안드로이드 추가 구현사항 

모달을 열고 물리버튼 뒤로 가기을 눌렀을 때 페이지 이동이 아닌 모달만 닫혀 야하기 때문에 모달이 열렸을 때 임의의 히스토리 스택을 쌓아줘야 한다고 생각했다

구현방법

1. 유저가 모달을 여는 동작을 하면 history.pushState를통해 무의미한 history stack을 하나 쌓아주면서 모달을 열어준다

2. 유저가 모달을 닫는 동작을하면 쌓았던 history stack을 pop 해주면서 모달을 닫아준다

이렇게 되면 실제로 유저입장에선 물리버튼 뒤로 가기를 눌렀을 때 현재 페이지의 history stack이 pop되는 대신에 1번에서 쌓아둔 무의미한 hisotry stack이 pop되면서 페이지의 화면이 아루런 변화가 없는것처럼 느껴지고, 2번에서 설정해준 함수를통해 모달을 닫을 수 있게되어 결과적으로 물리버튼 뒤로가기를 눌렀을때 모달이 닫히는 UX를 경험하게 된다

코드

react+vite+tailwind를 활용하여 구현하였다

 

웹에서 modal을 조작하는 코드를 보면 다음과 같다

import { createPortal } from "react-dom";
import { useState } from "react";

import { Button } from "@components/Button";
import Modal from "@components/Modal";

function App() {
  const [isOpen, setIsOpen] = useState(false);

  const openModal = () => {
    setIsOpen(true);
  };

  const closeModal = () => {
    setIsOpen(false);
  };

  return (
    <>
      {isOpen &&
        createPortal(
          <Modal
            onClose={() => {
              closeModal();
            }}
          />,
          document.body
        )}

      <div className=" bg-red-500 w-screen h-screen">
        <div className="bg-blue-50 h-screen w-80 m-auto flex items-center justify-center">
          <Button onClick={() => openModal()}>모달 열기</Button>
        </div>
      </div>
    </>
  );
}

export default App;

 

 

 

여기에 안드로이드 웹뷰를 위한 코드를 추가로 작성해 보면 다음과 같다

import { createPortal } from "react-dom";
import { useState } from "react";

import { Button } from "@components/Button";
import Modal from "@components/Modal";
import { isAndroid } from "react-device-detect";

function App() {
  const [isOpen, setIsOpen] = useState(false);

  const openModal = () => {
    setIsOpen(true);
  };

  const closeModal = () => {
    setIsOpen(false);
  };

  const openModalInAndroidDevice = () => {
    openModal();
    history.pushState({}, "", "");
  };

  const closeModalInAndroidDevice = () => {
    closeModal();
    history.back();
  };

  return (
    <>
      {isOpen &&
        createPortal(
          <Modal
            onClose={() => {
              if (isAndroid) {
                closeModalInAndroidDevice();

                return;
              }

              closeModal();
            }}
          />,
          document.body
        )}

      <div className="bg-red-500 w-screen h-screen">
        <div className="bg-blue-50 h-screen w-80 m-auto flex items-center justify-center">
          <Button
            onClick={() => {
              if (isAndroid) {
                openModalInAndroidDevice();

                return;
              }

              openModal();
            }}
          >
            모달 열기
          </Button>
        </div>
      </div>
    </>
  );
}

export default App;

window.navigator.userAgent를 활용하면 현재 웹이 동작하고 있는 기기환경을 감지할 수 있다(개발자도구에서 확인가능)

하지만 좀 더 깔끔한 값으로 사용하기 위해 'react-device-detect' 라이브러리를 사용했다

안드로이드 대응을 위한 코드가 늘어났고, 조건문 또한 추가되었다

 

ios 추가 구현사항

ios는 모달이 열려있을 때 뒤로 가기 자체를 막아야 하므로 웹에서 처리가 불가했다

따라서 앱과의 통신이 필수적이었으며, 모달이 열였을 때 앱 쪽에 스와이프를 비활성화하는 트리거함수를 실행시키고, 모달이 닫혔을 때 스와이프를 활성화하는 트리거함수를 실행시키는 방법을 생각했다

구현방법

1. 유저가 모달을 여는 동작을 하면 앱 쪽에 InAppWebView옵션중 allowsBackForwardNavigationGestures를 false로 설정하는 통신을 하면서 모달을 열어준다

2. 유저가 모달을 닫는 동작을하면 앱쪽에 allowsBackForwardNavigationGestures를 true로 설정하는 통신을 하면서 모달을 닫아준다

코드

// 웹
import { createPortal } from "react-dom";
import { useState } from "react";

import { Button } from "@components/Button";
import Modal from "@components/Modal";
import { isAndroid, isIOS } from "react-device-detect";

const activeSwipe = async () => {
  await window.flutter_inappwebview.callHandler("swipe", true);
};

const inactiveSwipe = async () => {
  await window.flutter_inappwebview.callHandler("swipe", false);
};

function App() {
  const [isOpen, setIsOpen] = useState(false);

  const openModal = () => {
    setIsOpen(true);
  };

  const closeModal = () => {
    setIsOpen(false);
  };

  const openModalInAndroidDevice = () => {
    openModal();
    history.pushState({}, "", "");
  };

  const closeModalInAndroidDevice = () => {
    closeModal();
    history.back();
  };

  const openModalInIosDevice = () => {
    openModal();
    activeSwipe();
  };

  const closeModalInIosDevice = () => {
    closeModal();
    inactiveSwipe();
  };

  return (
    <>
      {isOpen &&
        createPortal(
          <Modal
            onClose={() => {
              if (isAndroid) {
                closeModalInAndroidDevice();

                return;
              }

              if (isIOS) {
                closeModalInIosDevice();

                return;
              }

              closeModal();
            }}
          />,
          document.body
        )}

      <div className=" bg-red-500 w-screen h-screen">
        <div className="bg-blue-50 h-screen w-80 m-auto flex items-center justify-center">
          <Button
            onClick={() => {
              if (isAndroid) {
                openModalInAndroidDevice();

                return;
              }

              if (isIOS) {
                openModalInIosDevice();

                return;
              }

              openModal();
            }}
          >
            모달 열기
          </Button>
        </div>
      </div>
    </>
  );
}

export default App;

 

 

앱코드

// flutter
onLoadStop: (controller, url) async {
    controller.addJavaScriptHandler(
        handlerName: 'swipe',
        callback: (args) {
         bool swipeState = args[0];
         InAppWebViewGroupOptions newOptions = InAppWebViewGroupOptions(
            crossPlatform: InAppWebViewOptions(useShouldOverrideUrlLoading: true),
            android: AndroidInAppWebViewOptions(
                useHybridComposition: true,
                supportMultipleWindows: true,
            ),
            ios: IOSInAppWebViewOptions(allowsBackForwardNavigationGestures: swipeState) // 이부분
        );
  	  	webViewController.setOptions(options: newOptions);
       }
  );
}, ...

웹과 통신을 통해 webViewController 객체 내에 존재하는 setOptions으로 새로운 옵션값(newOptions)으로 업데이트해 주는 부분이 핵심이다.

 

웹뷰 환경을 처음 접했고 하필 flutter로 구현된 웹뷰라 구현에 있어서 구현사항 외에 환경에 대해 이해하는 부분에서도 시간이 오래 걸렸다. 특히 ios스와이프를 제어하는 부분에서 대부분의 시간을 사용했다.

공식문서를 통해 allowsBackForwardNavigationGestures가 스와이프를 제어한다는 사실을 아는 데는 오래 걸리지 않았지만, 옵션값을 새롭게 세팅하는 방식에 대해서 예제가 나와있지가 않았다.(못 찾은 것일 수도 있다) 그래서 혹시나 해서 vs코드 내에서 정의부분을 들어가서 인터페이스를 확인해보니 setState와 유사한 setOptions를 확인해서 혹시나 세터함수가 존재하는것이였나?? 하고 적용해본결과 원하는대로 동작했었다. vs코드내 인터페이스 부분을 좀 더 꼼꼼히 살펴보는 습관을 들여야겠다