Flutter 웹앱을 InAppWebView package 사용 기준으로 작성했습니다.

 

[배경]

iOS는 앱이 종료된 상태에서 백그라운드 작업을 실행시킬 수 없습니다.

따라서 알림을 받았을때, 플러터로 알림으로 전달된 데이터를 저장할 수 있는 방법을 찾기 어렵습니다.

 

그렇다면 어떻게 앱이 종료된 상태에서 FCM Push notification으로 전달받은 url 데이터를 앱에 저장할 수 있는가?

 

AppDelegate.swift 를 통해 가능합니다!

 

[원리]

AppDelegate.swift에서는 앱이 종료/백그라운드 상태에서 알림을 받았을때 호출되는 함수를 오버라이드 할 수 있습니다.

 

그 때 !!

 

iOS단에서 UserDefaults에 받아온 데이터를 저장하고 앱이 켜졌을때 Flutter와 채널을 통해 데이터를 전달할 수 있습니다.

 

[구현 방법]

 

1. xcode에서 AppDelegate.swift 파일을 열어서 다음과 같이 코드를 작성해줍니다.

FCM관련 함수가 있으므로 FCM을 안 사용하신다면 코드가 상이할 수 있어요~

import Flutter
import UIKit
import Firebase
import UserNotifications

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Flutter와 연결 채널
        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
        let notificationChannel = FlutterMethodChannel(name: "com.my.test.app/notification",
                                                       binaryMessenger: controller.binaryMessenger)
        notificationChannel.setMethodCallHandler({
            (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
            if call.method == "getNotificationURL" {
                self.getNotificationURL(result: result)
            } else {
                result(FlutterMethodNotImplemented)
            }
        })
        
        // 푸시 알림 권한 요청
        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()
        
        if #available(iOS 10.0, *) {
            UNUserNotificationCenter.current().delegate = self
        }
        
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    
    
    private func getNotificationURL(result: @escaping FlutterResult) {
        if let url = UserDefaults.standard.string(forKey: "notificationURL") {
            result(url)
            // Optionally, clear the URL after retrieving it
            UserDefaults.standard.removeObject(forKey: "notificationURL")
        } else {
            result(nil)
        }
    }
    
    
    override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        
        if let urlString = userInfo["contents"] as? String {
            UserDefaults.standard.set(urlString, forKey: "notificationURL")
        }
        
        completionHandler(.newData)
    }
}

@available(iOS 10, *)
extension AppDelegate {
    override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        if let urlString = userInfo["contents"] as? String {
            UserDefaults.standard.set(urlString, forKey: "notificationURL")
        }
        completionHandler()
    }
}

 

 

2. Flutter 

 

import 'package:flutter/services.dart';

const MethodChannel notiChannel = MethodChannel('com.my.test.app/notification');
class NotificationURLChecker {
  static Future<String?> getNotificationURL() async {
    try {
      final String? url = await notiChannel.invokeMethod('getNotificationURL');
      return url;
    } on PlatformException catch (e) {
      print("${e.message}");
      return null;
    }
  }
}


class _InAppWebViewScreenState extends State<InAppWebViewScreen> {
  final webViewKey = GlobalKey();
  late String initialUrl;

  @override
  void initState() {
    super.initState();
    initialUrl = _loadInitialUrl();
  }

  String _loadInitialUrl() async {
      String? notificationUrl = await NotificationURLChecker.getNotificationURL();
      return notificationUrl ?? "기본url";
  }

  @override
  Widget build(BuildContext context) {
          return Scaffold(
              body: SafeArea(
                child: Column(
                  children: <Widget>[
                    Expanded(
                      child: InAppWebView(
                        key: webViewKey,
                        initialUrlRequest: URLRequest(url: WebUri(initialUrl)),
                        onWebViewCreated: (controller) {
                          WebControllerSingleton().webController = controller;
                        },
                        // ... (rest of the InAppWebView configuration)
                      ),

 

 

3. 백엔드

FCM 으로 요청을 보낼때 POST로 보낼 Json Data에 '// 코드 추가' 라고 적은 라인을 추가합니다.

// google api request json data
{
  "message": {
    "topic": "news",
    "notification": {
      "title": "Breaking News",
      "body": "New news story available."
    },
    "data": {
      "contents": "url" // 코드 추가 (원하는 url 작성)
      "click_action": "FLUTTER_NOTIFICATION_CLICK", // 코드 추가
    },
    "android": {
      "notification": {
        "click_action": "TOP_STORY_ACTIVITY",
        "body": "Check out the Top Story"
      }
    },
    "apns": {
      "payload": {
        "aps": {
            "alert": {
               "title": "Notificaion Title",
               "body": "알림 내용"
            },
          "content-available": 1, // 코드 추가
          "mutable-content": 1, // 코드 추가
          "interruption-level": "active" // 코드 추가
        }
      }
    }
  }
}

+ Recent posts