UIKit框架(十八) —— 基于CALayer屬性的一種3D邊欄動(dòng)畫的實(shí)現(xiàn)(一)

版本記錄

版本號(hào) 時(shí)間
V1.0 2019.05.16 星期四

前言

iOS中有關(guān)視圖控件用戶能看到的都在UIKit框架里面辐宾,用戶交互也是通過UIKit進(jìn)行的狱从。感興趣的參考上面幾篇文章。
1. UIKit框架(一) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(一)
2. UIKit框架(二) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(二)
3. UIKit框架(三) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(一)
4. UIKit框架(四) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(二)
5. UIKit框架(五) —— 自定義控件:可重復(fù)使用的滑塊(一)
6. UIKit框架(六) —— 自定義控件:可重復(fù)使用的滑塊(二)
7. UIKit框架(七) —— 動(dòng)態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(一)
8. UIKit框架(八) —— 動(dòng)態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(二)
9. UIKit框架(九) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(一)
10. UIKit框架(十) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(二)
11. UIKit框架(十一) —— UICollectionView的重用叠纹、選擇和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用季研、選擇和重排序(二)
13. UIKit框架(十三) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(一)
14. UIKit框架(十四) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(二)
15. UIKit框架(十五) —— 基于自定義UICollectionViewLayout布局的簡(jiǎn)單示例(一)
16. UIKit框架(十六) —— 基于自定義UICollectionViewLayout布局的簡(jiǎn)單示例(二)
17. UIKit框架(十七) —— 基于自定義UICollectionViewLayout布局的簡(jiǎn)單示例(三)

開始

首先看下寫作環(huán)境

Swift 4.2, iOS 12, Xcode 10

許多iOS應(yīng)用程序需要一個(gè)菜單來在視圖之間導(dǎo)航或讓用戶做出選擇。 一種常用的設(shè)計(jì)是側(cè)面菜單誉察。

您可以使用簡(jiǎn)單的表單輕松制作側(cè)邊菜單与涡,但是如何在UI中引入一些樂趣呢? 你想在用戶的臉上露出微笑持偏,并一次又一次地將它們帶回你的應(yīng)用程序驼卖。 實(shí)現(xiàn)此目的的一種方法是創(chuàng)建3D側(cè)邊欄動(dòng)畫。

在本教程中鸿秆,您將學(xué)習(xí)如何通過操縱CALayer屬性來創(chuàng)建3D側(cè)邊欄動(dòng)畫來為一些UIView元素設(shè)置動(dòng)畫款慨。 這個(gè)動(dòng)畫的靈感來自一個(gè)名為TaaskyTo-Do應(yīng)用程序。

在本教程中谬莹,您將使用以下元素:

  • Storyboards
  • Auto Layout constraints
  • UIScrollView
  • View controller containment
  • Core Animation

打開名為TaskChooser的入門項(xiàng)目。

想象一下,您正在創(chuàng)建一個(gè)與您的同事或朋友談判活動(dòng)的基本應(yīng)用程序附帽。如果你在里面豎起大拇指埠戳,如果你不能成功,請(qǐng)大拇指向下蕉扮。你甚至可以因天氣惡劣而下降整胃。

花點(diǎn)時(shí)間看一下這個(gè)項(xiàng)目。你會(huì)看到它是一個(gè)標(biāo)準(zhǔn)的Xcode Master-Detail模板應(yīng)用程序喳钟,它顯示了一個(gè)圖像表屁使。

  • MenuViewController:一個(gè)UITableViewController,它使用自定義表格視圖單元MenuItemCell來設(shè)置每個(gè)單元格的背景顏色奔则。它還有一個(gè)圖像蛮寂。
  • MenuDataSource:實(shí)現(xiàn)UITableViewDataSource以從MenuItems.json提供表數(shù)據(jù)的對(duì)象。這些數(shù)據(jù)可能來自生產(chǎn)情況下的服務(wù)器易茬。
  • DetailViewController:使用與您選擇的單元格相同的背景顏色顯示大圖像酬蹋。

構(gòu)建并運(yùn)行應(yīng)用程序。您應(yīng)該看到啟動(dòng)項(xiàng)目加載了7行顏色和圖標(biāo):

使用菜單顯示您選擇的選項(xiàng):

這是功能性的抽莱,但外觀和感覺相當(dāng)普通范抓。 你希望你的應(yīng)用程序既令人驚喜又高興!

