Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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 does not go away when you raise you finger. How do I replicate this?

enter image description here

like image 281
Austin E Avatar asked Jul 17 '16 00:07

Austin E


2 Answers

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.

like image 22
Varun Avatar answered Nov 10 '22 19:11

Varun


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

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


}

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)
    }
}
like image 199
Brandon Avatar answered Nov 10 '22 19:11

Brandon