Add delegate to custom iOS Flutter Plugin - ios

I'm working on integrating a custom iOS plugin into my Flutter app, problem is that I'm not getting delegate callbacks from the custom SDK Protocol.
I have to connect a bluetooth device to my app and I from the delegate calls I should receive the device's ID and pair it.
From the Flutter side, I can call the native functions from the customSdk: sdkInstance.scan() and there are even some internal (inside the sdk) prints with the scan results but my delegate calls are not in place.
I think I'm not correctly adding the delegate to the SDK, I can get this to work in a swift native app but not as a Flutter Plugin.
So here's more or less the code:
iOS Code
AppDelegate.swift
import UIKit
import Flutter
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
SwiftIosPlugin.swift
import Flutter
import UIKit
import CustomSDK
public class SwiftIosPlugin: NSObject, FlutterPlugin {
let sdkInstance = CustomSDK.shared // This returns an instance of the SDK
let channel: FlutterMethodChannel
public static func register(with registrar: FlutterPluginRegistrar)
let channel = FlutterMethodChannel(name: "ios_plugin_channel", binaryMessenger: registrar.messenger())
let instance = SwiftIosPlugin(channel)
registrar.addMethodCallDelegate(instance, channel: channel)
registrar.addApplicationDelegate(instance)
}
init (_ channel: FlutterMethodChannel) {
self.channel = channel
super.init()
// In Swift, this is done in viewDidLoad()
// Is this the correct place to do this?
sdkInstance.addDelegate(self)
}
public func handle(_ call: FlutterMethodCall, result: #escaping FlutterResult) {
switch call.method {
case "startScan":
do {
// This is being called and results printed
try sdkInstance.scan()
} catch {
result(FlutterError(code: "400", message: "\(error)", details: nil))
}
case "connect":
sdkInstance.connect(call, result)
default:
result(FlutterMethodNotImplemented)
}
}
}
// These should be called but are not
extension SwiftIosPlugin: CustomSDKDelegate {
// Isn't called when scan() is executeed!
public func onScanDevice(didScan id:String) {
// do logic
}
public func onPairedDevice(didPair id:String) {
// do logic
}
}

Update:
Silly thing that I hope nobody else has this trouble...
Two things to consider:
The problem was some of the delegate's functions public func onScanDevice(didScan id:String) was missing a parameter (even though there weren't any errors pointed out by Xcode).
sdkInstance.addDelegate(self) was called too early in the class "lifecycle".
Be mindful of these things and you won't have any trouble!

Related

SceneDelegate.swift deprecated/changed architecture? [duplicate]

