Can I write/read data in application directory in flutter iOS? - ios

Is there anything else on iOS like getExternalStorageDirectory() ?
Is it getApplicationDocumentsDirectory() ?
If so, can the user access it?
The files in getApplicationDocumentsDirectory() can be shown as a list in the flutter iOS app?

use the path package, supported on all main os
https://pub.dev/packages/path

Unfortunately, you cannot access other app directories except for yours in iOS because of sandboxing. You can read it here as well:
https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories
By the way, there is a way to get other directories using swift as provided in the documentation, but I did not see any solution for it using flutter.
Hope it helps you.

If I'm not mistaken, you are trying to get another application directory in iOS using flutter.
There is a way to do so.
At first, let me mention that you do not need any permission for writing & reading data in iOS. It is given by default. But, the problem is getting their path. As others already mentioned that, iOS uses sandboxing, you cannot directly get access to all files and folders excluding shared storage.
Steps you need to do for reading and writing directories of other apps.
Install file_picker package. Link: https://pub.dev/packages/file_picker
Using it, popup system directory picker:
String? selectedDirectory = await FilePicker.platform.getDirectoryPath();
PS: Users should know which folder they need to get an access.
3. When they select the folder, get the folder path and use it as you want. But there is still one thing to complete. You need to use a little bit Swift code for getting access it.
import UIKit
import Flutter
import Photos
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(name: "example.startAccessingToSharedStorage",
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
// This method is invoked on the UI thread.
guard call.method == "startAccessingToSharedStorage" else {
result(FlutterMethodNotImplemented)
return
}
print("\(call.arguments)")
self?.startAccessingToSharedStorage(result: result, call: call)
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func startAccessingToSharedStorage(result: FlutterResult, call: FlutterMethodCall) {
let args = call.arguments as? Dictionary<String, Any>
if(args != nil){
let fileUrl = URL(fileURLWithPath: (args!["url"] as? String) ?? "")
// Get bookmark data from the provided URL
let bookmarkData = try? fileUrl.bookmarkData()
if let data = bookmarkData {
// Save data
} else {
result("Some bad thing happened")
}
// Access to an external document by the bookmark data
if let data = bookmarkData {
var stale = false
if let url = try? URL(resolvingBookmarkData: data, bookmarkDataIsStale: &stale),
stale == false,
url.startAccessingSecurityScopedResource()
{
var error: NSError?
NSFileCoordinator().coordinate(readingItemAt: url, error: &error) { readURL in
if let data = try? Data(contentsOf: readURL) {
result("Error occured while getting access")
}
}
result("\(url.startAccessingSecurityScopedResource())\(args!["url"])")
}
}
} else {result("\(args!["url"])")}
}
}
Use method channel for using this function in flutter.

Yes, on iOS in order to get path set import:
import 'package:path_provider/path_provider.dart' as syspath;
then use:
final appDir = await syspath
.getApplicationDocumentsDirectory();
if you save the path, keep in mind that on iOS the path changes every time we run the application.

Related

Sharing data between an notification service extension and main app

In my scenario, I'm sharing data between the parent iOS app and notification service extension. I'm using Xcode 10.2.1, iOS Deployment Target 10.0
We have tried the NSUserDefaults and Keychain group it's working as expected. Is any other way to save the values(Store Model or datatypes) from notification service extension(TargetB) to MainApp(TargetA).
We have appended the values into the model once the app is in terminated state and save it in the keychain.
For saving to Keycahin:
NotificationAPI.shared.NotificationInstance.append(Notifications(title: messageTitle, message: messageBody,date: Date()))
let notification = NotificationAPI.shared.NotificationInstance
let value = keychain.set(try! PropertyListEncoder().encode(notification), forKey: "Notification")
For USerDefault :
var defaults = NSUserDefaults(suiteName: "group.yourappgroup.example")
I want to transfer the data from Target B to Target A. when the app is in an inactive state? Another to transfer or saving data? Please help me?
Maybe I didn't get the question correctly.
You can share data between the app through CoreData:
Here it is explained.
In general, you need to add the extension and the main app in a group and to add your xcdatamodeled file in the target of the extension.
I think UserDefaults, Keychain, CoreData, iCloud, and storing data to your backend from where the other app take it is all the options that you have.
Add below property in your VC.
var typeArray = [DataModelType]()
let path = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.ABC.app")?.appendingPathComponent("type.plist")
DataModel which you want to share
struct DataModelType: Codable {
//define properties which you wanna use.
}
Add Extention to ViewController
extension ViewController {
fileprivate func saveData() {
let encoder = PropertyListEncoder()
do{
let dataEncode = try encoder.encode(typeArray)
try dataEncode.write(to:path!)
}
catch{
print("Error")
}
}
fileprivate func loadData() {
if let data = try? Data(contentsOf: path!) {
let decoder = PropertyListDecoder()
do {
typeArray = try decoder.decode([DataModelType].self, from: data)
}
catch {
print("Error")
}
}
}
func fetchDataModel() -> DataModelType {
loadData()
return typeArray
}
}
In your extension add below code wherever you wanna access data.
let viewController = ViewController()
let currentDataModel = viewController.fetchDataModel()

