| // 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 = 4, | |
| repeating: Bool = true, | |
| options: UIView.AnimationOptions = [.curveEaseOut] | |
| ) { | |
| self.alpha = 1 | |
| let initialTransform = self.transform.translatedBy(x: -150, y: 0) | |
| self.transform = initialTransform | |
| let transform1 = | |
| initialTransform | |
| .concatenating(CGAffineTransform(translationX: 80, y: 0)) | |
| .rotated(by: CGFloat.pi * 0.8) | |
| let transform2 = | |
| initialTransform | |
| .concatenating(CGAffineTransform(translationX: 160, y: 0)) | |
| .rotated(by: CGFloat.pi * 1.5) | |
| let transform3 = | |
| initialTransform | |
| .concatenating(CGAffineTransform(translationX: 240, y: 0)) | |
| let transform4 = | |
| initialTransform | |
| .concatenating(CGAffineTransform(translationX: 360, y: 0)) | |
| .rotated(by: CGFloat.pi * 0.75) | |
| let animationOptions: UInt | |
| if repeating { | |
| animationOptions = | |
| UIView.AnimationOptions.curveLinear.rawValue | UIView.AnimationOptions.repeat.rawValue | |
| } else { | |
| animationOptions = UIView.AnimationOptions.curveLinear.rawValue | |
| } | |
| let keyFrameAnimationOptions = UIView.KeyframeAnimationOptions(rawValue: animationOptions) | |
| UIView.animateKeyframes( | |
| withDuration: duration, delay: 0, | |
| options: [keyFrameAnimationOptions, .calculationModeLinear], | |
| animations: { | |
| UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.14) { // 0.375 | |
| self.transform = transform1 | |
| } | |
| UIView.addKeyframe(withRelativeStartTime: 0.14, relativeDuration: 0.14) { // 0.375 | |
| self.transform = transform2 | |
| } | |
| UIView.addKeyframe(withRelativeStartTime: 0.28, relativeDuration: 0.15) { //0.25) { | |
| self.transform = transform3 | |
| } | |
| UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 0.15) { //0.25) { | |
| self.transform = transform4 | |
| } | |
| }, completion: nil) | |
| } | |
| } |