Skip to content

Commit 0de2cc2

Browse files
authored
Merge pull request #2 from icerockdev/ios-javascript
iOS implementation
2 parents 9fd5a5c + f27d38a commit 0de2cc2

File tree

8 files changed

+256
-24
lines changed

8 files changed

+256
-24
lines changed

javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/SampleTest.kt

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package dev.icerock.moko.javascript
6+
7+
import kotlinx.serialization.json.Json
8+
import kotlinx.serialization.json.JsonArray
9+
import kotlinx.serialization.json.JsonElement
10+
import kotlinx.serialization.json.JsonObject
11+
import kotlinx.serialization.json.JsonPrimitive
12+
import kotlinx.serialization.json.encodeToJsonElement
13+
import platform.Foundation.NSArray
14+
import platform.Foundation.NSDictionary
15+
import platform.Foundation.NSJSONSerialization
16+
import platform.Foundation.NSString
17+
import platform.Foundation.NSUTF8StringEncoding
18+
import platform.Foundation.create
19+
import platform.Foundation.dataUsingEncoding
20+
import platform.JavaScriptCore.JSContext
21+
import platform.JavaScriptCore.JSValue
22+
import platform.JavaScriptCore.setObject
23+
24+
actual class JavaScriptEngine actual constructor() {
25+
actual fun evaluate(context: Map<String, JsType>, script: String): JsType {
26+
27+
val jsContext = JSContext()
28+
29+
jsContext.exceptionHandler = { exceptionContext, exception ->
30+
val message = "\"context = $exceptionContext, exception = $exception\""
31+
throw JavaScriptEvaluationException(cause = null, message = message)
32+
}
33+
34+
context.forEach {
35+
jsContext.setObject(
36+
`object` = it.value.getValue(),
37+
forKeyedSubscript = NSString.create(string = it.key)
38+
)
39+
}
40+
41+
val result = jsContext.evaluateScript(script)
42+
43+
return result?.toMokoJSType() ?: JsType.Null
44+
}
45+
46+
actual fun close() {
47+
// Nothing to do here
48+
}
49+
}
50+
51+
private fun JsonObject.toNSDictionary(): NSDictionary {
52+
val data = NSString.create(string = this.toString()).dataUsingEncoding(NSUTF8StringEncoding)
53+
?: return NSDictionary()
54+
return (NSJSONSerialization.JSONObjectWithData(
55+
data = data,
56+
options = 0,
57+
error = null
58+
) as? NSDictionary) ?: NSDictionary()
59+
}
60+
61+
private fun JsonArray.toNSArray(): NSArray {
62+
val data = NSString.create(string = this.toString()).dataUsingEncoding(NSUTF8StringEncoding)
63+
?: return NSArray()
64+
return (NSJSONSerialization.JSONObjectWithData(
65+
data = data,
66+
options = 0,
67+
error = null
68+
) as? NSArray) ?: NSArray()
69+
}
70+
71+
private fun JsonElement.getValue(): Any? {
72+
return (this as? JsonObject)?.toNSDictionary()
73+
?: (this as? JsonArray)?.toNSArray()
74+
?: (this as? JsonPrimitive)?.content
75+
}
76+
77+
private fun JsType.getValue(): Any? {
78+
return when (this) {
79+
is JsType.Bool -> value
80+
is JsType.Str -> value
81+
is JsType.IntNum -> value
82+
is JsType.DoubleNum -> value
83+
is JsType.Json -> value.getValue()
84+
is JsType.Null -> null
85+
}
86+
}
87+
88+
private fun JSValue.toMokoJSType(): JsType {
89+
return when {
90+
isBoolean -> JsType.Bool(toBool())
91+
isString -> JsType.Str(toString_().orEmpty())
92+
isNumber -> JsType.DoubleNum(toDouble())
93+
isObject -> JsType.Json(Json.encodeToJsonElement(toDictionary()))
94+
isArray -> JsType.Json(Json.encodeToJsonElement(toArray()))
95+
isUndefined -> JsType.Null
96+
isNull -> JsType.Null
97+
else -> JsType.Null
98+
}
99+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package dev.icerock.moko.javascript
6+
7+
import kotlin.test.Test
8+
import kotlin.test.assertEquals
9+
import kotlinx.serialization.json.Json
10+
import kotlinx.serialization.json.encodeToJsonElement
11+
12+
class JavaScriptEngineTest {
13+
@Test
14+
fun `test json context`() {
15+
val form = mapOf("selector_1" to "first_value", "selector_2" to "second_value")
16+
val profile = mapOf("email" to "[email protected]")
17+
val formJson = Json.encodeToJsonElement(form)
18+
val profileJson = Json.encodeToJsonElement(profile)
19+
val context: Map<String, JsType> = mapOf("form" to JsType.Json(formJson), "profile" to JsType.Json(profileJson))
20+
21+
val jsEngine = JavaScriptEngine()
22+
23+
assertEquals(JsType.Bool(true), jsEngine.evaluate(context = context, script = "form.selector_1 == \"first_value\""))
24+
25+
assertEquals(JsType.Bool(true), jsEngine.evaluate(context = context, script = "profile.email != null"))
26+
27+
assertEquals(JsType.Str("[email protected]"), jsEngine.evaluate(context = context, script = "profile.email"))
28+
29+
assertEquals(JsType.Null, jsEngine.evaluate(context = context, script = "profile.first_name"))
30+
}
31+
32+
@Test
33+
fun `test plus script`() {
34+
val list = listOf<Int>(5, 15)
35+
val listJson = Json.encodeToJsonElement(list)
36+
val context: Map<String, JsType> = mapOf("list" to JsType.Json(listJson), "number" to JsType.IntNum(4), "doubleString" to JsType.Str(" Hello "))
37+
38+
val jsEngine = JavaScriptEngine()
39+
40+
assertEquals(JsType.DoubleNum(19.0), jsEngine.evaluate(context = context, script = "list[1]+number"))
41+
42+
assertEquals(JsType.Str(" Hello 5"), jsEngine.evaluate(context = context, script = "doubleString+list[0]"))
43+
}
44+
}

sample/ios-app/ios-app.xcodeproj/project.pbxproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
hasScannedForEncodings = 0;
146146
knownRegions = (
147147
English,
148+
Base,
148149
);
149150
mainGroup = 287627F61F319065007FA12B;
150151
productRefGroup = 287628001F319065007FA12B /* Products */;
@@ -270,6 +271,7 @@
270271
CODE_SIGN_STYLE = Automatic;
271272
DEVELOPMENT_TEAM = 4VU932NX78;
272273
INFOPLIST_FILE = src/Info.plist;
274+
ONLY_ACTIVE_ARCH = YES;
273275
PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.sample.javascript;
274276
PRODUCT_NAME = mokoSampleJavascript;
275277
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -288,6 +290,7 @@
288290
CODE_SIGN_STYLE = Automatic;
289291
DEVELOPMENT_TEAM = 4VU932NX78;
290292
INFOPLIST_FILE = src/Info.plist;
293+
ONLY_ACTIVE_ARCH = YES;
291294
PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.sample.javascript;
292295
PRODUCT_NAME = mokoSampleJavascript;
293296
PROVISIONING_PROFILE_SPECIFIER = "";

sample/ios-app/src/Resources/Base.lproj/Main.storyboard

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="zIh-nI-gcX">
3-
<device id="retina4_0" orientation="portrait">
4-
<adaptation id="fullscreen"/>
5-
</device>
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="zIh-nI-gcX">
3+
<device id="retina4_0" orientation="portrait" appearance="light"/>
64
<dependencies>
7-
<deployment identifier="iOS"/>
8-
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
5+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
96
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
107
</dependencies>
118
<scenes>
@@ -14,7 +11,7 @@
1411
<objects>
1512
<navigationController id="zIh-nI-gcX" sceneMemberID="viewController">
1613
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="aGe-Yj-KQZ">
17-
<rect key="frame" x="0.0" y="20" width="320" height="44"/>
14+
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
1815
<autoresizingMask key="autoresizingMask"/>
1916
</navigationBar>
2017
<connections>
@@ -28,16 +25,78 @@
2825
<!--Test-->
2926
<scene sceneID="B5V-K5-FDE">
3027
<objects>
31-
<viewController id="TTy-86-aNs" customClass="TestViewController" customModule="mokoSample{{cap name}}" customModuleProvider="target" sceneMemberID="viewController">
28+
<viewController id="TTy-86-aNs" customClass="TestViewController" customModule="mokoSampleJavascript" customModuleProvider="target" sceneMemberID="viewController">
3229
<layoutGuides>
3330
<viewControllerLayoutGuide type="top" id="gcA-zH-akF"/>
3431
<viewControllerLayoutGuide type="bottom" id="gTY-6T-yQv"/>
3532
</layoutGuides>
3633
<view key="view" contentMode="scaleToFill" id="KxK-oh-5KO">
3734
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
3835
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
36+
<subviews>
37+
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="Hello" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="HQE-GP-R06">
38+
<rect key="frame" x="32" y="60" width="256" height="34"/>
39+
<fontDescription key="fontDescription" type="system" pointSize="14"/>
40+
<textInputTraits key="textInputTraits"/>
41+
</textField>
42+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="+" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fuj-5A-cPv">
43+
<rect key="frame" x="155" y="102" width="10.5" height="21"/>
44+
<fontDescription key="fontDescription" type="system" pointSize="17"/>
45+
<nil key="textColor"/>
46+
<nil key="highlightedColor"/>
47+
</label>
48+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Result" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="99J-an-5DE">
49+
<rect key="frame" x="32" y="181" width="256" height="21"/>
50+
<fontDescription key="fontDescription" type="system" pointSize="17"/>
51+
<nil key="textColor"/>
52+
<nil key="highlightedColor"/>
53+
</label>
54+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wAL-t8-BaM">
55+
<rect key="frame" x="32" y="210" width="256" height="0.0"/>
56+
<fontDescription key="fontDescription" type="system" pointSize="17"/>
57+
<nil key="textColor"/>
58+
<nil key="highlightedColor"/>
59+
</label>
60+
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="uVI-QF-cG0">
61+
<rect key="frame" x="36" y="518" width="248" height="30"/>
62+
<state key="normal" title="Run"/>
63+
<connections>
64+
<action selector="run" destination="TTy-86-aNs" eventType="touchUpInside" id="gH6-Pt-K7R"/>
65+
</connections>
66+
</button>
67+
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" text=" World!" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="0cy-wj-dWi">
68+
<rect key="frame" x="32" y="131" width="256" height="34"/>
69+
<fontDescription key="fontDescription" type="system" pointSize="14"/>
70+
<textInputTraits key="textInputTraits"/>
71+
</textField>
72+
</subviews>
73+
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
74+
<constraints>
75+
<constraint firstItem="Fuj-5A-cPv" firstAttribute="centerX" secondItem="KxK-oh-5KO" secondAttribute="centerX" id="0rb-9x-EOW"/>
76+
<constraint firstItem="99J-an-5DE" firstAttribute="leading" secondItem="KxK-oh-5KO" secondAttribute="leadingMargin" constant="16" id="8s7-If-k2x"/>
77+
<constraint firstAttribute="trailingMargin" secondItem="0cy-wj-dWi" secondAttribute="trailing" constant="16" id="DQv-Ss-QFC"/>
78+
<constraint firstItem="Fuj-5A-cPv" firstAttribute="top" secondItem="HQE-GP-R06" secondAttribute="bottom" constant="8" id="GdN-ff-D8d"/>
79+
<constraint firstItem="99J-an-5DE" firstAttribute="top" secondItem="0cy-wj-dWi" secondAttribute="bottom" constant="16" id="Ilt-yV-j2g"/>
80+
<constraint firstItem="gTY-6T-yQv" firstAttribute="top" secondItem="uVI-QF-cG0" secondAttribute="bottom" constant="20" id="Npq-Tz-PAf"/>
81+
<constraint firstAttribute="trailingMargin" secondItem="wAL-t8-BaM" secondAttribute="trailing" constant="16" id="TFu-NC-MLP"/>
82+
<constraint firstItem="HQE-GP-R06" firstAttribute="top" secondItem="gcA-zH-akF" secondAttribute="bottom" constant="16" id="VzT-sB-6q9"/>
83+
<constraint firstAttribute="trailingMargin" secondItem="HQE-GP-R06" secondAttribute="trailing" constant="16" id="XoK-8d-2Kg"/>
84+
<constraint firstItem="0cy-wj-dWi" firstAttribute="leading" secondItem="KxK-oh-5KO" secondAttribute="leadingMargin" constant="16" id="bxp-kd-M9O"/>
85+
<constraint firstItem="uVI-QF-cG0" firstAttribute="leading" secondItem="KxK-oh-5KO" secondAttribute="leadingMargin" constant="20" id="i30-ZX-5U3"/>
86+
<constraint firstAttribute="trailingMargin" secondItem="99J-an-5DE" secondAttribute="trailing" constant="16" id="jeK-JC-qIA"/>
87+
<constraint firstItem="0cy-wj-dWi" firstAttribute="top" secondItem="Fuj-5A-cPv" secondAttribute="bottom" constant="8" symbolic="YES" id="kXc-kv-qYy"/>
88+
<constraint firstAttribute="trailingMargin" secondItem="uVI-QF-cG0" secondAttribute="trailing" constant="20" id="leE-eN-qQ1"/>
89+
<constraint firstItem="HQE-GP-R06" firstAttribute="leading" secondItem="KxK-oh-5KO" secondAttribute="leadingMargin" constant="16" id="mey-lt-COi"/>
90+
<constraint firstItem="wAL-t8-BaM" firstAttribute="leading" secondItem="KxK-oh-5KO" secondAttribute="leadingMargin" constant="16" id="owH-qy-NVU"/>
91+
<constraint firstItem="wAL-t8-BaM" firstAttribute="top" secondItem="99J-an-5DE" secondAttribute="bottom" constant="8" id="tLk-AL-liM"/>
92+
</constraints>
3993
</view>
4094
<navigationItem key="navigationItem" title="Test" id="0jM-60-fjM"/>
95+
<connections>
96+
<outlet property="firstValueTextField" destination="HQE-GP-R06" id="XWA-np-uEg"/>
97+
<outlet property="resultLabel" destination="wAL-t8-BaM" id="OHw-r5-JIK"/>
98+
<outlet property="secondValueTextField" destination="0cy-wj-dWi" id="r0H-8r-1Hu"/>
99+
</connections>
41100
</viewController>
42101
<placeholder placeholderIdentifier="IBFirstResponder" id="Jxh-nl-GiI" userLabel="First Responder" sceneMemberID="firstResponder"/>
43102
</objects>

sample/ios-app/src/TestViewController.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,22 @@ import MultiPlatformLibrary
77

88
class TestViewController: UIViewController {
99

10+
@IBOutlet private var firstValueTextField: UITextField!
11+
@IBOutlet private var secondValueTextField: UITextField!
12+
@IBOutlet private var resultLabel: UILabel!
13+
1014
override func viewDidLoad() {
1115
super.viewDidLoad()
1216
}
17+
18+
@IBAction private func run() {
19+
let result = Calculator().run(a: firstValueTextField.text ?? "", b: secondValueTextField.text ?? "")
20+
if let value = (result as? JsType.Str)?.value {
21+
resultLabel.text = value
22+
}
23+
24+
if let value = (result as? JsType.DoubleNum)?.value {
25+
resultLabel.text = "\(value)"
26+
}
27+
}
1328
}

sample/mpp-library/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ plugins {
99

1010
dependencies {
1111
commonMainApi(Deps.Libs.MultiPlatform.coroutines)
12+
commonMainApi(Deps.Libs.MultiPlatform.kotlinSerialization)
1213

1314
commonMainApi(Deps.Libs.MultiPlatform.mokoJavascript)
1415
}

sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/Calculator.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@
44

55
package com.icerockdev.library
66

7+
import dev.icerock.moko.javascript.JavaScriptEngine
8+
import dev.icerock.moko.javascript.JsType
9+
710
class Calculator {
8-
fun run() = Unit
11+
fun run(a: String, b: String): JsType {
12+
val engine = JavaScriptEngine()
13+
val testScript = "a+b"
14+
15+
val intA = a.toIntOrNull()
16+
val intB = b.toIntOrNull()
17+
18+
val context = if (intA != null && intB != null) {
19+
mapOf(
20+
"a" to JsType.IntNum(intA),
21+
"b" to JsType.IntNum(intB)
22+
)
23+
} else {
24+
mapOf(
25+
"a" to JsType.Str(a),
26+
"b" to JsType.Str(b)
27+
)
28+
}
29+
30+
return engine.evaluate(
31+
context = context,
32+
script = testScript
33+
)
34+
}
935
}

0 commit comments

Comments
 (0)