Back link to previous app from containing app progmatically in case of deeplinks

I created a keyboard extension with a scan button to open a barcode scanner in my containing app. When the scan is completed, it should navigate back to the initial app and the barcode data should be set as text to the textfield that initiated the keyboard and we clicked on scan button.
There is this app Scandit Wedge that does it the same way. But I couldn't find a way to achieve the same.
Please refer GIF below.
https://s3.amazonaws.com/id123-dev-ios/scandit.gif
Any help would be much appreciated.
There is no public API to switch to the previous app, here is the answer: https://stackoverflow.com/a/13447282/1433612
But you could do that if you know the app's bundle id and url scheme. You can find unofficial lists on internet. Assuming that you are able to recognize the source app you can do something like this in your AppDelegate:
public func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
guard let applicationBundleId = options[UIApplicationOpenURLOptionsKey.sourceApplication] as? String else {
return true
}
// Save your source application
sourceApplicationBundleId = applicationBundleId
return true
}
var sourceApplicationBundleId: String?
// Attempt to open application from which your app was opened
func openApplication() {
guard let applicationBundleId = sourceApplicationBundleId, let url = url(for: applicationBundleId) else {
return
}
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
func url(for bundleId: String) -> URL? {
guard let scheme = knownUrlSchemes[bundleId] else {
return nil
}
return URL(string: scheme)!
}
// A list of known url schemes
var knownUrlSchemes: Dictionary<String, String> = {
return ["com.google.Maps": "comgooglemaps://",
"com.facebook.Facebook": "fb://"]
}()

Swift 3 Photo Share Extension with Firebase Database Not Working After First Send

I am developing an iOS photo sharing extension in Swift 3 that captures a user-selected photo in the iOS Photos app along with a user-entered caption and stores it in Firebase. The photo is stored in Firebase storage. The caption and the path of the photo in Firebase Storage are stored in Firebase Realtime Database.
The problem I'm encountering is that the share extension stops working after first send. The strange thing is, if I do a similar approach in a regular View Controller in the iOS app, the code works. I noticed two issues with the Share Extension:
Issue #1: The share extension view isn't dismissed completely. In a normal situation, the view would return to a "gallery" mode of Photos. However, the share extension view is going away but the share menu with the complete list of apps that you can use to share is not dismissing.
Screenshot of what the Photos view looks like after the second send
Issue #2: the data isn't being sent up to Firebase storage or Firebase database.
Below please find my ShareViewController code:
import UIKit
import Social
import Firebase
import MobileCoreServices
class ShareViewController: SLComposeServiceViewController {
var ref: DatabaseReference!
var storageRef: StorageReference!
override func isContentValid() -> Bool {
// Do validation of contentText and/or NSExtensionContext attachments here
return true
}
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
FirebaseApp.configure()
ref = Database.database().reference()
storageRef = Storage.storage().reference()
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
if let item = self.extensionContext?.inputItems[0] as? NSExtensionItem {
for ele in item.attachments!{
let itemProvider = ele as! NSItemProvider
if itemProvider.hasItemConformingToTypeIdentifier("public.jpeg"){
itemProvider.loadItem(forTypeIdentifier: "public.jpeg", options: nil, completionHandler: { (item, error) in
do {
var imgData: Data!
if let url = item as? URL{
imgData = try Data(contentsOf: url)
}
if let img = item as? UIImage{
imgData = UIImagePNGRepresentation(img)
}
var updateRef = self.ref.child("demo_group").child("demo_patient").child("demo_updates").childByAutoId()
var updateStorageRef = self.storageRef.child("demo_photos" + "/\(Double(Date.timeIntervalSinceReferenceDate * 1000)).jpg")
updateRef.child("event_name").setValue(self.contentText)
updateRef.child("sender").setValue("demo_ff")
let metadata = StorageMetadata()
metadata.contentType = "image/jpeg"
updateStorageRef.putData(imgData, metadata: metadata) { (metadata, error) in
if let error = error {
print("Error uploading: \(error)")
return
}
// use sendMessage to add imageURL to database
updateRef.child("photos").childByAutoId().setValue(metadata?.path)
}
} catch let err{
print(err)
}
})
}
}
}
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
override func configurationItems() -> [Any]! {
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
return []
}
}
Please let me know if there's anything I can do to fix this! I would be greatly appreciated.
FirebaseApp.configure() will crash your extension if call multiple times in a row because the system does not fully deallocate the firebase app instance created for the extension.
My solution was to do:
if let _ = FirebaseApp.app() {...} else {
FirebaseApp.configure()
}
This should fix your problem.
Edit: Also note that you should call extensionContext.complete after your storage & database updates have completed... Not before
So, you will need to use the methods for setValue that take a completion block.
1- Upload to storage, when the task completes
2- Update the database reference,
3- When that completion block is called... Then you call extensionContext.completeWith() or .cancel()

Firebase Dynamic Links does not survive installation process

I updated Firebase Dynamic Link pod to 1.4 version. In this version I found very useful class named FIRDynamicLinkComponents. I decided to use it to generate dynamic links. But I have 2 problems:
Firebase doc says that dynamic links can survive installation process and open app on the content I described in dynamic link after installation from AppStore. It is not work.
When user without installed app taps on dynamic links, he will see strange screen with button "Open in App". After click AppStore appears.
Can we skip this screen?
My implementation:
static func createDynamicLinks(forChallangeId challangeId: String, authorId: String, authorEmail: String, completion: #escaping (_ dynamicLink: String?, _ error: Error?) -> Void) {
let link = URL(string: "https://CODE.app.goo.gl/challange/\(challangeId)/author/\(authorId)")!
let domain = DOMAIN
let components = FIRDynamicLinkComponents(link: link, domain: domain)
//add iOS params
let iOSParams = FIRDynamicLinkIOSParameters(bundleID: bundleId)
iOSParams.appStoreID = APP_STORE_ID
components.iOSParameters = iOSParams
//add Android params
let androidParams = FIRDynamicLinkAndroidParameters(packageName: PACKAGE_NAME)
androidParams.minimumVersion = 19
components.androidParameters = androidParams
//add social meta tag params
let socialParams = FIRDynamicLinkSocialMetaTagParameters()
socialParams.title = "You got new challenge"
socialParams.descriptionText = "\(authorEmail) sent the challenge to you."
socialParams.imageURL = IMAGE_URL
components.socialMetaTagParameters = socialParams
//add options
let options = FIRDynamicLinkComponentsOptions()
options.pathLength = .short
components.options = options
//make link shorter
components.shorten { (shortURL, warnings, error) in
if let error = error {
completion(nil, error)
return
}
guard let shortLinkString = shortURL?.absoluteString else {
completion(nil, error)
return
}
completion(shortLinkString, error)
}
}
Edit
3rd problem.
Target iOS10. Handle:
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([Any]?) -> Void) -> Bool {
guard let dynamicLinks = FIRDynamicLinks.dynamicLinks() else {
return false
}
let handled = dynamicLinks.handleUniversalLink(userActivity.webpageURL!) { (dynamiclink, error) in
if let url = dynamiclink?.url {
DynamicLinksManager.handleDeepLink(link: url)
}
}
return handled
}
handled is true but in closure dynamiclink and error are nil.
A lot of solutions depend on having more context than you've currently included. I'll edit this answer with updates as possible.
Dynamic Links definitely can survive the installation process in most situations. However, there are a lot of edge cases. Could you add specific reproduction steps for exactly the process you're using to test?
No, unfortunately this modal cannot be skipped. Apple made some changes in iOS 10.3 that make something like this unavoidable (read here for more on what happened, and how we handled the same problem in a slightly more elegant way at Branch)
This might be expected, if no valid Dynamic Link were triggered. Again, could you add specific reproduction steps?