在本教程中食铐,您將把Master-Detail應(yīng)用程序重構(gòu)為水平滾動(dòng)視圖匕垫。 您將在容器視圖中嵌入masterdetail視圖。

接下來虐呻,您將添加一個(gè)按鈕來顯示或隱藏菜單象泵。 然后,您將在菜單上添加整齊的3D折疊效果铃慷。

作為此3D動(dòng)畫側(cè)邊欄的最后一步单芜,您將同步旋轉(zhuǎn)菜單按鈕以顯示或隱藏菜單。

您的第一個(gè)任務(wù)是將MenuViewControllerDetailViewController轉(zhuǎn)換為滑出側(cè)邊欄犁柜,其中滾動(dòng)視圖包含菜單和詳細(xì)視圖并排洲鸠。


Restructuring Your Storyboard

在重建菜單之前,您需要進(jìn)行一些拆卸馋缅。

Project導(dǎo)航器的Views文件夾中打開Main.storyboard扒腕。 你可以看到由segues連接的UINavigationControllerMenuViewControllerDetailViewController

1. Deleting the Old Structure

導(dǎo)航控制器場(chǎng)景(Navigation Controller Scene)不會(huì)激發(fā)快樂萤悴。 選擇該場(chǎng)景并將其刪除瘾腰。 接下來,選擇MenuViewControllerDetailViewController之間的segue并刪除它覆履。

完成后蹋盆,開始工作费薄。

2. Adding a New Root Container

由于UINavigationController消失了,您不再擁有項(xiàng)目中視圖控制器的頂級(jí)容器栖雾。 你現(xiàn)在就加一個(gè)楞抡。

Project導(dǎo)航器中選擇Views文件夾。 按Command-N將新文件添加到項(xiàng)目中析藕。 然后:

  • 1) 選擇iOS?CocoaTouch Class召廷。 點(diǎn)擊Next
  • 2) 將類命名為RootViewController账胧。
  • 3) 確保RootViewControllerUIViewController的子類竞慢。
  • 4) 確保未選中Also create XIB file
  • 5) 語(yǔ)言應(yīng)該是Swift治泥。

再次打開Main.storyboard筹煮。

使用快捷鍵Command-Shift-L打開對(duì)象庫(kù),并將UIViewController的實(shí)例拖到故事板车摄。

從對(duì)象層次結(jié)構(gòu)中選擇View Controller Scene寺谤,然后打開Identity inspector。 將Class區(qū)域設(shè)置為RootViewController吮播。

接下來变屁,打開Attributes inspector,然后選中Is Initial View Controller框意狠。

3. Adding Identifiers to View Controllers

由于MenuViewControllerDetailViewController不再通過segues連接粟关,因此您需要一種從代碼中訪問它們的方法。 因此环戈,您的下一步是提供一些標(biāo)識(shí)符來執(zhí)行此操作闷板。

從對(duì)象層次結(jié)構(gòu)中選擇Menu View Controller Scene。 打開Identity inspector并將Storyboard ID設(shè)置為MenuViewController院塞。

這個(gè)字符串可以是任何合理的值遮晚,但一個(gè)易于記憶的技術(shù)是使用類的名稱。

接下來拦止,從Object層次結(jié)構(gòu)中選擇Detail View Controller Scene并執(zhí)行相同的操作县遣。 將Storyboard ID設(shè)置為DetailViewController

這就是你需要在Main.storyboard中做的所有事情汹族。 本教程的其余部分將在代碼中萧求。


Creating Contained View Controllers

在本節(jié)中,您將創(chuàng)建一個(gè)UIScrollView并向該滾動(dòng)視圖添加兩個(gè)容器顶瞒。 容器將保存MenuViewControllerDetailViewController夸政。

1. Creating a Scroll View

您的第一步是創(chuàng)建UIScrollView

Project導(dǎo)航器中打開RootViewController.swift榴徐。 刪除Xcode從RootViewController內(nèi)部提供的所有內(nèi)容守问。

RootViewController上面添加此擴(kuò)展:

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
  }
}

這是一個(gè)幫助方法匀归,您將在本教程中使用幾次。 代碼將傳入的視圖添加為子視圖酪碘,然后添加四個(gè)約束以將子視圖粘貼到其自身內(nèi)朋譬。

接下來在文件末尾添加此擴(kuò)展名:

