How do I replicate iOS 10's Apple Music “Peek and pop action menu”

后端 未结 3 1371
既然无缘
既然无缘 2020-12-29 12:05

iOS 10 has a feature I would like to replicate. When you 3D touch an album in the Apple Music app it opens the menu shown below. However unlike a normal peek and pop, it doe

相关标签:
3条回答
  • 2020-12-29 12:48

    This actually might be done using UIPreviewInteraction API.

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

    It is almost similar to the Peek and Pop API.

    Here we have 2 phases: Preview and Commit which are corresponding to the Peek and Pop in the later API. we have UIPreviewInteractionDelegate which gives us the access to the transition through these phases.

    So what one should do is, to replicate the above Apple Music Popup,

    • Manually show a blur overlay during didUpdatePreviewTransition

    • Build an xib of the above menu and show it during didUpdateCommitTransition

    • You can make the view stay there on commitTransition phase end.

    Actually, apple has built a demo of this in the form of a Chat App.

    Download the sample code from here and test it out.

    0 讨论(0)
  • 2020-12-29 12:50

    I wrote some code to replicate like apple music style peek and pop.

    Work like below

    Explanation

    • TopView.xib, TopView.swift (You can customize it)
    • PeekAndPopActionView.swift (View for single action, such as download, share ..)
    • PeekAndPopController.swift (Present, Dismiss the view)
    • ForceTouchGestureRecognizer.swift (Detect Force Touch)

    Usage

    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)
    }
    
    0 讨论(0)
  • 2020-12-29 12:58

    The closest I got to replicating it is the following code.. It create a dummy-replica of the Music application.. Then I added the PeekPop-3D-Touch delegates.

    However, in the delegate, I add an observer to the gesture recognizer and then cancel the gesture upon peeking but then re-enable it when the finger is lifted. To re-enable it, I did it async because the preview will disappear immediately without the async dispatch. I couldn't find a way around it..

    Now if you tap outside the blue box, it will disappear like normal =]

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

    //
    //  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];
        }
    
    
    }
    

    And the implementation of ForceTouch pausing..

    //
    //  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)
        }
    }
    
    0 讨论(0)
提交回复
热议问题