Now that AppDelegate and SceneDelegate are removed from SwiftUI, where do I put the code that I used to have in SceneDelegate and AppDelegate, Firebase config for ex?
So I have this code currently in my AppDelegate:
Where should I put this code now?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseConfiguration.shared.setLoggerLevel(.min)
FirebaseApp.configure()
return true
}
Here is a solution for SwiftUI life-cycle. Tested with Xcode 12b / iOS 14
import SwiftUI
import UIKit
// no changes in your AppDelegate class
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print(">> your code here !!")
return true
}
}
#main
struct Testing_SwiftUI2App: App {
// inject into SwiftUI life-cycle via adaptor !!!
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Overriding the initializer in your App also works:
import SwiftUI
import Firebase
#main
struct BookSpineApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
BooksListView()
}
}
}
Find a more detailed write-up here:
The Ultimate Guide to the SwiftUI 2 Application Life Cycle
Firebase and the new SwiftUI 2 Application Life Cycle
You should not put that kind of codes in the app delegate at all or you will end up facing the Massive App Delegate. Instead, you should consider refactoring your code to more meaningful pieces and then put the right part in the right place. For this case, the only thing you need is to be sure that the code is executing those functions once the app is ready and only once. So the init method could be great:
#main
struct MyApp: App {
init() {
setupFirebase()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
private extension MyApp {
func setupFirebase() {
FirebaseConfiguration.shared.setLoggerLevel(.min)
FirebaseApp.configure()
}
}
AppDelegate ?
You can have your own custom class and assign it as the delegate. But note that it will not work for events that happen before assignment. For example:
class CustomDelegate: NSObject, UIApplicationDelegate {
static let Shared = CustomDelegate()
}
And later:
UIApplication.shared.delegate = CustomDelegate.Shared
Observing For Notifications
Most of AppDelegate methods are actually observing on notifications that you can observe manually instead of defining a new class. For example:
NotificationCenter.default.addObserver(
self,
selector: #selector(<#T###objc method#>),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
Native AppDelegate Wrapper
You can directly inject app delegate into the #main struct:
#UIApplicationDelegateAdaptor(CustomDelegate.self) var appDelegate
Note: Using AppDelegate
Remember that adding AppDelegate means that you are killing default multiplatform support and you have to check for platform manually.
You can also use the new ScenePhase for certain code that the AppDelegate and SceneDelegate had. Like going to the background or becoming active. From
struct PodcastScene: Scene {
#Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
TabView {
LibraryView()
DiscoverView()
SearchView()
}
}
.onChange(of: phase) { newPhase in
switch newPhase {
case .active:
// App became active
case .inactive:
// App became inactive
case .background:
// App is running in the background
#unknown default:
// Fallback for future cases
}
}
}
}
Example credit: https://wwdcbysundell.com/2020/building-entire-apps-with-swiftui/
Note the method below will stop cross platform support so should only be used if you are planning on building for iOS only.
It should also be noted that this doesn’t use the SwiftUI lifecycle method, instead it allows you to return to the UIKit lifecycle method.
You can still have an AppDelegate and a SceneDelegate when you create a SwiftUI app in Xcode 12-beta.
You just need to make sure that you have chosen the correct option for the Life Cycle when you create your app.
Make sure you choose UIKit App Delegate for the Life Cycle and you will get an AppDelegate and a SceneDelegate
I would also advise in using the main App's init method for this one, as it seems safe to use (any objections?).
What I usually do, that might be useful to share, is to have a couple of utility types, combined with the Builder pattern.
/// An abstraction for a predefined set of functionality,
/// aimed to be ran once, at app startup.
protocol StartupProcess {
func run()
}
/// A convenience type used for running StartupProcesses.
/// Uses the Builder pattern for some coding eye candy.
final class StartupProcessService {
init() { }
/// Executes the passed-in StartupProcess by running it's "run()" method.
/// - Parameter process: A StartupProcess instance, to be initiated.
/// - Returns: Returns "self", as a means to chain invocations of StartupProcess instances.
#discardableResult
func execute(process: any StartupProcess) -> StartupProcessService {
process.run()
return self
}
}
and then we have some processes
struct CrashlyticsProcess: StartupProcess {
func run() {
// Do stuff, like SDK initialization, etc.
}
}
struct FirebaseProcess: StartupProcess {
func run() {
// Do stuff, like SDK initialization, etc.
}
}
struct AppearanceCustomizationProcess: StartupProcess {
func run() {
// Do stuff, like SDK initialization, etc.
}
}
and finally, running them
#main
struct TheApp: App {
init() {
initiateStartupProcesses()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
private extension TheApp {
func initiateStartupProcesses() {
StartupProcessService()
.execute(process: ExampleProcess())
.execute(process: FirebaseProcess())
.execute(process: AppearanceCustomizationProcess)
}
}
Seems quite nice and super clean.
I see a lot of solutions where init gets used as didFinishLaunching. However, didFinishLaunching gets called AFTER init of the App struct.
Solution 1
Use the init of the View that is created in the App struct. When the body of the App struct gets called, didFinishLaunching just happened.
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#ViewBuilder
var body: some Scene {
WindowGroup {
MainView(appDelegate: appDelegate)
}
}
}
struct MainView: View {
init(appDelegate: AppDelegate) {
// at this point `didFinishLaunching` is completed
setup()
}
}
Solution 2
We can create a block to notify us when didFinishLaunching gets called. This allows to keep more code in SwiftUI world (rather than in AppDelegate).
class AppDelegate: NSObject, UIApplicationDelegate {
var didFinishLaunching: ((AppDelegate) -> Void)?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions
launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
didFinishLaunching?(self)
return true
}
}
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#ObservedObject private var applicationModel = ApplicationModel()
// `init` gets called BEFORE `didFinishLaunchingWithOptions`
init() {
// Subscribe to get a `didFinishLaunching` call
appDelegate.didFinishLaunching = { [weak applicationObject] appDelegate in
// Setup any application code...
applicationModel?.setup()
}
}
var body: some Scene {
return WindowGroup {
if applicationObject.isUserLoggedIn {
LoggedInView()
} else {
LoggedOutView()
}
}
}
}

