Skip to content

Latest commit

 

History

History
390 lines (296 loc) · 11.3 KB

README.md

File metadata and controls

390 lines (296 loc) · 11.3 KB

Bottom Menu Popup

So the interesting thing about the way this is built is that the pop-up draw on the bottom is just a full screen take over with a large spacer constraint limitting everything to half screen.

The main view controller contains a custom AccountView which is tappable. When tapped it asks the main view controller to present another view controller with a specific height spacer on top of itself.

extension ViewController: AccountViewDelegate {
    func didTap(_ sender: AccountView) {
        let height = UIScreen.main.bounds.height * 0.5
        let vc = AccountSelectorViewController(height: height)
        vc.modalPresentationStyle = .overCurrentContext
        vc.delegate = self
        
        navigationController?.present(vc, animated: true, completion: nil)
    }
}

This newly presented AccountSelectorViewController has a back splash over lay set to an alpha=0.3 for that nice dark effect.

Then the height spacer kicks in pushing the top of the stack and all it's contents down half-way. That's how we get the drawer effect.

Inside the stack we have a custom header view, a table view, which when tapped, passes back to the main view controller the selected account.

All in all a pretty cool little way to get a poppable drawer 🚀.

Source

You can find a demo in this directory. Or view the source code below.

ViewController.swift

//
//  ViewController.swift
//  PopupFun
//
//  Created by jrasmusson on 2021-03-09.
//

import UIKit

class ViewController: UIViewController {
    
    let accountView = AccountView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        style()
        setup()
        layout()
    }
}

extension ViewController {
    
    func style() {
        accountView.translatesAutoresizingMaskIntoConstraints = false
    }
    
    func setup() {
        accountView.delegate = self
    }
    
    func layout() {
        view.addSubview(accountView)
        
        NSLayoutConstraint.activate([
            accountView.topAnchor.constraint(equalToSystemSpacingBelow: view.safeAreaLayoutGuide.topAnchor, multiplier: 3),
            accountView.leadingAnchor.constraint(equalToSystemSpacingAfter: view.leadingAnchor, multiplier: 3),
            view.trailingAnchor.constraint(equalToSystemSpacingAfter: accountView.trailingAnchor, multiplier: 3),
        ])
    }
}

// MARK: - AccountViewDelegate

extension ViewController: AccountViewDelegate {
    func didTap(_ sender: AccountView) {
        let height = UIScreen.main.bounds.height * 0.5
        let vc = AccountSelectorViewController(height: height)
        vc.modalPresentationStyle = .overCurrentContext
        vc.delegate = self
        
        navigationController?.present(vc, animated: true, completion: nil)
    }
}

// MARK: - AccountSelectorDelegate

extension ViewController: AccountSelectorDelegate {
    func didTap(_ account: String) {
        accountView.contentLabel.text = account
    }
}

AccountView.swift

//
//  AccountView.swift
//  PopupFun
//
//  Created by jrasmusson on 2021-03-09.
//

import Foundation
import UIKit

protocol AccountViewDelegate: AnyObject {
    func didTap(_ sender: AccountView)
}

class AccountView: UIView {
    
    let imageView = UIImageView()
    let titleLabel = UILabel()
    let contentLabel = UILabel()
    let dividerView = UIView()
    
    weak var delegate: AccountViewDelegate?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        style()
        setup()
        layout()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override var intrinsicContentSize: CGSize {
        return CGSize(width: 200, height: 32)
    }
}

extension AccountView {
    
    func style() {
        translatesAutoresizingMaskIntoConstraints = false
        
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.image = UIImage(systemName: "dollarsign.circle")
        
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.text = "To"
        
        contentLabel.translatesAutoresizingMaskIntoConstraints = false
        
        dividerView.translatesAutoresizingMaskIntoConstraints = false
        dividerView.backgroundColor = .black
    }
    
