iOS

SnapKit框架解析(一) —— SnapKit应用的一个简单

2019-07-02  本文已影响161人  刀客传奇

版本记录

版本号 时间
V1.0 2019.07.02 星期二

前言

OC中自动布局可以使用三方比如说Masonry,那么Swift呢?Swift也有很优秀的第三方框架,其中SnapKit就很优秀,接下来几篇我们就一起来看下这个框架。

开始

首先看下写作环境

Swift 5, iOS 12, Xcode 10

在本教程中,您将了解SnapKit,一种轻量级DSL(domain-specific language),使自动布局和约束变得轻而易举。

Auto Layout是一种功能强大的工具,用于描述应用程序中不同视图和复杂视图层次结构之间的关系和约束,但是编写这些约束通常在开始时非常不直观。

直到几年前,以编程方式编写这些约束对于使用Visual Formatting Language或手动创建NSLayoutConstraints等神秘和冗长的方法非常繁琐。

iOS 9通过引入布局锚点(Layout Anchors)大大改进了这些机制,这使得创建约束非常直观。 然而,仍然有很多东西需要让你创建约束更加快捷。 这正是SnapKit发挥作用的地方!

在本教程中,您将使用SnappyQuiz - 一个简单的游戏,玩家可以获得随机问题/陈述,并选择是真还是假。

打开SnappyQuiz.xcworkspace而不是项目文件 - 这很重要。

该项目包含一些您需要的Swift文件:

该项目还包括表示游戏状态的State.swift和找到原始问题数据的Questions.swift,但在本教程中您不会真正触及这些。

构建并运行项目。您应该看到倒数计时器滴答的第一个问题以及代表当前游戏进度的进度条:

QuizViewController + Constraints.swift中,探索setupConstraints()。此代码使用上述布局锚点来定义应用程序中不同视图之间的关系。

在本教程中,您将使用其SnapKit变体替换每个约束。


Snappin’ & Chainin’

在您实际修改SnappyQuiz应用程序之前,您应该更多地了解SnapKit的实际内容。在本教程的介绍中,我提到SnapKit使用DSL,但这究竟意味着什么?

1. What is a DSL?

域特定语言domain-specific language(DSL)是为表达和处理特定域或解决特定问题而创建的语言。

SnapKit的案例中,它旨在创建一种更直观,易于使用的语法,专门用于自动布局(Auto Layout)约束。

需要了解的一件重要事情是,作为DSLSnapKit主要是syntactic sugar - 你可以在没有SnapKit的情况下做任何SnapKit。但是,SnapKit提供了更流畅和富有表现力的语法来解决这个特定的领域和问题。

2. SnapKit Basics

采用一组非常常见的约束 - 将视图附加到其所有superview的边缘:

没有SnapKit,代码看起来类似于以下内容:

child.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
  child.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
  child.topAnchor.constraint(equalTo: parent.topAnchor),
  child.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
  child.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
])

这非常有声明性,但SnapKit可以做得更好。

SnapKit在系统中的每个UIView(和NSView,在macOS上)引入了一个名为snp的命名空间。 该命名空间以及makeConstraints(_ :)方法是SnapKit的精髓。

SnapKit表示这样的约束:

child.snp.makeConstraints { make in
  make.leading.equalToSuperview()
  make.top.equalToSuperview()
  make.trailing.equalToSuperview()
  make.bottom.equalToSuperview()
}

这可能看起来像是相似数量的代码,但它极大地提高了可读性。 你可能注意到的两件事是:

3. Composability & Chaining

你刚刚看到了你的第一个SnapKit代码,但是SnapKit真正发挥作用的是它的组合功能。 您可以将任何锚点链接在一起,以及约束本身。

您可以将上面的示例重写为:

child.snp.makeConstraints { make in
  make.leading.top.trailing.bottom.equalToSuperview()
}

或者更简洁

child.snp.makeConstraints { make in
  make.edges.equalToSuperview()
}

想在视图中添加16inset? 另一个简单的链接将帮助你实现:

