如何使用良好的 OO 设计以编程方式创建动态视图



我在 Swift 中创建动态视图时遇到了问题。然而,这个问题与 Swift 本身没有直接关系,而是一个面向对象的编程问题。

问题是我需要能够动态地向视图添加其他视图元素。而且我不确定我做得是否正确。我的解决方案对我来说似乎有点矫枉过正。

为了解决这个问题,我认为装饰器模式将是一个很好的候选者。此外,为了更好地控制流,我还引入了模板方法模式。

我有许多类定义某些视图控件(如标签、文本字段和按钮(的默认外观。在下面,您可以看到它的大致概念。

这是我的代码:

class ViewElement{
// this class inherits from default UIKit elemnts and provides default UI view
}
// default cell is the cell that implements default elements layout and margings, etc
class DefaultCell: UITableViewCell {
let mainStack: UIViewStack
func addElement(ViewElement)
}
class BlueCell: DefaultCell {
let textField1: TextField
let label : Label
let button: Button
init(){
textField = TextField()
label = Label()
button = Button()
addElement(textField)
addElement(label)
addElement(button)
}
}

下面是表视图数据源实现

class BlueTable: UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
switch weather {
case good: 
labelOne
labelTwo
buttonOne
cell.addElement(labelOne)
cell.addElement(labelTwo)
cell.addElement(buttonOne)
case bad: 
// idem     
cell.addView(badWeatherView)
}
return cell
}
}

如您所见,条件数量越多,我的 switch 语句就越大。

其他问题源于我需要访问我在条件中分配的其他元素,例如回调、点击事件等。此外,条件中的那些元素是通过addElement方法添加的,这意味着这些元素将被添加到mainStack的底部。

为了控制将元素添加到堆栈的方式,我决定使用以下解决方案: 模板方法模式

protocol OrderableElements {
func top()
func middle()
func bottom()
}
extension OrderableElements {
func render() {
top()
middle()
bottom()
}
}

现在BlueCell实现了协议,看起来像这样

class BlueCell: DefaultCell, OrderableElements {
init(){
textField = TextField()
label = Label()
button = Button()
}
func top() {
addElement(textField)
}
func middle() {
addElement(label)
}
func bottom(){
addElement(button)
}
}

然后,表数据源类将如下所示:

class BlueTable: UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
switch weather {
case good: 
labelOne
labelTwo
buttonOne
cell.addElement(labelOne)
cell.addElement(labelTwo)
cell.addElement(buttonOne)
case bad: 
// idem
cell.addView(badWeatherView)
}
...
**cell.render()**
return cell
}
}

现在,因为我需要在某个位置添加新的视图元素,或者更好地说,在BlueCell范围内的某些时刻,我为单元格引入了装饰器,如下所示:

class CellDecorator: OrderableElements {
var cell: BlueCell
init(cell: BlueCell){
self.cell = cell
}
func top() {
self.cell.top()
}
func middle(){
self.cell.middle()
}
func bottom(){
self.cell.bottom()
}
func getCell() {
return self.cell
}
}

这是具体的实现

class GoodWeatherDecorator: CellDecorator {
let goodLabel
let goodTextField
let goodButton
override top() {
super.top()
addElement(goodLabel)
}
override middle(){
super.middle()
addElement(goodTextfield)
}
override bottom(){
super.bottom()
addElement(goodButton)
}
}

cellForRowAt 方法的最终实现如下所示:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
var decoratedCell = CellDecorator(cell: cell)
switch weather {
case good: 
decoratedCell = GoodWeatherDecorator(cell: cell)
case bad: 
decoratedCell = BadWeatherDecorator(cell: cell)
}
decoratedCell.configure() // <------------ here is the configure call 
cell = decoratedCell.getCell() // <------- here I get cell from the decorator
return cell
}
}

现在我确实明白了我对装饰器模式的实现不是 100% 有效的,因为我不是从 BlueCell 类继承的,例如。但这并没有那么困扰我。困扰我的事情是,我认为这个问题的解决方案有点矫枉过正。

一切都以正确的方式工作,但我可以帮助感觉为解决这个微不足道的问题做了太多。

你觉得怎么样?你会如何解决这类问题?

感谢在advace

鉴于您只显示两种类型的单元格,并且您的解决方案实际上并没有摆脱switch语句,我会说您的解决方案算作"矫枉过正"。

你没有表现出来,但似乎你有一个Weather枚举。我假设...

enum Weather: String {
case good
case bad
}

在表视图数据源中,我的目标是具有如下所示的内容:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let weather = weathers[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: weather.rawValue, for: indexPath) as! ConfigurableCell
cell.configure(with: weather)
return cell as! UITableViewCell
}

为了实现上述目标,我将在情节提要文件中布置几个具有不同标识符的单元格。我会为代码中的每种类型的单元格提供一个子类,其中所有单元格都符合ConfigurableCell协议。

protocol ConfigurableCell {
func configure(with weather: Weather)
}

如果无法使Weather枚举符合 String 类型,则需要 switch 语句将天气对象转换为字符串标识符,否则不需要 switch 语句。

你应该遵循Daniel T.的回答。

但这是我在自己的项目上使用的建议升级。

而不是仅仅使用

protocol ConfigurableCell {
func configure(with weather: Weather)
}

我将其用于许多不同的场景的可重用性目的。

protocol Configurable {
associatedtype Initializables
func configure(_ model: Initializables) -> Self
}

示例用例

UIView控制器

class SomeViewController: UIViewController { 
var someIntProperty: Int?
... 
} 
extension SomeViewController: Configurable {
struct Initializables {
let someIntProperty: Int?
}
func configure(_ model: SomeViewController.Initializables) -> Self {
self.someIntProperty = model.someIntProperty
return self
}
}
// on some other part of the code.
let someViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! SomeViewController
_ = someViewController.configure(SomeViewController.Initializables(someIntProperty: 100))

UITableViewCell

class SomeTableViewCell: UITableViewCell { 
var someIntProperty: Int?
var someStringProperty: Int?
... 
} 
extension SomeTableViewCell: Configurable {
struct Initializables {
let someIntProperty: Int?
let someStringProperty: Int?
}
func configure(_ model: SomeTableViewCell.Initializables) -> Self {
self.someIntProperty = model.someIntProperty
self.someStringProperty = model.someStringProperty
return self
}
}
// on cellForRow
let cell = tableView.dequeueReusableCell(withIdentifier: "SomeTableViewCell", for: indexPath) as! SomeTableViewCell
return cell.configure(SomeTableViewCell.Initializables(someIntProperty: 100, someStringProperty: "SomeString"))

笔记:

如您所见,它非常可重用,易于使用和实现。缺点是使用时生成的代码可能会很长configure

最新更新