    func setup() {
        let singleTap = UITapGestureRecognizer(target: self, action: #selector(tapped(_: )))
        addGestureRecognizer(singleTap)
    }
    
    @objc func tapped(_ recognizer: UITapGestureRecognizer) {
        delegate?.didTap(self)
    }
    
    func layout() {
        addSubview(imageView)
        addSubview(titleLabel)
        addSubview(contentLabel)
        addSubview(dividerView)
        
        NSLayoutConstraint.activate([
            imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
            imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
            titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
            titleLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: imageView.trailingAnchor, multiplier: 2),
            contentLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
            contentLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: titleLabel.trailingAnchor, multiplier: 2),
            trailingAnchor.constraint(equalToSystemSpacingAfter: contentLabel.trailingAnchor, multiplier: 2),
            dividerView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
            dividerView.trailingAnchor.constraint(equalTo: contentLabel.trailingAnchor),
            dividerView.bottomAnchor.constraint(equalTo: bottomAnchor),
            dividerView.heightAnchor.constraint(equalToConstant: 1)
        ])
        
        imageView.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal)
        titleLabel.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal)
    }
}

AccountSelectorViewController.swift

//
//  AccountSelectorViewController.swift
//  PopupFun
//
//  Created by jrasmusson on 2021-03-09.
//

import UIKit

protocol AccountSelectorDelegate: AnyObject {
    func didTap(_ account: String)
}

class AccountSelectorViewController: UIViewController {
        
    let backgroundOverlayView = UIView()
    let stackView = UIStackView()
    let headerView = AccountSelectorHeaderView()
    let topSpacerHeight: CGFloat
    
    weak var delegate: AccountSelectorDelegate?
    
    // table view
    let accounts = [
        "Checking",
        "Savings",
        "Expenses",
        "Investing",
        "Retirement",
        "Travel"
    ]
    
    let cellId = "cellId"
    var tableView = UITableView()
    
    init(height: CGFloat) {
        self.topSpacerHeight = height
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        style()
        setup()
        layout()
    }
}

extension AccountSelectorViewController {
    
    func style() {
        backgroundOverlayView.translatesAutoresizingMaskIntoConstraints = false
        backgroundOverlayView.backgroundColor = .black
        backgroundOverlayView.alpha = 0.3
        
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        
        headerView.translatesAutoresizingMaskIntoConstraints = false
        tableView.translatesAutoresizingMaskIntoConstraints = false
    }

    func setup() {
        let tap = UITapGestureRecognizer(target: self, action: #selector(tappedOverlay))
        backgroundOverlayView.addGestureRecognizer(tap)
        
        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
        tableView.tableFooterView = UIView()
    }
    
    func layout() {
        stackView.addArrangedSubview(headerView)
        stackView.addArrangedSubview(tableView)
        
        view.addSubview(backgroundOverlayView)
        view.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            backgroundOverlayView.topAnchor.constraint(equalTo: view.topAnchor),
            backgroundOverlayView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            backgroundOverlayView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            backgroundOverlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            
            stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: topSpacerHeight),
            stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }
}

extension AccountSelectorViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        delegate?.didTap(accounts[indexPath.row])
        dismiss(animated: true, completion: nil)
    }
}

extension AccountSelectorViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)

        cell.textLabel?.text = accounts[indexPath.row]
        cell.accessoryType = UITableViewCell.AccessoryType.disclosureIndicator

        return cell
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return accounts.count
    }
}

// MARK: - Actions

extension AccountSelectorViewController {
    @objc func tappedOverlay() {
        dismiss(animated: true, completion: nil)
    }
}

AccountSelectorHeaderView.swift

//
//  AccountSelectorHeaderView.swift
//  PopupFun
//
//  Created by jrasmusson on 2021-03-10.
//

import Foundation
import UIKit

class AccountSelectorHeaderView: UIView {
    
    let headerLabel = UILabel()
    let dividerView = UIView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        style()
        layout()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override var intrinsicContentSize: CGSize {
        return CGSize(width: 200, height: 44)
    }
}

extension AccountSelectorHeaderView {
    
    func style() {
        translatesAutoresizingMaskIntoConstraints = false
        backgroundColor = .white
        
        headerLabel.translatesAutoresizingMaskIntoConstraints = false
        headerLabel.text = "To Account"
        
        dividerView.translatesAutoresizingMaskIntoConstraints = false
        dividerView.backgroundColor = .systemGray
    }
    
    func layout() {
        addSubview(headerLabel)
        addSubview(dividerView)
        
        NSLayoutConstraint.activate([
            headerLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
            headerLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
            dividerView.bottomAnchor.constraint(equalTo: bottomAnchor),
            dividerView.widthAnchor.constraint(equalTo: widthAnchor),
            dividerView.heightAnchor.constraint(equalToConstant: 1)
        ])
    }
}