extension RootViewController: UIScrollViewDelegate {
}

您將需要監(jiān)聽UIScrollView以進(jìn)行更改。 該操作稍后在本教程中進(jìn)行兴垦,因此此擴(kuò)展目前為空。

最后字柠,在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) 首先探越,創(chuàng)建一個(gè)UIScrollView。 您希望啟用分頁(yè)窑业,以便內(nèi)容在滾動(dòng)視圖內(nèi)以原子單位移動(dòng)钦幔。 您已禁用delayedContentTouches,以便內(nèi)部控制器能夠快速響應(yīng)用戶觸摸常柄。 bounces設(shè)置為false鲤氢,因此您不會(huì)從滾動(dòng)條獲得彈性感。 然后西潘,將RootViewController設(shè)置為scroll view的代理卷玉。
  • 2) 在viewDidLoad()中,您可以使用之前添加的幫助方法設(shè)置背景顏色并將scroll view嵌入根視圖中喷市。
  • 3) 對(duì)preferredStatusBarStyle的覆蓋允許狀態(tài)欄在深色背景上顯示為淺色相种。

構(gòu)建并運(yùn)行您的應(yīng)用程序,以顯示它在此重構(gòu)后正確啟動(dòng):

由于您尚未將按鈕和內(nèi)容添加到新的RootViewController品姓,因此您應(yīng)該只能看到已設(shè)置的深色背景沸移。 別擔(dān)心章贞,您將在下一節(jié)中將它們添加回來。

2. Creating Containers

現(xiàn)在,您將創(chuàng)建UIView實(shí)例醋旦,它們將充當(dāng)MenuViewControllerDetailViewController的容器。 然后阻桅,您將它們添加到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的子視圖惧互,并為其添加臨時(shí)顏色哎媚。 在開發(fā)過程中使用非品牌顏色是了解開發(fā)過程中工作進(jìn)展的好方法。
  • 2) 接下來喊儡,將menuContainer的頂部和底部固定到scroll view的相同邊緣拨与。
  • 3) 最后,將width設(shè)置為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) 頂部今缚,底部和右側(cè)邊緣固定到它們各自的scroll view邊緣。 detailContainerleading edge連接到menuContainer低淡。
  • 3) 最后姓言,容器的寬度始終與scroll view的寬度相同。

要讓UIScrollView滾動(dòng)其內(nèi)容蔗蹋,它需要知道該內(nèi)容有多大何荚。 您可以通過使用UIScrollViewcontentSize屬性或隱式定義內(nèi)容的大小來實(shí)現(xiàn)。

在這種情況下猪杭,內(nèi)容大小由五件事隱式定義:

  • 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()

構(gòu)建并運(yùn)行您的應(yīng)用程序,看看一些糖果色的奇跡皂吮。 您可以拖動(dòng)內(nèi)容以隱藏橙色菜單容器戒傻。 您已經(jīng)可以看到成品開始形成。

3. Adding Contained View Controllers

您正在構(gòu)建創(chuàng)建界面所需的視圖堆棧蜂筹。 下一步是在您創(chuàng)建的容器中安裝MenuViewControllerDetailViewController需纳。

你仍然想要一個(gè)導(dǎo)航欄,因?yàn)槟阆胍粋€(gè)放置菜單顯示按鈕的地方狂票。 將此擴(kuò)展添加到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
  }
}

以下是此代碼中發(fā)生的情況:

  • 1) 此方法采用視圖控制器候齿,將其安裝在UINavigationController中,然后設(shè)置導(dǎo)航欄的視覺樣式闺属。
  • 2) 視圖控制器包含的最重要部分是addChild(nav)慌盯。 這會(huì)將UINavigationController安裝為RootViewController的子視圖控制器。 這意味著在iPad上旋轉(zhuǎn)或拆分視圖導(dǎo)致的特征變化等事件可以在層次結(jié)構(gòu)中向下傳播給子節(jié)點(diǎn)掂器。

接下來亚皂,在installInNavigationController(_ :)之后將此方法添加到同一擴(kuò)展中以幫助安裝MenuViewControllerDetailViewController

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
}

此方法從故事板中實(shí)例化視圖控制器,警告開發(fā)人員故事板中斷国瓮。

然后灭必,代碼將視圖控制器放在UINavigationController中,并將該導(dǎo)航控制器嵌入到容器中乃摹。

