Swift, ReactiveCocoa第一个app

引言

这篇文章将纪录用Swift语言配合ReactiveCocoa写一个伪搜索引擎app的历程。
大量参考了RayWenderlich.com上的文章(原文链接1 原文链接2)。原文是针对Objective C的,但是现在Swift都已经更新到了3.0(虽然因为作者没有developer id,用的还是2.2),ReactiveCocoa也更新到了4.2,原来的大多数技术都已经不能直接使用了(ReactiveCocoa的开发者甚至说在未来的5.0版本中要移除Objective C的支持)。作者为此也走过不少弯路,参考了多方资料,最终完成了这个sample app,希望可以让别人少走一点弯路。
作者推荐新接触iOS的开发者在学完基础的swift之后直接使用ReactiveCocoa+Swift来写代码,这将节省大量的精力。

ReactiveCocoa简介(翻译自RayWenderlich.com,有改动)

作为iOS开发者,我们写的每一行代码几乎都是在和“事件”打交道,例如用户点击了一个按钮,网络上发来一条信息,一个属性值的变化(Key Value Observation),或者是用户的位置改变了。但是,CocoaTouch把这些事件以不同的格式封装在了一起,例如target-action,delegate,KVO,回调之类的。这就给开发带来了很大的麻烦,降低了代码的可读性,随之而来的就是更多的维护成本和更多的bug。ReactiveCocoa把这一切封装到了一个标准的接口中,这样它们就可以很容易地被组合、过滤。

ReactiveCocoa把函数式编程和响应式编程组合在了一起。

函数式编程:这种编程方式使用了高阶函数,也就是把其他函数作为参数的函数(作者认为RayWenderlich.com的解释不太好。作者认为函数式编程就是一个不同的计算机架构方式。它注重数学在计算机科学中发挥的作用,把函数理解成真正的数学上的函数。其核心就是没有Side Effect,也没有变量。这非常好的避免了并发编程中的很多问题,因而在这两年逐渐流行。Swift中有良好的函数式编程支持,其语法对著名而经常使用的Monad结构的支持甚至比经典的Haskell语言还好)

响应式编程:这种编程方式注重数据流的传播和管道式的程序结构。这种结构也是为了复杂的并发程序而生的,先天具有简洁、安全的特点。

因为这个原因,ReactiveCocoa也被称为是一个函数响应式编程框架。

这里就不再在学术上深究了,打开Xcode吧!

程序结构设计

程序最终运行的效果如下:

当用户在文本框中输入长度大于4的文本时,下方的列表就会显示和用户输入字符长度相同的“搜索结果”(为了保持简单,这里就直接生成了一些字符串,而不是去调用搜索引擎的API)。并且只有当用户的输入在0.5秒中没有变化时,动作才会被触发。由于非常简单,不考虑错误处理。由于真正的Web API需要访问网络,引发异步事件,这个程序如果使用普通的方法将具有相当的复杂性。

程序内部将采用管道式,数据流经过管道之后最终将被一个UITableView显示。由于Swift中变量绑定的问题,程序并未采用MVVM设计模式,代码中的ViewModel只是一个保存数据的容器。(未来将会改进为MVVM模式)

那么就 开始吧!

第一步

建立一个iOS工程,类型是Single View Application,设备选择iPhone(方便UI设计),语言选择swift

安装ReactiveCocoa

ReactiveCocoa官方推荐使用Carthage安装。(当然CocoaPod用不了,原因你懂的)Carthage安装外部库的操作非常简单:

  1. 打开终端,定位到工程的根目录下(即*.xcodeproj所在的地方),使用文本编辑器建立一个Cartfile
    在终端输入nano Cartfile 在新建立的文件中加入一行github "ReactiveCocoa/ReactiveCocoa" Control-O保存,Control-X离开 回到终端,输入命令carthage update 等待Carthage下载并编译框架 (如果尚未安装Carthage,可以到官网下载二进制文件,或者用Homebrew:brew install carthage)

  2. 打开工程文件,在General选项卡下加入刚刚编译出的framework文件(注意到还有一个Result.framework):

  3. 在Build Phases选项卡下加入一个新的Run Script,并添加文件,最终看起来应该是这样(命令需要手工输入 如果不说Homebrew安装的Carthage,路径可能不一样):

  4. 现在框架已经引入完了,可以试验一下,到ViewController.swift中输入import R,Xcode的自动补全应当在这时给出ReactiveCocoa的提示,那就说明安装完成了

设计UI,编写Table View的代码

UITableView是UIKit中操作较为复杂的一个,但这个特性也让它可以不需要绑定就直接使用ReactiveCocoa的特性,因此在这里选用它来做介绍。
不了解UITableView的操作并不影响接下去的阅读,因为所有操作都被说明了

