material-components_materia.../components/Dialogs/examples/DialogsAccessoryExampleViewController.swift
Galia Kaufman 9d9bd93847 [Dialogs] Adding accessoryViewHorizontalInset API
The API allows setting horizontal insets for the accessory view that are different from the message. This is most often used for dialogs that have both a message and an accessory view, as demonstrated in the attached example.

Additionally, this example demonstrates how to add an horizontal hairline as shown on the material.io spec for Confirmation Dialogs.

Note:
This new API is needed because clients do not always have control over the view that is being used as an accessory view, or that the view is used in multiple areas in the app, and cannot be customized.
Additionally, there's no way currently to set 0 insets for the accessory view, while still keeping the 24 value insets for the message, which is a relatively common scenario.
PiperOrigin-RevId: 334142026
2020-09-28 06:12:48 -07:00

365 lines
13 KiB
Swift

// Copyright 2019-present the Material Components for iOS authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import UIKit
import MaterialComponents.MaterialCollections
import MaterialComponents.MaterialDialogs
import MaterialComponents.MaterialDialogs_Theming
import MaterialComponents.MaterialTextControls_FilledTextFields
import MaterialComponents.MaterialTextControls_FilledTextFieldsTheming
import MaterialComponents.MaterialContainerScheme
import MaterialComponents.MaterialTypographyScheme
class DialogsAccessoryExampleViewController: MDCCollectionViewController {
@objc lazy var containerScheme: MDCContainerScheming = {
let scheme = MDCContainerScheme()
scheme.colorScheme = MDCSemanticColorScheme(defaults: .material201907)
scheme.typographyScheme = MDCTypographyScheme(defaults: .material201902)
return scheme
}()
let kReusableIdentifierItem = "customCell"
var menu: [String] = []
var handler: MDCActionHandler = { action in
print(action.title ?? "Some Action")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = containerScheme.colorScheme.backgroundColor
loadCollectionView(menu: [
"Material Filled Text Field",
"UI Text Field",
"Confirmation Dialog",
"Autolayout in Custom View",
])
}
func loadCollectionView(menu: [String]) {
self.collectionView?.register(
MDCCollectionViewTextCell.self, forCellWithReuseIdentifier: kReusableIdentifierItem)
self.menu = menu
}
override func collectionView(
_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath
) {
guard let alert = performActionFor(row: indexPath.row) else { return }
self.present(alert, animated: true, completion: nil)
}
private func performActionFor(row: Int) -> MDCAlertController? {
switch row {
case 0:
return performMDCTextField()
case 1:
return performUITextField()
case 2:
return performConfirmationDialog()
case 3:
return performCustomLabelWithButton()
default:
print("No row is selected")
return nil
}
}
// Demonstrate a custom view with MDCFilledTextField being assigned to the accessoryView API.
// This example also demonstrates the use of autolayout in custom views.
func performMDCTextField() -> MDCAlertController {
let alert = MDCAlertController(title: "Rename File", message: nil)
alert.addAction(MDCAlertAction(title: "Rename", emphasis: .medium, handler: handler))
alert.addAction(MDCAlertAction(title: "Cancel", emphasis: .low, handler: handler))
if let alertView = alert.view as? MDCAlertControllerView {
alertView.contentInsets.bottom = 16.0
}
let view = UIView(frame: CGRect.zero)
let label = newLabel(text: "OLD_FILE.PNG will be renamed:")
let namefield = MDCFilledTextField()
namefield.label.text = "New File Name"
namefield.placeholder = "Enter a new file name"
namefield.labelBehavior = MDCTextControlLabelBehavior.floats
namefield.clearButtonMode = UITextField.ViewMode.whileEditing
namefield.leadingAssistiveLabel.text = "An optional assistive message"
namefield.applyTheme(withScheme: containerScheme)
// Enable dynamic type.
namefield.adjustsFontForContentSizeCategory = true
namefield.font = UIFont.preferredFont(
forTextStyle: .body, compatibleWith: namefield.traitCollection)
namefield.leadingAssistiveLabel.font = UIFont.preferredFont(
forTextStyle: .caption2, compatibleWith: namefield.traitCollection)
label.translatesAutoresizingMaskIntoConstraints = false
namefield.translatesAutoresizingMaskIntoConstraints = false
view.translatesAutoresizingMaskIntoConstraints = true
view.addSubview(label)
view.addSubview(namefield)
label.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: namefield.topAnchor, constant: -10).isActive = true
namefield.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
namefield.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
namefield.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
alert.accessoryView = view
alert.mdc_adjustsFontForContentSizeCategory = true // Enable dynamic type.
alert.applyTheme(withScheme: self.containerScheme)
return alert
}
func performUITextField() -> MDCAlertController {
let alert = MDCAlertController(title: "This is a title", message: "This is a message")
let textField = UITextField()
textField.placeholder = "This is a text field"
alert.accessoryView = textField
alert.addAction(MDCAlertAction(title: "Dismiss", emphasis: .medium, handler: handler))
alert.mdc_adjustsFontForContentSizeCategory = true // Enable dynamic type.
alert.applyTheme(withScheme: self.containerScheme)
return alert
}
// Demonstrate a confirmation dialog with a custom table view.
func performConfirmationDialog() -> MDCAlertController {
let alert = MDCAlertController(title: "Phone ringtone", message: "Please select a ringtone:")
alert.addAction(MDCAlertAction(title: "OK", handler: handler))
alert.addAction(MDCAlertAction(title: "Cancel", handler: handler))
alert.accessoryView = ExampleTableSeparatorView()
if let alertView = alert.view as? MDCAlertControllerView {
// Zero bottom-inset ensuring the bottom separator appears immediately above the actions.
alertView.contentInsets.bottom = 0
// Decreasing vertical margin between the accessory view and the message
alertView.accessoryViewVerticalInset = 8
// Aligning the accessory view with the dialog's edge by removing all horizontal insets.
alertView.accessoryViewHorizontalInset = -alertView.contentInsets.left
}
alert.mdc_adjustsFontForContentSizeCategory = true // Enable dynamic type.
alert.applyTheme(withScheme: self.containerScheme)
return alert
}
// Demonstrate a custom accessory view with auto layout, presenting a label and a button.
func performCustomLabelWithButton() -> MDCAlertController {
let alert = MDCAlertController(title: "Title", message: nil)
alert.addAction(MDCAlertAction(title: "Dismiss", emphasis: .medium, handler: handler))
let view = UIView(frame: CGRect.zero)
let label = newLabel(text: "Your storage is full. Your storage is full.")
let button = MDCButton()
button.setTitle("Learn More", for: UIControl.State.normal)
button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 0, bottom: 0, right: 8)
button.applyTextTheme(withScheme: containerScheme)
label.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
view.translatesAutoresizingMaskIntoConstraints = true
view.addSubview(label)
view.addSubview(button)
label.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: button.topAnchor).isActive = true
button.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
button.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor).isActive = true
button.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
alert.accessoryView = view
alert.mdc_adjustsFontForContentSizeCategory = true // Enable dynamic type.
alert.applyTheme(withScheme: self.containerScheme)
return alert
}
func newLabel(text: String) -> UILabel {
let label = UILabel()
label.textColor = containerScheme.colorScheme.onSurfaceColor
label.font = containerScheme.typographyScheme.subtitle2
label.text = text
label.numberOfLines = 0
return label
}
}
// MDCCollectionViewController Data Source
extension DialogsAccessoryExampleViewController {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(
_ collectionView: UICollectionView, numberOfItemsInSection section: Int
) -> Int {
return menu.count
}
override func collectionView(
_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: kReusableIdentifierItem,
for: indexPath)
guard let customCell = cell as? MDCCollectionViewTextCell else { return cell }
customCell.isAccessibilityElement = true
customCell.accessibilityTraits = .button
let cellTitle = menu[indexPath.row]
customCell.accessibilityLabel = cellTitle
customCell.textLabel?.text = cellTitle
return customCell
}
}
// MARK: Catalog by convention
extension DialogsAccessoryExampleViewController {
@objc class func catalogMetadata() -> [String: Any] {
return [
"breadcrumbs": ["Dialogs", "Dialog With Accessory View"],
"primaryDemo": false,
"presentable": true,
]
}
}
// MARK: Snapshot Testing by Convention
extension DialogsAccessoryExampleViewController {
func resetTests() {
if presentedViewController != nil {
dismiss(animated: false)
}
}
@objc func testTextField() {
resetTests()
self.present(performUITextField(), animated: false, completion: nil)
}
@objc func testMDCTextField() {
resetTests()
self.present(performMDCTextField(), animated: false, completion: nil)
}
@objc func testCustomLabelWithButton() {
resetTests()
self.present(performCustomLabelWithButton(), animated: false, completion: nil)
}
@objc func testConfirmationDialog() {
resetTests()
self.present(performConfirmationDialog(), animated: false, completion: nil)
}
}
// An example view with a tableview and a bottom separator.
class ExampleTableSeparatorView: UIView, UITableViewDataSource {
let ringtones = ["Callisto", "Luna", "Phobos", "Dione"]
let tableView: UITableView = {
let tv = AutoSizedTableView()
tv.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tv.separatorStyle = .none
tv.rowHeight = 40
return tv
}()
init() {
super.init(frame: .zero)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
tableView.dataSource = self
tableView.alwaysBounceVertical = false
addSubview(tableView)
let separator = UIView(frame: .zero)
separator.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
addSubview(separator)
tableView.translatesAutoresizingMaskIntoConstraints = false
separator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(lessThanOrEqualTo: self.topAnchor),
tableView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
separator.topAnchor.constraint(equalTo: tableView.bottomAnchor),
separator.leadingAnchor.constraint(equalTo: self.leadingAnchor),
separator.trailingAnchor.constraint(equalTo: self.trailingAnchor),
separator.bottomAnchor.constraint(equalTo: self.bottomAnchor),
separator.heightAnchor.constraint(equalToConstant: 2),
])
tableView.reloadData()
let currentRingtone = IndexPath(row: 1, section: 0)
tableView.selectRow(at: currentRingtone, animated: false, scrollPosition: .top)
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return ringtones.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = ringtones[indexPath.row]
cell.indentationLevel = 1
return cell
}
}
// A tableview with intrinsic size that matches its content size.
final class AutoSizedTableView: UITableView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}