Tag: 协议

迅捷泛型

Hola技术人员,直到现在我们所有人都已经阅读了许多有关最酷的主题泛型的博客和文章,但仍然有模糊的想法吗? 让我们尝试再次擦拭灰尘。 通用功能使程序更灵活,可重用和可伸缩,可以与任何类型的数据类型一起使用。 泛型是Swift非常强大的功能。 大多数基于泛型的库和框架。 甚至我们在日常编码生活中都使用了泛型而不了解它。 示例:数组和字典都是swift的通用集合类型,它们可以容纳任何类型的数据类型,例如Int,String,CGPoint等。让我们不必过多谈直接进入一些实际的东西。 让我们编码… 假设我们有一个电子商务应用程序,并且必须在该应用程序上的大部分位置显示金额标签,并且金额也来自服务器,而该服务器也不是特定的数据类型,有时是Int,Float,Double并可能是String本身。 因此,我们通常要做的是。 每次都厌倦了类型转换,如果我们得到一个函数,可以传递任何类型的数据类型,该函数会将其转换为带有$($)的字符串,以便将其直接传递给我们的UILabels或UIButtons。 是不是很酷。 在上面的扩展中,我们符合我们的协议,在该协议中,我们还将返回Double和String的转换,并附加了美元货币。 我们必须对所有数据类型实施相同的操作。 现在将在我们的Utilities类或Global类中编写一个全局函数amountText ,可以在整个项目中访问它而无需创建任何实例。 将使此函数公开,以便它也可以在模块外部使用,默认情况下,它将在内部使用,因此不能在定义模块之外使用。 此函数将采用通用数据类型,该数据类型均符合我们的协议AmountConversion 。 现在,使用我们的标签和按钮检查它是否可以接受。 来吧,我们来创建一个UILabel和一些带有若干数据类型的实例。 这是可以预期的数量。 并尝试直接将其与标签文本一起传递。 在这里,我们在协议中创建了一个初始化程序,这将有助于我们在通用方法中返回默认值,将在下一节中讨论它。 再次来到您的Utility或Global类,并编写一个通用方法。 它将接受所有符合我们的协议UnwrapOptional的任何类型的参数。 现在让我们创建一些可选的变量,并检查我们的函数是否给出了预期的结果。 下面一些变量具有值,而另一些则为零,让我们看看函数返回给我们什么值。 哇哈哈! 我们得到了期望的输出。 现在将进一步扩展此功能。 如果我们有任何自定义类或自定义类,在其中也有可选的var可以使用,那么它也可以是可选的链接。 能行吗 我们来看看 我有电影类,现在有一些可选变量尝试用电影类型的实例解开可选对象。 YOOO,之所以起作用,是因为最终我们试图解包已经符合我们的UnwrapOptional协议的字符串或指定的数据类型。 现在查看是否尝试解开Movie类的实例。 没运气! 它将出现错误“在参数类型’电影?’中,’电影’与预期的类型’UnwrapOptional’不符” 因此,在这里我们必须使我们的类符合协议。 并且它将为类添加必需的init,因为我们的UnwrapOptional协议具有自己的构造函数,可返回自定义类的默认值。 是的,任务完成了。 现在,我们可以尝试使用可选链接获取值。 抢啤酒! 干杯! 它的工作原理,足以满足一天的学习需求。 好了,我们了解了泛型如何与协议握手,并解决了许多问题。 泛型有很多附带功能。 欢迎反馈。

从封装到协议扩展