flutter : The screen freezes when I try to output the iOS built-in dictionary with UIReferenceLibraryViewController

I am making an English word learning app with flutter.
I am using the built-in dictionary of iOS.
It is a basic application that the built-in dictionary is displayed when the user presses the "answer confirmation button". I can basically do what I want to do, but I found a bug yesterday.
When I pressed the answer confirmation button for the word "buffalo", the screen froze. The screen itself is displayed instead of being killed, but no further operations are accepted.
It's the first time I've tested over 500 words. The other words are working fine.
The method channel is used to call the built-in dictionary.
//AppDelegate.swift
import UIKit
import Flutter
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
var ref: UIReferenceLibraryViewController = UIReferenceLibraryViewController(term:"")
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
let methodChannel = FlutterMethodChannel(name: "hello_ios", binaryMessenger: controller.binaryMessenger)
methodChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: FlutterResult) -> Void in
if call.method == "searchDictionary" {
print(call.arguments)
self.searchDictionary(result:result,controller:controller,queryWord:call.arguments as! String)
//result("Hello from iOddddddddddddS")
} else {
result(FlutterMethodNotImplemented)
}
})
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func searchDictionary(result: FlutterResult, controller: FlutterViewController, queryWord: String){
print("416\(UIReferenceLibraryViewController.dictionaryHasDefinition(forTerm:queryWord))")
//let ref: UIReferenceLibraryViewController = UIReferenceLibraryViewController(term: queryWord)
ref = UIReferenceLibraryViewController(term:queryWord)
if(UIReferenceLibraryViewController.dictionaryHasDefinition(forTerm:queryWord)){
controller.present(ref, animated: true, completion: nil)
}
}
}
I wrote the above code thinking that it should be conditional branching by
"UIReferenceLibraryViewController.dictionaryHasDefinition",
but it seems that it is not a solution because true is returned even at the time of "buffalo".
Since it works normally with other words, it seems that the caller is not the cause, but the following is the caller.
onPressed: () async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
await HomePage._channel.invokeMethod('searchDictionary',
editingTargetTextCtrl.text != '' ? editingTargetTextCtrl.text : data == null ? '' : data.text,);
},
There are almost no error messages, so I can't get any clues. Is there anything?

Flutter: How to avoid automatic pluggin initialization on Flutter iOS

I'm working with a plugin that I would like to manually register in iOS only if some condition is meet. Right now, the autogenerated AppDelegate.swift registers all plugins included in the pubspect.yaml with this line:
GeneratedPluginRegistrant.register(with: self)
Is there any way to avoid registering a single plugin?
Thank you
Yes, there is a way (after browsing and reading Flutter iOS engine documentation).
write your plugin in Swift (this language is easier to understand. I am bad in objC ;) )
have AppDelegate.swift (in Swift language too)
create plugin in AppDelegate.swift (below AppDelegate class). This if for simplicity. In my case it is:
public class FlutterNativeTimezonePlugin: NSObject, FlutterPlugin {
public static func addManuallyToRegistry(registry: FlutterPluginRegistry) {
// https://api.flutter.dev/objcdoc/Protocols/FlutterPluginRegistry.html#/c:objc(pl)FlutterPluginRegistry(im)registrarForPlugin:
let registrar = registry.registrar(forPlugin: "flutter_native_timezone")
if let safeRegistrar = registrar {
register(with: safeRegistrar)
}
}
// this is an override of this fucnion:
// https://api.flutter.dev/objcdoc/Protocols/FlutterPlugin.html#/c:objc(pl)FlutterPlugin(cm)registerWithRegistrar:
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "flutter_native_timezone", binaryMessenger: registrar.messenger())
let instance = FlutterNativeTimezonePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: #escaping FlutterResult) {
switch call.method {
case "getLocalTimezone":
result(NSTimeZone.local.identifier)
case "getAvailableTimezones":
result(NSTimeZone.knownTimeZoneNames)
default:
result(FlutterMethodNotImplemented)
}
}
}
below GeneratedPluginRegistrant.register(with: self) method add the plugin initialisation one: FlutterNativeTimezonePlugin.addManuallyToRegistry(registry: self)
So this is manual way of adding a plugin for the iOS build. Shame there is no documentation.