接下來禁漓,在主類中添加這些屬性以跟蹤MenuViewControllerDetailViewController

var menuViewController: MenuViewController?
var detailViewController: DetailViewController?

然后在viewDidLoad()的末尾插入這些行:

menuViewController = 
  installFromStoryboard("MenuViewController", 
                        into: menuContainer) as? MenuViewController

detailViewController = 
  installFromStoryboard("DetailViewController",
                        into: detailContainer) as? DetailViewController

在這個(gè)片段中,您實(shí)例化了MenuViewControllerDetailViewController并保留了對(duì)它們的引用孵睬,因?yàn)樯院竽鷮⑿枰鼈儭?/p>

構(gòu)建并運(yùn)行應(yīng)用程序播歼,您將看到菜單可見,雖然比以前更瘦掰读。

這些按鈕不會(huì)導(dǎo)致DetailViewController更新秘狞,因?yàn)?code>segue不再存在叭莫。 你將在下一節(jié)中解決這個(gè)問題。

您已完成本教程的視圖包含部分烁试。 現(xiàn)在你可以進(jìn)入真正有趣的東西了雇初。


Reconnect Menu and Detail Views

在你進(jìn)行拆除橫沖直撞之前,在MenuViewController中選擇一個(gè)表格單元格會(huì)觸發(fā)一個(gè)segue减响,它將選定的MenuItem傳遞給DetailViewController靖诗。

它很便宜而且完成了工作,但是有一個(gè)小問題辩蛋。 該模式需要MenuViewController了解DetailViewController呻畸。

這意味著MenuViewControllerDetailViewController緊密綁定。 如果您不再想使用DetailViewController來顯示菜單選項(xiàng)的結(jié)果悼院,會(huì)發(fā)生什么?

作為優(yōu)秀的開發(fā)人員咒循,您應(yīng)該尋求減少系統(tǒng)中的緊密綁定量据途。 您現(xiàn)在將設(shè)置一個(gè)新模式。

1. Creating a Delegate Protocol

首先要做的是在MenuViewController中創(chuàng)建一個(gè)委托協(xié)議叙甸,它允許您傳達(dá)菜單選擇更改颖医。

Project導(dǎo)航器中找到MenuViewController.swift并打開該文件。

由于您不再使用segue裆蒸,您可以繼續(xù)刪除prepare(for:sender :)熔萧。

接下來,在MenuViewController類聲明之上添加此協(xié)議定義:

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) 在第一個(gè)代碼片段中佛致,您聲明了一個(gè)感興趣的各方可以采用的協(xié)議。 在MenuViewController中辙谜,您聲明了一個(gè)weak delegate屬性俺榆。 在協(xié)議引用中使用weak有助于避免創(chuàng)建保留周期。
  • 2) 接下來装哆,實(shí)現(xiàn)UITableViewDelegate方法tableView(_:didSelectRowAt :)將選定的MenuItem傳遞給委托罐脊。
  • 3) 最后一個(gè)聲明是取消選擇單元格并刪除其突出顯示的整體操作。

2. Implementing the MenuDelegate Protocol

您現(xiàn)在可以實(shí)現(xiàn)您創(chuàng)建的協(xié)議蜕琴,以將選擇更改發(fā)送到DetailViewController萍桌。

打開RootViewController.swift并將此擴(kuò)展名添加到文件末尾:

extension RootViewController: MenuDelegate {
  func didSelectMenuItem(_ item: MenuItem) {
    detailViewController?.menuItem = item
  }
}

此代碼聲明RootViewController采用MenuDelegate。 當(dāng)您選擇一個(gè)菜單項(xiàng)時(shí)凌简,RootViewController通過將選定的MenuItem傳遞給實(shí)例來告訴DetailViewController該更改上炎。

最后,在viewDidLoad()的末尾插入此行:

menuViewController?.delegate = self

這告訴MenuViewController RootViewController是委托号醉。

構(gòu)建并運(yùn)行應(yīng)用程序反症。 您的菜單選擇現(xiàn)在將更改DetailViewController的內(nèi)容辛块。 豎起大拇指。


Controlling the Scroll View

到現(xiàn)在為止還挺好铅碍。 你的菜單工作润绵,應(yīng)用程序看起來更好。

