1. 1. 最终效果
  2. 2. 开始
  3. 3. 为UIScrollView添加约束
  4. 4. 创建UIScrollView接着,需要添加UIScrollView到ContainerViewController中,并添加约束。具体操作如下: 拖一个UIScrollView到ContainerViewController中,并让它填充整个ContentViewController; 取消横、纵滚动条的显示(Shows Horizontal Indicator与Shows Vertical Indicator); 取消Delays Content Touches,让响应事件及时传递给子控件,防止响应延迟; 将ContainerViewController设置为UIScrollView的delegate(按住control键,从UIScrollView拖到UIViewController)。 接下来是给UIScrollView添加约束的步骤: 找到右下角的Pin按钮,打开添加约束的弹窗; 取消Constrain to margins,防止系统自动加左右边距; 添加上下左右四个约束,并确保约束的值都是0; 点击Add 4 Constraints按钮,将约束加上。 仍然选中UIScrollView,打开Size Inspector选项(Xcode右边栏倒数第二个选项),确认一下约束是否与教程所加的约束相同: Trailing Space to: Superview Leading Space to: Superview Top Space to: Superview Bottom Space to: Bottom Layout Guide 如果有类似16这样的数字出现,说明在添加约束的时候,没有取消Constrain to margins。解决方案:删除UIScrollView的约束,重新添加一次,注意记得取消Constrain to margins。 在添加或修改约束以后,可能需要根据新的约束来展示frame。这个需求可以通过右下角Resolve Auto Layout Issues弹出框(Pin按钮旁边)的Update Frames选项来解决。 创建Content View接下来需要做的是添加一个Content View到UIScrollView上,并添加响应约束,这些约束对设置UIScrollView的contentSize很重要。在下一节中,要将添加菜单栏与详情两个容器视图添加到Content View上。 拖一个新的View到UIScrollView上,让它自动填满整个父视图,并将背景色设置为Default。 译者注:上图中,作者将Width设置为了680,但在文字描述中并未提到。这个宽度可加可不加,因为之后界面上的宽度其实全都要通过约束来表现。 选中刚加上的这个View,打开Identity Inspector,将Document**\Label设置为Content View**。这个名字将会在document outline(IB左边视图层级栏)显示出来,可以很容易追踪到这个View,并且在之后加与它相关的约束的时候,这个名字也会显示出来。 在给Content View添加约束的过程中,可以看到警告,不过不用惊慌,这些警告在这一节完之前能得到解决。   打开Pin弹出框,给Content View添加相对于父视图的约束。 在Size Inspector中,确保Trailing Space的约束值为0。 译者注:这里有点不明白作者的操作,直接在添加约束的时候,将右边距设置为0就可以了,但是作者却先将宽设置为680,然后添加约束的时候也是添加的-80,最后又改成0。感觉有点多此一举,还请大神们解答下。 现在之所以有警告是因为StoryBoard需要Content View的高和宽来设置contentSize。添加相对于Scroll View的父容器的这些约束,可以使之适配各种设备与横竖屏。 在document outline中,按住Control键,从Content View拖到View(Scroll View的父容器)。按住Shift键,选择Equal Widths与 Equal Heights。   然后将Equal Width约束改为80。 将约束改为80的用意是:这样Content View就会比View宽80,就可以刚好装下菜单栏。同时,可以发现之前的警告也不存在了——干得漂亮。 添加菜单栏与详情界面现在Scroll View上有Content View可以作为菜单栏和详情页的容器,然后将菜单栏与详情页(后文中称为Menu View与Detail View)嵌入Content View,这样便可以创建一个可以滑动显示.隐藏的Menu View。 首先,创建Menu Container View:拖一个Container View到Content View上,在Size Inspector中,将其宽度设为80,然后在Identity Inspector中将Document\Label设置为Menu Container View: 高度应该是默认的600,不过如果你不确定,也可以设置一下。 译者注:这里作者只提到设置宽度,但根据后文的一些描述,这里应该还需要将x设置为0,y设置为0。 然后,添加Detail Container View:新拖一个Container View到Content View上,并放置在menu container的右边。打开Size Inspector,并设置以下值: X: 80 Y: 0 Width: 600 Height: 600 接下来,打开Identity Inspector,将Document**\Label设置为Detail Container View**。 这样,Menu View与Detail View的宽度就和Content View相等了。 当添加完这两个自控制器后(container view),可在在IB中看到系统已经默认添加了contained view controller,但是我们需要用到已存在的Menu View与Detail View,所以需要在document outline或者画布中删除这两个控制器。 译者注:这个教程中的操作,都在文章开头下载的那个Demo中进行(称为starter project),所以,已存在的Menu View与Detail View就是指starter project中的控制器视图。 接下来,需要给两个container view添加约束。 给Menu Container View添加宽为80,上下左右边距为0,共5个约束。 给Detail Container View添加上、右、下边距为0,共3个约束;注意不要添加左边距约束,否则会和Menu Container View的右边距约束重复。 给两个Container View添加的约束都要确保Constrain to margins为取消。 嵌入Menu与Detail View Controllers这一节中,将会把starter project中的menu和detail view controllers拆开,然后分别嵌入Menu Container View和Detail Container View。 首先,将Container View Controller设为入口控制器:将原本指向Navigation Controller的入口箭头指向Container View Controller: 接下来,按住Control键,从Menu Container View拖到Navigation Controller,在弹出框中选择embed。 当Menu Container View嵌入Navigation Controller之后,相关的视图的宽度就都缩小到了80: 现在来调整一下menu与detail:首先,将table view cell中imageView的宽度调整为80: 然后,删除menu与detail之间的push连线。选中Detail View Controller,然后在Xcode菜单中选择Editor**\Embed In\Navigation Controller**。 现在detail应该加在了Navigation Controller上,同时拥有了黑色的导航栏。 选中这个新的Navigation Controller的导航栏,打开Attributes Inspector,选择Style为Blank,取消Translucent,并将Bar Tint设置为Black Color: 译者注:如果不好选中导航栏,可以在document outline中进行选择,注意不要选错了。 这个设置完以后,新加的导航栏就和第一个导航栏的样式一致了。 接着,打开Detail View Controller控制器的Attributes Inspector,确保View Controller**\Layout\Adjust Scroll View Insets**是处于选中状态。 Adjust Scroll View Insets的选中是为了将视图从Navigation Bar下面开始显示,而不会被Navigation Bar盖住 最后,将Detail View Controller嵌入Detail Container View:按住Control键,从Detail Container View拖到Detail View Controller的Navigation Controller,在弹出框中选择embed: 运行程序,左右拖动视图,就可以显示/隐藏菜单栏了。你是否注意到,在拖动Scroll View是,可以超出左右边界,而且还可以停止滚动,显示部分菜单这个问题? 为了修复这个问题,需要在Scroll View的Attributes Inspector中修改一下属性: 选中Scrolling\Paging Enabled属性,这样就可以…(译者注:原谅我找不到好的词语来表达原词了,原文中用到的是“snaps”,大概就是有一种惯性、巧劲的感觉,具体Paging Enabled的效果相信大家都懂的,就不说了); 取消Bounce\Bounces属性,防止左右滚出边距(译者注:即没有回弹效果)。 译者注:原文的动图没有截导航栏部分,我在跟着做的时候,发现菜单的导航栏上有白边,原因是Menu Container View与Detail Container View的背景色为默认色的原因,设置为黑色即可。 这时,在运行程序,就不会出现左右滑出边界的情况了,并且,也不会出现菜单栏可以显示一部分的情况。但是,在向左滑动,试图隐藏菜单栏时,又出现了新问题。 本来滚动隐藏的菜单栏又弹了出来。这个问题在StackOverflow上讨论过,我们将在关联Scroll View的时候再去解决它。 现在我们需要先解决另外一个问题:详情页是空白的,并且在点击菜单栏的时候,没有任何事件触发。 这完全在意料之中,因为还没有代码对container views进行关联。 对容器进行编码在开始之前,先将MenuViewController.swift中的viewDidLoad()复制到DetailViewController.swift中,如下: override func viewDidLoad() { super.viewDidLoad() // Remove the drop shadow from the navigation bar navigationController!.navigationBar.clipsToBounds = true} 这句代码的目的是不显示导航栏下面的那条阴影,虽然是个小细节,但就是这些细节使我们的App更加优雅。 当用户选中某个单元格时, MenuViewController必须要设置DetailViewController中的menuItem属性,但是这两个类已经没有直接联系了。所以,这两个类之间的通信将有ContainerViewController来代替。 在ContainerViewController.swift的顶部添加DetailViewController属性: private var detailViewController: DetailViewController? 在ContainerViewController.swift中实现prepareForSegue(_:sender:)这个方法,并添加以下代码: override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { // 译者注:这里的"DetailViewSegue"设置接下来的描述中讲解(就是这段代码下面的这段话) if segue.identifier == "DetailViewSegue" { let navigationController = segue.destinationViewController as! UINavigationController detailViewController = navigationController.topViewController as? DetailViewController }} segue.identifier是啥?在将DetailViewController嵌入到容器时,需要在Attributes inspector中将Storyboard Embed Segue的Identifier设置为DetailViewSegue。 然后,声明menuItem属性,并在didSet中,在它进行赋值时,也给DetailViewController中的menuItem属性进行赋值。 var menuItem: NSDictionary? { didSet { if let detailViewController = detailViewController { detailViewController.menuItem = menuItem } }} 这已不再是table view cell与content view之间的交互了,而是用户选择菜单时,MenuViewController需要做出响应。 译者注:上面这句话纠结好久,翻译出来还是不通顺,原话是这样的:There’s no longer a segue from a table view cell to the content view, but MenuViewController needs to respond when the user selects an item. 删除MenuVie**wController中prepareForSegue(:sender:)部分的代码,并且添加下面这段代码,注意,在选择方法的时候,不要选成tableView(:didDeselectRowAtIndexPath:)**而造成麻烦。 // MARK: UITableViewDelegateoverride func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { // 译者注:取消单元格选中 tableView.deselectRowAtIndexPath(indexPath, animated: true) // 译者注:从menuItems数组中取出menuItem字典,里面的值是根据MenuItems.plist生成的,包含小图,大图,背景色 let menuItem = menuItems[indexPath.row] as! NSDictionary // 译者注:当前的navigationController!.parentViewController就是ContainerViewController,因为navigationController的视图是作为80像素的Container View添加在ContainerViewController中的 (navigationController!.parentViewController as! ContainerViewController).menuItem = menuItem} 这里,只是简单将基于选择的单元行设置了ContainerViewController的menuItem属性。这将使属性的didSet方法触发(就是上面将menuItem赋值给detailViewController.menuItem的那段代码)。 最后,在MenuViewController.swift的viewDidLoad()方法中,添加以下代码: (navigationController!.parentViewController as! ContainerViewController).menuItem = (menuItems[0] as! NSDictionary) 这句代码将在App第一次加载的时候给detail view添加对应的图片。 运行程序,可以看到程序加载以后,详情页已经有了图片,菜单栏正常显示,并且在切换菜单的时候,详情页也能显示对应的图片: 显示/隐藏菜单为了达到菜单栏点击以后应该自动隐藏的目的,应该在单元行点击以后,设置Scroll View的水平位移为菜单栏的宽度,这样就可以完全展示出详情页。 首先,需要将Scroll View与Container View关联。 建立scrollView与ContainerViewController.swift的关联:在Storyboard的document outline中,选中Scroll View,打开Assistant Editor,按住Control键,从Scroll View拖到ContainerViewController.swift。然后在弹出框的Name一栏中将Scroll View命名为scrollView。 这里,我设置了View**\Assistant Editor\Assistant Editors on Bottom**,所以Assistant Editor就会在Xcode窗口的底部(译者注:默认是在右边),这样我就不用在拖动关联时,跨越整个画布了。 同样的步骤,按住Control键,从Menu Container View拖到ContainerViewController.swift,创建一个menuContainerView。 然后,给ContainerViewController.swift添加hideOrShowMenu(**_:animated:)**这个方法: // MARK: ContainerViewControllerfunc hideOrShowMenu(show: Bool, animated: Bool) { let menuOffset = CGRectGetWidth(menuContainerView.bounds) scrollView.setContentOffset(show ? CGPointZero : CGPoint(x: menuOffset, y: 0), animated: animated)} menuOffset的值就是Menu Container View宽度为80,如果show为true,scrollView的偏移量就为0,这是菜单栏就会显示,同样,scrollView的偏移量为80,菜单栏就会隐藏。 现在,在menuItem的didSet中调用hideOrShowMenu(_:animated:)方法: var menuItem: NSDictionary? { didSet { hideOrShowMenu(false, animated: true) // ... 在用户点击单元行之后,应该关闭菜单栏,所以show置为false。 同样,在viewDidLoad() 中调用hideOrShowMenu(_:animated:)方法,让程序启动时,菜单栏处于隐藏状态: override func viewDidLoad() { super.viewDidLoad() hideOrShowMenu(false, animated: false)} 运行程序,这时详情页显示的是笑脸那张图,并且菜单栏处于隐藏状态。滑出菜单,选择某个单元行,可以看到菜单栏会隐藏,并且详情页能显示出对应的图片与背景色。 但是,之前因为Paging Enable出现的问题依然存在:如果滑出菜单栏,不选择单元行,然后滑动隐藏菜单栏时,菜单栏又会弹出来。这个问题可以通过实现UIScrollViewDelegate中的一个方法来解决。 在ContainerViewController类声明的地方,遵守UIScrollViewDelegate协议: class ContainerViewController: UIViewController, UIScrollViewDelegate { 然后,在ContainerViewController添加UIScrollViewDelegate的代理方法: // MARK: - UIScrollViewDelegatefunc scrollViewDidScroll(scrollView: UIScrollView) { /* Fix for the UIScrollView paging-related issue mentioned here: http://stackoverflow.com/questions/4480512/uiscrollview-single-tap-scrolls-it-to-top */ scrollView.pagingEnabled = scrollView.contentOffset.x < (scrollView.contentSize.width - CGRectGetWidth(scrollView.frame))} 这将在Scroll View的偏移量与菜单栏宽度相等的时候,禁用Paging Enable;这样,当菜单栏完全隐藏的时候,就会保持隐藏状态。当用户滑出菜单时,Paging Enable又会启用,从而菜单能一次性滑出。 运行程序,可以发现,这个问题已经解决了。 看起来不错,但依然缺少一些东西。详情也的导航栏上少了汉堡按钮(The hamburger menu button,即有三条横线的菜单按钮),它能控制菜单的开关,并且还能随着菜单的显示而旋转。 添加菜单按钮我们需要菜单按钮是自定义视图,这样才可以在菜单栏显示/隐藏的时候进行相应的旋转动画。 创建一个HamburgerView.swift,继承自UIView。 在这个类中添加以下代码: class HamburgerView: UIView { let imageView: UIImageView! = UIImageView(image: UIImage(named: “Hamburger”)) required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) configure() } required override init(frame: CGRect) { super.init(frame: frame) configure() } // MARK: Private private func configure() { imageView.contentMode = UIViewContentMode.Center addSubview(imageView) }} 上面的代码重写了两个初始化方法,并调用了configure()来将imageView加载到父视图上。 在DetailViewController.swift中添加hamburgerView属性: var hamburgerView: HamburgerView? 在viewDidLoad()中,给hamburgerView创建实例,并将它作为Navigation Bar的left bar button,并添加一个点击手势。 let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: “hamburgerViewTapped”)hamburgerView = HamburgerView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))hamburgerView!.addGestureRecognizer(tapGestureRecognizer)navigationItem.leftBarButtonItem = UIBarButtonItem(customView: hamburgerView!) hamburgerViewTapped() 方法需要触发ContainerViewController中的hideOrShowMenu(**_:animated:)方法,但是问题是show应该传什么参数?所以ContainerViewController**需要一个Bool值来记录菜单显示/隐藏的状态。 在ContainerViewController.swift中添加以下属性: var showingMenu = false 初始时菜单的显示状态是false,重写viewDidLayoutSubviews()方法,在bounds改变的时候来显示/隐藏菜单。 override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() hideOrShowMenu(showingMenu, animated: false)} 你将不再需要viewDidLoad()方法,所以将它从ContainerViewController.swift中删除。 打开DetailViewController.swift,添加以下代码: func hamburgerViewTapped() { let navigationController = parentViewController as! UINavigationController let containerViewController = navigationController.parentViewController as! ContainerViewController containerViewController.hideOrShowMenu(!containerViewController.showingMenu, animated: true)} 如果showingMenu的值为false,菜单栏为隐藏状态,这是,当用户点击按钮,hideOrShowMenu(:animated:)方法就会被调用,show参数传入true,然后菜单显示。相反,当菜单处于显示状态时,即showingMenu值为true,这时用户点击按钮,菜单栏就会隐藏。因此,需要更新ContainerViewController.swift中hideOrShowMenu(:animated:)方法的showingMenu属性。 在 hideOrShowMenu(_:animated:):中添加以下代码: showingMenu = show 运行程序,尝试去滚动视图、点击菜单按钮、点击单元行这些操作。 这存在一个问题:如果是采用滑出/滑入菜单栏的操作,那么菜单栏按钮要点两次,菜单栏才有响应。这是为什么? 这个问题出现的原因为:在滚动的后,showingMenu没有更改,菜单滑出的时候showingMenu还是false。当第一次点击的时候,showingMenu的值设置为true,所以菜单还是显示状态。第二次点击的时候,才能将showingMenu置为false,从而隐藏菜单。 要解决这个问题,需要在ContainerViewController的UIScrollViewDelegate代理方法中将showingMenu设置一下,具体如下: func scrollViewDidEndDecelerating(scrollView: UIScrollView) { let menuOffset = CGRectGetWidth(menuContainerView.bounds) showingMenu = !CGPointEqualToPoint(CGPoint(x: menuOffset, y: 0), scrollView.contentOffset) println(“didEndDecelerating showingMenu \(showingMenu)”)} 当滚动结束以后,如果Scroll View的偏移量与菜单栏的宽度相同(即,菜单栏处于隐藏状态),那就将showingMenu设置为false。相反,则置为true。 运行程序,向右滑动,当滚动停止时,查看控制台打印信息。它的调用取决于Scroll View的滚动速度。当我在模拟器上运行时,只有慢慢的滚动,才会调用这个方法,但是当我在真机上运行时,它又需要快速滚动才能调用。 所以,将这段代码放入到scrollViewDidScroll(_:)方法中,这样可以更好的响应滚动。这个方法在用户滚动时就会调用,这也更加的可靠。 接下来就可以给菜单按钮加旋转动画了,但在此之前,我们先给菜单栏加3D翻转动画。 给菜单栏加立体效果这个超级炫酷的动画看起来就像开门和关门。与此同时,菜单按钮也应该跟随菜单的状态,有一个平滑的旋转效果。 为了达到这个效果,我们将计算菜单滑出的比例(后文称为fraction),从而得到按钮应该旋转的角度。 在ContainerViewController.swift中,添加一个私有方法来通过比例进行3D旋转动画: func transformForFraction(fraction:CGFloat) -> CATransform3D { var identity = CATransform3DIdentity identity.m34 = -1.0 / 1000.0; let angle = Double(1.0 - fraction) * -M_PI_2 let xOffset = CGRectGetWidth(menuContainerView.bounds) * 0.5 let rotateTransform = CATransform3DRotate(identity, CGFloat(angle), 0.0, 1.0, 0.0) let translateTransform = CATransform3DMakeTranslation(xOffset, 0.0, 0.0) return CATransform3DConcat(rotateTransform, translateTransform)} 下面是transformForFraction(_:)这个方法的实现步骤: <li> 在菜单完全隐藏时,fraction = 0,完全显示时,fraction = 1; CATransform3DIdentity是一个4*4的矩阵,对角线是1,其他是0; CATransform3DIdentity的m34属性是这个矩阵中的第三列第四行,它控制着变换中的立体程度; CATransform3DRotate通过angle这个变量来控制y轴的旋转量:-90度呈现的是菜单栏垂直于屏幕的状态,0度呈现的是与xy轴平行的状态; rotateTransform的旋转变换,是根据m34矩阵与y轴旋转量来的; translateTransform设置了x轴的偏移量为菜单栏的一般; CATransform3DConcat将rotateTransform与translateTransform联系起来,使得在做翻转动画时,也有偏移动画。 接下来,在scrollViewDidScroll(_:):中添加下面代码: let multiplier = 1.0 / CGRectGetWidth(menuContainerView.bounds)let offset = scrollView.contentOffset.x * multiplierlet fraction = 1.0 - offsetmenuContainerView.layer.transform = transformForFraction(fraction)menuContainerView.alpha = fraction offset的值在0到1之间。当offset的值为0的时候,菜单栏处于显示状态;当offset为1的时候,菜单栏处于隐藏状态。 fraction是菜单显示部分所占的比例,范围在0到1之间,0是菜单栏完全隐藏,1是显示状态。 同时,fraction也用来调整菜单栏的alpha值,从暗到明,从隐藏到显示。 运行程序,滑动看一下这个3D效果,但是…菜单栏的变换关系有点问题(译者注:原文用的”hinge”这个词,这里的问题是菜单栏沿着中心轴在旋转),原因是菜单栏的锚点在中心点。 为了让菜单栏沿着右边距旋转,需要在ContainerViewController.swift的viewDidLayoutSubviews()方法中添加以下方法: menuContainerView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5) 将锚点的x设为1,y设为0,这样就可以沿右边距旋转了。 运行程序,可以看到完美的3D旋转效果。 最后一件事:给菜单按钮加旋转动画 菜单按钮动画将是整个App的点睛之笔。 当菜单栏隐藏的时候,按钮处于原有状态,当菜单栏显示的时候,按钮旋转90度。 技术上来讲,是按钮的图片在旋转,但是实际看起来的就效果,就像是按钮在旋转。 在HamburgerView.swift中添加以下代码: func rotate(fraction: CGFloat) { let angle = Double(fraction) * M_PI_2 imageView.transform = CGAffineTransformMakeRotation(CGFloat(angle))} 现在菜单按钮可以平滑的旋转了。通过fraction来计算应该旋转的角度,其中M_PI_2常量是定义在math.h头文件中的,表示pi/2。 添加以下代码到scrollViewDidScroll(_:)方法中,这样旋转角度可以和滚动偏移量结合起来: if let detailViewController = detailViewController { if let rotatingView = detailViewController.hamburgerView { rotatingView.rotate(fraction) }} 运行程序,可以看到动画已经能很好的结合起来了。 最后可以在这里下载这个项目的最终版。 本文简单的实验了以下m34的3D旋转效果,如果想要了解更多3D变换,可以看Richard Turton的Visual Tool for CATransform3D。 维基百科上也有一些文章),使用图片很好的解释了立体视觉这个概念。 同时,你也可以思考一下,还有那些3D效果可以用在你的App中,来提高用户交互体验。就像是菜单按钮这样的微妙的动画,细节的处理能很好的提高用户体验。 译者注第一次翻译,还请见谅,之后也会尝试着翻译一些其他的文章,尽请期待。如果有什么建议,请留言或者邮箱联系,谢谢。
  5. 5. 创建UIScrollView
  6. 6. 创建Content View
  7. 7. 添加菜单栏与详情界面
  8. 8. 嵌入Menu与Detail View Controllers
  9. 9. 对容器进行编码
  10. 10. 显示/隐藏菜单为了达到菜单栏点击以后应该自动隐藏的目的,应该在单元行点击以后,设置Scroll View的水平位移为菜单栏的宽度,这样就可以完全展示出详情页。 首先,需要将Scroll View与Container View关联。 建立scrollView与ContainerViewController.swift的关联:在Storyboard的document outline中,选中Scroll View,打开Assistant Editor,按住Control键,从Scroll View拖到ContainerViewController.swift。然后在弹出框的Name一栏中将Scroll View命名为scrollView。 这里,我设置了View**\Assistant Editor\Assistant Editors on Bottom**,所以Assistant Editor就会在Xcode窗口的底部(译者注:默认是在右边),这样我就不用在拖动关联时,跨越整个画布了。 同样的步骤,按住Control键,从Menu Container View拖到ContainerViewController.swift,创建一个menuContainerView。 然后,给ContainerViewController.swift添加hideOrShowMenu(**_:animated:)**这个方法: // MARK: ContainerViewControllerfunc hideOrShowMenu(show: Bool, animated: Bool) { let menuOffset = CGRectGetWidth(menuContainerView.bounds) scrollView.setContentOffset(show ? CGPointZero : CGPoint(x: menuOffset, y: 0), animated: animated)} menuOffset的值就是Menu Container View宽度为80,如果show为true,scrollView的偏移量就为0,这是菜单栏就会显示,同样,scrollView的偏移量为80,菜单栏就会隐藏。 现在,在menuItem的didSet中调用hideOrShowMenu(_:animated:)方法: var menuItem: NSDictionary? { didSet { hideOrShowMenu(false, animated: true) // ... 在用户点击单元行之后,应该关闭菜单栏,所以show置为false。 同样,在viewDidLoad() 中调用hideOrShowMenu(_:animated:)方法,让程序启动时,菜单栏处于隐藏状态: override func viewDidLoad() { super.viewDidLoad() hideOrShowMenu(false, animated: false)} 运行程序,这时详情页显示的是笑脸那张图,并且菜单栏处于隐藏状态。滑出菜单,选择某个单元行,可以看到菜单栏会隐藏,并且详情页能显示出对应的图片与背景色。 但是,之前因为Paging Enable出现的问题依然存在:如果滑出菜单栏,不选择单元行,然后滑动隐藏菜单栏时,菜单栏又会弹出来。这个问题可以通过实现UIScrollViewDelegate中的一个方法来解决。 在ContainerViewController类声明的地方,遵守UIScrollViewDelegate协议: class ContainerViewController: UIViewController, UIScrollViewDelegate { 然后,在ContainerViewController添加UIScrollViewDelegate的代理方法: // MARK: - UIScrollViewDelegatefunc scrollViewDidScroll(scrollView: UIScrollView) { /* Fix for the UIScrollView paging-related issue mentioned here: http://stackoverflow.com/questions/4480512/uiscrollview-single-tap-scrolls-it-to-top */ scrollView.pagingEnabled = scrollView.contentOffset.x < (scrollView.contentSize.width - CGRectGetWidth(scrollView.frame))} 这将在Scroll View的偏移量与菜单栏宽度相等的时候,禁用Paging Enable;这样,当菜单栏完全隐藏的时候,就会保持隐藏状态。当用户滑出菜单时,Paging Enable又会启用,从而菜单能一次性滑出。 运行程序,可以发现,这个问题已经解决了。 看起来不错,但依然缺少一些东西。详情也的导航栏上少了汉堡按钮(The hamburger menu button,即有三条横线的菜单按钮),它能控制菜单的开关,并且还能随着菜单的显示而旋转。 添加菜单按钮我们需要菜单按钮是自定义视图,这样才可以在菜单栏显示/隐藏的时候进行相应的旋转动画。 创建一个HamburgerView.swift,继承自UIView。 在这个类中添加以下代码: class HamburgerView: UIView { let imageView: UIImageView! = UIImageView(image: UIImage(named: “Hamburger”)) required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) configure() } required override init(frame: CGRect) { super.init(frame: frame) configure() } // MARK: Private private func configure() { imageView.contentMode = UIViewContentMode.Center addSubview(imageView) }} 上面的代码重写了两个初始化方法,并调用了configure()来将imageView加载到父视图上。 在DetailViewController.swift中添加hamburgerView属性: var hamburgerView: HamburgerView? 在viewDidLoad()中,给hamburgerView创建实例,并将它作为Navigation Bar的left bar button,并添加一个点击手势。 let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: “hamburgerViewTapped”)hamburgerView = HamburgerView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))hamburgerView!.addGestureRecognizer(tapGestureRecognizer)navigationItem.leftBarButtonItem = UIBarButtonItem(customView: hamburgerView!) hamburgerViewTapped() 方法需要触发ContainerViewController中的hideOrShowMenu(**_:animated:)方法,但是问题是show应该传什么参数?所以ContainerViewController**需要一个Bool值来记录菜单显示/隐藏的状态。 在ContainerViewController.swift中添加以下属性: var showingMenu = false 初始时菜单的显示状态是false,重写viewDidLayoutSubviews()方法,在bounds改变的时候来显示/隐藏菜单。 override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() hideOrShowMenu(showingMenu, animated: false)} 你将不再需要viewDidLoad()方法,所以将它从ContainerViewController.swift中删除。 打开DetailViewController.swift,添加以下代码: func hamburgerViewTapped() { let navigationController = parentViewController as! UINavigationController let containerViewController = navigationController.parentViewController as! ContainerViewController containerViewController.hideOrShowMenu(!containerViewController.showingMenu, animated: true)} 如果showingMenu的值为false,菜单栏为隐藏状态,这是,当用户点击按钮,hideOrShowMenu(:animated:)方法就会被调用,show参数传入true,然后菜单显示。相反,当菜单处于显示状态时,即showingMenu值为true,这时用户点击按钮,菜单栏就会隐藏。因此,需要更新ContainerViewController.swift中hideOrShowMenu(:animated:)方法的showingMenu属性。 在 hideOrShowMenu(_:animated:):中添加以下代码: showingMenu = show 运行程序,尝试去滚动视图、点击菜单按钮、点击单元行这些操作。 这存在一个问题:如果是采用滑出/滑入菜单栏的操作,那么菜单栏按钮要点两次,菜单栏才有响应。这是为什么? 这个问题出现的原因为:在滚动的后,showingMenu没有更改,菜单滑出的时候showingMenu还是false。当第一次点击的时候,showingMenu的值设置为true,所以菜单还是显示状态。第二次点击的时候,才能将showingMenu置为false,从而隐藏菜单。 要解决这个问题,需要在ContainerViewController的UIScrollViewDelegate代理方法中将showingMenu设置一下,具体如下: func scrollViewDidEndDecelerating(scrollView: UIScrollView) { let menuOffset = CGRectGetWidth(menuContainerView.bounds) showingMenu = !CGPointEqualToPoint(CGPoint(x: menuOffset, y: 0), scrollView.contentOffset) println(“didEndDecelerating showingMenu \(showingMenu)”)} 当滚动结束以后,如果Scroll View的偏移量与菜单栏的宽度相同(即,菜单栏处于隐藏状态),那就将showingMenu设置为false。相反,则置为true。 运行程序,向右滑动,当滚动停止时,查看控制台打印信息。它的调用取决于Scroll View的滚动速度。当我在模拟器上运行时,只有慢慢的滚动,才会调用这个方法,但是当我在真机上运行时,它又需要快速滚动才能调用。 所以,将这段代码放入到scrollViewDidScroll(_:)方法中,这样可以更好的响应滚动。这个方法在用户滚动时就会调用,这也更加的可靠。 接下来就可以给菜单按钮加旋转动画了,但在此之前,我们先给菜单栏加3D翻转动画。 给菜单栏加立体效果这个超级炫酷的动画看起来就像开门和关门。与此同时,菜单按钮也应该跟随菜单的状态,有一个平滑的旋转效果。 为了达到这个效果,我们将计算菜单滑出的比例(后文称为fraction),从而得到按钮应该旋转的角度。 在ContainerViewController.swift中,添加一个私有方法来通过比例进行3D旋转动画: func transformForFraction(fraction:CGFloat) -> CATransform3D { var identity = CATransform3DIdentity identity.m34 = -1.0 / 1000.0; let angle = Double(1.0 - fraction) * -M_PI_2 let xOffset = CGRectGetWidth(menuContainerView.bounds) * 0.5 let rotateTransform = CATransform3DRotate(identity, CGFloat(angle), 0.0, 1.0, 0.0) let translateTransform = CATransform3DMakeTranslation(xOffset, 0.0, 0.0) return CATransform3DConcat(rotateTransform, translateTransform)} 下面是transformForFraction(_:)这个方法的实现步骤: <li> 在菜单完全隐藏时,fraction = 0,完全显示时,fraction = 1; CATransform3DIdentity是一个4*4的矩阵,对角线是1,其他是0; CATransform3DIdentity的m34属性是这个矩阵中的第三列第四行,它控制着变换中的立体程度; CATransform3DRotate通过angle这个变量来控制y轴的旋转量:-90度呈现的是菜单栏垂直于屏幕的状态,0度呈现的是与xy轴平行的状态; rotateTransform的旋转变换,是根据m34矩阵与y轴旋转量来的; translateTransform设置了x轴的偏移量为菜单栏的一般; CATransform3DConcat将rotateTransform与translateTransform联系起来,使得在做翻转动画时,也有偏移动画。 接下来,在scrollViewDidScroll(_:):中添加下面代码: let multiplier = 1.0 / CGRectGetWidth(menuContainerView.bounds)let offset = scrollView.contentOffset.x * multiplierlet fraction = 1.0 - offsetmenuContainerView.layer.transform = transformForFraction(fraction)menuContainerView.alpha = fraction offset的值在0到1之间。当offset的值为0的时候,菜单栏处于显示状态;当offset为1的时候,菜单栏处于隐藏状态。 fraction是菜单显示部分所占的比例,范围在0到1之间,0是菜单栏完全隐藏,1是显示状态。 同时,fraction也用来调整菜单栏的alpha值,从暗到明,从隐藏到显示。 运行程序,滑动看一下这个3D效果,但是…菜单栏的变换关系有点问题(译者注:原文用的”hinge”这个词,这里的问题是菜单栏沿着中心轴在旋转),原因是菜单栏的锚点在中心点。 为了让菜单栏沿着右边距旋转,需要在ContainerViewController.swift的viewDidLayoutSubviews()方法中添加以下方法: menuContainerView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5) 将锚点的x设为1,y设为0,这样就可以沿右边距旋转了。 运行程序,可以看到完美的3D旋转效果。 最后一件事:给菜单按钮加旋转动画 菜单按钮动画将是整个App的点睛之笔。 当菜单栏隐藏的时候,按钮处于原有状态,当菜单栏显示的时候,按钮旋转90度。 技术上来讲,是按钮的图片在旋转,但是实际看起来的就效果,就像是按钮在旋转。 在HamburgerView.swift中添加以下代码: func rotate(fraction: CGFloat) { let angle = Double(fraction) * M_PI_2 imageView.transform = CGAffineTransformMakeRotation(CGFloat(angle))} 现在菜单按钮可以平滑的旋转了。通过fraction来计算应该旋转的角度,其中M_PI_2常量是定义在math.h头文件中的,表示pi/2。 添加以下代码到scrollViewDidScroll(_:)方法中,这样旋转角度可以和滚动偏移量结合起来: if let detailViewController = detailViewController { if let rotatingView = detailViewController.hamburgerView { rotatingView.rotate(fraction) }} 运行程序,可以看到动画已经能很好的结合起来了。 最后可以在这里下载这个项目的最终版。 本文简单的实验了以下m34的3D旋转效果,如果想要了解更多3D变换,可以看Richard Turton的Visual Tool for CATransform3D。 维基百科上也有一些文章),使用图片很好的解释了立体视觉这个概念。 同时,你也可以思考一下,还有那些3D效果可以用在你的App中,来提高用户交互体验。就像是菜单按钮这样的微妙的动画,细节的处理能很好的提高用户体验。 译者注第一次翻译,还请见谅,之后也会尝试着翻译一些其他的文章,尽请期待。如果有什么建议,请留言或者邮箱联系,谢谢。
  11. 11. 显示/隐藏菜单
  12. 12. 添加菜单按钮我们需要菜单按钮是自定义视图,这样才可以在菜单栏显示/隐藏的时候进行相应的旋转动画。 创建一个HamburgerView.swift,继承自UIView。 在这个类中添加以下代码: class HamburgerView: UIView { let imageView: UIImageView! = UIImageView(image: UIImage(named: “Hamburger”)) required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) configure() } required override init(frame: CGRect) { super.init(frame: frame) configure() } // MARK: Private private func configure() { imageView.contentMode = UIViewContentMode.Center addSubview(imageView) }} 上面的代码重写了两个初始化方法,并调用了configure()来将imageView加载到父视图上。 在DetailViewController.swift中添加hamburgerView属性: var hamburgerView: HamburgerView? 在viewDidLoad()中,给hamburgerView创建实例,并将它作为Navigation Bar的left bar button,并添加一个点击手势。 let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: “hamburgerViewTapped”)hamburgerView = HamburgerView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))hamburgerView!.addGestureRecognizer(tapGestureRecognizer)navigationItem.leftBarButtonItem = UIBarButtonItem(customView: hamburgerView!) hamburgerViewTapped() 方法需要触发ContainerViewController中的hideOrShowMenu(**_:animated:)方法,但是问题是show应该传什么参数?所以ContainerViewController**需要一个Bool值来记录菜单显示/隐藏的状态。 在ContainerViewController.swift中添加以下属性: var showingMenu = false 初始时菜单的显示状态是false,重写viewDidLayoutSubviews()方法,在bounds改变的时候来显示/隐藏菜单。 override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() hideOrShowMenu(showingMenu, animated: false)} 你将不再需要viewDidLoad()方法,所以将它从ContainerViewController.swift中删除。 打开DetailViewController.swift,添加以下代码: func hamburgerViewTapped() { let navigationController = parentViewController as! UINavigationController let containerViewController = navigationController.parentViewController as! ContainerViewController containerViewController.hideOrShowMenu(!containerViewController.showingMenu, animated: true)} 如果showingMenu的值为false,菜单栏为隐藏状态,这是,当用户点击按钮,hideOrShowMenu(:animated:)方法就会被调用,show参数传入true,然后菜单显示。相反,当菜单处于显示状态时,即showingMenu值为true,这时用户点击按钮,菜单栏就会隐藏。因此,需要更新ContainerViewController.swift中hideOrShowMenu(:animated:)方法的showingMenu属性。 在 hideOrShowMenu(_:animated:):中添加以下代码: showingMenu = show 运行程序,尝试去滚动视图、点击菜单按钮、点击单元行这些操作。 这存在一个问题:如果是采用滑出/滑入菜单栏的操作,那么菜单栏按钮要点两次,菜单栏才有响应。这是为什么? 这个问题出现的原因为:在滚动的后,showingMenu没有更改,菜单滑出的时候showingMenu还是false。当第一次点击的时候,showingMenu的值设置为true,所以菜单还是显示状态。第二次点击的时候,才能将showingMenu置为false,从而隐藏菜单。 要解决这个问题,需要在ContainerViewController的UIScrollViewDelegate代理方法中将showingMenu设置一下,具体如下: func scrollViewDidEndDecelerating(scrollView: UIScrollView) { let menuOffset = CGRectGetWidth(menuContainerView.bounds) showingMenu = !CGPointEqualToPoint(CGPoint(x: menuOffset, y: 0), scrollView.contentOffset) println(“didEndDecelerating showingMenu \(showingMenu)”)} 当滚动结束以后,如果Scroll View的偏移量与菜单栏的宽度相同(即,菜单栏处于隐藏状态),那就将showingMenu设置为false。相反,则置为true。 运行程序,向右滑动,当滚动停止时,查看控制台打印信息。它的调用取决于Scroll View的滚动速度。当我在模拟器上运行时,只有慢慢的滚动,才会调用这个方法,但是当我在真机上运行时,它又需要快速滚动才能调用。 所以,将这段代码放入到scrollViewDidScroll(_:)方法中,这样可以更好的响应滚动。这个方法在用户滚动时就会调用,这也更加的可靠。 接下来就可以给菜单按钮加旋转动画了,但在此之前,我们先给菜单栏加3D翻转动画。 给菜单栏加立体效果这个超级炫酷的动画看起来就像开门和关门。与此同时,菜单按钮也应该跟随菜单的状态,有一个平滑的旋转效果。 为了达到这个效果,我们将计算菜单滑出的比例(后文称为fraction),从而得到按钮应该旋转的角度。 在ContainerViewController.swift中,添加一个私有方法来通过比例进行3D旋转动画: func transformForFraction(fraction:CGFloat) -> CATransform3D { var identity = CATransform3DIdentity identity.m34 = -1.0 / 1000.0; let angle = Double(1.0 - fraction) * -M_PI_2 let xOffset = CGRectGetWidth(menuContainerView.bounds) * 0.5 let rotateTransform = CATransform3DRotate(identity, CGFloat(angle), 0.0, 1.0, 0.0) let translateTransform = CATransform3DMakeTranslation(xOffset, 0.0, 0.0) return CATransform3DConcat(rotateTransform, translateTransform)} 下面是transformForFraction(_:)这个方法的实现步骤: <li> 在菜单完全隐藏时,fraction = 0,完全显示时,fraction = 1; CATransform3DIdentity是一个4*4的矩阵,对角线是1,其他是0; CATransform3DIdentity的m34属性是这个矩阵中的第三列第四行,它控制着变换中的立体程度; CATransform3DRotate通过angle这个变量来控制y轴的旋转量:-90度呈现的是菜单栏垂直于屏幕的状态,0度呈现的是与xy轴平行的状态; rotateTransform的旋转变换,是根据m34矩阵与y轴旋转量来的; translateTransform设置了x轴的偏移量为菜单栏的一般; CATransform3DConcat将rotateTransform与translateTransform联系起来,使得在做翻转动画时,也有偏移动画。 接下来,在scrollViewDidScroll(_:):中添加下面代码: let multiplier = 1.0 / CGRectGetWidth(menuContainerView.bounds)let offset = scrollView.contentOffset.x * multiplierlet fraction = 1.0 - offsetmenuContainerView.layer.transform = transformForFraction(fraction)menuContainerView.alpha = fraction offset的值在0到1之间。当offset的值为0的时候,菜单栏处于显示状态;当offset为1的时候,菜单栏处于隐藏状态。 fraction是菜单显示部分所占的比例,范围在0到1之间,0是菜单栏完全隐藏,1是显示状态。 同时,fraction也用来调整菜单栏的alpha值,从暗到明,从隐藏到显示。 运行程序,滑动看一下这个3D效果,但是…菜单栏的变换关系有点问题(译者注:原文用的”hinge”这个词,这里的问题是菜单栏沿着中心轴在旋转),原因是菜单栏的锚点在中心点。 为了让菜单栏沿着右边距旋转,需要在ContainerViewController.swift的viewDidLayoutSubviews()方法中添加以下方法: menuContainerView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5) 将锚点的x设为1,y设为0,这样就可以沿右边距旋转了。 运行程序,可以看到完美的3D旋转效果。 最后一件事:给菜单按钮加旋转动画 菜单按钮动画将是整个App的点睛之笔。 当菜单栏隐藏的时候,按钮处于原有状态,当菜单栏显示的时候,按钮旋转90度。 技术上来讲,是按钮的图片在旋转,但是实际看起来的就效果,就像是按钮在旋转。 在HamburgerView.swift中添加以下代码: func rotate(fraction: CGFloat) { let angle = Double(fraction) * M_PI_2 imageView.transform = CGAffineTransformMakeRotation(CGFloat(angle))} 现在菜单按钮可以平滑的旋转了。通过fraction来计算应该旋转的角度,其中M_PI_2常量是定义在math.h头文件中的,表示pi/2。 添加以下代码到scrollViewDidScroll(_:)方法中,这样旋转角度可以和滚动偏移量结合起来: if let detailViewController = detailViewController { if let rotatingView = detailViewController.hamburgerView { rotatingView.rotate(fraction) }} 运行程序,可以看到动画已经能很好的结合起来了。 最后可以在这里下载这个项目的最终版。 本文简单的实验了以下m34的3D旋转效果,如果想要了解更多3D变换,可以看Richard Turton的Visual Tool for CATransform3D。 维基百科上也有一些文章),使用图片很好的解释了立体视觉这个概念。 同时,你也可以思考一下,还有那些3D效果可以用在你的App中,来提高用户交互体验。就像是菜单按钮这样的微妙的动画,细节的处理能很好的提高用户体验。 译者注第一次翻译,还请见谅,之后也会尝试着翻译一些其他的文章,尽请期待。如果有什么建议,请留言或者邮箱联系,谢谢。
  13. 13. 添加菜单按钮
  14. 14. 给菜单栏加立体效果
  15. 15. 最后一件事:给菜单按钮加旋转动画 菜单按钮动画将是整个App的点睛之笔。 当菜单栏隐藏的时候,按钮处于原有状态,当菜单栏显示的时候,按钮旋转90度。 技术上来讲,是按钮的图片在旋转,但是实际看起来的就效果,就像是按钮在旋转。 在HamburgerView.swift中添加以下代码: func rotate(fraction: CGFloat) { let angle = Double(fraction) * M_PI_2 imageView.transform = CGAffineTransformMakeRotation(CGFloat(angle))} 现在菜单按钮可以平滑的旋转了。通过fraction来计算应该旋转的角度,其中M_PI_2常量是定义在math.h头文件中的,表示pi/2。 添加以下代码到scrollViewDidScroll(_:)方法中,这样旋转角度可以和滚动偏移量结合起来: if let detailViewController = detailViewController { if let rotatingView = detailViewController.hamburgerView { rotatingView.rotate(fraction) }} 运行程序,可以看到动画已经能很好的结合起来了。 最后可以在这里下载这个项目的最终版。 本文简单的实验了以下m34的3D旋转效果,如果想要了解更多3D变换,可以看Richard Turton的Visual Tool for CATransform3D。 维基百科上也有一些文章),使用图片很好的解释了立体视觉这个概念。 同时,你也可以思考一下,还有那些3D效果可以用在你的App中,来提高用户交互体验。就像是菜单按钮这样的微妙的动画,细节的处理能很好的提高用户体验。 译者注第一次翻译,还请见谅,之后也会尝试着翻译一些其他的文章,尽请期待。如果有什么建议,请留言或者邮箱联系,谢谢。
  16. 16. 最后一件事:给菜单按钮加旋转动画
  17. 17. 最后
  18. 18. 译者注