child.snp.makeConstraints { make in
  make.edges.equalToSuperview().inset(16)
}

正如您所看到的,可组合性和链接(composability and chaining)SnapKit的核心,并提供了使用vanilla NSLayoutConstraints无法实现的表现力。


Your First Constraints

现在您已经掌握了SnapKit的一些基础知识,现在是时候转换setupConstraints()中的所有约束来使用它了。 它比你想象的要简单得多,你将逐一探讨SnapKit的各种功能。

返回QuizViewController + Constraints.swift并找到setupConstraints()。 您将开始修改updateProgress(to:0)行以下的约束。 稍后您将回到该行之上的约束。

找到以下代码块,定义计时器label的约束:

lblTimer.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
  lblTimer.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.45),
  lblTimer.heightAnchor.constraint(equalToConstant: 45),
  lblTimer.topAnchor.constraint(equalTo: viewProgress.bottomAnchor, constant: 32),
  lblTimer.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])

替换成下面内容

lblTimer.snp.makeConstraints { make in
  make.width.equalToSuperview().multipliedBy(0.45) // 1
  make.height.equalTo(45) // 2
  make.top.equalTo(viewProgress.snp.bottom).offset(32) // 3
  make.centerX.equalToSuperview() // 4
}

和以前一样,这是使用SnapKit的链接语法直接翻译原始约束。 快速进行分解下:

虽然与基于NSLayoutConstraint的代码没有太大区别,但它提供了更好的可读性和受约束的视图的范围。

注意:注意其他不同的东西? SnapKit不再要求您将translatesAutoresizingMaskIntoConstraints设置为false! 库为你做好。 不再忘记这样做并且不知疲倦地调试混乱的约束。

1. Do That Again

转到下一个UI元素 - 问题label。 找到以下代码:

lblQuestion.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
  lblQuestion.topAnchor.constraint(equalTo: lblTimer.bottomAnchor, constant: 24),
  lblQuestion.leadingAnchor
    .constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
  lblQuestion.trailingAnchor
    .constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16)
])

这里有三个限制。 在这一点上,逐一替换这些可能会让人感到熟悉。 第一个约束可以很容易地转换为:

make.top.equalTo(lblTimer.snp.bottom).offset(24)

最后两个约束也可以用同样的直接方式转化:

make.leading.equalToSuperview().offset(16)
make.trailing.equalToSuperview().offset(-16)

但实际上,您是否注意到这两个约束对leading锚点和trailing锚点执行相同的操作? 听起来像一个完美适合一些链接! 使用以下内容替换上面的整个代码块:

lblQuestion.snp.makeConstraints { make in
  make.top.equalTo(lblTimer.snp.bottom).offset(24)
  make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(16)
}

注意两件事:

另一个有趣的事实是inset选项不必是数字。 它也可以采用UIEdgeInsets结构。 您可以将上面的行重写为:

make.leading.trailing.equalTo(view.safeAreaLayoutGuide)
  .inset(UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16))

这在这里可能不太有用,但是当insets边缘不同时,它会变得非常有用。

两个限制下来,三个去!

2. A Quick Challenge!

下一个约束是您之前已经看到的约束 - 消息label的边缘应该简单地等于父视图的边缘。 你为什么不亲自尝试这个?

如果您遇到困难,请随时点击下面的按钮查看代码:

替换下面的内容

lblMessage.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
  lblMessage.topAnchor.constraint(equalTo: navView.topAnchor),
  lblMessage.bottomAnchor.constraint(equalTo: navView.bottomAnchor),
  lblMessage.leadingAnchor.constraint(equalTo: navView.leadingAnchor),
  lblMessage.trailingAnchor.constraint(equalTo: navView.trailingAnchor)
])

lblMessage.snp.makeConstraints { make in
  make.edges.equalToSuperview()
}

3. Final Constraint

移动到SnapKit的语法仍有一个最终约束。 水平UIStackView持有TrueFalse按钮。

找到以下代码:

