diff --git a/ios/Podfile b/ios/Podfile index 3997894f..f93ebf97 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '13.0' +platform :ios, '14.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 5226e77b..b9194cfe 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,118 +1,150 @@ -import UIKit import Flutter +import UIKit import UniformTypeIdentifiers @main -@objc class AppDelegate: FlutterAppDelegate { - - var pickerDelegate: FolderPickerDelegate? - - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - if #available(iOS 10.0, *) { - UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate - } - let controller : FlutterViewController = window?.rootViewController as! FlutterViewController - let channel = FlutterMethodChannel(name: "app.wispar.wispar/database_access", binaryMessenger: controller.binaryMessenger) - - channel.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in - guard let self = self else { return } - - switch call.method { - case "getExportDirectory": - // Temporary access for export/import path selection - self.presentDocumentPicker(for: .folder, completion: { path in - result(path) - }) - case "selectCustomDatabasePath": - // Persistent access, returns Base64 bookmark string - self.presentDocumentPicker(for: .folder, completion: { bookmarkString in - result(bookmarkString) - }) - case "startSecurityScopedAccess": - guard let path = call.arguments as? String else { result(false); return } - result(self.startSecurityScopedAccess(for: path)) - - case "stopSecurityScopedAccess": - guard let path = call.arguments as? String else { result(false); return } - result(self.stopSecurityScopedAccess(for: path)) - case "resolveCustomPath": - // Resolve the bookmark path for persistent access - guard let bookmarkString = call.arguments as? String else { - result(nil) - return - } - result(self.resolvePathFromBookmark(bookmarkString: bookmarkString)) - - default: - result(FlutterMethodNotImplemented) - } - }) - - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + + var pickerDelegate: FolderPickerDelegate? + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate } - - private func presentDocumentPicker(for contentType: UTType, completion: @escaping (String?) -> Void) { - if #available(iOS 14.0, *) { - let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [contentType], asCopy: false) - documentPicker.allowsMultipleSelection = false - documentPicker.delegate = pickerDelegate - - let delegate = FolderPickerDelegate() - delegate.pickerCompletionHandler = completion - self.pickerDelegate = delegate - documentPicker.delegate = delegate - - documentPicker.directoryURL = nil - documentPicker.shouldShowFileExtensions = true - - self.window?.rootViewController?.present(documentPicker, animated: true, completion: nil) - } else { - completion(nil) - } -} - private func resolvePathFromBookmark(bookmarkString: String) -> String? { - guard let bookmarkData = Data(base64Encoded: bookmarkString) else { - print("Error: Could not decode Base64 bookmark string.") - return nil + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + + let channel = FlutterMethodChannel( + name: "app.wispar.wispar/database_access", + binaryMessenger: engineBridge.applicationRegistrar.messenger() + ) + + channel.setMethodCallHandler({ + [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in + guard let self = self else { return } + + switch call.method { + case "getExportDirectory": + // Temporary access for export/import path selection + self.presentDocumentPicker( + for: .folder, + completion: { path in + result(path) + }) + case "selectCustomDatabasePath": + // Persistent access, returns Base64 bookmark string + self.presentDocumentPicker( + for: .folder, + completion: { bookmarkString in + result(bookmarkString) + }) + case "startSecurityScopedAccess": + guard let path = call.arguments as? String else { + result(false) + return } + result(self.startSecurityScopedAccess(for: path)) - var isStale = false - do { - let restoredURL = try URL( - resolvingBookmarkData: bookmarkData, - options: [.withoutUI], - relativeTo: nil, - bookmarkDataIsStale: &isStale - ) - if isStale { - print("Bookmark data was stale. User must re-select the folder.") - return nil - } - if restoredURL.startAccessingSecurityScopedResource() { - return restoredURL.path - } else { - print("Failed to start accessing security-scoped resource with restored URL.") - return nil - } - } catch { - print("Error resolving bookmark: \(error)") - return nil + case "stopSecurityScopedAccess": + guard let path = call.arguments as? String else { + result(false) + return } + result(self.stopSecurityScopedAccess(for: path)) + case "resolveCustomPath": + // Resolve the bookmark path for persistent access + guard let bookmarkString = call.arguments as? String else { + result(nil) + return + } + result(self.resolvePathFromBookmark(bookmarkString: bookmarkString)) + + default: + result(FlutterMethodNotImplemented) + } + }) + } + + private func presentDocumentPicker( + for contentType: UTType, completion: @escaping (String?) -> Void + ) { + if #available(iOS 14.0, *) { + let documentPicker = UIDocumentPickerViewController( + forOpeningContentTypes: [contentType], asCopy: false) + documentPicker.allowsMultipleSelection = false + + let delegate = FolderPickerDelegate() + delegate.pickerCompletionHandler = completion + self.pickerDelegate = delegate + documentPicker.delegate = delegate + + documentPicker.directoryURL = nil + documentPicker.shouldShowFileExtensions = true + + guard + let rootViewController = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .flatMap({ $0.windows }) + .first(where: { $0.isKeyWindow })?.rootViewController + else { + + print("Error: Could not find the active root view controller.") + completion(nil) + return + } + + rootViewController.present(documentPicker, animated: true, completion: nil) + + } else { + completion(nil) } + } - private func startSecurityScopedAccess(for path: String) -> Bool { - let url = URL(fileURLWithPath: path) - return url.startAccessingSecurityScopedResource() -} + private func resolvePathFromBookmark(bookmarkString: String) -> String? { + guard let bookmarkData = Data(base64Encoded: bookmarkString) else { + print("Error: Could not decode Base64 bookmark string.") + return nil + } + + var isStale = false + do { + let restoredURL = try URL( + resolvingBookmarkData: bookmarkData, + options: [.withoutUI], + relativeTo: nil, + bookmarkDataIsStale: &isStale + ) + if isStale { + print("Bookmark data was stale. User must re-select the folder.") + return nil + } + if restoredURL.startAccessingSecurityScopedResource() { + return restoredURL.path + } else { + print("Failed to start accessing security-scoped resource with restored URL.") + return nil + } + } catch { + print("Error resolving bookmark: \(error)") + return nil + } + } + + private func startSecurityScopedAccess(for path: String) -> Bool { + let url = URL(fileURLWithPath: path) + return url.startAccessingSecurityScopedResource() + } private func stopSecurityScopedAccess(for path: String) -> Bool { let url = URL(fileURLWithPath: path) url.stopAccessingSecurityScopedResource() return true } -} \ No newline at end of file +} diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index d82382cf..bc331bdd 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,27 @@ + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + BGTaskSchedulerPermittedIdentifiers com.transistorsoft.fetch