仿Taasky的3D翻转菜单动画实现

本文翻译自Audrey Tam发布在raywenderlich上的文章How To Create a Cool 3D Sidebar Animation Like in Taasky。第一次翻译,用词方面可能不是很准确,我尽量把句子翻译通顺。本文并未逐字翻译,部分不痛不痒的句子我就直接简化或忽略了,如果想要了解详细内容,还请查看原文。

最终效果

开始

首先下载并打开一个事先搭好架子的Demo,然后来分析一下。这个Demo包含一个主页和详情页,其中MenuViewController继承自UITableViewController,它主要用于展示左边侧栏,自定义的MenuItemCell中设置了每一个菜单的图标和颜色。DetailViewController为详情页,显示了每个cell点击后,对应的颜色和图标。

这个教程将详细的介绍实现步骤,具体步骤如下:

<li>

整个教程将使用自动布局来实现,需要将现在的主页push到详情页的这种模式改为横向ScrollView滚动的模式。

  • 需要在左上角添加显示/隐藏菜单栏的按钮。
  • 接下来,需要实现类似于Taasky的菜单栏的3D翻转效果。

  • 最后,需要将菜单栏的翻转效果与按钮的旋转效果结合起来。

首要任务是把菜单栏改成滑出的效果,UIScrollView中需要包含菜单栏和详情页面,就像是 Swift Scroll View School Part 13这个视频教程中介绍的SwiftSideNav一样。示意图如下:

