배경:

테스트폰을 18.2 버전으로 업데이트 한 뒤, 잊고있었다가 똑같은 코드로 xcode로 앱을 실행시켰는데 흰 화면이 나타남.

 

증상:

플러터 기존 앱 코드를 실행했을 때, 안드로이드는 정상작동.

하지만 iOS 기기에서만 웹뷰가 처음 로드될 때 흰 화면이 나타남.

앱 실행 - 홈탭의 웹뷰가 onWebViewCreated 까지만 실행 - 흰 화면 - 다른 탭은 정상 연결 

 

로그:

Warning: -[BETextInput attributedMarkedText] is unimplemented

nw_application_id_create_self NECP_CLIENT_ACTION_GET_SIGNED_CLIENT_ID [80: Authentication error]

Failed to resolve host network app id

Invalidating grant <invalid NS/CF object> failed

Error acquiring assertion: <Error Domain=RBSServiceErrorDomain Code=1 "((target is not running or doesn't have entitlement com.apple.developer.web-browser-engine.rendering AND target is not running or doesn't have entitlement com.apple.developer.web-browser-engine.networking AND target is not running or doesn't have entitlement com.apple.developer.web-browser-engine.webcontent))" UserInfo={NSLocalizedFailureReason=((target is not running or doesn't have entitlement com.apple.developer.web-browser-engine.rendering AND target is not running or doesn't have entitlement com.apple.developer.web-browser-engine.networking AND target is not running or doesn't have entitlement com.apple.developer.web-browser-engine.webcontent))}>

 

이런 경고 로그가 자꾸 나타남..

결과적으론 저 로그는 별 의미가 없긴했지만.

 

히스토리:

 

  1. Info.plist 수정
  2. Entitlements 수정
  3. Singletone 수정
  4. 웹뷰 로드 타이밍 수정
  5. Url 수정 테스트
  6. Domain exception 추가
  7. Capability 추가 수정
  8. InAppWebView 설정 수정
  9. Xcode 업데이트
  10. Firebase app name 일치 테스트
  11. Flutter clear, pub get 시도
  12. iOS 캐시 삭제 재시작 시도
  13. 웹뷰 init 완료된 직접 url 연결
  14. Flutter 업데이트

위와 같은 삽질을 했다.. 

기존에 작성한 글은 그대로 남겨둔다.

 

2025 Feb 10 

: 구글링해서 에러를 해결하기 위한 단서들을 찾았지만 별 의미 없었음

 

1. 

https://github.com/apache/cordova-ios/issues/1440

 

IOS report "Failed to resolve host network app id" error, Android is working properly · Issue #1440 · apache/cordova-ios

Bug Report Problem The Android platform app is working normally, and then we added the iOS platform. When running in the emulator, a white screen appears and the console reports "Failed to resolve ...

github.com

 

 

2.

https://developer.apple.com/forums/thread/762223?answerId=812340022#812340022

 

WKWebView can't connect to externa… | Apple Developer Forums

Finally solved: it turned out I just needed to set the 'customUserAgent' property of the WKWebView. It's not clear why this wasn't required in iOS versions prior to iOS 17.5, but in any case the behavior seems to be working correctly again with this fix!

developer.apple.com

 

InAppWebView 설정에서 다음 코드 추가

userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/605.1.15",

 

 

그런데 2번은 내겐 아무 의미가 없었다.

아직 해결하지 못해서 기록차원에서 작성

 

하.. 안드로이드는 참 잘되는데... 아이폰.....

 

 

---------

 

2025 Feb 11

: 드디어 원인을 발견했다.

iOS를 18.2 로 업데이트 해서 이전 버전의 Flutter 혹은 패키지들과 호환 오류가 발생함.

 

원인을 발견한 계기는 다음 코드를 실행.

...

onWebViewCreated: (controller) {
    // 여기에서 강제로 url 로드
    controller.loadUrl(
        urlRequest: URLRequest(url: WebUri("https://www.google.co.kr"))
    );
 },
 
 ...

 

 

이렇게 했더니 웹뷰에 탭 이벤트가 안 먹혔다.

화면은 나타나지만 탭이 안 되는 상황... 그렇게 검색했더니 드디어 정확한 원인을 확인했다..!

 

https://github.com/pichillilorenzo/flutter_inappwebview/issues/2415

 

Tap interactions not working on iOS 18.2 · Issue #2415 · pichillilorenzo/flutter_inappwebview

Is there an existing issue for this? I have searched the existing issues Current Behavior Once I interact with any widget in the application that isn't the web view, the webview stops recognizing a...

github.com

 

InAppWebView 제작자의 예제 코드:

https://github.com/flutter/flutter/issues/159911#issuecomment-2539973403

 

[CP][For 3.28 and NOT 3.27!!!][ios][platform_view] workaround for non-tappable webview · Issue #159911 · flutter/flutter

Issue Link flutter/engine#57030 Target beta Cherry pick PR Link flutter/engine#57032 Changelog Description Fix an issue on iOS 18.2 where web view's link is not tappable. Impacted Users all end cus...

github.com

 

 

해결방법 :

 

1. 흰화면 오류 해결 / whiteScreen :

 

만약 웹뷰 설정시 다음 값을 사용하고있는지 확인하자.

useShouldOverrideUrlLoading: true,

 

 

값이 true 일 때 웹뷰 구현코드에 "shouldOverrideUrlLoading"이 작성되어있어야 했다.

 

@override
Widget build(BuildContext context) {
return Scaffold(
        appBar: AppBar(title: const Text("Example")),
        body: SafeArea(
        child: Column(children: <Widget>[
        Expanded(
                child: Stack(
                  children: [
                    InAppWebView(
                      key: webViewKey,
                      initialUrlRequest:
                      URLRequest(url: WebUri("https://flutter.dev/")),
                      initialSettings:
                      InAppWebViewSettings(isInspectable: kDebugMode),
                      onWebViewCreated: (controller) {
                        webViewController = controller;
                      },
                      onLoadStart: (controller, url) {
                        setState(() {
                          this.url = url.toString();
                          urlController.text = this.url;
                        });
                      },
                      onLoadStop: (controller, url) async {
                        setState(() {
                          this.url = url.toString();
                          urlController.text = this.url;
                        });
                      },
                      // 여기서부터 필요한 코드
                      shouldOverrideUrlLoading: (controller, navigationAction) {
                        return NavigationActionPolicy.ALLOW;
                      },
                      ...

 

만약 사용중이라면?

저 코드때문에 iOS 기기에서 흰화면 오류가 발생한다.

 

저 코드가 원인이었던 것!!!!!!

 

하지만 나는 android에서는 저 코드가 필요했기때문에 다음과 같이 수정했다.

 

shouldOverrideUrlLoading: Platform.isAndroid ?
    (controller, navigationAction) async {
  var uri = navigationAction.request.url!;
  // 기존 코드
  return NavigationActionPolicy.ALLOW;
} : null,

 

이러면 문제 해결 !

 

 

iOS 업데이트 할때마다 참 즐겁다..

 

 

 

2. 웹뷰에서 터치 오류 해결:

 

- Flutter Upgrade 3.27.4

https://flutter-ko.dev/development/tools/sdk/releases

 

Flutter SDK archive

All current Flutter SDK releases: stable, beta, and main.

docs.flutter.dev

 

- Xcode Upgrade 16.2

 

 

Xcode 16.2 버전부터 iOS 18.2 버전 케어하니까 업데이트를 해주자..

 

ERROR - Database connection failed: (20002, b'DB-Lib error message 20002,

severity 9: Adaptive Server connection failed 

DB-Lib error message 20002, severity 9:Adaptive Server

 

 

api 서버 마이그레이션 작업 중에 이런 에러가 발생했다.

 

해결방법 : 

 

self.connection_params = {
    'server': settings.MS_HOST,
    'port': settings.MS_PORT,
    'user': settings.MS_USER,
    'password': settings.MS_PASSWORD,
    'database': settings.MS_DB,
    'tds_version': '7.0' // 추가!!!!!!!!
}

 


에러 배경:

 

Flutter Flavor 를 통해 앱을 Dev, Prod 모드로 분리시켜서 빌드 시키기 위해 개발을 하고있었다.

Xcode에서 Debug-Dev, Debug-Prod, Release-Dev, Release-Prod, Profile-Dev, Profile-Prod 로 configurations 생성하고 스키마도 추가해서 알맞게 적용까지 완료.

기존에 있었던 파이어베이스도 잘 분리시켜서 적용해서 Xcode로 빌드까지 성공했다.

 

그런데 Xcode로 빌드한 앱이 기기에서 그냥 실행하면 크래시가 발생했다.

 

에러:

Terminal로 연결해서 빌드해도 잘 되고 Xcode로 연결해서 빌드해도 문제 없이 잘 작동되던 앱이

기기에서 실행만 하면 크래시가 발생하는 현상 발생. 

빌드하지 않고 기기에서 앱을 실행하면 앱이 터진다.

 

콘솔로 확인한 에러 메시지:

 

[ERROR:flutter/runtime/ptrace_check.cc(75)] Could not call ptrace(PT_TRACE_ME): Operation not permitted

Cannot create a FlutterEngine instance in debug mode without Flutter tooling or Xcode.

 

To launch in debug mode in iOS 14+, run flutter run from Flutter tools, run from an IDE with a Flutter IDE plugin or run the iOS project from Xcode.

 

Alternatively profile and release mode apps can be launched from the home screen.

 

 

최근 Xcode를 15로 업데이트 했더니 이런 새로운 이슈가 발생했다. ㅎㅎ 즐거운 iOS 앱개발..

 

 

원인: 

configuration이 Debug 로 선택되어있으면 기기에서 앱을 단독으로 실행시킬 때 권한이 없어서 크래시가 발생한다.

즉, 빌드할때 사용하는 configuration을 Release 로 설정해야한다.

 

 

해결방안:

 

 

1. 빌드할때 사용하는 스키마를 선택 - Edit Scheme 클릭

 

 

2. 앱을 빌드할때 사용하는 configurations를 수정하자.

Run - Info - Build Configuration - Release 

 

 

 

이렇게 빌드하면 Xcode와 터미널로 실행하지 않아도 기기에서 앱이 제대로 실행된다.

 

별 것 아닌 이유로 오랜 시간을 허비했다... ㅠ


하 ㅋㅋㅋㅋ ㅋ구글 공식문서에 속았다.
나를 속인 문서 링크 : https://firebase.google.com/docs/cloud-messaging/send-message?hl=ko&_gl=1*6icy9e*_up*MQ..*_ga*Mzk1OTMyMTQ1LjE3MzEzOTk2OTc.*_ga_CW55HF8NVT*MTczMTM5OTY5Ny4xLjAuMTczMTM5OTY5Ny4wLjAuMA..#python_1

 

분명 이렇게 하면 된다고 적어놨으면서!!!!!!

# Create a list containing up to 500 registration tokens.
# These registration tokens come from the client FCM SDKs.
registration_tokens = [
    'YOUR_REGISTRATION_TOKEN_1',
    # ...
    'YOUR_REGISTRATION_TOKEN_N',
]

message = messaging.MulticastMessage(
    data={'score': '850', 'time': '2:45'},
    tokens=registration_tokens,
)
response = messaging.send_multicast(message)
# See the BatchResponse reference documentation
# for the contents of response.
print('{0} messages were sent successfully'.format(response.success_count))

 

 

 

 

결론부터 말하자면 

Messaging.sendMulticast() is deprecated.

수많은 501 에러와 

Operation is not implemented, or supported, or enabled.
 
에러 메시지를 받으며 원인을 찾아봤는데, 이제 사용하지 않아서 에러가 발생하는 것이다.
 
 
 
 
이제 사용하지 않는다고 적어둔 공식문서 :
 

https://firebase.google.com/docs/reference/admin/node/firebase-admin.messaging.messaging?hl=ko#messagingsendmulticast

 

Messaging class  |  Firebase Admin SDK

 

firebase.google.com

 

 

 

해결책은 공식문서에 적혀있지만, 요약 정리를 해보자면

 

Messaging.send_multicast() 를 대신해서

Messaging.send_each_for_multicast() 사용하면 해결 된다.

 

 

 

기존에 사용하던 rest api 서버에서 fast api 서버를 새로 구축하여 마이그레이션을 진행중이다.

그런데 api 서버는 하나지만 지원하고있는 프론트엔드가 여럿일 경우였다.

큰 규모의 서비스를 제공하는 것이 아니고 

작은 규모로 여러개의 서비스를 제공하는 형태였기때문에 이런 구조를 Fast api로 구현이 가능할지 궁금했다.

 

Fast api는 독립된 환경으로 여러 서비스를 제공하는 것이 가능한가?

 

답은 가능하다.

 

다음은 목표로 삼았던 구조 형식의 예제이다.

 

 

home/
├── app/
   ├── __init__.py
   ├── core/
      ├── __init__.py
      ├── config.py
      ├── logging.py
      └── middleware.py
   ├── projectA/
      ├── __init__.py
      ├── main.py
      ├── api/
         ├── __init__.py
         └── v1/
             ├── __init__.py
             ├── users.py
             └── posts.py
      ├── models/
         ├── __init__.py
         └── user.py
      └── schemas/
          ├── __init__.py
          └── user.py
   ├── projectB/
      ├── __init__.py
      ├── main.py
      ├── api/
         ├── __init__.py
         └── v1/
      ├── models/
      └── schemas/
   ├── projectC/
      ├── __init__.py
      ├── main.py
      ├── api/
         ├── __init__.py
         └── v1/
      ├── models/
      └── schemas/
   ├── db/
      ├── __init__.py
      └── database.py
├── .env
└── requirements.txt

 

 

이런 구조로 만들고 projectA, projectB, projectC를 systectl에 각각 서비스로 올려두면

FastAPI 서버를 유지하면서 각 프로젝트별로 독립적인 서비스 제공이 가능하다.

 

요새 FAST API로 api 서버를 구축하고있다.

 

가상 컴퓨터에서 ubuntu를 설치하고 Fast API를 설치했는데 문제는 접속할때마다 서버를 실행해줘서 번거롭다.

그래서 리눅스의 system을 활용해서 항상 서비스를 실행시킬 수 있도록 설정했다.

 

어떻게 그게 가능한가? 

 

redhat 참조:

https://docs.redhat.com/ko/documentation/red_hat_enterprise_linux/7/html/system_administrators_guide/chap-managing_services_with_systemd

 

즉, 리눅스에서 윈도우의 시스템 제어판같은 역할을 하는 systemd 를 활용해서 설정하는것.

OS에서 설정을 해주는 것으로 이해하면 된다.

 

데몬을 활용하는 명령어 systemctl을 사용하여 서비스를 관리할 수 있다. 

명령어 설명은 redhat의 다음 페이지 참조:
https://docs.redhat.com/ko/documentation/red_hat_ceph_storage/7/html/administration_guide/starting-stopping-and-restarting-all-the-ceph-daemons_admin

 

 

 

작업에 필요한 내용만 정리하자면 이렇게 볼 수 있다.

 

 

1. /etc/sytemd/sytem 디렉토리에 생성하려는 서비스이름으로 .service 파일을 만든다.

sudo nano /etc/systemd/system/서비스이름.service

 

2. 서비스 파일 코드 설정

[Unit]
Description=FastAPI Application # 서비스 설명
After=network.target 

[Service]
User=root  # 사용자
Group=dev # 사용할 그룹
WorkingDirectory=/var/www/application # 실행시키려는 서비스의 위치
Environment="PATH=/var/www/application/venv/bin" # 가상환경으로 실행해야하는 경우 
ExecStart=/usr/local/bin/uvicorn main:app --host 0.0.0.0 --port 1234 # 실행해야하는 파일
Restart=always # 항상 재시작하도록 설정
RestartSec=3 # 3초

[Install]
WantedBy=multi-user.target # 유닛 등록 종속성검사

 

nano 편집기는 ctrl + s 로 저장, ctrl + x 로 종료시킬 수 있다.

 

3. 서비스 설정을 저장했다면 systemd에 해당 설정을 업데이트할 수 있도록 재시작을 한다.

sudo systemctl daemon-reload

 

4. 서비스를 자동시작 할 수 있도록 설정 

sudo systemctl enable service-name

 

5. 서비스 시작

sudo systemctl start service-name

 

 

설정을 마쳤다면 서비스가 잘 실행되고 있는지 확인이 필요하다.

다음은 서비스 실행을 확인할 수 있는 코드이다.

 

현재 서비스가 실행중인지 확인

sudo systemctl status service-name

 

 

실행중인 서비스의 로그를 실시간으로 확인

sudo journalctl -u service-name -f

 

'Linux' 카테고리의 다른 글

[Linux] 사용중인 포트 종료 port kill  (0) 2024.08.08

 

FCM 구현은 잘 적힌 글이 많으니 생략하겠습니다.

 

- 구현하고싶은 알림 화면 ( Heads-up ) push notification image

 

구현하는데 애먹은 부분은 안드로이드에서 FCM 을 받았을때

앱이 종료/비활성화 일때 다음과 같이 배너 알림 (혹은 heads-up 알림) 띄우기였습니다.

 

결론부터 말하자면 해결책은 백엔드에 있다!!

 

 

다음은 구글에서 안내하는 서버 알림 예제입니다.

https://firebase.google.com/docs/cloud-messaging/migrate-v1?_gl=1*14deht6*_up*MQ..*_ga*MjA1NTg3NzcwNS4xNzI4MzU0MjAz*_ga_CW55HF8NVT*MTcyODM1NDIwMy4xLjAuMTcyODM1NDIyMC4wLjAuMA..

 

기존 HTTP에서 HTTP v1로 마이그레이션  |  Firebase Cloud Messaging

의견 보내기 기존 HTTP에서 HTTP v1로 마이그레이션 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. FCM의 기존 HTTP API를 사용하는 앱은 이 가이드의 안내에 따라

firebase.google.com

 

 

{
  "message": {
    "topic": "news",
    "notification": {
      "title": "Breaking News",
      "body": "New news story available."
    },
    "data": {
      "story_id": "story_12345"
    },
    "android": {
      "notification": {
        "click_action": "TOP_STORY_ACTIVITY",
        "body": "Check out the Top Story"
      }
    },
    "apns": {
      "payload": {
        "aps": {
          "category" : "NEW_MESSAGE_CATEGORY"
        }
      }
    }
  }
}

 

위와 같이 예제를 따라 서버단에서 FCM 에 전송할 json을 구현했다고 가정하면,

flutter 단에서는 해당 데이터를 받아 패키지나 firebase 함수를 통해 메시지를 화면에 띄워줍니다.

 

 

 

Flutter단에서 필요한 작업 :

 

1. AndroidManifest.xml 

<meta-data
    android:name="com.google.firebase.messaging.default_notification_channel_id"
    android:value="high_importance_channel"
    />

 

 

2. FlutterLocalNotificationPlugin 설정 

    final NotificationDetails notiDetail = const NotificationDetails(
      android: AndroidNotificationDetails(
        'high_importance_channel',
        'High Importance Notifications',
        importance: Importance.high,
        priority: Priority.high,
        playSound: true,
        enableVibration: true,
        icon: "mipmap/m_logo",
      ),
      iOS: DarwinNotificationDetails(
        presentAlert: true,
        presentBadge: true,
        presentSound: true,
        presentBanner: true,
      ),
    );

      await flutterLocalNotificationsPlugin
          .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
          ?.createNotificationChannel(
          const AndroidNotificationChannel(
            'high_importance_channel', // id
            'High Importance Notifications', // title
            description:
            'This channel is used for important notifications.', // description
            importance: Importance.high,
            showBadge: true,
          )
      );

 

 

백엔드에서 해주어야할 작업:

 

{
  "message": {
    "topic": "news",
    "notification": {
      "title": "Breaking News",
      "body": "New news story available."
    },
    "data": {
      "story_id": "story_12345"
    },
    "android": {
    "priority": "high", // 코드 추가
      "notification": {
      	"channel_id": "high_importance_channel", // 코드 추가
        "click_action": "TOP_STORY_ACTIVITY",
        "body": "Check out the Top Story"
      }
    },
    "apns": {
      "payload": {
        "aps": {
          "category" : "NEW_MESSAGE_CATEGORY"
        }
      }
    }
  }
}

 

 

'// 코드 추가' 주석을 달아놓은 줄을 추가해주면 된다.

 

메시지를 전달할때 안드로이드에서 priority와 channel_id 를 추가하는 것이 중요!

 

InAppWebView 6.0 버전 기준으로 작성했습니다.

 

web에서 mailto: 로 만들어둔 링크를 터치하면 아무 반응이 없다면 

inAppWebView 에서 따로 처리해주어야합니다.

 

1. 안드로이드 설정

AndroidManifest.xml  에 다음과 같이 코드를 추가합니다.

이때, 꼭!!!!! application 밖에, uses-permission 과 같은 뎁스에서 작성해주셔야합니다.

저는 manifest 하단부분에 작성했습니다.

    <queries>
        <intent>
            <action android:name="android.intent.action.SENDTO" />
            <data android:scheme="mailto" />
        </intent>
    </queries>

 

 

2. iOS 설정

info.plist 에 다음 코드를 추가합니다.

    <key>LSApplicationQueriesSchemes</key>
    <array>
      <string>mailto</string>
    </array>

 

3.

inAppWebView 위젯 Widget build(BuildContext context) 

내부에서 다음과 같이 코드를 작성해주면 됩니다.

                  shouldOverrideUrlLoading: (controller, navigationAction) async {
                    var uri = navigationAction.request.url!;
                    if (uri.scheme == 'mailto') {
                      // Properly encode the mailto URL
                      final encodedSubject = Uri.encodeComponent(uri.queryParameters['subject'] ?? '');
                      final encodedBody = Uri.encodeComponent(uri.queryParameters['body'] ?? '');
                      final mailtoUri = Uri.parse('mailto:${uri.path}?subject=$encodedSubject&body=$encodedBody');

                      try {
                        if (await canLaunchUrl(mailtoUri)) {
                          await launchUrl(mailtoUri);
                        } else {
                          print("Could not launch $mailtoUri");
                        }
                      } catch (e) {
                        print("Error launching mail app: $e");
                      }
                      return NavigationActionPolicy.CANCEL;
                    }
                    return NavigationActionPolicy.ALLOW;
                  },

 

 

이렇게 설정해두면 링크 클릭시 메일 앱으로 연동됩니다.

 

iOS 에서는 잘 되는데 안드로이드에서는 인앱웹뷰를 통해 만든 웹뷰 화면에서 다운로드 링크를 눌렀을 경우,

아무 일도 일어나지 않는다.

 

그래서 찾은 가장 쉬운 해결방법!

 

1. 패키지를 다운받는다. 

그런데 InAppWebView를 사용하려면 받아두었을지도?

 

https://pub.dev/packages/url_launcher

 

url_launcher | Flutter package

Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes.

pub.dev

 

 

2. AndroidManifest.xml에 다음 코드 추가

    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

 

 

 

3. 다운로드 요청을 잡아서 수동으로 처리


child: InAppWebView(
                  key: webViewKey,
                  initialUrlRequest: URLRequest(url: WebUri(initUrl)),
                  onWebViewCreated: (controller) {
                    webController = controller;
                  },
                  
                  ...
                  
                  // 하기 코드 추가

                  onDownloadStartRequest: (controller, downloadRequest) async {
                    String downloadUrl = downloadRequest.url.toString();

                    if (Platform.isAndroid) {
                      if (await canLaunchUrl(WebUri(downloadUrl))) {
                        await launchUrl(WebUri(downloadUrl));
                      } else {
                        throw 'Could not launch $downloadUrl';
                      }
                    }

                  },
                  
                  ...

 

 

이렇게 처리하면 번거롭게 다른 패키지들을 설치하지 않아도 다운로드가 된다.

 

 

Flutter 에서 안드로이드 고유 번호를 확인하는 방법!

이걸 확인하기 위해서 안드로이드 native와 채널로 소통하여 받아오도록 구현했다.

 

 

1. project로 폴더를 확인할 때,

/android/app/src/main/jotlin/com/[패키지폴더]/app/MainActivity.kt

 

class MainActivity: FlutterActivity() {
    private val CHANNEL = "device_info_channel"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Android SSAID
        flutterEngine?.let {
            MethodChannel(it.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
                if (call.method == "getAndroidID") {
                    val androidID = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
                    result.success(androidID)
                } else {
                    result.notImplemented()
                }
            }
        }
    }
}

 

 

이렇게 네이티브단에서 설정을 통해 android ID 값을 받는다.

 

 

2. Flutter 

  static Future<String?> getAndroidID() async {
    const MethodChannel _channel = MethodChannel('device_info_channel');
    final String? androidId = await _channel.invokeMethod('getAndroidID');
    return androidId;
  }

 

 

플러터는 원하는 곳에서 받아오는 함수를 작성하여 호출 해서 사용한다.

 

 

플러터 웹뷰를 사용중에 화면을 당겨서 웹페이지 새로고침을 하는 기능이 있다.

이때, 새로고침이 완료되었음에도 상단에서 로딩 아이콘이 계속 돌아가는 이슈 해결 방법을 공유한다.

 

  void initState() {
    super.initState();

    pullToRefreshController = kIsWeb
        ? null
        : PullToRefreshController(
      settings: PullToRefreshSettings(
        color: Colors.black,
      ),
      onRefresh: () async {
        if (Platform.isAndroid) {
          webController?.reload();
        } else if (Platform.isIOS) {
          webController?.loadUrl(
            urlRequest:
            URLRequest(
              url: await webController?.getUrl()),
          );
        }

        pullToRefreshController?.endRefreshing(); // 이 코드 추가
      },
    );
  }

 

 

고난과 역경의 시간이었다.

 

Flutter - iOS device app badge

 

플러터로 개발하는 아이폰 앱 배지에 알림 숫자를 컨트롤 하기 위해서는 수동으로 swift를 수정해주어야한다.

안타까운 플러터 개발자들을 위해 왜 xcode로 직접 코드를 짜야하는 배경을 설명하자면,

 

1. FlutterLocalNotificaionPlugin package

: 이 패키지에 badgeNumber가 있지만 전혀 소용이 없다.

final NotificationDetails notiDetail = const NotificationDetails(
  android: AndroidNotificationDetails(
    'high_importance_channel',
    'High Importance Notifications',
    importance: Importance.high,
    priority: Priority.high,
    playSound: true,
    enableVibration: true,
    icon: "mipmap/m_logo",
  ),
  iOS: DarwinNotificationDetails(
    presentAlert: true,
    presentBadge: true,
    presentSound: true,
    presentBanner: true,
    badgeNumber: 3
  ),
);

 

이에 관련한 개발자의 답변:

 

https://github.com/MaikuB/flutter_local_notifications/issues/81

 

Support for notification badges on iOS and Android? · Issue #81 · MaikuB/flutter_local_notifications

Do you plan to support app icon notification badges for iOS and Android? :) Or maybe I just overlooked the functionality in the current plugin. Anyhow, would love to see the feature built in. My us...

github.com

 

 

2. FlutterAppBadger package

: discontinued. 더이상의 업데이트가 안되어있어서 최근 개발을 진행하는 프로젝트에는 소용이없다.

 

3. 서버단에서 해결

https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification

ios badge는 서버단에서 해결하면 아주... 쉽게 해결된다.

 

24/09/02 수정 - 코드는 앱이 백그라운드일때만 작동됩니다.

앱이 종료되었을땐 실행이 안됩니다. 

 

포스트를 삭제하려다가 빅데이터를 위해 남겨두기로 결정.

앱 종료시 실행이 안되고 버그처럼 나타나서 저는 서버에서 핸들링하기로 결정했습니다.

 

기존 포스트 내용은 접어두었습니다.

 

더보기

위와 같은 배경으로 나는 swift를 통해 iOS badge count 개발을 진행해야했다.

 

AppDelegate.swift 파일을 수정하면된다.

FCM message 를 전달받았을때 native 에서 받은 메시지를 바로 반영해준다.

 

import Flutter
import UIKit
import Firebase
import UserNotifications
import os.log

@main
@objc class AppDelegate: FlutterAppDelegate {

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        
        if #available(iOS 10.0, *) {
            UNUserNotificationCenter.current().delegate = self
            UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
                if granted {
                    os_log("iOS: Notification permission granted.", type: .info)
                } else if let error = error {
                    os_log("iOS: Notification permission error: %@", type: .error, error.localizedDescription)
                }
            }
        }
        
        application.registerForRemoteNotifications()
        
        FirebaseApp.configure()
        
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    // This method will be called when the app receives a notification in background or terminated state
    override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        os_log("### iOS: Received a remote notification", type: .info)
        
        // Handle the notification data (e.g., for Firebase Analytics)
        Messaging.messaging().appDidReceiveMessage(userInfo)
        
        // Increment the badge count and update the app icon
        if let badgeCount = userInfo["badge"] as? Int {
            setBadgeCount(badgeCount, application: application)
        } else {
            // Increment the badge count locally if "badge" not provided in payload
            let currentBadgeCount = UserDefaults.standard.integer(forKey: "badgeCount")
            let newBadgeCount = currentBadgeCount + 1
            UserDefaults.standard.set(newBadgeCount, forKey: "badgeCount")
            setBadgeCount(newBadgeCount, application: application)
        }
        
        // Call the completion handler to let the system know the fetch is complete
        completionHandler(.newData)
    }
    
    // method to set the badge count
    func setBadgeCount(_ count: Int, application: UIApplication) {
        os_log("### iOS: Setting badge count to %d", type: .info, count)
        if #available(iOS 16.0, *) {
            UNUserNotificationCenter.current().setBadgeCount(count) { error in
                if let error = error {
                    os_log("Failed to set badge count: %@", type: .error, error.localizedDescription)
                } else {
                    os_log("Badge count set to %d", type: .info, count)
                }
            }
        } else {
            application.applicationIconBadgeNumber = count
            os_log("Badge count set to %d using deprecated method", type: .info, count)
        }
    }
    
    // 앱이 실행될때 쌓아둔 알림 카운트를 모두 초기화
    override func applicationDidBecomeActive(_ application: UIApplication) {
        UserDefaults.standard.set(0, forKey: "badgeCount")
        setBadgeCount(0, application: application)
    }
}

 

 

os_log 는 콘솔로 앱이 background 상태일때 로그를 찍기위해 사용했으니 필요 없다면 생략하면 된다.

 

그리고 더 중요한 점,

 

서버단에서 FCM payload를 수정해주어야한다.

 

{
  "message": {
    "topic": "테스트토픽",
    "data": {
        "title": "TEST 3",
        "body": "data - body message"
    },
    "apns": {
      "payload": {
        "aps": {
          "alert": {
            "title": "Breaking News ",
            "body": "New news story available."
          },
          "content-available": 1,
          "interruption-level": "active" 
        }
      }
    }
  }
}

 

 

나는 notification을 사용하지 않아 빼두었지만, 필요하다면 추가해서 사용하면 된다.

중요한 점은 apns 안의 구조이므로 참조해서 사용하시길...

 

+ Recent posts