[Flutter] 實現 iOS 在 UIActivityViewController 加入行事曆

本篇介紹在 iOS 使用 UIActivityViewController 時加入自訂按鈕,以加入行事曆為例。

如果要在 Flutter 呼叫分享很簡單,利用 share plugin 一下就完成了。開發 Flutter 的人一定都知道。

剛好今天有需求是希望在 iOS 分享活動連結時可以多顯示一個自訂的按鈕,讓使用者可以加入到行事曆。

 

要做到 iOS 分享時加入自訂的按鈕,有兩個重要的元素:UIActivityControllerUIActivity

UIActivityController

系統提供多種 services (例如:複製內容到剪貼簿,分享文字到 social media 或 email ...等)。也提供開發者自訂 service 來提供服務。

UIActivityController 集合這些 services 來呈現在畫面上供用戶選擇。可設定該 View Controller 傳遞的資料結構與對應的 services。

最簡單的分享如下:

let shareItems = ["Hello"]
let activityVC = UIActivityViewController(activityItems: shareItems, applicationActivities: nil)

self.presentViewController(activityVC, animated: true, completion: nil)
UIActivity

該類別配合 UIActivityController 使用,如果想要提供自訂的 service 給用戶使用,需要繼承該類別來實作並處理用戶傳入的資料做互動。

需要 override 幾個地方:

 

操作行事曆則需要:EKEventStoreEKEventEditViewController

EKEventStore

管理存取行事曆與提醒權限的元件。需要在初始化後,利用 requestAccess(to:completion:) 來取得存取權限。

需要在 Info.plist 加入  NSRemindersUsageDescription 與 NSCalendarsUsageDescription 的宣告,才能使用  EKEventStore。

EKEventEditViewController

view controller 用來建立,編輯或刪除行事曆的活動。使用該 view congtroller 的 class 需要實作 EKEventEditViewDelegate

 

介紹完 iOS 怎麼使用分享介面(UIActivityViewController) 與操作行事曆(EKEventEditViewController)後,下面串起來從 Flutter 利用 method channel 通知 iOS 顯示分享的介面。

1. 實作 EventActivity 繼承 UIActivity 處理傳入的活動資訊,並呼叫行事曆(EKEventEditViewController)

Info.plist 加入存取行事曆的宣告。

<plist version="1.0">
  <dict>
    ...

    <key>NSCalendarsUsageDescription</key>
    <string>to add this event to your calendar</string>
  </dict>
</plist>

EventActivity.swift

override func prepare(withActivityItems activityItems: [Any]) {
  // 利用 EKEventStore 請求操作 Calendar 的權限
  let eventStore = EKEventStore()
  eventStore.requestAccess(to: .event) { (granted, error) in
    if (granted && error == nil) {
      // 先關閉 UIActivityViewController 再開啟 EKEventEditViewController
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
        // 把 activityItems 帶入的參數包裝成 EKEvent 
        let event = self.genereateEvent(eventStore: eventStore, arguments: activityItems as NSArray);

        if (event == nil) {
          return
        }
        // 利用 EKEventStore 把 EKEvent 加入             
        self.insertEvent(event: event!, eventStore: eventStore)
      }
    } else {
      // 如果被取消授權要顯示訊息告訴使用者
      self.showAccessDeinedOrRestricted()
    }
  }
}

利用 EKEventStore 請求並取得權限之後,利用  DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) 延後 7 秒的方式來開啟 EKEventEditViewController

為什麼需要延後?

因為 rootViewController 開啟了 UIActivityViewController ,無法再從 UIActivityViewController 開一個 ViewController需要先關它後才能再開啟 EKEventEditViewController

 

2. iOS 定義 method channel 接受來自 Flutter 的傳入活動資訊

打開 AppDelegate.swift 加入下面的 code:

override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
  
  guard let controller = window?.rootViewController as? FlutterViewController else {
    fatalError("rootViewController is not type FlutterViewController")
  }
  
  // 定義要處理的 method channel      
  let methodChannel = FlutterMethodChannel(name: "sample.poumason.dev/channels", binaryMessenger: controller.binaryMessenger)
        
  methodChannel.setMethodCallHandler({
    (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
    // 處理 shared 的 method       
    if (call.method == "shared") {
      // 呼叫自訂的 UIActivity 
      self.showSharedActivityViewController(arguments: call.arguments)
        result("OK")
        return
    }
             
    result(FlutterMethodNotImplemented)
  })
        
  GeneratedPluginRegistrant.register(with: self)
  return super.application(application, didFinishLaunchingWithOptions: launchOptions)        
}

詳細介紹整合 method channel 的說明可以參考:Writing custom platform-specific code

 
3. 在 AppDelegate.swift 實現 showSharedActivityViewController method 來呼叫 UIActivityViewController,並加入自訂的 UIActivity
// 要實現 EKEventEditViewDelegate 接受關閉 EKEventEditViewController 的事件
@objc class AppDelegate: FlutterAppDelegate, EKEventEditViewDelegate  {

  func eventEditViewController(_ controller: EKEventEditViewController, didCompleteWith action: EKEventEditViewAction)
  {
      print(action)
      controller.dismiss(animated: true, completion: nil)
  }

  private func showSharedActivityViewController(arguments: Any?) {
    if let args = arguments as? Dictionary<String, Any?> , !args.isEmpty {
            
      guard let url = args["url"] as? String, !url.isEmpty else {
        print("no any be shared data")
        return
      }
            
      // 把傳入的資料裝到一個自訂的 Event 資料結構
      let event = Event.init(title: args["title"] as? String,
                             location: args["location"] as? String,
                             url: args["url"] as? String,
                             startDate: args["startDate"] as? Double,
                             endDate: args["endDate"] as? Double)
            
      let items: [Any]
      let activities: [UIActivity]?
      // 判斷如果是 Event 類型才呼叫自訂的 EventActivity,不然視為一般的分享
      if (event.isValidated()) {
        items = [ url, event ]
        activities = [ EventActivity() ]
      } else {
        items = [ url ]
        activities = nil
      }
            
      let activityVC = UIActivityViewController(activityItems: items, applicationActivities: activities)
      self.window.rootViewController?.present(activityVC, animated: true, completion:  nil)
    }
  }
}

 

4. 從 Flutter 使用 method channel 送出資料
Future<void> _sharedEvent() async {
  // 建立相同 name 的 method channel
  final platform = const MethodChannel('sample.poumason.dev/channels');
  try {
    // 呼叫定義好的 shared method name
    var result = await platform.invokeMethod('shared', {
      'url': _urlKey.currentState.value,
      'title': _titleKey.currentState.value,
      'location': _addressKey.currentState.value,
      'startDate':
         (_startKey.currentState.value.millisecondsSinceEpoch / 1000).roundToDouble(),
      'endDate': (_endKey.currentState.value.millisecondsSinceEpoch / 1000).roundToDouble(),
      });
    print(result);
  } on PlatformException catch (e) {
    print(e.message);
  }
}

method channel 傳遞參數的類型是有限制的,可以參考 Platform channel data types support and codecs 的定義。

 

5. 範例結果
範例程式:share_calednar

===

以上是介紹如何從 Flutter 加入活動資料到 iOS 的行事曆。希望對大家有所幫助,謝謝。

 

參考資料