How to pass the presenting view controller and client ID for your app to the Google Sign In sign-in method

Hello I have been following (https://firebase.google.com/docs/auth/ios/google-signin) to setup a simple Google SignIn with Firebase in an iOS app. I use Xcode 12.5.1. I created a new Storyboard (UIKit) app. I did pod init and added Firebase and GoogleSignIn and did pod install.
I'm also total noob in iOS.
I'm trying to follow the steps and for non of the steps it really explains where to put the code. For some I think I succeeded, but for the step 4. I have no idea where to put that code. I tried looking for other posts/tutorials for setting up GoogleSignIn with Firebase and SwiftUI and non of them work. Steps seem pretty simple, but they probably omit something very simple that a noob in iOS doesn't know.
Can some1 please explain a bit this step 4 at least.
Some of you commented to put it in the AppDelegate. I have created AppDelegate, but not sure where to put it there. Creating a signIn function in AppDelegate for dumping the code from the step 4 does not work. I also tried to wrap it with UIViewControllerRepresentable like here with the same problem.
It complains that this presenting: self from GIDSignIn.sharedInstance.signIn(with: config, presenting: self) { [unowned self] user, error in (code from step 4 that I don't know where to put) is AppDelegate that can't be converted to UIViewController that is expected.
import SwiftUI
import Firebase
import GoogleSignIn
#main
struct FolderlesApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
#available(iOS 9.0, *)
func application(_ application: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any])
-> Bool {
return GIDSignIn.sharedInstance.handle(url)
}
}
I think you are using SwfitUI not UIkit in the code you provided. If this is true. I can answer the SwiftUI solution.
Assume you have already set up the Step 1 (URL Types) and import the GoogleService-Info.plist. If you don’t, make sure to do it first.
Then, we have to create AppDelegate by ourselves in SwiftUI. You can add this code into your “App” type struct file (usually, the file name is your Project_NameApp.swift). Just like the example down below. This is step 2, step 3 in Authenticate Using Google Sign-In on iOS  |  Firebase
// Project_NameApp.swift
struct Project_NameApp: App {
// add this var define line
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
TestLoginView()
}
}
}
// add this class
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// add this function for init FirebassApp
FirebaseApp.configure()
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
// add this function for Google Sign in to handle the url
return GIDSignIn.sharedInstance.handle(url)
}
}
The next one is your main question - Where the step 4’s code should we be put? Following code is the example SwiftUI view that you want to login with google. You can see it just turn the step 4 code into the function, and bind to the Button you want the user to tap and Sign in with a Google account.
import SwiftUI
import GoogleSignIn
import Firebase
struct TestLoginView: View {
var body: some View {
VStack {
Button(action: {
// Just add the action function, it will works.
googleSignIn()
}, label: {
Text("Google Sign In")
})
}
}
func googleSignIn(){
guard let clientID = FirebaseApp.app()?.options.clientID else { return }
// Create Google Sign In configuration object.
let config = GIDConfiguration(clientID: clientID)
// Start the sign in flow!
GIDSignIn.sharedInstance.signIn(with: config, presenting: (UIApplication.shared.windows.first?.rootViewController)!) { user, error in
if let error = error {
print(error.localizedDescription)
return
}
guard
let authentication = user?.authentication,
let idToken = authentication.idToken
else {
return
}
let credential = GoogleAuthProvider.credential(withIDToken: idToken,
accessToken: authentication.accessToken)
// Authenticate with Firebase using the credential object
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print("authentication error \(error.localizedDescription)")
return
}
print(authResult ?? "none")
}
}
}
}
struct TestLoginView_Previews: PreviewProvider {
static var previews: some View {
TestLoginView()
}
}
I have tested it on my Xcode and it works for me. You can try it and hope you can solve the problem!
Tips 1: This is just a simple code. If you want a better implementation. I recommend you implement with MVVM structure. In this case, you should put the function into your ViewModel which you create by yourself. The button should call the Google Sign in function in your ViewModel.
Tips 2: This code doesn’t implement the multi-factor login with phone verify, check out this article for more detail. Most use case won’t use this less frequently used multi-factor login.

