mirror of
https://github.com/material-components/material-components-ios.git
synced 2026-02-20 08:27:32 +08:00
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
365 lines
13 KiB
Swift
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)
|
|
}
|
|
}
|