首先打开Main.storyboard,向场景中拖入一个Text Field。设置AutoLayout:左20,右20,上0。再拖入一个Table View,放在Text Field下方,设置AutoLayout:左20,右20,下20,上8。再拖入一个Table View Cell放在Table View里面,拖入一个Label放在Cell里面,设置AutoLayout:竖直居中,左50。整个场景看上去应该像这样:

之后,在StoryBoard中设置Cell的identifier为ResultCell。我们将在之后编写完Cell的代码之后改变这个Cell的其他属性。如图:

接下来我们就来编写Table View的代码。

首先是Cell:

  1. 新建一个swift文件,命名为TableViewCell.swift

  2. 定义一个类:TableViewCell,声明为UITableViewCell的子类,并且用Interface Builder连接之前在storyboard里面创建的Cell中的Label。加入一个字符串常量,和之前输入的identifier一样。最终看起来应该是这样(作者使用了自己的类前缀LF):

  3. 在storyboard中更改Cell的类型:

这样,Cell的就定义好了。这可以被很容易地改为更复杂的情形。

之后是Table View本身的代码。UITableView采用了Data Source - Update的模式。这种模式的实现需要一个Data Source。
在作者的实现中,需要先有一个ViewModel来封装数据。为此,建立一个swift文件,名为MainViewModel.swift,并加入以下代码:

struct MainViewModel {
    var resultCount: Int!
    var results: [String]!
    
    static func isValidSearchString(text text: String) -> Bool {
        return text.characters.count > 4
    }
    
    static func produceSearchResult(text text: String) -> [String] {
        return (1...text.characters.count).map {
            i in
            return "somebody \(i)"
        }
    }
}

这里定义了封装数据的格式,并提供了两个辅助函数。
在这个例子中这两个函数相当简单,但是随着代码变得复杂,把操作聚合起来是很有利的。

之后新建一个swift文件,命名为ResultViewController.swift,加入以下代码:

import UIKit

class LFResultViewController: NSObject,UITableViewDataSource {
    var viewModel = MainViewModel()
    
    @objc func tableView(tableView: UITableView,numberOfRowsInSection section: Int) -> Int {
        return viewModel.resultCount
    }
    
    func tableView(tableView: UITableView,cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cellRetired = tableView.dequeueReusableCellWithIdentifier(LFTableViewCell.identifier) as? LFTableViewCell
        if cellRetired == nil {
            cellRetired = LFTableViewCell(style: .Default,reuseIdentifier: LFTableViewCell.identifier)
            cellRetired?.outputLabel = UILabel()
        }
        cellRetired?.outputLabel.text = self.viewModel.results[indexPath.row]
        return cellRetired!
    }
}

这里实现了Table View更新时需要的数据的方法。具体的关于UITableViewDataSource协议的内容可以查看Apple的官方文档.
这个模式看起来比较复杂,不像其他控件那样直截了当的就是赋值。但这种模式对响应式编程和MVVM模式特别有利,作者在未来可能会为整个UIKit开发这样的接口来避免使用keypath。

构建管道

这里先贴出ViewController.swift中其他部分代码:

import UIKit
import ReactiveCocoa

class ViewController: UIViewController {
    @IBOutlet weak var outputTableView: UITableView!

    @IBOutlet weak var searchTextInput: UITextField!
    
    var resultViewController = LFResultViewController()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        self.resultViewController.viewModel.resultCount = 0
        self.resultViewController.viewModel.results = []
        self.outputTableView.dataSource = self.resultViewController
        self.outputTableView.reloadData()
    }

}

最关键的部分开始了!
RayWenderlich.com上的教程中使用了Signal,但是在swift中,ReactiveCocoa的开发者并没有提供泛型的Signal,而使用AnyObject来和Objective C兼容。但这个严重损伤了代码的可读性,也让bug有了遁身之处。因此在这里,我们将使用SignalProducer,这样可以使用swift的类型检查来杜绝bug。
viewDidLoad中加入以下代码:

let searchText = searchTextInput.rac_textSignal()
            .toSignalProducer()
            .map {text in text as! String}

map函数把一个集合类型(Array例如)中的元素逐一操作,并返回新的元素构成的集合类型。用Array举例可以清楚地解释这一切。(不恰当地说,你可以把SignalProducer当成一个Array,startWithNext就相当于for-in)

[1,2,3,4].map {$0 * 2} //[2,4,6,8]

