普通视图

发现新文章,点击刷新页面。
昨天以前SwiftGG

Swift 中的集合(Set)

作者 SwiftGG
2019年7月9日 13:00

作者:Thomas Hanning,原文链接,原文日期:2018-09-06
译者:rsenjoyer;校对:numbbbbbpmst;定稿:Forelax

集合(Set)是 Swift 集合类型(collection types)之一,集合用来存储类型相同且没有确定顺序唯一的值。你可以将集合想象成一盒台球:它们在颜色和数量上独一无二,但在盒内是无序的。

提示:这篇文章使用的是 Swift 4 和 Xcode 10

创建集合

创建一个集合非常简单:

let setA: Set<String> = ["a","b","c"]

在这个例子中,创建一个 String 类型的集合,命名为 setA。它存储着 abc 三个值。与数组相比,集合内元素是无序的。通过编译器的类型推导功能,你也可以像如下方式创建集合:

let setB: Set = ["a","b","c"]

同样也可以使用集合的构造器:

let setC = Set(["a","b","c"])

跟数组一样,如果使用 let 来定义一个集合,它就是不可变的。使用 var定义的是一个可变集合。

var setD = Set(["a","b"])

稍后我们将了解更多有关可变集合的信息。

访问集合中的元素

你可以使用循环来访问集合中的元素:

for value in setA {
print(value)
}

注意:每次运行代码时,循环中值的顺序可能不同。从表面来看,它们像是随机返回一样。

集合分析

首先,你可以检查集合是否为空:

print(setA.isEmpty)

也可以获取集合中元素的个数:

print(setA.count)

上面的操作对数组同样有效,对集合而言,更加普遍的问题是判断集合中是否包含某个元素。为此,你可以使用 contains 方法:

print(setA.contains("a"))

从集合中添加和删除元素

你可以向可变集合里面添加和删除元素:

setD.insert("c")
setD.remove("a")

由于集合元素的唯一性,因此只能将同一个元素添加到集合中一次。可以多次使用相同的值调用 insert 方法,但集合不会改变。

var setE: Set = [1,2,3,4]

setE.insert(5)
setE.insert(5)
setE.insert(5)

print(setE) //[4,5,1,2,3]

和前面所说的一样,上面代码每次执行时输出的顺序可能不同,因为集合元素无序。

集合比较

集合间能进行比较。显然,可以比较两个集合是否相等:

let setA: = [“a”, “b”, “c”]
let setB: = [“a”, “b”, “c”]

if setA == setB {
print(“the sets are equal”)
}

这种情况下,集合是相等的。

比较两个集合的大小是没有明确的定义,但可以检查一个集合是否是另一个集合的子集:

let intSetA: Set = [1,2,3,4,5,6,7,8,9,0]
let intSetB: Set = [1,2,3]
intSetB.isSubset(of: intSetA) //true

也可以检查集合是否是另一个集合的真子集。这种情况就是该集合是另一个集合的子集但不想等。

let intSetA: Set = [1,2,3,4,5,6,7,8,9,0]
let intSetB: Set = [1,2,3,4,5,6,7,8,9,0]
let intSetC: Set = [3,4,5]

intSetB.isSubset(of: intSetA) //true
intSetB.isStrictSubset(of: intSetA) //false
intSetC.isSubset(of: intSetA) // true
intSetC.isStrictSubset(of: intSetA) //true

与之相对的概念就是超集:

let intSetA: Set = [1,2,3,4,5,6,7,8,9,0]
let intSetC: Set = [3,4,5]
intSetA.isSuperset(of: intSetC) //true
intSetA.isStrictSuperset(of: intSetC) //true

如果两个集合没有相同的元素,那么就说这两个集合不相交

let intSetA: Set = [1,2,3,4,5,6,7,8,9,0]
let intSetC: Set = [3,4,5]
let intSetD: Set = [13,14,15]

intSetA.isDisjoint(with: intSetC) //false
intSetA.isDisjoint(with: intSetD) //true

集合结合

你可以将两个集合合并成为一个新集合,新的集合中包含两个集合中所有的元素:

let stringSetA: Set = ["a","b","c"]
let stringSetB: Set = ["c","d","e"]

let unionSetAB = stringSetA.union(stringSetB)
print(unionSetAB) //["d", "b", "c", "a", "e"]

