iOS两个客户端代码复用小技巧
SangDahl
8年前
<p>一般一个App只有一个客户端,因此也只有一份代码仓库,也就无所谓复用不复用。但两个客户端也不是没有,美团、饿了么等就分商家版和买家版,贝聊App也分为老师版和家长版两个客户端。有两个客户端代码复用就在所难免,比如基本的工具类,比如一些共用的业务。本文就以贝聊App为例,分享当有两个客户端时代码复用的小技巧。</p> <h2>repository仓库划分</h2> <p>贝聊APP远程仓库划分为三个,一个 家长端repository ,一个 老师端repository ,一个两端共用的 BLKit repository 。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/de196e021f5abaf57bf4d089f510580f.png"></p> <p>家长端的本地 repository 包含远程的 家长端repository 和两端共用的 Kit repository 。</p> <p>老师端的本地 repository 包含远程的 老师端repository 和两端共用的 Kit repository 。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/7a25fc13b39ea0ed5a984df256dc5b6a.png"></p> <h2>用submodule管理共用模块BLKit</h2> <p>一个端要包括两个 repository ,而且这两个 repository 还是相互依赖,怎么管理也是个问题。</p> <p>git 中 submodule 允许在一个 主git 中存在另外独立的 子git ,而且 主git 还能记录每一次 commit 中 子git 所在的 commit ,这是完美的仓库独立却又相互依赖的管理方案。</p> <p>如下图:</p> <p><img src="https://simg.open-open.com/show/150ea3e4fa1e703956d310c682ba5c5b.png"></p> <h2>通过cocoapods将BLKit引入项目</h2> <p>虽然利用 submodule 可以很好的管理共用模块 BLKit ,但又如何将 BLKit 引入到 project 中呢?</p> <p>有两个方案,</p> <p>直接加入主project</p> <p>可以通过 add files to project ,选择所有 BLKit 中的文件,添加到 主project 中。但这样做的隐患非常大:</p> <ul> <li> <p>BLKit不独立</p> <p>BLKit 是两端共用模块,因此 BLKit 只能使用 BLKit 内部的类,否则在另外一个端中是不能编译的。如果 BLKit 的文件被添加到 主project 中,很容易在 BLKit 的某个文件中初始化了主项目中的一个类,导致 BLKit 很难维护。</p> </li> <li> <p>BLKit不能更新</p> <p>BLKit 直接添加到 主project 中,如果一端有添加新文件,另外一端拉取下来后,还得手动通过 add files to project 将新添加的文件添加到 主project 中。如果每次只是添加一个文件还好,如果文件一多,想死的心都有。</p> </li> </ul> <p>通过cocoapods管理</p> <p>将 BLKit 像其他第三方依赖库一样,用 cocoapods 管理,只不过 pods 的文件源连接到本地的 BLKit 仓库而已。</p> <p>首先在主项目中的 submodule 文件夹中,创建一个 BLKit.podspec ,然后按照 podspec 的规则,填写 podsspec ,然后在主项目中的 Podfile ,像添加普通 pod 一样,将 BLKit 添加到 Podfile 中,</p> <pre> <code class="language-objectivec">pod 'BLKit', :path => "./BLKit/" </code></pre> <p>然后 pod update 即可,这时 BLKit 就像普通的第三方依赖库一样,出现在 pods 中:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/b1ba16b3eda1e67eee995e2c9ceac6e8.jpg"></p> <p>用 cocoapods 管理好处是很明显的:</p> <ul> <li>BLKit完全独立<br> BLKit 由于在 pods 中,不能引用主项目中的类,完全独立,不会出现一端更新,另外一端不能使用的情况。</li> <li>BLKit自动更新<br> 由于 BLKit 其实就是个 pods 而已,如果 远程BLKit 有更新,拉取到本地后,运行 pod update --no-repo-update 即可。千万加上 --no-repo-update 这个选项,因为 pod update 很慢很慢很慢。(当然如果利用 proxychain , pod update 也很快,但实在没有必要为乐更新 BLKit 更新整个 pod )</li> </ul> <h2>如何在BLKit中引用主项目业务模块</h2> <p>两端共用的模块,一般都是基础功能模块,比如网络模块,数据缓存模块、图片下载模块、视频发送模块等,都完全独立,与主项目没有耦合。但有时业务模块也有共用,比如聊天模块、大图浏览模块等。业务模块与主项目很难完全没有耦合,比如大图浏览模块长按时的操作逻辑,比如聊天模块长按消息的跳转逻辑,况且有时,有些业务模块被抽象成共用的,有些业务模块却没有,共用模块却要引用主项目中没有抽象成共用的业务模块。比如贝聊App中的聊天模块是共用的聊天模块,但大图浏览模块却不是共用的,因为历史原因,家长端和老师端各有各的实现,但点击聊天页面的图片消息,会进入大图浏览模块。</p> <p>那如何在 BLKit 中引用主项目中业务模块?</p> <p>以 BLKit 中的聊天模块为例,</p> <pre> <code class="language-objectivec">// 这个类在Pods中 class ChatViewController: UIViewController { let chatID: String init(chatID: String) { self.chatID = chatID } // 其他业务代码 // Table Cell delegate func didTapImageIn(cell: UITableViewCell) { // 进入大图浏览模块 let allImages: [URL] = [self getAllMessageImages] } } </code></pre> <p>聊天的 Cell 的代理为 ChatViewController ,当点击到图片 Cell ,会调用代理方法 didTapImageIn(cell:) 。 ChatViewController 实现了这个代理方法,获取到所有图片的 URL 后,需要调用主项目中的大图浏览模块。但 Pods 中是不可能直接调用主项目中的方法的。所以需要一个桥梁。</p> <p>定义一个信使协议</p> <p>可以定义一个信使协议,然后在主项目中初始化 ChatViewController 时,传入具体实现这个信使协议的类。</p> <pre> <code class="language-objectivec">// pods中 // 信使协议 protocol ChatMessenger { func openPhotoViewer(with photoes: [URL], in context: UIViewController) } </code></pre> <p>然后主项目中创建一个类实现 ChatMessenger 协议,</p> <pre> <code class="language-objectivec">// 主项目中 class ChatMessengerConcrete: ChatMessenger { func openPhotoViewer(with photoes: [URL], in context: UIViewController) { // 具体调用主项目中的大图浏览模块 } } </code></pre> <p>修改 ChatViewController 接受一个 ChatMessenger 作为初始化参数,然后在点击图片的 cell 代理方法中调用 ChatMessenger 的大图浏览:</p> <pre> <code class="language-objectivec">// 这个类在Pods中 class ChatViewController: UIViewController { let chatMessenger: ChatMessenger let chatID: String // 初始化方法中传入ChatMessenger init(chatMessenger: ChatMessenger, chatID: String) { self.chatMessenger = chatMessenger self.chatID = chatID } // 其他业务代码 // Table Cell delegate func didTapImageIn(cell: UITableViewCell) { let allImages: [URL] = [self getAllMessageImages] // 进入大图浏览模块 chatMessenger.openPhotoViewer(with: allImages, in: self) } } </code></pre> <p>然后在主项目中的聊天入口,初始化 ChatViewController ,并传入实现 ChatMessenger 协议的 ChatMessengerConcrete :</p> <pre> <code class="language-objectivec">// 比如消息列表页 class ChatListViewController: UIViewController { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { // 获取对应indexPath的聊天ID let chatID = "" let chatVC = ChatViewController(chatMessenger: ChatMessengerConcrete(), chatID: ) navigationController?.pushViewController(chatVC, animated: true) } } </code></pre> <p>使用协议扩展</p> <p>虽然可以在主项目中聊天的入口处,传入 ChatMessenger 的实体类,达到在 Pods 中调用主项目业务模块,但如果聊天入口也都抽象到 Pods 中的 BLKit 中,那在 Pods 中是无法初始化一个主项目中的 ChatMessenger 实体类的。</p> <p>这时可以利用 Swift 中的 protocol extension ,在主项目中给 Pods 中的 ChatMessenger 协议一个默认实现。</p> <pre> <code class="language-objectivec">// 主项目中 extension ChatMessenger { func openPhotoViewer(with photoes: [URL], in context: UIViewController) { // 具体调用主项目中的大图浏览模块 } } </code></pre> <p>再让 Pods 中聊天页类 ChatViewController 遵循 ChatMessenger 协议,然后需要主项目中的业务直接调用对应的方法即可。</p> <pre> <code class="language-objectivec">// 这个类在Pods中 class ChatViewController: UIViewController, ChatMessenger { let chatID: String // 初始化方法中传入ChatMessenger init(chatID: String) { self.chatID = chatID } // 其他业务代码 // Table Cell delegate func didTapImageIn(cell: UITableViewCell) { let allImages: [URL] = [self getAllMessageImages] // 进入大图浏览模块 openPhotoViewer(with: allImages, in: self) } } </code></pre> <p>协议扩展是实现 Pods 中调用主项目业务模块的最好方式。</p> <h2>总结</h2> <p>当有两个客户端时,</p> <p>1)建立一个或多个两端共用的 repository ,管理两端相同的业务模块和功能模块</p> <p>2)利用 submodule 在主项目中管理两端共用的 repository</p> <p>3)利用私有 pods 将 submodule 中的共用代码引入 project 中</p> <p>4)利用协议扩展轻松实现在 pods 中调用主项目中的业务模块</p> <p> </p> <p>来自:http://shellhue.github.io/2017/04/08/sharecode/</p> <p> </p>