// 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) } }