用户可以通过左右滑动来显示/隐藏菜单栏。在上图中,紫色方框是当菜单栏显示的时候,屏幕所显示的部分,绿色方框是当菜单栏隐藏时,屏幕所显示的内容。当菜单栏打开时,显示的是一个局部侧滑边栏。

如果对UIScrollView不熟悉,请先学习Part 1Part2一系列教程,它们讲介绍UIScrollView的工作流程。

刚才提到的SwiftSideNav是一个使用自动布局完成例子,但是今天的教程将会直接在StoryBoard中嵌入菜单与详情页,完成后的结构如图:

为UIScrollView添加约束

这个部分会使用到控制器约束等知识(在iOS5时推出),如果还不熟悉,可以先学习iOS 5 By Tutorials书中的第18章UIViewController Containment。

菜单栏与详情。整个视图层级应该是这样的:

  • 有一个根控制器,并将UIScrollView添加到根控制器上;
  • 然后添加一个UIView到UIScrollView上,暂且叫它”Content View”;

  • 添加两个子容器到”Content View”上,然后分别把菜单栏和详情页填到到子容器中。

创建一个名为ContainerViewController的控制机,继承自UIViewController,语言选择Swift

 

打开Main.StoryBoard,从Object Library中拖一个View Controller到画布中,并在Identity Inspector(Xcode右边栏的第三个选项)中设置自定义类为ContainerViewController,唯一标识为ContainerVC

