blob: 882c50447a907c204a7e69c914b83ca91ab5db43 [file] [log] [blame] [edit]
// Copyright 2016-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.
// swiftlint:disable file_length
// swiftlint:disable line_length
// swiftlint:disable type_body_length
// swiftlint:disable function_body_length
import UIKit
import MaterialComponents.MaterialAppBar
import MaterialComponents.MaterialButtons
import MaterialComponents.MaterialTextFields
extension TextFieldKitchenSinkSwiftExample {
func setupExampleViews() {
view.backgroundColor = UIColor(white:0.97, alpha: 1.0)
title = "Material Text Fields"
let textFieldControllersFullWidth = setupFullWidthTextFields()
allTextFieldControllers = [setupFilledTextFields(), setupInlineUnderlineTextFields(),
textFieldControllersFullWidth,
setupFloatingUnderlineTextFields(),
setupSpecialTextFields()].flatMap { $0 as! [MDCTextInputController] }
let multilineTextFieldControllersFullWidth = setupFullWidthMultilineTextFields()
allMultilineTextFieldControllers = [setupAreaTextFields(), setupUnderlineMultilineTextFields(),
multilineTextFieldControllersFullWidth,
setupFloatingMultilineTextFields(),
setupSpecialMultilineTextFields()].flatMap { $0 }
controllersFullWidth = textFieldControllersFullWidth + multilineTextFieldControllersFullWidth
allInputControllers = allTextFieldControllers + allMultilineTextFieldControllers
setupScrollView()
NotificationCenter.default.addObserver(self,
selector: #selector(TextFieldKitchenSinkSwiftExample.contentSizeCategoryDidChange(notif:)),
name: UIContentSizeCategory.didChangeNotification,
object: nil)
}
func setupButton() -> MDCButton {
let button = MDCButton()
button.setTitleColor(.white, for: .normal)
button.mdc_adjustsFontForContentSizeCategory = true
button.translatesAutoresizingMaskIntoConstraints = false
return button
}
func setupControls() -> [UIView] {
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(container)
container.addSubview(errorLabel)
let errorSwitch = UISwitch()
errorSwitch.translatesAutoresizingMaskIntoConstraints = false
errorSwitch.addTarget(self,
action: #selector(TextFieldKitchenSinkSwiftExample.errorSwitchDidChange(errorSwitch:)),
for: .touchUpInside)
container.addSubview(errorSwitch)
errorSwitch.accessibilityLabel = "Show errors"
container.addSubview(helperLabel)
let helperSwitch = UISwitch()
helperSwitch.translatesAutoresizingMaskIntoConstraints = false
helperSwitch.addTarget(self,
action: #selector(TextFieldKitchenSinkSwiftExample.helperSwitchDidChange(helperSwitch:)),
for: .touchUpInside)
container.addSubview(helperSwitch)
helperSwitch.accessibilityLabel = "Helper text"
let views = ["errorLabel": errorLabel, "errorSwitch": errorSwitch,
"helperLabel": helperLabel, "helperSwitch": helperSwitch]
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat:
"H:|-[errorLabel]-[errorSwitch]|", options: [.alignAllCenterY], metrics: nil, views: views))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat:
"H:|-[helperLabel]-[helperSwitch]|", options: [.alignAllCenterY], metrics: nil, views: views))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat:
"V:|-[errorSwitch]-[helperSwitch]|", options: [], metrics: nil, views: views))
textInsetsModeButton.addTarget(self,
action: #selector(textInsetsModeButtonDidTouch(button:)),
for: .touchUpInside)
textInsetsModeButton.setTitle("Text Insets Mode: If Content", for: .normal)
scrollView.addSubview(textInsetsModeButton)
characterModeButton.addTarget(self,
action: #selector(textFieldModeButtonDidTouch(button:)),
for: .touchUpInside)
characterModeButton.setTitle("Character Count Mode: Always", for: .normal)
scrollView.addSubview(characterModeButton)
clearModeButton.addTarget(self,
action: #selector(textFieldModeButtonDidTouch(button:)),
for: .touchUpInside)
clearModeButton.setTitle("Clear Button Mode: While Editing", for: .normal)
scrollView.addSubview(clearModeButton)
underlineButton.addTarget(self,
action: #selector(textFieldModeButtonDidTouch(button:)),
for: .touchUpInside)
underlineButton.setTitle("Underline Mode: While Editing", for: .normal)
scrollView.addSubview(underlineButton)
return [container, textInsetsModeButton, characterModeButton, underlineButton, clearModeButton]
}
func setupSectionLabels() {
scrollView.addSubview(controlLabel)
scrollView.addSubview(singleLabel)
scrollView.addSubview(multiLabel)
NSLayoutConstraint(item: singleLabel,
attribute: .leading,
relatedBy: .equal,
toItem: view,
attribute: .leadingMargin,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint(item: singleLabel,
attribute: .trailing,
relatedBy: .equal,
toItem: view,
attribute: .trailingMargin,
multiplier: 1,
constant: 0).isActive = true
}
func setupScrollView() {
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(
withVisualFormat: "V:|[topGuide]-[scrollView]|",
options: [],
metrics: nil,
views: ["scrollView": scrollView, "topGuide": topLayoutGuide]))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|[scrollView]|",
options: [],
metrics: nil,
views: ["scrollView": scrollView]))
let marginOffset: CGFloat = 16
let margins = UIEdgeInsets(top: 0, left: marginOffset, bottom: 0, right: marginOffset)
scrollView.layoutMargins = margins
setupSectionLabels()
let prefix = "view"
let concatenatingClosure = {
(accumulator, object: AnyObject) in
accumulator + "-[" + self.unique(from: object, with: prefix) +
"]"
}
let allControls = setupControls()
let controlsString = allControls.reduce("", concatenatingClosure)
var controls = [String: UIView]()
allControls.forEach { control in
controls[unique(from: control, with: prefix)] =
control
}
let allTextFields = allTextFieldControllers.compactMap { $0.textInput }
let textFieldsString = allTextFields.reduce("", concatenatingClosure)
var textFields = [String: UIView]()
allTextFields.forEach { textInput in
textFields[unique(from: textInput, with: prefix)] = textInput
}
let allTextViews = allMultilineTextFieldControllers.compactMap { $0.textInput }
let textViewsString = allTextViews.reduce("", concatenatingClosure)
var textViews = [String: UIView]()
allTextViews.forEach { input in
textViews[unique(from: input, with: prefix)] = input
}
let visualString = "V:[singleLabel]" +
textFieldsString + "[unstyledTextField]-20-[multiLabel]" + textViewsString +
"[unstyledTextView]-10-[controlLabel]" + controlsString
let labels: [String: UIView] = ["controlLabel": controlLabel,
"singleLabel": singleLabel,
"multiLabel": multiLabel,
"unstyledTextField": unstyledTextField,
"unstyledTextView": unstyledMultilineTextField]
var views = [String: UIView]()
let dictionaries = [labels, textFields, controls, textViews]
dictionaries.forEach { dictionary in
dictionary.forEach { (key, value) in
views[key] = value
// We have a scrollview and we're adding some elements that are subclassed from scrollviews.
// So constraints need to be in relation to something that doesn't have a content size.
// We'll use the view controller's view.
let leading = NSLayoutConstraint(item: value,
attribute: .leading,
relatedBy: .equal,
toItem: view,
attribute: .leadingMargin,
multiplier: 1.0,
constant: 0.0)
leading.priority = UILayoutPriority.defaultHigh
leading.isActive = true
let trailing = NSLayoutConstraint(item: value,
attribute: .trailing,
relatedBy: .equal,
toItem: view,
attribute: .trailing,
multiplier: 1.0,
constant: 0.0)
trailing.priority = UILayoutPriority.defaultHigh
trailing.isActive = true
}
}
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: visualString,
options: [.alignAllCenterX],
metrics: nil,
views: views))
controllersFullWidth.compactMap { $0.textInput }.forEach { textInput in
NSLayoutConstraint(item: textInput,
attribute: .leading,
relatedBy: .equal,
toItem: view,
attribute: .leading,
multiplier: 1.0,
constant: 0).isActive = true
NSLayoutConstraint(item: textInput,
attribute: .trailing,
relatedBy: .equal,
toItem: view,
attribute: .trailing,
multiplier: 1.0,
constant: 0).isActive = true
// This constraint is necessary for the scrollview to have a content width.
NSLayoutConstraint(item: textInput,
attribute: .trailing,
relatedBy: .equal,
toItem: scrollView,
attribute: .trailing,
multiplier: 1.0,
constant: 0).isActive = true
}
// These used to be done in the visual format string but iOS 11 changed that.
if #available(iOS 11.0, *) {
NSLayoutConstraint(item: singleLabel,
attribute: .topMargin,
relatedBy: .equal,
toItem: scrollView.contentLayoutGuide,
attribute: .top,
multiplier: 1.0,
constant: 20).isActive = true
NSLayoutConstraint(item: allControls.last as Any,
attribute: .bottom,
relatedBy: .equal,
toItem: scrollView.contentLayoutGuide,
attribute: .bottomMargin,
multiplier: 1.0,
constant: -20).isActive = true
} else {
NSLayoutConstraint(item: singleLabel,
attribute: .topMargin,
relatedBy: .equal,
toItem: scrollView,
attribute: .top,
multiplier: 1.0,
constant: 20).isActive = true
NSLayoutConstraint(item: allControls.last as Any,
attribute: .bottom,
relatedBy: .equal,
toItem: scrollView,
attribute: .bottomMargin,
multiplier: 1.0,
constant: -20).isActive = true
}
registerKeyboardNotifications()
addGestureRecognizer()
}
func addGestureRecognizer() {
let tapRecognizer = UITapGestureRecognizer(target: self,
action: #selector(tapDidTouch(sender: )))
self.scrollView.addGestureRecognizer(tapRecognizer)
}
func registerKeyboardNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(
self,
selector: #selector(TextFieldKitchenSinkSwiftExample.keyboardWillShow(notif:)),
name: UIResponder.keyboardWillShowNotification,
object: nil)
notificationCenter.addObserver(
self,
selector: #selector(TextFieldKitchenSinkSwiftExample.keyboardWillShow(notif:)),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil)
notificationCenter.addObserver(
self,
selector: #selector(TextFieldKitchenSinkSwiftExample.keyboardWillHide(notif:)),
name: UIResponder.keyboardWillHideNotification,
object: nil)
}
@objc func keyboardWillShow(notif: Notification) {
guard let frame = notif.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
return
}
scrollView.contentInset = UIEdgeInsets(top: 0.0,
left: 0.0,
bottom: frame.height,
right: 0.0)
}
@objc func keyboardWillHide(notif: Notification) {
scrollView.contentInset = UIEdgeInsets()
}
func unique(from input: AnyObject, with prefix: String) -> String {
return prefix + String(describing: Unmanaged.passUnretained(input).toOpaque())
}
}
extension TextFieldKitchenSinkSwiftExample {
// 3 of the 'mode' buttons all are similar. The following code is shared by them
@objc func textFieldModeButtonDidTouch(button: MDCButton) {
var controllersToChange = allInputControllers
var partialTitle = ""
if button == characterModeButton {
partialTitle = "Character Count Mode"
controllersToChange = controllersWithCharacterCount
} else if button == clearModeButton {
partialTitle = "Clear Button Mode"
controllersToChange = allTextFieldControllers
} else {
partialTitle = "Underline View Mode"
}
let closure: (UITextField.ViewMode, String) -> Void = { mode, title in
controllersToChange.forEach { controller in
if button == self.characterModeButton {
controller.characterCountViewMode = mode
} else if button == self.clearModeButton {
if let input = controller.textInput as? MDCTextField {
input.clearButtonMode = mode
}
} else {
controller.underlineViewMode = mode
}
button.setTitle(title + ": " + self.modeName(mode: mode), for: .normal)
}
}
let alert = UIAlertController(title: partialTitle,
message: nil,
preferredStyle: .alert)
presentAlert(alert: alert, partialTitle: partialTitle, closure: closure)
}
func presentAlert (alert: UIAlertController,
partialTitle: String,
closure: @escaping (_ mode: UITextField.ViewMode, _ title: String) -> Void) -> Void {
for rawMode in 0...3 {
let mode = UITextField.ViewMode(rawValue: rawMode)!
alert.addAction(UIAlertAction(title: modeName(mode: mode),
style: .default,
handler: { _ in
closure(mode, partialTitle)
}))
}
present(alert, animated: true, completion: nil)
}
func modeName(mode: UITextField.ViewMode) -> String {
switch mode {
case .always:
return "Always"
case .whileEditing:
return "While Editing"
case .unlessEditing:
return "Unless Editing"
case .never:
return "Never"
}
}
}
extension TextFieldKitchenSinkSwiftExample {
// The 'text insets' button does not have the same options as the other mode buttons
@objc func textInsetsModeButtonDidTouch(button: MDCButton) {
let closure: (MDCTextInputTextInsetsMode, String) -> Void = { mode, title in
self.allInputControllers.forEach { controller in
guard let input = controller.textInput else {
return
}
input.textInsetsMode = mode
button.setTitle(title + ": " + self.textInsetsModeName(mode: mode), for: .normal)
}
}
let title = "Text Insets Mode"
let alert = UIAlertController(title: title,
message: nil,
preferredStyle: .alert)
for rawMode: UInt in 0...2 {
let mode = MDCTextInputTextInsetsMode(rawValue: rawMode)!
alert.addAction(UIAlertAction(title: textInsetsModeName(mode: mode),
style: .default,
handler: { _ in
closure(mode, title)
}))
}
present(alert, animated: true, completion: nil)
}
func textInsetsModeName(mode: MDCTextInputTextInsetsMode) -> String {
switch mode {
case .always:
return "Always"
case .ifContent:
return "If Content"
case .never:
return "Never"
}
}
}
extension TextFieldKitchenSinkSwiftExample {
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
}
extension TextFieldKitchenSinkSwiftExample {
@objc class func catalogMetadata() -> [String: Any] {
return [
"breadcrumbs": ["Text Field", "Kitchen Sink"],
"primaryDemo": false,
"presentable": false,
]
}
}