这里把一个RACSignal转化成了一个SignalProducer,并且这个是有类型检查的。为了更好地使用类型检查,我们把原本的SignalProducer<AnyObject,NSError>转化成了一个SignalProducer<String,NSError>。在这里,作者想说,Xcode编辑器的类型检查可以很好的帮助我们避免一些问题。尤其是在接下来构造Monad结构时,常常可以三指点按(即LookUp手势)来查看构造出来的对象的类型。这可以帮助理清楚Monad每一步的流程。
接下来,我们有了用来搜索的文本,我们要先执行一些过滤。在viewDidLoad中加入以下代码:

searchText.filter(MainViewModel.isValidSearchString)

还是用Array举例说明:

[1,4].filter {$0 % 2 == 0} //[2,4]

这样,我们就用之前定义的过滤函数来检查字符串是否是合法的搜索字符串。这个方法输出的还是一个SignalProducer<String,NSError>
我们还需要做一点过滤,也就是说,只有当用户输入在500毫秒内没有变化时,更新才会被触发。为此我们需要throttle函数。在之前那句话下面加上

.throttle(0.5,onScheduler: QueueScheduler.mainQueueScheduler)

注意前面的点。我们事实上是在调用上一步结果的一个方法。这就是Monad的特点:流畅接口。每一步都会构造一个对象,下一步调用它的方法。Xcode似乎并不喜欢Monad结构,缩进做得很差。Xcode 8和Xcode Extension或许可以解决这个问题,但是现在还得手工格式化。还有swift编译器在处理链式调用时会出现一些问题,最常见的是报错:Expression too complex to be resolved in reasonable time. 这种时候只需要在链式调用的每个点之前换一行就可以了。这也是推荐的用法。

接下来是传统方法最费力一部分了:异步请求。(Accept the fact that we are living in a asynchronous world)所幸ReactiveCocoa提供了一个良好的方法来解决这个问题:把它们包装成SignalProducer。但是我们在这里遇到一个问题:如果用map并且返回一个SignalProducer,我们将会在下一步得到一个SignalProducer<SignalProducer<([String],Int),NoError>,NoError>。这显然不是我们想要的。这里就要介绍flatMap函数了。先看Array的举例:

[1,4].map {[$0]} //[[1],[2],[3],[4]]
[1,4].flatMap {[$0]} //[1,4]

也就是说,flatMap方法会自动“剥掉一层”。加上代码:

.flatMap(.Latest) {
                (text: String) in
                return SignalProducer {
                    (o: Observer<([String],c: CompositeDisposable) in
                    let rst = MainViewModel.produceSearchResult(text: text)
                    let cnt = rst.count
                    o.sendNext((rst,cnt))
                    o.sendCompleted()
                }
            }

通过这个flatMap,我们把原来的SignalProducer<String,NSError>转化成了SignalProducer<([String],NoError>。(注意使用NoError类型需要包含Result框架)

现在,我们有了“搜索结果”,可以去显示了。
加上代码:

.observeOn(UIScheduler())
            .startWithNext {
                [weak self] (x: ([String],Int)) in
                if let strong = self {
                    strong.resultViewController.viewModel.resultCount = x.1
                    strong.resultViewController.viewModel.results = x.0
                    strong.outputTableView.reloadData()
                }
            }

这里有两个要说明的地方:一是在iOS上只有主线程可以更新UI,因此我们需要借助UIScheduler来把工作转移到主线程。还有为了避免循环引用,我们需要声明一个[weak self] 来告诉编译器我们不希望闭包持有对self的引用。详细说明
最终,viewDidLoad函数应该看起来像这样:

override func viewDidLoad() {
        super.viewDidLoad()
        self.resultViewController.viewModel.resultCount = 0
        self.resultViewController.viewModel.results = []
        self.outputTableView.dataSource = self.resultViewController
        self.outputTableView.reloadData()
        
        let searchText = searchTextInput.rac_textSignal()
            .toSignalProducer()
            .map {text in text as! String}
        searchText.filter(MainViewModel.isValidSearchString)
            .throttle(0.5,onScheduler: QueueScheduler.mainQueueScheduler)
            .flatMap(.Latest) {
                (text: String) in
                return SignalProducer {
                    (o: Observer<([String],cnt))
                    o.sendCompleted()
                }
            }
            .observeOn(UIScheduler())
            .startWithNext {
                [weak self] (x: ([String],Int)) in
                if let strong = self {
                    strong.resultViewController.viewModel.resultCount = x.1
                    strong.resultViewController.viewModel.results = x.0
                    strong.outputTableView.reloadData()
                }
            }
    }

编译运行,程序如预期执行。

写在后面

ReactiveCocoa代表了一种全新的方式。它的核心就在于:“高聚合 低耦合” 同时具有强大的异步处理能力。Forget dispatch_async,let's startWithNext.

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。