假设我们要在Swift中定义一个简单的服务类,如果某些输入为正,则只输出一些输出。 (哇:要求跨度少于2行!) 对象封装 简单地说-容易完成吗? 是的,如果我们不在乎客户端的可扩展性: 使用这种方法,客户端类可以简单地实例化服务,并将doSomething工作委托给它。 非常简单(这很好),但是非常令人讨厌的是,当服务实例本身正在执行工作时,客户端无法干预要执行的输入数据和输出操作。 继承-不错的尝试! 不,继承并不总是我们的敌人。 它确实有(两个?)有效的用例。 但是恕我直言,这不是我们最好的朋友(即使代码确实更短!): 通过仅覆盖需要的内容,客户端可以将实现注入所需的位置。 每当定义时,服务将调用客户端替代而不是其自身的实现。 而且一切都会很顺利,例如,客户端现在不能是自定义的NSView (或UIView ),因为在这种情况下,有必要从所需的Cocoa基类而不是我们的服务中继承它…… 功能编程 哦! 我们正在变得现代化,是吗? 尽管如此,对于Swift而言,我也不认为这也是服务开发人员应该遵循的最佳方法-仅仅是因为这将(可能)需要在客户端开发人员级别上过多的基础设施意识。 确实,我们服务的用户需要执行一些“技巧”(例如,如果要在该处使用客户端状态,则捕获弱者或无主的 自我 ),以确保当他们将自定义闭包传递给我们时,不会在应用程序中引入内存泄漏: 代表和协议扩展 现在我们在说话! 我们最终将有一个真正的解决方案。 不需要继承,也不需要客户端开发人员的ARC知识! 我们只需要: 具有可选扩展名的服务委托协议,以提供我们要使用纯Swift公开的默认实现(即,没有@objc可选成员); 在服务中托管此类委托对象的弱实例或无主实例(尽管要求客户端类型为类,但无需我们的客户端开发人员处理内存管理问题); 并在适用时最终调用委托的(属性和)方法: 一体 最后,让我们看一看适用于我们所有类型客户的服务解决方案:如果喜欢继承,则接受; 否则,人们可以使用函数式编程并传递(设计良好)闭包; 或者,他们可以仅仅依靠(弱)聚集的代表来完成工作;或者 当然,所有这些都是为了防止默认情况下首先出现以下问题: 现在轮到你了。 您不需要上面的(我承认,很丑)多合一解决方案,但是至少您现在可以根据自己项目的需要对其进行调整。 无论如何,我想补充一点,我通常发现它足以支持继承和具有(部分或全部)默认实现的委托,但是有时功能样式也可以很好地工作(当您不希望客户需要在转义它们将通过的闭包)。

如何在iOS中应用依赖反转