但是胞谈,您還會(huì)注意到手動(dòng)滾動(dòng)菜單不會(huì)持續(xù)很長(zhǎng)時(shí)間尘盼。 菜單總是反彈回視圖。

滾動(dòng)視圖屬性isPagingEnabled會(huì)導(dǎo)致該效果烦绳,因?yàn)槟褜⑵湓O(shè)置為true卿捎。 你現(xiàn)在就解決這個(gè)問題。

仍然在RootViewController中工作径密,在下面添加以下行:menuWidth:CGFloat = 80.0

lazy var threshold = menuWidth/2.0

在這里午阵,您可以選擇一個(gè)任意點(diǎn),菜單將選擇隱藏或顯示自己享扔。 你使用lazy底桂,因?yàn)槟阏谟?jì)算相對(duì)于menuWidth的值。

RootViewController中找到extension RootViewController: UIScrollViewDelegate并在擴(kuò)展中插入此代碼:

//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) 第一個(gè)UIScrollViewDelegate方法惧眠,scrollViewDidScroll(_ :)籽懦,非常有用。 它總是會(huì)告訴您何時(shí)更改了scroll viewcontentOffset氛魁。 您可以根據(jù)水平偏移量是否高于閾值threshold來設(shè)置isPagingEnabled暮顺。
  • 2) 接下來,實(shí)現(xiàn)scrollViewDidEndDragging(_:willDecelerate :)以檢測(cè)滾動(dòng)視圖上的抬起觸摸秀存。 只要內(nèi)容偏移量大于閾值捶码,就隱藏菜單;否則分頁(yè)效果將保持并顯示菜單应又。
  • 3) 最后一種方法是幫助菜單將菜單設(shè)置到位置:顯示宙项,隱藏和切換。

構(gòu)建并運(yùn)行您的應(yīng)用程序株扛。 現(xiàn)在尤筐,嘗試拖動(dòng)scroll view,看看會(huì)發(fā)生什么洞就。 越過閾值時(shí)盆繁,菜單會(huì)彈出或關(guān)閉:


Adding a Menu Button

在本節(jié)中,您將向?qū)Ш綑谔砑訚h堡(burger)按鈕旬蟋,這樣您的用戶就不必拖動(dòng)來顯示和隱藏菜單油昂。

因?yàn)槟肷院鬄榇税粹o設(shè)置動(dòng)畫,所以這需要是UIView而不是基于圖像的UIBarButton

1. Creating a Hamburger View

Project導(dǎo)航器中選擇Views文件夾冕碟,然后添加一個(gè)新的Swift文件拦惋。

  • 1) 選擇iOS?CocoaTouch Class。 點(diǎn)擊Next安寺。
  • 2) 將類命名為HamburgerView厕妖。
  • 3) 確保HamburgerViewUIView的子類。
  • 4) 語(yǔ)言應(yīng)該是Swift挑庶。

打開HamburgerView.swift并使用以下代碼替換HamburgerView類中的所有內(nèi)容:

//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) 首先言秸,使用庫(kù)中的資源創(chuàng)建UIImageView
  • 2) 然后添加該圖像視圖迎捺,為兩種可能的init方法創(chuàng)建路徑举畸。

2. Installing the Hamburger View

現(xiàn)在您有了一個(gè)視圖,您可以將其安裝在屬于DetailViewController的導(dǎo)航欄中凳枝。

再次打開RootViewController.swift并在主RootViewController類的頂部插入此屬性:

var hamburgerView: HamburgerView?

接下來將此擴(kuò)展名附加到文件末尾:

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()
  }
}

最后將此語(yǔ)句添加到viewDidLoad()的底部:

if let detailViewController = detailViewController {
  installBurger(in: detailViewController)
}

這組代碼為漢堡按鈕提供了一個(gè)實(shí)例變量抄沮,因?yàn)槟芸炀拖胍獮樗O(shè)置動(dòng)畫。然后岖瑰,您可以創(chuàng)建一個(gè)方法合是,以在任何視圖控制器的導(dǎo)航欄中安裝漢堡。

方法installBurger(in :)在視圖中創(chuàng)建一個(gè)tap方法锭环,調(diào)用方法burgerTapped(_ :)

請(qǐng)注意泊藕,您必須使用@objc注釋burgerTapped(_ :)辅辩,因?yàn)槟诖颂幨褂?code>Objective-C運(yùn)行時(shí)。此方法根據(jù)當(dāng)前狀態(tài)切換菜單娃圆。