svButtons.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
  svButtons.leadingAnchor.constraint(equalTo: lblQuestion.leadingAnchor),
  svButtons.trailingAnchor.constraint(equalTo: lblQuestion.trailingAnchor),
  svButtons.topAnchor.constraint(equalTo: lblQuestion.bottomAnchor, constant: 16),
  svButtons.heightAnchor.constraint(equalToConstant: 80)
])

像以前一样,leading and trailing约束可以链接,因为它们负责相同的关系。 但由于您不想为superview创建约束,这应该是什么样的?

用以下内容替换上面的代码:

svButtons.snp.makeConstraints { make in
  make.leading.trailing.equalTo(lblQuestion)
  make.top.equalTo(lblQuestion.snp.bottom).offset(16)
  make.height.equalTo(80)
}

注意makeConstraints闭包中的第一行 - 您只需定义前导和尾随约束应该等于lblQuestion! 不需要特殊性! SnapKit能够推断出你指的是lblQuestion的那些特定约束。

对于更简单的约束也是如此。 以下代码:

view.snp.makeConstraints { make in
  make.width.equalTo(otherView.snp.width)
  make.centerX.equalTo(otherView.snp.centerX)
}

可以被重写为

view.snp.makeConstraints { make in
  make.width.equalTo(otherView)
  make.centerX.equalTo(otherView)
}

请注意,不需要otherView的特性 - SnapKit根据关系中的第一个视图知道需要创建哪种约束。

您甚至可以通过简单编写来进一步减小代码大小:

view.snp.makeConstraints { make in
  make.width.centerX.equalTo(otherView)
}

哇! 多么酷啊?

构建并运行项目。 你会注意到它仍然像以前一样工作。很好~


Modifying Constraints

在本教程的前几节中,您学习了如何创建新约束。但是,有时您想要修改现有约束。

是时候尝试一些您可能想要这样做的用例,以及如何在SnapKit中实现这一点。

1. Updating a Constraint’s Constant

一些SnappyQuiz的用户对切换到横向时应用程序的外观非常沮丧。

当应用程序切换方向时,您可以通过修改UI的某些方面来使其更好,这样您就可以做到这一点。

对于此任务,您将以横向方向增加倒数计时器的高度,并增加字体大小。在此特定上下文中,您需要更新计时器标签的高度约束的常量。

当你只对更新常量感兴趣时,SnapKit有一个名为updateConstraints(_ :)的超级有用的方法,它在这里非常适合。

回到QuizViewController + Constraints.swift,在文件末尾添加以下代码:

// MARK: - Orientation Transition Handling
extension QuizViewController {
  override func willTransition(
    to newCollection: UITraitCollection,
    with coordinator: UIViewControllerTransitionCoordinator
    ) {
    super.willTransition(to: newCollection, with: coordinator)
    // 1
    let isPortrait = UIDevice.current.orientation.isPortrait
    
    // 2
    lblTimer.snp.updateConstraints { make in
      make.height.equalTo(isPortrait ? 45 : 65)
    }
    
    // 3
    lblTimer.font = UIFont.systemFont(ofSize: isPortrait ? 20 : 32, weight: .light)
  }
}

这增加了一个扩展,它将处理视图控制器的旋转。 这是代码的作用:

以为会很难吗? 抱歉让你失望!

构建并运行项目。 应用程序在模拟器中启动后,按Command-Right ArrowCommand-Left Arrow更改设备方向。 请注意label如何根据设备的方向增加其高度和字体大小。

2. Remaking Constraints

有时,您需要的不仅仅是修改一些常量。 您可能希望完全更改特定视图上的整个约束集。 对于那种非常常见的情况,SnapKit有另一个有用的方法 - 你猜对了--rekekeConstraints(_ :)

SnappyQuiz中有一个完美的地方可以尝试这种方法:顶部的进度条。 现在,进度条的宽度约束保存在QuizViewController.swift中名为progressConstraints的变量中。 然后,updateProgress(to :)简单地销毁旧约束并创建一个新约束。

是时候看看你是否可以让这个烂摊子好一点。

