
哈Ha!
我叫Valera,作为Badoo团队的一员,两年来我一直在开发iOS应用程序。 我们的优先事项之一是易于维护代码。 由于每周都会使用大量新功能,因此我们需要首先考虑应用程序的体系结构,否则很难在不破坏现有功能的情况下向产品添加新功能。 显然,这也适用于用户界面(UI)的实现,而不管这是使用代码,Xcode(XIB)还是混合方法完成的。 在本文中,我将介绍一些UI实现技术,这些技术使我们能够简化用户界面的开发,使其灵活,方便地进行测试。 本文还有
英文版 。
开始之前...
我将使用Swift编写的示例应用程序考虑用户界面实现技术。 单击按钮的应用程序将显示一个朋友列表。
它包括三个部分:
- 组件是自定义UI组件,即仅与用户界面相关的代码。
- 演示应用程序 -演示视图模型和其他仅具有UI依赖项的用户界面实体。
- 真正的应用是视图模型和其他可能包含特定依赖关系和逻辑的实体。
为什么会有这样的分离? 我将在下面回答这个问题,但现在,请查看我们应用程序的用户界面:
这是一个弹出视图,其内容位于另一个全屏视图的顶部。 一切都很简单。
该项目的完整源代码可在
GitHub上
找到 。
在研究UI代码之前,我想向您介绍这里使用的辅助类Observable。 其界面如下所示:
var value: T func observe(_ closure: @escaping (_ old: T, _ new: T) -> Void) -> ObserverProtocol func observeNewAndCall(_ closure: @escaping (_ new: T) -> Void) -> ObserverProtocol
它只是将所有更改通知所有先前签署的观察者,因此这是KVO(键值观察)的一种替代方法,或者,如果您愿意,还可以采用反应式编程。 这是一个用法示例:
self.observers.append(self.viewModel.items.observe { [weak self] (_, newItems) in self?.state = newItems.isEmpty ? .zeroCase(type: .empty) : .normal self?.collectionView.reloadSections(IndexSet(integer: 0)) })
控制器将更改订阅
self.viewModel.items
属性,并且当更改发生时,处理程序将执行业务逻辑。 例如,它更新视图状态并用新项目重新加载集合视图。
您将在下面看到更多使用示例。
方法论
在本节中,我将讨论Badoo中使用的四种UI开发技术:
1.用代码实现用户界面。
2.使用布局锚点。
3.组成部分-分而治之。
4.用户界面和逻辑的分离。
#1:在代码中实现用户界面
在Badoo中,大多数用户兴趣是通过代码实现的。 我们为什么不使用XIB或情节提要? 公平的问题。 主要原因是为中型团队维护代码的便利性,即:
- 代码中的更改清晰可见,这意味着无需解析XML故事板/ XIB文件即可查找同事所做的更改;
- 版本控制系统(例如,Git)比使用“大量” XLM文件更容易使用代码,尤其是在中等冲突时; 还应考虑到,即使界面没有更改,XIB /情节提要文件的内容也会在每次保存时更改(尽管我听说在Xcode 9中该问题已得到解决);
- 在子视图(布局子视图)的重新布局过程中,可能很难在Interface Builder(IB)中修改和维护某些属性,例如CALayer属性,这可能导致视图状态的多个事实来源;
- Interface Builder不是最快的工具,有时直接使用代码会更快。
看一下以下控制器(FriendsListViewController):
final class FriendsListViewController: UIViewController { struct ViewConfig { let backgroundColor: UIColor let cornerRadius: CGFloat } private var infoView: FriendsListView! private let viewModel: FriendsListViewModelProtocol private let viewConfig: ViewConfig init(viewModel: FriendsListViewModelProtocol, viewConfig: ViewConfig) { self.viewModel = viewModel self.viewConfig = viewConfig super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() self.setupContainerView() } private func setupContainerView() { self.view.backgroundColor = self.viewConfig.backgroundColor let infoView = FriendsListView( frame: .zero, viewModel: self.viewModel, viewConfig: .defaultConfig) infoView.backgroundColor = self.viewConfig.backgroundColor self.view.addSubview(infoView) self.infoView = infoView infoView.translatesAutoresizingMaskIntoConstraints = false infoView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true infoView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true infoView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true infoView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true }
本示例说明您只能通过提供视图模型和视图配置来创建视图控制器。 您可以
在此处阅读有关表示模型的更多信息,即MVVM设计模型(Model-View-ViewModel)。 由于视图配置是定义视图的布局和样式(即缩进,大小,颜色,字体等)的简单结构实体,因此我认为提供这样的标准配置是合适的:
extension FriendsListViewController.ViewConfig { static var defaultConfig: FriendsListViewController.ViewConfig { return FriendsListViewController.ViewConfig(backgroundColor: .white, cornerRadius: 16) } }
所有视图初始化都在
setupContainerView
方法中进行,当已经创建并加载视图但尚未在屏幕上绘制视图时,仅从viewDidLoad调用一次,也就是说,所有必要的元素(子视图)都简单地添加到视图层次结构中,然后应用标记(布局)和样式。
这是视图控制器现在的样子:
final class FriendsListPresenter: FriendsListPresenterProtocol { // … func presentFriendsList(from presentingViewController: UIViewController) { let controller = Class.createFriendsListViewController( presentingViewController: presentingViewController, headerViewModel: self.headerViewModel, contentViewModel: self.contentViewModel) controller.modalPresentationStyle = .overCurrentContext controller.modalTransitionStyle = .crossDissolve presentingViewController.present(controller, animated: true, completion: nil) } private class func createFriendsListViewController( presentingViewController: UIViewController, headerViewModel: FriendsListHeaderViewModelProtocol, contentViewModel: FriendsListContentViewModelProtocol) -> FriendsListContainerViewController { let dismissViewControllerBlock: VoidBlock = { [weak presentingViewController] in presentingViewController?.dismiss(animated: true, completion: nil) } let infoViewModel = FriendsListViewModel( headerViewModel: headerViewModel, contentViewModel: contentViewModel) let containerViewModel = FriendsListContainerViewModel(onOutsideContentTapAction: dismissViewControllerBlock) let friendsListViewController = FriendsListViewController( viewModel: infoViewModel, viewConfig: .defaultConfig) let controller = FriendsListContainerViewController( contentViewController: friendsListViewController, viewModel: containerViewModel, viewConfig: .defaultConfig) return controller } }
您可以清楚地看到
职责分工 ,这个概念并不比在故事板上调用segue复杂得多。
鉴于我们拥有模型,因此创建视图控制器非常简单,您可以简单地使用标准视图配置:
let friendsListViewController = FriendsListViewController( viewModel: infoViewModel, viewConfig: .defaultConfig)
#2:使用布局锚点
这是布局代码:
self.view.addSubview(infoView) self.infoView = infoView infoView.translatesAutoresizingMaskIntoConstraints = false infoView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true infoView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true infoView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true infoView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
简而言之,此代码将
infoView
置于父视图(
infoView
视图)的内部,相对于
infoView
视图的原始大小位于坐标(0,0)处。
为什么要使用布局锚点? 快速简便。 当然,您可以手动设置UIView.frame并即时计算所有位置和大小,但是有时它可能会变得过于混乱和/或代码过于庞大。
您还可以使用文本格式进行标记,如此处所述,但这经常会导致错误,因为您需要严格遵循格式,并且Xcode在编写/编译代码阶段不会检查标记描述文本,并且您不能使用《安全区域布局指南》:
NSLayoutConstraint.constraints( withVisualFormat: "V:|-(\(topSpace))-[headerView(headerHeight@200)]-[collectionView(collectionViewHeight@990)]|", options: [], metrics: metrics, views: views)
在定义标记的文本字符串中犯错误或错字很容易,对吧?
#3:组件-分而治之
我们的示例用户界面分为多个组件,每个组件仅执行一项特定功能。
例如:
FriendsListHeaderView
显示有关朋友的信息和“关闭”按钮。FriendsListContentView
显示具有可单击单元格的朋友列表,到达列表末尾时将动态加载内容。FriendsListView
先前两个视图的容器。
如前所述,当每个组件负责一个单独的功能时,Badoo我们喜欢
唯一负责的
原则 。 这不仅在错误修复过程中(这可能不是iOS开发人员工作中最有趣的部分)有所帮助,而且在开发新功能期间也有帮助,因为这种方法极大地扩展了将来重用代码的可能性。
#4:分离用户界面和逻辑
最后但同样重要的一点是用户界面和逻辑的分离。 一种可以为您的团队节省时间和神经的技术。 从字面上看:一个用于用户界面的项目和一个用于业务逻辑的项目。
让我们回到我们的例子。 您还记得,演示文稿(presenter)的本质如下所示:
func presentFriendsList(from presentingViewController: UIViewController) { let controller = Class.createFriendsListViewController( presentingViewController: presentingViewController, headerViewModel: self.headerViewModel, contentViewModel: self.contentViewModel) controller.modalPresentationStyle = .overCurrentContext controller.modalTransitionStyle = .crossDissolve presentingViewController.present(controller, animated: true, completion: nil) }
您只需要提供标题和内容的视图模型。 其余部分隐藏在UI组件的上述实现中。
标头视图模型协议如下所示:
protocol FriendsListHeaderViewModelProtocol { var friendsCountIcon: UIImage? { get } var closeButtonIcon: UIImage? { get } var friendsCount: Observable<String> { get } var onCloseAction: VoidBlock? { get set } }
现在,假设您要为UI添加视觉测试-就像为UI组件传递存根模型一样简单。
final class FriendsListHeaderDemoViewModel: FriendsListHeaderViewModelProtocol { var friendsCountIcon: UIImage? = UIImage(named: "ic_friends_count") var closeButtonIcon: UIImage? = UIImage(named: "ic_close_cross") var friendsCount: Observable<String> var onCloseAction: VoidBlock? init() { let friendsCountString = "\(Int.random(min: 1, max: 5000))" self.friendsCount = Observable(friendsCountString) } }
看起来很简单,对吧? 现在,我们想向应用程序的组件中添加业务逻辑,这可能需要数据提供者,数据模型等:
final class FriendsListHeaderViewModel: FriendsListHeaderViewModelProtocol { let friendsCountIcon: UIImage? let closeButtonIcon: UIImage? let friendsCount: Observable<String> = Observable("0") var onCloseAction: VoidBlock? private let dataProvider: FriendsListDataProviderProtocol private var observers: [ObserverProtocol] = [] init(dataProvider: FriendsListDataProviderProtocol, friendsCountIcon: UIImage?, closeButtonIcon: UIImage?) { self.dataProvider = dataProvider self.friendsCountIcon = friendsCountIcon self.closeButtonIcon = closeButtonIcon self.setupDataObservers() } private func setupDataObservers() { self.observers.append(self.dataProvider.totalItemsCount.observeNewAndCall { [weak self] (newCount) in self?.friendsCount.value = "\(newCount)" }) } }
有什么会更容易? 只需实现数据提供程序-那就开始吧!
内容模型的实现看起来有些复杂,但是职责分离仍然大大简化了生活。 这是一个如何在单击按钮时实例化并显示朋友列表的示例:
private func presentRealFriendsList(sender: Any) { let avatarPlaceholderImage = UIImage(named: "avatar-placeholder") let itemFactory = FriendsListItemFactory(avatarPlaceholderImage: avatarPlaceholderImage) let dataProvider = FriendsListDataProvider(itemFactory: itemFactory) let viewModelFactory = FriendsListViewModelFactory(dataProvider: dataProvider) var headerViewModel = viewModelFactory.makeHeaderViewModel() headerViewModel.onCloseAction = { [weak self] in self?.dismiss(animated: true, completion: nil) } let contentViewModel = viewModelFactory.makeContentViewModel() let presenter = FriendsListPresenter( headerViewModel: headerViewModel, contentViewModel: contentViewModel) presenter.presentFriendsList(from: self) }
此技术有助于将用户界面与业务逻辑隔离。 此外,这使您可以通过可视化测试覆盖整个UI,并将测试数据传递给组件! 因此,无论是启动产品还是已经完成的产品,用户界面和相关业务逻辑的分离对于项目的成功至关重要。
结论
当然,这些只是Badoo中使用的一些技术,并不是所有情况下的通用解决方案。 因此,请在评估它们是否适合您和您的项目后使用它们。
还有其他方法,例如使用Interface Builder的XIB可配置UI组件(在
我们的另
一篇文章中进行
了介绍 ),但是由于各种原因,它们在Badoo中没有使用。 请记住,每个人对全局都有自己的看法和远见,因此,为了开发成功的项目,您应该在团队中达成共识,并选择最适合大多数情况的方法。
愿斯威夫特和你在一起!
资料来源