然后使用此方法在屬于DetailViewControllerUINavigationBar中安裝按鈕玫锋。從體系結(jié)構(gòu)的角度來看,DetailViewController不知道這個(gè)按鈕讼呢,也不需要處理任何菜單狀態(tài)操作撩鹿。你保持責(zé)任分離。

就這些悦屏。在構(gòu)建對(duì)象堆棧時(shí)节沦,使3D側(cè)邊欄動(dòng)畫生動(dòng)的步驟變得越來越少。

構(gòu)建并運(yùn)行您的應(yīng)用程序础爬。你會(huì)看到你現(xiàn)在有一個(gè)漢堡按鈕可以切換菜單甫贯。


Adding Perspective to the Menu

為了回顧你到目前為止所做的事情,你已經(jīng)將Master-Detail應(yīng)用程序重構(gòu)為可行的側(cè)面菜單式應(yīng)用程序看蚜,用戶可以拖動(dòng)或使用按鈕來顯示和隱藏菜單叫搁。

現(xiàn)在,為您的下一步:菜單的動(dòng)畫版本應(yīng)該看起來像一個(gè)面板打開和關(guān)閉。 菜單按鈕將在菜單打開時(shí)順時(shí)針旋轉(zhuǎn)渴逻,在菜單關(guān)閉時(shí)逆時(shí)針旋轉(zhuǎn)疾党。

為此,您將計(jì)算可見的菜單視圖的分?jǐn)?shù)惨奕,然后使用它來計(jì)算菜單的旋轉(zhuǎn)角度雪位。

1. Manipulating the Menu Layer

仍在RootViewController.swift中,將此擴(kuò)展名添加到文件中:

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 :)的逐個(gè)分析:

  • 1) CATransform3DIdentity是一個(gè)4×4矩陣墓贿,其中對(duì)角線為1茧泪,其他地方為零。 CATransform3DIdentitym34屬性是第3行第4列中的值聋袋,它控制轉(zhuǎn)換中的透視量队伟。
  • 2) 角度和偏移量作為輸入fraction的函數(shù)計(jì)算。 當(dāng)fraction1.0時(shí)幽勒,菜單將完全隱藏嗜侮,當(dāng)它為0.0時(shí),菜單將完全可見啥容。
  • 3) 計(jì)算最終變換锈颗。 CATransform3DRotate使用angle來確定圍繞y軸的旋轉(zhuǎn)量:-90度使菜單垂直于視圖的背面,0度渲染菜單與x-y平面平行咪惠,CATransform3DMakeTranslation將菜單移動(dòng)到中心的右側(cè)击吱, 和CATransform3DConcat連接translateTransformrotateTransform,以便菜單在旋轉(zhuǎn)時(shí)顯示為側(cè)滑遥昧。

注意:m34值通常計(jì)算為1除以表示觀察者在z軸上的位置的數(shù)字覆醇,同時(shí)觀察2D x-y平面。 負(fù)z值表示觀察者在平面前面炭臭,而正z值表示觀察者在平面后面永脓。

在此viewer與平面中對(duì)象邊緣之間繪制線條會(huì)產(chǎn)生3D透視效果。 當(dāng)viewer移動(dòng)得更遠(yuǎn)時(shí)鞋仍,視角不太明顯常摧。 嘗試更改1,0005002,000,以查看菜單的透視圖如何更改威创。

接下來落午,將此擴(kuò)展添加到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
  }  
}

此代碼提供了一些用于打開和關(guān)閉菜單的幫助程序:

  • 1) calculateMenuDisplayFraction(_ :)將原始水平偏移量轉(zhuǎn)換為相對(duì)于菜單寬度的1.0的fraction。 該值夾在0.0和1.0之間那婉。
  • 2) updateViewVisibility(_:fraction :)將分?jǐn)?shù)生成的變換應(yīng)用于視圖層板甘。 anchorPoint是轉(zhuǎn)換應(yīng)用的鉸鏈,因此CGPoint(x:1.0详炬,y:0.5)表示右手邊緣和垂直中心盐类。

通過設(shè)置alpha寞奸,隨著轉(zhuǎn)換的進(jìn)行,視圖也會(huì)變暗在跳。

現(xiàn)在枪萄,找到scrollViewDidScroll(_ :)并在方法的末尾插入這些行:

