Search code examples
iosswiftuiactivityviewcontroller

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
}

Solution

  • 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...