版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.05.16 星期四 |
前言
iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)
开始
首先看下写作环境
Swift 4.2, iOS 12, Xcode 10
许多iOS应用程序需要一个菜单来在视图之间导航或让用户做出选择。 一种常用的设计是侧面菜单。
您可以使用简单的表单轻松制作侧边菜单,但是如何在UI中引入一些乐趣呢? 你想在用户的脸上露出微笑,并一次又一次地将它们带回你的应用程序。 实现此目的的一种方法是创建3D侧边栏动画。
在本教程中,您将学习如何通过操纵CALayer属性来创建3D侧边栏动画来为一些UIView元素设置动画。 这个动画的灵感来自一个名为Taasky
的To-Do
应用程序。
在本教程中,您将使用以下元素:
Storyboards
Auto Layout constraints
UIScrollView
View controller containment
Core Animation
打开名为TaskChooser
的入门项目。
想象一下,您正在创建一个与您的同事或朋友谈判活动的基本应用程序。如果你在里面竖起大拇指,如果你不能成功,请大拇指向下。你甚至可以因天气恶劣而下降。
花点时间看一下这个项目。你会看到它是一个标准的Xcode Master-Detail
模板应用程序,它显示了一个图像表。
-
MenuViewController:一个
UITableViewController
,它使用自定义表格视图单元MenuItemCell
来设置每个单元格的背景颜色。它还有一个图像。 -
MenuDataSource:实现
UITableViewDataSource
以从MenuItems.json
提供表数据的对象。这些数据可能来自生产情况下的服务器。 - DetailViewController:使用与您选择的单元格相同的背景颜色显示大图像。
构建并运行应用程序。您应该看到启动项目加载了7行颜色和图标:
使用菜单显示您选择的选项:
这是功能性的,但外观和感觉相当普通。 你希望你的应用程序既令人惊喜又高兴!
在本教程中,您将把Master-Detail
应用程序重构为水平滚动视图。 您将在容器视图中嵌入master
和 detail
视图。
接下来,您将添加一个按钮来显示或隐藏菜单。 然后,您将在菜单上添加整齐的3D折叠效果。
作为此3D动画侧边栏的最后一步,您将同步旋转菜单按钮以显示或隐藏菜单。
您的第一个任务是将MenuViewController
和DetailViewController
转换为滑出侧边栏,其中滚动视图包含菜单和详细视图并排。
Restructuring Your Storyboard
在重建菜单之前,您需要进行一些拆卸。
在Project
导航器的Views
文件夹中打开Main.storyboard
。 你可以看到由segues
连接的UINavigationController
,MenuViewController
和DetailViewController
:
1. Deleting the Old Structure
导航控制器场景(Navigation Controller Scene)
不会激发快乐。 选择该场景并将其删除。 接下来,选择MenuViewController
和DetailViewController
之间的segue
并删除它。
完成后,开始工作。
2. Adding a New Root Container
由于UINavigationController
消失了,您不再拥有项目中视图控制器的顶级容器。 你现在就加一个。
在Project
导航器中选择Views
文件夹。 按Command-N
将新文件添加到项目中。 然后:
- 1) 选择
iOS▸CocoaTouch Class
。 点击Next
。 - 2) 将类命名为
RootViewController
。 - 3) 确保
RootViewController
是UIViewController
的子类。
- 4) 确保未选中
Also create XIB file
。 - 5) 语言应该是
Swift
。
再次打开Main.storyboard
。
使用快捷键Command-Shift-L
打开对象库,并将UIViewController
的实例拖到故事板。
从对象层次结构中选择View Controller Scene
,然后打开Identity inspector
。 将Class
区域设置为RootViewController
。
接下来,打开Attributes inspector
,然后选中Is Initial View Controller
框。
3. Adding Identifiers to View Controllers
由于MenuViewController
和DetailViewController
不再通过segues
连接,因此您需要一种从代码中访问它们的方法。 因此,您的下一步是提供一些标识符来执行此操作。
从对象层次结构中选择Menu View Controller Scene
。 打开Identity inspector
并将Storyboard ID
设置为MenuViewController
。
这个字符串可以是任何合理的值,但一个易于记忆的技术是使用类的名称。
接下来,从Object
层次结构中选择Detail View Controller Scene
并执行相同的操作。 将Storyboard ID
设置为DetailViewController
。
这就是你需要在Main.storyboard
中做的所有事情。 本教程的其余部分将在代码中。
Creating Contained View Controllers
在本节中,您将创建一个UIScrollView
并向该滚动视图添加两个容器。 容器将保存MenuViewController
和DetailViewController
。
1. Creating a Scroll View
您的第一步是创建UIScrollView
。
在Project
导航器中打开RootViewController.swift
。 删除Xcode从RootViewController
内部提供的所有内容。
在RootViewController
上面添加此扩展:
extension UIView {
func embedInsideSafeArea(_ subview: UIView) {
addSubview(subview)
subview.translatesAutoresizingMaskIntoConstraints = false
subview.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor)
.isActive = true
subview.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor)
.isActive = true
subview.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor)
.isActive = true
subview.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor)
.isActive = true
}
}
这是一个帮助方法,您将在本教程中使用几次。 代码将传入的视图添加为子视图,然后添加四个约束以将子视图粘贴到其自身内。
接下来在文件末尾添加此扩展名:
extension RootViewController: UIScrollViewDelegate {
}
您将需要监听UIScrollView
以进行更改。 该操作稍后在本教程中进行,因此此扩展目前为空。
最后,在RootViewController
中插入以下代码:
// 1
lazy var scroller: UIScrollView = {
let scroller = UIScrollView(frame: .zero)
scroller.isPagingEnabled = true
scroller.delaysContentTouches = false
scroller.bounces = false
scroller.showsHorizontalScrollIndicator = false
scroller.delegate = self
return scroller
}()
// 2
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(named: "rw-dark")
view.embedInsideSafeArea(scroller)
}
// 3
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
以下是您在此代码中所做的事情:
- 1) 首先,创建一个
UIScrollView
。 您希望启用分页,以便内容在滚动视图内以原子单位移动。 您已禁用delayedContentTouches
,以便内部控制器能够快速响应用户触摸。bounces
设置为false
,因此您不会从滚动条获得弹性感。 然后,将RootViewController
设置为scroll view
的代理。 - 2) 在
viewDidLoad()
中,您可以使用之前添加的帮助方法设置背景颜色并将scroll view
嵌入根视图中。 - 3) 对
preferredStatusBarStyle
的覆盖允许状态栏在深色背景上显示为浅色。
构建并运行您的应用程序,以显示它在此重构后正确启动:
由于您尚未将按钮和内容添加到新的RootViewController
,因此您应该只能看到已设置的深色背景。 别担心,您将在下一节中将它们添加回来。
2. Creating Containers
现在,您将创建UIView
实例,它们将充当MenuViewController
和DetailViewController
的容器。 然后,您将它们添加到scroll view
。
在RootViewController
的顶部添加这些属性:
let menuWidth: CGFloat = 80.0
var menuContainer = UIView(frame: .zero)
var detailContainer = UIView(frame: .zero)
接下来,将此方法添加到RootViewController
:
func installMenuContainer() {
// 1
scroller.addSubview(menuContainer)
menuContainer.translatesAutoresizingMaskIntoConstraints = false
menuContainer.backgroundColor = .orange
// 2
menuContainer.leadingAnchor.constraint(equalTo: scroller.leadingAnchor)
.isActive = true
menuContainer.topAnchor.constraint(equalTo: scroller.topAnchor)
.isActive = true
menuContainer.bottomAnchor.constraint(equalTo: scroller.bottomAnchor)
.isActive = true
// 3
menuContainer.widthAnchor.constraint(equalToConstant: menuWidth)
.isActive = true
menuContainer.heightAnchor.constraint(equalTo: scroller.heightAnchor)
.isActive = true
}
以下是您使用此代码所做的事情:
- 1) 添加
menuContainer
作为scroller
的子视图,并为其添加临时颜色。 在开发过程中使用非品牌颜色是了解开发过程中工作进展的好方法。 - 2) 接下来,将
menuContainer
的顶部和底部固定到scroll view
的相同边缘。 - 3) 最后,将
width
设置为80.0
的常量值,并将容器的高度固定为scroll view
的高度。
接下来,将以下方法添加到RootViewController
:
func installDetailContainer() {
//1
scroller.addSubview(detailContainer)
detailContainer.translatesAutoresizingMaskIntoConstraints = false
detailContainer.backgroundColor = .red
//2
detailContainer.trailingAnchor.constraint(equalTo: scroller.trailingAnchor)
.isActive = true
detailContainer.topAnchor.constraint(equalTo: scroller.topAnchor)
.isActive = true
detailContainer.bottomAnchor.constraint(equalTo: scroller.bottomAnchor)
.isActive = true
//3
detailContainer.leadingAnchor
.constraint(equalTo: menuContainer.trailingAnchor)
.isActive = true
detailContainer.widthAnchor.constraint(equalTo: scroller.widthAnchor)
.isActive = true
}
- 1) 与
installMenuContainer
类似,您将detailContainer
作为子视图添加到scroll view
。 - 2) 顶部,底部和右侧边缘固定到它们各自的
scroll view
边缘。detailContainer
的leading edge
连接到menuContainer
。 - 3) 最后,容器的宽度始终与
scroll view
的宽度相同。
要让UIScrollView
滚动其内容,它需要知道该内容有多大。 您可以通过使用UIScrollView
的contentSize
属性或隐式定义内容的大小来实现。
在这种情况下,内容大小由五件事隐式定义:
- 1)
menu container
高度==scroll view
高度 - 2)
detail container
的后缘固定到menu container
的前缘 - 3)
menu container
的宽度==80
- 4)
detail container
的宽度==scroll view
的宽度 - 5) 外部
detail and menu container
的边缘锚定到scroller
的边缘
最后要做的是使用这两种方法。 在viewDidLoad()
的末尾添加这些行:
installMenuContainer()
installDetailContainer()
构建并运行您的应用程序,看看一些糖果色的奇迹。 您可以拖动内容以隐藏橙色菜单容器。 您已经可以看到成品开始形成。
3. Adding Contained View Controllers
您正在构建创建界面所需的视图堆栈。 下一步是在您创建的容器中安装MenuViewController
和DetailViewController
。
你仍然想要一个导航栏,因为你想要一个放置菜单显示按钮的地方。 将此扩展添加到RootViewController.swift
的末尾:
extension RootViewController {
func installInNavigationController(_ rootController: UIViewController)
-> UINavigationController {
let nav = UINavigationController(rootViewController: rootController)
//1
nav.navigationBar.barTintColor = UIColor(named: "rw-dark")
nav.navigationBar.tintColor = UIColor(named: "rw-light")
nav.navigationBar.isTranslucent = false
nav.navigationBar.clipsToBounds = true
//2
addChild(nav)
return nav
}
}
以下是此代码中发生的情况:
- 1) 此方法采用视图控制器,将其安装在
UINavigationController
中,然后设置导航栏的视觉样式。 - 2) 视图控制器包含的最重要部分是
addChild(nav)
。 这会将UINavigationController
安装为RootViewController
的子视图控制器。 这意味着在iPad
上旋转或拆分视图导致的特征变化等事件可以在层次结构中向下传播给子节点。
接下来,在installInNavigationController(_ :)
之后将此方法添加到同一扩展中以帮助安装MenuViewController
和DetailViewController
:
func installFromStoryboard(_ identifier: String,
into container: UIView)
-> UIViewController {
guard let viewController = storyboard?
.instantiateViewController(withIdentifier: identifier) else {
fatalError("broken storyboard expected \(identifier) to be available")
}
let nav = installInNavigationController(viewController)
container.embedInsideSafeArea(nav.view)
return viewController
}
此方法从故事板中实例化视图控制器,警告开发人员故事板中断。
然后,代码将视图控制器放在UINavigationController
中,并将该导航控制器嵌入到容器中。
接下来,在主类中添加这些属性以跟踪MenuViewController
和DetailViewController
:
var menuViewController: MenuViewController?
var detailViewController: DetailViewController?
然后在viewDidLoad()
的末尾插入这些行:
menuViewController =
installFromStoryboard("MenuViewController",
into: menuContainer) as? MenuViewController
detailViewController =
installFromStoryboard("DetailViewController",
into: detailContainer) as? DetailViewController
在这个片段中,您实例化了MenuViewController
和DetailViewController
并保留了对它们的引用,因为稍后您将需要它们。
构建并运行应用程序,您将看到菜单可见,虽然比以前更瘦。
这些按钮不会导致DetailViewController
更新,因为segue
不再存在。 你将在下一节中解决这个问题。
您已完成本教程的视图包含部分。 现在你可以进入真正有趣的东西了。
Reconnect Menu and Detail Views
在你进行拆除横冲直撞之前,在MenuViewController
中选择一个表格单元格会触发一个segue
,它将选定的MenuItem
传递给DetailViewController
。
它很便宜而且完成了工作,但是有一个小问题。 该模式需要MenuViewController
了解DetailViewController
。
这意味着MenuViewController
与DetailViewController
紧密绑定。 如果您不再想使用DetailViewController
来显示菜单选项的结果,会发生什么?
作为优秀的开发人员,您应该寻求减少系统中的紧密绑定量。 您现在将设置一个新模式。
1. Creating a Delegate Protocol
首先要做的是在MenuViewController
中创建一个委托协议,它允许您传达菜单选择更改。
在Project
导航器中找到MenuViewController.swift
并打开该文件。
由于您不再使用segue
,您可以继续删除prepare(for:sender :)
。
接下来,在MenuViewController
类声明之上添加此协议定义:
protocol MenuDelegate: class {
func didSelectMenuItem(_ item: MenuItem)
}
接下来,在MenuViewController
的主体中插入以下代码:
//1
weak var delegate: MenuDelegate?
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
//2
let item = datasource.menuItems[indexPath.row]
delegate?.didSelectMenuItem(item)
//3
DispatchQueue.main.async {
tableView.deselectRow(at: indexPath, animated: true)
}
}
这是代码的作用:
- 1) 在第一个代码片段中,您声明了一个感兴趣的各方可以采用的协议。 在
MenuViewController
中,您声明了一个weak delegate
属性。 在协议引用中使用weak
有助于避免创建保留周期。 - 2) 接下来,实现
UITableViewDelegate
方法tableView(_:didSelectRowAt :)
将选定的MenuItem
传递给委托。 - 3) 最后一个声明是取消选择单元格并删除其突出显示的整体操作。
2. Implementing the MenuDelegate Protocol
您现在可以实现您创建的协议,以将选择更改发送到DetailViewController
。
打开RootViewController.swift
并将此扩展名添加到文件末尾:
extension RootViewController: MenuDelegate {
func didSelectMenuItem(_ item: MenuItem) {
detailViewController?.menuItem = item
}
}
此代码声明RootViewController
采用MenuDelegate
。 当您选择一个菜单项时,RootViewController
通过将选定的MenuItem
传递给实例来告诉DetailViewController
该更改。
最后,在viewDidLoad()
的末尾插入此行:
menuViewController?.delegate = self
这告诉MenuViewController
RootViewController
是委托。
构建并运行应用程序。 您的菜单选择现在将更改DetailViewController
的内容。 竖起大拇指。
Controlling the Scroll View
到现在为止还挺好。 你的菜单工作,应用程序看起来更好。
但是,您还会注意到手动滚动菜单不会持续很长时间。 菜单总是反弹回视图。
滚动视图属性isPagingEnabled
会导致该效果,因为您已将其设置为true
。 你现在就解决这个问题。
仍然在RootViewController
中工作,在下面添加以下行:menuWidth:CGFloat = 80.0
:
lazy var threshold = menuWidth/2.0
在这里,您可以选择一个任意点,菜单将选择隐藏或显示自己。 你使用lazy
,因为你正在计算相对于menuWidth
的值。
在RootViewController
中找到extension RootViewController: UIScrollViewDelegate
并在扩展中插入此代码:
//1
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset
scrollView.isPagingEnabled = offset.x < threshold
}
//2
func scrollViewDidEndDragging(_ scrollView: UIScrollView,
willDecelerate decelerate: Bool) {
let offset = scrollView.contentOffset
if offset.x > threshold {
hideMenu()
}
}
//3
func moveMenu(nextPosition: CGFloat) {
let nextOffset = CGPoint(x: nextPosition, y: 0)
scroller.setContentOffset(nextOffset, animated: true)
}
func hideMenu() {
moveMenu(nextPosition: menuWidth)
}
func showMenu() {
moveMenu(nextPosition: 0)
}
func toggleMenu() {
let menuIsHidden = scroller.contentOffset.x > threshold
if menuIsHidden {
showMenu()
} else {
hideMenu()
}
}
看看这段代码的作用:
- 1) 第一个
UIScrollViewDelegate
方法,scrollViewDidScroll(_ :)
,非常有用。 它总是会告诉您何时更改了scroll view
的contentOffset
。 您可以根据水平偏移量是否高于阈值threshold
来设置isPagingEnabled
。 - 2) 接下来,实现
scrollViewDidEndDragging(_:willDecelerate :)
以检测滚动视图上的抬起触摸。 只要内容偏移量大于阈值,就隐藏菜单;否则分页效果将保持并显示菜单。 - 3) 最后一种方法是帮助菜单将菜单设置到位置:显示,隐藏和切换。
构建并运行您的应用程序。 现在,尝试拖动scroll view
,看看会发生什么。 越过阈值时,菜单会弹出或关闭:
Adding a Menu Button
在本节中,您将向导航栏添加汉堡(burger)
按钮,这样您的用户就不必拖动来显示和隐藏菜单。
因为您想稍后为此按钮设置动画,所以这需要是UIView
而不是基于图像的UIBarButton
。
1. Creating a Hamburger View
在Project
导航器中选择Views
文件夹,然后添加一个新的Swift文件。
- 1) 选择
iOS▸CocoaTouch Class
。 点击Next
。 - 2) 将类命名为
HamburgerView
。 - 3) 确保
HamburgerView
是UIView
的子类。 - 4) 语言应该是
Swift
。
打开HamburgerView.swift
并使用以下代码替换HamburgerView
类中的所有内容:
//1
let imageView: UIImageView = {
let view = UIImageView(image: UIImage(imageLiteralResourceName: "Hamburger"))
view.contentMode = .center
return view
}()
//2
required override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
addSubview(imageView)
}
这是你在这里做的事情:
- 1) 首先,使用库中的资源创建
UIImageView
。 - 2) 然后添加该图像视图,为两种可能的
init
方法创建路径。
2. Installing the Hamburger View
现在您有了一个视图,您可以将其安装在属于DetailViewController
的导航栏中。
再次打开RootViewController.swift
并在主RootViewController
类的顶部插入此属性:
var hamburgerView: HamburgerView?
接下来将此扩展名附加到文件末尾:
extension RootViewController {
func installBurger(in viewController: UIViewController) {
let action = #selector(burgerTapped(_:))
let tapGestureRecognizer = UITapGestureRecognizer(target: self,
action: action)
let burger = HamburgerView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
burger.addGestureRecognizer(tapGestureRecognizer)
viewController.navigationItem.leftBarButtonItem
= UIBarButtonItem(customView: burger)
hamburgerView = burger
}
@objc func burgerTapped(_ sender: Any) {
toggleMenu()
}
}
最后将此语句添加到viewDidLoad()
的底部:
if let detailViewController = detailViewController {
installBurger(in: detailViewController)
}
这组代码为汉堡按钮提供了一个实例变量,因为您很快就想要为它设置动画。然后,您可以创建一个方法,以在任何视图控制器的导航栏中安装汉堡。
方法installBurger(in :)
在视图中创建一个tap
方法,调用方法burgerTapped(_ :)
。
请注意,您必须使用@objc
注释burgerTapped(_ :)
,因为您在此处使用Objective-C
运行时。此方法根据当前状态切换菜单。
然后使用此方法在属于DetailViewController
的UINavigationBar
中安装按钮。从体系结构的角度来看,DetailViewController
不知道这个按钮,也不需要处理任何菜单状态操作。你保持责任分离。
就这些。在构建对象堆栈时,使3D侧边栏动画生动的步骤变得越来越少。
构建并运行您的应用程序。你会看到你现在有一个汉堡按钮可以切换菜单。
Adding Perspective to the Menu
为了回顾你到目前为止所做的事情,你已经将Master-Detail
应用程序重构为可行的侧面菜单式应用程序,用户可以拖动或使用按钮来显示和隐藏菜单。
现在,为您的下一步:菜单的动画版本应该看起来像一个面板打开和关闭。 菜单按钮将在菜单打开时顺时针旋转,在菜单关闭时逆时针旋转。
为此,您将计算可见的菜单视图的分数,然后使用它来计算菜单的旋转角度。
1. Manipulating the Menu Layer
仍在RootViewController.swift
中,将此扩展名添加到文件中:
extension RootViewController {
func transformForFraction(_ fraction: CGFloat, ofWidth width: CGFloat)
-> CATransform3D {
//1
var identity = CATransform3DIdentity
identity.m34 = -1.0 / 1000.0
//2
let angle = -fraction * .pi/2.0
let xOffset = width/2.0 + width * fraction/4.0
//3
let rotateTransform = CATransform3DRotate(identity, angle, 0.0, 1.0, 0.0)
let translateTransform = CATransform3DMakeTranslation(xOffset, 0.0, 0.0)
return CATransform3DConcat(rotateTransform, translateTransform)
}
}
这是transformForFraction(_:ofWidth :)
的逐个分析:
- 1)
CATransform3DIdentity
是一个4×4
矩阵,其中对角线为1,其他地方为零。CATransform3DIdentity
的m34
属性是第3行第4列中的值,它控制转换中的透视量。 - 2) 角度和偏移量作为输入
fraction
的函数计算。 当fraction
为1.0
时,菜单将完全隐藏,当它为0.0
时,菜单将完全可见。 - 3) 计算最终变换。
CATransform3DRotate
使用angle
来确定围绕y轴的旋转量:-90
度使菜单垂直于视图的背面,0
度渲染菜单与x-y
平面平行,CATransform3DMakeTranslation
将菜单移动到中心的右侧, 和CATransform3DConcat
连接translateTransform
和rotateTransform
,以便菜单在旋转时显示为侧滑。
注意:
m34
值通常计算为1
除以表示观察者在z轴上的位置的数字,同时观察2D x-y
平面。 负z
值表示观察者在平面前面,而正z
值表示观察者在平面后面。在此
viewer
与平面中对象边缘之间绘制线条会产生3D透视效果。 当viewer
移动得更远时,视角不太明显。 尝试更改1,000
到500
或2,000
,以查看菜单的透视图如何更改。
接下来,将此扩展添加到RootViewController.swift
:
extension RootViewController {
//1
func calculateMenuDisplayFraction(_ scrollview: UIScrollView) -> CGFloat {
let fraction = scrollview.contentOffset.x/menuWidth
let clamped = Swift.min(Swift.max(0, fraction), 1.0)
return clamped
}
//2
func updateViewVisibility(_ container: UIView, fraction: CGFloat) {
container.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
container.layer.transform = transformForFraction(fraction,
ofWidth: menuWidth)
container.alpha = 1.0 - fraction
}
}
此代码提供了一些用于打开和关闭菜单的帮助程序:
- 1)
calculateMenuDisplayFraction(_ :)
将原始水平偏移量转换为相对于菜单宽度的1.0的fraction
。 该值夹在0.0和1.0之间。 - 2)
updateViewVisibility(_:fraction :)
将分数生成的变换应用于视图层。anchorPoint
是转换应用的铰链,因此CGPoint(x:1.0,y:0.5)
表示右手边缘和垂直中心。
通过设置alpha
,随着转换的进行,视图也会变暗。
现在,找到scrollViewDidScroll(_ :)
并在方法的末尾插入这些行:
let fraction = calculateMenuDisplayFraction(scrollView)
updateViewVisibility(menuContainer, fraction: fraction)
构建并运行应用程序。 当您向左拖动详细视图时,菜单现在似乎在细节视图下折叠。
Rotating the Burger Button
在本教程中,您要做的最后一件事是在scroll view
移动时使汉堡按钮看起来在屏幕上滚动。
打开HamburgerView.swift
并将此方法插入到类中:
func setFractionOpen(_ fraction: CGFloat) {
let angle = fraction * .pi/2.0
imageView.transform = CGAffineTransform(rotationAngle: angle)
}
此代码将视图作为fraction
的函数旋转。 当菜单完全打开时,视图旋转90度。
返回RootViewController.swift
。 找到scrollViewDidScroll(_ :)
并将此行追加到方法的末尾:
hamburgerView?.setFractionOpen(1.0 - fraction)
当scroll view
移动时,这会旋转汉堡按钮。
然后,因为启动应用程序时菜单已打开,所以将此行添加到viewDidLoad()
的末尾以将菜单置于正确的初始状态:
hamburgerView?.setFractionOpen(1.0)
构建并运行您的应用程序。 滑动并点按菜单以查看动态和同步的3D侧边栏动画:
在本教程中,您用到了:
- 视图控制器。
-
UIScrollView
隐式内容大小。 - 代理模式。
- 透视随
CATransform3D
和m34
而变化。
尝试使用m34
值来查看它对转换的影响。 如果您想了解有关m34
的更多信息,请阅读this xdPixel blog post。
Wikipedia
的Perspective页面有一些很好的照片,解释了视觉透视Perspective
的概念。
另外,请考虑如何在自己的应用程序中使用此3D侧边栏动画,为用户交互添加一点生命。 令人惊讶的是,对菜单这么简单的东西的微妙影响可以增加整个用户体验。
后记
本篇主要讲述了基于CALayer属性的一种3D边栏动画的实现,感兴趣的给个赞或者关注~~~