let fraction = calculateMenuDisplayFraction(scrollView)
updateViewVisibility(menuContainer, fraction: fraction)

構(gòu)建并運(yùn)行應(yīng)用程序。 當(dāng)您向左拖動(dòng)詳細(xì)視圖時(shí)猫妙,菜單現(xiàn)在似乎在細(xì)節(jié)視圖下折疊瓷翻。


Rotating the Burger Button

在本教程中,您要做的最后一件事是在scroll view移動(dòng)時(shí)使?jié)h堡按鈕看起來在屏幕上滾動(dòng)割坠。

打開HamburgerView.swift并將此方法插入到類中:

func setFractionOpen(_ fraction: CGFloat) {
  let angle = fraction * .pi/2.0
  imageView.transform = CGAffineTransform(rotationAngle: angle)
}

此代碼將視圖作為fraction的函數(shù)旋轉(zhuǎn)齐帚。 當(dāng)菜單完全打開時(shí),視圖旋轉(zhuǎn)90度彼哼。

返回RootViewController.swift对妄。 找到scrollViewDidScroll(_ :)并將此行追加到方法的末尾:

hamburgerView?.setFractionOpen(1.0 - fraction)

當(dāng)scroll view移動(dòng)時(shí),這會(huì)旋轉(zhuǎn)漢堡按鈕敢朱。

然后剪菱,因?yàn)閱?dòng)應(yīng)用程序時(shí)菜單已打開,所以將此行添加到viewDidLoad()的末尾以將菜單置于正確的初始狀態(tài):

hamburgerView?.setFractionOpen(1.0)

構(gòu)建并運(yùn)行您的應(yīng)用程序拴签。 滑動(dòng)并點(diǎn)按菜單以查看動(dòng)態(tài)和同步的3D側(cè)邊欄動(dòng)畫:

在本教程中孝常,您用到了:

  • 視圖控制器。
  • UIScrollView隱式內(nèi)容大小蚓哩。
  • 代理模式构灸。
  • 透視隨CATransform3Dm34而變化。

嘗試使用m34值來查看它對(duì)轉(zhuǎn)換的影響岸梨。 如果您想了解有關(guān)m34的更多信息冻押,請(qǐng)閱讀this xdPixel blog post

WikipediaPerspective頁(yè)面有一些很好的照片盛嘿,解釋了視覺透視Perspective的概念。

另外括袒,請(qǐng)考慮如何在自己的應(yīng)用程序中使用此3D側(cè)邊欄動(dòng)畫次兆,為用戶交互添加一點(diǎn)生命。 令人驚訝的是锹锰,對(duì)菜單這么簡(jiǎn)單的東西的微妙影響可以增加整個(gè)用戶體驗(yàn)芥炭。

后記

本篇主要講述了基于CALayer屬性的一種3D邊欄動(dòng)畫的實(shí)現(xiàn),感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末恃慧,一起剝皮案震驚了整個(gè)濱河市园蝠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌痢士,老刑警劉巖彪薛,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡善延,警方通過查閱死者的電腦和手機(jī)少态,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來易遣,“玉大人彼妻,你說我怎么就攤上這事《姑#” “怎么了侨歉?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)揩魂。 經(jīng)常有香客問我幽邓,道長(zhǎng),這世上最難降的妖魔是什么肤京? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任颊艳,我火速辦了婚禮,結(jié)果婚禮上忘分,老公的妹妹穿的比我還像新娘棋枕。我一直安慰自己,他們只是感情好妒峦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布重斑。 她就那樣靜靜地躺著,像睡著了一般肯骇。 火紅的嫁衣襯著肌膚如雪窥浪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天笛丙,我揣著相機(jī)與錄音漾脂,去河邊找鬼。 笑死胚鸯,一個(gè)胖子當(dāng)著我的面吹牛骨稿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播姜钳,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼坦冠,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了哥桥?” 一聲冷哼從身側(cè)響起辙浑,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拟糕,沒想到半個(gè)月后判呕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體倦踢,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年佛玄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了硼一。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡梦抢,死狀恐怖般贼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情奥吩,我是刑警寧澤哼蛆,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站霞赫,受9級(jí)特大地震影響腮介,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜端衰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一叠洗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧旅东,春花似錦灭抑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至荤牍,卻和暖如春案腺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背康吵。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工劈榨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人晦嵌。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓鞋既,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親耍铜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容