Wael
Wael

Reputation: 411

Swift : Sending custom data between two devices (not on the same network)

I am new to ios developpement.

I have built and apps that saves data in the core data. What I would like to do is sharing this data with my Ipad, or my kids Iphone.

The devices are not on the same network or closed to each other, so the shared process will be via Email or Imessage.

the app will be installed on all devices to be abel to send/receive data.

I would like to be sure that the only way to do that is to use UIActivityViewController.

I didn't start coding part yet ( Sharing part), I am still doing some research and some good advices.

Thank you for helping

// Full code

after doing a lot of search here is my code , I don't know if there is a better way to do it but here is how I solved :

1 - creating a Json String

      let savedData = ["Something": 1]
      let jsonObject: [String: Any] = [
        "type_id": 1,
        "model_id": true,
        "Ok": [
            "startDate": "10-04-2015 12:45",
            "endDate": "10-04-2015 16:00"
        ],
        "custom": savedData
    ]

2 - Saving in as file

     let objectsToShare = [jsonObject as AnyObject]
   let data: Data? = try? JSONSerialization.data(withJSONObject: objectsToShare, options: .prettyPrinted)
     let filePath = NSTemporaryDirectory() + "/" + NSUUID().uuidString + ".kis"

    do
   {
    try data?.write(to: URL(fileURLWithPath: filePath))

    }
    catch {

    }
    print (filePath)
    // create activity view controller
    let activityItem:NSURL = NSURL(fileURLWithPath:filePath)
    let activityViewController = UIActivityViewController(activityItems: [activityItem], applicationActivities: nil)
    self.present(activityViewController, animated: true, completion: nil)

3 - update info.plist

 <key>CFBundleDocumentTypes</key>
  <array>
    <dict>
        <key>LSItemContentTypes</key>
        <array>
            <string>XXXSharingData.kis</string>
        </array>
        <key>CFBundleTypeRole</key>
        <string>Viewer</string>
        <key>CFBundleTypeName</key>
        <string>kis file</string>
        <key>LSHandlerRank</key>
        <string>Owner</string>
    </dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
    <dict>
        <key>UTTypeConformsTo</key>
        <array>
            <string>public.data</string>
        </array>
        <key>UTTypeIdentifier</key>
        <string>XXXSharingData.kis</string>
        <key>UTTypeTagSpecification</key>
        <dict>
            <key>public.mime-type</key>
            <string>application/pry</string>
            <key>public.filename-extension</key>
            <string>kis</string>
        </dict>
    </dict>
</array>

finally when sending/receiving file as attachment via email and opened in my app : open method appdelegate, I was able to see the Json string

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
    do
    {
        let dictionary =  try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue)
        print (dictionary)
    }
    catch {

    }
    return true
}

Upvotes: 0

Views: 2313

Answers (2)

Matic Oblak
Matic Oblak

Reputation: 16774

Well there are in general 2 principles overall. You will either wan't to send difference of data or the whole data itself.

To begin with I would just go with sending whole packet on demand because the difference means the target device will need to firsts report what state it is in.

So to transfer the whole core data I guess it should be possible to simply zip the whole folder where your database is stored, change the zip extension to some custom name and simply send this file. Then unzip this file on the other side to the corresponding location.

But this is not really good because it is not portable to any other system (Android, Remote storage...) and will have versioning issues: If you update your data model then your new app will crash when importing old database.

So I would say sending any standard data chunk would be the best: Simply create a conversion of all your core data entities to dictionaries and then serialise this data into a JSON. Ergo something like this:

var JSONDictionary: [String: Any] = [String: Any]()
JSONDictionary["users"] = User.fetchAll().map { $0.toDictionary() }
JSONDictionary["tags"] = Tag.fetchAll().map { $0.toDictionary() }
JSONDictionary["notifications"] = Notification.fetchAll().map { $0.toDictionary() }
let data: Data? = try? JSONSerialization.data(withJSONObject: JSONDictionary, options: .prettyPrinted)
let filePath = NSTemporaryDirectory() + "/" + NSUUID().uuidString + ".myApplicationFileExtension"
Data().write(to: URL(fileURLWithPath: filePath))

