Tag: 编程

RxSwift和您可以使用响应式编程完成的出色工作-第一部分

我第一次听说反应式编程,这就是我的样子: 第二,第三和第四次也没有太大不同。 而且,即使我是第一次从事一个具有一些响应式代码的项目,也不要让我开始……那个表情在我脸上呆了整整两周! 现在我知道,很多人第一次遇到反应式编程时,都会有与我相同的感觉。 我也认识很多人,在经历了最初的不良印象之后,他们再也没有去研究它了,因为这听起来像是付出了太多的努力才开始使用。 但是我可以告诉你的事实是,我不认识一个人,在最终了解了它的工作原理之后,他才后悔参加了反应式编程。 现在我知道在线上有足够的资源来处理反应式和RxSwift的理论和复杂性,还有很多关于如何使用Rx进行各种操作的教程(我在其中最后放了一些链接)的帖子)。 不,我不会再编写有关流和可观察对象如何工作的教程或解释。 我想做的是提供一个简单,清晰,几乎没有理论的摘要,概述您可以使用RxSwift进行的操作以及为什么要使用它。 由于Rx有很多内容,我将在3个博客文章系列中对此进行细分。 让我们开始第一部分! 第1部分:数据绑定,控制事件和手势识别器 数据绑定 “数据绑定”听起来像是花哨的单词中的另一个,使事情听起来很漂亮,但这确实非常简单。 假设您有一个需要用户在文本字段中输入其名称的应用。 当他们这样做时,您要向他们打招呼,说“你好,\(名称)”。 很基本吧? 在非反应式应用程序中,应将UITextFieldDelegate协议添加到视图控制器,并实现textFieldDidEndEditing方法以跟踪用户何时完成其名称的书写,这时应设置标签的文本以对该名称问好。 不幸的是,与代表打交道可能会很烦人。 如果您有多个文本字段怎么办? 您将必须添加检查以确保用户已完成编辑正确的文本字段。 而且,如果客户决定在用户​​输入姓名时(而不只是在完成文本字段编辑时)更新标签,该怎么办? 在反应式中,这种行为可以通过数据绑定来实现。 简而言之,您想将用户在文本字段中提供给您的数据绑定到另一个UI对象(标签)。 使用RxSwift,没有比绑定数据更简单的了。 在我刚才提到的情况下,这将是这样的: var nameField = UITextField() var helloLabel = UILabel() 覆盖func viewDidLoad(){ nameField.rx.text.map {“ Hello \($ 0)”} .bindTo(helloLabel.rx.text) } 让我们分解一下:首先,我们获取文本字段的文本,然后将其映射为要在标签中设置的格式。 在这种情况下,它只是意味着在文本(又称用户名)之前添加一个问候,由于map是一个闭包,因此可以简单地称为无名闭包参数( $0是第一个参数, $1是第二个参数,依此类推)。 然后,我们将该映射文本绑定到标签中的文本。 就是这样,工作完成了! 没有委托,没有if语句,只有一点简单明了的代码。 现在,我知道您在想什么:这很重要,但实际上有多少应用程序会执行此类操作? 我只能告诉您的是,不要卡在示例中。 能够将数据绑定到视图非常强大,只需考虑一下:您可以根据天气改变视图的backgroundColor,根据一些数据,使用一些非常简单的逻辑,根据用户的位置在商店应用中导航用户可能会改变。 再说一次,我不会对理论进行过多介绍,但这是背后的主要思想。 2.控制事件和手势识别器 […]

使用Swift快速通过iCloud同步用户数据和首选项

