diff --git a/components/Dialogs/examples/DialogsTitleImageExampleViewController.swift b/components/Dialogs/examples/DialogsTitleImageExampleViewController.swift new file mode 100644 index 00000000000..2da49caad42 --- /dev/null +++ b/components/Dialogs/examples/DialogsTitleImageExampleViewController.swift @@ -0,0 +1,301 @@ +// Copyright 2020-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 DialogsTitleImageExampleViewController: 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: [ + "Title Icon", + "Custom Title Icon", + "Title Image - Scaled Down to Fit", + "Title Image - Bleeding Edge", + "Custom Title 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 = alertController(for: indexPath.row) else { return } + self.present(alert, animated: true, completion: nil) + } + + private func alertController(for row: Int) -> MDCAlertController? { + switch row { + case 0: + return alertWithTitleIcon() + case 1: + return alertWithCustomTitleIcon() + case 2: + return alertWithScaledToFitImage() + case 3: + return alertWithScaledBleedingImage() + case 4: + presentAlertWithCustomTitleView() + return nil + default: + print("No row is selected") + return nil + } + } + + func alertWithTitleIcon() -> MDCAlertController { + let alert = createMDCAlertController(title: "Title Icon") + alert.titleIcon = image(named: "outline_lock_black_24pt") + alert.applyTheme(withScheme: self.containerScheme) + return alert + } + + func alertWithCustomTitleIcon() -> MDCAlertController { + let alert = createMDCAlertController(title: "Custom Title Icon") + // Custom size. + alert.titleIcon = image(named: "baseline_alarm_on_black_48pt") + // Custom alignment. + alert.titleIconAlignment = .center + // Custom insets. + if let alertView = alert.view as? MDCAlertControllerView { + alertView.titleIconInsets.bottom = 20 + } + alert.applyTheme(withScheme: self.containerScheme) + // Custom color (overriding the theme's title icon color). + alert.titleIconTintColor = .orange + return alert + } + + func alertWithScaledToFitImage() -> MDCAlertController { + let alert = createMDCAlertController(title: "Scaled Title Icon") + alert.titleIcon = image(named: "STAY_AMSTERDAM") + // Justified alignment size the image to fit the top space of the dialog. Images are scaled down + // if needed, but never scaled up. + alert.titleIconAlignment = .justified + alert.applyTheme(withScheme: self.containerScheme) + return alert + } + + func alertWithScaledBleedingImage() -> MDCAlertController { + let alert = createMDCAlertController(title: "Scaled to fill Title Icon") + alert.titleIcon = image(named: "STAY_AMSTERDAM-WIDE") + // Justified alignment size the image to fit the top space of the dialog. + alert.titleIconAlignment = .justified + if let alertView = alert.view as? MDCAlertControllerView { + alertView.titleIconInsets = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0) + } + alert.applyTheme(withScheme: self.containerScheme) + return alert + } + + func presentAlertWithCustomTitleView(animated: Bool = true) { + let alert = createMDCAlertController(title: "Custom Title View") + + // Create a custom view with a centered image and a light background. + let view = UIView(frame: CGRect(x: 0, y: 0, width: 160, height: 86)) + let alarmView = UIImageView() + view.addSubview(alarmView) + + // Apply theme colors. + view.backgroundColor = containerScheme.colorScheme.primaryColor.withAlphaComponent(0.2) + alarmView.tintColor = containerScheme.colorScheme.primaryColor + + // Resize the imageView to fit to the size of the new loaded image. + if let alarm = image(named: "baseline_alarm_on_black_48pt") { + alarmView.image = alarm + alarmView.sizeToFit() + } + + // Sets the customView as the titleIconView. + alert.titleIconView = view + + // Set .justified alignment with 0 insets to ensure the view's color bleeds through to the edge. + alert.titleIconAlignment = .justified + if let alertView = alert.view as? MDCAlertControllerView { + alertView.titleIconInsets = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0) + } + + alert.applyTheme(withScheme: self.containerScheme) + + if animated { + alarmView.alpha = 0 + } + self.present( + alert, animated: animated, + completion: { + // The view is centered correctly after the alert is presented. + alarmView.center = view.center + if animated { + // Start the image animation after the alert is presetned. + alarmView.animateIn() + } + }) + } + + private func image(named: String) -> UIImage? { + let bundle = Bundle(for: DialogsTitleImageExampleViewController.self) + return UIImage(named: named, in: bundle, compatibleWith: nil) + } + + private func createMDCAlertController(title: String?) -> MDCAlertController { + let alert = MDCAlertController( + title: title, + message: """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + """) + alert.addAction(MDCAlertAction(title: "OK", emphasis: .high, handler: handler)) + alert.addAction(MDCAlertAction(title: "Cancel", handler: handler)) + + // Enable dynamic type. + alert.mdc_adjustsFontForContentSizeCategory = true + + return alert + } +} + +// MDCCollectionViewController Data Source +extension DialogsTitleImageExampleViewController { + + 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 DialogsTitleImageExampleViewController { + + @objc class func catalogMetadata() -> [String: Any] { + return [ + "breadcrumbs": ["Dialogs", "Title Icon, Image & View"], + "primaryDemo": false, + "presentable": true, + ] + } +} + +// MARK: Snapshot Testing by Convention +extension DialogsTitleImageExampleViewController { + + func resetTests() { + if presentedViewController != nil { + dismiss(animated: false) + } + } + + @objc func testTitleIcon() { + resetTests() + self.present( + alertWithTitleIcon(), animated: false, completion: nil) + } + + @objc func testCustomTitleIcon() { + resetTests() + self.present( + alertWithCustomTitleIcon(), animated: false, completion: nil) + } + + @objc func testScaledToFit() { + resetTests() + self.present( + alertWithScaledToFitImage(), animated: false, completion: nil) + } + + @objc func testScaledBleeding() { + resetTests() + self.present( + alertWithScaledBleedingImage(), animated: false, completion: nil) + } + + @objc func testCustomTitleView() { + resetTests() + presentAlertWithCustomTitleView(animated: false) + } +} + +extension UIView { + fileprivate func animateIn( + duration: TimeInterval = 2, options: UIView.AnimationOptions = [.curveEaseOut] + ) { + alpha = 0.2 + self.transform = self.transform + .translatedBy(x: -self.frame.origin.x / 2, y: 0) + .scaledBy(x: 0.2, y: 0.2) + .rotated(by: CGFloat.pi) + UIView.animate( + withDuration: duration, + delay: 0, + usingSpringWithDamping: 0.3, + initialSpringVelocity: 0.3, + options: options, + animations: { + self.transform = CGAffineTransform.identity + self.alpha = 1 + }, completion: nil) + } +} diff --git a/components/Dialogs/examples/resources/DialogsAssets.xcassets/STAY_AMSTERDAM-WIDE.imageset/Contents.json b/components/Dialogs/examples/resources/DialogsAssets.xcassets/STAY_AMSTERDAM-WIDE.imageset/Contents.json new file mode 100644 index 00000000000..9667b565e96 --- /dev/null +++ b/components/Dialogs/examples/resources/DialogsAssets.xcassets/STAY_AMSTERDAM-WIDE.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "STAY_AMSTERDAM-WIDE.jpg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/components/Dialogs/examples/resources/DialogsAssets.xcassets/STAY_AMSTERDAM-WIDE.imageset/STAY_AMSTERDAM-WIDE.jpg b/components/Dialogs/examples/resources/DialogsAssets.xcassets/STAY_AMSTERDAM-WIDE.imageset/STAY_AMSTERDAM-WIDE.jpg new file mode 100644 index 00000000000..9cc3775ceb9 Binary files /dev/null and b/components/Dialogs/examples/resources/DialogsAssets.xcassets/STAY_AMSTERDAM-WIDE.imageset/STAY_AMSTERDAM-WIDE.jpg differ diff --git a/components/Dialogs/examples/resources/DialogsAssets.xcassets/STAY_AMSTERDAM.imageset/Contents.json b/components/Dialogs/examples/resources/DialogsAssets.xcassets/STAY_AMSTERDAM.imageset/Contents.json new file mode 100644 index 00000000000..060f6364e0b --- /dev/null +++ b/components/Dialogs/examples/resources/DialogsAssets.xcassets/STAY_AMSTERDAM.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "STAY_AMSTERDAM.jpg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/components/Dialogs/examples/resources/DialogsAssets.xcassets/STAY_AMSTERDAM.imageset/STAY_AMSTERDAM.jpg b/components/Dialogs/examples/resources/DialogsAssets.xcassets/STAY_AMSTERDAM.imageset/STAY_AMSTERDAM.jpg new file mode 100644 index 00000000000..df1f23daac0 Binary files /dev/null and b/components/Dialogs/examples/resources/DialogsAssets.xcassets/STAY_AMSTERDAM.imageset/STAY_AMSTERDAM.jpg differ