如何制作智能的UIStackView垫片

您是否曾经需要创建一个在每个已排列的子视图之间具有不同间距的StackView? 好吧,如果您有并且您的项目需要支持比iOS 11更旧的操作系统(您可以在iOS 11+中本地运行),则可能只是创建了一个空的子视图,并定义了宽度/高度并将其添加到其他子视图之间。 但是,嘿,有一个陷阱。

如果要隐藏原始排列的子视图之一(假设是中间的子视图),该怎么办? 隐藏它之后,很可能会有不希望有的间距,因为它的两侧都存在空的隔离子视图。 因此,您也必须隐藏一个空的隔离子视图,这可能是您要避免保持代码清洁的一种方法。 在本博文中,我将向您展示如何以更聪明的方式实现此方法,以便您的spacer视图自动随内容视图一起隐藏。

这个想法是添加一个连接到内容视图并与其隐藏的间隔器。 我们将通过在UIView(这将是我们的内容视图)上创建扩展来实现此目的,该扩展具有创建间隔视图并返回它的功能。 在函数内部,间隔视图将观察内容视图的“隐藏”属性,并对该属性的任何更改做出反应。 该功能将使用参数来区分垫片的尺寸,轴和优先级。

这是我们将要实现的功能的概要:

 扩展UIView { 
func createSpacer(_ size:CGFloat,axis:UILayoutConstraintAxis,priority:Int)-> UIView {
//这将是一个创建并返回间隔符的代码,该间隔符将观察self的“隐藏”属性(这是我们的内容视图)
}
}

然后,您将在ViewController中创建类似于以下内容的间隔符:

 让aLabel = UILabel() 
yourStackView.addArrangedSubview(aLabel)
yourStackView.addArrangedSubiew(aLabel.createSpacer(10,轴:.vertical,优先级:1000))

UIStackView间隔符:实现

我将向您展示依赖于ReactiveSwift的有效实现。 然后,我将描述我尝试过的其他方法以及为什么它们不起作用。 该实现还取决于SnapKit(这是自动布局框架),当然也取决于UIKit。 因此,请不要忘记将其导入文件顶部。

 导入UIKit 
导入SnapKit
导入ReactiveSwift

我认为实现非常简单。 在“ createSpacer”函数内,您将创建一个隔离器,即UIView。 然后,将其“ isHidden”属性设置为最初与内容视图的(isself)“ isHidden”属性匹配。 然后,您可以将内容视图的“ isHidden”属性的反应绑定到spacer的“ isHidden”属性,因此每次隐藏或取消隐藏内容视图时,都可以。 垫片也会自动隐藏或取消隐藏。 请注意,由于Objective-C的旧名称,signal:forKeyPath函数必须使用“隐藏”字符串作为参数。 之后,仅使用SnapKit框架创建所需的间隔物高度或宽度(取决于轴)大小约束,然后返回间隔物。

 扩展UIView { 
私有函数createSpacer(_大小:CGFloat,轴:UILayoutConstraintAxis,优先级:Int)-> UIView {
让spacer = UIView()
spacer.isHidden = self.isHidden
spacer.reactive.isHidden <〜self.reactive.signal(forKeyPath:“ hidden”)。filterMap {$ 0 as? 布尔}
spacer.snp.makeConstraints {制造于
开关轴{
case .vertical:make.height.equalTo(size).priority(priority)
大小写。水平:make.width.equalTo(size).priority(priority)
}
}
返回垫片
}
}

让我们在没有ReactiveSwift的情况下尝试

那么,当您无疑可以使用Swift 4中引入的所有基于块的新KVO来原生地导入ReactiveSwift时,为什么还要导入它呢? 好吧,让我们尝试! 新的KVO确实非常易于使用,您不必担心自己添加和删除观察者,因为您的对象将对观察值有很强的引用,观察对象随对象一起释放。 在我们的案例中,我们将必须为间隔视图创建UIView的子类,以便它可以保留对观察的引用。 不幸的是,我们无法将存储的属性添加到扩展中。

从上一个示例中删除反应性绑定,并将spacer view更改为名为SpacerView的新UIView子类的实例,如下所示:

 让spacer = SpacerView(contentView:self) 

现在,创建一个SpacerView类,它将把内容视图作为初始化程序中的参数。 我们将使用基于块的新KVO来观察其“隐藏”属性,并将其分配给“观察”变量。

  fileprivate类SpacerView:UIView { 
var观察:NSKeyValueObservation?
  init(contentView:UIView){ 
super.init(frame:.zero)
观察= contentView.observe(\。hidden,选项:[.new]){(_,change)in
如果让change = change.newValue {
self.isHidden =更改
}
}
}
 需要初始化吗?(编码器aDecoder:NSCoder){ 
fatalError(“ init(coder :)尚未实现”)
}
}

如果您使用的是iOS 11,它的工作原理就像一个魅力。如果您尝试从堆栈视图中删除内容视图,则iOS 10上的应用程序会崩溃,因为iOS 10无法从对象中删除(取消注册)观察者。 当我尝试使用旧的KVO时,我得到了相同的结果。 我什至尝试在UIView扩展中创建关联的对象,该对象同时包含对内容视图和间隔视图的引用,并且在其deinit中我将注销观察者。 它也不起作用,因为内容视图先于其关联视图取消初始化,因此无法移除观察者。

UIStackView间隔符:结论

您可能会问ReactiveSwift是如何做到的。 因此,它适用于所有iOS版本。 它使用swizzling方法进入UIView的deinit,在其中取消注册观察者。 您可以尝试自己实现它,但这是非常高级的编程,并且可能导致许多未定义的行为。 这一切都意味着,如果我们要使用没有ReactiveSwift且没有方法混乱的智能垫片,则我们的项目必须是iOS 11+。 但是正如我之前说的,从iOS 11开始,可以使用StackView上的setCustomSpacing函数以本机方式执行此操作。 因此,无需实现这些智能垫片。 但是,如果您需要项目来支持较旧的iOS,则导入ReactiveSwift并编写此简单扩展可以使您的堆栈视图更加智能。