另一方面,交集就是仅包含两个集合中共同的元素:

let stringSetA: Set = ["a","b","c"]
let stringSetB: Set = ["c","d","e"]

let intersectionAB = stringSetA.intersection(stringSetB)
print(intersectionAB) //[“c”]

自定义集合元素类型

你可以在集合中存储自定义的类型。这种类型可以是类或者结构体。为了能正常使用集合,该类型必须遵循 hashable 协议。

class Movie: Hashable {

var title: String
var year: Int

init(title: String, year: Int) {
self.title = title
self.year = year
}

static func == (lhs: Movie, rhs: Movie) -> Bool {
return lhs.title == rhs.title &&
lhs.year == rhs.year
}

var hashValue: Int {
return title.hashValue ^ year.hashValue
}

}

let terminator = Movie(title: "Terminator", year: 1980)
let backToTheFuture = Movie(title: "Back to the Future", year: 1985)

let movieSetA: Set = [terminator,backToTheFuture]

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

Hacking Hit Tests

作者 SwiftGG
2018年12月27日 14:00

作者:Soroush Khanlou,原文链接,原文日期:2018-09-07
译者:Nemocdz;校对:Yousanflicspmst;定稿:Forelax

回想 Crusty 教我们使用面向协议编程之前的日子,我们大多使用继承来共享代码的实现。通常在 UIKit 编程中,你可能会用 UIView 的子类去添加一些子视图,重写 -layoutSubviews,然后重复这些工作。也许你还会重写 -drawRect。但当你需要做一些特别的事情时,就需要看看 UIView 中其他可以被重写的方法。

UIKit 有个十分古怪的地方,那是它的触摸事件处理系统。它主要包括两个方法,-pointInstide:withEvent:-hitTest:withEvent:

-pointInside: 会告诉调用者给定点是否包含在指定的视图区域中。而 -hitTest:pointInside: 这个方法来告诉调用者哪个子视图(如果有的话)是当前触摸在给定点的接收者。现在我比较感兴趣的是后面这个方法。

苹果的文档勉强能够让你理解怎么重新实现这个方法。在你学会怎么重新实现方法之前,你都不能改变它的功能。接下来让我们看一遍 文档,并尝试重写这个函数。

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// ...
}

首先,让我们从文档的第二段开始吧:

这个方法会忽略那些隐藏的视图,禁用用户交互视图和 alpha 等级小于 0.01 的视图。

让我们通过一些 gurad 语句来快速预处理这些前提条件。

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

guard isUserInteractionEnabled else { return nil }

guard !isHidden else { return nil }

guard alpha >= 0.01 else { return nil }

// ...

相当简单吧。那接下来是?

这个方法调用 pointInside:withEvent: 方法来遍历接收视图层级中每一个子视图,来决定哪个子视图来接收该触摸事件。

逐字阅读文档后,感觉 -pointInside: 会在每一个子视图里被调用(用一个 for 循环),但这并不是完全正确的。

感谢这个 读者。通过他在 -hitTest:-pointInside: 中放置了断点的试验,我们知道 -pointInside: 会在 self 中调用(在有上面那些 guard 的情况下),而不是在每一个子视图中。 所以应该添加另外的 guard 语句,像下面这行代码一样:

guard self.point(inside: point, with: event) else { return nil }

-pointInside:UIView 另一个需要重写的方法。它的默认实现会检查传入的某个点是否包含在视图的 bounds 中。如果调用 -pointInside 返回 true,那么意味着触摸事件发生在它的 bounds 中。

理解完这个小小的差别后,我们可以继续阅读文档了:

如果 -pointInside:withEvnet: 返回 YES,那么子视图的层级也会进行类似的遍历直到找到包含指定点的最前面的视图。

所以,从这里知道我们需要遍历视图树。这意味着循环遍历所有的视图,并调用 -hitTest: 在它们每一个上去找到合适的子视图。在这种情况下,这个方法是递归的。

为了遍历视图层级,我们需要一个循环。然而,这个方法其中一个更反人类的是需要反向遍历视图。子视图数组中尾部的视图反而会处在 Z 轴中更高的位置,所以它们应该被最先检验。(如果没有这篇 文章,我可记不起这个点。)

// ...
for subview in subviews.reversed() {

}
// ...