您在大学时代曾经遇到过有关依赖倒置原则的话题吗? 在面向对象的编程中,该原理指的是解耦软件模块的一种特定形式(如Wiki所述:https://en.wikipedia.org/wiki/Dependency_inversion_principle) 高级模块不应依赖于低级模块。 两者都应依赖抽象。 抽象不应依赖细节,而细节应依赖抽象。 在我大学期间,这个概念似乎真的很有价值,但是我从来没有实践过。 当我们将此原则付诸实践时,我们可以意识到它可以在开发项目结构方面起到多大的作用。 对于许多开发原则,去耦是设计代码中最重要的方面之一,而该原则解决了这一挑战。 但是我们如何在我们的iOS项目中应用这一原理? 实际上,我们不需要从头开始创建遵循依赖关系反转的类。 实际上,Apple鼓励我们使用这种模式。 委派模式是整个iOS应用程序中最常用的模式之一。 如果您曾经使用过UIImagePickerController并实现了它的方法以符合标准,那么您已经遵循了依赖关系反转原理。 同样,当我们通读以下内容时: 高级模块不应依赖于低级模块。 两者都应依赖抽象。 高级模块是UIImagePickerController-该模块可以独立运行,并且具有功能框架(即抽象)。 但是其功能的使用尚不确定。 这是低级模块现在定义的时间(这是您的代码) 抽象不应依赖细节,而细节应依赖抽象。 还要注意这一点,当您在iOS中使用要求您遵守协议的类或模块时,需要您的类来实现其方法。 即,UITableViewDatasource,UItableViewDelegate,UIImagePickerController等。 即使苹果已经为我们提供了具有依赖关系反转就绪类的类,但是在我们公司的发展中仍有一段时间我们公司希望我们创建模块化的类,这些类可以在其他项目中重用。 (是的,为将来的项目节省时间和金钱。) 借助依赖关系反转的心态,我们可以重构一些旧项目,甚至可以从那里的可重用模块中提取出来。 就像您真正喜欢的“位置选择器”或“自定义相机控制器”一样。 您甚至可以制作符合特定标准或公司感觉的模块。

协议深入故事

按照苹果的定义, 协议定义了适合特定任务或功能的方法,属性和其他要求的蓝图。 然后,该协议可以由类,结构或枚举采用,以提供这些要求的实际实现。 满足协议要求的任何类型都被称为符合该协议。 作为工作的一部分,我接受了iOS开发人员的面试。 什么是协议? 是我在面试中问的一个常见问题。 许多候选人试图记住上面的定义,并重复一遍。 尽管他们对此进行了努力,但许多人仍无法正确解释。 如果我问一些交叉问题,许多人将无法回答。 在刚开始的日子里,尽管我在应用程序中大量使用协议,但我在向某人解释时也难以正确解释协议。 因此,我正在写此博客,以从最基本的角度讨论协议。 在讨论协议之前,让我们假设Swift中没有协议。 通常我们将如何从一个班级交流到另一个班级? 我们需要为该类创建一个对象。 通过使用该对象,我们可以在该类中调用函数。 这很简单。 现在,让我们从一个示例开始,您已经创建了一个名为Stepper的UIVIew子类,我们需要在项目的多个视图控制器中使用该Stepper。 棘手的是,Stepper需要将视图中发生的更改通知给视图控制器。 因此,如果没有协议,Stepper应该具有视图控制器的对象。 假设有三个控制器使用步进器。 因此,对于所有这些人,Stepper应该具有对象。 然后,只有Stepper才能将消息传递到控制器。 而且,如果您想在另一个项目中使用此Stepper,则再次需要在Stepper中进行更改以兼容新项目中的视图控制器。 实际上,它将非常难以管理。 这就是协议帮助我们在项目中避免这种无意义的代码的地方。 协议有何帮助? 为了通知计数值变化,我们将创建一个带有函数的协议,该函数将通知计数值变化。 现在,我们只需要在Stepper类中为此协议创建一个对象。 var委托:StepperDelegate? 现在,我们将研究视图控制器的实现。 上面的代码指示FirstViewController是UIViewController的子类。 因此,根据继承,通过使用FirstViewController对象,我们还可以调用在UIViewController中声明的public / open函数。 现在,我们将在FirstViewController中添加StepperDelegate协议。 通过在类声明中添加StepperDelegate,我们告诉Stepper该FirstViewController也将包含在StepperDelegate协议中声明的函数的定义。 (在某种程度上,协议是继承的一部分)。 最后,每当计数更改时,Stepper都会使用StepperDelegate协议发送该消息。 协议是应用程序开发中非常有用的东西,如果我们不知道它是如何工作的,也很难解释。 希望您喜欢这篇文章。 编码愉快。

当我说代表时,您说什么?

如果您使用过TableView或CollectionView ,那么您一定听说过“ 委托 ”一词,或者可能已经实现了它。 但是,代表究竟做什么呢? 你能自己做一个吗? 本文试图过分简化委托人的解释,以便您可以理解并自己编写。 让我们从基于Apple文档的报价开始: 委托是一种简单而强大的模式,其中程序中的一个对象代表另一个对象或与另一个对象协同工作。 委托对象保留对另一个对象(委托人)的引用,并在适当的时候向其发送消息。 我们可以参考的最简单的现实生活委派关系是老板和雇员之间的关系,其中一个代表他人。 让我们用代码描述关系。 让我们创建一个虚构的类Boss ,它具有doSpeech()函数, 该类在被调用时使老板为员工做励志演讲。 然后再假设一个虚构的类Employee ,它具有doApplause()函数, 您猜对了,它使员工给予它可以给予的最热情的掌声。 现在,虚构的时刻到了老板去做doSpeech()的时候了 。 老板为此花了整夜的时间做准备,希望他的员工听完他的话后能更有动力。 片刻之后 ,员工们听到了完美的演讲 。 但是有些事情感觉不对劲, 没有人正确地掌声 。 有些人在演讲过程中鼓掌,有些甚至在演讲开始之前就鼓掌,其他人甚至不知道何时应该鼓掌。 老板在拐角处哭泣,知道他的员工没有能力给他“正确”的掌声。 老板不告诉员工什么时候鼓掌 谁该怪? 员工呢 当然不是! 让我们像往常一样责怪系统! 只是开个玩笑 〜老板要怪,因为他从不告诉员工正确的doApplause()时间。 当然,只要老板要求他们掌声,告诉员工doApplause()即可解决此问题。 老板告诉员工何时是应该鼓掌的时候 经过适当的培训后,老板现在很高兴员工知道何时鼓掌。 是的! 任务完成! 但是,尽管我们根本没有提到协议 ,但是对这样的标题颇为讽刺,不是吗? 现在让我们谈谈。 让我们给老板另一个问题,如果老板在员工热烈掌声之后想要doFollowUpSpeech()怎么办? 老板不知道掌声何时结束,他永远也不想错过正确的时机来跟进演讲! 也许员工应该广播通知,将其放在NSNotificationCenter上,然后让老板听,以便他知道进行后续演讲的正确时间。 但是知道老板是一个非常感性的人,他不希望其他人知道掌声的确切计时,他不希望其他人冒出来,接管舞台,并提供更好的跟进讲话。 真麻烦! 老板几乎不知道,员工有一个秘密协议,只有获得信任的人,可以代表他们的人,可以委派他们的人才能知道! (我知道这有点强迫) 员工对获得信任的人有一个秘密协议 更好的是, 无论谁实现此协议 ,例如: […]

快速进行面向协议的编程

你们都有关于POP的知识:面向过程的编程&OOP:面向对象的编程。 如果您了解C,那么您必须了解POP。 我心爱的Objective C也遵循OOP概念。 现在,SWIFT。 它遵循面向协议的编程(Protocol OP)。 有人说Swift使用了太多协议,所以它是Protocol OP。 大声笑。 这绝对是错误的概念。 协议 首先让我们了解协议。 协议不过是接口,或者您可以说它是属性和功能的蓝图,任何类或结构都可以采用它来执行特定任务。 让我们看看它是如何工作的。 协议播放器{ var song:字符串{获取设置} var artist:字符串{获取设置} var audioLink:字符串{获取设置} func play() func pause() } 我创建了一个简单的Player协议 ,该协议具有三个属性和两个功能。 下面,我有一个用Player协议装饰的AudioPlayer类 。 采用协议将迫使您实现它具有的所有属性和功能。 类AudioPlayer:播放器{ var song:字符串 var艺术家:字符串 var audioLink:字符串 init(歌曲:String,艺术家:String,audioLink:String){ self.song =歌曲 self.artist =艺术家 self.audioLink =音频链接 } func play(){ //播放音频的代码 } func pause(){ //暂停音频的代码 }} 只能为类类型设计协议。 […]

在Swift 3中使用代理的简单示例

当我需要快速学习如何使用委托/协议时,我就了解了他们的高级工作(帮助两个对象相互通信),但是却不了解内部工作原理以及如何在代码中创建它们跟我一样容易 对我来说,我决定做一个简单的项目,用一个视图中的textField中的文本更新一个视图上的标签。 如果您想从头开始,则设置非常简单: 两个ViewController(我将其中一个称为SenderVC,将一个称为ReceiverVC) ReceiverVC是初始视图控制器,顶部带有导航栏,一个导航项和一个UILabel位于视图中心。 导航项应该是SenderVC的序号,SenderVC仅具有文本字段和按钮。 而已! 如果您对此有疑问或想直接委托它,我已经创建了主项目的一个分支作为外壳,其中包含所有UI元素: https://github.com/almusto/GoodOleDelegate/tree/Shell 创建协议: 在我们的SenderVC中,我们要创建如下协议: 在此协议中,我声明了一个函数,任何想要成为我的代理人(在本例中为SenderVC的代理人)都必须实现,并且还要创建一个名为UpdateLabelTextDelegate类型的变量? 它将存储要成为我的委托并遵守我的协议的对象。 之所以将委托设置为可选,是因为如果没有人希望成为SenderVC的委托,则该值将为nil。 成为代表: 现在,我们已经创建了协议,让我们转到ReceivingVC并设置我们的视图以成为SenderVC的委托。 让我们从上到下进行检查。 首先,我们在UIViewController旁边添加了UpdateLabelTextDelegate。 所有这些说明我(ReceiveVC)将遵守UpdateLabelTextDelegate(协议)。 现在,ReceiveVC已经做到了这一点,它现在必须实现功能updateLabelText,您可以看到我们正在执行。 回到开始。 此处的目标是使用SenderVC中的textField文本更新ReceiveVC上的标签。 为此,SenderVC希望在委托者上调用协议函数,并将其textField文本作为参数传递。 有道理吧? 因此,如果进入函数的参数将是SenderVC的textField,我们希望将ReceiverVC上的标签文本设置为该参数。 这是您在上面的updateLabelText函数中看到的。 我们在这里做的最后一件事实际上是当我们选择SenderVC时将ReceiverVC设置为委托。 发送文字: 我们要做的最后一件事实际上是发送文本。 的代码如下: 正如我在一开始所提到的,我们在SenderVC上有一个按钮可以返回到ReceiverVC。 按下按钮是发生魔术的地方,它在代码方面没有太多意义,但是让我们逐步介绍一下它以帮助我们了解发生了什么。 通常,您需要在此处添加一些代码以检查是否确实存在委托,并且textField中是否确实存在文本,但是对于这个简单的示例,我们不需要这样做。 如果您回想起我们连接到SenderVC的最后一步,则将ReceiverVC设置为委托。 因此,我们知道ReceiverVC已经实现了updateLabelText函数。 在这里,我们只是调用委托的(ReceiverVC)的updateLabelText函数,并将该参数传递给textField的文本。 代码的最后一行只是关闭了视图,因此我们可以回到ReceiverVC。 就是这样! 现在,ReceiverVC上的标签应该具有您在SenderVC上的textField中输入的文本。 完成的项目位于此处: https://github.com/almusto/GoodOleDelegate

协议关联类型-PAT

简而言之,我们都知道面向协议的编程的功能。 要在蛋糕上添加樱桃,我们在协议中提供了“关联类型”。 让我们创建一个简单的Stack功能: 协议栈{var count:Int {get}变异func push(item:Int)变异func pop()-> Int}结构容器:Stack {var items = [Int]() var count:Int { 返回items.count }更改func push(item:Int){ items.append(item) }更改func pop()-> Int { 返回items.removeLast() } } var intContainer =容器(项目:[]) intContainer.push(项目:10) intContainer.push(项目:20) intContainer.push(项目:30) intContainer.push(项目:40) intContainer.push(项目:50)print(intContainer.count) 打印(intContainer.pop()) 但是,这对我们当前的Stack协议很有用,但是有一个主要的缺点。 只有处理Int的容器才能符合此协议。 我们可以使用“关联类型”功能来消除此限制。 关联类型像泛型一样工作。 为了演示,让我们更改Stack协议以利用关联的类型: 协议栈{ 关联类型ItemType var count:Int {get} 变异func push(项目:ItemType) 变异func pop()-> ItemType } 现在,堆栈协议允许存储任何类型的项目(整数,字符串或任何其他自定义类型) struct Container […]

Swift Universe中的迭代器设计模式

(本文最初是用俄语撰写的,并在此处发布。) 迭代器是设计模式之一,通常不为程序员所注意,因为其实现细节通常嵌入在编程语言的标准库中。 但是,它也是四人帮经典著作“设计模式:可重用的面向对象软件的元素”中描述的行为模式之一。 它的理解永远不会被淘汰,甚至可能会有所帮助。 迭代器是一种提供对复合对象的所有元素(通常是容器类型,例如数组和集合)的串行访问的方法。 语言内置资源 创建一个数组: 让numbersArray = [0,1,2] …并循环遍历: 代表numbersArray { 打印(数量) } ……这是很常见的事情,尤其是对于像Swift这样的现代编程语言而言。 但是,此功能由实现Iterator设计模式基础的代码支持。 在Swift中,一种类型必须符合Sequence协议才能被for循环迭代。 除其他事项外,此协议要求具有关联的Iterator类型(必须符合IteratorProtocol )并实现结构方法makeIterator() (此类型返回此类型的特定迭代器): 协议序列{ relatedtype Iterator:IteratorProtocol func makeIterator()-> Self.Iterator //另一个要求在这里… } IteratorProtocol包含唯一的方法— next() ,该方法按顺序返回以下对象: 协议IteratorProtocol { 关联类型元素 变异func next()-> Self.Element吗? } 感觉好像很多复杂的代码,但实际上并非如此。 我们将在一段时间后确定。 例如, Array转换为Sequence (虽然不是直接,但通过协议继承链: MutableCollection继承自Collection ,而Collection继承自Sequence ),这就是为什么可以通过for循环来标识其实例的原因。 用户类型 如果我们的类型必须是可迭代的,我们该怎么办? 像往常一样,显示示例更容易。 让我们定义用于存储书本的书架的类型: 结构书{ 让作者:字符串 让标题:字符串 } […]

如何使用协议构建具有多种单元格类型的TableView

了解UITableViews(和UICollectionViews)作为iOS开发人员的工作方式就像了解盐和胡椒如何做饭一样。 它们是大多数受欢迎的应用程序中的一部分,尽管使用起来相对简单,但我们可以对其实现进行真正的复杂化。 由于在UIKit中使用了协议,因此实现简单的UITableView可以非常简单。 我们需要将数据连接到UITableVIew的主要协议是:UITableViewDataSource和UITableViewDelegate。 但是,这意味着我们的ViewController符合这些协议是什么意思? (他们为什么被任命为代表?) 有趣的是,这个词在我们考虑时会有所解释。 符合协议的东西就是满足协议要求的东西,有点像诺言。 因此,实质上,当我们说ViewController符合UITableView协议时,我们保证ViewController将具有指定的变量并能够执行指定的方法 。 查看UITableViewDataSource协议,我们看到: 从文档中我们可以看到,我们所需要做的就是实现两个必需的功能以满足协议。 那么代表来自哪里呢? 当我们委派某些东西时,我们将责任转移给了其他人。 例如,朋友A可以将扔垃圾的任务委托给朋友B。朋友A不一定关心朋友B如何摆脱垃圾,他们只需要知道朋友B有能力扔垃圾即可。 。 将其带回到iOS开发中,实质上,我们是在声明一种方法,该方法是我们要委派给实现职责的一种操作。 这是有道理的,因为并非所有朋友都喜欢以相同的方式扔垃圾。 当我们想让UITableViews变得复杂时,我们可以创建多个原型单元。 对于诸如FaceBook feed这样的事情,您可能具有不同的帖子类型(新照片,共享视频等),这是有意义的。 那么,当所有这些不同的单元接收不同的数据并进行不同的配置时,我们如何管理它们呢? 当我第一次遇到这个问题时,我最初的想法是在cellForRowAt方法中有一个巨大的switch语句,例如: switch语句定义了扩展困难的最大原因。 围绕枚举构建的逻辑要求对跨多个对象共享的模型进行更改。 我们可以通过协议和一些强制转换来简化此过程。 tableView函数dequeueReusableCell具有保护语句,因为我们试图将单元格强制转换为特定类型。 这是确保单元格类型正确的安全方法,但在失败时也会使我们处于怪异状态。 如果方法dequeueReusableCell无法找到指定的单元格,则将出现fatalError。 这样做很大程度上有助于了解应用程序的状态,因为不希望返回默认单元格。 那好多了,但是我们仍然停留在那个奇怪的switch语句上。 让我们尝试使用协议删除它。 这是我们回到扔垃圾的时候。 如果我们看一下cellForRowAt方法内部的单元格在做什么,它们都是在配置。 我们可以将单元格转换为符合协议的对象吗? 我们要告诉单元执行类似的操作,更新单元的信息。 首先,这是一个棘手的例子: ViewController ViewController通常是UITableView的委托/数据源,dataSource是提供数据的源,在这种情况下,我们将引用CustomElementModels数组。 请注意,这是符合CustomElementModel的对象数组。 CustomElementModel协议 在这里,您可以放置​​所有模型的所有必需变量和函数。 在这里,我只是指定我希望每个模型至少告诉我它是什么类型,将类型限制为枚举:C ustomElementType。 CustomElementCell协议 同样,在这里您可以将所有必需的内容放到所有单元格中,在这种情况下,我只想确保每个符合CustomElementCell协议的对象都具有一个configure函数,该函数接受一个符合CustomElementModel的对象。 现在,我们基本上可以使用我们知道每个模型具有的类型作为变量来确定UITableView中的单元格标识符,然后将其强制转换为符合CustomElementCell的对象。 我知道你们中的一些人可能会吓坏了,我强迫很多,别担心。 我知道这很吓人,但这是可以强制施放的例子之一,这就是原因。 就像我之前说过的那样,如果dequeueReusableCell无法找到具有指定参数的单元格,则会引发致命错误。 如果我们成功地使一个单元出队,是否有任何场景可以按预期方式将其成功投射为另一个单元? 在开发中,您希望强制转换该单元格,因为您希望它是该类型 ,如果将其转换为您不期望的另一个单元格,则可以在应用程序中引入更多错误。 那最后一行呢? 返回customCell为! […]