Where best to call updateApplicationContext using Watch Connectivity?

Several of the good blog posts detailing Watch Connectivity (http://www.kristinathai.com/watchos-2-tutorial-using-application-context-to-transfer-data-watch-connectivity-2/ and http://natashatherobot.com/watchconnectivity-application-context/) use simple app examples that send data to the watch when you tap on UI on the iPhone.
My app simply lists the data from my iPhone app, so I don't need to send data immediately, I just wanted to send it when the app loads or enters background...to this end I've made the updateApplicationContext in didFinishLaunching and didEnterBackground...however my dataSource delegate in my watch interface controllers are very spotting at getting triggered...particularly the glance only loads on the simulator and never on device. Is there a better time and place to push the info?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
WatchSessionManager.sharedManager.startSession()
do {
try WatchSessionManager.sharedManager.updateApplicationContext(["peopleDict" : peopleDict])
} catch {
print(error)
}
return true
}
func applicationDidEnterBackground(application: UIApplication) {
do {
try WatchSessionManager.sharedManager.updateApplicationContext(["peopleDict" : peopleDict])
} catch {
print(error)
}
}
below is my WatchSessionManager I used to call activiateSession in my extensionDelegate's appliciationDidFinishLaunching
import WatchConnectivity
protocol DataSourceChangedDelegate {
func dataSourceDidUpdate(dataSource: DataSource)
}
class WatchSessionManager: NSObject, WCSessionDelegate {
static let sharedManager = WatchSessionManager()
private override init() {
super.init()
}
private var dataSourceChangedDelegates = [DataSourceChangedDelegate]()
private let session: WCSession = WCSession.defaultSession()
func startSession() {
session.delegate = self
session.activateSession()
}
func addDataSourceChangedDelegate<T where T: DataSourceChangedDelegate, T: Equatable>(delegate: T) {
dataSourceChangedDelegates.append(delegate)
}
func removeDataSourceChangedDelegate<T where T: DataSourceChangedDelegate, T: Equatable>(delegate: T) {
for (index, indexDelegate) in dataSourceChangedDelegates.enumerate() {
if let indexDelegate = indexDelegate as? T where indexDelegate == delegate {
dataSourceChangedDelegates.removeAtIndex(index)
break
}
}
}
}
// MARK: Application Context
// use when your app needs only the latest information
// if the data was not sent, it will be replaced
extension WatchSessionManager {
// Receiver
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
dispatch_async(dispatch_get_main_queue()) { [weak self] in
self?.dataSourceChangedDelegates.forEach { $0.dataSourceDidUpdate(DataSource(data: applicationContext))}
}
}
}
As updateApplicationContext only stores the newest application context you can update it whenever you like. The watch will only get the newest data. There is no queue with old contexts.
On the watch side the most secure location to activate the session and set the WCSessionDelegate is in the ExtensionDelegate init method:
class ExtensionDelegate: NSObject, WKExtensionDelegate {
override init() {
super.init()
WatchSessionManager.sharedManager.startSession()
}
...
}
Your Glance does not update because when the Glance is shown, applicationDidFinishLaunching is not being called (because the watch app is not launched when only the Glance is launched)

Resources