传入的坐标点会转换到当前视图的坐标系中,而非我们关心子视图中。幸运的是,UIKit 给了一个处理函数,去转换坐标点的参考系到其他任何的视图的 frame 的参考系中。

// ...
for subview in subviews.reversed() {
let convertedPoint = subview.convert(point, from: self)
// ...
}
// ...

一旦有了转换后的坐标点,我们就可以很简单地询问每一个子视图该点的目标视图。需要注意的是,如果点处于该视图外部(也就是说,-pointInside: 返回 false),-hitTest 会返回 nil。这时就应该检查层级里的下一个子视图。

// ...
let convertedPoint = subview.convert(point, from: self)
if let candidate = subview.hitTest(convertedPoint, with: event) {
return candidate
}
//...

一旦我们有了合适的循环语句,最后一件需要做的事是 return self。如果视图是可被点击(被我们的 guard 语句断言过的情况),但却没有子视图想要处理这个触摸的话,意味着当前视图,也就是 self,是这个触摸正确的目标。

这是完整的算法:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

guard isUserInteractionEnabled else { return nil }

guard !isHidden else { return nil }

guard alpha >= 0.01 else { return nil }

guard self.point(inside: point, with: event) else { return nil }

for subview in subviews.reversed() {
let convertedPoint = subview.convert(point, from: self)
if let candidate = subview.hitTest(convertedPoint, with: event) {
return candidate
}
}
return self
}

现在我们有了一个参考的实现,可以开始修改它来实现具体的行为。

在之前的这篇播客《Changing the size of a paging scroll view》中,我就已经讨论过其中一种行为。我谈到一种“落后并该被废弃”的方法来产生这种效果。本质上,你必须:

  1. 关掉 clipsToBounds
  2. 在滑动区域中放一个非隐藏视图
  3. 在非隐藏视图上重写 -hitTest: 来传递所有触摸到 scrollview 中

-hitTest: 方法是这种技术的基石。因为在 UIKit 中,hitTest 方法会代理给每一个视图去实现,决定触摸事件传递给哪个视图接收。这可以让你去重写默认的实现(期望和普通的实现)并替换它为你想做的,甚至返回一个不是原始视图的子视图。多么疯狂。

让我们看一下另一个例子。如果你已经用过 Beacon 今年的版本,你会注意到滑动删除事件行为的物理效果感觉上和其他用原生系统实现的效果有点不一样。这是因为用系统的途径不能完全获得我们想要的表现,所以需要自己重新实现这个功能。

如你所想,重写滑动和反弹物理效果不需要那么复杂,所以我们用一个 UIScrollView 和将 pagingEnabled 设为 true 来获得尽可能自由的反弹力。用和这篇旧博客里说的类似的技术,将滑动的视图的 bounds 设置得更小一些并将 panGestureRecognizer 移到事件的 cell 顶层的一个覆盖视图中,来设置一个自定义页面大小。

然而,当覆盖视图正确的传递触摸事件到 scroll view 时,那里会有覆盖视图不能正确拦截的其他事件。cell 包含着按钮,像 “join event” 按钮和 “delete event” 按钮,都需要接收触摸。有几种自定义实现在 -hitTest: 中可以处理这种情况,其中一种实现就是直接检查这两个按钮的子视图:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

guard isUserInteractionEnabled else { return nil }

guard !isHidden else { return nil }

guard alpha >= 0.01 else { return nil }

guard self.point(inside: point, with: event) else { return nil }

if joinButton.point(inside: convert(point, to: joinButton), with: event) {
return joinButton
}

if isDeleteButtonOpen && deleteButton.point(inside: convert(point, to: deleteButton), with: event) {
return deleteButton
}
return super.hitTest(point, with: event)
}

这种方法会正确地传递正确的点击事件到正确的的按钮中,而且不用打断显示删除按钮的滑动表现。(你可以尝试只忽略 deletionOverlay,不过它不会正确的传递滑动事件。)

-hitTest: 是视图中一个很少重写的地方,但是在需要时,可以提供其他工具很难做到的行为。理解如何自己实现有助于随意替换它。你可以用这个技术去扩大点击的目标区域,去除触摸处理中的某些子视图,而不用把它们从可见的层级中去掉,又或是用一个视图作为另一个将响应触摸的视图的兜底。所有东西都是可能的。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

❌
❌