Showing Posts From
Ui
iOS开发图片格式选择
图片是如何显示的 在讲解如何选择图片格式之前,我感觉有必要先了解下,图片是如何展示的。如果我们要展示一张图片,一般步骤是这样的: /// Assets.xcassets中的图片,不需要后缀 let image = UIImage(named: "icon") let imageView = UIImageView(frame: rect) imageView.image = image view.addSubview(imageView)运行程序,我们就可以在指定位置看到这个icon。看似简单的代码背后隐藏了很多细节工作。一张图片的展示,从代码执行到展示出来大致经历了这些步骤:1. 加载图片从磁盘中加载一张图片;然后将生成的 UIImage 赋值给 UIImageView ;接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;分配内存缓冲区用于管理文件 IO 和解压缩操作,将文件数据从磁盘读到内存中;2. 图片解码(解压)将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作,默认在主线程进行;3. 图片渲染Core Animation 中CALayer使用解压(解码)的位图数据渲染 UIImageView 的图层;CPU计算好图片的Frame,对图片解压之后,就会交给GPU来做图片渲染渲染流程;GPU获取获取图片的坐标,将坐标交给顶点着色器(顶点计算),将图片光栅化(获取图片对应屏幕上的像素点),片元着色器计算(计算每个像素点的最终显示的颜色值);从帧缓存区中渲染到屏幕上;这其中有个关键步骤是图片解码。那为什么要解码呢,这是因为我们平常使用的图片一般为了节约空间都会经过一些压缩算法进行封装,而使用时屏幕要精确的渲染到每个像素点,这就需要把压缩的图片解码展开,便于系统处理。 名词解释 有损vs无损 有损压缩:指在压缩文件大小的过程中,损失了一部分图片的信息,也即降低了图片的质量,并且这种损失是不可逆的,我们不可能从有一个有损压缩过的图片中恢复出全来的图片。常见的有损压缩手段,是按照一定的算法将临近的像素点进行合并。 无损压缩:只在压缩文件大小的过程中,图片的质量没有任何损耗。我们任何时候都可以从无损压缩过的图片中恢复出原来的信息。 索引色vs直接色 索引色:用一个数字来代表(索引)一种颜色,在存储图片的时候,存储一个数字的组合,同时存储数字到图片颜色的映射。这种方式只能存储有限种颜色,通常是256种颜色,对应到计算机系统中,使用一个字节的数字来索引一种颜色。 直接色:使用四个数字来代表一种颜色,这四个数字分别代表这个颜色中红色、绿色、蓝色以及透明度。现在流行的显示设备可以在这四个维度分别支持256种变化,所以直接色可以表示2的32次方种颜色。当然并非所有的直接色都支持这么多种,为压缩空间使用,有可能只有表达红、绿、蓝的三个数字,每个数字也可能不支持256种变化之多。 点阵图vs矢量图 点阵图:也叫做位图,像素图。构成点阵图的最小单位是象素,位图就是由象素阵列的排列来实现其显示效果的,每个象素有自己的颜色信息,在对位图图像进行编辑操作的时候,可操作的对象是每个象素,我们可以改变图像的色相、饱和度、明度,从而改变图像的显示效果。点阵图缩放会失真,用最近非常流行的沙画来比喻最恰当不过,当你从远处看的时候,画面细腻多彩,但是当你靠的非常近的时候,你就能看到组成画面的每粒沙子以及每个沙粒的颜色。 矢量图:也叫做向量图。矢量图并不纪录画面上每一点的信息,而是纪录了元素形状及颜色的算法,当你打开一张矢量图的时候,软件对图形象对应的函数进行运算,将运算结果[图形的形状和颜色]显示给你看。无论显示画面是大还是小,画面上的对象对应的算法是不变的,所以,即使对画面进行倍数相当大的缩放,其显示效果仍然相同(不失真)。 几种格式的对比 一张图片,如果我们将它的每一个像素及其对应的颜色都存储起来(BMP格式),是会很大的。为了减小图片占用的存储空间,派生出了各种不同压缩算法所代表的图片格式。常见的图片格式有png、jpeg、heic、gif、webp,svg等。 PNG PNG有两种类型:PNG-8和PNG-24。PNG-8是无损的、索引色、点阵图。它支持透明度的调节。PNG-24是无损的、直接色、点阵图。因为使用直接色,颜色范围更大,也占用更大的空间。他的目标是替代JPEG,但一般而言,PNG-24的文件大小是JPEG的五倍之多,而显示效果则通常只能获得一点点提升。所以如果不是对图片质量要求特别高,不建议使用PNG-24PNG是苹果推荐的图片格式,而且这些图片会被一个叫pngcrush的开源工具优化,这样iOS设备就能在显示时更快的解压和渲染图片。该工具位于目录: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/binJPEG JPEG是有损的、采用直接色的、点阵图压缩方式。 JEPG目标是在不影响人类可分辨的图片质量的前提下,尽可能的压缩文件大小。一般都是用于相机图片的格式。但因为有损,会导致图片失真。iOS可以通过以下方式压缩图片: // 压缩比范围从0到1 func jpegData(compressionQuality: CGFloat) -> Data?题外话 一般来说,相同的图片采用JPEG的压缩方式会比png得到更小的尺寸,但也有例外。在网上查了资料说是JEPG更适合处理带有很多杂色的风景图,而对于使用数位板等电子绘制的纯色卡通系风格图片,JEPG的压缩方式会适得其反,导致体积更大。 HEIC HEIC是HEIF(High Efficiency Image Format 高效率图像文件格式)的一种。它并非苹果开发,而是由运动图像专家组(MPEG)开发。它同时支持有损压缩、无损压缩、透明度等特性。HEIF规范的完成是在2015年,是这几种图片格式中最新的一种了,目前除了苹果,还没有哪家大厂去拥抱这种格式。 在iOS 11更新后,iPhone 7及其后硬件,在拍摄照片时的默认图像存储格式。与JPG相比,它占用的空间更小,画质更加无损。HEIC的目的就是作为JPEG的继任者,以后或许会成为一种趋势。目前可以想到的在开发中的应用是,对于一些需要下载的大图可以转成HEIC格式,供客户端使用。但是当前却很少应用,大概率是考虑到图片兼容问题吧。 题外话 一个有趣的现象,我用相机(iPhoneXR)拍摄一张照片,通过AirDrop传到电脑,显示为HEIC格式。当我在拍照时选择系统自带的任意一种滤镜,图片格式就变成了JPEG。这是为什么? 有小伙伴解答: iOS拍照选择滤镜会“转”为JPEG,是因为拍照的格式还是HEIF,加滤镜和编辑图片都是相当于复制了一份再做操作的,点击复原又会“转”为HEIF。 经过测试确实是这样的,而且既然苹果提供复原的操作,说明原图(HEIF)并没有被覆盖。那为什么滤镜不能直接在HEIF格式下操作,猜测可能是跟滤镜的算法相关,该算法只能对JEPG格式编码的图片进行渲染,所以需要中间转成JEPG。 Live Photo Live图片的实质是:一张heic格式封面图 + mov格式视频。 对于Live Photo的展示,在原生应用中可以使用PHLivePhotoView,在Web应用中可以使用LivePhotosKit JS。 WebP WebP最初由Google发布于2010年,图片格式派生自VP8视频编码,也同时支持有损压缩、无损压缩、透明度等特性。2013年低,推出了Animated WebP,还可以支持动图。 WebP 集合了多种图片文件格式的特点。它像 JPEG 一样适合压缩照片和其他细节丰富的图片,像 GIF 一样可以显示动态图片,像 PNG 一样支持透明图像。根据 Google 的测试,WebP 无损压缩图片比 PNG 图片少了 45% 的文件体积,即使这些 PNG 图片在使用 pngcrush 和 PNGOUT 处理后,WebP 依旧可以减少 28% 的文件体积。可以在点击这里查看WebP对其它格式转换的效果。小是WebP的最大优点,小意味着更少的流量,这也是各大流量入口在意的地方。目前Google、Facebook、阿里等大厂已经在广泛使用WebP格式,国内的一些图床服务(七牛、又拍云)也支持将图片自动转成WebP格式。 诚然WebP非常优秀,独自完成了图片格式"大一统"的任务。但苹果对WebP的支持却很少,只Safari目前还不支持WebP显示就阻断了很多人应用WebP的决心。 如果我们需要在项目中显示WebP格式图片就不得不导入Google的libwebp解码库。当然WebP的解码任务在iOS端有些库已经封装好了,OC端可以用SDWebImageWebPCoder,Swift端可以用KingfisherWebP。以下是使用Kingfisher展示WebP图像的事例: // 全局配置对WebP图片的解码(仅针对WebP格式) KingfisherManager.shared.defaultOptions += [ .processor(WebPProcessor.default), .cacheSerializer(WebPSerializer.default) ] // 本地webp图片解码 let localUrl = Bundle.main.url(forResource: "sunset", withExtension: "webp") let dataProvider = LocalFileImageDataProvider.init(fileURL: localUrl!) imageView.kf.setImage(with: dataProvider)// 远程webp图片解码。一些图像服务器可能期望“Accept”标头包含“image/webp”,我们还需要加上 let modifier = AnyModifier { request in var req = request req.addValue("image/webp */*", forHTTPHeaderField: "Accept") return req } KingfisherManager.shared.defaultOptions += [ .requestModifier(modifier), // ... other options ]PDF pdf图片通常是矢量的,它的导入方式有些特殊。我们需要在Assets.xcassets文件,创建一个New Image Set,然后将该文件的Scales设置为Single Scale,拖入1x尺寸的pdf文件即可:使用时我们可以把它当做普通图片对待: let image = UIImage(named: "sunset")在运行期间Xcode会根据屏幕的比例因子生成对应尺寸的png图像。比如导入一张100x100的pdf图片,在2x和3x的机型里面会生成对应的200x200,300x300的png(可以在Assets.car中找到)。所以pdf只不过是Xcode处理图片的中间状态,下载到手机的应用包里面是没有这张pdf的。 这种处理方式有一个好处就是,当苹果以后发布一款4x屏幕的手机时,使用pdf处理的图片会自适应生成对应的4x资源,不需要再手动导入。但相比优点,pdf作为图片资源的缺点更多。 首先是尺寸上,因为是自动生成对应的png,并没有任何优化和压缩,而且我们也并不能在这中间做什么。对比相同尺寸经过ImageOptim压缩过的png,在大小上后者会是前者的1/2,甚至1/4。 另外pdf对阴影和渐变的处理会存在失真的情况:左边是png,右边是pdf。在一些渐变和光影的图像部分可以看出明显的失真。 更多关于pdf和png的差别,可以看这篇:Why I don't use PDFs for iOS assets: https://bjango.com/articles/idontusepdfs/ SVG/SF Symbol SVG是一种无损的矢量图,是众多矢量图中的一种,它的特点是使用XML来描述图片。使用XML的优点是,任何时候你都可以把它当做一个文本文件来对待,也就是说,你可以非常方便的修改SVG图片,你所需要的只需要一个文本编辑器。 在iOS13之前应用中直接使用SVG的场景非常少,但从iOS13开始,苹果推出了SF Symbol,一种svg格式的矢量符号集。而且苹果还提供了多于1500多种icon模板,我们可以在这里下载查看。我们可以从中选择适合自己的icon,选中之后,从File > Export Custom Symbol Templete中导出svg格式图片集,然后拖到Xcode的Assets.xcassets。SF Symbol有9种粗细的调节——从ultralight到black——每一种都相当于San Francisco系统字体的重量(weight)。(SF Symbol中的SF是San Francisco(旧金山)的缩写)。这种对应使您能够在符号和相邻文本之间实现精确的权重匹配,同时支持不同大小和上下文的灵活性。 当然如果这些图标都不能满足需求,我们还可以自定义SF图标,然后通过SF Symbol App进行验证和导出。操作细节可以看这里:Creating Custom Symbol Images for Your App。 SF Symbol使用起来也很简单: let configuration = UIImage.SymbolConfiguration.init(scale: .small) imageView.image = UIImage(systemName: "alarm", withConfiguration: configuration)SF Symbol可以一次性解决相同icon,不同尺寸,不同粗细的问题,它让我们处理图片像处理字体一样方便。可以想象这就是应用图标的未来。 当看到SF Symbol仅支持iOS13+,watchOS6+,我又不得不退回到现实,png也挺好的。 题外话 我在测试SF Symbol图标时,从生成的应用包中查看图片,会得到这样的结果:代码中的我将图片设置为100x100,仅有这一处地方使用。跟pdf类似我们找不到svg源文件,这好理解,svg只是中间状态,我们最终使用的还是png,但为什么会有多个小尺寸的png图像呢? 如何选择图片格式 我们平常开发时,使用最多的就是png了,甚至可能是不加考虑的全部使用png。其实这样是不好的,我们应该充分发挥不同格式图片的优点,从兼容性、空间占用、展示效果三方面考量选取最佳格式。 关于图片格式的选择,苹果的Human Interface Guidelines有以下说法:一般情况下,使用PNG图片,因为PNG支持透明性,而且是无损的,压缩工件不会模糊重要的细节或改变颜色。对于需要阴影、纹理和高光效果的复杂艺术品来说,这是一个不错的选择。使用8位的PNG图形,不需要完全24位的颜色。8位PNG可以在不降低图像质量的情况下减小文件大小。精致的应用图标最好使用png。 对于照片应该使用JPEG格式,它的压缩算法通常比无损格式产生更小的尺寸,而且很难在照片中辨别出来。应该尝试优化JPEG文件,在大小和质量之间找到平衡。大多数JPEG文件可以被压缩而不会导致图像明显的退化。即使是少量的压缩也可以节省大量的磁盘空间。 使用PDF处理字形和其他需要高分辨率缩放的平面矢量图形。最终可以做以下总结。图片格式 适用范围 注意事项png 应用icon,界面icon,卡通风格的背景图 导入项目前可以使用ImageOptim进行压缩jpeg 尺寸较大的风景图,照片 不支持透明度;因为可以调节压缩比,可以在大小和质量之间寻找最佳平衡。webp 支持有损、无损压缩、透明度、动图等特性,因为苹果本身不支持一般只应用于服务端返回来的图片 无法在xcode预览,不建议内置该类型图片pdf 字形,高分辨率的矢量图 存在展开尺寸较大,光效失真的情况svg(sf symbol) 指示性icon 仅支持iOS13及以上,系统sf符号是有版权的,使用时要注意应用范围和苹果要求引用 谈谈 iOS 中图片的解压缩 图片格式那么多,哪种更适合你 iOS图片格式选择 Why I don't use PDFs for iOS assets SF Symbols: The benefits and how to use them guide WebP 的前世今生
【译】iOS 架构模式--浅析MVC, MVP, MVVM 和 VIPER
作者:Bohdan Orlov 原文地址:https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52在iOS开发过程中你是否对MVC的使用感觉很别扭?你是否对转向MVVM有疑惑?你听说过VIPER,但不清楚这个东西是否值得一试。 接着读下去,你会找到上面这些问题的答案。如果读完仍不能解惑,欢迎到评论区捶我。 接下来你将在iOS环境下构建关于架构模式的知识体系。我们将简要构建一些经典的例子,并在理论和实践上进行比较他们的不同。如果你需要更多关于任何一个特定的细节,请关注我。掌握设计模式容易让人上瘾,所以要注意:阅读本文之前要问自己几个问题:谁应该持有网络请求:Model还是Controller? 如何在一个新的View中向ViewModel传递Model 谁创建了一个新的VIPER模块:Router还是Presenter为什么应该关心选用何种架构 因为如果你不这么做的话,总有一天,等这个类庞大到同时处理十几种事务,你会发现你根本无法从中找到对应代码并修改bug。当然,将这整个类了然于心是很难的,你会常常忘记一些重要的细节。如果你的程序已经处于这种状态了,那它很可能具有下面这些特征:这个类是UIViewController的子类 你的数据直接在UIViewController中进行存储 你的UIView基本什么都没做 你的Model只是一个单纯的数据结构 你的单元测试没有覆盖任何代码即使你认为自己遵守了苹果的指导,并按照苹果推荐的MVC设计规范进行开发,但还是遇到了这些问题。不要担心,这是因为苹果的MVC本身就存在一些问题,我们稍后会再来讨论它。 让我们定义一下好的架构应该具备的特点: 1、平衡的分配实体和具体角色的职责 2、把可测试性放在第一位(通过合适的架构,这将很容易实现) 3、易用性和低维护成本 为什么要分配职责 职责的分配能让我们在尝试搞清楚事情如何运作这一过程中保持一个正常的负荷。你可能会认为你投入的精力越多你的大脑越能适应更加复杂的东西,这没错。但是这个能力是非线性的,而且会很快达到临界点。所以降低复杂性的最好的方式是,根据职责单一原则将它的功能(职责)分配到多个实体中。 为什么要可测试性 对于那些已经添加了单元测试的项目来说,当他们增加一个新的功能或者重构一个复杂的类时会由单元测试告知失败与否,这多让人很放心啊。同时这也意味着这些测试项将在运行时帮助开发者找到问题,而如果这些问题发生在用户设备上的,解决他们通常会花费一周。 为什么要易用性 这并不需要答案,但值得一提的是,最好的代码是那些从未写过的代码。所以,代码越少,bug就越少。这意味着,编写更少代码的愿望不应该仅仅由开发人员的懒惰来解释,而且你不应该为了采用更好的解决方案,而对其维护成本视而不见。 MV(X)的要素 如今我们又很多可选的架构方案:MVC MVP MVVM VIPER 前三项方案是把应用程序的实体分为三类: Modes -- 负责数据域和操作数据的数据访问层,例如'Person’类, 'PersonDataProvider'类。 Views -- 负责展示层(GUI),对于iOS环境就是指所有已‘UI’开头的类。 Controller/Presenter/ViewModel -- 是Model和View的中介,通常的职责是通过响应用户在View的操作改变Model,然后根据Model的变化更新View。这些实体的分割帮助我们:更好的理解他们 重用他们(通常是View和Model) 单独测试他们让我们开始讲解MV(X)模式,随后是VIPER MVC 它原本是什么样的 在讨论苹果的MVC版本之前,让我们看一下传统的MVC模式:这个模式下,View是无状态的。它只是简单的被Controller渲染当Model变化的时候。想一下Web页面,当你点一个链接尝试跳转时,整个页面都会重新渲染。尽管可以在iOS应用程序中实现传统的MVC,但由于架构问题,这并没有多大意义—— 所有三个实体都是紧密耦合的,每个实体都知道其他两个。这正好降低了他们的重用性,而这又是你在程序中不想看到的。因为这个原因,我们将跳过编写传统MVC代码的示例。传统的MVC似乎不适合现代的iOS开发苹果的MVC 预期效果Controller是View和Model的中介,因此它俩互相不知道对方。可重用性最差的就是Controller,因为我们必须为复杂的业务逻辑提供一个位置,Model又不适合。 理论上,这看起来很简单,但是你总感觉有什么地方不对,是吧?你甚至听到人们解读MVC为Massive View Controller。也因此,视图控制器的简化成了iOS开发一个重要的课题。苹果只是采用传统的MVC并对其进行一些改进,为什么会出现这种情况呢? 现实情况Cocoa MVC鼓励你编写大量的视图控制器,因为它们是视图生命周期的一部分,很难说它们是独立的。尽管你有能力转移一些业务逻辑和数据转换工作到Model中,当需要转移工作到View时你仍然没有太多选择,因为大多数情况View的职责就是发送行为到Controller。视图控制器最终将成为一个所有东西的委托、数据源、负责调度和取消网络请求,等等。 这种代码,你肯定见过很多少次了: var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell userCell.configureWithUser(user)这个cell是View,直接通过Model进行配置,这明显违反了MVC的要求,但这种事情却经常发生,而且认为还不认为这是错的。如果你严格按照MVC的 做法,你应该在Controller里面配置cell,而不是将Model直接传递给View,但这样就会增加Controller的大小。Cocoa MVC 被称为 Massive View Controller是多么合理啊。这个问题还不是那么明显,直到提到单元测试(希望它存在于你的项目)。因为你的视图控制器跟View是紧耦合的,这将使得测试非常困难。所以你应该让你的业务逻辑和视图布局代码尽可能分割开来。 让我们看一个简单的例子: import UIKitstruct Person { // Model let firstName: String let lastName: String }class GreetingViewController : UIViewController { // View + Controller var person: Person! let showGreetingButton = UIButton() let greetingLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside) } func didTapButton(button: UIButton) { let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName self.greetingLabel.text = greeting } // layout code goes here } // Assembling of MVC let model = Person(firstName: "David", lastName: "Blaine") let view = GreetingViewController()这样根本没法测试,对吧?我们可以把greeting的赋值移到一个新的类GreetingModel中,然后分开测试它。但是我们无法在不直接调用UIView相关方法(viewDidLoad, didTapButton)的情况下测试任何外在的逻辑,而如果这样做,这些方法就导致所有view的刷新,所以这本身就是一个不好的单元测试。 事实上,在一个模拟器上加载和测试UIViews表现正常,不代表它在别的设备依然这样。所以我建议测试时移除单元测试对“宿主程序”的依赖,而直接测试代码。View和Controller之间的交互行为无法通过Unit Tests进行。根据上面的说法,Cocoa MVC是一个相当不好的架构方案。让我们再来根据文章开头定义的好架构应有的特性来评价下它:职责分离 -- View和Model是分离的,但View和Controller是紧耦合关系。 可测试性 -- 由于不好的职责分离特性,只有Model层是可以测试的。 易用性 -- 这几种架构模式中它的代码量是最少的。每个人都很熟悉这种模式,即使是一个经验有限的开发者也可以很容易的维护这份代码。如果你不打算投入很多事情在架构上,或者你感觉对于你们的小项目来说不值得投入过多维护成本,那你应该选择Cocoa MVC。Cocoa MVC 是开发速度最快的一种架构。MVP这是不是更苹果的MVC非常像?确实是这样的,它的名字叫做MVP。等一下,这是不是意味着苹果的MVC事实上就是MVP?不。你可以再观察下这个结构,View和Controller是紧耦合关系,作为MVP的中介者 -- Presenter并没有管理视图控制器的生命周期,它里面也不含有布局代码,它的职责是根据数据的状态变化更新View,所以呢,View这一层就可以很简单的抽出来。我会告诉你,UIViewController也是View在MVP模式下,UIViewController的子类实际上是Views而不是Presenters。这种区别提供了极好的可测试性,但这是以开发速度为代价的,因为你必须手动绑定数据和时间,就像这个例子: import UIKitstruct Person { // Model let firstName: String let lastName: String }protocol GreetingView: class { func setGreeting(greeting: String) }protocol GreetingViewPresenter { init(view: GreetingView, person: Person) func showGreeting() }class GreetingPresenter : GreetingViewPresenter { unowned let view: GreetingView let person: Person required init(view: GreetingView, person: Person) { self.view = view self.person = person } func showGreeting() { let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName self.view.setGreeting(greeting) } }class GreetingViewController : UIViewController, GreetingView { var presenter: GreetingViewPresenter! let showGreetingButton = UIButton() let greetingLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside) } func didTapButton(button: UIButton) { self.presenter.showGreeting() } func setGreeting(greeting: String) { self.greetingLabel.text = greeting } // layout code goes here }关于装配(assembly)的重要说明 MVP是第一个揭露三层模型装配问题的模式。我们不想让View和Model互通,因为在试图控制器(View)中执行装配操作是明显不对的,所以我们只能换个地方放装配的代码。例如,我们可以做一个应用范围的Router服务,它负责装配工作和View到View的展示。这个问题的出现不仅要在MVP中解决,在以下的几个模式中也都要解决。 我们看下MVP的特性:职责分离 -- 我们将大部分职责分配给了Presenter和Model,而视图则什么都不需要做(上面的Model也是什么都不用做) 可测试性 -- 非常好,我们可以通过静态的View测试大多数逻辑。 易用性 -- 在我们上个简单示例中,代码量是MVC的两倍,但是它的逻辑是很清晰的。在iOS中MVP模式意味着良好的可测试性和大量代码。MVP这是另一个MVP的样式 -- 由视同控制器担当管理的MVP。这个变体中,View和Model是直接绑定的,Presenter(担当管理的控制器)仍然处理着来自View的操作,并且能够改变View。 但是通过上面的学习我们已经知道了,将View和Model紧耦合处理,这种不明确的职责分离是很糟糕的。这与Cocoa桌面开发中的工作原理类似。 跟传统MVC一样,我找不到要为这个有缺陷的架构写示例的理由。 MVVM 最新而且是最好的一个MV(X)类型 MVVM是最新的MV(X)类型,希望它能解决我们之前讨论过的问题。 MVVM理论上看起来是很好的,View和Model我们已经很熟悉了,它俩之间的中介者由View Model表示。这和MVP很像:MVVM也是把视图控制器当做View 在View和Model之间没有紧耦合关系此外它的绑定逻辑很像MVP的监管版本;但是这次不是View和Model,而是View和View Model之间的绑定。 所以iOS当中的View Model到底是什么呢?它是UIKit独立于视图及其状态的表示。View Model调用Model执行更改,然后根据Model的更新再更新自己,因为我们绑定了View和View Model,第一个模型将相应的更新。 绑定 我在MVP部分明确提到过绑定,这次让我们再来讨论一下。绑定出自于MacOS开发,在iOS中是没有的。我们虽然可以通过KVO和通知完成这一过程,但是这样的绑定方式并不方便。 如果我们不想自己实现的话,有两个选项可供参考:一个是基于KVO的绑定库像RZDataBinding,SwiftBond 完整的函数式编程工具,像ReactiveCocoa, RxSwift, PromiseKit。如今当你听到“MVVM”,就应该想到ReactiveCocoa。因为它可以让你用很简单的绑定方式构建MVVM,几乎涵盖所有MVVM中的逻辑。 但是使用响应式框架会面临一个不好的现实:能力越大责任越大。使用reactive很容易将事情复杂化。也就是说,如果发生了一处错误,你需要花费很多时间去调试问题,可以简单看下响应式的调用堆栈。在我们的示例中,响应式框架甚至KVO都是多余的,我们将使用showGreeting方法显式地要求View Model更新,并使用greetingDidChange回调函数的简单属性。 import UIKitstruct Person { // Model let firstName: String let lastName: String }protocol GreetingViewModelProtocol: class { var greeting: String? { get } var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change init(person: Person) func showGreeting() }class GreetingViewModel : GreetingViewModelProtocol { let person: Person var greeting: String? { didSet { self.greetingDidChange?(self) } } var greetingDidChange: ((GreetingViewModelProtocol) -> ())? required init(person: Person) { self.person = person } func showGreeting() { self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName } }class GreetingViewController : UIViewController { var viewModel: GreetingViewModelProtocol! { didSet { self.viewModel.greetingDidChange = { [unowned self] viewModel in self.greetingLabel.text = viewModel.greeting } } } let showGreetingButton = UIButton() let greetingLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside) } // layout code goes here } // Assembling of MVVM再次回来用这三个特征验证一下:职责分离 -- 这在我们的小示例还无法体现,但是MVVM的View比着MVP的View有着更多的职责。因为前者通过View Model建立绑定更新它的状态,后者仅仅是把所有事件都交给Presenter,不更新自己。 可测试性 -- View Model是不知道View的,这可以让我们很容易的对它进行测试。View可能也需要测试,但因为它依赖UIKit,你可能想跳过它。 易用性 -- 它有河MVP模式相同的代码量,但是实际项目中,你不得不把所有事件通过View传给Presenter,然后还要手动更新View,比较而言,MVVM使用绑定将更加简洁。MVVM是很有吸引力的,因为它包含了前面提到的优点,此外通过View层的绑定,也不需要额外的代码处理View更新。测试性也还不错。VIPER 把乐高的搭建流程应用到iOS设计模式 VIPER是我们最后一个候选模式,有趣的一点是它不属于MV(X)类型。 目前为止,你必须同意职责的粒度是很重要的。VIPER在划分职责层面又做了一次迭代,它将项目划分成5层。Interactor-- 包含跟数据(Entities)和网络相关的业务逻辑,像是创建新的实例对象后者从服务器拉取数据。出于这些目的,你也可以使用Services和Mananger类完成功能,但这就不属于VIPER模块,而是外部依赖类。 Presenter -- 包含UI相关的业务逻辑,调用Interactor中的方法。 Entities -- 普通的数据对象,不是数据访问层,因为这是Interactor的责任。 Router -- 负责VIPER模块之间的切换。基本上,VIPER模块可以是一整屏内容,也可以是你应用中完整的用户行为 -- 想一下授权行为,它可以在一个或者几个相关联的界面。“乐高”方块应该多小呢?这取决于你。 如果我们将它和MV(X)类比,会发现一些在职责划分上的区别:Model(数据交互)逻辑转移到了包含Entities数据结构的Interactor中。 只有Controller/Presenter/ViewModel这种UI表示层的职责转移到了Presenter中,不包含数据。 VIPER是第一个明确导航职责的模式,并通过Router解决这个问题。在iOS应用中用一个优雅的方式处理跳转问题确实是一个挑战,MV(X)模式没有处理这个问题。该示例不涉及模块之间的路由或交互,因为MV(X)模式根本不涉及这些主题。 import UIKitstruct Person { // Entity (usually more complex e.g. NSManagedObject) let firstName: String let lastName: String }struct GreetingData { // Transport data structure (not Entity) let greeting: String let subject: String }protocol GreetingProvider { func provideGreetingData() }protocol GreetingOutput: class { func receiveGreetingData(greetingData: GreetingData) }class GreetingInteractor : GreetingProvider { weak var output: GreetingOutput! func provideGreetingData() { let person = Person(firstName: "David", lastName: "Blaine") // usually comes from data access layer let subject = person.firstName + " " + person.lastName let greeting = GreetingData(greeting: "Hello", subject: subject) self.output.receiveGreetingData(greeting) } }protocol GreetingViewEventHandler { func didTapShowGreetingButton() }protocol GreetingView: class { func setGreeting(greeting: String) }class GreetingPresenter : GreetingOutput, GreetingViewEventHandler { weak var view: GreetingView! var greetingProvider: GreetingProvider! func didTapShowGreetingButton() { self.greetingProvider.provideGreetingData() } func receiveGreetingData(greetingData: GreetingData) { let greeting = greetingData.greeting + " " + greetingData.subject self.view.setGreeting(greeting) } }class GreetingViewController : UIViewController, GreetingView { var eventHandler: GreetingViewEventHandler! let showGreetingButton = UIButton() let greetingLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside) } func didTapButton(button: UIButton) { self.eventHandler.didTapShowGreetingButton() } func setGreeting(greeting: String) { self.greetingLabel.text = greeting } // layout code goes here }// Assembling of VIPER module, without Router let view = GreetingViewController() let presenter = GreetingPresenter() let interactor = GreetingInteractor() view.eventHandler = presenter presenter.view = view presenter.greetingProvider = interactor让我们再一次对比那几个特征:职责分离 -- 毫无疑问,VIPER是职责分离做的最好的。 可测试性 -- 职责分离越好,可测试性当然也会更好 易用性 -- 你可能已经猜到了,上面两项意味着维护成本的提升。你必须写很多处理各个类之间交互的代码。所有这个乐高模式到底怎么样呢? 当使用VIPER时,如果你感觉就像是通过乐高方块搭建帝国大厦,这就意味着出现了问题。不应该过早在你的应用中使用VIPER,你需要考虑简便性。有些人不注意简便性,直接使用VIPER,会有点大材小用。我猜测很多人是这么想的,他们的应用迟早都会发展到适用VIPER的复杂程度,所以早晚都会做的事,即使现在维护成本高也应该继续做下去。如果你就是这么想的,我推荐你试一下Generamba -- 一个生成VIPER组件的工具。虽然对我个人来说,这感觉就像使用自动瞄准系统而不是简单的弹射。 总结 我们已经讲解了几个架构模式,希望你能解答曾经困扰你的问题。我敢肯定你也意识到了架构模式的选择没有最好这一说,它取决于你在特定环境下权衡利弊之后做的选择。 所以,在一个应用中出现混合一种混合的架构模式也是很常见的。例如,你一开始使用MVC,然后你发现有一个界面的逻辑变得很复杂,然后你转向了MVVM,但也是仅限于这个界面。你不必重构别的使用MVC的界面,因为它原本就是工作的好好的,而且这两个架构模式是很容易兼容的。事情应该力求简单,不过不能过于简单 -- 阿尔伯特·爱因斯坦
可能被忽略的UIButton细节
关于System Button看一个简单的例子: button.setTitle("Title", for: .normal) button.setImage(UIImage(named: "icon"), for: .normal)buttonType分别设置为system和custom,仅做如上设置,显示效果对比(上面的custom,下面的是system)system Button显示出蓝色其实是tintColor的效果,关于tintColor的说法是:This property has no default effect for buttons with type custom. For custom buttons, you must implement any behavior related to tintColor yourself. 在custom类型的button中设置tintColor是不生效的,需要自定义样式。在system类型的button里有一个默认蓝色的tintColor,当然我们可以修改它为其他颜色,会对image和title同时生效。 另外可以发现image不是原始图片,而是被填充为tintColor的颜色。这是因为system类型下button的image被默认以alwaysTemplate类型渲染的,如果想要显示原始图片可以做如下操作: let image = UIImage(named: "icon")?.withRenderingMode(.alwaysOriginal) button.setImage(image, for: .normal)关于触摸反馈看一个常见的代码: let button = UIButton()//默认样式custom button.setTitle("Title", for: .normal) button.setTitleColor(UIColor.blue, for: .normal) button.backgroundColor = UIColor.red view.addSubview(button)以上是的button的常见写法。遗憾的是这种写法,不会带触摸反馈效果。那如果我们想加触摸反馈,需要如何处理: 1、仅文字的触摸反馈 //system: let button = UIButton(type: .system) button.setTitleColor(UIColor.blue, for: .normal)//自动添加反馈效果 button.setTitleColor(UIColor.green, for: .highlighted)//会和系统效果叠加,不可控,不建议写 //custom let button = UIButton(type: .custom) button.setTitleColor(UIColor.blue, for: .normal) button.setTitleColor(UIColor.green, for: .highlighted)//自定义反馈样式2、带图片和文字的触摸反馈 button.setTitle("Title", for: .normal) button.setImage(UIImage(named: "icon"), for: .normal) //system:会同时对图片文字添加反馈效果 //custom:默认仅对图片有触摸反馈3、带背景图的按钮 button.setBackgroundImage(UIImage(named: "background"), for: .normal) //system是按钮整体反馈 //custom是仅背景图片反馈,title,image无反馈4、关闭触摸反馈 button.isUserInteractionEnabled = false //custome,system均会关闭触摸反馈 button.adjustsImageWhenHighlighted = false //custom:会关闭image,backgroundImage的反馈 //system:此设置无效5、showsTouchWhenHighlighted 这个属性是系统提供的一种highlighted样式,点击时按钮高亮。但是效果确实有点丑丑的,基本不用这种效果 其他特性 全局修改UIButton的样式可以: let gobalBtn = UIButton.appearance()//所有继承UIView的类都可以使用这个方法 gobalBtn.setTitle("Good", for: .normal)//会将所有button的title改为GoodsetAttributedTitle方法 这个方法可以将button的title以富文本的形式进行设置,支持对不同state的设置。需要注意它和setTitle的优先级 //用attributed方式设置button的title和titleColor let string = "Title" let attributed = NSMutableAttributedString(string: string) let range = NSMakeRange(0, string.count) attributed.addAttributes([NSAttributedStringKey.foregroundColor : UIColor.green], range: range) button.setAttributedTitle(attributed, for: .normal) //此时用setTitle重新设置title样式,不会生效,attributed优先级大于直接设置 button.setTitle("Next Button", for: .normal) button.setTitleColor(UIColor.blue, for: .normal)