如何复制iOS 10的Apple Music“Peek and Pop操作菜单”

11

iOS 10 拥有一项我想要复制的功能。当你在 Apple Music 应用程序中使用 3D Touch 打开一个专辑时,它会打开下面所示的菜单。但与正常的 Peek and Pop 不同的是,当你抬起手指时它不会消失。我该如何复制这个功能呢?

在此输入图片描述


你确定这是3D Touch而不仅仅是长按吗?它在我的iPhone 6+上也可以使用,而该设备并没有3D Touch功能。 - Fogmeister
@Fogmeister 好的,我已经关闭了3D Touch。如果你注意到它显示相同的内容,但在底部添加了一个取消按钮。实际上,我想两者都做。但如何实现仍然是个问题。 - Austin E
不使用 peek 和 pop segue,是否可以使用某种力触手势来触发它?我现在不在电脑旁边,但那就是我要找的东西。 - Fogmeister
@Fogmeister 好的,我会研究一下。在切换保持和3D触摸时,它们似乎是以不同的方式完成的。当保持时,它从底部向上动画。当进行3D触摸时,它从施加力的位置动画进入。 - Austin E
3D Touch的好处在于您不必抬起手指即可选择操作,非常快捷。希望这可以成为默认组件。 - Lapidus
3个回答

2
我最接近复制它的代码如下。它创建了一个音乐应用程序的虚拟副本。然后我添加了PeekPop-3D-Touch委托。
但是,在委托中,我向手势识别器添加了一个观察者,并在查看时取消了手势,但在抬起手指时重新启用了它。为了重新启用它,我使用了异步方法,因为没有异步调度预览会立即消失。我找不到其他解决方法。
现在,如果你在蓝色框外面点击,它会像平常一样消失。

http://i.imgur.com/073M2Ku.jpg http://i.imgur.com/XkwUBly.jpg

enter image description here enter image description here

//
//  ViewController.swift
//  PeekPopExample
//
//  Created by Brandon Anthony on 2016-07-16.
//  Copyright © 2016 XIO. All rights reserved.
//

import UIKit


class MusicViewController: UITabBarController, UITabBarControllerDelegate {

    var tableView: UITableView!
    var collectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.initControllers()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    func initControllers() {
        let libraryController = LibraryViewController()
        let forYouController = UIViewController()
        let browseController = UIViewController()
        let radioController = UIViewController()
        let searchController = UIViewController()

        libraryController.title = "Library"
        libraryController.tabBarItem.image = nil

        forYouController.title = "For You"
        forYouController.tabBarItem.image = nil

        browseController.title = "Browse"
        browseController.tabBarItem.image = nil

        radioController.title = "Radio"
        radioController.tabBarItem.image = nil

        searchController.title = "Search"
        searchController.tabBarItem.image = nil

        self.viewControllers = [libraryController, forYouController, browseController, radioController, searchController];
    }


}

实施ForceTouch暂停功能。
//
//  LibraryViewController.swift
//  PeekPopExample
//
//  Created by Brandon Anthony on 2016-07-16.
//  Copyright © 2016 XIO. All rights reserved.
//

import Foundation
import UIKit


//Views and Cells..

class AlbumView : UIView {
    var albumCover: UIImageView!
    var title: UILabel!
    var artist: UILabel!

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.initControls()
        self.setTheme()
        self.doLayout()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func initControls() {
        self.albumCover = UIImageView()
        self.title = UILabel()
        self.artist = UILabel()
    }

    func setTheme() {
        self.albumCover.contentMode = .scaleAspectFit
        self.albumCover.layer.cornerRadius = 5.0
        self.albumCover.backgroundColor = UIColor.lightGray()

        self.title.text = "Unknown"
        self.title.font = UIFont.systemFont(ofSize: 12)

        self.artist.text = "Unknown"
        self.artist.textColor = UIColor.lightGray()
        self.artist.font = UIFont.systemFont(ofSize: 12)
    }

    func doLayout() {
        self.addSubview(self.albumCover)
        self.addSubview(self.title)
        self.addSubview(self.artist)

        let views = ["albumCover": self.albumCover, "title": self.title, "artist": self.artist];
        var constraints = Array<String>()

        constraints.append("H:|-0-[albumCover]-0-|")
        constraints.append("H:|-0-[title]-0-|")
        constraints.append("H:|-0-[artist]-0-|")
        constraints.append("V:|-0-[albumCover]-[title]-[artist]-0-|")

        let aspectRatioConstraint = NSLayoutConstraint(item: self.albumCover, attribute: .width, relatedBy: .equal, toItem: self.albumCover, attribute: .height, multiplier: 1.0, constant: 0.0)

        self.addConstraint(aspectRatioConstraint)

        for constraint in constraints {
            self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: constraint, options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views))
        }

        for view in self.subviews {
            view.translatesAutoresizingMaskIntoConstraints = false
        }
    }
}