回到QuizViewController + Constraints.swift,看看updateProgress(to:)。 它检查是否已存在约束,如果是,则将其停用。 然后,它创建一个新约束并激活它。

用以下内容替换updateProgress(to :)

func updateProgress(to progress: Double) {
  viewProgress.snp.remakeConstraints { make in
    make.top.equalTo(view.safeAreaLayoutGuide)
    make.width.equalToSuperview().multipliedBy(progress)
    make.height.equalTo(32)
    make.leading.equalToSuperview()
  }
}

哇,这更好! 整个有点神秘的代码片段完全被几行代码所取代。 remakeConstraints(_ :)只是每次都替换整个约束集,因此您不必手动引用约束并对其进行管理。

另一个好处是你可以进一步清理当前代码中的一些混乱。

setupConstraints()中,删除以下代码:

guard let navView = navigationController?.view else { return }

viewProgress.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
  viewProgress.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
  viewProgress.heightAnchor.constraint(equalToConstant: 32),
  viewProgress.leadingAnchor.constraint(equalTo: view.leadingAnchor)
])

该方法的第一行现在应该只是updateProgress(to:0)

最后,您可以在QuizViewController.swift中删除以下行:

/// Progress bar constraint
var progressConstraint: NSLayoutConstraint!

全部完成! 构建并运行您的应用程序,一切都应该像以前一样工作,但具有更清晰的约束管理代码。

3. Keeping a Reference

虽然你不会在SnappyQuiz中试验这个选项,但它仍然是你应该知道的。

在标准的NSLayoutConstraint方式中,您可以存储对约束的引用并在以后修改它。 SnapKit也可以使用Constraint类型:

var topConstraint: Constraint?

lblTimer.snp.makeConstraints { make in 
  // Store your constraint
  self.topConstraint = make.top.equalToSuperview().inset(16)
  make.leading.trailing.bottom.equalToSuperView()
}

// Which you can later modify
self.topConstraint?.update(inset: 32)

// Or entirely deactivate
self.topConstraint?.deactivate()

When Things Go Wrong

有时在生活中,事情出了问题。 在谈论自动布局(Auto Layout)约束时,情况更是如此。

回到QuizViewController+Constraints.swift,找到以下行:

make.centerX.equalToSuperview()

在它下面,但仍然在makeConstraints闭包内,添加:

make.centerY.equalToSuperview()

构建并运行应用程序。 正如您所看到的,UI完全被破坏了:

此外,正如预期的那样,您将在调试控制台中看到一个破坏约束的巨大墙,其外观应类似于以下内容:

[LayoutConstraints] Unable to simultaneously satisfy constraints.

"<SnapKit.LayoutConstraint:0x600001b251a0@QuizViewController+Constraints.swift#62 UIView:0x7f9371e004a0.top == UILayoutGuide:0x60000062c0e0.top>",
    "<SnapKit.LayoutConstraint:0x600001b25260@QuizViewController+Constraints.swift#64 UIView:0x7f9371e004a0.height == 32.0>",
    "<SnapKit.LayoutConstraint:0x600001b2dc80@QuizViewController+Constraints.swift#38 UILabel:0x7f9371e088c0.height == 45.0>",
    "<SnapKit.LayoutConstraint:0x600001b2dce0@QuizViewController+Constraints.swift#39 UILabel:0x7f9371e088c0.top == UIView:0x7f9371e004a0.bottom + 32.0>",
    "<SnapKit.LayoutConstraint:0x600001b2dda0@QuizViewController+Constraints.swift#41 UILabel:0x7f9371e088c0.centerY == UIView:0x7f9371e09a50.centerY>",
    "<NSLayoutConstraint:0x600001c6c2d0 'UIView-Encapsulated-Layout-Height' UIView:0x7f9371e09a50.height == 551   (active)>",
    "<NSLayoutConstraint:0x600001c61450 'UIViewSafeAreaLayoutGuide-top' V:|-(0)-[UILayoutGuide:0x60000062c0e0'UIViewSafeAreaLayoutGuide']   (active, names: '|':UIView:0x7f9371e09a50 )>"

