iOS
CocoaPods
Realm
watchOS
swift3

How to exchange Realm data between watchOS and iOS

More than 1 year has passed since last update.


Introduction


  • How to exchange Realm data between watchOS and iOS.

  • watchOS 2 +, iOS 9 +, and Swift 3.

  • I have released the code on GitHub as a sample project. Experience the speed.

This is the material presented at Next Inc., at 25th, Nov., 2016.



About me


  • I'm Yuta Hoshino, an iOS app developer from ge-nie inc.

  • The company developed ge-calc has been downloaded more than 720,000 units.

  • I have published an UI library called "SwiftyPickerPopover" on Github. The library is for displaying Popover with Picker without UIPopoverController by a Swift line.



Basic idea


  • In watchOS 1, iOS's Realm file was shared by watchOS.

  • In watchOS 2, two Realm files exist as separate entities.

  • So, let's synchronize the Realm files by fileTransfer().



Implementation policy


  1. Save the text of iOS side textField to Realm each time it is changed.

  2. When changing, transfer the iOS's Realm file to watchOS side by fileTransfer () of WCSession.

  3. When it arrives, refresh the watchOS's Realm file, read the contents from it and display it on the watchOS's label.



Run sample code

SampleRealmOnWatchOS3

https://github.com/hsylife/SampleRealmOnWatchOS3

In this sample, I also implemented a method to send data directly by sendMessage() for comparison of transfer speed.


  1. Clone the repository on GitHub.

  2. To import RealmSwift, launch Terminal and run pod install of CocoaPods.

  3. If you want to check the operation with the simulator, keep running the scheme watchOS and run it. Start watchOS app at first.

  4. Start the iOS app.

  5. Following the steps displayed in the app, if you enter text in the iOS's textField, it will be reflected on watchOS side.



Introducing Realm with CocoaPods

Since you want to use RealmSwift, with CocoaPods 1.0.0 and later, write a Podfile as follows.

target 'SampleRealmOnWatchOS3' do

dynamic frameworks
use_frameworks!
pod 'RealmSwift'
end

target 'SampleWatchApp_Extension' do
use_frameworks!
platform :watchos, '2.0'
pod 'RealmSwift'
end

post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['SWIFT_VERSION'] = '3.0'
end
end
end

In the sample, SampleRealmOnWatchOS3 is the iOS's target name andSampleWatchApp_Extension is watchOS's name.

Run pod install from terminal and it is ready if it ends normally.

Now you can import RealmSwift on both iOS / watchOS.



Share the source file of Realm data model

To exchange data of Realm between iOS and watchOS, you want to share .swift file of data model. Take the following steps.


1. Make the data model public for sharing.


  • Create a data model for iOS app target.

  • For the sample, I created Field.swift for the classField which describes the data model for the iOS's target SampleRealmOnWatchOS3.


Field.swift

import RealmSwift

public class Field: Object {
public dynamic var text: String?
}



  • The access controls are specified as public.

  • Since the iOS's target SampleRealmOnWatchOS3 and the watchOS's targetSampleWatchApp_Extension are separate modules, this specification allows you to share the source beyond that barrier.



2. Add it to Compile Sources


  • The Swift file created in the above step belongs to the iOS's target SampleRealmOnWatchOS3.

  • For source sharing, assign the source to the watchOS's extension target SampleWatchApp_Extension.

screenshot.png


  • Both iOS/watchOS app has the same source file and can use the data model.



Activate WCSession

To exchange data between iOS and watchOS, use WCSession of WatchConnectivity framework.

Activate the WCSession on iOS/watchOS.



Implementation on the iOS side


  • Declare WCSessionDelegate because we want to use a delegate method that can get success and failure of activation.

class ViewController: UIViewController, WCSessionDelegate {


  • Describe the process to activate in viewDidLoad().


ViewController.swift

override func viewDidLoad() {

super.viewDidLoad()
if WCSession.isSupported() {
let session = WCSession.default()
session.delegate = self
session.activate()
}
}


  • You can now activate the WCSession on startup. You also specified WCSessionDelegate, so it is ready for the delegte method to be called.

  • Since iOS 9.3+ is required for session (_: activationDidCompleteWith: error :) which is called when activation is completed, if you want to target earlier (such as iOS 9.0), you must write @available (iOS 9.3 , *).


ViewController.swift

@available(iOS 9.3, *)

func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("activationDidComplete")
}