Realm - Add file with initial data to project (iOS/Swift)

I'm developing an application for iOS using swift and chose Realm as a database solution for it. I wrote default data in AppDelegate using write/add function from realm docs and it works just fine. So after first launch I have a *.realm file with my initial data. In Realm documentation I found a section called "Bundling a Realm with an App", I add my *.realm file to project and to Build Phases as it written.
And I can't understand what I should do next (and part about compressing a *.realm file). I've tried to understand a code from Migration Example but I don't know Obj-C well.
Please give as clear steps as you can to add *.realm file with initial data to swift ios project and load this data to the Realm db with the first launch.
Implement this function openRealm in AppDelegate and call it in
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
...
openRealm()
return true
}
func openRealm() {
let defaultRealmPath = Realm.defaultPath
let bundleReamPath = NSBundle.mainBundle().resourcePath?.stringByAppendingPathComponent("default.realm")
if !NSFileManager.defaultManager().fileExistsAtPath(defaultRealmPath) {
NSFileManager.defaultManager().copyItemAtPath(bundleReamPath!, toPath: defaultRealmPath, error: nil)
}
}
It will copy your realm file that you bundled in the app to the default realm path, if it doesn't exist already. After that you use Realm normally like you used before.
There's also the Migration example that you talked about in Swift.
In Swift 3.0.1 you may prefer this:
let defaultRealmPath = Realm.Configuration.defaultConfiguration.fileURL!
let bundleRealmPath = Bundle.main.url(forResource: "seeds", withExtension: "realm")
if !FileManager.default.fileExists(atPath: defaultRealmPath.absoluteString) {
do {
try FileManager.default.copyItem(at: bundleRealmPath!, to: defaultRealmPath)
} catch let error {
print("error copying seeds: \(error)")
}
}
(but please be careful with the optionals)
Swift version 3, courtesy of Kishikawa Katsumi:
let defaultRealmPath = Realm.Configuration.defaultConfiguration.fileURL!
let bundleReamPath = Bundle.main.path(forResource: "default", ofType:"realm")
if !FileManager.default.fileExists(atPath: defaultRealmPath.path) {
do
{
try FileManager.default.copyItem(atPath: bundleReamPath!, toPath: defaultRealmPath.path)
}
catch let error as NSError {
// Catch fires here, with an NSError being thrown
print("error occurred, here are the details:\n \(error)")
}
}
And for those that need #pteofil's answer in Objective-c
- (void)openRealm {
NSString *defaultRealmPath = [RLMRealm defaultRealm].path;
NSString *bundleRealmPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:#"default.realm"];
if(![[NSFileManager defaultManager] fileExistsAtPath:defaultRealmPath]) {
[[NSFileManager defaultManager] copyItemAtPath:bundleRealmPath toPath:defaultRealmPath error:nil];
}
}
Updating #pteofil's openRealm function for Swift 2.2/Realm 1.0.2:
func openRealm() {
let defaultURL = Realm.Configuration.defaultConfiguration.fileURL!
let bundleReamPath = NSBundle.mainBundle().URLForResource("default", withExtension: "realm")
if !NSFileManager.defaultManager().fileExistsAtPath(defaultURL.path!) {
do {
try NSFileManager.defaultManager().copyItemAtURL(bundleReamPath!, toURL: defaultURL)
}
catch {}
}
}
Work in the enterprise space, I need to open a Realm for each application without reusing Realm across all applications so I put this together for Swift 3.0. Add this function to the AppDelegate.
func openRealm()
{
let appName = "ApplcationNameGoesHere"
var rlmConfig = Realm.Configuration()
let defaultRealmPath = Realm.Configuration.defaultConfiguration.fileURL!
let appRealmPath = defaultRealmPath.deletingLastPathComponent().appendingPathComponent("\(appName).realm")
if !FileManager.default.fileExists(atPath: appRealmPath.path) {
// Use the default directory, but replace the filename with the application name: appName
rlmConfig.fileURL = rlmConfig.fileURL!.deletingLastPathComponent().appendingPathComponent("\(appName).realm")
}else
{
rlmConfig.fileURL = appRealmPath
}
// Set this as the configuration used for the default Realm
Realm.Configuration.defaultConfiguration = rlmConfig
}// open the Realm database for the application
The code above opens or creates a Realm with the file name of "ApplicationNameGoesHere.realm" based on the appName variable in this example.
place
openRealm() before return true in application: didFinishLaunchingWithOptions
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
openRealm()
return true
}
call it in another class like this:
let uiRealm = try! Realm()
If you want to open it straight from the bundle location and not bother copying it to the default Realm path, look at the implementation here
Download Realm Studio in your system. Then print the path from Xcode and copy it:
print(Realm.Configuration.defaultConfiguration.fileURL!)
Then open the terminal and write:
open //file path
It will open the file in Realm Studio and you can see your model data there.

Resources