class AlbumCell : UITableViewCell {
    var firstAlbumView: AlbumView!
    var secondAlbumView: AlbumView!

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        self.initControls()
        self.setTheme()
        self.doLayout()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func initControls() {
        self.firstAlbumView = AlbumView(frame: CGRect.zero)
        self.secondAlbumView = AlbumView(frame: CGRect.zero)
    }

    func setTheme() {

    }

    func doLayout() {
        self.contentView.addSubview(self.firstAlbumView)
        self.contentView.addSubview(self.secondAlbumView)

        let views: [String: AnyObject] = ["firstAlbumView": self.firstAlbumView, "secondAlbumView": self.secondAlbumView];
        var constraints = Array<String>()

        constraints.append("H:|-15-[firstAlbumView(==secondAlbumView)]-15-[secondAlbumView(==firstAlbumView)]-15-|")
        constraints.append("V:|-15-[firstAlbumView]-15-|")
        constraints.append("V:|-15-[secondAlbumView]-15-|")

        for constraint in constraints {
            self.contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: constraint, options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views))
        }

        for view in self.contentView.subviews {
            view.translatesAutoresizingMaskIntoConstraints = false
        }
    }
}



//Details..

class DetailSongViewController : UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = UIColor.blue()
    }

    /*override func previewActionItems() -> [UIPreviewActionItem] {
        let regularAction = UIPreviewAction(title: "Regular", style: .default) { (action: UIPreviewAction, vc: UIViewController) -> Void in

        }

        let destructiveAction = UIPreviewAction(title: "Destructive", style: .destructive) { (action: UIPreviewAction, vc: UIViewController) -> Void in

        }

        let actionGroup = UIPreviewActionGroup(title: "Group...", style: .default, actions: [regularAction, destructiveAction])

        return [actionGroup]
    }*/
}











//Implementation..

extension LibraryViewController : UIViewControllerPreviewingDelegate {
    func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {

        guard let indexPath = self.tableView.indexPathForRow(at: location) else {
            return nil
        }

        guard let cell = self.tableView.cellForRow(at: indexPath) else {
            return nil
        }


        previewingContext.previewingGestureRecognizerForFailureRelationship.addObserver(self, forKeyPath: "state", options: .new, context: nil)


        let detailViewController = DetailSongViewController()
        detailViewController.preferredContentSize = CGSize(width: 0.0, height: 300.0)
        previewingContext.sourceRect = cell.frame
        return detailViewController
    }

    func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) {

        //self.show(viewControllerToCommit, sender: self)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: AnyObject?, change: [NSKeyValueChangeKey : AnyObject]?, context: UnsafeMutablePointer<Void>?) {
        if let object = object {
            if keyPath == "state" {
                let newValue = change![NSKeyValueChangeKey.newKey]!.integerValue
                let state = UIGestureRecognizerState(rawValue: newValue!)!
                switch state {
                case .began, .changed:
                    self.navigationItem.title = "Peeking"
                    (object as! UIGestureRecognizer).isEnabled = false

                case .ended, .failed, .cancelled:
                    self.navigationItem.title = "Not committed"
                    object.removeObserver(self, forKeyPath: "state")

                    DispatchQueue.main.async(execute: { 
                        (object as! UIGestureRecognizer).isEnabled = true
                    })


                case .possible:
                    break
                }
            }
        }
    }
}


class LibraryViewController : UIViewController, UITableViewDelegate, UITableViewDataSource {

