为 iOS 15 和 Android 12 准备您的应用
大家好!
随着今年晚些时候新的移动操作系统版本发布,我们建议您提前准备好您的 React Native 应用,以避免在这些版本普遍可用时出现问题。
大家好!
随着今年晚些时候新的移动操作系统版本发布,我们建议您提前准备好您的 React Native 应用,以避免在这些版本普遍可用时出现问题。
长期以来,Apple 一直不鼓励使用 UIWebView,而更倾向于使用 WKWebView。在即将在未来几个月发布的 iOS 12 中,UIWebView 将被正式弃用。React Native 的 iOS WebView 实现严重依赖 UIWebView 类。因此,鉴于这些发展,我们为 WebView React Native 组件构建了一个使用 WKWebView 的新原生 iOS 后端。
这些更改的最后部分已在此提交中落地,并将随0.57版本发布。
要选择此新实现,请使用useWebKit prop。
<WebView
useWebKit={true}
source={{url: 'https://www.google.com'}}
/>
UIWebView 没有合法的方式来促进 WebView 中运行的 JavaScript 与 React Native 之间的通信。当消息从 WebView 发送时,我们依靠一种 hack 方式将其传递给 React Native。简单来说,我们将消息数据编码到一个带有特殊方案的 URL 中,然后将 WebView 导航到该 URL。在原生端,我们拦截并取消了这次导航,从 URL 中解析出数据,最后调用了 React Native。这种实现容易出错且不安全。我很高兴地宣布,我们利用了 WKWebView 的特性来完全取代它。
WKWebView 相对于 UIWebView 的其他优点包括更快的 JavaScript 执行速度和多进程架构。请参阅这个2014 年 WWDC 了解更多详情。
如果您的组件使用以下属性,那么在切换到WKWebView时可能会遇到问题。目前,我们建议您避免使用这些属性
不一致的行为
automaticallyAdjustContentInsets 和 contentInsets (提交)
当您为 WKWebView 添加 contentInsets 时,它不会改变 WKWebView 的视口。视口的大小保持与帧相同。而对于 UIWebView,视口大小实际上会改变(如果内容内边距为正,则会变小)。
backgroundColor (提交)
使用 WebView 的新 iOS 实现,如果您使用此属性,您的背景颜色可能会闪烁显示。此外,WKWebView 渲染透明背景的方式与 UIWebview 不同。请查看提交说明了解更多详情。
不支持
scalesPageToFit (提交)
WKWebView 不支持 `scalesPageToFit` 属性,因此我们无法在 WebView React Native 组件中实现此功能。
随着技术的进步和移动应用在日常生活中变得越来越重要,创建无障碍应用的需求也同样变得越来越重要。
React Native有限的无障碍API一直是开发人员的一大痛点,因此我们对无障碍API进行了一些更新,使其更容易创建包容性移动应用。
accessibilityComponentType 和 accessibilityTraits 是两个属性,用于告诉 Android 上的 TalkBack 和 iOS 上的 VoiceOver 用户正在与哪种 UI 元素进行交互。这两个属性最大的问题是:
accessibilityTraits 允许 17 个不同的值,而 Android 上的 accessibilityComponentType 只允许 4 个值。此外,这些值在大多数情况下没有重叠。甚至这两个属性的输入类型也不同。accessibilityTraits 允许传入特性数组或单个特性,而 accessibilityComponentType 只允许单个值。无障碍提示可帮助使用 TalkBack 或 VoiceOver 的用户了解当他们对无障碍元素执行操作时会发生什么,而这些信息仅凭无障碍标签是无法得知的。这些提示可以在设置面板中开启和关闭。以前,React Native 的 API 完全不支持无障碍提示。
一些视力受损的用户会在手机上使用反色以获得更高的屏幕对比度。Apple 为 iOS 提供了一个 API,允许开发人员忽略某些视图。这样,当用户开启反色设置时,图像和视频就不会失真。React Native 目前不支持此 API。
为了解决 accessibilityComponentType 和 accessibilityTraits 之间的混淆,我们决定将它们合并为一个属性。这样做是有道理的,因为它们在技术上具有相同的预期功能,通过合并它们,开发人员在构建无障碍功能时不再需要担心平台特定的复杂性。
背景
在 iOS 上,UIAccessibilityTraits 是一个可以设置在任何 NSObject 上的属性。通过 JavaScript 属性传递到原生代码的 17 个特性中的每一个都映射到 Objective-C 中的 UIAccessibilityTraits 元素。特性由长整型表示,所有设置的特性都通过按位或运算组合在一起。
然而,在 Android 上,AccessibilityComponentType 是 React Native 创造的一个概念,并没有直接映射到 Android 中的任何属性。无障碍功能由无障碍委托处理。每个视图都有一个默认的无障碍委托。如果你想自定义任何无障碍操作,你必须创建一个新的无障碍委托,覆盖你想要自定义的特定方法,然后将被处理的视图的无障碍委托设置为与新的委托关联。当开发人员设置 AccessibilityComponentType 时,原生代码会根据传入的组件创建一个新的委托,并将视图设置为具有该无障碍委托。
所做的更改
对于我们的新属性,我们希望创建这两个属性的超集。我们决定主要以现有属性 accessibilityTraits 为蓝本设计新属性,因为 accessibilityTraits 的值明显更多。Android 对这些特性的功能将通过修改无障碍委托进行兼容。
iOS 上的 accessibilityTraits 可以设置为 17 个 UIAccessibilityTraits 值。然而,我们并未将所有这些值都包含在新属性的可能值中。这是因为设置其中一些特性的效果实际上并不为人所知,而且其中许多值几乎从未使用过。
UIAccessibilityTraits 的值通常具有两种目的之一。它们要么描述 UI 元素所扮演的角色,要么描述 UI 元素所处的状态。我们观察到的以前属性的大多数用法通常使用一个表示角色的值,并将其与“已选状态”、“已禁用状态”或两者结合使用。因此,我们决定创建两个新的无障碍属性:accessibilityRole 和 accessibilityState。
accessibilityRole
新属性accessibilityRole用于告诉Talkback或Voiceover UI元素的角色。此新属性可以取以下值之一
无按钮链接搜索图片键盘键文本可调整标题总结图片按钮此属性只允许传入一个值,因为UI元素通常逻辑上不会承担其中多个角色。例外是图片和按钮,因此我们添加了一个组合两者的角色“图片按钮”。
辅助功能状态
新属性accessibilityStates用于告诉Talkback或Voiceover UI元素的状态。此属性接受一个包含以下一个或两个值的数组
selecteddisabled为此,我们添加了一个新属性:accessibilityHint。设置此属性将允许Talkback或Voiceover向用户朗读提示。
accessibilityHint
此属性以字符串形式接收要读取的辅助功能提示。
在 iOS 上,设置此属性将设置视图上相应的原生属性 AccessibilityHint。如果 iPhone 中开启了辅助功能提示,则 Voiceover 将朗读该提示。
在 Android 上,设置此属性会将提示的值附加到无障碍标签的末尾。此实现的好处是它模仿了 iOS 上提示的行为,但缺点是这些提示不能像 iOS 上那样在 Android 的设置中关闭。
我们在Android上做出这个决定的原因是,通常情况下,辅助功能提示与特定动作(例如,点击)相关联,我们希望在不同平台之间保持行为一致。
accessibilityIgnoresInvertColors
我们已将 Apple 的 API `AccessibilityIgnoresInvertColors` 暴露给 JavaScript,因此现在当您有一个不想反转颜色的视图(例如图片)时,您可以将此属性设置为 true,它就不会被反转。
这些新属性将在React Native 0.57版本中可用。
如果您当前正在使用accessibilityComponentType和accessibilityTraits,这里是您可以采取的步骤来升级到新属性。
最简单的用例可以通过运行jscodeshift脚本来替换。
这个脚本替换了以下实例
accessibilityTraits=“trait”
accessibilityTraits={[“trait”]}
带有
accessibilityRole= “trait”
此脚本还删除了AccessibilityComponentType的实例(假设您在所有设置AccessibilityComponentType的地方都同时设置了AccessibilityTraits)。
对于使用了 AccessibilityTraits 但 AccessibilityRole 没有相应值的情况,以及将多个特性传递给 AccessibilityTraits 的情况,需要进行手动代码转换。
通常,
accessibilityTraits= {[“button”, “selected”]}
将被手动替换为
accessibilityRole=“button”
accessibilityStates={[“selected”]}
这些属性已经在 Facebook 的代码库中使用了。Facebook 的代码转换出奇地简单。jscodeshift 脚本修复了大约一半的实例,另一半是手动修复的。总的来说,整个过程不到几个小时。
希望您会发现更新后的API很有用!并请继续让应用程序无障碍!#包容
距离我们上次发布 React Native 的状态更新已经有一段时间了。
在 Facebook,我们比以往任何时候都更多地使用 React Native,并且将其用于许多重要项目。我们最受欢迎的产品之一是 Marketplace,它是我们应用程序中顶级标签之一,每月有 8 亿人使用。自 2015 年创建以来,Marketplace 的所有内容都使用 React Native 构建,包括应用程序不同部分的数百个全屏视图。
我们还将 React Native 用于应用程序的许多新部分。如果您观看了上个月的 F8 主题演讲,您会认出献血、危机响应、隐私快捷方式和健康检查——所有这些都是使用 React Native 构建的最新功能。主 Facebook 应用程序之外的项目也在使用 React Native。新的 Oculus Go VR 头戴设备包括一个完全使用 React Native 构建的配套移动应用程序,更不用说 React VR 为头戴设备本身提供了许多体验。
当然,我们还使用许多其他技术来构建我们的应用程序。Litho 和 ComponentKit 是我们在应用程序中广泛使用的两个库;两者都提供类似于 React 的组件 API 来构建原生屏幕。React Native 的目标从来都不是取代所有其他技术——我们专注于使 React Native 本身更好,但我们乐于看到其他团队借鉴 React Native 的思想,例如将即时重载带到非 JavaScript 代码中。
当我们在 2013 年启动 React Native 项目时,我们将其设计为在 JavaScript 和原生之间有一个单一的“桥接”,它是异步的、可序列化的和批处理的。正如 React DOM 将 React 状态更新转换为对 DOM API 的命令式、可变调用,例如 document.createElement(attrs) 和 .appendChild(),React Native 被设计为返回一个单一的 JSON 消息,其中列出了要执行的突变,例如 [["createView", attrs], ["manageChildren", ...]]。我们设计整个系统是为了不依赖于同步响应,并确保该列表中的所有内容都可以完全序列化为 JSON 并返回。我们这样做是因为它提供了灵活性:在此架构之上,我们能够构建诸如Chrome 调试之类的工具,它通过 WebSocket 连接异步运行所有 JavaScript 代码。
在过去的 5 年里,我们发现这些最初的原则使得构建某些功能变得更加困难。异步桥接意味着您无法将 JavaScript 逻辑直接与许多期望同步响应的原生 API 集成。批处理桥接会队列化原生调用,这意味着 React Native 应用程序更难调用原生实现的功能。可序列化桥接意味着不必要的复制,而不是直接在两个世界之间共享内存。对于完全用 React Native 构建的应用程序,这些限制通常是可以忍受的。但对于 React Native 和现有应用程序代码之间复杂集成的应用程序,它们令人沮丧。
我们正在对 React Native 进行大规模的重新架构,以使框架更灵活,并在混合 JavaScript/原生应用程序中更好地与原生基础设施集成。通过这个项目,我们将应用我们在过去 5 年中学到的知识,并逐步将我们的架构带入更现代的境界。我们正在重写 React Native 的许多内部组件,但大部分更改都在幕后:现有的 React Native 应用程序将继续运行,几乎不需要或根本不需要更改。
为了让 React Native 更轻量化,更好地融入现有的原生应用,这次重构有三个主要的内部变化。首先,我们正在改变线程模型。以前每次 UI 更新都需要在三个不同的线程上执行工作,现在可以通过在任何线程上同步调用 JavaScript 来进行高优先级更新,同时将低优先级工作从主线程中分离,以保持响应性。其次,我们正在将 异步渲染 能力整合到 React Native 中,以允许多种渲染优先级并简化异步数据处理。最后,我们正在简化我们的桥接,使其更快、更轻量;原生和 JavaScript 之间的直接调用更高效,并且将更容易构建调试工具,例如跨语言堆栈跟踪。
一旦这些更改完成,更紧密的集成将成为可能。如今,如果没有复杂的 hack,就不可能集成原生导航和手势处理,或者像 UICollectionView 和 RecyclerView 这样的原生组件。在我们对线程模型进行更改之后,构建这样的功能将变得简单直接。
我们将在今年晚些时候,待其接近完成时,发布更多关于这项工作的细节。
除了 Facebook 内部的社区,我们也很高兴在 Facebook 之外拥有蓬勃发展的 React Native 用户和合作者群体。我们希望通过更好地服务 React Native 用户并使项目更容易贡献,来更多地支持 React Native 社区。
正如我们的架构更改将帮助 React Native 更清晰地与其他原生基础设施互操作一样,React Native 在 JavaScript 方面也应该更精简,以便更好地适应 JavaScript 生态系统,其中包括使 VM 和打包器可互换。我们知道破坏性更改的速度可能难以跟上,因此我们希望找到减少主要版本发布的方法。最后,我们知道有些团队正在寻求更详尽的文档,例如启动优化等主题,而我们的专业知识尚未被记录下来。预计在未来一年内会看到其中一些变化。
如果您正在使用 React Native,您就是我们社区的一员;请继续告诉我们如何才能让 React Native 为您做得更好。
React Native只是移动开发人员工具箱中的一个工具,但我们坚信它——而且我们每天都在让它变得更好,去年有500多名贡献者提交了2500多次提交。
JavaScript!我们都喜欢它。但我们中的一些人也喜欢类型。幸运的是,有多种选择可以为 JavaScript 添加更强的类型。我最喜欢的是TypeScript,但 React Native 也开箱即用地支持Flow。选择哪一个取决于个人偏好,它们都有各自的方法来为 JavaScript 添加类型的魔力。今天,我们将探讨如何在 React Native 应用中使用 TypeScript。
这篇文章以微软的TypeScript-React-Native-Starter仓库作为指南。
更新:自此博客文章撰写以来,事情变得更加容易。您可以通过运行一个命令来替换本文中描述的所有设置
npx react-native init MyAwesomeProject --template react-native-template-typescript
然而,Babel 对 TypeScript 的支持确实存在一些限制,上述博客文章对此进行了详细阐述。本篇文章中概述的步骤仍然有效,并且 Artsy 仍在生产环境中使用react-native-typescript-transformer,但使用上述命令是开始使用 React Native 和 TypeScript 的最快方式。如果需要,您随时可以稍后切换。
无论如何,玩得开心!原始博客文章继续如下。
由于您可能在不同的平台开发,并且针对不同类型的设备,因此基本设置可能比较复杂。您应该首先确保可以在不使用 TypeScript 的情况下运行一个普通的 React Native 应用。请按照React Native 网站上的说明开始。当您成功部署到设备或模拟器后,就可以开始 TypeScript React Native 应用了。
一旦您尝试搭建一个普通的React Native项目,您就可以开始添加TypeScript了。让我们继续进行。
react-native init MyAwesomeProject
cd MyAwesomeProject
下一步是将 TypeScript 添加到您的项目中。以下命令将:
好的,让我们继续运行这些命令。
yarn add --dev typescript
yarn add --dev react-native-typescript-transformer
yarn tsc --init --pretty --jsx react
touch rn-cli.config.js
yarn add --dev @types/react @types/react-native
tsconfig.json 文件包含 TypeScript 编译器的所有设置。上面命令创建的默认设置大部分都很好,但请打开文件并取消注释以下行
{
/* Search the config file for the following line and uncomment it. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
}
rn-cli.config.js 包含 React Native TypeScript Transformer 的设置。打开它并添加以下内容
module.exports = {
getTransformModulePath() {
return require.resolve('react-native-typescript-transformer');
},
getSourceExts() {
return ['ts', 'tsx'];
},
};
将生成的 App.js 和 __tests_/App.js 文件重命名为 App.tsx。index.js 需要使用 .js 扩展名。所有新文件都应使用 .tsx 扩展名(如果文件不包含任何 JSX,则使用 .ts)。
如果您现在尝试运行该应用,将会收到类似 object prototype may only be an object or null 的错误。这是由于未能同时导入 React 的默认导出和同一行中的命名导出所致。打开 App.tsx 并修改文件顶部的导入语句
-import React, { Component } from 'react';
+import React from 'react'
+import { Component } from 'react';
其中一些与 Babel 和 TypeScript 如何与 CommonJS 模块互操作的差异有关。将来,两者将在相同的行为上稳定下来。
此时,您应该能够运行React Native应用程序。
React Native 附带了Jest,因此为了使用 TypeScript 测试 React Native 应用,我们需要将ts-jest 添加到我们的 devDependencies 中。
yarn add --dev ts-jest
然后,我们将打开 package.json 并将 jest 字段替换为以下内容
{
"jest": {
"preset": "react-native",
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"transform": {
"^.+\\.(js)$": "<rootDir>/node_modules/babel-jest",
"\\.(ts|tsx)$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"testPathIgnorePatterns": [
"\\.snap$",
"<rootDir>/node_modules/"
],
"cacheDirectory": ".jest/cache"
}
}
这将配置 Jest 以使用 `ts-jest` 运行 `.ts` 和 `.tsx` 文件。
为了在 TypeScript 中获得最佳体验,我们希望类型检查器能够理解我们依赖项的形状和 API。一些库会发布带有 .d.ts 文件(类型声明/类型定义文件)的包,这些文件可以描述底层 JavaScript 的形状。对于其他库,我们需要在 @types/ npm 作用域中显式安装相应的包。
例如,这里我们需要 Jest、React、React Native 和 React Test Renderer 的类型。
yarn add --dev @types/jest @types/react @types/react-native @types/react-test-renderer
我们将这些声明文件包保存到我们的 开发依赖项中,因为这是一个 React Native 应用,它只在开发过程中而不是运行时使用这些依赖项。如果我们要将一个库发布到 NPM,我们可能需要将其中一些类型依赖项添加为常规依赖项。
您可以在此处阅读更多关于获取.d.ts文件的信息。
为了您的源代码控制,您将希望开始忽略 .jest 文件夹。如果您正在使用 git,我们可以直接将条目添加到我们的 .gitignore 文件中。
# Jest
#
.jest/
作为一个检查点,考虑将您的文件提交到版本控制。
git init
git add .gitignore # import to do this first, to ignore our files
git add .
git commit -am "Initial commit."
让我们为我们的应用程序添加一个组件。我们来创建一个 `Hello.tsx` 组件。这是一个教学组件,不是您在实际应用程序中会编写的东西,而是一些非平凡的例子,展示了如何在 React Native 中使用 TypeScript。
创建一个 components 目录并添加以下示例。
// components/Hello.tsx
import React from 'react';
import {Button, StyleSheet, Text, View} from 'react-native';
export interface Props {
name: string;
enthusiasmLevel?: number;
}
interface State {
enthusiasmLevel: number;
}
export class Hello extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
if ((props.enthusiasmLevel || 0) <= 0) {
throw new Error(
'You could be a little more enthusiastic. :D',
);
}
this.state = {
enthusiasmLevel: props.enthusiasmLevel || 1,
};
}
onIncrement = () =>
this.setState({
enthusiasmLevel: this.state.enthusiasmLevel + 1,
});
onDecrement = () =>
this.setState({
enthusiasmLevel: this.state.enthusiasmLevel - 1,
});
getExclamationMarks = (numChars: number) =>
Array(numChars + 1).join('!');
render() {
return (
<View style={styles.root}>
<Text style={styles.greeting}>
Hello{' '}
{this.props.name +
this.getExclamationMarks(this.state.enthusiasmLevel)}
</Text>
<View style={styles.buttons}>
<View style={styles.button}>
<Button
title="-"
onPress={this.onDecrement}
accessibilityLabel="decrement"
color="red"
/>
</View>
<View style={styles.button}>
<Button
title="+"
onPress={this.onIncrement}
accessibilityLabel="increment"
color="blue"
/>
</View>
</View>
</View>
);
}
}
// styles
const styles = StyleSheet.create({
root: {
alignItems: 'center',
alignSelf: 'center',
},
buttons: {
flexDirection: 'row',
minHeight: 70,
alignItems: 'stretch',
alignSelf: 'center',
borderWidth: 5,
},
button: {
flex: 1,
paddingVertical: 0,
},
greeting: {
color: '#999',
fontWeight: 'bold',
},
});
哇!这很多,但让我们分解一下:
StyleSheet.create函数指定的。React的样式表允许我们使用Flexbox控制布局,并使用类似于CSS的其他结构来设置样式。现在我们有了一个组件,让我们尝试测试它。
我们已经安装了 Jest 作为测试运行器。我们将为我们的组件编写快照测试,让我们添加快照测试所需的附加组件
yarn add --dev react-addons-test-utils
现在,让我们在 components 目录中创建一个 __tests__ 文件夹,并为 Hello.tsx 添加一个测试
// components/__tests__/Hello.tsx
import React from 'react';
import renderer from 'react-test-renderer';
import {Hello} from '../Hello';
it('renders correctly with defaults', () => {
const button = renderer
.create(<Hello name="World" enthusiasmLevel={1} />)
.toJSON();
expect(button).toMatchSnapshot();
});
第一次运行测试时,它会创建渲染组件的快照并将其存储在 components/__tests__/__snapshots__/Hello.tsx.snap 文件中。当您修改组件时,需要更新快照并检查更新以查找意外更改。您可以在此处阅读有关测试 React Native 组件的更多信息。
请查阅官方的React 教程和状态管理库Redux。这些资源在编写 React Native 应用时会很有帮助。此外,您可能还会想了解ReactXP,这是一个完全用 TypeScript 编写的组件库,支持 Web 上的 React 和 React Native。
祝您在更类型安全的React Native开发环境中玩得开心!
三年前,GitHub 上有一个 issue 被提出,要求 React Native 支持输入辅助视图。
在接下来的几年里,出现了无数的“+1”、各种变通方法,但在这个问题上 RN 却没有具体改变——直到今天。从 iOS 开始,我们正在公开一个 API 用于访问原生输入附件视图,我们很高兴能分享我们是如何构建它的。
输入附件视图到底是什么?阅读 Apple 的开发者文档,我们了解到它是一个自定义视图,当接收者成为第一响应者时,它可以固定在系统键盘的顶部。任何继承自 UIResponder 的东西都可以将 .inputAccessoryView 属性重新声明为读写,并在此处管理自定义视图。响应者基础设施会挂载视图,并使其与系统键盘保持同步。解散键盘的手势(如拖动或轻点)会在框架级别应用于输入附件视图。这使我们能够构建具有交互式键盘解散功能的内容,这是 iMessage 和 WhatsApp 等顶级消息应用程序不可或缺的功能。
将视图锚定到键盘顶部有两种常见用例。第一种是创建键盘工具栏,例如Facebook撰写器背景选择器。
在这种情况下,键盘聚焦在文本输入字段上,输入附件视图用于提供额外的键盘功能。此功能与输入字段的类型相关联。在地图应用程序中,它可能是地址建议,或者在文本编辑器中,它可能是富文本格式工具。
在这种情况下,拥有 <InputAccessoryView> 的 Objective-C UIResponder 应该很清楚。<TextInput> 已成为第一响应者,在底层,它变成了 UITextView 或 UITextField 的实例。
第二种常见情况是固定文本输入
这里,文本输入实际上是输入辅助视图本身的一部分。这通常用于消息应用程序中,用户可以在滚动浏览之前的消息线程时撰写消息。
在这个例子中,谁拥有 <InputAccessoryView>?它还能是 UITextView 或 UITextField 吗?文本输入在输入附件视图_里面_,这听起来像一个循环依赖。单独解决这个问题本身就是另一篇博文。剧透:所有者是一个通用的 UIView 子类,我们手动告知它成为第一响应者。
我们现在知道`<InputAccessoryView>`是什么,以及我们想如何使用它。下一步是设计一个对两种用例都适用,并且与现有React Native组件(如`<TextInput>`)良好协作的API。
对于键盘工具栏,有几件事需要考虑:
<InputAccessoryView> 中。<InputAccessoryView>链接到特定的<TextInput>。<InputAccessoryView>,而无需复制任何代码。我们可以通过类似于 React portals 的概念来实现 #1。在此设计中,我们将 React Native 视图传送到由响应者基础设施管理的 UIView 层次结构中。由于 React Native 视图渲染为 UIViews,这实际上非常简单——我们只需覆盖
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
并将所有子视图管道到一个新的 UIView 层次结构。对于 #2,我们为 <InputAccessoryView> 设置了一个新的 RCTTouchHandler。状态更新通过使用常规事件回调来实现。对于 #3 和 #4,我们使用 nativeID 字段在创建 <TextInput> 组件期间在原生代码中定位附件视图 UIView 层次结构。此函数使用底层原生文本输入的 .inputAccessoryView 属性。这样做有效地将 <InputAccessoryView> 与 <TextInput> 链接到它们的 ObjC 实现中。
支持粘性文本输入(场景2)增加了一些额外的限制。对于这种设计,输入附件视图有一个文本输入作为子视图,因此通过 nativeID 链接不是一个选项。相反,我们将通用屏幕外 UIView 的 .inputAccessoryView 设置为我们的原生 <InputAccessoryView> 层次结构。通过手动告知这个通用 UIView 成为第一响应者,层次结构由响应者基础设施挂载。这个概念在前面提到的博客文章中得到了详细解释。
当然,在构建这个API时并非一帆风顺。以下是我们遇到的一些陷阱,以及我们如何修复它们。
构建此 API 的一个初步想法是监听 NSNotificationCenter 的 UIKeyboardWill(Show/Hide/ChangeFrame) 事件。这种模式在一些开源库中以及 Facebook 应用程序的某些部分内部使用。不幸的是,UIKeyboardDidChangeFrame 事件没有及时调用来更新滑动时的 <InputAccessoryView> 帧。此外,键盘高度的变化也未被这些事件捕获。这导致了一类如下所示的错误:
在 iPhone X 上,文本和表情符号键盘的高度不同。大多数使用键盘事件来操纵文本输入帧的应用程序都必须修复上述错误。我们的解决方案是致力于使用 .inputAccessoryView 属性,这意味着响应者基础设施会处理此类帧更新。
我们遇到的另一个棘手的错误是避免 iPhone X 上的 Home 键区域。您可能会想,“Apple 开发了 safeAreaLayoutGuide 正是为了这个原因,这很简单!”我们和您一样天真。第一个问题是,原生 <InputAccessoryView> 实现直到即将出现的那一刻才会有窗口可以锚定。没关系,我们可以在那里重写 -(BOOL)becomeFirstResponder 并强制执行布局约束。遵守这些约束会将附件视图向上移动,但又出现了一个错误:
输入附件视图成功避开了 Home 键区域,但现在不安全区域后面的内容可见。解决方案在于此雷达。我将原生 <InputAccessoryView> 层次结构包装在一个不符合 safeAreaLayoutGuide 约束的容器中。原生容器覆盖不安全区域中的内容,而 <InputAccessoryView> 保持在安全区域边界内。
这里有一个示例,它构建了一个键盘工具栏按钮来重置<TextInput>状态。
class TextInputAccessoryViewExample extends React.Component<
{},
*,
> {
constructor(props) {
super(props);
this.state = {text: 'Placeholder Text'};
}
render() {
const inputAccessoryViewID = 'inputAccessoryView1';
return (
<View>
<TextInput
style={styles.default}
inputAccessoryViewID={inputAccessoryViewID}
onChangeText={text => this.setState({text})}
value={this.state.text}
/>
<InputAccessoryView nativeID={inputAccessoryViewID}>
<View style={{backgroundColor: 'white'}}>
<Button
onPress={() =>
this.setState({text: 'Placeholder Text'})
}
title="Reset Text"
/>
</View>
</InputAccessoryView>
</View>
);
}
}
固定文本输入的另一个例子可以在仓库中找到。
此功能实现的完整提交在此。<InputAccessoryView> 将在即将发布的 v0.55.0 版本中提供。
快乐打字 :)
AWS 在技术行业中以云服务提供商而闻名。这些服务包括计算、存储和数据库技术,以及完全托管的无服务器产品。AWS 移动团队一直与客户和 JavaScript 生态系统的成员密切合作,使云连接的移动和 Web 应用程序更加安全、可扩展,并且更易于开发和部署。我们最初提供了一个完整的入门套件,但最近又有了更多发展。
这篇博客文章将探讨一些对 React 和 React Native 开发者来说有趣的内容:
使用 Create React Native App 和 Expo 等工具可以非常轻松地引导 React Native 应用程序。然而,当你尝试将用例与基础设施服务进行匹配时,将其连接到云可能具有挑战性。例如,你的 React Native 应用程序可能需要上传照片。这些照片是否应该按用户进行保护?这可能意味着你需要某种注册或登录过程。你是想要自己的用户目录还是使用社交媒体提供商?也许你的应用程序还需要在用户登录后调用具有自定义业务逻辑的 API。
为了帮助 JavaScript 开发人员解决这些问题,我们发布了一个名为 AWS Amplify 的库。其设计分解为任务的“类别”,而不是 AWS 特定的实现。例如,如果你希望用户注册、登录,然后上传私人照片,你只需将 Auth 和 Storage 类别引入你的应用程序。
import { Auth } from 'aws-amplify';
Auth.signIn(username, password)
.then(user => console.log(user))
.catch(err => console.log(err));
Auth.confirmSignIn(user, code)
.then(data => console.log(data))
.catch(err => console.log(err));
在上面的代码中,您可以看到 Amplify 帮助您完成的一些常见任务示例,例如使用带有电子邮件或短信的多因素身份验证 (MFA) 代码。目前支持的类别有:
Amplify 的优点之一是它在设计中为你的特定编程环境编码了“最佳实践”。例如,我们发现与客户和 React Native 开发人员合作时,开发过程中为了快速实现功能而采取的捷径会延续到生产堆栈中。这可能会损害可扩展性或安全性,并强制进行基础设施重构和代码重构。
我们如何帮助开发人员避免这种情况的一个例子是 使用 AWS Lambda 的无服务器参考架构。这些架构展示了在构建后端时如何一起使用 Amazon API Gateway 和 AWS Lambda 的最佳实践。这种模式被编码到 Amplify 的 API 类别中。你可以使用这种模式与多个不同的 REST 端点进行交互,并将标头一直传递到你的 Lambda 函数,以实现自定义业务逻辑。我们还发布了一个 AWS Mobile CLI,用于使用这些功能引导新的或现有的 React Native 项目。要开始使用,只需通过 npm 安装,并按照配置提示进行操作。
npm install --global awsmobile-cli
awsmobile configure
移动生态系统中编码最佳实践的另一个具体例子是密码安全。默认的 Auth 类别实现利用 Amazon Cognito 用户池进行用户注册和登录。该服务实现了安全远程密码协议,作为在身份验证尝试期间保护用户的一种方式。如果你倾向于阅读该协议的数学原理,你会注意到在原始根上计算密码验证器以生成组时,你必须使用一个大素数。在 React Native 环境中,JIT 已禁用。这使得用于此类安全操作的 BigInteger 计算性能较低。为了解决这个问题,我们发布了可以在项目中链接的 Android 和 iOS 本机桥接。
npm install --save aws-amplify-react-native
react-native link amazon-cognito-identity-js
我们也很高兴地看到 Expo 团队已将此功能纳入其最新 SDK,因此您无需弹出即可使用 Amplify。
最后,特别是针对 React Native(和 React)开发,Amplify 包含高阶组件 (HOC),用于轻松封装功能,例如在应用程序中进行注册和登录。
import Amplify, { withAuthenticator } from 'aws-amplify-react-native';
import aws_exports from './aws-exports';
Amplify.configure(aws_exports);
class App extends React.Component {
...
}
export default withAuthenticator(App);
底层组件也作为 <Authenticator /> 提供,它使你能够完全控制自定义 UI。它还为你提供了一些关于管理用户状态的属性,例如他们是否已登录或正在等待 MFA 确认,以及在状态更改时可以触发的回调。
同样,您会找到可用于不同用例的通用 React 组件。您可以根据需要自定义这些组件,例如,在 `Storage` 模块中显示 Amazon S3 中的所有私有图像。
<S3Album
level="private"
path={path}
filter={(item) => /jpg/i.test(item.path)}/>
您可以通过 props 控制许多组件功能,如前面所示,包括公共或私人存储选项。甚至还具有在用户与某些 UI 组件交互时自动收集分析的功能。
return <S3Album track/>
AWS Amplify 倾向于使用约定优于配置的开发风格,具有全局初始化例程或在类别级别进行初始化。最快的入门方式是使用 aws-exports 文件。但是,开发人员也可以使用该库独立使用现有资源。
欲深入了解其理念并观看完整演示,请查看 AWS re:Invent 的视频。
在 AWS Amplify 发布后不久,我们还发布了 AWS AppSync。这是一个完全托管的 GraphQL 服务,具有离线和实时功能。虽然你可以在不同的客户端编程语言(包括原生 Android 和 iOS)中使用 GraphQL,但它在 React Native 开发人员中非常受欢迎。这是因为数据模型很好地契合了单向数据流和组件层次结构。
AWS AppSync 使你能够连接到你自己的 AWS 账户中的资源,这意味着你拥有并控制你的数据。这是通过使用数据源完成的,该服务支持 Amazon DynamoDB、Amazon Elasticsearch 和 AWS Lambda。这使你可以在单个 GraphQL API 中将功能(例如 NoSQL 和全文搜索)组合为一个模式。这使你能够混合和匹配数据源。AppSync 服务还可以 从模式进行配置,因此如果你不熟悉 AWS 服务,你可以编写 GraphQL SDL,点击一个按钮,然后你就可以自动启动并运行。
AWS AppSync 中的实时功能通过带有众所周知的事件驱动模式的 GraphQL 订阅进行控制。由于 AWS AppSync 中的订阅通过 GraphQL 指令在模式上进行控制,并且模式可以使用任何数据源,这意味着你可以从使用 Amazon DynamoDB 和 Amazon Elasticsearch Service 的数据库操作,或者从你的基础设施的其他部分(使用 AWS Lambda)触发通知。
与 AWS Amplify 类似,你可以在你的 GraphQL API 上使用 AWS AppSync 的企业安全功能。该服务允许你使用 API 密钥快速入门。但是,当你投入生产时,它可以过渡到使用 AWS Identity and Access Management (IAM) 或来自 Amazon Cognito 用户池的 OIDC 令牌。你可以通过类型上的策略在解析器级别控制访问。你甚至可以使用逻辑检查在运行时进行细粒度访问控制检查,例如检测用户是否是特定数据库资源的所有者。还有一些功能可以检查组是否可以执行解析器或访问单个数据库记录。
为了帮助 React Native 开发人员了解更多关于这些技术的信息,有一个内置的 GraphQL 示例模式,你可以在 AWS AppSync 控制台主页上启动它。这个示例部署一个 GraphQL 模式,配置数据库表,并为你自动连接查询、变异和订阅。还有一个可用的AWS AppSync 的 React Native 示例,它利用了这个内置模式(以及一个React 示例),它们使你可以在几分钟内启动并运行你的客户端和云组件。
当你使用 AWSAppSyncClient 时,入门非常简单,它与 Apollo Client 插件集成。AWSAppSyncClient 处理你的 GraphQL API 的安全和签名、离线功能以及订阅握手和协商过程。
import AWSAppSyncClient from "aws-appsync";
import { Rehydrated } from 'aws-appsync-react';
import { AUTH_TYPE } from "aws-appsync/lib/link/auth-link";
const client = new AWSAppSyncClient({
url: awsconfig.graphqlEndpoint,
region: awsconfig.region,
auth: {type: AUTH_TYPE.API_KEY, apiKey: awsconfig.apiKey}
});
AppSync 控制台提供了一个可供下载的配置文件,其中包含你的 GraphQL 端点、AWS 区域和 API 密钥。然后你可以使用 React Apollo 的客户端。
const WithProvider = () => (
<ApolloProvider client={client}>
<Rehydrated>
<App />
</Rehydrated>
</ApolloProvider>
);
此时,您可以使用标准的 GraphQL 查询。
query ListEvents {
listEvents{
items{
__typename
id
name
where
when
description
comments{
__typename
items{
__typename
eventId
commentId
content
createdAt
}
nextToken
}
}
}
}
上面的例子展示了一个使用 AppSync 提供的示例应用模式的查询。它不仅展示了与 DynamoDB 的交互,还包括数据分页(包括加密令牌)以及 Events 和 Comments 之间的类型关系。由于应用程序配置了 AWSAppSyncClient,数据会自动离线持久化,并在设备重新连接时进行同步。
您可以在此视频中深入了解其背后的客户端技术和 React Native 演示。
这些库背后的团队渴望了解这些库和服务对你的作用。他们还想听听我们还能做些什么,让你使用云服务进行 React 和 React Native 开发变得更容易。请在 GitHub 上联系 AWS Mobile 团队,了解 AWS Amplify 或 AWS AppSync。
Twitter的iOS应用有一个我非常喜欢的加载动画。
应用程序准备好后,Twitter 标志会令人愉悦地展开,露出应用程序。
我想弄清楚如何用 React Native 重现这个加载动画。
为了理解**如何**构建它,我首先必须理解加载动画的不同部分。最容易看出细微之处的方法是放慢速度。
这里有几个主要部分需要我们弄清楚如何构建。
我花了很长时间才弄清楚如何制作这个动画。
我最初有一个错误的假设,认为蓝色背景和 Twitter 小鸟是应用之上的一层,并且随着小鸟的变大,它会变得透明,从而显示下面的应用。这种方法行不通,因为 Twitter 小鸟变得透明会显示蓝色层,而不是下面的应用!
幸运的是,亲爱的读者,您不必经历我同样的沮丧。您将获得这个直接进入精彩部分的教程!
在开始编写代码之前,了解如何分解它非常重要。为了帮助可视化这种效果,我在 CodePen 中重新创建了它(嵌入在几个段落中),这样你就可以交互式地看到不同的图层。
这种效果主要有三层。第一层是蓝色背景层。尽管这看起来出现在应用程序的顶部,但它实际上在后面。
然后我们有一个纯白层。最后,在最前面,是我们的应用程序。
这个动画的主要技巧是使用 Twitter 标志作为遮罩,并遮罩应用和白色图层。我不会深入探讨遮罩的细节,网上有大量的资源可供参考。
在此背景下,遮罩的基本原理是:遮罩的不透明像素显示其遮罩的内容,而透明像素则隐藏其遮罩的内容。
我们使用 Twitter 标志作为遮罩,并让它遮盖两个图层:纯白图层和应用程序图层。
为了显示应用程序,我们将遮罩放大,直到它大于整个屏幕。
当遮罩放大时,我们逐渐增加应用图层的不透明度,显示应用并隐藏其后面的纯白色图层。为了完成效果,我们将应用图层从大于 1 的比例开始,并在动画结束时将其缩小到 1。然后我们隐藏非应用图层,因为它们永远不会再被看到。
人们常说一张图片胜过千言万语。那么,一个交互式可视化价值多少字呢?使用“下一步”按钮点击动画。显示图层可以为您提供侧视图。网格有助于可视化透明图层。
好的。既然我们已经知道了要构建什么以及动画如何工作,那么我们就可以开始编写代码了——这也是您真正来到这里的原因。
这个谜题的主要部分是MaskedViewIOS,一个核心的React Native组件。
import {MaskedViewIOS} from 'react-native';
<MaskedViewIOS maskElement={<Text>Basic Mask</Text>}>
<View style={{backgroundColor: 'blue'}} />
</MaskedViewIOS>;
MaskedViewIOS 接收 maskElement 和 children 属性。子元素由 maskElement 遮罩。请注意,遮罩不需要是图像,它可以是任何任意视图。上述示例的行为是渲染蓝色视图,但只有在 maskElement 中的“Basic Mask”文字处才能看到它。我们只是制作了复杂的蓝色文本。
我们想要做的是渲染我们的蓝色层,然后在顶部渲染我们的带有Twitter标志的遮罩应用程序和白色层。
{
fullScreenBlueLayer;
}
<MaskedViewIOS
style={{flex: 1}}
maskElement={
<View style={styles.centeredFullScreen}>
<Image source={twitterLogo} />
</View>
}>
{fullScreenWhiteLayer}
<View style={{flex: 1}}>
<MyApp />
</View>
</MaskedViewIOS>;
这将为我们提供下面看到的图层。
我们拥有了使这能正常工作所需的所有部分,下一步是为它们制作动画。为了让这个动画感觉良好,我们将利用 React Native 的 Animated API。
Animated 允许我们以声明式的方式在 JavaScript 中定义动画。默认情况下,这些动画在 JavaScript 中运行,并在每一帧告诉原生层需要进行哪些更改。尽管 JavaScript 会尝试在每一帧更新动画,但它可能无法足够快地完成,从而导致掉帧(卡顿)。这不是我们想要的!
Animated 具有特殊行为,允许您获得无卡顿的动画。Animated 有一个名为 useNativeDriver 的标志,它在动画开始时将您的动画定义从 JavaScript 发送到原生,从而允许原生端处理您的动画更新,而无需在每一帧都往返于 JavaScript。useNativeDriver 的缺点是您只能更新特定的属性集,主要是 transform 和 opacity。您不能使用 useNativeDriver 动画背景颜色之类的东西,至少目前还不能——我们将来会添加更多,当然您也可以随时提交 PR 来支持您项目所需的属性,从而造福整个社区 😀。
由于我们希望这个动画流畅,我们将在这些限制内工作。要更深入地了解 useNativeDriver 的内部工作原理,请查看我们宣布它的博客文章。
我们的动画有4个组成部分
使用 Animated,有两种主要方式来定义动画。第一种是使用 Animated.timing,它允许您精确地指定动画将运行多长时间,以及一个缓动曲线来平滑运动。另一种方法是使用基于物理的 API,例如 Animated.spring。使用 Animated.spring,您可以指定弹簧中的摩擦力和张力等参数,并让物理引擎运行您的动画。
我们希望同时运行多个动画,它们彼此之间都密切相关。例如,我们希望应用在遮罩显示到一半时开始淡入。由于这些动画密切相关,我们将使用 Animated.timing 和一个单独的 Animated.Value。
`Animated.Value` 是一个原生值的包装器,Animated 用它来了解动画的状态。通常,您希望一个完整的动画只拥有一个这样的值。大多数使用 Animated 的组件会将该值存储在状态中。
由于我将此动画视为在整个动画的不同时间点发生的步骤,因此我们将Animated.Value的起始值设为0(表示0%完成),结束值设为100(表示100%完成)。
我们的初始组件状态将如下。
state = {
loadingProgress: new Animated.Value(0),
};
当我们准备开始动画时,我们告诉Animated将这个值动画到100。
Animated.timing(this.state.loadingProgress, {
toValue: 100,
duration: 1000,
useNativeDriver: true, // This is important!
}).start();
然后我尝试估算动画不同部分的粗略值,以及在整个动画的不同阶段我希望它们具有的值。下面是一个动画不同部分的表格,以及我认为它们在随着时间的推移在不同点应该具有的值。

Twitter 小鸟遮罩应该从比例 1 开始,然后变小,然后猛地变大。因此,在动画进行到 10% 时,它的比例值应该为 0.8,然后猛地变大到 70。坦率地说,选择 70 是相当随意的,它需要足够大,以便小鸟完全显示屏幕,而 60 不够大 😀。不过,这部分有趣的是,数字越大,看起来它增长得越快,因为它必须在相同的时间内到达那里。这个数字经过了一些反复试验才使这个标志看起来不错。不同大小的标志/设备需要不同的最终比例,以确保整个屏幕都被显示出来。
应用程序应该保持不透明一段时间,至少在 Twitter 标志变小期间。根据官方动画,我希望在小鸟缩放过程中途开始显示它,并很快完全显示它。因此,在 15% 时我们开始显示它,在整个动画进行到 30% 时它完全可见。
应用程序的比例从1.1开始,并在动画结束时缩小到其常规比例。
我们上面所做的本质上是将动画进度百分比的值映射到各个部分的实际值。我们使用 Animated 的 .interpolate 来实现这一点。我们创建了 3 个不同的样式对象,每个动画部分一个,使用基于 this.state.loadingProgress 的插值值。
const loadingProgress = this.state.loadingProgress;
const opacityClearToVisible = {
opacity: loadingProgress.interpolate({
inputRange: [0, 15, 30],
outputRange: [0, 0, 1],
extrapolate: 'clamp',
// clamp means when the input is 30-100, output should stay at 1
}),
};
const imageScale = {
transform: [
{
scale: loadingProgress.interpolate({
inputRange: [0, 10, 100],
outputRange: [1, 0.8, 70],
}),
},
],
};
const appScale = {
transform: [
{
scale: loadingProgress.interpolate({
inputRange: [0, 100],
outputRange: [1.1, 1],
}),
},
],
};
现在我们有了这些样式对象,我们可以在渲染帖子前面提到的视图片段时使用它们。请注意,只有 Animated.View、Animated.Text 和 Animated.Image 能够使用包含 Animated.Value 的样式对象。
const fullScreenBlueLayer = (
<View style={styles.fullScreenBlueLayer} />
);
const fullScreenWhiteLayer = (
<View style={styles.fullScreenWhiteLayer} />
);
return (
<View style={styles.fullScreen}>
{fullScreenBlueLayer}
<MaskedViewIOS
style={{flex: 1}}
maskElement={
<View style={styles.centeredFullScreen}>
<Animated.Image
style={[styles.maskImageStyle, imageScale]}
source={twitterLogo}
/>
</View>
}>
{fullScreenWhiteLayer}
<Animated.View
style={[opacityClearToVisible, appScale, {flex: 1}]}>
{this.props.children}
</Animated.View>
</MaskedViewIOS>
</View>
);
耶!现在我们的动画片段看起来如我们所愿。现在我们只需清理那些不再需要的蓝色和白色图层。
要知道何时可以清理它们,我们需要知道动画何时完成。幸运的是,我们调用`Animated.timing`的`.start`方法接受一个可选的回调函数,该函数在动画完成时运行。
Animated.timing(this.state.loadingProgress, {
toValue: 100,
duration: 1000,
useNativeDriver: true,
}).start(() => {
this.setState({
animationDone: true,
});
});
现在我们在state中有一个值来判断动画是否完成,我们可以修改我们的蓝色和白色图层来使用它。
const fullScreenBlueLayer = this.state.animationDone ? null : (
<View style={[styles.fullScreenBlueLayer]} />
);
const fullScreenWhiteLayer = this.state.animationDone ? null : (
<View style={[styles.fullScreenWhiteLayer]} />
);
瞧!我们的动画现在正常工作了,并且一旦动画完成,我们就清理了未使用的图层。我们已经构建了Twitter应用程序的加载动画!
别担心,亲爱的读者。我同样讨厌那些只给出部分代码而不提供完整源代码的指南。
这个组件已经发布到 npm 并在 GitHub 上作为 react-native-mask-loader 提供。要在您的手机上试用,它可以在 Expo 上获得。
React Native 月度会议仍在如火如荼地进行中!请务必查看此帖子底部的下一届会议通知。
react-native-gesture-handler 这样的原始组件来构建 React Native 导航既可能又可取,我们对计划中的一些改进感到非常兴奋。如果您希望为社区做出贡献,请查看 react-native-maps 和 react-native-svg,它们都需要一些帮助!关于将这次会议用于讨论单个特定主题(例如导航、将 React Native 模块移动到单独的仓库、文档等)已经进行了一些讨论。这样我们觉得我们可以最好地为 React Native 社区做出贡献。它可能会在下次会议上进行。请随意推特您希望涵盖的主题。
React Native 月度会议继续进行!让我们看看各团队都在忙些什么。
I18nManager.swapLeftAndRightInRTL(false)来选择加入它们。下一次会议定于2017年12月6日星期三。如果您对我们如何改进会议成果有任何建议,请随时在Twitter 上联系我。