2018年4月13日 这是用户的常见要求……“我有iPad和iPhone,如何将我的收藏夹保存在一个设备上,让它们出现在另一台设备上?” 如果用户必须创建一个帐户才能使用您的应用,则可以将此功能内置到您的后端中。 但是,有时候,我们不想强迫用户创建帐户来使用我们的应用程序。 在这种情况下,有一个简单的解决方案,只需几行代码即可跨设备同步用户数据。 这是iCloud键值存储。 它就像本地词典一样工作,对于每个iCloud用户而言都是唯一的,并且会自动在该用户所属的每个设备之间进行同步。 如果您使用userDefaults存储用户数据,那么您已经到了一半。 iCloud键值存储的工作方式非常相似,其附加好处是数据可以在应用删除后继续存在。 假设有两个设备的用户下载了您的应用,他们将生日输入到一个设备中并保存了。 您要将这些数据存储到iCloud键值存储中,以便当他们在另一台设备上打开您的应用程序时,生日将已经自动存在。 这是做什么的… 在Xcode中选择您的项目目标,然后打开“功能”部分。 激活iCloud并确保选中“键值存储”。 首先,创建一个keyValueStore。 您可以根据需要创建任意数量的字典,每个字典实际上都是不同的字典。 我通常创建一个全局实例,然后可以从任何类中调用它。 var keyStore = NSUbiquitousKeyValueStore() 将数据添加到您的keyStore keyStore.set(“03/04/1989”, forKey: “userBirthdate”) 检索应用程序新实例上的数据 if let storedUserBirthdate = keyStore.string(forKey: “userBirthdate”) { //Use retrieved birthdate } else { //Ask user for birthdate } 到目前为止,keyStore的行为与UserDefaults完全相同,即数据将仅存储在本地。 为了允许所有iCloud用户的设备访问数据,您需要通过以下方式进行同步: keyStore.synchronize() 真的就是这么简单。 要记住的一件事是同步不一定立即发生。 正如Apple Docs所说:同步发生在“适当的稍后时间”。 iCloud键值存储旨在同步不需要持续和超快速更新的数据。 用户设置和偏好设置之类的东西。 最初于 2018 […]

警卫声明被低估

前几天,当我与一个朋友谈论函数式编程时,我意识到我们经常只喜欢讨论性感的东西,而对其他简单的东西没有给予足够的重视。 在Swift中, guard声明是我认为功能非常强大但没有得到编程社区足够重视的小功能之一。 Guard不仅是if !condition语句的语法糖。 考虑以下代码。 如果!loading { 加载() } 如果当前状态未加载,它将加载。 但是我们可以更好地编写如下: 如果正在加载{return} load() 当前状态加载时返回。 它更干净,我们能够从花括号中取出load() 。 并且请注意,上面的if语句不只是if 。 如果是特殊的,用于提早返回。 它应该拥有自己的语法。 从版本2开始,Swift为此提供了guard声明。 警卫!loading else {return} load() 确保当前状态未加载,否则返回。 如果错过了return ,编译器将抱怨。 与使用if语句的版本相比,它更容易理解,因为它使用特殊的条件语法。 我们马上就知道那条线是干什么的。 警卫队要早日返回。 通常,我们将它们放在函数的顶部。 但是, guard声明并不公平,因为对我们大多数人来说,这只是一个条件声明。 但是我认为, guard应该得到更多的荣誉。 考虑以下代码。 守护!loading else {return} 警卫!渲染其他{返回} 卫队个人资料已加载其他{返回} 警卫文章已加载其他{返回} 警卫评论加载其他(返回) 守卫likesLoaded else {return} render() 以上代码在控制器中非常常见。 控制器通常是最复杂,最难阅读的。 想象一下:当控制器从服务器加载数据时,可能会发生很多事情,例如,推送通知到达,按下后退按钮,按下主屏幕按钮进入背景,设备旋转,无意间轻按了随机按钮,网络中断等等。 所有这些都应该得到妥善管理,这非常困难。 这就是为什么控制器非常丑陋的原因。 但是guard声明使它更令人恶心! […]

积极的反应:使用React.js进行开发

自2016年11月发布以来,在开发社区中已经有大量关于React和React Native的话题。 不知不觉中,React是Facebook的开源JavaScript库,目前在许多主要网站上使用。 但是,它的姊妹项目React Native使应用程序开发人员能够仅使用JavaScript来创建本机应用程序。 对于任何Web开发人员而言,结合HTML和JavaScript开发的潜力都是巨大的,特别是在React成功构建网站界面之后。 React Native允许您创建直接转换为本机组件的React组件。 然后可以在保留其样式的情况下在应用程序的将来区域中重用这些组件。 它们本质上是您应用程序的构建块。 与传统的本机开发方法相比,使用React Native的优势很多。 使用React Native时,组件将在多个平台上实现一致的渲染,而使用其他类似的解决方案可能会导致性能不一致,这是市场上越来越多的设备池的真正风险。 尽管使用其他非本机框架进行的开发也涉及JavaScript,但它们倾向于在本机应用程序中嵌入的Webview中运行,这使过程更加复杂。 React Native运行JavaScriptCore的嵌入式实例,该实例与特定于平台的本机组件进行通信。 React Native不仅会更改创建可行的本机应用程序所需的步骤; 它通过强调声明性语法,函数式编程和单向数据流,极大地改变了现有的范例。 当从一个州转移到另一个州时,这也使隔离问题和解决错误变得更加容易。 使用React Native的视图声明,当用户操作您的应用程序时,管理任何可能的意外情况变得更加容易。 根据我在React Native上与其他类似框架相比的经验,确实感觉前者比后者更能巧妙地架起Android和iOS之间的鸿沟。 现有的框架经常谈论使应用看起来像是两个平台都原生的,只是为了使最终的应用看起来像它们都不属于两者。 另外,React Native的核心是JavaScript。 从根本上讲,该平台与React.js并没有特别的不同,它使具有以前开发经验的新手可以轻松地适应它。 当然,它还可以提高效率,并允许您继承已经熟悉的技术和快捷方式。 由于仍是React,Native使开发人员能够轻松集成现有的JavaScript / React.js库,这在Facebook的鼓励下。 有些组件比其他组件更适合与React配合使用,其中Redux和MobX因其简化了应用程序状态管理的能力而受到关注。 当然,React还有很多东西。 作为iOS开发人员,我很欣赏它能够扩展我设计的潜在平台的能力,而无需花费很多精力。 并且,鉴于其相对的新近度,我期待看到React Native社区如何继续激发移动开发领域的发展。 最初发布在http://damianesteban.com/positive-reactions-developing-with-react-js/。

在Swift中进行功能性思考

如上图所示,从服务器解析JSON响应需要进行两次转换: 从原始HTTP响应转换为数据的JSON表示形式。 从JSON表示形式到模型。 在第一步和第二步之间,我们将使用数据的JSON表示形式。 这种JSON表示形式使我们能够对服务器响应进行增量转换。 通过在模型之前解析为中间JSON表示,我们的代码变得更加可组合。 JSON的表示方式对于理解本文中的功能编程概念并不是必不可少的。 但是,为了完整起见,让我们现在对其进行定义。 如果阅读JSONSerialization的文档,您会注意到有效JSON的规则之一是: 所有对象都是NSString , NSNumber , NSArray , NSDictionary或NSNull 。 由于案例数量有限,因此使用枚举似乎是一个好情况。 定义了JSONObject类型后,我们现在知道第一次转换之前,第一次与第二次转换之间以及第二次转换之后的数据类型。 它们是Data -> JSONObject > Model (我们稍后会定义模型。) 功能方法 有一个核心概念将通过此练习来推动我们的思考过程。 我们将过程中的每个步骤都视为独立于函数外部变量的转换 。 让我们看一下如何将这种思考过程应用于上面概述的两个转换。 转换1:将数据转换为JSON对象 我们将从定义一个新类型Deserialize开始,该类型将Data转换为JSONObject 。 在函数式编程中,类型由其方法签名定义。 要使用此类型,我们编写了一个函数,该函数返回新的Deserialize类型。 我想强调关于JSON()函数的几件事。 该函数返回一个闭包。 以面向对象的思维方式看待这个问题可能看起来很奇怪,但这是标准的函数式编程。 请注意,从JSON()返回的闭包完全独立于该函数外部可能存在的任何状态。 这与我们非常依赖状态的面向对象编程形成了鲜明的对比。 转换2:将JSON对象转换为模型 正如我们在第一个转换中所做的那样,让我们​​定义一个新类型,该类型接受一个JSONObject作为参数并返回类型T的模型: typealias Decode =(JSONObject?)->(T?) 注意:我们使 Decode 函数通用,因此我们可以解码各种模型。 由于我们尚未定义任何模型,因此这是我们开始从JSON创建模型所需的所有样板代码! 下一步是定义我们的模型以及可以解析JSON的函数。 全部放在一起 让我们导出一个简单的示例,以显示与该代码交互的外观。 对于此示例,我们将期望服务器向我们发送有关用户的信息。 我们将定义User模型,并编写一个函数decodeUser() ,以执行从JSONObject到User的转换。 […]

Swift中的算术运算符

如果您要开发的应用程序需要进行大量的数学计算,则可能希望能够在错误扩散到各处之前捕获错误,例如被零除和整数类型溢出。 浮点错误包括被零除(产生±infinity )和未定义( NaN )表达式,例如sin(Double.infinity) 。 这些无声地失败了。 整数类型的运算符错误仅限于溢出,这会使您的代码大声崩溃。 Swift提供了方便的整数算术运算符,允许溢出; &+ , &− , &*以及更多有用的方法,例如addWithOverflow(_,_) 。 我利用这些方法来定义抛出运算符&& + , &&- , && * , && / , &&%和&&? 。 用法如下。 / * *示例。 * / //产生15.27543444817377 func x()throws-> Double { 返回尝试(12.5 && + 2.3) && /(&&?sin(3.1)&& + &&? atan2(4,3)) } //引发ArithmeticOperationError(.infinity,.division) func y()throws-> Double { 让y […]

Swift中的内存泄漏

在本文中,我们将讨论内存泄漏,并将学习如何使用单元测试来检测它们。 偷看一下: 这是用SpecLeaks编写的测试。 重要:我将解释什么是内存泄漏,讨论保留周期以及您可能已经知道的其他事情。 如果您只想阅读有关单元测试泄漏的信息,请跳至最后一部分。 内存泄漏 确实,这是我们作为开发人员面临的最常见的问题之一。 我们会逐个功能地编写代码,并且随着应用的增长,我们会引入漏洞。 内存泄漏是内存中永远被占用且永远不会再使用的一部分。 这是占用空间并导致问题的垃圾。 在某个时刻已分配但从未释放过并且不再被您的应用程序引用的内存。 由于没有对其的引用,因此现在无法释放它,并且该内存无法再次使用。 苹果文件 从初级到高级开发人员,我们都会在某个时候造成漏洞。 我们有多经验都没关系。 拥有一个干净,无崩溃的应用程序,消除它们是至关重要的。 为什么? 因为它们很危险。 泄漏很危险 它们不仅增加了应用程序的内存占用 ,而且还引入了有害的副作用和崩溃。 为什么内存占用量会增加? 这是对象未释放的直接结果。 这些对象实际上是垃圾。 随着创建这些对象的动作的重复,占用的内存将增加。 垃圾太多了! 这可能会导致出现内存警告情况,最终,该应用程序将崩溃。 解释不必要的副作用需要更多细节。 想象一下在init内创建通知时开始监听的对象。 它对此做出反应,将内容保存到数据库,播放视频或将事件发布到分析引擎。 由于需要平衡对象,因此我们在释放对象时在deinit内停止监听通知。 如果此类物体泄漏,会发生什么? 它永远不会死,也永远不会停止收听通知。 每次发布通知时,对象都会对此做出反应。 如果用户重复创建有问题的对象的操作,则将存在多个实例。 所有这些实例都响应该通知并相互介入。 在这种情况下, 崩溃可能是最好的事情。 多个泄漏的对象对应用程序通知做出反应,更改了数据库,UI,破坏了应用程序的整个状态。 您可以在The Pragmatic Programmer中的“死程序不说谎”中了解有关此类问题的重要性。 泄漏无疑会导致不良的用户体验和不良的App Store评分。 泄漏来自哪里? 例如,泄漏可能来自第三方SDK或框架。 甚至来自Apple创建的类,例如CALayer或UILabel 。 在这些情况下,除了等待更新或放弃SDK之外,我们无能为力。 但是我们很有可能在 我们的代码。 泄漏的 首要 原因是 保持周期 […]

查理和工厂方法模式

在阅读有关“设计模式”或“设计原理”的文档时,我发现大多数情况下创建示例都是非常简单的,足以理解其本质。 这种方法的不便之处在于,很多时候您看不到它所解释的内容的真实应用。 而且,因此,您很难知道如何在现实生活中遇到实际问题时应用这些原理和模式。 前几天,我开始开发Apple TV天气应用程序,以练习Swift并学习如何为tvOS平台开发应用程序。 使其变得更有趣和新颖的想法是让用户从已实施的气象服务列表中选择气象服务提供商。 从一开始,我就意识到这是一个很好的示例,用于解释Factory Method Pattern的工作方式以及如何在Swift中实现它。 从维基百科: 在基于类的编程中, 工厂方法模式是一种创建模式,该模式使用工厂方法来处理创建对象的问题,而不必指定将要创建的对象的确切类。 一个真实的例子 第一步将是在工厂方法中指定要创建的对象的接口,在本例中为Weather Service。 我们还需要一种方法,该方法将为完成的城市及其所在国家/地区在完成处理程序中返回带有天气预报的Weather结构。 在这一点上,我们可以创建自己想要的,符合该协议的任意数量的服务。 例如,我们可以创建实现以从“开放天气地图”和“世界天气在线”服务中提供天气预报(GitHub仓库中的完整实现): 最后一步是创建工厂本身: 现在,每次我们想要添加新服务时,我们都必须: 创建它并实现WeatherServiceProtocol 在枚举中添加一个case,并在开关的相应case中返回其实例化 因此,当我们要使用气象服务时,可以按以下步骤进行: 现在,当用户选择其他服务时,我们只需要用新的服务枚举 case替换.WorldWeather 。 包起来 我们已经实现了一个工厂方法模式的示例来创建气象服务对象,而不必指定将要创建的对象的确切类。 我们通过使用一个非常重要的设计原则来实现这一目标:“编程为接口,而不是实现。”在这种情况下,通过编程为WeatherServiceProtocol而不是不同的具体实现。 设计模式是一般性想法,没有一种实现每个想法的独特方法。 在Swift中可能有不同甚至更干净的方法来实现此模式,在这种情况下,我很想知道它。 因此,不要犹豫,发表自己的想法。 资源: 您可以在以下GitHub存储库中找到WeatherTV项目的代码:https://github.com/dadederk/WeatherTV Swift 2设计模式->第1章:创建模式->工厂方法模式:http://www.amazon.co.uk/gp/product/1785887610/ref=as_li_tl?ie=UTF8&camp=1634&creative=19450&linkCode=as2

面向协议的Swift是否比面向对象的Swift更好?

面向协议的编程是什么意思,为什么它比OOP更好? “像我五岁一样解释”-洗衣服务示例 首先,提供的示例非常容易理解。 我强烈建议您跳过阅读评论者写的内容。 主题是“洗衣服务”。 假设有一个Laundry对象,其中封装了某些与洗衣相关的功能……“请给我洗衣服”-“好,这是您的衣服!”。 您作为客户只需说“请洗钱!”即可与Laundry互动。 Laundry对象开始工作并执行其工作,其范围可能从简单到令人难以置信的复杂–美丽之处在于,作为客户,就像需要洗衣服的人一样,您不在乎。 只要洗完衣服就可以退回衣服,生活就很棒! 面向对象的“问题” 在构建软件时,我们是在使用其他人构建的代码,还是创建供他人使用的代码,不是吗? 我们正在使用的代码“挂钩”到其他开发人员设计并提供给我们的东西,或者我们正在创建其他代码将“挂钩”并与之交互的代码,即使“其他代码”是由我们在自己的应用程序中。 在成为代码的客户和创建者时,我们会同时戴上两顶帽子。 但是,如果我们将Laundry对象作为开发人员/创建者 ,而不是作为客户端 (即需要洗衣服的人)来工作,该怎么办? 如果我们作为开发人员将一个Laundry对象交给了一个库,并且想要自定义它的行为,那么……可能会提高launderClothes()的性能,或者重写它的实现以使用一些令人惊奇的新洗衣服务。 我们这样做的方法是创建一个子类 。 评论者说,这是具有对象定向的牛肉: [Object Orientation]通过继承来公开状态和内部,从而鼓励“封装复杂性”。 解说:软件具有天生的复杂性。 对象是封装这种复杂性的“事物”。 但是,他们以某种方式做到这一点:他们公开了某些状态和功能。 这些关于复杂性的抽象通过系统传播和定制的方式就是通过称为继承的机制。 但是评论者称这种方法是“麻烦的”。 为什么? 好… “我可能不想知道我的干洗是如何完成的,但是如果我想设计一条从脏衣服到干净衣服的更好方法,我就必须知道所执行步骤的每一个最后细节,这样我才能尝试在我的子类中完善它们。” 因此,评论者从开发人员/创建者的角度出发。 需要指出的是,要真正能够改善子类中的性能或优化算法,我们必须要知道在超类中执行的步骤的每个细节。 我们并非总是可以发现超类实现来改进它的情况。 转向协议方向 那么…协议方向? 如果更好,那会更好吗? 我喜欢评论者以洗衣服务的面向对象示例为例,并从协议而非对象的角度出发细化了细微差别。 真。 看一看。 我们看到的主要区别是,与其拥有一个Laundry对象而不是一个以自己的特定方式进行Laundry对象,而是发生了转变:我们开始着手描述完成洗衣的方式 。 如果我想由一个特定的人以一种特定的方式来完成洗衣, 那么我会去面向对象的,而不再关心事情如何完成。 但是,如果我想概括一下洗衣服的方法 ,则需要以协议为导向,并不再关心其他所有事情。 在协议定向中,唯一重要的是接口 ……客户端将与之交互的事物。 尽最大可能通过一个协议来描述它,然后让其他事情出现并担心该如何做 。 最后,需要洗衣服的人唯一需要知道的是他们可以使用哪些界面来完成他们需要完成的洗涤。 能够坐下来思考一下,如果您从图片中删除了状态和继承,并且只考虑了与该特定任务的最低要求接口,将会看起来如何,这是一种更强大,更简单的编程方法,使重用比OO曾经做到过。 外卖 对我来说,这个关于Protocol Orientation的要点是:Protocols是泛化的 。 […]

另一则关于以编程方式创建视图的iOS帖子!

我知道我知道。 关于为什么为什么以编程方式创建视图比通过Storyboards / Xibs进行创建更好或过时(取决于作者),有许多类似(已读:相同)的文章。 幸运的是,我不会讲解为什么编程视图优于Storyboards或Xibs。 为什么? 因为这三种方法都完成同一件事:创建iOS应用程序的用户界面。 关于这个话题将有无休止的辩论。 每种方法都有其自身的优势/劣势,每个人都有自己的偏好。 但更重要的是,只要iOS存活,每种方法都会存在。 正如我的金融教授曾经说过的:“前往拿骚街的方式不止一种。” 为什么我喜欢程序化视图 就个人而言,当我切换到以编程方式创建视图的方式比使用Storyboards或Xibs时,我了解的更多有关UIView及其实例/类方法的知识。 Storyboard / Xibs并没有真正教会我有关UIKit的内部工作的知识,就像它调用某些UIView方法时一样。 一切都是魔术。 作为软件开发人员,不知道事情是如何运作的,这是不行的。 就我个人而言,如果我不知道事情在软件开发中的工作方式,那将使我无休止。 我睡不着了。 在仔细阅读了Apple文档,Google,StackOverflow和iOSProgramming subreddit之后,我真的提高了对以下方面的理解: UIKit / UIView 自动版式 迅速 面向对象编程 此外,如果以编程方式创建视图,则可以更好地控制使用用户界面可以执行的操作。 (谁不喜欢他们的所作所为的绝对控制权?) 最后(也是我认为最重要的一点),您创建的UIView子类的每个属性都可以在一个.swift文件中看到。 您不必单击Interface Builder中的每个UIView即可查看其属性设置。 您修改的所有内容都是明确的。 我为什么要为一个已经有很多文章的话题发帖? 我已经阅读了许多关于iOS中程序化视图的文章/教程,但没有一个真正遵循Model-View-Controller模式。 这些教程很简短, UIView代码位于视图控制器中(您永远都不应在视图控制器中创建视图)。 我基本上不得不猜测如何将UIView代码从视图控制器中分离出来,但是幸运的是,在Google上进行了无数次搜索之后,我发现了一些可以帮助我入门的建议: 应该为每个视图控制器创建一个UIView子类,实质上是用Swift而不是XML编写的Xib。 这篇文章的目标 让读者(您)知道如何以编程方式(使用SnapKit)编写视图,而又不会破坏Model-View-Controller设计模式并防止创建Massive View Controller。 这不是初学者友好的教程 我假设您知道Xcode for Swift中单视图应用程序的基本项目结构,并且知道什么是Cocoapods以及如何使用它安装Pod。 您应该对保留周期有所了解,并对值类型和引用类型有所了解。 话虽如此,让我们开始吧! 教程! —基本设置 让我们从每个人最喜欢的视图开始进入编程视图:“登录身份验证”屏幕! 但是在开始之前,让我们从基本设置开始。 创建一个新项目:Single […]