fix: Reopen last component from previous session (#262)

This commit is contained in:
Tommy Nguyen 2021-01-04 22:13:50 +01:00 коммит произвёл GitHub
Родитель 0e11850e2a
Коммит 6f64e208e0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
25 изменённых файлов: 607 добавлений и 206 удалений

Просмотреть файл

@ -61,6 +61,11 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
allWarningsAsErrors = true
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId project.ext.react.applicationId
minSdkVersion 21

Просмотреть файл

@ -14,7 +14,6 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.facebook.react.ReactActivity
import com.facebook.react.bridge.ReactContext
import com.facebook.react.modules.systeminfo.ReactNativeVersion
import com.google.android.material.appbar.MaterialToolbar
import com.microsoft.reacttestapp.component.ComponentActivity
@ -37,8 +36,7 @@ class MainActivity : ReactActivity() {
@Inject
lateinit var bundleNameProvider: ReactBundleNameProvider
private val testAppReactNativeHost: TestAppReactNativeHost
get() = reactNativeHost as TestAppReactNativeHost
private var didInitialNavigation = false
private val newComponentViewModel = { component: Component ->
ComponentViewModel(
@ -49,8 +47,12 @@ class MainActivity : ReactActivity() {
)
}
private val startComponent: (ComponentViewModel) -> Unit = { component: ComponentViewModel ->
didInitialNavigation = true;
private val session by lazy {
Session(applicationContext)
}
private val startComponent: (ComponentViewModel) -> Unit = { component ->
didInitialNavigation = true
when (component.presentationStyle) {
"modal" -> {
ComponentBottomSheetDialogFragment
@ -63,21 +65,24 @@ class MainActivity : ReactActivity() {
}
}
private var didInitialNavigation = false;
private val testAppReactNativeHost: TestAppReactNativeHost
get() = reactNativeHost as TestAppReactNativeHost
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
didInitialNavigation = savedInstanceState?.getBoolean("didInitialNavigation", false) == true
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val manifest = manifestProvider.manifest
didInitialNavigation = savedInstanceState?.getBoolean("didInitialNavigation", false) == true
val (manifest, checksum) = manifestProvider.fromResources()
?: throw IllegalStateException("app.json is not provided or TestApp is misconfigured")
if (manifest.components.count() == 1) {
val component = newComponentViewModel(manifest.components[0])
testAppReactNativeHost.addReactInstanceEventListener { _: ReactContext ->
val index = if (manifest.components.count() == 1) 0 else session.lastOpenedComponent(checksum)
index?.let {
val component = newComponentViewModel(manifest.components[it])
testAppReactNativeHost.addReactInstanceEventListener {
if (!didInitialNavigation) {
startComponent(component)
}
@ -85,14 +90,44 @@ class MainActivity : ReactActivity() {
}
setupToolbar(manifest.displayName)
setupRecyclerView(manifest.components)
setupRecyclerView(manifest.components, checksum)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean("didInitialNavigation", didInitialNavigation);
outState.putBoolean("didInitialNavigation", didInitialNavigation)
super.onSaveInstanceState(outState)
}
private fun reload(bundleSource: BundleSource) {
testAppReactNativeHost.reload(this, bundleSource)
}
private fun setupRecyclerView(manifestComponents: List<Component>, manifestChecksum: String) {
val components = manifestComponents.map(newComponentViewModel)
findViewById<RecyclerView>(R.id.recyclerview).apply {
layoutManager = LinearLayoutManager(context)
adapter = ComponentListAdapter(
LayoutInflater.from(context),
components
) { component, index ->
startComponent(component)
session.storeComponent(index, manifestChecksum)
}
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
findViewById<TextView>(R.id.runtime_info).apply {
text = resources.getString(
R.string.runtime_info,
ReactNativeVersion.VERSION["major"] as Int,
ReactNativeVersion.VERSION["minor"] as Int,
ReactNativeVersion.VERSION["patch"] as Int,
reactInstanceManager.jsExecutorName
)
}
}
private fun setupToolbar(displayName: String) {
val toolbar = findViewById<MaterialToolbar>(R.id.top_app_bar)
@ -107,6 +142,12 @@ class MainActivity : ReactActivity() {
reload(BundleSource.Server)
true
}
R.id.remember_last_component -> {
val enable = !menuItem.isChecked
menuItem.isChecked = enable
session.shouldRememberLastComponent = enable
true
}
R.id.show_dev_options -> {
reactInstanceManager.devSupportManager.showDevOptionsDialog()
true
@ -121,36 +162,11 @@ class MainActivity : ReactActivity() {
}
}
private fun reload(bundleSource: BundleSource) {
testAppReactNativeHost.reload(this, bundleSource)
}
private fun updateMenuItemState(toolbar: MaterialToolbar, bundleSource: BundleSource) {
toolbar.menu.apply {
findItem(R.id.load_embedded_js_bundle).isEnabled = bundleNameProvider.bundleName != null
findItem(R.id.remember_last_component).isChecked = session.shouldRememberLastComponent
findItem(R.id.show_dev_options).isEnabled = bundleSource == BundleSource.Server
}
}
private fun setupRecyclerView(manifestComponents: List<Component>) {
val components = manifestComponents.map(newComponentViewModel)
findViewById<RecyclerView>(R.id.recyclerview).apply {
layoutManager = LinearLayoutManager(context)
adapter = ComponentListAdapter(
LayoutInflater.from(context), components, startComponent
)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
findViewById<TextView>(R.id.runtime_info).apply {
text = resources.getString(
R.string.runtime_info,
ReactNativeVersion.VERSION["major"] as Int,
ReactNativeVersion.VERSION["minor"] as Int,
ReactNativeVersion.VERSION["patch"] as Int,
reactInstanceManager.jsExecutorName
)
}
}
}

Просмотреть файл

@ -0,0 +1,42 @@
package com.microsoft.reacttestapp
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import androidx.core.content.edit
private const val MANIFEST_CHECKSUM = "ManifestChecksum"
private const val REMEMBER_LAST_COMPONENT_ENABLED = "RememberLastComponent/Enabled"
private const val REMEMBER_LAST_COMPONENT_INDEX = "RememberLastComponent/Index"
class Session(context: Context) {
private val sharedPreferences: SharedPreferences =
context.getSharedPreferences(BuildConfig.APPLICATION_ID, MODE_PRIVATE)
private var lastComponentIndex: Int
get() = sharedPreferences.getInt(REMEMBER_LAST_COMPONENT_INDEX, -1)
set(index) = sharedPreferences.edit { putInt(REMEMBER_LAST_COMPONENT_INDEX, index) }
private var manifestChecksum: String?
get() = sharedPreferences.getString(MANIFEST_CHECKSUM, null)
set(checksum) = sharedPreferences.edit { putString(MANIFEST_CHECKSUM, checksum) }
var shouldRememberLastComponent: Boolean
get() = sharedPreferences.getBoolean(REMEMBER_LAST_COMPONENT_ENABLED, false)
set(enabled) = sharedPreferences.edit { putBoolean(REMEMBER_LAST_COMPONENT_ENABLED, enabled) }
fun lastOpenedComponent(checksum: String): Int? {
if (!shouldRememberLastComponent || checksum != manifestChecksum) {
return null
}
val index = lastComponentIndex
return if (index < 0) null else index
}
fun storeComponent(index: Int, checksum: String) {
lastComponentIndex = index
manifestChecksum = checksum
}
}

Просмотреть файл

@ -9,7 +9,7 @@ import com.microsoft.reacttestapp.R
class ComponentListAdapter(
private val layoutInflater: LayoutInflater,
private val components: List<ComponentViewModel>,
private val listener: (ComponentViewModel) -> Unit
private val listener: (ComponentViewModel, Int) -> Unit
) : RecyclerView.Adapter<ComponentListAdapter.ComponentViewHolder>() {
override fun getItemCount() = components.size
@ -28,7 +28,7 @@ class ComponentListAdapter(
inner class ComponentViewHolder(private val view: TextView) : RecyclerView.ViewHolder(view) {
init {
view.setOnClickListener { listener(components[adapterPosition]) }
view.setOnClickListener { listener(components[adapterPosition], adapterPosition) }
}
fun bindTo(component: ComponentViewModel) {

Просмотреть файл

@ -2,6 +2,7 @@ package com.microsoft.reacttestapp.manifest
import android.content.Context
import com.squareup.moshi.JsonAdapter
import java.security.MessageDigest
import javax.inject.Inject
import javax.inject.Singleton
@ -10,16 +11,27 @@ class ManifestProvider @Inject constructor(
private val context: Context,
private val adapter: JsonAdapter<Manifest>
) {
val manifest: Manifest? by lazy {
fun fromResources(): Pair<Manifest, String>? {
val appIdentifier = context.resources
.getIdentifier("raw/app", null, context.packageName)
if (appIdentifier != 0) {
val manifest = context.resources
return if (appIdentifier == 0) {
null
} else {
val json = context.resources
.openRawResource(appIdentifier)
.bufferedReader()
.use { it.readText() }
adapter.fromJson(manifest)
} else null
val manifest = adapter.fromJson(json)
return if (manifest == null) null else Pair(manifest, json.checksum("SHA-256"))
}
}
}
fun ByteArray.toHex(): String {
return joinToString("") { "%02x".format(it) }
}
fun String.checksum(algorithm: String): String {
return MessageDigest.getInstance(algorithm).digest(toByteArray()).toHex()
}

Просмотреть файл

@ -11,6 +11,12 @@
android:contentDescription="@string/content_description_load_from_dev_server"
android:title="@string/load_from_dev_server"
app:showAsAction="never" />
<item
android:id="@+id/remember_last_component"
android:checkable="true"
android:contentDescription="@string/content_description_remember_last_component"
android:title="@string/remember_last_component"
app:showAsAction="never" />
<item
android:id="@+id/show_dev_options"
android:contentDescription="@string/content_description_show_dev_options"

Просмотреть файл

@ -4,6 +4,8 @@
<string name="load_embedded_js_bundle">Load embedded JS bundle</string>
<string name="content_description_load_from_dev_server">Load from dev server</string>
<string name="load_from_dev_server">Load from dev server</string>
<string name="content_description_remember_last_component">Remember last opened component</string>
<string name="remember_last_component">Remember last opened component</string>
<string name="content_description_show_dev_options">Show dev options</string>
<string name="show_dev_options">Show dev options</string>
<string name="runtime_info">React Native v%1$d.%2$d.%3$d\n(%4$s)</string>

Просмотреть файл

@ -11,11 +11,13 @@ import XCTest
class ManifestTests: XCTestCase {
func testReadFromFile() {
guard let manifest = Manifest.fromFile() else {
guard let (manifest, checksum) = Manifest.fromFile() else {
XCTFail("Failed to read 'app.json'")
return
}
XCTAssertEqual(checksum.count, 64)
XCTAssertEqual(manifest.name, "Example")
XCTAssertEqual(manifest.displayName, "Example")
XCTAssertEqual(manifest.components.count, 2)
@ -71,11 +73,13 @@ class ManifestTests: XCTestCase {
"""
guard let data = json.data(using: .utf8),
let manifest = Manifest.from(data: data) else {
let (manifest, checksum) = Manifest.from(data: data) else {
XCTFail("Failed to read manifest")
return
}
XCTAssertEqual(checksum.count, 64)
XCTAssertEqual(manifest.name, expected.name)
XCTAssertEqual(manifest.displayName, expected.displayName)
XCTAssertEqual(manifest.components.count, expected.components.count)
@ -136,7 +140,7 @@ class ManifestTests: XCTestCase {
"""
guard let data = json.data(using: .utf8),
let manifest = Manifest.from(data: data),
let (manifest, _) = Manifest.from(data: data),
let component = manifest.components.first,
let initialProperties = component.initialProperties else {
XCTFail("Failed to read manifest")

Просмотреть файл

@ -26,58 +26,58 @@ namespace ReactTestAppTests
public:
TEST_METHOD(ParseManifestWithOneComponent)
{
std::optional<ReactTestApp::Manifest> manifest =
ReactTestApp::GetManifest("manifestTestFiles/simpleManifest.json");
if (!manifest.has_value()) {
auto result = ReactTestApp::GetManifest("manifestTestFiles/simpleManifest.json");
if (!result.has_value()) {
Assert::Fail(L"Couldn't read manifest file");
}
auto &manifestContents = manifest.value();
Assert::AreEqual(manifestContents.name, {"Example"});
Assert::AreEqual(manifestContents.displayName, {"Example"});
Assert::AreEqual(manifestContents.components[0].appKey, {"Example"});
Assert::AreEqual(manifestContents.components[0].displayName.value(), {"App"});
Assert::IsFalse(manifestContents.components[0].initialProperties.has_value());
auto&& [manifest, checksum] = result.value();
Assert::AreEqual(manifest.name, {"Example"});
Assert::AreEqual(manifest.displayName, {"Example"});
Assert::AreEqual(manifest.components[0].appKey, {"Example"});
Assert::AreEqual(manifest.components[0].displayName.value(), {"App"});
Assert::IsFalse(manifest.components[0].initialProperties.has_value());
}
TEST_METHOD(ParseManifestWithMultipleComponents)
{
std::optional<ReactTestApp::Manifest> manifest =
ReactTestApp::GetManifest("manifestTestFiles/withMultipleComponents.json");
if (!manifest.has_value()) {
auto result = ReactTestApp::GetManifest("manifestTestFiles/withMultipleComponents.json");
if (!result.has_value()) {
Assert::Fail(L"Couldn't read manifest file");
}
auto &manifestContents = manifest.value();
Assert::AreEqual(manifestContents.name, {"Example"});
Assert::AreEqual(manifestContents.displayName, {"Example"});
Assert::AreEqual(manifestContents.components.size(), {2});
auto&& [manifest, checksum] = result.value();
Assert::AreEqual(manifestContents.components[0].appKey, {"0"});
Assert::IsFalse(manifestContents.components[0].displayName.has_value());
Assert::IsTrue(manifestContents.components[0].initialProperties.has_value());
Assert::AreEqual(manifest.name, {"Example"});
Assert::AreEqual(manifest.displayName, {"Example"});
Assert::AreEqual(manifest.components.size(), {2});
Assert::AreEqual(manifest.components[0].appKey, {"0"});
Assert::IsFalse(manifest.components[0].displayName.has_value());
Assert::IsTrue(manifest.components[0].initialProperties.has_value());
Assert::AreEqual(std::any_cast<std::string>(
manifestContents.components[0].initialProperties.value()["key"]),
manifest.components[0].initialProperties.value()["key"]),
{"value"});
Assert::AreEqual(manifestContents.components[1].appKey, {"1"});
Assert::AreEqual(manifestContents.components[1].displayName.value(), {"1"});
Assert::IsFalse(manifestContents.components[1].initialProperties.has_value());
Assert::AreEqual(manifest.components[1].appKey, {"1"});
Assert::AreEqual(manifest.components[1].displayName.value(), {"1"});
Assert::IsFalse(manifest.components[1].initialProperties.has_value());
}
TEST_METHOD(ParseManifestWithComplexInitialProperties)
{
std::optional<ReactTestApp::Manifest> manifest =
ReactTestApp::GetManifest("manifestTestFiles/withComplexInitialProperties.json");
if (!manifest.has_value()) {
auto result = ReactTestApp::GetManifest("manifestTestFiles/withComplexInitialProperties.json");
if (!result.has_value()) {
Assert::Fail(L"Couldn't read manifest file");
}
auto &manifestContents = manifest.value();
auto &component = manifestContents.components[0];
Assert::AreEqual(manifestContents.name, {"Name"});
Assert::AreEqual(manifestContents.displayName, {"Display Name"});
auto&& [manifest, checksum] = result.value();
Assert::AreEqual(manifest.name, {"Name"});
Assert::AreEqual(manifest.displayName, {"Display Name"});
auto &component = manifest.components[0];
Assert::AreEqual(component.appKey, {"AppKey"});
Assert::IsFalse(component.displayName.has_value());
Assert::IsTrue(component.initialProperties.has_value());

Просмотреть файл

@ -13,6 +13,7 @@
196C724123319A85006556ED /* QRCodeReaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196C724023319A85006556ED /* QRCodeReaderDelegate.swift */; };
196C7215232F1788006556ED /* ReactInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196C7214232F1788006556ED /* ReactInstance.swift */; };
19ECD0D8232ED425003D8557 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19ECD0D7232ED425003D8557 /* SceneDelegate.swift */; };
19A624A4258C95F000032776 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A624A3258C95F000032776 /* Session.swift */; };
1914199A234B2DD800D856AE /* RCTDevSupport+UIScene.m in Sources */ = {isa = PBXBuildFile; fileRef = 19141999234B2DD800D856AE /* RCTDevSupport+UIScene.m */; };
196C22622490CB7600449D3C /* React+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 196C22602490CB7600449D3C /* React+Compatibility.m */; };
1988284524105BEC005057FF /* UIViewController+ReactTestApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 1988284424105BEC005057FF /* UIViewController+ReactTestApp.m */; };
@ -40,24 +41,25 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
19141999234B2DD800D856AE /* RCTDevSupport+UIScene.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "RCTDevSupport+UIScene.m"; sourceTree = "<group>"; };
192DD200240FCAF5004E9CEB /* Manifest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Manifest.swift; sourceTree = "<group>"; };
192F052624AD3CC500A48456 /* ReactTestApp.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.release.xcconfig; sourceTree = "<group>"; };
192F052724AD3CC500A48456 /* ReactTestApp.common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.common.xcconfig; sourceTree = "<group>"; };
192F052824AD3CC500A48456 /* ReactTestApp.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.debug.xcconfig; sourceTree = "<group>"; };
196C22602490CB7600449D3C /* React+Compatibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "React+Compatibility.m"; sourceTree = "<group>"; };
196C22612490CB7600449D3C /* React+Compatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "React+Compatibility.h"; sourceTree = "<group>"; };
196C7207232EF5DC006556ED /* ReactTestApp-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactTestApp-Bridging-Header.h"; sourceTree = "<group>"; };
196C7214232F1788006556ED /* ReactInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactInstance.swift; sourceTree = "<group>"; };
196C7216232F6CD9006556ED /* ReactTestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ReactTestApp.entitlements; sourceTree = "<group>"; };
196C724023319A85006556ED /* QRCodeReaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeReaderDelegate.swift; sourceTree = "<group>"; };
1988282224105BCC005057FF /* UIViewController+ReactTestApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+ReactTestApp.h"; sourceTree = "<group>"; };
1988284424105BEC005057FF /* UIViewController+ReactTestApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+ReactTestApp.m"; sourceTree = "<group>"; };
19ECD0D2232ED425003D8557 /* ReactTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
19ECD0D5232ED425003D8557 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
19ECD0D7232ED425003D8557 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
19ECD0D9232ED425003D8557 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
192DD200240FCAF5004E9CEB /* Manifest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Manifest.swift; sourceTree = "<group>"; };
196C724023319A85006556ED /* QRCodeReaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeReaderDelegate.swift; sourceTree = "<group>"; };
196C7214232F1788006556ED /* ReactInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactInstance.swift; sourceTree = "<group>"; };
19ECD0D7232ED425003D8557 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
19A624A3258C95F000032776 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = "<group>"; };
196C7207232EF5DC006556ED /* ReactTestApp-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactTestApp-Bridging-Header.h"; sourceTree = "<group>"; };
19141999234B2DD800D856AE /* RCTDevSupport+UIScene.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "RCTDevSupport+UIScene.m"; sourceTree = "<group>"; };
196C22612490CB7600449D3C /* React+Compatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "React+Compatibility.h"; sourceTree = "<group>"; };
196C22602490CB7600449D3C /* React+Compatibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "React+Compatibility.m"; sourceTree = "<group>"; };
1988282224105BCC005057FF /* UIViewController+ReactTestApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+ReactTestApp.h"; sourceTree = "<group>"; };
1988284424105BEC005057FF /* UIViewController+ReactTestApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+ReactTestApp.m"; sourceTree = "<group>"; };
19ECD0DB232ED427003D8557 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
196C7216232F6CD9006556ED /* ReactTestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ReactTestApp.entitlements; sourceTree = "<group>"; };
192F052724AD3CC500A48456 /* ReactTestApp.common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.common.xcconfig; sourceTree = "<group>"; };
192F052824AD3CC500A48456 /* ReactTestApp.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.debug.xcconfig; sourceTree = "<group>"; };
192F052624AD3CC500A48456 /* ReactTestApp.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.release.xcconfig; sourceTree = "<group>"; };
19ECD0E1232ED427003D8557 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
19ECD0E3232ED427003D8557 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
19ECD0E8232ED427003D8557 /* ReactTestAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactTestAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@ -122,6 +124,7 @@
192DD200240FCAF5004E9CEB /* Manifest.swift */,
196C724023319A85006556ED /* QRCodeReaderDelegate.swift */,
196C7214232F1788006556ED /* ReactInstance.swift */,
19A624A3258C95F000032776 /* Session.swift */,
196C7207232EF5DC006556ED /* ReactTestApp-Bridging-Header.h */,
19141999234B2DD800D856AE /* RCTDevSupport+UIScene.m */,
196C22612490CB7600449D3C /* React+Compatibility.h */,
@ -319,6 +322,7 @@
196C22622490CB7600449D3C /* React+Compatibility.m in Sources */,
196C7215232F1788006556ED /* ReactInstance.swift in Sources */,
19ECD0D8232ED425003D8557 /* SceneDelegate.swift in Sources */,
19A624A4258C95F000032776 /* Session.swift in Sources */,
1988284524105BEC005057FF /* UIViewController+ReactTestApp.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

Просмотреть файл

@ -8,7 +8,23 @@
import QRCodeReader
import UIKit
typealias NavigationLink = (String, () -> Void)
private struct NavigationLink {
let title: String
let action: (() -> Void)?
let accessoryView: UIView?
init(title: String, action: @escaping () -> Void) {
self.title = title
self.action = action
accessoryView = nil
}
init(title: String, accessoryView: UIView) {
self.title = title
action = nil
self.accessoryView = accessoryView
}
}
private struct SectionData {
var items: [NavigationLink]
@ -16,6 +32,11 @@ private struct SectionData {
}
final class ContentViewController: UITableViewController {
private enum Section {
static let components = 0
static let settings = 1
}
private let reactInstance: ReactInstance
private var sections: [SectionData]
@ -27,18 +48,6 @@ final class ContentViewController: UITableViewController {
sections = []
super.init(style: .grouped)
title = "ReactTestApp"
#if targetEnvironment(simulator)
let keyboardShortcut = " (⌃⌘Z)"
#else
let keyboardShortcut = ""
#endif
sections.append(SectionData(
items: [],
footer: "\(runtimeInfo())\n\nShake your device\(keyboardShortcut) to open the React Native debug menu."
))
}
@available(*, unavailable)
@ -51,26 +60,55 @@ final class ContentViewController: UITableViewController {
override public func viewDidLoad() {
super.viewDidLoad()
guard let (manifest, checksum) = Manifest.fromFile() else {
return
}
let components = manifest.components
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.reactInstance.initReact { components in
let items: [NavigationLink] = components.map { component in
(component.displayName ?? component.appKey, { self?.navigate(to: component) })
}
DispatchQueue.main.async {
guard let strongSelf = self else {
return
self?.reactInstance.initReact {
if let index = components.count == 1 ? 0 : Session.lastOpenedComponent(checksum) {
DispatchQueue.main.async {
self?.navigate(to: components[index])
}
if components.count == 1, let component = components.first {
strongSelf.navigate(to: component)
}
strongSelf.sections[0].items = items
strongSelf.tableView.reloadData()
}
}
}
title = manifest.displayName
#if targetEnvironment(simulator)
let keyboardShortcut = " (⌃⌘Z)"
#else
let keyboardShortcut = ""
#endif
sections.append(SectionData(
items: components.enumerated().map { index, component in
NavigationLink(title: component.displayName ?? component.appKey) { [weak self] in
self?.navigate(to: component)
Session.storeComponent(index: index, checksum: checksum)
}
},
footer: "\(runtimeInfo())\n\nShake your device\(keyboardShortcut) to open the React Native debug menu."
))
let rememberLastComponentSwitch = UISwitch()
rememberLastComponentSwitch.isOn = Session.shouldRememberLastComponent
rememberLastComponentSwitch.addTarget(
self,
action: #selector(rememberLastComponentSwitchDidChangeValue(_:)),
for: .valueChanged
)
sections.append(SectionData(
items: [
NavigationLink(
title: "Remember Last Opened Component",
accessoryView: rememberLastComponentSwitch
),
],
footer: nil
))
NotificationCenter.default.addObserver(
self,
selector: #selector(scanForQRCode(_:)),
@ -81,10 +119,12 @@ final class ContentViewController: UITableViewController {
// MARK: - UITableViewDelegate overrides
override public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let (_, action) = sections[indexPath.section].items[indexPath.row]
action()
tableView.deselectRow(at: indexPath, animated: true)
override public func tableView(_: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
indexPath.section == Section.components
}
override public func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
sections[indexPath.section].items[indexPath.row].action?()
}
// MARK: - UITableViewDataSource overrides
@ -94,16 +134,27 @@ final class ContentViewController: UITableViewController {
}
override public func tableView(_: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let (title, _) = sections[indexPath.section].items[indexPath.row]
let link = sections[indexPath.section].items[indexPath.row]
let cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
let presentsNewContent = indexPath.section == 0
cell.accessoryType = presentsNewContent ? .disclosureIndicator : .none
if let textLabel = cell.textLabel {
textLabel.text = title
textLabel.textColor = presentsNewContent ? .label : .link
textLabel.text = link.title
textLabel.textColor = .label
textLabel.allowsDefaultTighteningForTruncation = true
textLabel.numberOfLines = 1
}
switch indexPath.section {
case Section.components:
cell.accessoryType = .disclosureIndicator
cell.accessoryView = nil
case Section.settings:
cell.accessoryType = .none
cell.accessoryView = link.accessoryView
default:
assertionFailure()
}
return cell
}
@ -146,6 +197,10 @@ final class ContentViewController: UITableViewController {
}
}
@objc private func rememberLastComponentSwitchDidChangeValue(_ sender: UISwitch) {
Session.shouldRememberLastComponent = sender.isOn
}
private func runtimeInfo() -> String {
let version: String = {
guard let version = RCTGetReactNativeVersion() else {

Просмотреть файл

@ -5,6 +5,8 @@
// LICENSE file in the root directory of this source tree.
//
import CommonCrypto
import CryptoKit
import Foundation
struct Component: Decodable {
@ -50,7 +52,7 @@ struct Manifest: Decodable {
let displayName: String
let components: [Component]
static func fromFile() -> Manifest? {
static func fromFile() -> (Manifest, String)? {
guard let manifestURL = Bundle.main.url(forResource: "app", withExtension: "json"),
let data = try? Data(contentsOf: manifestURL, options: .uncached)
else {
@ -59,8 +61,12 @@ struct Manifest: Decodable {
return from(data: data)
}
static func from(data: Data) -> Manifest? {
try? JSONDecoder().decode(self, from: data)
static func from(data: Data) -> (Manifest, String)? {
guard let manifest = try? JSONDecoder().decode(self, from: data) else {
return nil
}
return (manifest, data.sha256)
}
}
@ -114,6 +120,24 @@ extension Array where Element == Any {
}
}
extension Data {
var sha256: String {
guard #available(iOS 13.0, macOS 10.15, *) else {
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
_ = withUnsafeBytes {
CC_SHA256($0.baseAddress, UInt32(count), &digest)
}
return digest.reduce("") { str, byte in
str + String(format: "%02x", UInt8(byte))
}
}
let digest = SHA256.hash(data: self)
return Array(digest.makeIterator()).map { String(format: "%02x", $0) }.joined()
}
}
extension Dictionary where Key == AnyHashable, Value == Any {
static func decode(from decoder: Decoder) throws -> Dictionary? {
guard let container = try? decoder.container(keyedBy: DynamicCodingKey.self) else {

Просмотреть файл

@ -20,7 +20,7 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
var remoteBundleURL: URL? {
didSet {
initReact(onDidInitialize: { _ in /* noop */ })
initReact(onDidInitialize: { /* noop */ })
}
}
@ -96,7 +96,7 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
assert(forTestingPurposesOnly)
}
func initReact(onDidInitialize: @escaping ([Component]) -> Void) {
func initReact(onDidInitialize: @escaping () -> Void) {
if let bridge = bridge {
if remoteBundleURL == nil {
// When loading the embedded bundle, we must disable remote
@ -113,10 +113,7 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
object: bridge
)
guard let manifest = Manifest.fromFile() else {
return
}
onDidInitialize(manifest.components)
onDidInitialize()
} else {
assertionFailure("Failed to instantiate RCTBridge")
}

Просмотреть файл

@ -0,0 +1,44 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
//
import Foundation
struct Session {
private enum Keys {
static let checksum = "ManifestChecksum"
static let rememberLastComponentEnabled = "RememberLastComponent/Enabled"
static let rememberLastComponentIndex = "RememberLastComponent/Index"
}
private static var lastComponentIndex: Int {
get { UserDefaults.standard.integer(forKey: Keys.rememberLastComponentIndex) }
set { UserDefaults.standard.set(newValue, forKey: Keys.rememberLastComponentIndex) }
}
private static var manifestChecksum: String? {
get { UserDefaults.standard.string(forKey: Keys.checksum) }
set { UserDefaults.standard.set(newValue, forKey: Keys.checksum) }
}
static var shouldRememberLastComponent: Bool {
get { UserDefaults.standard.bool(forKey: Keys.rememberLastComponentEnabled) }
set { UserDefaults.standard.set(newValue, forKey: Keys.rememberLastComponentEnabled) }
}
static func lastOpenedComponent(_ checksum: String) -> Int? {
guard shouldRememberLastComponent, checksum == manifestChecksum else {
return nil
}
return lastComponentIndex
}
static func storeComponent(index: Int, checksum: String) {
lastComponentIndex = index
manifestChecksum = checksum
}
}

Просмотреть файл

@ -10,6 +10,7 @@
193EF063247A736200BE8C79 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF062247A736200BE8C79 /* AppDelegate.swift */; };
193EF08F247A799D00BE8C79 /* Manifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF08E247A799D00BE8C79 /* Manifest.swift */; };
193EF098247B130700BE8C79 /* ReactInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF097247B130700BE8C79 /* ReactInstance.swift */; };
1960F339258C97C400AEC7A2 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1960F338258C97C400AEC7A2 /* Session.swift */; };
193EF065247A736200BE8C79 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF064247A736200BE8C79 /* ViewController.swift */; };
196C22652490CBAB00449D3C /* React+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 196C22632490CBAB00449D3C /* React+Compatibility.m */; };
193EF093247A830200BE8C79 /* UIViewController+ReactTestApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 193EF091247A830200BE8C79 /* UIViewController+ReactTestApp.m */; };
@ -39,27 +40,28 @@
/* Begin PBXFileReference section */
193EF05F247A736200BE8C79 /* ReactTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
193EF062247A736200BE8C79 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
193EF064247A736200BE8C79 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
193EF066247A736300BE8C79 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
193EF069247A736300BE8C79 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
193EF06B247A736300BE8C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
193EF06C247A736300BE8C79 /* ReactTestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ReactTestApp.entitlements; sourceTree = "<group>"; };
193EF071247A736300BE8C79 /* ReactTestAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactTestAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
193EF077247A736300BE8C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
193EF07C247A736300BE8C79 /* ReactTestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactTestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
193EF082247A736300BE8C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
193EF08E247A799D00BE8C79 /* Manifest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Manifest.swift; path = ../ReactTestAppShared/Manifest.swift; sourceTree = "<group>"; };
193EF091247A830200BE8C79 /* UIViewController+ReactTestApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIViewController+ReactTestApp.m"; path = "../ReactTestAppShared/UIViewController+ReactTestApp.m"; sourceTree = "<group>"; };
193EF092247A830200BE8C79 /* UIViewController+ReactTestApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+ReactTestApp.h"; path = "../ReactTestAppShared/UIViewController+ReactTestApp.h"; sourceTree = "<group>"; };
193EF094247A84DA00BE8C79 /* ReactTestApp-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ReactTestApp-Bridging-Header.h"; path = "../ReactTestAppShared/ReactTestApp-Bridging-Header.h"; sourceTree = "<group>"; };
193EF097247B130700BE8C79 /* ReactInstance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ReactInstance.swift; path = ../ReactTestAppShared/ReactInstance.swift; sourceTree = "<group>"; };
196C22632490CBAB00449D3C /* React+Compatibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "React+Compatibility.m"; path = "../ReactTestAppShared/React+Compatibility.m"; sourceTree = "<group>"; };
1960F338258C97C400AEC7A2 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Session.swift; path = ../ReactTestAppShared/Session.swift; sourceTree = "<group>"; };
193EF064247A736200BE8C79 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
193EF094247A84DA00BE8C79 /* ReactTestApp-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ReactTestApp-Bridging-Header.h"; path = "../ReactTestAppShared/ReactTestApp-Bridging-Header.h"; sourceTree = "<group>"; };
196C22642490CBAB00449D3C /* React+Compatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "React+Compatibility.h"; path = "../ReactTestAppShared/React+Compatibility.h"; sourceTree = "<group>"; };
196C22632490CBAB00449D3C /* React+Compatibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "React+Compatibility.m"; path = "../ReactTestAppShared/React+Compatibility.m"; sourceTree = "<group>"; };
193EF092247A830200BE8C79 /* UIViewController+ReactTestApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+ReactTestApp.h"; path = "../ReactTestAppShared/UIViewController+ReactTestApp.h"; sourceTree = "<group>"; };
193EF091247A830200BE8C79 /* UIViewController+ReactTestApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIViewController+ReactTestApp.m"; path = "../ReactTestAppShared/UIViewController+ReactTestApp.m"; sourceTree = "<group>"; };
193EF066247A736300BE8C79 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
193EF06C247A736300BE8C79 /* ReactTestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ReactTestApp.entitlements; sourceTree = "<group>"; };
19B368BC24B12C24002CCEFF /* ReactTestApp.common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.common.xcconfig; sourceTree = "<group>"; };
19B368BD24B12C24002CCEFF /* ReactTestApp.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.debug.xcconfig; sourceTree = "<group>"; };
19B368BE24B12C24002CCEFF /* ReactTestApp.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.release.xcconfig; sourceTree = "<group>"; };
193EF069247A736300BE8C79 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
193EF06B247A736300BE8C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
193EF071247A736300BE8C79 /* ReactTestAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactTestAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
19E791BF24B08E1400FA6468 /* ReactTestAppTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactTestAppTests.swift; sourceTree = "<group>"; };
193EF077247A736300BE8C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
193EF07C247A736300BE8C79 /* ReactTestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactTestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
19E791C224B08E4D00FA6468 /* ReactTestAppUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactTestAppUITests.swift; sourceTree = "<group>"; };
193EF082247A736300BE8C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -113,6 +115,7 @@
193EF062247A736200BE8C79 /* AppDelegate.swift */,
193EF08E247A799D00BE8C79 /* Manifest.swift */,
193EF097247B130700BE8C79 /* ReactInstance.swift */,
1960F338258C97C400AEC7A2 /* Session.swift */,
193EF064247A736200BE8C79 /* ViewController.swift */,
193EF094247A84DA00BE8C79 /* ReactTestApp-Bridging-Header.h */,
196C22642490CBAB00449D3C /* React+Compatibility.h */,
@ -307,6 +310,7 @@
193EF08F247A799D00BE8C79 /* Manifest.swift in Sources */,
196C22652490CBAB00449D3C /* React+Compatibility.m in Sources */,
193EF098247B130700BE8C79 /* ReactInstance.swift in Sources */,
1960F339258C97C400AEC7A2 /* Session.swift in Sources */,
193EF065247A736200BE8C79 /* ViewController.swift in Sources */,
193EF093247A830200BE8C79 /* UIViewController+ReactTestApp.m in Sources */,
);

Просмотреть файл

@ -10,10 +10,12 @@ import Cocoa
@NSApplicationMain
final class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet var reactMenu: NSMenu!
@IBOutlet var rememberLastComponentMenuItem: NSMenuItem!
private(set) lazy var reactInstance = ReactInstance()
private weak var mainWindow: NSWindow?
private var manifestChecksum: String?
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
true
@ -25,7 +27,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
let windows = NSApplication.shared.windows
mainWindow = windows.first { $0.identifier?.rawValue == "MainWindow" }
guard let manifest = Manifest.fromFile() else {
if Session.shouldRememberLastComponent {
rememberLastComponentMenuItem.state = .on
}
guard let (manifest, checksum) = Manifest.fromFile() else {
let item = reactMenu.addItem(
withTitle: "Could not load 'app.json'",
action: nil,
@ -35,28 +41,37 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
return
}
for (index, component) in manifest.components.enumerated() {
manifest.components.enumerated().forEach { index, component in
let title = component.displayName ?? component.appKey
let item = reactMenu.addItem(
withTitle: title,
action: #selector(onComponentSelected),
keyEquivalent: index < 9 ? String(index + 1) : ""
)
item.tag = index
item.keyEquivalentModifierMask = [.shift, .command]
item.isEnabled = false
item.representedObject = component
}
let components = manifest.components
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.reactInstance.initReact { _ in
self?.reactInstance.initReact {
DispatchQueue.main.async {
if manifest.components.count == 1 {
self?.present(manifest.components[0])
guard let strongSelf = self else {
return
}
self?.reactMenu.items.forEach { $0.isEnabled = true }
if let index = components.count == 1 ? 0 : Session.lastOpenedComponent(checksum) {
strongSelf.present(components[index])
}
strongSelf.reactMenu.items.forEach { $0.isEnabled = true }
}
}
}
manifestChecksum = checksum
}
func applicationWillTerminate(_: Notification) {
@ -72,18 +87,36 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}
present(component)
if let checksum = manifestChecksum {
Session.storeComponent(index: menuItem.tag, checksum: checksum)
}
}
@IBAction
func onLoadEmbeddedBundle(_: NSMenuItem) {
func onLoadEmbeddedBundleSelected(_: NSMenuItem) {
reactInstance.remoteBundleURL = nil
}
@IBAction
func onLoadFromDevServer(_: NSMenuItem) {
func onLoadFromDevServerSelected(_: NSMenuItem) {
reactInstance.remoteBundleURL = ReactInstance.jsBundleURL()
}
@IBAction
func onRememberLastComponentSelected(_ menuItem: NSMenuItem) {
switch menuItem.state {
case .mixed, .on:
Session.shouldRememberLastComponent = false
menuItem.state = .off
case .off:
Session.shouldRememberLastComponent = true
menuItem.state = .on
default:
assertionFailure()
}
}
// MARK: - Private
private enum WindowSize {

Просмотреть файл

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17156"/>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17506"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -376,13 +377,19 @@
<menuItem title="Load Embedded JS Bundle" id="eKz-kn-Unx">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="onLoadEmbeddedBundle:" target="Voe-Tx-rLC" id="Dp5-bz-UTv"/>
<action selector="onLoadEmbeddedBundleSelected:" target="Voe-Tx-rLC" id="Dp5-bz-UTv"/>
</connections>
</menuItem>
<menuItem title="Load From Dev Server" id="175-aB-GZg">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="onLoadFromDevServer:" target="Voe-Tx-rLC" id="FOl-Nq-cWc"/>
<action selector="onLoadFromDevServerSelected:" target="Voe-Tx-rLC" id="FOl-Nq-cWc"/>
</connections>
</menuItem>
<menuItem title="Remember Last Opened Component" id="lzS-xk-P1D">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="onRememberLastComponentSelected:" target="Voe-Tx-rLC" id="TFw-gh-YQX"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uJa-x1-YNS"/>
@ -435,6 +442,7 @@
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="ReactTestApp" customModuleProvider="target">
<connections>
<outlet property="reactMenu" destination="nwF-AT-VY9" id="mZ4-MI-fvy"/>
<outlet property="rememberLastComponentMenuItem" destination="lzS-xk-P1D" id="azf-vw-Bvd"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>

Просмотреть файл

@ -13,8 +13,10 @@
#include <winrt/Windows.UI.ViewManagement.h>
#include "MainPage.g.cpp"
#include "Session.h"
using ReactTestApp::JSBundleSource;
using ReactTestApp::Session;
using winrt::Microsoft::ReactNative::IJSValueWriter;
using winrt::ReactTestApp::implementation::MainPage;
using winrt::Windows::ApplicationModel::Core::CoreApplication;
@ -28,6 +30,7 @@ using winrt::Windows::UI::ViewManagement::ApplicationView;
using winrt::Windows::UI::Xaml::RoutedEventArgs;
using winrt::Windows::UI::Xaml::Window;
using winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem;
using winrt::Windows::UI::Xaml::Controls::ToggleMenuFlyoutItem;
using winrt::Windows::UI::Xaml::Input::KeyboardAccelerator;
using winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs;
@ -143,6 +146,12 @@ void MainPage::LoadFromJSBundle(IInspectable const &, RoutedEventArgs)
LoadJSBundleFrom(JSBundleSource::Embedded);
}
void MainPage::ToggleRememberLastComponent(IInspectable const &sender, RoutedEventArgs)
{
auto item = sender.as<ToggleMenuFlyoutItem>();
Session::ShouldRememberLastComponent(item.IsChecked());
}
void MainPage::Reload(Windows::Foundation::IInspectable const &, Windows::UI::Xaml::RoutedEventArgs)
{
reactInstance_.Reload();
@ -238,51 +247,64 @@ void MainPage::InitializeDebugMenu()
void MainPage::InitializeReactMenu()
{
std::optional<::ReactTestApp::Manifest> manifest = ::ReactTestApp::GetManifest("app.json");
RememberLastComponentMenuItem().IsChecked(Session::ShouldRememberLastComponent());
auto result = ::ReactTestApp::GetManifest("app.json");
auto menuItems = ReactMenuBarItem().Items();
if (!manifest.has_value()) {
if (!result.has_value()) {
MenuFlyoutItem newMenuItem;
newMenuItem.Text(L"Couldn't parse 'app.json'");
newMenuItem.IsEnabled(false);
menuItems.Append(newMenuItem);
return;
}
auto &&[manifest, checksum] = result.value();
manifestChecksum_ = std::move(checksum);
// If only one component is present load, it automatically. Otherwise, check
// whether we can reopen a component from previous session.
auto &components = manifest.components;
auto index = components.size() == 1 ? 0 : Session::GetLastOpenedComponent(manifestChecksum_);
if (index.has_value()) {
Loaded([this, component = components[index.value()]](IInspectable const &,
RoutedEventArgs const &) {
LoadReactComponent(component);
});
} else {
// If only one component is present load it automatically
auto &components = manifest.value().components;
if (components.size() == 1) {
LoadReactComponent(components.at(0));
} else {
AppTitle().Text(to_hstring(manifest.value().displayName));
AppTitle().Text(to_hstring(manifest.displayName));
}
auto keyboardAcceleratorKey = VirtualKey::Number1;
for (int i = 0; i < static_cast<int>(components.size()); ++i) {
auto &&component = components[i];
MenuFlyoutItem newMenuItem;
newMenuItem.Text(winrt::to_hstring(component.displayName.value_or(component.appKey)));
newMenuItem.Click(
[this, component = std::move(component), i](IInspectable const &, RoutedEventArgs) {
LoadReactComponent(component);
Session::StoreComponent(i, manifestChecksum_);
});
// Add keyboard accelerator for first nine (1-9) components
if (keyboardAcceleratorKey <= VirtualKey::Number9) {
auto const num = std::underlying_type_t<VirtualKey>(keyboardAcceleratorKey) -
std::underlying_type_t<VirtualKey>(VirtualKey::Number0);
newMenuItem.AccessKey(to_hstring(num));
KeyboardAccelerator keyboardAccelerator;
keyboardAccelerator.Modifiers(VirtualKeyModifiers::Control |
VirtualKeyModifiers::Shift);
keyboardAccelerator.Key(keyboardAcceleratorKey);
newMenuItem.KeyboardAccelerators().Append(keyboardAccelerator);
keyboardAcceleratorKey =
static_cast<VirtualKey>(static_cast<int32_t>(keyboardAcceleratorKey) + 1);
}
auto keyboardAcceleratorKey = VirtualKey::Number1;
for (auto &&c : components) {
MenuFlyoutItem newMenuItem;
newMenuItem.Text(winrt::to_hstring(c.displayName.value_or(c.appKey)));
newMenuItem.Click(
[this, component = std::move(c)](IInspectable const &, RoutedEventArgs) {
LoadReactComponent(component);
});
// Add keyboard accelerators for first nine (1-9) components
if (keyboardAcceleratorKey <= VirtualKey::Number9) {
auto const num = std::underlying_type_t<VirtualKey>(keyboardAcceleratorKey) -
std::underlying_type_t<VirtualKey>(VirtualKey::Number0);
newMenuItem.AccessKey(to_hstring(num));
KeyboardAccelerator keyboardAccelerator;
keyboardAccelerator.Modifiers(VirtualKeyModifiers::Control |
VirtualKeyModifiers::Shift);
keyboardAccelerator.Key(keyboardAcceleratorKey);
newMenuItem.KeyboardAccelerators().Append(keyboardAccelerator);
keyboardAcceleratorKey =
static_cast<VirtualKey>(static_cast<int32_t>(keyboardAcceleratorKey) + 1);
}
menuItems.Append(newMenuItem);
}
menuItems.Append(newMenuItem);
}
}

Просмотреть файл

@ -22,10 +22,16 @@ namespace winrt::ReactTestApp::implementation
public:
MainPage();
// React menu
void LoadFromDevServer(Windows::Foundation::IInspectable const &,
Windows::UI::Xaml::RoutedEventArgs);
void LoadFromJSBundle(Windows::Foundation::IInspectable const &,
Windows::UI::Xaml::RoutedEventArgs);
void ToggleRememberLastComponent(Windows::Foundation::IInspectable const &,
Windows::UI::Xaml::RoutedEventArgs);
// Debug menu
void Reload(Windows::Foundation::IInspectable const &, Windows::UI::Xaml::RoutedEventArgs);
void ToggleBreakOnFirstLine(Windows::Foundation::IInspectable const &,
@ -46,6 +52,7 @@ namespace winrt::ReactTestApp::implementation
using Base = MainPageT;
::ReactTestApp::ReactInstance reactInstance_;
std::string manifestChecksum_;
void InitializeDebugMenu();
void InitializeReactMenu();

Просмотреть файл

@ -21,6 +21,12 @@
<MenuBarItem x:Name="ReactMenuBarItem" Title="React" AccessKey="R">
<MenuFlyoutItem Text="Load from JS bundle" Click="LoadFromJSBundle" AccessKey="E"/>
<MenuFlyoutItem Text="Load from dev server" Click="LoadFromDevServer" AccessKey="D"/>
<ToggleMenuFlyoutItem
x:Name="RememberLastComponentMenuItem"
Text="Remember last opened component"
Click="ToggleRememberLastComponent"
AccessKey="R"
/>
<MenuFlyoutSeparator/>
</MenuBarItem>
<MenuBarItem x:Name="DebugMenuBarItem" IsEnabled="false" Title="Debug" AccessKey="D">

Просмотреть файл

@ -13,11 +13,26 @@
#include <iostream>
#include <nlohmann/json.hpp>
#include <winrt/Windows.Security.Cryptography.Core.h>
#include <winrt/Windows.Security.Cryptography.h>
using nlohmann::detail::value_t;
using winrt::Windows::Security::Cryptography::BinaryStringEncoding;
using winrt::Windows::Security::Cryptography::CryptographicBuffer;
using winrt::Windows::Security::Cryptography::Core::HashAlgorithmNames;
using winrt::Windows::Security::Cryptography::Core::HashAlgorithmProvider;
namespace
{
std::string checksum(std::string const &data)
{
auto hasher = HashAlgorithmProvider::OpenAlgorithm(HashAlgorithmNames::Sha256());
auto digest = hasher.HashData(CryptographicBuffer::ConvertStringToBinary(
winrt::to_hstring(data), BinaryStringEncoding::Utf8));
auto checksum = CryptographicBuffer::EncodeToHexString(digest);
return winrt::to_string(checksum);
}
template <typename T>
std::optional<T> get_optional(const nlohmann::json &j, const std::string &key)
{
@ -99,15 +114,24 @@ namespace ReactTestApp
m.components = j.at("components").get<std::vector<Component>>();
}
std::optional<Manifest> GetManifest(std::string const &manifestFileName)
std::optional<std::pair<Manifest, std::string>> GetManifest(std::string const &filename)
{
std::ifstream manifestFile(manifestFileName);
nlohmann::json j = nlohmann::json::parse(manifestFile, nullptr, false);
std::string json;
{
std::ifstream stream(filename);
stream.seekg(0, std::ios::end);
json.reserve(static_cast<size_t>(stream.tellg()));
stream.seekg(0, std::ios::beg);
json.assign(std::istreambuf_iterator<char>(stream), {});
}
auto j = nlohmann::json::parse(json, nullptr, false);
if (j.is_discarded()) {
return std::nullopt;
}
Manifest m = j.get<Manifest>();
return m;
return std::make_pair(j.get<Manifest>(), checksum(json));
}
} // namespace ReactTestApp

Просмотреть файл

@ -11,6 +11,7 @@
#include <map>
#include <optional>
#include <string>
#include <utility>
#include <vector>
namespace ReactTestApp
@ -28,6 +29,6 @@ namespace ReactTestApp
std::vector<Component> components;
};
std::optional<Manifest> GetManifest(std::string const &manifestFileName);
std::optional<std::pair<Manifest, std::string>> GetManifest(std::string const &filename);
} // namespace ReactTestApp

Просмотреть файл

@ -153,6 +153,7 @@
<ClInclude Include="$(ReactTestAppDir)\Manifest.h" />
<ClInclude Include="$(ReactTestAppDir)\ReactInstance.h" />
<ClInclude Include="$(ReactTestAppDir)\ReactPackageProvider.h" />
<ClInclude Include="$(ReactTestAppDir)\Session.h" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="$(ReactTestAppDir)\App.xaml">

Просмотреть файл

@ -28,6 +28,7 @@
<ClInclude Include="$(ReactTestAppDir)\Manifest.h" />
<ClInclude Include="$(ReactTestAppDir)\ReactInstance.h" />
<ClInclude Include="$(ReactTestAppDir)\ReactPackageProvider.h" />
<ClInclude Include="$(ReactTestAppDir)\Session.h" />
</ItemGroup>
<ItemGroup>
<Text Include="$(ProjectRootDir)\app.json">

Просмотреть файл

@ -0,0 +1,83 @@
//
// Copyright (c) Microsoft Corporation
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
//
#pragma once
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Storage.h>
using winrt::Windows::Foundation::PropertyValue;
using winrt::Windows::Storage::ApplicationData;
namespace ReactTestApp
{
struct Session {
private:
static inline const std::wstring kChecksum = L"ManifestChecksum";
static inline const std::wstring kRememberLastComponentEnabled =
L"RememberLastComponent/Enabled";
static inline const std::wstring kRememberLastComponentIndex =
L"RememberLastComponent/Index";
static auto LocalSettings()
{
return ApplicationData::Current().LocalSettings().Values();
}
static int LastComponentIndex()
{
auto index = LocalSettings().Lookup(kRememberLastComponentIndex);
return winrt::unbox_value_or<int>(index, -1);
}
static void LastComponentIndex(int value)
{
LocalSettings().Insert(kRememberLastComponentIndex, PropertyValue::CreateInt32(value));
}
static std::string ManifestChecksum()
{
auto checksum = LocalSettings().Lookup(kChecksum);
auto value = winrt::unbox_value_or<winrt::hstring>(checksum, L"");
return winrt::to_string(value);
}
static void ManifestChecksum(std::string const &value)
{
auto v = PropertyValue::CreateString(winrt::to_hstring(value));
LocalSettings().Insert(kChecksum, v);
}
public:
static bool ShouldRememberLastComponent()
{
auto value = LocalSettings().Lookup(kRememberLastComponentEnabled);
return winrt::unbox_value_or<bool>(value, false);
}
static void ShouldRememberLastComponent(bool enable)
{
auto value = PropertyValue::CreateBoolean(enable);
LocalSettings().Insert(kRememberLastComponentEnabled, value);
}
static std::optional<int> GetLastOpenedComponent(std::string const &manifestChecksum)
{
if (!ShouldRememberLastComponent() || manifestChecksum != ManifestChecksum()) {
return std::nullopt;
}
return LastComponentIndex();
}
static void StoreComponent(int index, std::string const &manifestChecksum)
{
LastComponentIndex(index);
ManifestChecksum(manifestChecksum);
}
};
} // namespace ReactTestApp