So at this point you have your file which is easily sharable and can be reversed on the other side. You can then choose to merge data, overwrite it...

EDIT: Adding more description from comments and updated question

  1. I am not sure what your data structure it but as it seems I expect something like:

        class MyObject {
            enum ObjectType {
                case a,b,c
                var id: String {
                    switch self {
                    case .a: return "1"
                    case .b: return "2"
                    case .c: return "3"
                    }
                }
                static let supportedValues: [ObjectType] = [.a, .b, .c]
            }
    
            var savedData: [String: Any]?
            var type: ObjectType = .a
            var isModel: Bool = false
            var startDate: Date?
            var endDate: Date?
    
            func toDictionary() -> [String: Any] {
                var dictionary: [String: Any] = [String: Any]()
                dictionary["custom"] = savedData
                dictionary["type_id"] = type.id
                dictionary["model_id"] = isModel
                dictionary["startDate"] = startDate?.timeIntervalSince1970
                dictionary["endDate"] = endDate?.timeIntervalSince1970
                return dictionary
            }
    
            func loadWithDescriptor(_ descriptor: Any?) {
                if let dictionary = descriptor as? [String: Any] {
                    savedData = dictionary["custom"] as? [String: Any]
                    type = ObjectType.supportedValues.first(where: { $0.id == dictionary["type_id"] as? String }) ?? .a
                    isModel = dictionary["model_id"] as? Bool ?? false
                    startDate = {
                        guard let interval = dictionary["startDate"] as? TimeInterval else {
                            return nil
                        }
                        return Date(timeIntervalSince1970: interval)
                    }()
                    endDate = {
                        guard let interval = dictionary["endDate"] as? TimeInterval else {
                            return nil
                        }
                        return Date(timeIntervalSince1970: interval)
                    }()
                }
            }
        }
    

So this will now give you the capability to transition from and to dictionary (not JSON).

  1. The activity controller is not an issue and saving to file does not seem to be much of an issue as well. I am not sure about a few properties but it should look something like this:

        func saveToTemporaryFile(myConcreteObjects: [MyObject]) -> String {
            let dictionaryObjects: [[String: Any]] = myConcreteObjects.map { $0.toDictionary() }
    
            let path = NSTemporaryDirectory() + "/" + NSUUID().uuidString + ".kis"
    
            let data: Data? = try? JSONSerialization.data(withJSONObject: dictionaryObjects, options: .prettyPrinted)
            try? data?.write(to: URL(fileURLWithPath: path))
    
            return path
        }
    

If by chance you have different objects then this method should accept Any object which should be constructed as:

var myDataObject: [String: Any] = [String: Any]()
myDataObject["myObjects"] = myConcreteObjects.map { $0.toDictionary() }
...
  1. To load your JSON back from URL you simply need to construct your data. Since you got the URL don't bother with strings:

    func loadItemsFrom(url: URL) -> [MyObject]? {
        guard let data = try? Data(contentsOf: url) else {
            // Error, could not load data
            return nil
        }
        guard let array = (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)) as? [Any] else {
            // Error, could not convert to JSON or invalid type
            return nil
        }
    
        return array.map { descriptor in
            let newItem = MyObject()
            newItem.loadWithDescriptor(descriptor)
            return newItem
        }
    }
    

This constructor with getting data from URL is very powerful. It will work even with remote servers if you have access to internet/network. So in those cases make sure you do this operations on a separate thread or your UI may be blocked until the data is received.

I hope this clears a few things...

Upvotes: 2

Bogdan
Bogdan

Reputation: 863

Based on your description I assume you want to do something like Apple Handoff. Basically when you change some data on one device to continue working with an up to date copy from another device.

In my opinion you could achieve this by having some kind of backend that synchronise data between client and server. Also, the backend should send push notifications to the other devices to update the local copy. The application can be updated silently, without user interaction, by using Silent Push Notifications.

You can have a look at Cloudant or Couchbase which basically offer a solution of synchronising data between client and server. By using this approach you minimise the need of writing the code for the backend part of the application and also for the data and network layers from your client app. Both solutions have SDKs for iOS in Swift so you can integrate them easily.

Hope it helps!

Upvotes: 0

Related Questions