    var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.initControls()
        self.setTheme()
        self.registerClasses()
        self.registerPeekPopPreviews();
        self.doLayout()
    }

    func initControls() {
        self.tableView = UITableView(frame: CGRect.zero, style: .grouped)
    }

    func setTheme() {
        self.edgesForExtendedLayout = UIRectEdge()
        self.tableView.dataSource = self;
        self.tableView.delegate = self;
    }

    func registerClasses() {
        self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Default")
        self.tableView.register(AlbumCell.self, forCellReuseIdentifier: "AlbumCell")
    }

    func registerPeekPopPreviews() {
        //if (self.traitCollection.forceTouchCapability == .available) {
            self.registerForPreviewing(with: self, sourceView: self.tableView)
        //}
    }

    func doLayout() {
        self.view.addSubview(self.tableView)

        let views: [String: AnyObject] = ["tableView": self.tableView];
        var constraints = Array<String>()

        constraints.append("H:|-0-[tableView]-0-|")
        constraints.append("V:|-0-[tableView]-0-|")

        for constraint in constraints {
            self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: constraint, options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views))
        }

        for view in self.view.subviews {
            view.translatesAutoresizingMaskIntoConstraints = false
        }
    }



    func numberOfSections(in tableView: UITableView) -> Int {
        return 2
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return section == 0 ? 5 : 10
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return (indexPath as NSIndexPath).section == 0 ? 44.0 : 235.0
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return section == 0 ? 75.0 : 50.0
    }

    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return 0.0001
    }

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return section == 0 ? "Library" : "Recently Added"
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        if (indexPath as NSIndexPath).section == 0 { //Library
            let cell = tableView.dequeueReusableCell(withIdentifier: "Default", for: indexPath)

            switch (indexPath as NSIndexPath).row {
            case 0:
                cell.accessoryType = .disclosureIndicator
                cell.textLabel?.text = "Playlists"

            case 1:
                cell.accessoryType = .disclosureIndicator
                cell.textLabel?.text = "Artists"

            case 2:
                cell.accessoryType = .disclosureIndicator
                cell.textLabel?.text = "Albums"

            case 3:
                cell.accessoryType = .disclosureIndicator
                cell.textLabel?.text = "Songs"

            case 4:
                cell.accessoryType = .disclosureIndicator
                cell.textLabel?.text = "Downloads"


            default:
                break
            }
        }

        if (indexPath as NSIndexPath).section == 1 {  //Recently Added
            let cell = tableView.dequeueReusableCell(withIdentifier: "AlbumCell", for: indexPath)
            cell.selectionStyle = .none
            return cell
        }

        return tableView.dequeueReusableCell(withIdentifier: "Default", for: indexPath)
    }
}

我猜我迷失的地方在于我不需要向上滑动。我知道当我3D触摸文本线程并向上滑动时,会出现回复选项。但是当我3D触摸相册时,它只是播放上面显示的屏幕动画。没有向上滑动或其他任何操作。 - Austin E

1
这实际上可以使用UIPreviewInteraction API来完成。

https://developer.apple.com/documentation/uikit/uipreviewinteraction

这与Peek and Pop API非常相似。这里有两个阶段:预览和提交,它们对应于后来API中的Peek和Pop。我们有UIPreviewInteractionDelegate,它使我们可以通过这些阶段进行转换。
所以,要复制上述的Apple Music弹出窗口, 实际上,苹果已经通过聊天应用程序的形式构建了这个演示。
这里下载示例代码并测试它。

0

我写了一些代码来复制像苹果音乐风格的 Peek and Pop。

工作方式如下

enter image description here

说明

  • TopView.xib,TopView.swift(您可以自定义它)
  • PeekAndPopActionView.swift(用于单个操作的视图,例如下载、分享等)
  • PeekAndPopController.swift(呈现、关闭视图)
  • ForceTouchGestureRecognizer.swift(检测力触摸)

用法

fileprivate let peekedViewController = PeekAndPopController()

@IBAction func presentAction(_ sender: Any) {
    present(peekedViewController, animated: true)
}

let forceTouch = ForceTouchGestureRecognizer()

override func viewDidLoad() {
    super.viewDidLoad()

    forceTouch.addTarget(self, action: #selector(touchAction(_:)))
    forceTouch.cancelsTouchesInView = false
    view.addGestureRecognizer(forceTouch)

    let download = PeekAndPopActionView(text: "Download", image: #imageLiteral(resourceName: "btnDownload"), handler: {
        print("Download Action")
    })

    let playNext = PeekAndPopActionView(text: "Play Next", image: #imageLiteral(resourceName: "btnDownload"), handler: {
        print("Play Next Action")
    })

    let playLast = PeekAndPopActionView(text: "Play Later", image: #imageLiteral(resourceName: "btnDownload"), handler: {
        print("Play Last Action")
    })

    let share = PeekAndPopActionView(text: "Share", image: #imageLiteral(resourceName: "btnDownload"), handler: {
        print("Share Action")
    })

    peekedViewController.addAction(download)
    peekedViewController.addAction(playNext)
    peekedViewController.addAction(playLast)
    peekedViewController.addAction(share)
    peekedViewController.topView = TopView().loadNib()

    peekedViewController.topView?.handler = {
        print("Play Play Play")
    }
}

@objc func touchAction(_ gesture: ForceTouchGestureRecognizer) {
    print(#function, gesture.touch?.location(in: view) ?? "")
    present(peekedViewController, animated: true)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接