接下来,在 Attributes Inspector(Xcode右边栏的第四个选项)中将ContainerViewController的view背景色设置为黑色。

创建UIScrollView

接着,需要添加UIScrollView到ContainerViewController中,并添加约束。具体操作如下:

  • 拖一个UIScrollView到ContainerViewController中,并让它填充整个ContentViewController
  • 取消横、纵滚动条的显示(Shows Horizontal IndicatorShows Vertical Indicator);

  • 取消Delays Content Touches,让响应事件及时传递给子控件,防止响应延迟;

ContainerViewController设置为UIScrollView的delegate(按住control键,从UIScrollView拖到UIViewController)。

接下来是给UIScrollView添加约束的步骤:

    1. 找到右下角的Pin按钮,打开添加约束的弹窗;

    2. 取消Constrain to margins,防止系统自动加左右边距;
    3. 添加上下左右四个约束,并确保约束的值都是0;

    4. 点击Add 4 Constraints按钮,将约束加上。

    仍然选中UIScrollView,打开Size Inspector选项(Xcode右边栏倒数第二个选项),确认一下约束是否与教程所加的约束相同:

    • Trailing Space to: Superview
    • Leading Space to: Superview

    • Top Space to: Superview


    • Bottom Space to: Bottom Layout Guide

    如果有类似16这样的数字出现,说明在添加约束的时候,没有取消Constrain to margins。解决方案:删除UIScrollView的约束,重新添加一次,注意记得取消Constrain to margins。

    在添加或修改约束以后,可能需要根据新的约束来展示frame。这个需求可以通过右下角Resolve Auto Layout Issues弹出框(Pin按钮旁边)的Update Frames选项来解决。

    创建Content View

    接下来需要做的是添加一个Content View到UIScrollView上,并添加响应约束,这些约束对设置UIScrollView的contentSize很重要。在下一节中,要将添加菜单栏与详情两个容器视图添加到Content View上。

    拖一个新的View到UIScrollView上,让它自动填满整个父视图,并将背景色设置为Default

    译者注:上图中,作者将Width设置为了680,但在文字描述中并未提到。这个宽度可加可不加,因为之后界面上的宽度其实全都要通过约束来表现。

    选中刚加上的这个View,打开Identity Inspector,将Document**\Label设置为Content View**。这个名字将会在document outline(IB左边视图层级栏)显示出来,可以很容易追踪到这个View,并且在之后加与它相关的约束的时候,这个名字也会显示出来。

    在给Content View添加约束的过程中,可以看到警告,不过不用惊慌,这些警告在这一节完之前能得到解决。

     

    打开Pin弹出框,给Content View添加相对于父视图的约束。

    在Size Inspector中,确保Trailing Space的约束值为0。

    译者注:这里有点不明白作者的操作,直接在添加约束的时候,将右边距设置为0就可以了,但是作者却先将宽设置为680,然后添加约束的时候也是添加的-80,最后又改成0。感觉有点多此一举,还请大神们解答下。

    现在之所以有警告是因为StoryBoard需要Content View的高和宽来设置contentSize。添加相对于Scroll View的父容器的这些约束,可以使之适配各种设备与横竖屏。

    在document outline中,按住Control键,从Content View拖到View(Scroll View的父容器)。按住Shift键,选择Equal WidthsEqual Heights

     

    然后将Equal Width约束改为80。

    将约束改为80的用意是:这样Content View就会比View宽80,就可以刚好装下菜单栏。同时,可以发现之前的警告也不存在了——干得漂亮。

    添加菜单栏与详情界面

    现在Scroll View上有Content View可以作为菜单栏和详情页的容器,然后将菜单栏与详情页(后文中称为Menu View与Detail View)嵌入Content View,这样便可以创建一个可以滑动显示.隐藏的Menu View。

    首先,创建Menu Container View:拖一个Container ViewContent View上,在Size Inspector中,将其宽度设为80,然后在Identity Inspector中将Document\Label设置为Menu Container View

    高度应该是默认的600,不过如果你不确定,也可以设置一下。

    译者注:这里作者只提到设置宽度,但根据后文的一些描述,这里应该还需要将x设置为0,y设置为0。

    然后,添加Detail Container View:新拖一个Container ViewContent View上,并放置在menu container的右边。打开Size Inspector,并设置以下值:

    • X: 80
    • Y: 0
    • Width: 600
    • Height: 600

    接下来,打开Identity Inspector,将Document**\Label设置为Detail Container View**。

    这样,Menu ViewDetail View的宽度就和Content View相等了。

    当添加完这两个自控制器后(container view),可在在IB中看到系统已经默认添加了contained view controller,但是我们需要用到已存在的Menu View与Detail View,所以需要在document outline或者画布中删除这两个控制器。

    译者注:这个教程中的操作,都在文章开头下载的那个Demo中进行(称为starter project),所以,已存在的Menu View与Detail View就是指starter project中的控制器视图。

    接下来,需要给两个container view添加约束。

    Menu Container View添加宽为80,上下左右边距为0,共5个约束。

    Detail Container View添加上、右、下边距为0,共3个约束;注意不要添加左边距约束,否则会和Menu Container View的右边距约束重复。

    给两个Container View添加的约束都要确保Constrain to margins为取消。

    嵌入Menu与Detail View Controllers

    这一节中,将会把starter project中的menu和detail view controllers拆开,然后分别嵌入Menu Container ViewDetail Container View

    首先,将Container View Controller设为入口控制器:将原本指向Navigation Controller的入口箭头指向Container View Controller

    接下来,按住Control键,从Menu Container View拖到Navigation Controller,在弹出框中选择embed

    Menu Container View嵌入Navigation Controller之后,相关的视图的宽度就都缩小到了80:

    现在来调整一下menu与detail:首先,将table view cell中imageView的宽度调整为80

    然后,删除menu与detail之间的push连线。选中Detail View Controller,然后在Xcode菜单中选择Editor**\Embed In\Navigation Controller**。

    现在detail应该加在了Navigation Controller上,同时拥有了黑色的导航栏。

    选中这个新的Navigation Controller的导航栏,打开Attributes Inspector,选择StyleBlank,取消Translucent,并将Bar Tint设置为Black Color

    译者注:如果不好选中导航栏,可以在document outline中进行选择,注意不要选错了。

    这个设置完以后,新加的导航栏就和第一个导航栏的样式一致了。

    接着,打开Detail View Controller控制器的Attributes Inspector,确保View Controller**\Layout\Adjust Scroll View Insets**是处于选中状态。

    Adjust Scroll View Insets的选中是为了将视图从Navigation Bar下面开始显示,而不会被Navigation Bar盖住

    最后,将Detail View Controller嵌入Detail Container View:按住Control键,从Detail Container View拖到Detail View ControllerNavigation Controller,在弹出框中选择embed

    运行程序,左右拖动视图,就可以显示/隐藏菜单栏了。你是否注意到,在拖动Scroll View是,可以超出左右边界,而且还可以停止滚动,显示部分菜单这个问题?

    为了修复这个问题,需要在Scroll View的Attributes Inspector中修改一下属性:

    • 选中Scrolling\Paging Enabled属性,这样就可以…(译者注:原谅我找不到好的词语来表达原词了,原文中用到的是“snaps”,大概就是有一种惯性、巧劲的感觉,具体Paging Enabled的效果相信大家都懂的,就不说了);
    • 取消Bounce\Bounces属性,防止左右滚出边距(译者注:即没有回弹效果)。

    译者注:原文的动图没有截导航栏部分,我在跟着做的时候,发现菜单的导航栏上有白边,原因是Menu Container View与Detail Container View的背景色为默认色的原因,设置为黑色即可。

    这时,在运行程序,就不会出现左右滑出边界的情况了,并且,也不会出现菜单栏可以显示一部分的情况。但是,在向左滑动,试图隐藏菜单栏时,又出现了新问题。

    本来滚动隐藏的菜单栏又弹了出来。这个问题在StackOverflow上讨论过,我们将在关联Scroll View的时候再去解决它。

    现在我们需要先解决另外一个问题:详情页是空白的,并且在点击菜单栏的时候,没有任何事件触发。

    这完全在意料之中,因为还没有代码对container views进行关联。

    对容器进行编码

    在开始之前,先将MenuViewController.swift中的viewDidLoad()复制到DetailViewController.swift中,如下:

    override func viewDidLoad() {
    super.viewDidLoad()
    // Remove the drop shadow from the navigation bar
    navigationController!.navigationBar.clipsToBounds = true
    }

    这句代码的目的是不显示导航栏下面的那条阴影,虽然是个小细节,但就是这些细节使我们的App更加优雅。

    当用户选中某个单元格时, MenuViewController必须要设置DetailViewController中的menuItem属性,但是这两个类已经没有直接联系了。所以,这两个类之间的通信将有ContainerViewController来代替。

    ContainerViewController.swift的顶部添加DetailViewController属性:

    private var detailViewController: DetailViewController?

    ContainerViewController.swift中实现prepareForSegue(_:sender:)这个方法,并添加以下代码:

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    // 译者注:这里的"DetailViewSegue"设置接下来的描述中讲解(就是这段代码下面的这段话)
    if segue.identifier == "DetailViewSegue" {
    let navigationController = segue.destinationViewController as! UINavigationController
    detailViewController = navigationController.topViewController as? DetailViewController
    }
    }

    segue.identifier是啥?在将DetailViewController嵌入到容器时,需要在Attributes inspector中将Storyboard Embed SegueIdentifier设置为DetailViewSegue

    然后,声明menuItem属性,并在didSet中,在它进行赋值时,也给DetailViewController中的menuItem属性进行赋值。

    var menuItem: NSDictionary? {
    didSet {
    if let detailViewController = detailViewController {
    detailViewController.menuItem = menuItem
    }
    }
    }

    这已不再是table view cell与content view之间的交互了,而是用户选择菜单时,MenuViewController需要做出响应。

    译者注:上面这句话纠结好久,翻译出来还是不通顺,原话是这样的:There’s no longer a segue from a table view cell to the content view, but MenuViewController needs to respond when the user selects an item.

    删除MenuVie**wControllerprepareForSegue(:sender:)部分的代码,并且添加下面这段代码,注意,在选择方法的时候,不要选成tableView(:didDeselectRowAtIndexPath:)**而造成麻烦。

    // MARK: UITableViewDelegate
    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    // 译者注:取消单元格选中
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
    // 译者注:从menuItems数组中取出menuItem字典,里面的值是根据MenuItems.plist生成的,包含小图,大图,背景色
    let menuItem = menuItems[indexPath.row] as! NSDictionary
    // 译者注:当前的navigationController!.parentViewController就是ContainerViewController,因为navigationController的视图是作为80像素的Container View添加在ContainerViewController中的
    (navigationController!.parentViewController as! ContainerViewController).menuItem = menuItem
    }

    这里,只是简单将基于选择的单元行设置了ContainerViewControllermenuItem属性。这将使属性的didSet方法触发(就是上面将menuItem赋值给detailViewController.menuItem的那段代码)。

    最后,在MenuViewController.swiftviewDidLoad()方法中,添加以下代码:

    (navigationController!.parentViewController as! ContainerViewController).menuItem =
    (menuItems[0] as! NSDictionary)

    这句代码将在App第一次加载的时候给detail view添加对应的图片。

    运行程序,可以看到程序加载以后,详情页已经有了图片,菜单栏正常显示,并且在切换菜单的时候,详情页也能显示对应的图片:

    显示/隐藏菜单

    为了达到菜单栏点击以后应该自动隐藏的目的,应该在单元行点击以后,设置Scroll View的水平位移为菜单栏的宽度,这样就可以完全展示出详情页。

    首先,需要将Scroll ViewContainer View关联。

    建立scrollView与ContainerViewController.swift的关联:在Storyboard的document outline中,选中Scroll View,打开Assistant Editor,按住Control键,从Scroll View拖到ContainerViewController.swift。然后在弹出框的Name一栏中将Scroll View命名为scrollView

    这里,我设置了View**\Assistant Editor\Assistant Editors on Bottom**,所以Assistant Editor就会在Xcode窗口的底部(译者注:默认是在右边),这样我就不用在拖动关联时,跨越整个画布了。

    同样的步骤,按住Control键,从Menu Container View拖到ContainerViewController.swift,创建一个menuContainerView

    然后,给ContainerViewController.swift添加hideOrShowMenu(**_:animated:)**这个方法:

    // MARK: ContainerViewController
    func hideOrShowMenu(show: Bool, animated: Bool) {
    let menuOffset = CGRectGetWidth(menuContainerView.bounds)
    scrollView.setContentOffset(show ? CGPointZero : CGPoint(x: menuOffset, y: 0), animated: animated)
    }

    menuOffset的值就是Menu Container View宽度为80,如果showtrue,scrollView的偏移量就为0,这是菜单栏就会显示,同样,scrollView的偏移量为80,菜单栏就会隐藏。

    现在,在menuItem的didSet中调用hideOrShowMenu(_:animated:)方法:

    var menuItem: NSDictionary? {
    didSet {
    hideOrShowMenu(false, animated: true)
    // ...

    在用户点击单元行之后,应该关闭菜单栏,所以show置为false

    同样,在viewDidLoad() 中调用hideOrShowMenu(_:animated:)方法,让程序启动时,菜单栏处于隐藏状态:

    override func viewDidLoad() {
    super.viewDidLoad()
    hideOrShowMenu(false, animated: false)
    }

    运行程序,这时详情页显示的是笑脸那张图,并且菜单栏处于隐藏状态。滑出菜单,选择某个单元行,可以看到菜单栏会隐藏,并且详情页能显示出对应的图片与背景色。

    但是,之前因为Paging Enable出现的问题依然存在:如果滑出菜单栏,不选择单元行,然后滑动隐藏菜单栏时,菜单栏又会弹出来。这个问题可以通过实现UIScrollViewDelegate中的一个方法来解决。

    ContainerViewController类声明的地方,遵守UIScrollViewDelegate协议:

    class ContainerViewController: UIViewController, UIScrollViewDelegate {

    然后,在ContainerViewController添加UIScrollViewDelegate的代理方法:

    // MARK: - UIScrollViewDelegate
    func scrollViewDidScroll(scrollView: UIScrollView) {
    /*
    Fix for the UIScrollView paging-related issue mentioned here:
    http://stackoverflow.com/questions/4480512/uiscrollview-single-tap-scrolls-it-to-top
    */
    scrollView.pagingEnabled = scrollView.contentOffset.x < (scrollView.contentSize.width - CGRectGetWidth(scrollView.frame))
    }

    这将在Scroll View的偏移量与菜单栏宽度相等的时候,禁用Paging Enable;这样,当菜单栏完全隐藏的时候,就会保持隐藏状态。当用户滑出菜单时,Paging Enable又会启用,从而菜单能一次性滑出。

    运行程序,可以发现,这个问题已经解决了。

    看起来不错,但依然缺少一些东西。详情也的导航栏上少了汉堡按钮(The hamburger menu button,即有三条横线的菜单按钮),它能控制菜单的开关,并且还能随着菜单的显示而旋转。

    添加菜单按钮

    我们需要菜单按钮是自定义视图,这样才可以在菜单栏显示/隐藏的时候进行相应的旋转动画。

    创建一个HamburgerView.swift,继承自UIView

    在这个类中添加以下代码:

     class HamburgerView: UIView {

    let imageView: UIImageView! = UIImageView(image: UIImage(named: “Hamburger”))

    required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    configure()
    }

    required override init(frame: CGRect) {
    super.init(frame: frame)
    configure()
    }

    // MARK: Private

    private func configure() {
    imageView.contentMode = UIViewContentMode.Center
    addSubview(imageView)
    }

    }

    上面的代码重写了两个初始化方法,并调用了configure()来将imageView加载到父视图上。

    DetailViewController.swift中添加hamburgerView属性:

    var hamburgerView: HamburgerView?

    viewDidLoad()中,给hamburgerView创建实例,并将它作为Navigation Barleft bar button,并添加一个点击手势。

    let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: “hamburgerViewTapped”)
    hamburgerView = HamburgerView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
    hamburgerView!.addGestureRecognizer(tapGestureRecognizer)
    navigationItem.leftBarButtonItem = UIBarButtonItem(customView: hamburgerView!)

    hamburgerViewTapped() 方法需要触发ContainerViewController中的hideOrShowMenu(**_:animated:)方法,但是问题是show应该传什么参数?所以ContainerViewController**需要一个Bool值来记录菜单显示/隐藏的状态。

    ContainerViewController.swift中添加以下属性:

    var showingMenu = false

    初始时菜单的显示状态是false,重写viewDidLayoutSubviews()方法,在bounds改变的时候来显示/隐藏菜单。

    override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    hideOrShowMenu(showingMenu, animated: false)
    }

    你将不再需要viewDidLoad()方法,所以将它从ContainerViewController.swift中删除。

    打开DetailViewController.swift,添加以下代码:

    func hamburgerViewTapped() {
    let navigationController = parentViewController as! UINavigationController
    let containerViewController = navigationController.parentViewController as! ContainerViewController
    containerViewController.hideOrShowMenu(!containerViewController.showingMenu, animated: true)
    }

    如果showingMenu的值为false,菜单栏为隐藏状态,这是,当用户点击按钮,hideOrShowMenu(:animated:)方法就会被调用,show参数传入true,然后菜单显示。相反,当菜单处于显示状态时,即showingMenu值为true,这时用户点击按钮,菜单栏就会隐藏。因此,需要更新ContainerViewController.swifthideOrShowMenu(:animated:)方法的showingMenu属性。

    hideOrShowMenu(_:animated:):中添加以下代码:

    showingMenu = show

    运行程序,尝试去滚动视图、点击菜单按钮、点击单元行这些操作。

    这存在一个问题:如果是采用滑出/滑入菜单栏的操作,那么菜单栏按钮要点两次,菜单栏才有响应。这是为什么?

    这个问题出现的原因为:在滚动的后,showingMenu没有更改,菜单滑出的时候showingMenu还是false。当第一次点击的时候,showingMenu的值设置为true,所以菜单还是显示状态。第二次点击的时候,才能将showingMenu置为false,从而隐藏菜单。

    要解决这个问题,需要在ContainerViewControllerUIScrollViewDelegate代理方法中将showingMenu设置一下,具体如下:

    func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
    let menuOffset = CGRectGetWidth(menuContainerView.bounds)
    showingMenu = !CGPointEqualToPoint(CGPoint(x: menuOffset, y: 0), scrollView.contentOffset)
    println(“didEndDecelerating showingMenu \(showingMenu)”)
    }

    当滚动结束以后,如果Scroll View的偏移量与菜单栏的宽度相同(即,菜单栏处于隐藏状态),那就将showingMenu设置为false。相反,则置为true。

    运行程序,向右滑动,当滚动停止时,查看控制台打印信息。它的调用取决于Scroll View的滚动速度。当我在模拟器上运行时,只有慢慢的滚动,才会调用这个方法,但是当我在真机上运行时,它又需要快速滚动才能调用。

    所以,将这段代码放入到scrollViewDidScroll(_:)方法中,这样可以更好的响应滚动。这个方法在用户滚动时就会调用,这也更加的可靠。

    接下来就可以给菜单按钮加旋转动画了,但在此之前,我们先给菜单栏加3D翻转动画。

    给菜单栏加立体效果

    这个超级炫酷的动画看起来就像开门和关门。与此同时,菜单按钮也应该跟随菜单的状态,有一个平滑的旋转效果。

    为了达到这个效果,我们将计算菜单滑出的比例(后文称为fraction),从而得到按钮应该旋转的角度。

    ContainerViewController.swift中,添加一个私有方法来通过比例进行3D旋转动画:

    func transformForFraction(fraction:CGFloat) -> CATransform3D {
    var identity = CATransform3DIdentity
    identity.m34 = -1.0 / 1000.0;
    let angle = Double(1.0 - fraction) * -M_PI_2
    let xOffset = CGRectGetWidth(menuContainerView.bounds) * 0.5
    let rotateTransform = CATransform3DRotate(identity, CGFloat(angle), 0.0, 1.0, 0.0)
    let translateTransform = CATransform3DMakeTranslation(xOffset, 0.0, 0.0)
    return CATransform3DConcat(rotateTransform, translateTransform)
    }

    下面是transformForFraction(_:)这个方法的实现步骤:

    <li>
    

    在菜单完全隐藏时,fraction = 0,完全显示时,fraction = 1;

    • CATransform3DIdentity是一个4*4的矩阵,对角线是1,其他是0;
    • CATransform3DIdentitym34属性是这个矩阵中的第三列第四行,它控制着变换中的立体程度;
    • CATransform3DRotate通过angle这个变量来控制y轴的旋转量:-90度呈现的是菜单栏垂直于屏幕的状态,0度呈现的是与xy轴平行的状态;
    • rotateTransform的旋转变换,是根据m34矩阵与y轴旋转量来的;
    • translateTransform设置了x轴的偏移量为菜单栏的一般;

    • CATransform3DConcatrotateTransformtranslateTransform联系起来,使得在做翻转动画时,也有偏移动画。

    接下来,在scrollViewDidScroll(_:):中添加下面代码:

    let multiplier = 1.0 / CGRectGetWidth(menuContainerView.bounds)
    let offset = scrollView.contentOffset.x * multiplier
    let fraction = 1.0 - offset
    menuContainerView.layer.transform = transformForFraction(fraction)
    menuContainerView.alpha = fraction

    offset的值在0到1之间。当offset的值为0的时候,菜单栏处于显示状态;当offset为1的时候,菜单栏处于隐藏状态。

    fraction是菜单显示部分所占的比例,范围在0到1之间,0是菜单栏完全隐藏,1是显示状态。

    同时,fraction也用来调整菜单栏的alpha值,从暗到明,从隐藏到显示。

    运行程序,滑动看一下这个3D效果,但是…菜单栏的变换关系有点问题(译者注:原文用的”hinge”这个词,这里的问题是菜单栏沿着中心轴在旋转),原因是菜单栏的锚点在中心点。

    为了让菜单栏沿着右边距旋转,需要在ContainerViewController.swiftviewDidLayoutSubviews()方法中添加以下方法:

    menuContainerView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)

    将锚点的x设为1,y设为0,这样就可以沿右边距旋转了。

    运行程序,可以看到完美的3D旋转效果。

    最后一件事:给菜单按钮加旋转动画

    菜单按钮动画将是整个App的点睛之笔。

    当菜单栏隐藏的时候,按钮处于原有状态,当菜单栏显示的时候,按钮旋转90度。

    技术上来讲,是按钮的图片在旋转,但是实际看起来的就效果,就像是按钮在旋转。

    HamburgerView.swift中添加以下代码:

    func rotate(fraction: CGFloat) {
    let angle = Double(fraction) * M_PI_2
    imageView.transform = CGAffineTransformMakeRotation(CGFloat(angle))
    }

    现在菜单按钮可以平滑的旋转了。通过fraction来计算应该旋转的角度,其中M_PI_2常量是定义在math.h头文件中的,表示pi/2。

    添加以下代码到scrollViewDidScroll(_:)方法中,这样旋转角度可以和滚动偏移量结合起来:

    if let detailViewController = detailViewController {
    if let rotatingView = detailViewController.hamburgerView {
    rotatingView.rotate(fraction)
    }
    }

    运行程序,可以看到动画已经能很好的结合起来了。

    最后

    可以在这里下载这个项目的最终版。

    本文简单的实验了以下m34的3D旋转效果,如果想要了解更多3D变换,可以看Richard Turton的Visual Tool for CATransform3D

    维基百科上也有一些文章),使用图片很好的解释了立体视觉这个概念。

    同时,你也可以思考一下,还有那些3D效果可以用在你的App中,来提高用户交互体验。就像是菜单按钮这样的微妙的动画,细节的处理能很好的提高用户体验。

    译者注

    第一次翻译,还请见谅,之后也会尝试着翻译一些其他的文章,尽请期待。如果有什么建议,请留言或者邮箱联系,谢谢。