Will attempt to recover by breaking constraint 
<SnapKit.LayoutConstraint:0x600001b2dc80@QuizViewController+Constraints.swift#38 UILabel:0x7fc53e41d060.height == 45.0>

好家伙。 你甚至从哪里开始? 所有你看到的是一堆内存地址,并不一定意味着太多。 理解哪些约束被打破也很困难。

幸运的是,SnapKit提供了一个很好的附加修饰符来跟踪这些问题,称为labeled(_:)

用以下内容替换整个lblTimer约束块:

lblTimer.snp.makeConstraints { make in
  make.width.equalToSuperview().multipliedBy(0.45).labeled("timerWidth")
  make.height.equalTo(45).labeled("timerHeight")
  make.top.equalTo(viewProgress.snp.bottom).offset(32).labeled("timerTop")
  make.centerX.equalToSuperview().labeled("timerCenterX")
  make.centerY.equalToSuperview().labeled("timerCenterY")
}

注意到每个约束的labeled(_:)添加? 这允许您为每个约束附加描述性标题,因此您不必选择内存地址并失去理智。

最后一次构建并运行您的应用程序。 你破坏的约束在这一点上应该提供更清晰的信息:

[LayoutConstraints] Unable to simultaneously satisfy constraints.

"<SnapKit.LayoutConstraint:0x60000365c4e0@QuizViewController+Constraints.swift#62 UIView:0x7fc53e4181d0.top == UILayoutGuide:0x600002b0ae60.top>",
"<SnapKit.LayoutConstraint:0x60000365e8e0@QuizViewController+Constraints.swift#64 UIView:0x7fc53e4181d0.height == 32.0>",
"<SnapKit.LayoutConstraint:timerCenterY@QuizViewController+Constraints.swift#41 UILabel:0x7fc53e41d060.centerY == UIView:0x7fc4fe507170.centerY>",
"<SnapKit.LayoutConstraint:timerHeight@QuizViewController+Constraints.swift#38 UILabel:0x7fc53e41d060.height == 45.0>",
"<SnapKit.LayoutConstraint:timerTop@QuizViewController+Constraints.swift#39 UILabel:0x7fc53e41d060.top == UIView:0x7fc53e4181d0.bottom + 32.0>",
"<NSLayoutConstraint:0x6000031346e0 'UIView-Encapsulated-Layout-Height' UIView:0x7fc4fe507170.height == 551   (active)>",
"<NSLayoutConstraint:0x600003139c70 'UIViewSafeAreaLayoutGuide-top' V:|-(0)-[UILayoutGuide:0x600002b0ae60'UIViewSafeAreaLayoutGuide']   (active, names: '|':UIView:0x7fc4fe507170 )>"

Will attempt to recover by breaking constraint 
<SnapKit.LayoutConstraint:timerHeight@QuizViewController+Constraints.swift#38 UILabel:0x7fc53e41d060.height == 45.0>

这看起来很相似,但要仔细看。 你可以看到像timerCenterY这样的宝石。 这提供了更多信息,并且您有一些很好的标记限制来开始调试。

更具体地说,您可以在此输出中识别的唯一三个标签是timerCenterYtimerHeighttimerTop。 由于高度是静态的,您可以确定冲突是在两个约束之间留下的。 这比缩小自动布局调试输出的原始混乱要快得多!

完成后,随意删除开始此混乱的centerY约束。

恭喜! 您现在已经了解了SnapKit所提供的大部分内容,但您仍需要考虑一些功能和修饰符,例如priority, divided等等。 查看SnapKit的官方GitHub仓库SnapKit’s official GitHub repo了解更多信息。

请记住,SnapKit可以帮助您创建一个易于使用,特定于问题的语法来创建约束,但它不提供常规NSLayoutConstraints无法实现的功能。 随意尝试两者并找到适合每种情况的良好中间立场。

后记

本篇主要讲述了SnapKit自动布局的框架,感兴趣的给个赞或者关注~~~

上一篇下一篇

猜你喜欢

热点阅读