Implement watchOS side


  • Declare WCSessionDelegate like iOS side.


InterfaceController.swift

class InterfaceController: WKInterfaceController, WCSessionDelegate {



  • Since you want to process it at startup, we describe the process to activate in awake().


InterfaceController.swift

override func awake(withContext context: Any?) {

super.awake(withContext: context)
if WCSession.isSupported() {
let session = WCSession.default()
session.delegate = self
session.activate()
}
}


InterfaceController.swift

@available(watchOS 2.2, *)

func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("activationDidComplete")
}



Send the file


  • After you activated the session, send the iOS's Realm file to watchOS side by using fileTransfer().


  • Write the transmission process at the necessary timing.



ViewController.swift

if let path = Realm.Configuration().fileURL {

WCSession.default().transferFile(path, metadata: nil)
}


  • In the sample, since you want to send every time when changing the textField, you send it at the realmTextFieldEditingChanged () action of the textField. In addition, write the same process at iOS's viewDidLoad() in order to transmit once at startup.


  • Although it is written to execute every time textField is edited, transferFile() may be executed frequently in a short time.


  • As described by "those transfers happen opportunistically in the background." at the beginning of the official reference, transmission does not occur every time.




Receiving Realm file


  • The file sent from iOS side is received by watchOS side.

  • Since InterfaceController declares WCSessionDelegate as described above, you can use session (_ session: WCSession, didReceive file: WCSessionFile) which is its delegate method.

  • Since it is called when a file is received, write processing to sequentially replace the Realm default file.

  • In the sample, the data is acquired after replacing the file. And it hands it to the UI system.


InterfaceController.swift

func session(_ session: WCSession, didReceive file: WCSessionFile) {

var config = Realm.Configuration()
config.fileURL = file.fileURL
Realm.Configuration.defaultConfiguration = config

let realm = try! Realm()
if let firstField = realm.objects(Field.self).first{
realmLabel.setText(firstField.text)
}
}



  • Now you can store the data in the Realm database each time the iOS side's textField changes. And it is possible to send the whole file from iOS side to watch OS side via WCSession transferFile. After replacing the Realm default file on the watchOS side that received this, it is possible to refresh the display system.


  • I have published it on GitHub as Sample Project, so please check the whole thing again.




Comparison with sendMessage


  • What we saw above was a way to send a Realm file using one of the background transfers provided by WCSesion transferFile().

    For performance comparison, I also implemented the sample code about sendMessage() provided as a method of mutual messaging (Interactive messaging) in the sample.


  • sendMessage() needs to be in the foreground state on watchOS side as described in this article. This is a method that can send it on the spot.


  • The flow of implementation is as follows.

    1 .. Activate WCSession.

    2 .. Execute transmission by letting sendMessage () have data in iOS side.



ViewController.swift

func sendMessage(){

if WCSession.default().isReachable {
let applicationDict = ["text": messageTextField.text ?? ""]
WCSession.default().sendMessage(applicationDict,
replyHandler: { replyDict in print(replyDict) },
errorHandler: { error in print(error.localizedDescription)})
}

}


3 .. Since watchOS side calls session (_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @ escaping ([String: Any]) -> Void) of the WCSessionDelegate method, write the appropriate processing.


InterfaceController.swift

  func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {

if let text = message["text"] as! String?{
messageLabel.setText(text)
}

replyHandler(["reply" : "OK"])
}



  • In this case, it is an implementation that returns [" reply ":" OK "] as replyHandler. Since this value is passed to sendMessage () 's replyHandler {} on ViewController.swift on the iOS side, the content of [" reply ":" OK "] will be displayed on the debugger console on success.



Reverse data transmission


  • WCSession supports not only iOS → watch OS but also data transmission of watchOS → iOS in the opposite direction. So you can support to iOS side to Realm changes on watchOS side.



Does the file transferred by transferFile () gather?

Q: When file transfer is repeated between iOS-watchOS through transferFile(), will the files be accumulated?

A: It will not accumulate.

Let's check the folders actually after doing transferFile() several times.

screenshot.png

Not accumulated.

According to the explanation of An Introduction to Watch Connectivity in watch OS 2, the transferred file is placed in the Document/Inbox folder of the transfer destination app. It is then deleted automatically when the delegate method returns.

Conversely, if you want to keep the file at the transfer destination, you must save it intentionally.