From 3a157e1f4309fbf2c2fe4404acbbfcd34136666c Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 7 Apr 2020 13:48:46 -0700 Subject: [PATCH 01/31] [Buttons] Standardize all examples on the same Example pattern. PiperOrigin-RevId: 305332675 --- ...tViewController.swift => ButtonsCustomFontExample.swift} | 2 +- ...ViewController.swift => ButtonsDynamicTypeExample.swift} | 4 ++-- ...ShapesExampleViewController.m => ButtonsShapesExample.m} | 4 ++-- ...AndProgrammatic.swift => ButtonsStoryboardExample.swift} | 6 +++--- ...seExampleViewController.m => ButtonsTypicalUseExample.m} | 4 ++-- ...Controller.swift => ButtonsTypicalUseSwiftExample.swift} | 4 ++-- ...leViewController.m => FloatingButtonTypicalUseExample.m} | 4 ++-- ...ler.swift => FloatingButtonTypicalUseSwiftExample.swift} | 6 +++--- .../examples/supplemental/ButtonsTypicalUseSupplemental.h | 4 ++-- .../examples/supplemental/ButtonsTypicalUseSupplemental.m | 4 ++-- 10 files changed, 21 insertions(+), 21 deletions(-) rename components/Buttons/examples/{ButtonsCustomFontViewController.swift => ButtonsCustomFontExample.swift} (98%) rename components/Buttons/examples/{ButtonsDynamicTypeViewController.swift => ButtonsDynamicTypeExample.swift} (97%) rename components/Buttons/examples/{ButtonsShapesExampleViewController.m => ButtonsShapesExample.m} (98%) rename components/Buttons/examples/{ButtonsStoryboardAndProgrammatic.swift => ButtonsStoryboardExample.swift} (97%) rename components/Buttons/examples/{ButtonsTypicalUseExampleViewController.m => ButtonsTypicalUseExample.m} (98%) rename components/Buttons/examples/{ButtonsSimpleExampleSwiftViewController.swift => ButtonsTypicalUseSwiftExample.swift} (97%) rename components/Buttons/examples/{FloatingButtonExampleViewController.m => FloatingButtonTypicalUseExample.m} (98%) rename components/Buttons/examples/{FloatingButtonExampleSwiftViewController.swift => FloatingButtonTypicalUseSwiftExample.swift} (97%) diff --git a/components/Buttons/examples/ButtonsCustomFontViewController.swift b/components/Buttons/examples/ButtonsCustomFontExample.swift similarity index 98% rename from components/Buttons/examples/ButtonsCustomFontViewController.swift rename to components/Buttons/examples/ButtonsCustomFontExample.swift index 7ed6b67a981..2679ec335e7 100644 --- a/components/Buttons/examples/ButtonsCustomFontViewController.swift +++ b/components/Buttons/examples/ButtonsCustomFontExample.swift @@ -18,7 +18,7 @@ import MaterialComponents.MaterialButtons import MaterialComponents.MaterialContainerScheme import MaterialComponents.MaterialButtons_Theming -class ButtonsCustomFontViewController: UIViewController { +class ButtonsCustomFontExample: UIViewController { var containerScheme = MDCContainerScheme() diff --git a/components/Buttons/examples/ButtonsDynamicTypeViewController.swift b/components/Buttons/examples/ButtonsDynamicTypeExample.swift similarity index 97% rename from components/Buttons/examples/ButtonsDynamicTypeViewController.swift rename to components/Buttons/examples/ButtonsDynamicTypeExample.swift index 96d9239af61..f424eff912e 100644 --- a/components/Buttons/examples/ButtonsDynamicTypeViewController.swift +++ b/components/Buttons/examples/ButtonsDynamicTypeExample.swift @@ -19,7 +19,7 @@ import MaterialComponents.MaterialButtons_Theming import MaterialComponents.MaterialContainerScheme import MaterialComponents.MaterialTypography -class ButtonsDynamicTypeViewController: UIViewController { +class ButtonsDynamicTypeExample: UIViewController { @objc var containerScheme = MDCContainerScheme() @@ -102,7 +102,7 @@ class ButtonsDynamicTypeViewController: UIViewController { } // MARK: Catalog by conventions -extension ButtonsDynamicTypeViewController { +extension ButtonsDynamicTypeExample { @objc class func catalogMetadata() -> [String: Any] { return [ "breadcrumbs": ["Buttons", "Buttons (DynamicType)"], diff --git a/components/Buttons/examples/ButtonsShapesExampleViewController.m b/components/Buttons/examples/ButtonsShapesExample.m similarity index 98% rename from components/Buttons/examples/ButtonsShapesExampleViewController.m rename to components/Buttons/examples/ButtonsShapesExample.m index fbe76a9268c..82143b912b1 100644 --- a/components/Buttons/examples/ButtonsShapesExampleViewController.m +++ b/components/Buttons/examples/ButtonsShapesExample.m @@ -21,11 +21,11 @@ #import "supplemental/ButtonsTypicalUseSupplemental.h" -@interface ButtonsShapesExampleViewController () +@interface ButtonsShapesExample () @property(nonatomic, strong) MDCFloatingButton *floatingButton; @end -@implementation ButtonsShapesExampleViewController +@implementation ButtonsShapesExample - (id)init { self = [super init]; diff --git a/components/Buttons/examples/ButtonsStoryboardAndProgrammatic.swift b/components/Buttons/examples/ButtonsStoryboardExample.swift similarity index 97% rename from components/Buttons/examples/ButtonsStoryboardAndProgrammatic.swift rename to components/Buttons/examples/ButtonsStoryboardExample.swift index b97721db748..496e09f0603 100644 --- a/components/Buttons/examples/ButtonsStoryboardAndProgrammatic.swift +++ b/components/Buttons/examples/ButtonsStoryboardExample.swift @@ -19,7 +19,7 @@ import MaterialComponents.MaterialButtons import MaterialComponents.MaterialContainerScheme import MaterialComponents.MaterialButtons_Theming -class ButtonsSwiftAndStoryboardController: UIViewController { +class ButtonsStoryboardExample: UIViewController { let containedButton = MDCButton() let flatButton = MDCButton() @@ -209,11 +209,11 @@ class ButtonsSwiftAndStoryboardController: UIViewController { } -extension ButtonsSwiftAndStoryboardController { +extension ButtonsStoryboardExample { @objc class func catalogMetadata() -> [String: Any] { return [ - "breadcrumbs": ["Buttons", "Buttons (Swift and Storyboard)"], + "breadcrumbs": ["Buttons", "Buttons (Storyboard)"], "primaryDemo": false, "presentable": false, "storyboardName": "ButtonsStoryboardAndProgrammatic", diff --git a/components/Buttons/examples/ButtonsTypicalUseExampleViewController.m b/components/Buttons/examples/ButtonsTypicalUseExample.m similarity index 98% rename from components/Buttons/examples/ButtonsTypicalUseExampleViewController.m rename to components/Buttons/examples/ButtonsTypicalUseExample.m index 5a44770fcb7..d441aef9e65 100644 --- a/components/Buttons/examples/ButtonsTypicalUseExampleViewController.m +++ b/components/Buttons/examples/ButtonsTypicalUseExample.m @@ -21,11 +21,11 @@ const CGSize kMinimumAccessibleButtonSize = {64.0, 48.0}; -@interface ButtonsTypicalUseExampleViewController () +@interface ButtonsTypicalUseExample () @property(nonatomic, strong) MDCFloatingButton *floatingButton; @end -@implementation ButtonsTypicalUseExampleViewController +@implementation ButtonsTypicalUseExample - (id)init { self = [super init]; diff --git a/components/Buttons/examples/ButtonsSimpleExampleSwiftViewController.swift b/components/Buttons/examples/ButtonsTypicalUseSwiftExample.swift similarity index 97% rename from components/Buttons/examples/ButtonsSimpleExampleSwiftViewController.swift rename to components/Buttons/examples/ButtonsTypicalUseSwiftExample.swift index c461689e877..c9535014db9 100644 --- a/components/Buttons/examples/ButtonsSimpleExampleSwiftViewController.swift +++ b/components/Buttons/examples/ButtonsTypicalUseSwiftExample.swift @@ -19,7 +19,7 @@ import MaterialComponents.MaterialButtons import MaterialComponents.MaterialContainerScheme import MaterialComponents.MaterialButtons_Theming -class ButtonsSimpleExampleSwiftViewController: UIViewController { +class ButtonsTypicalUseSwiftExample: UIViewController { let floatingButtonPlusDimension = CGFloat(24) let kMinimumAccessibleButtonSize = CGSize(width: 64, height: 48) @@ -118,7 +118,7 @@ class ButtonsSimpleExampleSwiftViewController: UIViewController { } -extension ButtonsSimpleExampleSwiftViewController { +extension ButtonsTypicalUseSwiftExample { @objc class func catalogMetadata() -> [String: Any] { return [ diff --git a/components/Buttons/examples/FloatingButtonExampleViewController.m b/components/Buttons/examples/FloatingButtonTypicalUseExample.m similarity index 98% rename from components/Buttons/examples/FloatingButtonExampleViewController.m rename to components/Buttons/examples/FloatingButtonTypicalUseExample.m index 062dad6c02b..d3727d253c6 100644 --- a/components/Buttons/examples/FloatingButtonExampleViewController.m +++ b/components/Buttons/examples/FloatingButtonTypicalUseExample.m @@ -21,7 +21,7 @@ NSString *kButtonLabel = @"Create"; NSString *kMiniButtonLabel = @"Add"; -@interface FloatingButtonExampleViewController : UIViewController +@interface FloatingButtonTypicalUseExample : UIViewController @property(nonatomic, strong) UILabel *iPadLabel; @property(nonatomic, strong) MDCFloatingButton *miniFloatingButton; @property(nonatomic, strong) MDCFloatingButton *defaultFloatingButton; @@ -29,7 +29,7 @@ @interface FloatingButtonExampleViewController : UIViewController @property(nonatomic, strong) id containerScheme; @end -@implementation FloatingButtonExampleViewController +@implementation FloatingButtonTypicalUseExample - (id)init { self = [super init]; diff --git a/components/Buttons/examples/FloatingButtonExampleSwiftViewController.swift b/components/Buttons/examples/FloatingButtonTypicalUseSwiftExample.swift similarity index 97% rename from components/Buttons/examples/FloatingButtonExampleSwiftViewController.swift rename to components/Buttons/examples/FloatingButtonTypicalUseSwiftExample.swift index d6960bf49ff..6f4b05694fd 100644 --- a/components/Buttons/examples/FloatingButtonExampleSwiftViewController.swift +++ b/components/Buttons/examples/FloatingButtonTypicalUseSwiftExample.swift @@ -18,7 +18,7 @@ import MaterialComponents.MaterialButtons import MaterialComponents.MaterialContainerScheme import MaterialComponents.MaterialButtons_Theming -class FloatingButtonExampleSwiftViewController: UIViewController { +class FloatingButtonTypicalUseSwiftExample: UIViewController { let miniFloatingButton = MDCFloatingButton(frame: .zero, shape: .mini) let defaultFloatingButton = MDCFloatingButton() @@ -114,7 +114,7 @@ class FloatingButtonExampleSwiftViewController: UIViewController { } } -extension FloatingButtonExampleSwiftViewController { +extension FloatingButtonTypicalUseSwiftExample { @objc class func catalogMetadata() -> [String: Any] { return [ @@ -125,7 +125,7 @@ extension FloatingButtonExampleSwiftViewController { } } -extension FloatingButtonExampleSwiftViewController { +extension FloatingButtonTypicalUseSwiftExample { func updateFloatingButtons(to mode: MDCFloatingButtonMode) { if (miniFloatingButton.mode != mode) { miniFloatingButton.mode = mode diff --git a/components/Buttons/examples/supplemental/ButtonsTypicalUseSupplemental.h b/components/Buttons/examples/supplemental/ButtonsTypicalUseSupplemental.h index 64f99a9c6eb..f6c2295d601 100644 --- a/components/Buttons/examples/supplemental/ButtonsTypicalUseSupplemental.h +++ b/components/Buttons/examples/supplemental/ButtonsTypicalUseSupplemental.h @@ -37,10 +37,10 @@ @end -@interface ButtonsTypicalUseExampleViewController : ButtonsTypicalUseViewController +@interface ButtonsTypicalUseExample : ButtonsTypicalUseViewController @end -@interface ButtonsShapesExampleViewController : ButtonsTypicalUseViewController +@interface ButtonsShapesExample : ButtonsTypicalUseViewController @end diff --git a/components/Buttons/examples/supplemental/ButtonsTypicalUseSupplemental.m b/components/Buttons/examples/supplemental/ButtonsTypicalUseSupplemental.m index 5513b136ac9..a9d15679b78 100644 --- a/components/Buttons/examples/supplemental/ButtonsTypicalUseSupplemental.m +++ b/components/Buttons/examples/supplemental/ButtonsTypicalUseSupplemental.m @@ -26,7 +26,7 @@ #pragma mark - ButtonsTypicalUseViewController -@implementation ButtonsTypicalUseExampleViewController (CatalogByConvention) +@implementation ButtonsTypicalUseExample (CatalogByConvention) + (NSDictionary *)catalogMetadata { return @{ @@ -40,7 +40,7 @@ + (NSDictionary *)catalogMetadata { @end -@implementation ButtonsShapesExampleViewController (CatalogByConvention) +@implementation ButtonsShapesExample (CatalogByConvention) + (NSDictionary *)catalogMetadata { return @{ From 4454d0be942d588fcea5a390fb78711c07513a6c Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 7 Apr 2020 14:09:41 -0700 Subject: [PATCH 02/31] [Buttons] Use static storage for all local consts. Two examples were using non-static storage for effectively static consts, causing the symbols to be available available across compilation units and resulting in linker errors when the symbols happen to collide across compilation units. PiperOrigin-RevId: 305337107 --- components/Buttons/examples/ButtonsTypicalUseExample.m | 2 +- components/Buttons/examples/FloatingButtonTypicalUseExample.m | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/Buttons/examples/ButtonsTypicalUseExample.m b/components/Buttons/examples/ButtonsTypicalUseExample.m index d441aef9e65..cc4ceb88ba2 100644 --- a/components/Buttons/examples/ButtonsTypicalUseExample.m +++ b/components/Buttons/examples/ButtonsTypicalUseExample.m @@ -19,7 +19,7 @@ #import "supplemental/ButtonsTypicalUseSupplemental.h" -const CGSize kMinimumAccessibleButtonSize = {64.0, 48.0}; +static const CGSize kMinimumAccessibleButtonSize = {64.0, 48.0}; @interface ButtonsTypicalUseExample () @property(nonatomic, strong) MDCFloatingButton *floatingButton; diff --git a/components/Buttons/examples/FloatingButtonTypicalUseExample.m b/components/Buttons/examples/FloatingButtonTypicalUseExample.m index d3727d253c6..1e44518d3cf 100644 --- a/components/Buttons/examples/FloatingButtonTypicalUseExample.m +++ b/components/Buttons/examples/FloatingButtonTypicalUseExample.m @@ -18,8 +18,8 @@ #import "MaterialButtons.h" #import "MaterialContainerScheme.h" -NSString *kButtonLabel = @"Create"; -NSString *kMiniButtonLabel = @"Add"; +static NSString *const kButtonLabel = @"Create"; +static NSString *const kMiniButtonLabel = @"Add"; @interface FloatingButtonTypicalUseExample : UIViewController @property(nonatomic, strong) UILabel *iPadLabel; From 965cde39c85ca3e0e1e6295784321065c786dfe0 Mon Sep 17 00:00:00 2001 From: Andrew Overton Date: Wed, 8 Apr 2020 07:09:09 -0700 Subject: [PATCH 03/31] This change finishes adding TextControls examples to the internal Catalog. PiperOrigin-RevId: 305470131 --- .../examples/MDCTextControlTextFieldsStoryboardExample.swift | 2 +- .../supplemental/MDCTextControlTextAreaConfiguratorExample.m | 2 +- .../supplemental/MDCTextControlTextFieldConfiguratorExample.m | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/TextControls/examples/MDCTextControlTextFieldsStoryboardExample.swift b/components/TextControls/examples/MDCTextControlTextFieldsStoryboardExample.swift index e701de306a8..77222500aec 100644 --- a/components/TextControls/examples/MDCTextControlTextFieldsStoryboardExample.swift +++ b/components/TextControls/examples/MDCTextControlTextFieldsStoryboardExample.swift @@ -63,7 +63,7 @@ extension MDCTextControlTextFieldsStoryboardExample { return [ "breadcrumbs": ["Text Controls", "MDCTextControl TextFields (Storyboard)"], "primaryDemo": false, - "presentable": false, + "presentable": true, "storyboardName": "MDCTextControlTextFieldsStoryboardExample" ] } diff --git a/components/TextControls/examples/supplemental/MDCTextControlTextAreaConfiguratorExample.m b/components/TextControls/examples/supplemental/MDCTextControlTextAreaConfiguratorExample.m index 0776794439f..6d119a6835c 100644 --- a/components/TextControls/examples/supplemental/MDCTextControlTextAreaConfiguratorExample.m +++ b/components/TextControls/examples/supplemental/MDCTextControlTextAreaConfiguratorExample.m @@ -46,7 +46,7 @@ + (NSDictionary *)catalogMetadata { return @{ @"breadcrumbs" : @[ @"Text Controls", kExampleTitle ], @"primaryDemo" : @NO, - @"presentable" : @NO, + @"presentable" : @YES, }; } diff --git a/components/TextControls/examples/supplemental/MDCTextControlTextFieldConfiguratorExample.m b/components/TextControls/examples/supplemental/MDCTextControlTextFieldConfiguratorExample.m index 2f725049d64..7bd88b45baa 100644 --- a/components/TextControls/examples/supplemental/MDCTextControlTextFieldConfiguratorExample.m +++ b/components/TextControls/examples/supplemental/MDCTextControlTextFieldConfiguratorExample.m @@ -45,8 +45,8 @@ @implementation MDCTextControlTextFieldConfiguratorExample (CatalogByConvention) + (NSDictionary *)catalogMetadata { return @{ @"breadcrumbs" : @[ @"Text Controls", kExampleTitle ], - @"primaryDemo" : @NO, - @"presentable" : @NO, + @"primaryDemo" : @YES, + @"presentable" : @YES, }; } From 8c27dcf2e53f038ba97e4e6452143a8f223c35b7 Mon Sep 17 00:00:00 2001 From: Andrew Overton Date: Wed, 8 Apr 2020 14:57:10 -0700 Subject: [PATCH 04/31] Fix broken link in Buttons docs PiperOrigin-RevId: 305561023 --- components/Buttons/docs/fabs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Buttons/docs/fabs.md b/components/Buttons/docs/fabs.md index e1642067424..e9a201c4bf3 100644 --- a/components/Buttons/docs/fabs.md +++ b/components/Buttons/docs/fabs.md @@ -317,7 +317,7 @@ An extended FAB has a text label, a transparent container and an optional icon. ## Theming You can theme an MDCFloatingButton to have a secondary theme using the MDCFloatingButton theming -extension. [Learn more about theming extensions and container schemes](../../docs/theming.md). Below is a screenshot of Material FABs with the Material Design Shrine theme: +extension. [Learn more about theming extensions and container schemes](../../../docs/theming.md). Below is a screenshot of Material FABs with the Material Design Shrine theme: ![Shrine FABs](assets/shrine_fabs.png) From 57ec631be27332140267393711e1c06400452fb1 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 9 Apr 2020 05:36:53 -0700 Subject: [PATCH 05/31] [Buttons] Standardize all test names on Tests. PiperOrigin-RevId: 305667188 --- ...sColorThemerTests.m => MDCButtonColorThemerTests.m} | 4 ++-- ...ttonsCustomFontTest.m => MDCButtonCustomFontTest.m} | 4 ++-- ...m => MDCButtonInterfaceBuilderCompatibilityTests.m} | 4 ++-- .../{ButtonRippleTests.m => MDCButtonRippleTests.m} | 4 ++-- ...sShapeThemerTests.m => MDCButtonShapeThemerTests.m} | 4 ++-- ...nSubclassingTests.m => MDCButtonSubclassingTests.m} | 10 +++++----- .../{ButtonThemerTests.m => MDCButtonThemerTests.m} | 4 ++-- ... => MDCButtonTitleColorAccessibilityMutatorTests.m} | 4 ++-- ...yThemerTests.m => MDCButtonTypographyThemerTests.m} | 4 ++-- .../unit/{FlatButtonTests.m => MDCFlatButtonTests.m} | 4 ++-- ...{FloatingButtonTests.m => MDCFloatingButtonTests.m} | 4 ++-- .../{RaisedButtonTests.m => MDCRaisedButtonTests.m} | 4 ++-- 12 files changed, 27 insertions(+), 27 deletions(-) rename components/Buttons/tests/unit/{ButtonsColorThemerTests.m => MDCButtonColorThemerTests.m} (99%) rename components/Buttons/tests/unit/{ButtonsCustomFontTest.m => MDCButtonCustomFontTest.m} (95%) rename components/Buttons/tests/unit/{ButtonInterfaceBuilderCompatibilityTests.m => MDCButtonInterfaceBuilderCompatibilityTests.m} (90%) rename components/Buttons/tests/unit/{ButtonRippleTests.m => MDCButtonRippleTests.m} (98%) rename components/Buttons/tests/unit/{ButtonsShapeThemerTests.m => MDCButtonShapeThemerTests.m} (96%) rename components/Buttons/tests/unit/{ButtonSubclassingTests.m => MDCButtonSubclassingTests.m} (89%) rename components/Buttons/tests/unit/{ButtonThemerTests.m => MDCButtonThemerTests.m} (98%) rename components/Buttons/tests/unit/{ButtonTitleColorAccessibilityMutatorTests.m => MDCButtonTitleColorAccessibilityMutatorTests.m} (97%) rename components/Buttons/tests/unit/{ButtonTypographyThemerTests.m => MDCButtonTypographyThemerTests.m} (94%) rename components/Buttons/tests/unit/{FlatButtonTests.m => MDCFlatButtonTests.m} (95%) rename components/Buttons/tests/unit/{FloatingButtonTests.m => MDCFloatingButtonTests.m} (99%) rename components/Buttons/tests/unit/{RaisedButtonTests.m => MDCRaisedButtonTests.m} (93%) diff --git a/components/Buttons/tests/unit/ButtonsColorThemerTests.m b/components/Buttons/tests/unit/MDCButtonColorThemerTests.m similarity index 99% rename from components/Buttons/tests/unit/ButtonsColorThemerTests.m rename to components/Buttons/tests/unit/MDCButtonColorThemerTests.m index 3085142fac9..bdf7c88bf0e 100644 --- a/components/Buttons/tests/unit/ButtonsColorThemerTests.m +++ b/components/Buttons/tests/unit/MDCButtonColorThemerTests.m @@ -19,11 +19,11 @@ static const CGFloat kEpsilonAccuracy = (CGFloat)0.001; -@interface ButtonsColorThemerTests : XCTestCase +@interface MDCButtonColorThemerTests : XCTestCase @end -@implementation ButtonsColorThemerTests +@implementation MDCButtonColorThemerTests - (void)testTextButtonColorThemer { // Given diff --git a/components/Buttons/tests/unit/ButtonsCustomFontTest.m b/components/Buttons/tests/unit/MDCButtonCustomFontTest.m similarity index 95% rename from components/Buttons/tests/unit/ButtonsCustomFontTest.m rename to components/Buttons/tests/unit/MDCButtonCustomFontTest.m index a9117bea7db..3bbc1376bf6 100644 --- a/components/Buttons/tests/unit/ButtonsCustomFontTest.m +++ b/components/Buttons/tests/unit/MDCButtonCustomFontTest.m @@ -17,10 +17,10 @@ #import "MaterialButtons.h" #import "MaterialTypography.h" -@interface ButtonsCustomFontTests : XCTestCase +@interface MDCButtonCustomFontTests : XCTestCase @end -@implementation ButtonsCustomFontTests +@implementation MDCButtonCustomFontTests - (void)testCustomFontTitle { // Given diff --git a/components/Buttons/tests/unit/ButtonInterfaceBuilderCompatibilityTests.m b/components/Buttons/tests/unit/MDCButtonInterfaceBuilderCompatibilityTests.m similarity index 90% rename from components/Buttons/tests/unit/ButtonInterfaceBuilderCompatibilityTests.m rename to components/Buttons/tests/unit/MDCButtonInterfaceBuilderCompatibilityTests.m index de9340d2f94..55aebafcc72 100644 --- a/components/Buttons/tests/unit/ButtonInterfaceBuilderCompatibilityTests.m +++ b/components/Buttons/tests/unit/MDCButtonInterfaceBuilderCompatibilityTests.m @@ -17,11 +17,11 @@ #import "supplemental/ButtonTestView.h" -@interface ButtonInterfaceBuilderCompatibilityTests : XCTestCase +@interface MDCButtonInterfaceBuilderCompatibilityTests : XCTestCase @end -@implementation ButtonInterfaceBuilderCompatibilityTests +@implementation MDCButtonInterfaceBuilderCompatibilityTests - (void)testFontRestoredFromNib { // Given diff --git a/components/Buttons/tests/unit/ButtonRippleTests.m b/components/Buttons/tests/unit/MDCButtonRippleTests.m similarity index 98% rename from components/Buttons/tests/unit/ButtonRippleTests.m rename to components/Buttons/tests/unit/MDCButtonRippleTests.m index edd01d94904..37517b828a4 100644 --- a/components/Buttons/tests/unit/ButtonRippleTests.m +++ b/components/Buttons/tests/unit/MDCButtonRippleTests.m @@ -25,13 +25,13 @@ @interface MDCButton (Testing) /** This class confirms behavior of @c MDCButton when used with @c MDCStatefulRippleView. */ -@interface ButtonRippleTests : XCTestCase +@interface MDCButtonRippleTests : XCTestCase @property(nonatomic, strong, nullable) MDCButton *button; @end -@implementation ButtonRippleTests +@implementation MDCButtonRippleTests - (void)setUp { [super setUp]; diff --git a/components/Buttons/tests/unit/ButtonsShapeThemerTests.m b/components/Buttons/tests/unit/MDCButtonShapeThemerTests.m similarity index 96% rename from components/Buttons/tests/unit/ButtonsShapeThemerTests.m rename to components/Buttons/tests/unit/MDCButtonShapeThemerTests.m index 8aed464eecc..3fca0044904 100644 --- a/components/Buttons/tests/unit/ButtonsShapeThemerTests.m +++ b/components/Buttons/tests/unit/MDCButtonShapeThemerTests.m @@ -18,14 +18,14 @@ #import "MaterialButtons.h" #import "MaterialShapeLibrary.h" -@interface ButtonsShapeThemerTests : XCTestCase +@interface MDCButtonShapeThemerTests : XCTestCase @property(nonatomic, strong) MDCButton *button; @property(nonatomic, strong) MDCShapeScheme *shapeScheme; @end -@implementation ButtonsShapeThemerTests +@implementation MDCButtonShapeThemerTests - (void)setUp { self.button = [[MDCButton alloc] init]; diff --git a/components/Buttons/tests/unit/ButtonSubclassingTests.m b/components/Buttons/tests/unit/MDCButtonSubclassingTests.m similarity index 89% rename from components/Buttons/tests/unit/ButtonSubclassingTests.m rename to components/Buttons/tests/unit/MDCButtonSubclassingTests.m index 6bd10a13fb2..4980ea5c47b 100644 --- a/components/Buttons/tests/unit/ButtonSubclassingTests.m +++ b/components/Buttons/tests/unit/MDCButtonSubclassingTests.m @@ -22,10 +22,10 @@ static const UIEdgeInsets ButtonTestContentEdgeInsets = {1, 2, 3, 4}; static const CGFloat ButtonTestCornerRadius = (CGFloat)1.234; -@interface ButtonSubclass : MDCButton +@interface MDCButtonSubclass : MDCButton @end -@implementation ButtonSubclass +@implementation MDCButtonSubclass - (UIEdgeInsets)defaultContentEdgeInsets { return ButtonTestContentEdgeInsets; @@ -33,14 +33,14 @@ - (UIEdgeInsets)defaultContentEdgeInsets { @end -@interface ButtonSubclassingTests : XCTestCase +@interface MDCButtonSubclassingTests : XCTestCase @end -@implementation ButtonSubclassingTests +@implementation MDCButtonSubclassingTests - (void)testSubclassContentEdgeInsets { // Given - MDCButton *button = [[ButtonSubclass alloc] init]; + MDCButton *button = [[MDCButtonSubclass alloc] init]; // Then XCTAssertTrue( diff --git a/components/Buttons/tests/unit/ButtonThemerTests.m b/components/Buttons/tests/unit/MDCButtonThemerTests.m similarity index 98% rename from components/Buttons/tests/unit/ButtonThemerTests.m rename to components/Buttons/tests/unit/MDCButtonThemerTests.m index 8fc54959c1f..118ce8af7f0 100644 --- a/components/Buttons/tests/unit/ButtonThemerTests.m +++ b/components/Buttons/tests/unit/MDCButtonThemerTests.m @@ -22,10 +22,10 @@ static const CGFloat kEpsilonAccuracy = (CGFloat)0.001; -@interface ButtonThemerTests : XCTestCase +@interface MDCButtonThemerTests : XCTestCase @end -@implementation ButtonThemerTests +@implementation MDCButtonThemerTests - (void)testTextButtonThemer { // Given diff --git a/components/Buttons/tests/unit/ButtonTitleColorAccessibilityMutatorTests.m b/components/Buttons/tests/unit/MDCButtonTitleColorAccessibilityMutatorTests.m similarity index 97% rename from components/Buttons/tests/unit/ButtonTitleColorAccessibilityMutatorTests.m rename to components/Buttons/tests/unit/MDCButtonTitleColorAccessibilityMutatorTests.m index 48b1582fed7..49721d55572 100644 --- a/components/Buttons/tests/unit/ButtonTitleColorAccessibilityMutatorTests.m +++ b/components/Buttons/tests/unit/MDCButtonTitleColorAccessibilityMutatorTests.m @@ -34,10 +34,10 @@ static NSString *controlStateDescription(UIControlState controlState); -@interface ButtonTitleColorAccessibilityMutatorTests : XCTestCase +@interface MDCButtonTitleColorAccessibilityMutatorTests : XCTestCase @end -@implementation ButtonTitleColorAccessibilityMutatorTests +@implementation MDCButtonTitleColorAccessibilityMutatorTests - (void)testMutateChangesTextColor { for (UIColor *color in testColors()) { diff --git a/components/Buttons/tests/unit/ButtonTypographyThemerTests.m b/components/Buttons/tests/unit/MDCButtonTypographyThemerTests.m similarity index 94% rename from components/Buttons/tests/unit/ButtonTypographyThemerTests.m rename to components/Buttons/tests/unit/MDCButtonTypographyThemerTests.m index 4c1f8c4a3b3..90dd5f2a556 100644 --- a/components/Buttons/tests/unit/ButtonTypographyThemerTests.m +++ b/components/Buttons/tests/unit/MDCButtonTypographyThemerTests.m @@ -17,10 +17,10 @@ #import "MaterialButtons+TypographyThemer.h" #import "MaterialButtons.h" -@interface ButtonTypographyThemerTests : XCTestCase +@interface MDCButtonTypographyThemerTests : XCTestCase @end -@implementation ButtonTypographyThemerTests +@implementation MDCButtonTypographyThemerTests - (void)testButtonTypographyThemer { // Given diff --git a/components/Buttons/tests/unit/FlatButtonTests.m b/components/Buttons/tests/unit/MDCFlatButtonTests.m similarity index 95% rename from components/Buttons/tests/unit/FlatButtonTests.m rename to components/Buttons/tests/unit/MDCFlatButtonTests.m index 157566737b1..3e871cc94e0 100644 --- a/components/Buttons/tests/unit/FlatButtonTests.m +++ b/components/Buttons/tests/unit/MDCFlatButtonTests.m @@ -17,10 +17,10 @@ #import "MaterialButtons.h" #import "MaterialShadowElevations.h" -@interface FlatButtonsTests : XCTestCase +@interface MDCFlatButtonsTests : XCTestCase @end -@implementation FlatButtonsTests +@implementation MDCFlatButtonsTests - (void)testDefaultElevationsForState { // Given diff --git a/components/Buttons/tests/unit/FloatingButtonTests.m b/components/Buttons/tests/unit/MDCFloatingButtonTests.m similarity index 99% rename from components/Buttons/tests/unit/FloatingButtonTests.m rename to components/Buttons/tests/unit/MDCFloatingButtonTests.m index c5f1e4689fe..2a3b68d7eb1 100644 --- a/components/Buttons/tests/unit/FloatingButtonTests.m +++ b/components/Buttons/tests/unit/MDCFloatingButtonTests.m @@ -67,10 +67,10 @@ - (void)layoutSubviews { @end -@interface FloatingButtonsTests : XCTestCase +@interface MDCFloatingButtonsTests : XCTestCase @end -@implementation FloatingButtonsTests +@implementation MDCFloatingButtonsTests #pragma mark - setHitAreaInsets:forShape:inMode: diff --git a/components/Buttons/tests/unit/RaisedButtonTests.m b/components/Buttons/tests/unit/MDCRaisedButtonTests.m similarity index 93% rename from components/Buttons/tests/unit/RaisedButtonTests.m rename to components/Buttons/tests/unit/MDCRaisedButtonTests.m index fd69690e45a..3738d972959 100644 --- a/components/Buttons/tests/unit/RaisedButtonTests.m +++ b/components/Buttons/tests/unit/MDCRaisedButtonTests.m @@ -17,10 +17,10 @@ #import "MaterialButtons.h" #import "MaterialShadowElevations.h" -@interface RaisedButtonsTests : XCTestCase +@interface MDCRaisedButtonsTests : XCTestCase @end -@implementation RaisedButtonsTests +@implementation MDCRaisedButtonsTests - (void)testDefaultElevationsForState { // Given From bc113b658c9a9e1befe6e3b4f83d6fa3b0e9db87 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 9 Apr 2020 05:46:01 -0700 Subject: [PATCH 06/31] [Buttons] Add a snapshot test for floating buttons in normal mode with a label. This snapshot test demonstrates the incorrect behavior of the title label being visible while the button is in normal mode. PiperOrigin-RevId: 305668064 --- .../snapshot/MDCFloatingButtonSnapshotTests.m | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/components/Buttons/tests/snapshot/MDCFloatingButtonSnapshotTests.m b/components/Buttons/tests/snapshot/MDCFloatingButtonSnapshotTests.m index 875926344c7..8f1071c54da 100644 --- a/components/Buttons/tests/snapshot/MDCFloatingButtonSnapshotTests.m +++ b/components/Buttons/tests/snapshot/MDCFloatingButtonSnapshotTests.m @@ -66,4 +66,19 @@ - (void)testDefaultShapeInNormalModeWithLargeIconShowsLargeIcon { [self generateSnapshotAndVerifyForView:button]; } +// TODO(b/153576427): The label should be hidden in this test, but it is not. +- (void)testNormalModeWithTitleLabelDoesNotShowLabel { + // Given + MDCFloatingButton *button = + [MDCFloatingButton floatingButtonWithShape:MDCFloatingButtonShapeDefault]; + [button setTitle:@"Title" forState:UIControlStateNormal]; + UIImage *buttonImage = [[UIImage mdc_testImageOfSize:CGSizeMake(36, 36)] + imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [button setImage:buttonImage forState:UIControlStateNormal]; + button.mode = MDCFloatingButtonModeNormal; + + // Then + [self generateSnapshotAndVerifyForView:button]; +} + @end From 57a566909fb7d89247b93887262c7cb93597c75f Mon Sep 17 00:00:00 2001 From: Dave MacLachlan Date: Thu, 9 Apr 2020 07:36:43 -0700 Subject: [PATCH 07/31] Clean up some comments. PiperOrigin-RevId: 305681610 --- components/Dialogs/src/MDCAlertController+ButtonForAction.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Dialogs/src/MDCAlertController+ButtonForAction.h b/components/Dialogs/src/MDCAlertController+ButtonForAction.h index a38f4b6e65b..ce1e62d7d36 100644 --- a/components/Dialogs/src/MDCAlertController+ButtonForAction.h +++ b/components/Dialogs/src/MDCAlertController+ButtonForAction.h @@ -18,7 +18,7 @@ /** Returns an MDCButton associated with the given action. This method might create the button if - it no associated button exist for the action yet. Buttons returned by thismethod may not (yet) + no associated button exists for the action. Buttons returned by this method may not (yet) be attached to the view hierarchy at the time the method is called. This method is commonly used by themers to style the button associated with the action. From 6ac7e6c3d87795a6ade75500c7c6af768f1463f0 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 9 Apr 2020 09:54:47 -0700 Subject: [PATCH 08/31] [Buttons] Add support to MDCFloatingButton for animating mode changes. The new API allows the floating button to animate changes between normal and expanded states. PiperOrigin-RevId: 305703859 --- .../FloatingButtonModeAnimationExample.m | 94 +++++ components/Buttons/src/MDCFloatingButton.h | 33 ++ components/Buttons/src/MDCFloatingButton.m | 86 ++++- .../private/MDCFloatingButtonModeAnimator.h | 49 +++ .../private/MDCFloatingButtonModeAnimator.m | 205 ++++++++++ .../MDCFloatingButtonModeAnimatorDelegate.h | 35 ++ .../snapshot/MDCFloatingButtonSnapshotTests.m | 76 ++++ .../unit/MDCFloatingButtonModeAnimatorTests.m | 350 ++++++++++++++++++ 8 files changed, 916 insertions(+), 12 deletions(-) create mode 100644 components/Buttons/examples/FloatingButtonModeAnimationExample.m create mode 100644 components/Buttons/src/private/MDCFloatingButtonModeAnimator.h create mode 100644 components/Buttons/src/private/MDCFloatingButtonModeAnimator.m create mode 100644 components/Buttons/src/private/MDCFloatingButtonModeAnimatorDelegate.h create mode 100644 components/Buttons/tests/unit/MDCFloatingButtonModeAnimatorTests.m diff --git a/components/Buttons/examples/FloatingButtonModeAnimationExample.m b/components/Buttons/examples/FloatingButtonModeAnimationExample.m new file mode 100644 index 00000000000..1b289e664dd --- /dev/null +++ b/components/Buttons/examples/FloatingButtonModeAnimationExample.m @@ -0,0 +1,94 @@ +// 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 + +#import "MaterialButtons.h" +#import "MaterialButtons+Theming.h" +#import "MaterialContainerScheme.h" + +static NSString *const kButtonLabel = @"Create"; + +@interface FloatingButtonModeAnimationExample : UIViewController +@property(nonatomic, strong) MDCFloatingButton *floatingButton; +@property(nonatomic, strong) id containerScheme; +@end + +@implementation FloatingButtonModeAnimationExample + +- (id)init { + self = [super init]; + if (self) { + _containerScheme = [[MDCContainerScheme alloc] init]; + } + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor colorWithWhite:0.9f alpha:1]; + + UIImage *plusImage = + [[UIImage imageNamed:@"Plus"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + UIImage *plusImage36 = [UIImage imageNamed:@"plus_white_36" + inBundle:[NSBundle bundleForClass:[self class]] + compatibleWithTraitCollection:self.traitCollection]; + plusImage36 = [plusImage36 imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + + self.floatingButton = [[MDCFloatingButton alloc] init]; + [self.floatingButton setImage:plusImage forState:UIControlStateNormal]; + self.floatingButton.autoresizingMask = + (UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin | + UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin); + [self.floatingButton applySecondaryThemeWithScheme:self.containerScheme]; + [self.floatingButton setTitle:kButtonLabel forState:UIControlStateNormal]; + [self.floatingButton addTarget:self + action:@selector(didTap:) + forControlEvents:UIControlEventTouchUpInside]; + [self.floatingButton sizeToFit]; + self.floatingButton.center = + CGPointMake(self.view.bounds.size.width / 2.0f, self.view.bounds.size.height / 2.0f); + [self.view addSubview:self.floatingButton]; +} + +- (void)didTap:(MDCFloatingButton *)sender { + void (^animations)(void) = ^{ + self.floatingButton.center = + CGPointMake(self.view.bounds.size.width / 2.0f, self.view.bounds.size.height / 2.0f); + }; + if (sender.mode == MDCFloatingButtonModeExpanded) { + [sender setMode:MDCFloatingButtonModeNormal + animated:YES + animateAlongside:animations + completion:nil]; + } else { + [sender setMode:MDCFloatingButtonModeExpanded + animated:YES + animateAlongside:animations + completion:nil]; + } +} + +#pragma mark - Catalog by Convention + ++ (NSDictionary *)catalogMetadata { + return @{ + @"breadcrumbs" : @[ @"Buttons", @"Floating Action Button Mode Animation" ], + @"primaryDemo" : @NO, + @"presentable" : @YES, + }; +} + +@end diff --git a/components/Buttons/src/MDCFloatingButton.h b/components/Buttons/src/MDCFloatingButton.h index 633df5bcedf..ae94b63bee3 100644 --- a/components/Buttons/src/MDCFloatingButton.h +++ b/components/Buttons/src/MDCFloatingButton.h @@ -82,10 +82,43 @@ typedef NS_ENUM(NSInteger, MDCFloatingButtonImageLocation) { leading-aligned within this box. In @c .expanded mode, the @c contentVerticalAlignment and @c contentHorizontalAlignment properties are ignored. + @note Setting the mode directly is equivalent to calling + @code [self setMode:mode animated:NO] @endcode. + The default value is @c .normal . */ @property(nonatomic, assign) MDCFloatingButtonMode mode; +/** + Changes the mode (with animation, if desired). + + If animated, the floating button's size will be updated automatically as part of the animation. + Otherwise, the floating button's size will need to be explicitly recalculated after the mode has + changed. + + @see @c mode for more details about the mode value. + */ +- (void)setMode:(MDCFloatingButtonMode)mode animated:(BOOL)animated; + +/** + Changes the mode (with animation, if desired). + + If animated, the floating button's size will be updated automatically as part of the animation. + Otherwise, the floating button's size will need to be explicitly recalculated after the mode has + changed. + + @param animateAlongside An optional block that will be invoked alongside the animation, if + animated, otherwise it will be invoked immediately. + @param completion An optional block that will be invoked upon completion of the animation, if + animated, otherwise it will be invoked immediately. + + @see @c mode for more details about the mode value. + */ +- (void)setMode:(MDCFloatingButtonMode)mode + animated:(BOOL)animated + animateAlongside:(nullable void (^)(void))animateAlongside + completion:(nullable void (^)(BOOL finished))completion; + /** The location of the image relative to the title when the floating button is in @c expanded mode. diff --git a/components/Buttons/src/MDCFloatingButton.m b/components/Buttons/src/MDCFloatingButton.m index 43f29cecca8..dca4ef72d15 100644 --- a/components/Buttons/src/MDCFloatingButton.m +++ b/components/Buttons/src/MDCFloatingButton.m @@ -14,8 +14,10 @@ #import "MDCFloatingButton.h" -#import "MaterialShadowElevations.h" #import "private/MDCButton+Subclassing.h" +#import "private/MDCFloatingButtonModeAnimator.h" +#import "private/MDCFloatingButtonModeAnimatorDelegate.h" +#import "MaterialShadowElevations.h" #import @@ -24,7 +26,7 @@ static const CGFloat MDCFloatingButtonDefaultImageTitleSpace = 8; static const UIEdgeInsets internalLayoutInsets = (UIEdgeInsets){0, 16, 0, 24}; -@interface MDCFloatingButton () +@interface MDCFloatingButton () @property(nonatomic, readonly) NSMutableDictionary *> @@ -46,6 +48,10 @@ @interface MDCFloatingButton () @implementation MDCFloatingButton { MDCFloatingButtonShape _shape; + + MDCFloatingButtonModeAnimator *_modeAnimator; + // Allows us to perform masking effects during mode animations. + UIView *_titleLabelContainerView; } + (void)initialize { @@ -109,6 +115,16 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { - (void)commonMDCFloatingButtonInit { _imageTitleSpace = MDCFloatingButtonDefaultImageTitleSpace; + // Create a container view for titleLabel and add the titelLabel to it. This will enable us to + // mask the titleLabel mode animations if desired, while acting effectively as a pass-through for + // the superview layout logic. + _titleLabelContainerView = [[UIView alloc] initWithFrame:self.bounds]; + _titleLabelContainerView.autoresizingMask = + UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self insertSubview:_titleLabelContainerView belowSubview:self.titleLabel]; + _titleLabelContainerView.userInteractionEnabled = NO; + [_titleLabelContainerView addSubview:self.titleLabel]; + const CGSize miniNormalSize = CGSizeMake(MDCFloatingButtonMiniDimension, MDCFloatingButtonMiniDimension); const CGSize defaultNormalSize = @@ -252,12 +268,7 @@ - (void)layoutSubviews { // The diagram above assumes an LTR user interface orientation // and a .leadingIcon imageLocation for this button. - BOOL isLeadingIcon = self.imageLocation == MDCFloatingButtonImageLocationLeading; - UIEdgeInsets adjustedLayoutInsets = - isLeadingIcon ? internalLayoutInsets : MDFInsetsFlippedHorizontally(internalLayoutInsets); - - const CGRect insetBounds = UIEdgeInsetsInsetRect( - UIEdgeInsetsInsetRect(self.bounds, adjustedLayoutInsets), self.contentEdgeInsets); + const CGRect insetBounds = [self insetBoundsForBounds:self.bounds]; const CGFloat imageViewWidth = CGRectGetWidth(self.imageView.bounds); const CGFloat boundsCenterY = CGRectGetMidY(insetBounds); @@ -276,6 +287,7 @@ - (void)layoutSubviews { CGPoint imageCenter; BOOL isLTR = self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionLeftToRight; + BOOL isLeadingIcon = self.imageLocation == MDCFloatingButtonImageLocationLeading; // If we are LTR with a leading image, the image goes on the left. // If we are RTL with a trailing image, the image goes on the left. @@ -304,14 +316,64 @@ - (void)layoutSubviews { self.titleLabel.frame = UIEdgeInsetsInsetRect(self.titleLabel.frame, self.titleEdgeInsets); } +- (CGRect)insetBoundsForBounds:(CGRect)bounds { + BOOL isLeadingIcon = self.imageLocation == MDCFloatingButtonImageLocationLeading; + UIEdgeInsets adjustedLayoutInsets = + (isLeadingIcon ? internalLayoutInsets : MDFInsetsFlippedHorizontally(internalLayoutInsets)); + return UIEdgeInsetsInsetRect(UIEdgeInsetsInsetRect(bounds, adjustedLayoutInsets), + self.contentEdgeInsets); +} + +#pragma mark - Mode animator + +- (MDCFloatingButtonModeAnimator *)modeAnimator { + if (!_modeAnimator) { + _modeAnimator = + [[MDCFloatingButtonModeAnimator alloc] initWithTitleLabel:self.titleLabel + titleLabelContainerView:_titleLabelContainerView]; + _modeAnimator.delegate = self; + } + return _modeAnimator; +} + +#pragma mark MDCFloatingButtonModeAnimatorDelegate + +- (void)floatingButtonModeAnimatorCommitLayoutChanges:(MDCFloatingButtonModeAnimator *)modeAnimator + mode:(MDCFloatingButtonMode)mode { + [self sizeToFit]; +} + #pragma mark - Property Setters/Getters - (void)setMode:(MDCFloatingButtonMode)mode { - BOOL needsShapeUpdate = self.mode != mode; - _mode = mode; - if (needsShapeUpdate) { - [self updateShapeAndAllowResize:YES]; + [self setMode:mode animated:NO animateAlongside:nil completion:nil]; +} + +- (void)setMode:(MDCFloatingButtonMode)mode animated:(BOOL)animated { + [self setMode:mode animated:animated animateAlongside:nil completion:nil]; +} + +- (void)setMode:(MDCFloatingButtonMode)mode + animated:(BOOL)animated + animateAlongside:(void (^)(void))animateAlongside + completion:(void (^)(BOOL finished))completion { + if (_mode == mode) { + if (animateAlongside) { + animateAlongside(); + } + if (completion) { + completion(YES); + } + return; } + _mode = mode; + + [self updateShapeAndAllowResize:YES]; + + [[self modeAnimator] modeDidChange:mode + animated:animated + animateAlongside:animateAlongside + completion:completion]; } - (void)setMinimumSize:(CGSize)size diff --git a/components/Buttons/src/private/MDCFloatingButtonModeAnimator.h b/components/Buttons/src/private/MDCFloatingButtonModeAnimator.h new file mode 100644 index 00000000000..203646d3585 --- /dev/null +++ b/components/Buttons/src/private/MDCFloatingButtonModeAnimator.h @@ -0,0 +1,49 @@ +// 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 +#import + +#import "MDCFloatingButton.h" + +@protocol MDCFloatingButtonModeAnimatorDelegate; + +/** + Animates an MDCFloatingButton's mode. + */ +__attribute__((objc_subclassing_restricted)) @interface MDCFloatingButtonModeAnimator : NSObject + +- (nonnull instancetype)initWithTitleLabel:(nonnull UILabel *)titleLabel + titleLabelContainerView:(nonnull UIView *)titleLabelContainerView + NS_DESIGNATED_INITIALIZER; + +/** + Informs the animator that the floating button mode has changed. + + If the change was animated, then the animator will initiate the necessary animations to create the + visual effect of the modes animating from one state to the next. + */ +- (void)modeDidChange:(MDCFloatingButtonMode)mode + animated:(BOOL)animated + animateAlongside:(nullable void (^)(void))animateAlongside + completion:(nullable void (^)(BOOL finished))completion; + +/** + The animator uses the delegate to interact with its owning context: the MDCFloatingButton instance. + */ +@property(nonatomic, weak, nullable) id delegate; + +- (nonnull instancetype)init NS_UNAVAILABLE; + +@end diff --git a/components/Buttons/src/private/MDCFloatingButtonModeAnimator.m b/components/Buttons/src/private/MDCFloatingButtonModeAnimator.m new file mode 100644 index 00000000000..52ccae0e6d1 --- /dev/null +++ b/components/Buttons/src/private/MDCFloatingButtonModeAnimator.m @@ -0,0 +1,205 @@ +// 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 "MDCFloatingButtonModeAnimator.h" + +#import "MDCFloatingButtonModeAnimatorDelegate.h" + +#if TARGET_IPHONE_SIMULATOR +UIKIT_EXTERN float UIAnimationDragCoefficient(void); // UIKit private drag coefficient. +#endif + +static CGFloat SimulatorAnimationDragCoefficient(void) { +#if TARGET_IPHONE_SIMULATOR + return UIAnimationDragCoefficient(); +#else + return 1.0; +#endif +} + +typedef struct { + NSTimeInterval duration; + NSTimeInterval titleOpacityDelay; + NSTimeInterval titleOpacityDuration; +} AnimationTiming; + +// go/mdc-fab-expansion-animation +static const AnimationTiming kExpandAnimationTiming = (AnimationTiming){ + .duration = 0.200, + .titleOpacityDelay = 0.083, + .titleOpacityDuration = 0.067, +}; + +// go/mdc-fab-collapse-animation +static const AnimationTiming kCollapseAnimationTiming = (AnimationTiming){ + .duration = 0.167, + .titleOpacityDelay = 0.016, + .titleOpacityDuration = 0.033, +}; + +static const UIViewAnimationOptions kTitleOpacityAnimationOptions = + UIViewAnimationOptionCurveLinear; + +static NSString *const kModeVerticalDriftAnimationKey = @"position.y.fix"; + +@interface MDCFloatingButtonModeAnimator () +@property(nonatomic, strong) UILabel *titleLabel; +@property(nonatomic, strong) UIView *titleLabelContainerView; +@end + +@implementation MDCFloatingButtonModeAnimator + +- (instancetype)initWithTitleLabel:(UILabel *)titleLabel + titleLabelContainerView:(UIView *)titleLabelContainerView { + self = [super init]; + if (self) { + self.titleLabel = titleLabel; + self.titleLabelContainerView = titleLabelContainerView; + } + return self; +} + +- (void)modeDidChange:(MDCFloatingButtonMode)mode + animated:(BOOL)animated + animateAlongside:(nullable void (^)(void))animateAlongside + completion:(nullable void (^)(BOOL finished))completion { + if (!animated) { + self.titleLabelContainerView.clipsToBounds = NO; + if (animateAlongside) { + animateAlongside(); + } + if (completion) { + completion(YES); + } + return; + } + + // Floating button mode animations are relatively rare, so to avoid having the non-animated steady + // state pay any compositing costs due to masking we only enable clipsToBounds for the course of + // the mode animation. The bounds clipping is necessary to achieve the clipped label effect as the + // button expands / collapses. + _titleLabelContainerView.clipsToBounds = YES; + + const BOOL expanding = mode == MDCFloatingButtonModeExpanded; + + // ## Prepare the label for animation + + // Because the titleLabel has an empty frame in the collapsed state, we key this entire animation + // off of the expanded state's frame. When expanding, we can animate the titleLabel directly + // because the destination frame is non-empty. When collapsing, we need to animate a snapshot + // because the titleLabel's destination frame is empty. + UIView *animationTitleLabel; + void (^titleLabelCleanup)(BOOL); + if (expanding) { + animationTitleLabel = self.titleLabel; + animationTitleLabel.alpha = 0; // Start off initially transparent. + titleLabelCleanup = nil; + } else { + animationTitleLabel = [self.titleLabel snapshotViewAfterScreenUpdates:NO]; + animationTitleLabel.frame = self.titleLabel.frame; + [_titleLabelContainerView addSubview:animationTitleLabel]; + titleLabelCleanup = ^(BOOL finished) { + [animationTitleLabel removeFromSuperview]; + }; + } + + // ## Perform the frame animation + + CGRect priorTitleLabelFrame = self.titleLabel.frame; + AnimationTiming timing = expanding ? kExpandAnimationTiming : kCollapseAnimationTiming; + [UIView animateWithDuration:timing.duration + animations:^{ + NSSet *priorTitleAnimationKeys = [NSSet setWithArray:self.titleLabel.layer.animationKeys]; + + // Force the button to adjust its frame in order to generate the default animations. Note + // that this will also animate the title label and any other subviews within the button. + [self.delegate floatingButtonModeAnimatorCommitLayoutChanges:self mode:mode]; + + // If we allowed the title label to animate as a result of the sizeToFit changes, then we + // would see the title expand / collapse its frame in a squishy and undesirable manner. To + // avoid this, we remove any *newly added* animations from the title label before they get + // committed to the render server. The resulting effect is that the title label's frame is + // instantly where it needs to be and we can animate its alpha independently and compensate + // for any vertical drift below. + NSMutableSet *newTitleAnimationKeys = + [NSMutableSet setWithArray:self.titleLabel.layer.animationKeys]; + [newTitleAnimationKeys minusSet:priorTitleAnimationKeys]; + for (NSString *animationKey in newTitleAnimationKeys) { + [self.titleLabel.layer removeAnimationForKey:animationKey]; + } + + if (animateAlongside) { + animateAlongside(); + } + } + completion:^(BOOL finished) { + if (titleLabelCleanup) { + titleLabelCleanup(finished); + } + _titleLabelContainerView.clipsToBounds = NO; + if (completion) { + completion(finished); + } + }]; + + // ## Compensate for vertical drift + + // As noted above, the default titleLabel frame animations cause an undesired scaling effect of + // the label and so the default animations are removed. The titleLabel's frame is now set to the + // final state of the animation which is generally desired in terms of the titleLabel's size, but + // if the button's height changes then we need to animate the label's y position, otherwise the + // label will appear to be pinned to the top - rather than the center - of the button as the + // button expands / contracts. + // + // We compensate for this effect by creating an additive animation below. The purpose of this + // additive animation is to give the appearance that the label is centered vertically within the + // button over the course of the animation and without making adjustments to the model layer. + + CGRect newTitleLabelFrame = self.titleLabel.frame; + CGFloat centerYDelta = CGRectGetMidY(newTitleLabelFrame) - CGRectGetMidY(priorTitleLabelFrame); + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position.y"]; + animation.additive = YES; + animation.timingFunction = + [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + if (expanding) { + // When expanding, we initially compensate for the vertical shift and then gradually reduce that + // compensation as the animation progresses. This is because the label's model layer is already + // shifted relative to the original center position. + animation.fromValue = @(-centerYDelta); + animation.toValue = @0; + } else { + // When contracting we undo the effect of the expansion by adding the compensation gradually + // back to the label. + animation.fromValue = @0; + animation.toValue = @(centerYDelta); + } + animation.duration = timing.duration * SimulatorAnimationDragCoefficient(); + [animationTitleLabel.layer addAnimation:animation forKey:kModeVerticalDriftAnimationKey]; + + // ## Animate the title opacity + + [UIView animateWithDuration:timing.titleOpacityDuration + delay:timing.titleOpacityDelay + options:kTitleOpacityAnimationOptions + animations:^{ + if (mode == MDCFloatingButtonModeExpanded) { + animationTitleLabel.alpha = 1; + } else { + animationTitleLabel.alpha = 0; + } + } + completion:nil]; +} + +@end diff --git a/components/Buttons/src/private/MDCFloatingButtonModeAnimatorDelegate.h b/components/Buttons/src/private/MDCFloatingButtonModeAnimatorDelegate.h new file mode 100644 index 00000000000..d7d2d71ea7b --- /dev/null +++ b/components/Buttons/src/private/MDCFloatingButtonModeAnimatorDelegate.h @@ -0,0 +1,35 @@ +// 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 +#import + +#import "MDCFloatingButton.h" + +@class MDCFloatingButtonModeAnimator; + +/** + MDCFloatingButtonModeAnimator uses this delegate to interact with its owning context. + */ +@protocol MDCFloatingButtonModeAnimatorDelegate +@required + +/** + Asks the receiver to commit any layout changes relevant to the mode change. + */ +- (void)floatingButtonModeAnimatorCommitLayoutChanges: + (nonnull MDCFloatingButtonModeAnimator *)modeAnimator + mode:(MDCFloatingButtonMode)mode; + +@end diff --git a/components/Buttons/tests/snapshot/MDCFloatingButtonSnapshotTests.m b/components/Buttons/tests/snapshot/MDCFloatingButtonSnapshotTests.m index 8f1071c54da..781163b3455 100644 --- a/components/Buttons/tests/snapshot/MDCFloatingButtonSnapshotTests.m +++ b/components/Buttons/tests/snapshot/MDCFloatingButtonSnapshotTests.m @@ -81,4 +81,80 @@ - (void)testNormalModeWithTitleLabelDoesNotShowLabel { [self generateSnapshotAndVerifyForView:button]; } +#pragma mark - Animated mode changes + +// This screenshot should identically match testModeToExpandedAnimated +- (void)testModeFromNormalToExpandedImmediate { + // Given + MDCFloatingButton *button = + [MDCFloatingButton floatingButtonWithShape:MDCFloatingButtonShapeDefault]; + [button setTitle:@"Title" forState:UIControlStateNormal]; + UIImage *buttonImage = [[UIImage mdc_testImageOfSize:CGSizeMake(36, 36)] + imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [button setImage:buttonImage forState:UIControlStateNormal]; + + // When + [button setMode:MDCFloatingButtonModeExpanded animated:NO]; + + // Then + [self generateSnapshotAndVerifyForView:button]; +} + +// This screenshot should identically match testModeToExpandedImmediate +- (void)testModeFromNormalToExpandedAnimated { + // Given + MDCFloatingButton *button = + [MDCFloatingButton floatingButtonWithShape:MDCFloatingButtonShapeDefault]; + [button setTitle:@"Title" forState:UIControlStateNormal]; + UIImage *buttonImage = [[UIImage mdc_testImageOfSize:CGSizeMake(36, 36)] + imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [button setImage:buttonImage forState:UIControlStateNormal]; + + // When + button.layer.speed = 1000; + [button setMode:MDCFloatingButtonModeExpanded animated:YES]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; + + // Then + [self generateSnapshotAndVerifyForView:button]; +} + +// This screenshot should identically match testModeFromExpandedToNormalAnimated +- (void)testModeFromExpandedToNormalImmediate { + // Given + MDCFloatingButton *button = + [MDCFloatingButton floatingButtonWithShape:MDCFloatingButtonShapeDefault]; + [button setTitle:@"Title" forState:UIControlStateNormal]; + UIImage *buttonImage = [[UIImage mdc_testImageOfSize:CGSizeMake(36, 36)] + imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [button setImage:buttonImage forState:UIControlStateNormal]; + [button setMode:MDCFloatingButtonModeExpanded animated:NO]; + + // When + [button setMode:MDCFloatingButtonModeNormal animated:NO]; + + // Then + [self generateSnapshotAndVerifyForView:button]; +} + +// This screenshot should identically match testModeFromExpandedToNormalImmediate +- (void)testModeFromExpandedToNormalAnimated { + // Given + MDCFloatingButton *button = + [MDCFloatingButton floatingButtonWithShape:MDCFloatingButtonShapeDefault]; + [button setTitle:@"Title" forState:UIControlStateNormal]; + UIImage *buttonImage = [[UIImage mdc_testImageOfSize:CGSizeMake(36, 36)] + imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [button setImage:buttonImage forState:UIControlStateNormal]; + [button setMode:MDCFloatingButtonModeExpanded animated:NO]; + + // When + button.layer.speed = 1000; + [button setMode:MDCFloatingButtonModeNormal animated:YES]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; + + // Then + [self generateSnapshotAndVerifyForView:button]; +} + @end diff --git a/components/Buttons/tests/unit/MDCFloatingButtonModeAnimatorTests.m b/components/Buttons/tests/unit/MDCFloatingButtonModeAnimatorTests.m new file mode 100644 index 00000000000..e6063089620 --- /dev/null +++ b/components/Buttons/tests/unit/MDCFloatingButtonModeAnimatorTests.m @@ -0,0 +1,350 @@ +// 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 "MaterialButtons.h" +#import "MDCFloatingButtonModeAnimator.h" +#import "MDCFloatingButtonModeAnimatorDelegate.h" + +#import + +@interface MDCFloatingButtonModeAnimatorTests : XCTestCase +@property(nonatomic, strong) MDCFloatingButtonModeAnimator *animator; +@property(nonatomic, strong) UILabel *titleLabel; +@property(nonatomic, strong) UIView *containerView; +@property(nonatomic, strong) UIView *buttonView; +@property(nonatomic) BOOL delegateDidAskToCommitLayoutChanges; +@end + +static const CGRect kNormalFrame = (CGRect){{0, 0}, {100, 50}}; +static const CGRect kExpandedFrame = (CGRect){{0, 0}, {200, 40}}; + +@implementation MDCFloatingButtonModeAnimatorTests + +- (void)setUp { + [super setUp]; + + self.containerView = [[UIView alloc] init]; + self.buttonView = [[UIView alloc] init]; + self.titleLabel = [[UILabel alloc] init]; + self.titleLabel.text = @"Some text"; + + self.buttonView.frame = kNormalFrame; + + self.containerView.frame = self.buttonView.bounds; + self.containerView.autoresizingMask = + (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); + [self.buttonView addSubview:self.containerView]; + + [self.titleLabel sizeToFit]; + self.titleLabel.center = CGPointMake(CGRectGetMidX(self.containerView.bounds), + CGRectGetMidY(self.containerView.bounds)); + self.titleLabel.autoresizingMask = + (UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin | + UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin); + [self.containerView addSubview:self.titleLabel]; + + self.animator = [[MDCFloatingButtonModeAnimator alloc] initWithTitleLabel:self.titleLabel + titleLabelContainerView:self.containerView]; + self.delegateDidAskToCommitLayoutChanges = NO; +} + +- (void)tearDown { + self.titleLabel = nil; + self.containerView = nil; + self.buttonView = nil; + self.animator = nil; + self.delegateDidAskToCommitLayoutChanges = NO; + + [super tearDown]; +} + +#pragma mark - MDCFloatingButtonModeAnimatorDelegate + +- (void)floatingButtonModeAnimatorCommitLayoutChanges:(MDCFloatingButtonModeAnimator *)modeAnimator + mode:(MDCFloatingButtonMode)mode { + self.delegateDidAskToCommitLayoutChanges = YES; + [self commitMode:mode]; +} + +#pragma mark - Defaults + +- (void)testDefaults { + XCTAssertNil(self.animator.delegate); + XCTAssertFalse(self.containerView.clipsToBounds); + XCTAssertEqual([[self.titleLabel.layer animationKeys] count], 0); + XCTAssertEqual([self.containerView.subviews count], 1); +} + +#pragma mark - Container +// The only effect the animator has on the container is to toggle clipsToBounds while animating. + +#pragma mark Non-animated + +- (void)testImmediateToNormalContainerDoesNotClip { + // When + [self.animator modeDidChange:MDCFloatingButtonModeNormal + animated:NO + animateAlongside:nil + completion:nil]; + + // Then + XCTAssertFalse(self.containerView.clipsToBounds); +} + +- (void)testImmediateToExpandedContainerDoesNotClip { + // When + [self.animator modeDidChange:MDCFloatingButtonModeExpanded + animated:NO + animateAlongside:nil + completion:nil]; + + // Then + XCTAssertFalse(self.containerView.clipsToBounds); +} + +#pragma mark Animated + +- (void)testAnimateToNormalContainerDoesClip { + // When + [self.animator modeDidChange:MDCFloatingButtonModeNormal + animated:YES + animateAlongside:nil + completion:nil]; + + // Then + XCTAssertTrue(self.containerView.clipsToBounds); +} + +- (void)testAnimateToExpandedContainerDoesClip { + // When + [self.animator modeDidChange:MDCFloatingButtonModeExpanded + animated:YES + animateAlongside:nil + completion:nil]; + + // Then + XCTAssertTrue(self.containerView.clipsToBounds); +} + +#pragma mark - Title label + +#pragma mark Non-animated + +- (void)testImmediateToExpandedDoesNotAskToCommitLayoutChanges { + // Given + self.animator.delegate = self; + + // When + [self.animator modeDidChange:MDCFloatingButtonModeExpanded + animated:NO + animateAlongside:nil + completion:nil]; + + // Then + XCTAssertFalse(self.delegateDidAskToCommitLayoutChanges); +} + +- (void)testImmediateToNormalDoesNotAskToCommitLayoutChanges { + // Given + self.animator.delegate = self; + + // When + [self.animator modeDidChange:MDCFloatingButtonModeNormal + animated:NO + animateAlongside:nil + completion:nil]; + + // Then + XCTAssertFalse(self.delegateDidAskToCommitLayoutChanges); +} + +#pragma mark Animated + +- (void)testAnimateToExpandedAsksToCommitLayoutChanges { + // Given + self.animator.delegate = self; + + // When + [self.animator modeDidChange:MDCFloatingButtonModeExpanded + animated:YES + animateAlongside:nil + completion:nil]; + + // Then + XCTAssertTrue(self.delegateDidAskToCommitLayoutChanges); +} + +- (void)testAnimateToNormalAsksToCommitLayoutChanges { + // Given + self.animator.delegate = self; + + // When + [self.animator modeDidChange:MDCFloatingButtonModeExpanded + animated:YES + animateAlongside:nil + completion:nil]; + + // Then + XCTAssertTrue(self.delegateDidAskToCommitLayoutChanges); +} + +- (void)testAnimateFromNormalToExpandedGeneratesLabelAnimations { + // Given + self.animator.delegate = self; + + // When + [self.animator modeDidChange:MDCFloatingButtonModeExpanded + animated:YES + animateAlongside:nil + completion:nil]; + + // Then + NSSet *animatedKeyPaths = [self animatedKeyPathsForLayer:self.titleLabel.layer]; + NSSet *expectedKeyPaths = [NSSet setWithObjects:@"position.y", @"opacity", nil]; + XCTAssertEqualObjects(animatedKeyPaths, expectedKeyPaths); +} + +- (void)testAnimateFromNormalToNormalDoesNotGenerateLabelAnimations { + // Given + self.animator.delegate = self; + + // When + [self.animator modeDidChange:MDCFloatingButtonModeNormal + animated:YES + animateAlongside:nil + completion:nil]; + + // Then + XCTAssertEqual([[self.titleLabel.layer animationKeys] count], 0); +} + +// Does not generate animations because it animates a replica view instead. +- (void)testAnimateFromExpandedToNormalDoesNotGenerateLabelAnimations { + // Given + [self commitMode:MDCFloatingButtonModeExpanded]; + self.animator.delegate = self; + + // When + [self.animator modeDidChange:MDCFloatingButtonModeNormal + animated:YES + animateAlongside:nil + completion:nil]; + + // Then + XCTAssertEqual([[self.titleLabel.layer animationKeys] count], 0); +} + +#pragma mark - Blocks + +#pragma mark Non-animated + +- (void)testImmediateToExpandedInvokesAnimateAlongsideAndCompletion { + // When + __block BOOL didInvokeAnimateAlongside = NO; + __block BOOL didInvokeCompletion = NO; + [self.animator modeDidChange:MDCFloatingButtonModeExpanded + animated:NO + animateAlongside:^{ + didInvokeAnimateAlongside = YES; + } + completion:^(BOOL finished) { + didInvokeCompletion = YES; + }]; + + // Then + XCTAssertTrue(didInvokeAnimateAlongside); + XCTAssertTrue(didInvokeCompletion); +} + +- (void)testImmediateToNormalInvokesAnimateAlongsideAndCompletion { + // When + __block BOOL didInvokeAnimateAlongside = NO; + __block BOOL didInvokeCompletion = NO; + [self.animator modeDidChange:MDCFloatingButtonModeNormal + animated:NO + animateAlongside:^{ + didInvokeAnimateAlongside = YES; + } + completion:^(BOOL finished) { + didInvokeCompletion = YES; + }]; + + // Then + XCTAssertTrue(didInvokeAnimateAlongside); + XCTAssertTrue(didInvokeCompletion); +} + +#pragma mark Animated + +- (void)testAnimateToExpandedInvokesAnimateAlongsideButNotCompletion { + // When + __block BOOL didInvokeAnimateAlongside = NO; + __block BOOL didInvokeCompletion = NO; + [self.animator modeDidChange:MDCFloatingButtonModeExpanded + animated:YES + animateAlongside:^{ + didInvokeAnimateAlongside = YES; + } + completion:^(BOOL finished) { + didInvokeCompletion = YES; + }]; + + // Then + XCTAssertTrue(didInvokeAnimateAlongside); + XCTAssertFalse(didInvokeCompletion); +} + +- (void)testAnimateToNormalInvokesAnimateAlongsideButNotCompletion { + // When + __block BOOL didInvokeAnimateAlongside = NO; + __block BOOL didInvokeCompletion = NO; + [self.animator modeDidChange:MDCFloatingButtonModeNormal + animated:YES + animateAlongside:^{ + didInvokeAnimateAlongside = YES; + } + completion:^(BOOL finished) { + didInvokeCompletion = YES; + }]; + + // Then + XCTAssertTrue(didInvokeAnimateAlongside); + XCTAssertFalse(didInvokeCompletion); +} + +#pragma mark - Helper methods + +- (NSSet *)animatedKeyPathsForLayer:(CALayer *)layer { + NSMutableSet *animatedKeyPaths = [NSMutableSet set]; + NSArray *animationKeys = [layer animationKeys]; + for (NSString *key in animationKeys) { + CAAnimation *animation = [layer animationForKey:key]; + if ([animation isKindOfClass:[CABasicAnimation class]]) { + CABasicAnimation *basicAnimation = (CABasicAnimation *)animation; + [animatedKeyPaths addObject:basicAnimation.keyPath]; + } + } + return animatedKeyPaths; +} + +- (void)commitMode:(MDCFloatingButtonMode)mode { + if (mode == MDCFloatingButtonModeNormal) { + self.buttonView.frame = kNormalFrame; + } else if (mode == MDCFloatingButtonModeExpanded) { + self.buttonView.frame = kExpandedFrame; + } + [self.buttonView layoutIfNeeded]; +} + +@end From f9a72c3df80e89904fa72ad058032059e74fcf59 Mon Sep 17 00:00:00 2001 From: Andrew Overton Date: Thu, 9 Apr 2020 15:13:51 -0700 Subject: [PATCH 09/31] [Buttons] Fix a lot of formatting issues with material.io and some broken links TL;DR - This PR consists of fixes for broken links, broken image links, and broken Swift/Objective-C code snippets on material.io, and also adds some small copy edits. Sometimes our documentation renders okay on GitHub, but breaks in the conversion of markdown to HTML/CSS that takes place during material.io deploys. As of yesterday, I'm able to build material.io locally, which allows me _validate_ the documentation changes I will keep making for the material.io work. It also allows me to catch broken links elsewhere, in totally unrelated files. For example, there were broken links in some Shapes documentation that were already blocking the deployment of our docs unbeknownst to us. Closes https://github.com/material-components/material-components-ios/pull/9963 COPYBARA_INTEGRATE_REVIEW=https://github.com/material-components/material-components-ios/pull/9963 from andrewoverton:fix-formatting-issues cd54e90863274a49068e016547b1bf0b1af3afcb PiperOrigin-RevId: 305770167 --- ...{shrine_buttons.png => shrine-buttons.png} | Bin .../Buttons/docs/assets/shrine-fabs.png | Bin 0 -> 38056 bytes components/Buttons/docs/buttons.md | 175 +++++++------ components/Buttons/docs/fabs.md | 236 +++++++++--------- docs/supporting-shapes/README.md | 2 +- 5 files changed, 204 insertions(+), 209 deletions(-) rename components/Buttons/docs/assets/{shrine_buttons.png => shrine-buttons.png} (100%) create mode 100644 components/Buttons/docs/assets/shrine-fabs.png diff --git a/components/Buttons/docs/assets/shrine_buttons.png b/components/Buttons/docs/assets/shrine-buttons.png similarity index 100% rename from components/Buttons/docs/assets/shrine_buttons.png rename to components/Buttons/docs/assets/shrine-buttons.png diff --git a/components/Buttons/docs/assets/shrine-fabs.png b/components/Buttons/docs/assets/shrine-fabs.png new file mode 100644 index 0000000000000000000000000000000000000000..3eff092bbe3b6f84d52ac2a0fd16347ebbeabebe GIT binary patch literal 38056 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�H|mf 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 R;HQUM9PC_AR2=h*{U5-xhO7Vp literal 0 HcmV?d00001 diff --git a/components/Buttons/docs/buttons.md b/components/Buttons/docs/buttons.md index a0b8c8f9f7d..d3a143c6042 100644 --- a/components/Buttons/docs/buttons.md +++ b/components/Buttons/docs/buttons.md @@ -3,11 +3,9 @@ title: Buttons layout: detail section: components excerpt: "iOS Buttons" -ide_version: " " -material_package_version: "" iconId: -path: /catalog/buttons -api_doc_root: +path: /catalog/buttons/ +api_doc_root: true --> # Buttons @@ -32,41 +30,40 @@ All Material buttons are implemented by `MDCButton`, a subclass of [UIButton](ht ### Install `MDCButton` -`MDCButton` is used to implement all four Material Buttons. In order to use `MCDButton`, do the following: +`MDCButton` is used to implement all four Material Buttons. In order to use `MCDButton`, first add Buttons to your `Podfile`: -1. Install with Cocoapods - Add the following line to your `Podfile`: +```bash +pod MaterialComponents/Buttons +``` + + +Then, run the installer. - ``` - pod MaterialComponents/Buttons - ``` - - - Run the installer: - - ``` - pod install - ``` +```bash +pod install +``` -1. Import the Buttons and initialize them using `alloc`/`init`. +After that, import the Buttons and initialize them using `alloc`/`init`. + + +#### Swift +```swift +import MaterialComponents.MaterialButtons +import MaterialComponents.MaterialButtons_Theming + +let button = MDCButton() +``` - - #### Objective-C - ```objc - #import "MaterialButtons.h" - #import +#### Objective-C - MDCButton *button = [[MDCButton alloc] init]; - ``` +```objc +#import "MaterialButtons.h" +#import - #### Swift - ```swift - import MaterialComponents.MaterialButtons - import MaterialComponents.MaterialButtons_Theming +MDCButton *button = [[MDCButton alloc] init]; +``` + - let button = MDCButton() - ``` - ### Making Buttons accessible @@ -81,15 +78,15 @@ value if your button does not have a title. This is often the case with Floating Action Button instances which typically only have an icon. -##### Objective-C -```objc -button.accessibilityLabel = @"Create"; -``` - -##### Swift +#### Swift ```swift button.accessibilityLabel = "Create" ``` + +#### Objective-C +```objc +button.accessibilityLabel = @"Create"; +``` #### Minimum touch size @@ -112,14 +109,7 @@ targets](https://material.io/design/layout/spacing-methods.html#touch-click-targ in the spec. -##### Objective-C -```objc -CGFloat verticalInset = MIN(0, -(48 - CGRectGetHeight(button.bounds)) / 2); -CGFloat horizontalInset = MIN(0, -(48 - CGRectGetWidth(button.bounds)) / 2); -button.hitAreaInsets = UIEdgeInsetsMake(verticalInset, horizontalInset, verticalInset, horizontalInset); -``` - -##### Swift +#### Swift ```swift let buttonVerticalInset = min(0, -(kMinimumAccessibleButtonSize.height - button.bounds.height) / 2); @@ -129,6 +119,13 @@ button.hitAreaInsets = UIEdgeInsetsMake(buttonVerticalInset, buttonHorizontalInset, buttonVerticalInset, buttonHorizontalInset); ``` + +#### Objective-C +```objc +CGFloat verticalInset = MIN(0, -(48 - CGRectGetHeight(button.bounds)) / 2); +CGFloat horizontalInset = MIN(0, -(48 - CGRectGetWidth(button.bounds)) / 2); +button.hitAreaInsets = UIEdgeInsetsMake(verticalInset, horizontalInset, verticalInset, horizontalInset); +``` #### Set the minimum visual size of the button @@ -139,15 +136,15 @@ typically recommend [a minimum height of 36 points and a minimum width of 64 points](https://material.io/design/components/buttons.html#specs). -##### Objective-C -```objc -button.minimumSize = CGSizeMake(64, 36); -``` - -##### Swift +#### Swift ```swift button.minimumSize = CGSize(width: 64, height: 48) ``` + +#### Objective-C +```objc +button.minimumSize = CGSizeMake(64, 36); +``` #### Exceptions @@ -193,6 +190,7 @@ To use a text button use the text button theming method on the MDCButton theming ``` #### Swift + ```swift button.applyTextTheme(withScheme: containerScheme) ``` @@ -249,6 +247,7 @@ To achieve an outlined button use the outlined button theming method on the MDCB ``` #### Swift + ```swift button.applyOutlinedTheme(withScheme: containerScheme) ``` @@ -305,6 +304,7 @@ Contained buttons are implemented by [MDCButton](https://material.io/develop/ios ``` #### Swift + ```swift button.applyContainedTheme(withScheme: containerScheme) ``` @@ -353,44 +353,43 @@ A contained button has a text label, a container, and an optional icon. You can theme an MDCButton to match any of the Material Button styles using theming extensions. [Learn more about theming extensions](../../../docs/theming.md). Below is a screenshot of Material Buttons with the Material Design Shrine theme: -![Shrine buttons](assets/shrine_buttons.png) +![Shrine buttons](assets/shrine-buttons.png) ### Buttons theming example -To make use of the theming methods shown in the examples above do the following: - -1. Install the theming extensions with Cocoapods - Add the following line to your `Podfile`: - - ``` - pod MaterialComponents/Buttons+Theming - ``` - - - Run the installer: - - ``` - pod install - ``` - -1. Import the Buttons theming target - - - #### Objective-C - ```objc - #import "MaterialButtons.h" - #import "MaterialButtons+Theming.h" - - MDCButton *button = [[MDCButton alloc] init]; - ``` - - #### Swift - ```swift - import MaterialComponents.MaterialButtons - import MaterialComponents.MaterialButtons_Theming - - let button = MDCButton() - ``` - +To make use of the theming methods shown in the examples above install the Buttons theming extensions with Cocoapods. First, add the following line to your `Podfile`. + +```bash +pod MaterialComponents/Buttons+Theming +``` + + + +Then Run the installer. + +```bash +pod install +``` + +Next, import the Buttons theming target and initialize a button. + + +#### Swift +```swift +import MaterialComponents.MaterialButtons +import MaterialComponents.MaterialButtons_Theming + +let button = MDCButton() +``` + +#### Objective-C +```objc +#import "MaterialButtons.h" +#import "MaterialButtons+Theming.h" + +MDCButton *button = [[MDCButton alloc] init]; +``` + + From there, use the theming methods from the examples to achieve your preferred button style. diff --git a/components/Buttons/docs/fabs.md b/components/Buttons/docs/fabs.md index e9a201c4bf3..4a18ecf78eb 100644 --- a/components/Buttons/docs/fabs.md +++ b/components/Buttons/docs/fabs.md @@ -2,9 +2,10 @@ title: "Buttons: floating action buttons" layout: detail section: components -excerpt: "Dialogs are modal windows that require interaction." +excerpt: "A floating action button (FAB) represents the primary action of a screen" iconId: -path: /catalog/FAB/ +path: /catalog/fabs/ +api_doc_root: true --> # Buttons: floating action buttons @@ -33,44 +34,40 @@ FABs should be provided with a templated image for their normal state and then t ### Installing FABs -Because MDCFloatingButton is a subclass of [MDCButton](ios-button.md), the steps for installing it are the same. +Because MDCFloatingButton is a subclass of [MDCButton](buttons.md), the steps for installing it are the same. -In order to use `MDCFloatingButton`, do the following: +In order to use `MDCFloatingButton`, first add Buttons to your `Podfile`. -1. Install with Cocoapods - Add the following line to your `Podfile`: - - ``` - pod MaterialComponents/Buttons - ``` - +```bash +pod MaterialComponents/Buttons +``` + - Run the installer: +Then, run the installer. - ``` - pod install - ``` - +```bash +pod install +``` -1. Import Buttons and initialize an MDCFloatingButton using `alloc`/`init`. +After that, import the Buttons and initialize an MDCFloatingButton using `alloc`/`init`. - - #### Objective-C - ```objc - #import "MaterialButtons.h" - #import + +#### Swift +```swift +import MaterialComponents.MaterialButtons +import MaterialComponents.MaterialButtons_Theming - MDCFloatingButton *fab = [[MDCFloatingButton alloc] init]; - ``` +let fab = MDCFloatingButton() +``` - #### Swift - ```swift - import MaterialComponents.MaterialButtons - import MaterialComponents.MaterialButtons_Theming +#### Objective-C +```objc +#import "MaterialButtons.h" +#import - let fab = MDCFloatingButton() - ``` - +MDCFloatingButton *fab = [[MDCFloatingButton alloc] init]; +``` + ### Making FABs accessible @@ -79,30 +76,30 @@ To help make your FABs usable to as many users as possible, apply the following: * Set an appropriate [`accessibilityLabel`](https://developer.apple.com/documentation/uikit/uiaccessibilityelement/1619577-accessibilitylabel) value if your button does not have a title or only has an icon: -#### Objective-C -```objc -floatingButton.accessibilityLabel = @"Create"; -``` - #### Swift ```swift floatingButton.accessibilityLabel = "Create" ``` + +#### Objective-C +```objc +floatingButton.accessibilityLabel = @"Create"; +``` * Set the minimum [visual height to 36 and miniumum visual width to 64](https://material.io/design/components/buttons.html#specs) -#### Objective-C -```objc -floatingButton.minimumSize = CGSizeMake(64, 36); -``` - #### Swift ```swift floatingButton.minimumSize = CGSize(width: 64, height: 48) ``` + +#### Objective-C +```objc +floatingButton.minimumSize = CGSizeMake(64, 36); +``` @@ -113,13 +110,6 @@ targets](https://material.io/design/layout/spacing-methods.html#touch-click-targ in the spec. -#### Objective-C -```objc -CGFloat verticalInset = MIN(0, -(48 - CGRectGetHeight(fab.bounds)) / 2); -CGFloat horizontalInset = MIN(0, -(48 - CGRectGetWidth(fab.bounds)) / 2); -floatingButton.hitAreaInsets = UIEdgeInsetsMake(verticalInset, horizontalInset, verticalInset, horizontalInset); -``` - #### Swift ```swift let buttonVerticalInset = @@ -130,27 +120,34 @@ floatingButton.hitAreaInsets = UIEdgeInsetsMake(buttonVerticalInset, buttonHorizontalInset, buttonVerticalInset, buttonHorizontalInset); ``` + +#### Objective-C +```objc +CGFloat verticalInset = MIN(0, -(48 - CGRectGetHeight(fab.bounds)) / 2); +CGFloat horizontalInset = MIN(0, -(48 - CGRectGetWidth(fab.bounds)) / 2); +floatingButton.hitAreaInsets = UIEdgeInsetsMake(verticalInset, horizontalInset, verticalInset, horizontalInset); +``` - _**Note** There are [some](https://material.io/design/components/buttons.html#toggle-button) clear [exceptions](https://material.io/design/components/app-bars-bottom.html#specs) for these rules. Please adjust your buttons sizes accordingly._ +_**Note** There are [some](https://material.io/design/components/buttons.html#toggle-button) clear [exceptions](https://material.io/design/components/app-bars-bottom.html#specs) for these rules. Please adjust your buttons sizes accordingly._ * **Optional** Set an appropriate `accessibilityHint` - Apple rarely recommends using the `accessibilityHint` because the label should - already be clear enough to indicate what will happen. Before you consider - setting an `-accessibilityHint` consider if you need it or if the rest of your - UI could be adjusted to make it more contextually clear. +Apple rarely recommends using the `accessibilityHint` because the label should +already be clear enough to indicate what will happen. Before you consider +setting an `-accessibilityHint` consider if you need it or if the rest of your +UI could be adjusted to make it more contextually clear. - A well-crafted, thoughtful user interface can remove the need for - `accessibilityHint` in most situations. Examples for a selection dialog to - choose one or more days of the week for a repeating calendar event: +A well-crafted, thoughtful user interface can remove the need for +`accessibilityHint` in most situations. Examples for a selection dialog to +choose one or more days of the week for a repeating calendar event: - * (Good) The dialog includes a header above the list of days reading, "Event - repeats weekly on the following day(s)." The list items do not need - `accessibilityHint` values. - * (Bad) The dialog has no header above the list of days. Each list item - (representing a day of the week) has the `accessibilityHint` value, "Toggles - this day." +* (Good) The dialog includes a header above the list of days reading, "Event +repeats weekly on the following day(s)." The list items do not need +`accessibilityHint` values. +* (Bad) The dialog has no header above the list of days. Each list item +(representing a day of the week) has the `accessibilityHint` value, "Toggles +this day." ## Regular FABs @@ -165,16 +162,16 @@ To create a regular FAB use the `+floatingButtonWithShape:` constructor with a v For more information on theming FABs see the [Theming section](#theming). +#### Swift +```swift +let fab = MDCFloatingButton(shape: `default`) +``` + #### Objective-C ```objc MDCFloatingButton *fab = [MDCFloatingButton floatingButtonWithShape:MDCFloatingButtonShapeDefault]; ``` - -#### Swift -```swift -let fab = MDCFloatingButton(shape: `default`) -``` ### Anatomy and key properties @@ -263,7 +260,7 @@ The extended FAB is wider, and it includes a text label. ### Extended FABs example -To create a mini FAB use the `+floatingButtonWithShape:` constructor with a value of `MDCFloatingButtonShapeMini` and make sure the `mode` property is set to `MDCFloatingButtonModeNormal`. +To create an extended FAB use the `+floatingButtonWithShape:` constructor with a value of `MDCFloatingButtonShapeDefault` and make sure the `mode` property is set to `MDCFloatingButtonModeExpanded`. For more information on theming FABs see the [Theming section](#theming). @@ -271,12 +268,14 @@ For more information on theming FABs see the [Theming section](#theming). #### Objective-C ```objc MDCFloatingButton *fab = - [MDCFloatingButton floatingButtonWithShape:MDCFloatingButtonShapeMini]; + [MDCFloatingButton floatingButtonWithShape:MDCFloatingButtonShapeDefault]; +fab.mode = MDCFloatingButtonModeExpanded; ``` #### Swift ```swift -let fab = MDCFloatingButton(shape: mini) +let fab = MDCFloatingButton(shape: .default) +fab.mode = .expanded ``` @@ -319,56 +318,53 @@ An extended FAB has a text label, a transparent container and an optional icon. You can theme an MDCFloatingButton to have a secondary theme using the MDCFloatingButton theming extension. [Learn more about theming extensions and container schemes](../../../docs/theming.md). Below is a screenshot of Material FABs with the Material Design Shrine theme: -![Shrine FABs](assets/shrine_fabs.png) +![Shrine FABs](assets/shrine-fabs.png) ### FAB theming example -To make use of the theming methods shown in the examples above do the following: - -1. Install the Buttons theming extensions with Cocoapods - Add the following line to your `Podfile`: - - ``` - pod MaterialComponents/Buttons+Theming - ``` - - - Run the installer: - - ``` - pod install - ``` - - -1. Import the Buttons theming target - - - #### Objective-C - ```objc - #import "MaterialButtons.h" - #import - ``` - - #### Swift - ```swift - import MaterialComponents.MaterialButtons - import MaterialComponents.MaterialButtons_Theming - ``` - - -1. From there, pass a container scheme into the following theming method on an MDCFloatingButton instance: - - - #### Objective-C - ```objc - MDCFloatingButton *fab = - [MDCFloatingButton floatingButtonWithShape:MDCFloatingButtonShapeMini]; - [fab applySecondaryThemeWithScheme:self.containerScheme]; - ``` - - #### Swift - ```swift - let fab = MDCFloatingButton(shape: `default`) - fab.applySecondaryThemeWith(withScheme:containerScheme) - ``` - +To make use of the theming methods shown in the examples above install the Buttons theming extensions with Cocoapods. First, add the following line to your `Podfile`. + +```bash +pod MaterialComponents/Buttons+Theming +``` + + + +Then Run the installer. + +```bash +pod install +``` + +Next, import the Buttons theming target. + + +#### Swift +```swift +import MaterialComponents.MaterialButtons +import MaterialComponents.MaterialButtons_Theming +``` + +#### Objective-C +```objc +#import "MaterialButtons.h" +#import +``` + + +From there, pass a container scheme into the following theming method on an MDCFloatingButton instance. + + +#### Swift +```swift +let fab = MDCFloatingButton(shape: `default`) +fab.applySecondaryThemeWith(withScheme:containerScheme) +``` + +#### Objective-C +```objc +MDCFloatingButton *fab = + [MDCFloatingButton floatingButtonWithShape:MDCFloatingButtonShapeMini]; +[fab applySecondaryThemeWithScheme:self.containerScheme]; +``` + diff --git a/docs/supporting-shapes/README.md b/docs/supporting-shapes/README.md index 0c477938f6e..b845349048f 100644 --- a/docs/supporting-shapes/README.md +++ b/docs/supporting-shapes/README.md @@ -121,7 +121,7 @@ Now that our component can come in all different sizes and shapes, you will need If the component already supports shapes, it then already has an accessible id shapeGenerator property. In that case you only need to set the shapeGenerator to a shape of your choice and the component will be contained in that shape. There are available examples here: - * Shaped Buttons + * Shaped Buttons * Shaped Cards ### Examples From f5841c6d2fe269f4b3cadde869e1db44d1ca49cc Mon Sep 17 00:00:00 2001 From: Andrew Overton Date: Thu, 9 Apr 2020 15:52:44 -0700 Subject: [PATCH 10/31] I got this build error when building on the latest Xcode: ``` "MDCFloatingButtonModeAnimator.m:150:9: Block implicitly retains 'self'; explicitly mention 'self' to indicate this is intended behavior" ``` This change adds the explicit self. If I got this other people might. PiperOrigin-RevId: 305777188 --- components/Buttons/src/private/MDCFloatingButtonModeAnimator.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Buttons/src/private/MDCFloatingButtonModeAnimator.m b/components/Buttons/src/private/MDCFloatingButtonModeAnimator.m index 52ccae0e6d1..13e4d73f734 100644 --- a/components/Buttons/src/private/MDCFloatingButtonModeAnimator.m +++ b/components/Buttons/src/private/MDCFloatingButtonModeAnimator.m @@ -147,7 +147,7 @@ - (void)modeDidChange:(MDCFloatingButtonMode)mode if (titleLabelCleanup) { titleLabelCleanup(finished); } - _titleLabelContainerView.clipsToBounds = NO; + self.titleLabelContainerView.clipsToBounds = NO; if (completion) { completion(finished); } From 579370c4d6ae72931c83dcf32f6a62ef47473464 Mon Sep 17 00:00:00 2001 From: Nobody Date: Thu, 9 Apr 2020 16:41:26 -0700 Subject: [PATCH 11/31] [Dialogs] Resolve issue with sizing a dialog's accessoryView. PiperOrigin-RevId: 305786532 --- components/Dialogs/src/private/MDCAlertControllerView+Private.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Dialogs/src/private/MDCAlertControllerView+Private.m b/components/Dialogs/src/private/MDCAlertControllerView+Private.m index 24f34165640..76e7b06305e 100644 --- a/components/Dialogs/src/private/MDCAlertControllerView+Private.m +++ b/components/Dialogs/src/private/MDCAlertControllerView+Private.m @@ -581,7 +581,7 @@ - (CGSize)calculateContentSizeThatFitsWidth:(CGFloat)boundingWidth { CGFloat contentWidth = MAX(titleWidth + titleInsets, maxWidth + contentInsets); CGFloat totalElementsHeight = messageSize.height + accessoryViewSize.height; - CGFloat contentHeight = (fabs(maxWidth) <= FLT_EPSILON) + CGFloat contentHeight = (fabs(contentWidth) <= FLT_EPSILON) || totalElementsHeight <= FLT_EPSILON ? 0.0f : totalElementsHeight + [self accessoryVerticalInset] + self.contentInsets.bottom + [self contentInsetTop]; From 969f6f55460312e5f16236ad0e06c3e725f1fb74 Mon Sep 17 00:00:00 2001 From: Galia Kaufman Date: Thu, 9 Apr 2020 19:18:17 -0700 Subject: [PATCH 12/31] [Dialogs] Adding accessory view tests Adding tests for various views that may be used as a Dialogs accessory view. The textfield test is not showing a textfield because of issue b/153181102. PiperOrigin-RevId: 305808160 --- .../MDCAlertControllerAccessoryTests.m | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 components/Dialogs/tests/snapshot/MDCAlertControllerAccessoryTests.m diff --git a/components/Dialogs/tests/snapshot/MDCAlertControllerAccessoryTests.m b/components/Dialogs/tests/snapshot/MDCAlertControllerAccessoryTests.m new file mode 100644 index 00000000000..8034dfef0fa --- /dev/null +++ b/components/Dialogs/tests/snapshot/MDCAlertControllerAccessoryTests.m @@ -0,0 +1,161 @@ +// 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 + +#import "MaterialSnapshot.h" + +#import "MaterialDialogs.h" +#import "MaterialDialogs+Theming.h" +#import "MDCAlertControllerView+Private.h" +#import "MaterialTextFields.h" +#import "MaterialContainerScheme.h" + +static NSString *const kCellId = @"cellId"; + +@interface MDCAlertControllerAccessoryTests : MDCSnapshotTestCase +@property(nonatomic, strong) MDCAlertController *alert; +@property(nonatomic, strong) MDCContainerScheme *containerScheme; +@end + +@implementation MDCAlertControllerAccessoryTests + +- (void)setUp { + [super setUp]; + + // Uncomment below to recreate all the goldens (or add the following line to the specific + // test you wish to recreate the golden for). + // self.recordMode = YES; + + self.alert = [MDCAlertController alertControllerWithTitle:@"Title" message:nil]; + [self addOutlinedActionWithTitle:@"OK"]; + + self.alert.view.bounds = CGRectMake(0.f, 0.f, 300.f, 300.f); + + self.containerScheme = [[MDCContainerScheme alloc] init]; + self.containerScheme.colorScheme = + [[MDCSemanticColorScheme alloc] initWithDefaults:MDCColorSchemeDefaultsMaterial201907]; + self.containerScheme.typographyScheme = + [[MDCTypographyScheme alloc] initWithDefaults:MDCTypographySchemeDefaultsMaterial201902]; +} + +- (void)tearDown { + self.alert = nil; + self.containerScheme = nil; + + [super tearDown]; +} + +#pragma mark - Helpers + +- (void)sizeAlertToFitContentForAlert:(MDCAlertController *)alert { + // Ensure snapshot view size resembles actual runtime size of the alert. This is the closest + // simulation to how an actual dialog will be sized on a screen. The dialog layouts itself with + // final size when calculatePreferredContentSizeForBounds: is called - after all the dialog + // configuration is complete. + MDCAlertControllerView *alertView = (MDCAlertControllerView *)alert.view; + CGSize bounds = [alertView calculatePreferredContentSizeForBounds:alertView.bounds.size]; + alertView.bounds = CGRectMake(0.f, 0.f, bounds.width, bounds.height); +} + +- (void)addOutlinedActionWithTitle:(NSString *)actionTitle { + [self.alert addAction:[MDCAlertAction actionWithTitle:actionTitle + emphasis:MDCActionEmphasisMedium + handler:nil]]; +} + +- (void)generateSizedSnapshotAndVerifyForAlert:(MDCAlertController *)alert { + [self sizeAlertToFitContentForAlert:alert]; + [self highlightSectionsForAlert:self.alert]; + [self generateSnapshotAndVerifyForView:self.alert.view]; +} + +- (void)generateSnapshotAndVerifyForView:(UIView *)view { + [view layoutIfNeeded]; + + UIView *snapshotView = [view mdc_addToBackgroundView]; + [self snapshotVerifyView:snapshotView]; +} + +- (void)changeToRTL:(MDCAlertController *)alert { + [self changeViewToRTL:alert.view]; +} + +- (void)highlightSectionsForAlert:(MDCAlertController *)alert { + MDCAlertControllerView *alertView = (MDCAlertControllerView *)alert.view; + alertView.titleScrollView.backgroundColor = [[UIColor purpleColor] colorWithAlphaComponent:.2f]; + alertView.titleLabel.backgroundColor = [[UIColor purpleColor] colorWithAlphaComponent:.2f]; + alertView.contentScrollView.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:.1f]; + alertView.messageLabel.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:.2f]; + alertView.actionsScrollView.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:.2f]; +} + +#pragma mark - Tests + +- (void)testAlertHasTextFieldAccessory { + // Given + MDCTextField *textField = [[MDCTextField alloc] init]; + textField.placeholder = @"A TextField with a placeholder."; + [self.alert applyThemeWithScheme:self.containerScheme]; + + // When + self.alert.accessoryView = textField; + + // Then + [self generateSizedSnapshotAndVerifyForAlert:self.alert]; +} + +- (void)testAlertHasCollectionViewAccessory { + // Given + // Given + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + CGRect frame = CGRectMake(0.0f, 0.0f, 320.0f, 160.0f); + layout.itemSize = CGSizeMake(frame.size.width, frame.size.height / 4.0f); + layout.minimumLineSpacing = 0.0f; + UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame + collectionViewLayout:layout]; + collectionView.dataSource = self; + [collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:kCellId]; + collectionView.backgroundColor = [UIColor whiteColor]; + [self.alert applyThemeWithScheme:self.containerScheme]; + [collectionView reloadData]; + + // When + self.alert.accessoryView = collectionView; + + // Then + [self generateSizedSnapshotAndVerifyForAlert:self.alert]; +} + +#pragma mark - + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { + return 1; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView + numberOfItemsInSection:(NSInteger)section { + return 4; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView + cellForItemAtIndexPath:(NSIndexPath *)indexPath { + UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCellId + forIndexPath:indexPath]; + [cell setBackgroundColor:[[UIColor purpleColor] + colorWithAlphaComponent:0.1f * (indexPath.item + 1)]]; + return cell; +} + +@end From 1cf770cc40f1b29f1f89b0a3ed03f5f8899e4647 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 10 Apr 2020 04:12:14 -0700 Subject: [PATCH 13/31] [BottomSheet] Deprecate the ShapeThemer. PiperOrigin-RevId: 305856422 --- .../ShapeThemer/MDCBottomSheetControllerShapeThemer.h | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/components/BottomSheet/src/ShapeThemer/MDCBottomSheetControllerShapeThemer.h b/components/BottomSheet/src/ShapeThemer/MDCBottomSheetControllerShapeThemer.h index b010800612e..eaf20a10b9d 100644 --- a/components/BottomSheet/src/ShapeThemer/MDCBottomSheetControllerShapeThemer.h +++ b/components/BottomSheet/src/ShapeThemer/MDCBottomSheetControllerShapeThemer.h @@ -26,10 +26,8 @@ Track progress here: https://github.com/material-components/material-components-ios/issues/7172 Learn more at docs/theming.md#migration-guide-themers-to-theming-extensions */ -@interface MDCBottomSheetControllerShapeThemer : NSObject -@end - -@interface MDCBottomSheetControllerShapeThemer (ToBeDeprecated) +__deprecated_msg("There is no replacement for this API yet.") + @interface MDCBottomSheetControllerShapeThemer : NSObject /** Applies a shape scheme's properties to an MDCBottomSheetController. @@ -42,6 +40,7 @@ Learn more at docs/theming.md#migration-guide-themers-to-theming-extensions */ + (void)applyShapeScheme:(nonnull id)shapeScheme - toBottomSheetController:(nonnull MDCBottomSheetController *)bottomSheetController; + toBottomSheetController:(nonnull MDCBottomSheetController *)bottomSheetController + __deprecated_msg("There is no replacement for this API yet."); @end From f0747fdac58b97c1252e0c985e48913dce129c22 Mon Sep 17 00:00:00 2001 From: Galia Kaufman Date: Fri, 10 Apr 2020 04:25:15 -0700 Subject: [PATCH 14/31] [Dialogs] Add a Testing target Adding a public API that can be used to correctly size dialogs in unit and snapshot tests. Closes #9608, b/147241131 PiperOrigin-RevId: 305857443 --- .../src/Testing/MDCAlertController+Testing.h | 41 ++++++++++++++++ .../src/Testing/MDCAlertController+Testing.m | 47 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 components/Dialogs/src/Testing/MDCAlertController+Testing.h create mode 100644 components/Dialogs/src/Testing/MDCAlertController+Testing.m diff --git a/components/Dialogs/src/Testing/MDCAlertController+Testing.h b/components/Dialogs/src/Testing/MDCAlertController+Testing.h new file mode 100644 index 00000000000..cef433470d7 --- /dev/null +++ b/components/Dialogs/src/Testing/MDCAlertController+Testing.h @@ -0,0 +1,41 @@ +// 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 "MaterialDialogs.h" + +/** Dialogs testing utitlies */ +@interface MDCAlertController (Testing) + +/** + Use this method in snapshot or unit tests to determine the size of the alert view based on the + given bounds. + + @example [alert sizeToFitContentInBounds:CGSizeMake(300.0f, 300.0f)]; + + This method calls @c `calculatePreferredContentSizeForBounds:` to determine a size that fits its + content within the given bounds. This resulting size is a close approximation of the actual size + that would be used if the alert was presented on a device or a simulator. + + @note For best results, call this method after completing all alert set up, and before testing or + grabbing a snapshot. + */ +- (void)sizeToFitContentInBounds:(CGSize)bounds; + +/** + A convenience method that allows highlighting different areas of the alert (using background + colors) in snapshot tests. + */ +- (void)highlightAlertPanels; + +@end diff --git a/components/Dialogs/src/Testing/MDCAlertController+Testing.m b/components/Dialogs/src/Testing/MDCAlertController+Testing.m new file mode 100644 index 00000000000..5785a7bb98b --- /dev/null +++ b/components/Dialogs/src/Testing/MDCAlertController+Testing.m @@ -0,0 +1,47 @@ +// 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 "MDCAlertController+Customize.h" +#import "MDCAlertController+Testing.h" +#import "MDCAlertControllerView+Private.h" + +@implementation MDCAlertController (Testing) + +- (void)sizeToFitContentInBounds:(CGSize)bounds { + CGRect viewBounds = self.view.bounds; + viewBounds.size = bounds; + self.view.bounds = viewBounds; + [self sizeToBounds:bounds]; + [self.view layoutIfNeeded]; +} + +- (void)sizeToBounds:(CGSize)bounds { + MDCAlertControllerView *alertView = (MDCAlertControllerView *)self.view; + CGSize preferredSize = [alertView calculatePreferredContentSizeForBounds:bounds]; + alertView.bounds = CGRectMake(0.f, 0.f, preferredSize.width, preferredSize.height); +} + +- (void)highlightAlertPanels { + MDCAlertControllerView *alertView = (MDCAlertControllerView *)self.view; + alertView.titleScrollView.backgroundColor = [[UIColor purpleColor] colorWithAlphaComponent:.2f]; + alertView.titleLabel.backgroundColor = [[UIColor purpleColor] colorWithAlphaComponent:.2f]; + alertView.contentScrollView.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:.1f]; + alertView.messageLabel.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:.2f]; + alertView.actionsScrollView.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:.2f]; + + self.titleIconImageView.backgroundColor = [[UIColor purpleColor] colorWithAlphaComponent:.2f]; + self.titleIconView.backgroundColor = [[UIColor purpleColor] colorWithAlphaComponent:.3f]; +} + +@end From 2e19a8aa7e363e49c7d345f76ce4b402bf245b68 Mon Sep 17 00:00:00 2001 From: Galia Kaufman Date: Fri, 10 Apr 2020 05:21:18 -0700 Subject: [PATCH 15/31] [Dialogs] Using Testing target in configuration snapshots. A followup for cl/305814799 - replacing calls to all private APIs in MDCAlertControllerConfigurationsTests.m with the new testing utilities in MDCAlertController+Testing.h Note: Snapshot diffs affecting the width of snapshot are expected for tests that used non-standard sizing logic and were migrated to use the new sizing API. PiperOrigin-RevId: 305861786 --- .../MDCAlertControllerConfigurationsTests.m | 101 +++++++----------- 1 file changed, 37 insertions(+), 64 deletions(-) diff --git a/components/Dialogs/tests/snapshot/MDCAlertControllerConfigurationsTests.m b/components/Dialogs/tests/snapshot/MDCAlertControllerConfigurationsTests.m index 2156d4681d2..13a9414b9fc 100644 --- a/components/Dialogs/tests/snapshot/MDCAlertControllerConfigurationsTests.m +++ b/components/Dialogs/tests/snapshot/MDCAlertControllerConfigurationsTests.m @@ -16,8 +16,8 @@ #import "MDCAlertController+Customize.h" #import "MaterialDialogs.h" +#import "MDCAlertController+Testing.h" #import "MaterialDialogs+Theming.h" -#import "MDCAlertControllerView+Private.h" #import "MaterialContainerScheme.h" static NSString *const kTitleShortLatin = @"Title"; @@ -68,8 +68,6 @@ - (void)setUp { self.alertController = [MDCAlertController alertControllerWithTitle:nil message:nil]; [self addOutlinedActionWithTitle:@"OK"]; - self.alertController.view.bounds = CGRectMake(0.f, 0.f, 300.f, 300.f); - self.titleIcon = [[UIImage mdc_testImageOfSize:CGSizeMake(24.f, 24.f)] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; self.titleImage = [[UIImage mdc_testImageOfSize:CGSizeMake(180.f, 120.f)] @@ -94,16 +92,6 @@ - (void)tearDown { #pragma mark - Helpers -- (void)sizeAlertToFitContent { - // Ensure snapshot view size resembles actual runtime size of the alert. This is the closest - // simulation to how an actual dialog will be sized on a screen. The dialog layouts itself with - // final size when calculatePreferredContentSizeForBounds: is called - after all the dialog - // configuration is complete. - MDCAlertControllerView *alertView = (MDCAlertControllerView *)self.alertController.view; - CGSize bounds = [alertView calculatePreferredContentSizeForBounds:alertView.bounds.size]; - alertView.bounds = CGRectMake(0.f, 0.f, bounds.width, bounds.height); -} - - (void)addOutlinedActionWithTitle:(NSString *)actionTitle { [self.alertController addAction:[MDCAlertAction actionWithTitle:actionTitle emphasis:MDCActionEmphasisMedium @@ -111,20 +99,13 @@ - (void)addOutlinedActionWithTitle:(NSString *)actionTitle { } - (void)generateHighlightedSnapshotAndVerifyForAlert:(MDCAlertController *)alert { - MDCAlertControllerView *alertView = (MDCAlertControllerView *)alert.view; - alertView.titleScrollView.backgroundColor = [[UIColor purpleColor] colorWithAlphaComponent:.2f]; - alert.titleIconImageView.backgroundColor = [[UIColor purpleColor] colorWithAlphaComponent:.2f]; - alert.titleIconView.backgroundColor = [[UIColor purpleColor] colorWithAlphaComponent:.3f]; - alertView.titleLabel.backgroundColor = [[UIColor purpleColor] colorWithAlphaComponent:.2f]; - alertView.contentScrollView.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:.1f]; - alertView.messageLabel.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:.2f]; - alertView.actionsScrollView.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:.2f]; - [self generateSizedSnapshotAndVerifyForView:alertView]; + [alert highlightAlertPanels]; + [self generateSizedSnapshotAndVerifyForAlert:alert]; } -- (void)generateSizedSnapshotAndVerifyForView:(UIView *)view { - [self sizeAlertToFitContent]; - [self generateSnapshotAndVerifyForView:view]; +- (void)generateSizedSnapshotAndVerifyForAlert:(MDCAlertController *)alert { + [alert sizeToFitContentInBounds:CGSizeMake(300.0f, 300.0f)]; + [self generateSnapshotAndVerifyForView:alert.view]; } - (void)generateSnapshotAndVerifyForView:(UIView *)view { @@ -182,7 +163,7 @@ - (void)testAlertHasTitle { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // title-icon + actions @@ -194,7 +175,7 @@ - (void)testAlertHasTitleIcon { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // title-image + actions @@ -207,7 +188,7 @@ - (void)testAlertHasTitleImage { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // message + actions @@ -219,7 +200,7 @@ - (void)testAlertHasMessage { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // accessory-view + actions @@ -232,7 +213,7 @@ - (void)testAlertHasAccessoryView { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // title-icon + message + actions @@ -246,7 +227,7 @@ - (void)testAlertHasTitleIconAndMessage { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // title-image + message + actions @@ -260,7 +241,7 @@ - (void)testAlertHasTitleImageAndMessage { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // title-icon + accessory-view + actions @@ -273,7 +254,7 @@ - (void)testAlertHasTitleIconAndAccessoryView { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // title-image + accessory-view + actions @@ -287,7 +268,7 @@ - (void)testAlertHasTitleImageAndAccessoryView { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // message + accessory-view + actions @@ -300,7 +281,7 @@ - (void)testAlertHasMessageAndAccessoryView { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // title + accessory-view + actions @@ -314,7 +295,7 @@ - (void)testAlertHasTitleAndAccessoryView { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // title + title-icon + accessory-view + actions @@ -328,7 +309,7 @@ - (void)testAlertHasTitleAndTitleIconAndAccessoryView { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // title + title-image + accessory-view + actions @@ -344,7 +325,7 @@ - (void)testAlertHasTitleAndTitleImageAndAccessoryView { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // title + title-icon + message + accessory-view + actions @@ -360,7 +341,7 @@ - (void)testAlertHasTitleAndTitleIconAndMessageAndAccessoryView { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // title + title-image + message + accessory-view + actions @@ -375,7 +356,7 @@ - (void)testAlertHasTitleAndTitleImageAndMessageAndAccessoryView { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // title-image + vertical actions @@ -407,10 +388,8 @@ - (void)testAlertHasTitleIconAndMessageAndAccessoryViewAndVerticalButtons { // Then // Ensure enough vertical space for all buttons before layout, then size to fit content. - self.alertController.view.bounds = CGRectMake(0.f, 0.f, 300.f, 500.f); - [self.alertController.view layoutIfNeeded]; - [self sizeAlertToFitContent]; - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self.alertController sizeToFitContentInBounds:CGSizeMake(300.0f, 500.0f)]; + [self generateSnapshotAndVerifyForView:self.alertController.view]; } // accessory-view + actions in RTL @@ -423,7 +402,7 @@ - (void)testAlertHasAccessoryViewInRTL { [self changeToRTL:self.alertController]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // title + title-icon + message + accessory-view + actions in RTL @@ -440,7 +419,7 @@ - (void)testAlertHasTitleAndTitleIconAndMessageAndAccessoryViewInRTL { [self changeToRTL:self.alertController]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } #pragma mark - Use Cases Tests @@ -458,7 +437,7 @@ - (void)testAlertHasAccessoryViewAndCustomInsets { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } #pragma mark - Message Alignment Tests @@ -468,7 +447,7 @@ - (void)testMessageDefaultAlignmentIsNatural { // Given self.alertController.message = kMessageLongLatin; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // message alignment: center @@ -481,7 +460,7 @@ - (void)testMessageAlignmentIsCentered { self.alertController.messageAlignment = NSTextAlignmentCenter; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // message alignment: natural @@ -509,7 +488,7 @@ - (void)testMessageAlignmentIsNaturalInRTL { [self changeToRTL:self.alertController]; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // message alignment: right @@ -522,7 +501,7 @@ - (void)testMessageAlignmentIsRight { self.alertController.messageAlignment = NSTextAlignmentRight; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // message alignment: right in RTL @@ -595,7 +574,7 @@ - (void)testMessageAlignmentIsJustified { self.alertController.messageAlignment = NSTextAlignmentJustified; // Then - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } #pragma mark - Title Icon View Tests @@ -932,8 +911,7 @@ - (void)testTitleIconAlignmentIsJustifiedAndWideImageResized { // When self.alertController.titleIconAlignment = NSTextAlignmentJustified; // Recalculate layout and adjust the snapshot size to fit the new dialog size. - [self sizeAlertToFitContent]; - [self.alertController.view layoutIfNeeded]; + [self.alertController sizeToFitContentInBounds:CGSizeMake(300.0f, 300.0f)]; // Then [self generateHighlightedSnapshotAndVerifyForAlert:self.alertController]; @@ -949,8 +927,7 @@ - (void)testTitleIconAlignmentIsJustifiedAndSquareImageResized { // When self.alertController.titleIconAlignment = NSTextAlignmentJustified; // Recalculate layout and adjust the snapshot size to fit the new dialog size. - [self sizeAlertToFitContent]; - [self.alertController.view layoutIfNeeded]; + [self.alertController sizeToFitContentInBounds:CGSizeMake(300.0f, 300.0f)]; // Then [self generateHighlightedSnapshotAndVerifyForAlert:self.alertController]; @@ -966,10 +943,8 @@ - (void)testMinSizeDialog { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - self.alertController.view.bounds = CGRectMake(0.f, 0.f, 100.f, 100.f); - [self.alertController.view layoutIfNeeded]; - [self sizeAlertToFitContent]; - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self.alertController sizeToFitContentInBounds:CGSizeMake(100.0f, 100.0f)]; + [self generateSnapshotAndVerifyForView:self.alertController.view]; } // Max size message @@ -983,10 +958,8 @@ - (void)testMaxSizeDialog { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - self.alertController.view.bounds = CGRectMake(0.f, 0.f, 1000.f, 1000.f); - [self.alertController.view layoutIfNeeded]; - [self sizeAlertToFitContent]; - [self generateSizedSnapshotAndVerifyForView:self.alertController.view]; + [self.alertController sizeToFitContentInBounds:CGSizeMake(1000.0f, 1000.0f)]; + [self generateSnapshotAndVerifyForView:self.alertController.view]; } @end From 9c32878a48bc074e315f1edada3dd06b5302059e Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 10 Apr 2020 05:23:26 -0700 Subject: [PATCH 16/31] [Dialogs] Disable broken test. b/153457451 tracking fix of the test. PiperOrigin-RevId: 305861929 --- .../tests/unit/MDCAlertControllerInsetsTests.m | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/components/Dialogs/tests/unit/MDCAlertControllerInsetsTests.m b/components/Dialogs/tests/unit/MDCAlertControllerInsetsTests.m index 9ff863c063d..8a987ce8df8 100644 --- a/components/Dialogs/tests/unit/MDCAlertControllerInsetsTests.m +++ b/components/Dialogs/tests/unit/MDCAlertControllerInsetsTests.m @@ -127,7 +127,18 @@ - (void)testAlertFramesAdjustsToTitleInsets { CGRectMake(10.0f, 10.0f, titleRect.size.width - 20.f, titleRect.size.height - 20.f))); } -- (void)testAlertFramesAdjustsToContentInsets { +// TODO(b/153457451): Re-enable this test. +// +// This test is failing with the following error: +// +// components/Dialogs/tests/unit/MDCAlertControllerInsetsTests.m:144: error: +// -[MDCAlertControllerInsetsTests testAlertFramesAdjustsToContentInsets] : +// ((CGRectEqualToRect( self.alertView.messageLabel.frame, +// CGRectMake(10.0f, 0.0f, contentRect.size.width - 20.f, +// contentRect.size.height - 10.f))) is true) failed +// Test Case '-[MDCAlertControllerInsetsTests testAlertFramesAdjustsToContentInsets]' +// failed (0.115 seconds). +- (void)disabled_testAlertFramesAdjustsToContentInsets { // Given CGSize size = [self.alertView calculatePreferredContentSizeForBounds:self.alertView.bounds.size]; self.alertView.bounds = CGRectMake(0.f, 0.f, size.width, size.height); From 7346de3ce076b426cfc6f0e877ac457c255fd700 Mon Sep 17 00:00:00 2001 From: Galia Kaufman Date: Fri, 10 Apr 2020 05:43:39 -0700 Subject: [PATCH 17/31] [Dialogs] Using the new Testing target in insets unit tests. A followup for cl/305814799 - replacing calls to all private APIs in MDCAlertControllerInsetsTests.m with the new testing utilities in MDCAlertController+Testing.h PiperOrigin-RevId: 305863373 --- .../unit/MDCAlertControllerInsetsTests.m | 32 ++++--------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/components/Dialogs/tests/unit/MDCAlertControllerInsetsTests.m b/components/Dialogs/tests/unit/MDCAlertControllerInsetsTests.m index 8a987ce8df8..2897b86f0b8 100644 --- a/components/Dialogs/tests/unit/MDCAlertControllerInsetsTests.m +++ b/components/Dialogs/tests/unit/MDCAlertControllerInsetsTests.m @@ -15,6 +15,7 @@ #import "MaterialDialogs.h" #import "MDCAlertController+ButtonForAction.h" +#import "MDCAlertController+Testing.h" #import "MDCAlertControllerView+Private.h" #import @@ -64,9 +65,7 @@ - (void)tearDown { - (void)testAlertBottomInsetMatchesDefaultInsets { // Given - CGSize size = [self.alertView calculatePreferredContentSizeForBounds:self.alertView.bounds.size]; - self.alertView.bounds = CGRectMake(0.f, 0.f, size.width, size.height); - [self.alertView layoutIfNeeded]; + [self.alert sizeToFitContentInBounds:CGSizeMake(300.0f, 300.0f)]; // Then XCTAssertEqual(self.alertView.titleInsets.bottom, 20.f); @@ -76,14 +75,9 @@ - (void)testAlertBottomInsetMatchesDefaultInsets { } - (void)testAlertBottomInsetMatchesCustomInsets { - // Given - [self.alertView layoutIfNeeded]; - // When self.alertView.titleInsets = UIEdgeInsetsMake(14.f, 14.f, 14.f, 14.f); - CGSize size = [self.alertView calculatePreferredContentSizeForBounds:self.alertView.bounds.size]; - self.alertView.bounds = CGRectMake(0.f, 0.f, size.width, size.height); - [self.alertView layoutIfNeeded]; + [self.alert sizeToFitContentInBounds:CGSizeMake(300.0f, 300.0f)]; // Then XCTAssertEqual(self.alertView.titleInsets.bottom, 14.f); @@ -94,13 +88,11 @@ - (void)testAlertBottomInsetMatchesCustomInsets { - (void)testAlertFramesAdjustsToTitleIconInsets { // Given - CGSize size = [self.alertView calculatePreferredContentSizeForBounds:self.alertView.bounds.size]; - self.alertView.bounds = CGRectMake(0.f, 0.f, size.width, size.height); self.alert.titleIcon = TestImage(CGSizeMake(24, 24)); // When self.alertView.titleIconInsets = UIEdgeInsetsMake(10.0f, 10.0f, 10.0f, 10.0f); - [self.alertView layoutIfNeeded]; + [self.alert sizeToFitContentInBounds:CGSizeMake(300.0f, 300.0f)]; // Then CGSize iconSize = self.alert.titleIcon.size; @@ -111,13 +103,9 @@ - (void)testAlertFramesAdjustsToTitleIconInsets { } - (void)testAlertFramesAdjustsToTitleInsets { - // Given - CGSize size = [self.alertView calculatePreferredContentSizeForBounds:self.alertView.bounds.size]; - self.alertView.bounds = CGRectMake(0.f, 0.f, size.width, size.height); - // When self.alertView.titleInsets = UIEdgeInsetsMake(10.0f, 10.0f, 10.0f, 10.0f); - [self.alertView layoutIfNeeded]; + [self.alert sizeToFitContentInBounds:CGSizeMake(300.0f, 300.0f)]; // Then CGRect titleRect = self.alertView.titleScrollView.frame; @@ -139,13 +127,9 @@ - (void)testAlertFramesAdjustsToTitleInsets { // Test Case '-[MDCAlertControllerInsetsTests testAlertFramesAdjustsToContentInsets]' // failed (0.115 seconds). - (void)disabled_testAlertFramesAdjustsToContentInsets { - // Given - CGSize size = [self.alertView calculatePreferredContentSizeForBounds:self.alertView.bounds.size]; - self.alertView.bounds = CGRectMake(0.f, 0.f, size.width, size.height); - // When self.alertView.contentInsets = UIEdgeInsetsMake(10.0f, 10.0f, 10.0f, 10.0f); - [self.alertView layoutIfNeeded]; + [self.alert sizeToFitContentInBounds:CGSizeMake(300.0f, 300.0f)]; // Then CGRect contentRect = self.alertView.contentScrollView.frame; @@ -157,13 +141,11 @@ - (void)disabled_testAlertFramesAdjustsToContentInsets { - (void)testAlertFramesAdjustsToActionsInsets { // Given - CGSize size = [self.alertView calculatePreferredContentSizeForBounds:self.alertView.bounds.size]; - self.alertView.bounds = CGRectMake(0.f, 0.f, size.width, size.height); MDCButton *button = [self.alert buttonForAction:self.alertView.actionManager.actions.firstObject]; // When self.alertView.actionsInsets = UIEdgeInsetsMake(10.0f, 10.0f, 10.0f, 10.0f); - [self.alertView layoutIfNeeded]; + [self.alert sizeToFitContentInBounds:CGSizeMake(300.0f, 300.0f)]; // Then CGRect contentRect = self.alertView.actionsScrollView.frame; From 666ed14ba0cd16bb4914f0cbbad8f022b1e7ac15 Mon Sep 17 00:00:00 2001 From: Galia Kaufman Date: Fri, 10 Apr 2020 05:58:29 -0700 Subject: [PATCH 18/31] [Dialogs] Using the new Testing target in insets snapshots. A followup for cl/305814799 - replacing calls to all private APIs in MDCAlertControllerInsetsTests.m with the new testing utilities in MDCAlertController+Testing.h PiperOrigin-RevId: 305864413 --- .../snapshot/MDCAlertControllerInsetsTests.m | 65 +++++++++---------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/components/Dialogs/tests/snapshot/MDCAlertControllerInsetsTests.m b/components/Dialogs/tests/snapshot/MDCAlertControllerInsetsTests.m index 29ccadf21cd..d53aa48d755 100644 --- a/components/Dialogs/tests/snapshot/MDCAlertControllerInsetsTests.m +++ b/components/Dialogs/tests/snapshot/MDCAlertControllerInsetsTests.m @@ -14,10 +14,11 @@ #import "MaterialSnapshot.h" +#import "MaterialDialogs.h" +#import "MDCAlertController+Testing.h" +#import "MaterialDialogs+Theming.h" #import "MDCAlertControllerView+Private.h" #import "MaterialContainerScheme.h" -#import "MaterialDialogs+Theming.h" -#import "MaterialDialogs.h" static NSString *const kTitleShortLatin = @"Short Title"; static NSString *const kTitleLongLatin = @"Lorem ipsum dolor sit amet"; @@ -35,8 +36,6 @@ @interface MDCAlertControllerInsetsTests : MDCSnapshotTestCase @property(nonatomic, assign) CGFloat alertWidth; @end -// TODO: Test RTL using: [self changeToRTL:self.alertController]; - @implementation MDCAlertControllerInsetsTests - (void)setUp { @@ -50,7 +49,6 @@ - (void)setUp { message:kMessageMediumLatin]; self.alertView = (MDCAlertControllerView *)self.alertController.view; self.alertWidth = 300.f; - self.alertView.bounds = CGRectMake(0, 0, self.alertWidth, self.alertWidth); [self addOKAction]; [self addCancelAction]; @@ -78,10 +76,12 @@ - (void)tearDown { [super tearDown]; } +- (void)generateSizedSnapshotAndVerifyForAlert:(MDCAlertController *)alert { + [alert sizeToFitContentInBounds:CGSizeMake(self.alertWidth, self.alertWidth)]; + [self generateSnapshotAndVerifyForView:alert.view]; +} + - (void)generateSnapshotAndVerifyForView:(UIView *)view { - CGRect bounds = self.alertView.bounds; - bounds.size = [self.alertView calculatePreferredContentSizeForBounds:bounds.size]; - self.alertView.bounds = CGRectMake(0.f, 0.f, bounds.size.width, bounds.size.height); [self setElementsBackgroundColors]; [view layoutIfNeeded]; @@ -99,7 +99,7 @@ - (void)testDefaultAlertHasDefaultInsets { // Given [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testDefaultAlertTitleImageHasDefaultInsets { @@ -111,7 +111,7 @@ - (void)testDefaultAlertTitleImageHasDefaultInsets { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testDefaultAlertNoContentHasDefaultInsets { @@ -120,7 +120,7 @@ - (void)testDefaultAlertNoContentHasDefaultInsets { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testDefaultAlertNoTitleHasDefaultInsets { @@ -129,7 +129,7 @@ - (void)testDefaultAlertNoTitleHasDefaultInsets { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testDefaultAlertAccessoryHasDefaultInsets { @@ -139,7 +139,7 @@ - (void)testDefaultAlertAccessoryHasDefaultInsets { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testDefaultAlertContentAndAccessoryHaveDefaultInsets { @@ -148,19 +148,16 @@ - (void)testDefaultAlertContentAndAccessoryHaveDefaultInsets { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } #pragma mark - Custom insets -// Attempting to reproduce issue in cl/300827008 - -// https://drive.google.com/file/d/1w4wrrSMbG3E3C9qwfMfMZL_RHDbXkyjq/view - (void)testAlertTitleImageHasNoInsets { // Given self.alertController = [MDCAlertController alertControllerWithTitle:kTitleShortLatin message:kMessageMediumLatin]; self.alertView = (MDCAlertControllerView *)self.alertController.view; - self.alertView.bounds = CGRectMake(0, 0, self.alertWidth, self.alertWidth); [self addActionWithTitle:@"Extra Long Action Label"]; [self addActionWithTitle:@"Another Long Action Label"]; @@ -175,7 +172,7 @@ - (void)testAlertTitleImageHasNoInsets { self.alertView.titleIconInsets = UIEdgeInsetsMake(0.f, 0.f, 20.f, 0.f); // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testAlertHasCustomInsets { @@ -189,7 +186,7 @@ - (void)testAlertHasCustomInsets { self.alertView.actionsInsets = UIEdgeInsetsMake(10.f, 10.f, 10.f, 10.f); // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } #pragma mark - Custom title view insets @@ -202,7 +199,7 @@ - (void)testAlertTitleHasCustomInsets { self.alertView.titleInsets = UIEdgeInsetsMake(12.f, 12.f, 12.f, 12.f); // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testAlertTitleIconHasDefaultInsets { @@ -210,7 +207,7 @@ - (void)testAlertTitleIconHasDefaultInsets { self.alertController.titleIcon = self.titleIcon; [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testAlertTitleIconHasCustomInsets { @@ -222,7 +219,7 @@ - (void)testAlertTitleIconHasCustomInsets { self.alertView.titleIconInsets = UIEdgeInsetsMake(12.f, 12.f, 20.f, 12.f); // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testAlertTitleIconTitleZeroInsets { @@ -235,7 +232,7 @@ - (void)testAlertTitleIconTitleZeroInsets { self.alertView.titleInsets = UIEdgeInsetsZero; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testAlertTitleIconInsetsOverrideTitleInsets { @@ -248,7 +245,7 @@ - (void)testAlertTitleIconInsetsOverrideTitleInsets { self.alertView.titleInsets = UIEdgeInsetsMake(12.f, 12.f, 12.f, 12.f); // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testAlertTitleImageTitleZeroInsets { @@ -263,7 +260,7 @@ - (void)testAlertTitleImageTitleZeroInsets { self.alertView.titleInsets = UIEdgeInsetsZero; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } #pragma mark - Custom content insets @@ -276,7 +273,7 @@ - (void)testAlertContentHasZeroInsets { self.alertView.contentInsets = UIEdgeInsetsZero; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testAlertContentHasCustomInsets { @@ -287,7 +284,7 @@ - (void)testAlertContentHasCustomInsets { self.alertView.contentInsets = UIEdgeInsetsMake(30.f, 10.f, 10.f, 10.f); // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testAlertContentAccessoryHaveCustomInsets { @@ -300,7 +297,7 @@ - (void)testAlertContentAccessoryHaveCustomInsets { self.alertView.accessoryViewVerticalInset = 0.f; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } #pragma mark - Custom actions insets @@ -314,7 +311,7 @@ - (void)testAlertActionsHaveZeroInsets { self.alertView.actionsHorizontalMargin = 0.f; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testAlertActionsHaveCustomInsets { @@ -326,7 +323,7 @@ - (void)testAlertActionsHaveCustomInsets { self.alertView.actionsHorizontalMargin = 20.f; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testAlertActionsHaveCenteredCustomInsets { @@ -339,7 +336,7 @@ - (void)testAlertActionsHaveCenteredCustomInsets { self.alertView.actionsHorizontalMargin = 12.f; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testAlertActionsHaveJustifiedCustomInsets { @@ -354,7 +351,7 @@ - (void)testAlertActionsHaveJustifiedCustomInsets { self.alertView.actionsHorizontalMargin = 20.f; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testAlertActionsHaveVerticalCustomInsets { @@ -368,7 +365,7 @@ - (void)testAlertActionsHaveVerticalCustomInsets { self.alertView.actionsVerticalMargin = 1.f; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } - (void)testAlertActionsHaveJustifiedVerticalCustomInsets { @@ -385,7 +382,7 @@ - (void)testAlertActionsHaveJustifiedVerticalCustomInsets { self.alertView.actionsVerticalMargin = 6.f; // Then - [self generateSnapshotAndVerifyForView:self.alertView]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } #pragma mark - Helpers From e48d79a9cc82c9187a80dcd24d513052554dbb79 Mon Sep 17 00:00:00 2001 From: Galia Kaufman Date: Fri, 10 Apr 2020 06:08:19 -0700 Subject: [PATCH 19/31] [Dialogs] Using the new Testing target in actions snapshots. A followup for cl/305814799 - replacing calls to all private APIs in MDCAlertControllerActionsTests.m with the new testing utilities in MDCAlertController+Testing.h Removing the private header since it's no longer needed for testing. PiperOrigin-RevId: 305865529 --- .../snapshot/MDCAlertControllerActionsTests.m | 113 ++++++++---------- 1 file changed, 48 insertions(+), 65 deletions(-) diff --git a/components/Dialogs/tests/snapshot/MDCAlertControllerActionsTests.m b/components/Dialogs/tests/snapshot/MDCAlertControllerActionsTests.m index 9ea741c2c0e..02b6efe0d51 100644 --- a/components/Dialogs/tests/snapshot/MDCAlertControllerActionsTests.m +++ b/components/Dialogs/tests/snapshot/MDCAlertControllerActionsTests.m @@ -15,10 +15,10 @@ #import "MaterialSnapshot.h" #import "MDCAlertController+ButtonForAction.h" -#import "MDCAlertControllerView+Private.h" -#import "MaterialContainerScheme.h" -#import "MaterialDialogs+Theming.h" #import "MaterialDialogs.h" +#import "MDCAlertController+Testing.h" +#import "MaterialDialogs+Theming.h" +#import "MaterialContainerScheme.h" static NSString *const kTitleShortLatin = @"Title"; static NSString *const kMessageShortLatin = @"Message"; @@ -49,7 +49,6 @@ - (void)setUp { self.alertController = [MDCAlertController alertControllerWithTitle:kTitleShortLatin message:kMessageLongLatin]; - self.alertController.view.bounds = CGRectMake(0, 0, 300, 300); self.containerScheme2019 = [[MDCContainerScheme alloc] init]; self.containerScheme2019.colorScheme = @@ -65,19 +64,12 @@ - (void)tearDown { [super tearDown]; } -- (void)sizeAlertToFitContent { - // Ensure snapshot view size resembles actual runtime size of the alert. This is the closest - // simulation to how an actual dialog will be sized on a screen. The dialog layouts itself with - // final size when calculatePreferredContentSizeForBounds: is called - after all the dialog - // configuration is complete. - MDCAlertControllerView *alertView = (MDCAlertControllerView *)self.alertController.view; - CGRect bounds = alertView.bounds; - bounds.size = [alertView calculatePreferredContentSizeForBounds:bounds.size]; - alertView.bounds = CGRectMake(0.f, 0.f, bounds.size.width, bounds.size.height); +- (void)generateSizedSnapshotAndVerifyForAlert:(MDCAlertController *)alert { + [alert sizeToFitContentInBounds:CGSizeMake(300.0f, 300.0f)]; + [self generateSnapshotAndVerifyForView:alert.view]; } - (void)generateSnapshotAndVerifyForView:(UIView *)view { - [self sizeAlertToFitContent]; [view layoutIfNeeded]; UIView *snapshotView = [view mdc_addToBackgroundView]; @@ -102,7 +94,7 @@ - (void)testLowEmphasisActionsOrderInHorizontalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Vertical Layout | Low Emphasis | Default Alignment (center) @@ -117,7 +109,7 @@ - (void)testAutomaticChangeToVerticalLayoutForLongLowEmphasisActions { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Horizontal Layout | High Emphasis | Default Alignment (trailing) @@ -132,7 +124,7 @@ - (void)testMediumEmphasisActionsOrderInHorizontalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Vertical Layout | High Emphasis | Default Alignment (center) @@ -147,7 +139,7 @@ - (void)testAutomaticChangeToVerticalLayoutForLongMediumEmphasisActions { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Horizontal Layout | Low Emphasis | RTL | Default Alignment (trailing) @@ -163,7 +155,7 @@ - (void)testLowEmphasisActionsOrderInHorizontalLayoutInRTL { [self changeToRTL:self.alertController]; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Vertical Layout | Low Emphasis | RTL | Default Alignment (center) @@ -179,7 +171,7 @@ - (void)testAutomaticChangeToVerticalLayoutForLongLowEmphasisActionsInRTL { [self changeToRTL:self.alertController]; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Horizontal Layout | High Emphasis | RTL | Default Alignment (trailing) @@ -195,7 +187,7 @@ - (void)testMediumEmphasisActionsOrderInHorizontalLayoutInRTL { [self changeToRTL:self.alertController]; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Vertical Layout | High Emphasis | RTL | Default Alignment (center) @@ -211,7 +203,7 @@ - (void)testAutomaticChangeToVerticalLayoutForLongMediumEmphasisActionsInRTL { [self changeToRTL:self.alertController]; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Verify correct layout for issues reported in: @@ -234,9 +226,7 @@ - (void)testActionsLayoutHorizontallyForCapitalizedButtonCase { // Then // An extra wide view (generated using CGRectInfinite.size) is required for this test case. - MDCAlertControllerView *alertView = (MDCAlertControllerView *)self.alertController.view; - CGSize bounds = [alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size]; - alertView.bounds = CGRectMake(0.f, 0.f, bounds.width, bounds.height); + [self.alertController sizeToFitContentInBounds:CGRectInfinite.size]; [self generateSnapshotAndVerifyForView:self.alertController.view]; } @@ -251,7 +241,7 @@ - (void)testActionsLayoutHorizontallyForExtraLongButtons { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } #pragma mark - Alignment Tests @@ -264,11 +254,10 @@ - (void)testLowEmphasisActionsAreCenteredInHorizontalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // When - MDCAlertControllerView *view = (MDCAlertControllerView *)self.alertController.view; - view.actionsHorizontalAlignment = MDCContentHorizontalAlignmentCenter; + self.alertController.actionsHorizontalAlignment = MDCContentHorizontalAlignmentCenter; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Horizontal Layout | Low Emphasis | Leading Alignment @@ -279,11 +268,10 @@ - (void)testLowEmphasisActionsAreLeadingInHorizontalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // When - MDCAlertControllerView *view = (MDCAlertControllerView *)self.alertController.view; - view.actionsHorizontalAlignment = MDCContentHorizontalAlignmentLeading; + self.alertController.actionsHorizontalAlignment = MDCContentHorizontalAlignmentLeading; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Horizontal Layout | Low Emphasis | Justified Alignment @@ -294,11 +282,10 @@ - (void)testLowEmphasisActionsAreJustifiedInHorizontalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // When - MDCAlertControllerView *view = (MDCAlertControllerView *)self.alertController.view; - view.actionsHorizontalAlignment = MDCContentHorizontalAlignmentJustified; + self.alertController.actionsHorizontalAlignment = MDCContentHorizontalAlignmentJustified; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Horizontal Layout | Medium Emphasis | Center Alignment @@ -309,11 +296,10 @@ - (void)testMediumEmphasisActionsAreCenteredInHorizontalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // When - MDCAlertControllerView *view = (MDCAlertControllerView *)self.alertController.view; - view.actionsHorizontalAlignment = MDCContentHorizontalAlignmentCenter; + self.alertController.actionsHorizontalAlignment = MDCContentHorizontalAlignmentCenter; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Horizontal Layout | Medium Emphasis | Leading Alignment @@ -324,11 +310,10 @@ - (void)testMediumEmphasisActionsAreLeadingInHorizontalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // When - MDCAlertControllerView *view = (MDCAlertControllerView *)self.alertController.view; - view.actionsHorizontalAlignment = MDCContentHorizontalAlignmentLeading; + self.alertController.actionsHorizontalAlignment = MDCContentHorizontalAlignmentLeading; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Horizontal Layout | Medium Emphasis | Justified Alignment @@ -339,11 +324,10 @@ - (void)testMediumEmphasisActionsAreJustifiedInHorizontalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // When - MDCAlertControllerView *view = (MDCAlertControllerView *)self.alertController.view; - view.actionsHorizontalAlignment = MDCContentHorizontalAlignmentJustified; + self.alertController.actionsHorizontalAlignment = MDCContentHorizontalAlignmentJustified; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Vertical Layout | Low Emphasis | Trailing Alignment @@ -354,11 +338,11 @@ - (void)testLowEmphasisActionsAreTrailingInVerticalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // When - MDCAlertControllerView *view = (MDCAlertControllerView *)self.alertController.view; - view.actionsHorizontalAlignmentInVerticalLayout = MDCContentHorizontalAlignmentTrailing; + self.alertController.actionsHorizontalAlignmentInVerticalLayout = + MDCContentHorizontalAlignmentTrailing; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Vertical Layout | Low Emphasis | Leading Alignment @@ -369,11 +353,11 @@ - (void)testLowEmphasisActionsAreLeadingInVerticalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // When - MDCAlertControllerView *view = (MDCAlertControllerView *)self.alertController.view; - view.actionsHorizontalAlignmentInVerticalLayout = MDCContentHorizontalAlignmentLeading; + self.alertController.actionsHorizontalAlignmentInVerticalLayout = + MDCContentHorizontalAlignmentLeading; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Vertical Layout | Low Emphasis | Justified Alignment @@ -384,11 +368,11 @@ - (void)testLowEmphasisActionsAreJustifiedInVerticalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // When - MDCAlertControllerView *view = (MDCAlertControllerView *)self.alertController.view; - view.actionsHorizontalAlignmentInVerticalLayout = MDCContentHorizontalAlignmentJustified; + self.alertController.actionsHorizontalAlignmentInVerticalLayout = + MDCContentHorizontalAlignmentJustified; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Vertical Layout | Medium Emphasis | Trailing Alignment @@ -399,11 +383,11 @@ - (void)testMediumEmphasisActionsAreTrailingInVerticalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // When - MDCAlertControllerView *view = (MDCAlertControllerView *)self.alertController.view; - view.actionsHorizontalAlignmentInVerticalLayout = MDCContentHorizontalAlignmentTrailing; + self.alertController.actionsHorizontalAlignmentInVerticalLayout = + MDCContentHorizontalAlignmentTrailing; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Vertical Layout | Medium Emphasis | Leading Alignment @@ -414,11 +398,11 @@ - (void)testMediumEmphasisActionsAreLeadingInVerticalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // When - MDCAlertControllerView *view = (MDCAlertControllerView *)self.alertController.view; - view.actionsHorizontalAlignmentInVerticalLayout = MDCContentHorizontalAlignmentLeading; + self.alertController.actionsHorizontalAlignmentInVerticalLayout = + MDCContentHorizontalAlignmentLeading; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } // Vertical Layout | Medium Emphasis | Justified Alignment @@ -429,11 +413,11 @@ - (void)testMediumEmphasisActionsAreJustifiedInVerticalLayout { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // When - MDCAlertControllerView *view = (MDCAlertControllerView *)self.alertController.view; - view.actionsHorizontalAlignmentInVerticalLayout = MDCContentHorizontalAlignmentJustified; + self.alertController.actionsHorizontalAlignmentInVerticalLayout = + MDCContentHorizontalAlignmentJustified; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } #pragma mark - Vertical Order Tests @@ -445,11 +429,10 @@ - (void)testVerticalActionsAreOrderedByEmphasis { [self.alertController applyThemeWithScheme:self.containerScheme2019]; // When - MDCAlertControllerView *view = (MDCAlertControllerView *)self.alertController.view; - view.orderVerticalActionsByEmphasis = YES; + self.alertController.orderVerticalActionsByEmphasis = YES; // Then - [self generateSnapshotAndVerifyForView:self.alertController.view]; + [self generateSizedSnapshotAndVerifyForAlert:self.alertController]; } #pragma mark - Helpers From 45671537f593d07164d2d8ec15edd9a651f680a7 Mon Sep 17 00:00:00 2001 From: Wenyu Zhang Date: Fri, 10 Apr 2020 07:14:41 -0700 Subject: [PATCH 20/31] [ActionSheet] Add UIPointerInteraction support for actions.. PiperOrigin-RevId: 305871877 --- .../private/MDCActionSheetItemTableViewCell.m | 27 ++++++++++++++ .../tests/unit/MDCActionSheetTableCellTest.m | 37 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/components/ActionSheet/src/private/MDCActionSheetItemTableViewCell.m b/components/ActionSheet/src/private/MDCActionSheetItemTableViewCell.m index 6c085929db5..07594d7571e 100644 --- a/components/ActionSheet/src/private/MDCActionSheetItemTableViewCell.m +++ b/components/ActionSheet/src/private/MDCActionSheetItemTableViewCell.m @@ -38,6 +38,11 @@ @interface MDCActionSheetItemTableViewCell () @property(nonatomic, strong, nonnull) UIView *divider; @end +#ifdef __IPHONE_13_4 +@interface MDCActionSheetItemTableViewCell (PointerInteractions) +@end +#endif + @implementation MDCActionSheetItemTableViewCell { MDCActionSheetAction *_itemAction; NSLayoutConstraint *_titleLeadingConstraint; @@ -139,6 +144,13 @@ - (void)commonMDCActionSheetItemViewInit { .active = YES; [_actionImageView.widthAnchor constraintEqualToConstant:kImageHeightAndWidth].active = YES; [_actionImageView.heightAnchor constraintEqualToConstant:kImageHeightAndWidth].active = YES; + +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + UIPointerInteraction *pointerInteraction = [[UIPointerInteraction alloc] initWithDelegate:self]; + [self.contentView addInteraction:pointerInteraction]; + } +#endif } - (void)layoutSubviews { @@ -230,4 +242,19 @@ - (void)setImageRenderingMode:(UIImageRenderingMode)imageRenderingMode { [self setNeedsLayout]; } +#pragma mark - UIPointerInteractionDelegate + +#ifdef __IPHONE_13_4 +- (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction + styleForRegion:(UIPointerRegion *)region API_AVAILABLE(ios(13.4)) { + UIPointerStyle *pointerStyle = nil; + if (interaction.view) { + UITargetedPreview *targetedPreview = [[UITargetedPreview alloc] initWithView:interaction.view]; + UIPointerEffect *hoverEffect = [UIPointerHoverEffect effectWithPreview:targetedPreview]; + pointerStyle = [UIPointerStyle styleWithEffect:hoverEffect shape:nil]; + } + return pointerStyle; +} +#endif + @end diff --git a/components/ActionSheet/tests/unit/MDCActionSheetTableCellTest.m b/components/ActionSheet/tests/unit/MDCActionSheetTableCellTest.m index 7e94cffa950..fc708a4e587 100644 --- a/components/ActionSheet/tests/unit/MDCActionSheetTableCellTest.m +++ b/components/ActionSheet/tests/unit/MDCActionSheetTableCellTest.m @@ -336,4 +336,41 @@ - (void)testSetActionSheetItemDividerShownSetsTheDividerShownOnTheCell { XCTAssertTrue(cell.showsDivider); } +- (void)testPointerInteractionsOnTheCellByDefault { +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + // Given + MDCActionSheetItemTableViewCell *cell = + [[MDCActionSheetItemTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault + reuseIdentifier:@"Foo"]; + + // Then + XCTAssertEqual(cell.contentView.interactions.count, 1); + XCTAssertTrue( + [cell.contentView.interactions.firstObject isKindOfClass:[UIPointerInteraction class]]); + } +#endif +} + +- (void)testPointerInteractionsOnTheCellWithActionIsAdded { +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + // Given + MDCActionSheetAction *action = [MDCActionSheetAction actionWithTitle:@"Foo" + image:nil + handler:nil]; + + // When + [self.actionSheet addAction:action]; + + // Then + MDCActionSheetItemTableViewCell *cell = + [MDCActionSheetTestHelper getCellFromActionSheet:self.actionSheet atIndex:0]; + XCTAssertEqual(cell.contentView.interactions.count, 1); + XCTAssertTrue( + [cell.contentView.interactions.firstObject isKindOfClass:[UIPointerInteraction class]]); + } +#endif +} + @end From d36b5e8f3d12b4123032a3a3abc9296984a617a3 Mon Sep 17 00:00:00 2001 From: Wenyu Zhang Date: Fri, 10 Apr 2020 07:15:09 -0700 Subject: [PATCH 21/31] [Dialogs] Add UIPointerInteraction support for buttons.. PiperOrigin-RevId: 305871918 --- .../Dialogs/src/private/MDCAlertActionManager.m | 5 +++++ .../Dialogs/tests/unit/MDCAlertControllerTests.m | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/components/Dialogs/src/private/MDCAlertActionManager.m b/components/Dialogs/src/private/MDCAlertActionManager.m index edc0f8c1c61..41805aaf1b3 100644 --- a/components/Dialogs/src/private/MDCAlertActionManager.m +++ b/components/Dialogs/src/private/MDCAlertActionManager.m @@ -91,6 +91,11 @@ - (MDCButton *)makeButtonForAction:(MDCAlertAction *)action MDCButton *button = [[MDCButton alloc] initWithFrame:CGRectZero]; [button setTitle:action.title forState:UIControlStateNormal]; button.accessibilityIdentifier = action.accessibilityIdentifier; +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + button.pointerInteractionEnabled = YES; + } +#endif [button addTarget:target action:selector forControlEvents:UIControlEventTouchUpInside]; return button; } diff --git a/components/Dialogs/tests/unit/MDCAlertControllerTests.m b/components/Dialogs/tests/unit/MDCAlertControllerTests.m index ed373dc085f..622edb72469 100644 --- a/components/Dialogs/tests/unit/MDCAlertControllerTests.m +++ b/components/Dialogs/tests/unit/MDCAlertControllerTests.m @@ -971,4 +971,20 @@ - (void)testElevationDidChangeBlockNotCalledWhenElevationIsSetWithoutChangingVal XCTAssertFalse(blockCalled); } +- (void)testPointerInteractionsOnAlertControllerWithActionIsAdded { +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + // When + [self.alert addAction:[MDCAlertAction actionWithTitle:@"action1" handler:nil]]; + + // Then + MDCAlertControllerView *view = (MDCAlertControllerView *)self.alert.view; + for (UIButton *button in view.actionManager.buttonsInActionOrder) { + XCTAssertTrue(button.isPointerInteractionEnabled); + XCTAssertEqual(button.interactions.count, 1); + } + } +#endif +} + @end From 5a2b2caae09680e38950dfb4c81c2c14f7a57359 Mon Sep 17 00:00:00 2001 From: Bryan Oltman Date: Fri, 10 Apr 2020 10:21:29 -0400 Subject: [PATCH 22/31] [Button] Add custom UIButtonPointerStyleProvider to MDCButton for better iPadOS pointer interaction. Note: MDCButton does not enable pointer interactivity by default. This also adds an example to demonstrate MDCButtons with the default UIPointerInteraction enabled. The example exposes an issue with MDCFloatButton's collapse/expand animations (b/153666859), which will be addressed in a future CL. PiperOrigin-RevId: 305872547 --- .../ButtonsPointerInteractionExample.swift | 136 ++++++++++++++++++ components/Buttons/src/MDCButton.m | 14 ++ .../unit/MDCButtonPointerInteractionTests.m | 48 +++++++ 3 files changed, 198 insertions(+) create mode 100644 components/Buttons/examples/ButtonsPointerInteractionExample.swift create mode 100644 components/Buttons/tests/unit/MDCButtonPointerInteractionTests.m diff --git a/components/Buttons/examples/ButtonsPointerInteractionExample.swift b/components/Buttons/examples/ButtonsPointerInteractionExample.swift new file mode 100644 index 00000000000..b71ab6a5b51 --- /dev/null +++ b/components/Buttons/examples/ButtonsPointerInteractionExample.swift @@ -0,0 +1,136 @@ +// 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 Foundation +import UIKit +import MaterialComponents.MaterialButtons +import MaterialComponents.MaterialButtons_Theming +import MaterialComponents.MaterialContainerScheme + +class ButtonsPointerInteractionExample: UIViewController { + let floatingButtonPlusDimension = CGFloat(24) + let kMinimumAccessibleButtonSize = CGSize(width: 64, height: 48) + + @objc var containerScheme: MDCContainerScheming = MDCContainerScheme() + + lazy var containedButton: MDCButton = { + let containedButton = MDCButton() + containedButton.applyContainedTheme(withScheme: containerScheme) + containedButton.setTitle("Tap Me Too", for: UIControl.State()) + containedButton.sizeToFit() + let containedButtonVerticalInset = + min(0, -(kMinimumAccessibleButtonSize.height - containedButton.bounds.height) / 2) + let containedButtonHorizontalInset = + min(0, -(kMinimumAccessibleButtonSize.width - containedButton.bounds.width) / 2) + containedButton.hitAreaInsets = + UIEdgeInsets( + top: containedButtonVerticalInset, left: containedButtonHorizontalInset, + bottom: containedButtonVerticalInset, right: containedButtonHorizontalInset) + containedButton.translatesAutoresizingMaskIntoConstraints = false + containedButton.addTarget(self, action: #selector(tap), for: .touchUpInside) + #if compiler(>=5.2) + if #available(iOS 13.4, *) { + containedButton.isPointerInteractionEnabled = true + } + #endif + return containedButton + }() + + lazy var textButton: MDCButton = { + let textButton = MDCButton() + textButton.applyTextTheme(withScheme: MDCContainerScheme()) + textButton.setTitle("Touch me", for: UIControl.State()) + textButton.sizeToFit() + let textButtonVerticalInset = + min(0, -(kMinimumAccessibleButtonSize.height - textButton.bounds.height) / 2) + let textButtonHorizontalInset = + min(0, -(kMinimumAccessibleButtonSize.width - textButton.bounds.width) / 2) + textButton.hitAreaInsets = + UIEdgeInsets( + top: textButtonVerticalInset, left: textButtonHorizontalInset, + bottom: textButtonVerticalInset, right: textButtonHorizontalInset) + textButton.translatesAutoresizingMaskIntoConstraints = false + textButton.addTarget(self, action: #selector(tap), for: .touchUpInside) + #if compiler(>=5.2) + if #available(iOS 13.4, *) { + textButton.isPointerInteractionEnabled = true + } + #endif + return textButton + }() + + lazy var floatingButton: MDCFloatingButton = { + let floatingButton = MDCFloatingButton() + floatingButton.backgroundColor = containerScheme.colorScheme.backgroundColor + floatingButton.sizeToFit() + floatingButton.translatesAutoresizingMaskIntoConstraints = false + floatingButton.addTarget(self, action: #selector(floatingButtonTapped(_:)), for: .touchUpInside) + + let plusShapeLayer = ButtonsTypicalUseSupplemental.createPlusShapeLayer(floatingButton) + floatingButton.layer.addSublayer(plusShapeLayer) + floatingButton.accessibilityLabel = "Create" + floatingButton.applySecondaryTheme(withScheme: self.containerScheme) + #if compiler(>=5.2) + if #available(iOS 13.4, *) { + floatingButton.isPointerInteractionEnabled = true + } + #endif + return floatingButton + }() + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = containerScheme.colorScheme.backgroundColor + + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.spacing = 20 + stackView.alignment = .center + stackView.addArrangedSubview(containedButton) + stackView.addArrangedSubview(textButton) + stackView.addArrangedSubview(floatingButton) + + view.addSubview(stackView) + stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + } + + @objc func tap(_ sender: Any) { + print("\(type(of: sender)) was tapped.") + } + + @objc func floatingButtonTapped(_ sender: MDCFloatingButton) { + print("\(type(of: sender)) was tapped.") + guard !UIAccessibility.isVoiceOverRunning else { + return + } + + sender.collapse(true) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + sender.expand(true, completion: nil) + } + } + } +} + +extension ButtonsPointerInteractionExample { + @objc class func catalogMetadata() -> [String: Any] { + return [ + "breadcrumbs": ["Buttons", "Pointer Interactions"], + "primaryDemo": false, + "presentable": false, + ] + } +} diff --git a/components/Buttons/src/MDCButton.m b/components/Buttons/src/MDCButton.m index 9ae7222e026..3020f29d64e 100644 --- a/components/Buttons/src/MDCButton.m +++ b/components/Buttons/src/MDCButton.m @@ -225,6 +225,20 @@ - (void)commonMDCButtonInit { if (_uppercaseTitle) { [self updateTitleCase]; } + +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + UIButtonPointerStyleProvider buttonPointerStyleProvider = ^UIPointerStyle *( + UIButton *buttonToStyle, UIPointerEffect *proposedEffect, UIPointerShape *proposedShape) { + UITargetedPreview *targetedPreview = [[UITargetedPreview alloc] initWithView:buttonToStyle]; + UIPointerEffect *highlightEffect = + [UIPointerHighlightEffect effectWithPreview:targetedPreview]; + return [UIPointerStyle styleWithEffect:highlightEffect shape:nil]; + }; + self.pointerStyleProvider = buttonPointerStyleProvider; + self.pointerInteractionEnabled = NO; + } +#endif } - (void)dealloc { diff --git a/components/Buttons/tests/unit/MDCButtonPointerInteractionTests.m b/components/Buttons/tests/unit/MDCButtonPointerInteractionTests.m new file mode 100644 index 00000000000..e89a4a5becb --- /dev/null +++ b/components/Buttons/tests/unit/MDCButtonPointerInteractionTests.m @@ -0,0 +1,48 @@ +// 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 + +#import "MaterialButtons.h" + +@interface MDCButtonPointerInteractionTests : XCTestCase +@end + +@implementation MDCButtonPointerInteractionTests + +- (void)testButtonHasPointerStyleProvider { +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + // Given + MDCButton *button = [[MDCButton alloc] init]; + + // Then + XCTAssertNotNil(button.pointerStyleProvider); + } +#endif +} + +- (void)testPointerInteractionIsDisabledByDefault { +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + // Given + MDCButton *button = [[MDCButton alloc] init]; + + // Then + XCTAssertFalse(button.isPointerInteractionEnabled); + } +#endif +} + +@end From f7d799a9cfb50384e2f304920fcfb87d574b6a55 Mon Sep 17 00:00:00 2001 From: Galia Kaufman Date: Fri, 10 Apr 2020 11:12:15 -0400 Subject: [PATCH 23/31] [Dialogs] Using the new Testing target in accessory snapshots. A followup for cl/305814799 - replacing calls to all private APIs in MDCAlertControllerAccessoryTests.m with the new testing utilities in MDCAlertController+Testing.h PiperOrigin-RevId: 305878401 --- .../MDCAlertControllerAccessoryTests.m | 30 +++---------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/components/Dialogs/tests/snapshot/MDCAlertControllerAccessoryTests.m b/components/Dialogs/tests/snapshot/MDCAlertControllerAccessoryTests.m index 8034dfef0fa..7e533f2610a 100644 --- a/components/Dialogs/tests/snapshot/MDCAlertControllerAccessoryTests.m +++ b/components/Dialogs/tests/snapshot/MDCAlertControllerAccessoryTests.m @@ -17,8 +17,8 @@ #import "MaterialSnapshot.h" #import "MaterialDialogs.h" +#import "MDCAlertController+Testing.h" #import "MaterialDialogs+Theming.h" -#import "MDCAlertControllerView+Private.h" #import "MaterialTextFields.h" #import "MaterialContainerScheme.h" @@ -41,8 +41,6 @@ - (void)setUp { self.alert = [MDCAlertController alertControllerWithTitle:@"Title" message:nil]; [self addOutlinedActionWithTitle:@"OK"]; - self.alert.view.bounds = CGRectMake(0.f, 0.f, 300.f, 300.f); - self.containerScheme = [[MDCContainerScheme alloc] init]; self.containerScheme.colorScheme = [[MDCSemanticColorScheme alloc] initWithDefaults:MDCColorSchemeDefaultsMaterial201907]; @@ -59,16 +57,6 @@ - (void)tearDown { #pragma mark - Helpers -- (void)sizeAlertToFitContentForAlert:(MDCAlertController *)alert { - // Ensure snapshot view size resembles actual runtime size of the alert. This is the closest - // simulation to how an actual dialog will be sized on a screen. The dialog layouts itself with - // final size when calculatePreferredContentSizeForBounds: is called - after all the dialog - // configuration is complete. - MDCAlertControllerView *alertView = (MDCAlertControllerView *)alert.view; - CGSize bounds = [alertView calculatePreferredContentSizeForBounds:alertView.bounds.size]; - alertView.bounds = CGRectMake(0.f, 0.f, bounds.width, bounds.height); -} - - (void)addOutlinedActionWithTitle:(NSString *)actionTitle { [self.alert addAction:[MDCAlertAction actionWithTitle:actionTitle emphasis:MDCActionEmphasisMedium @@ -76,9 +64,9 @@ - (void)addOutlinedActionWithTitle:(NSString *)actionTitle { } - (void)generateSizedSnapshotAndVerifyForAlert:(MDCAlertController *)alert { - [self sizeAlertToFitContentForAlert:alert]; - [self highlightSectionsForAlert:self.alert]; - [self generateSnapshotAndVerifyForView:self.alert.view]; + [alert sizeToFitContentInBounds:CGSizeMake(300.0f, 300.0f)]; + [alert highlightAlertPanels]; + [self generateSnapshotAndVerifyForView:alert.view]; } - (void)generateSnapshotAndVerifyForView:(UIView *)view { @@ -92,15 +80,6 @@ - (void)changeToRTL:(MDCAlertController *)alert { [self changeViewToRTL:alert.view]; } -- (void)highlightSectionsForAlert:(MDCAlertController *)alert { - MDCAlertControllerView *alertView = (MDCAlertControllerView *)alert.view; - alertView.titleScrollView.backgroundColor = [[UIColor purpleColor] colorWithAlphaComponent:.2f]; - alertView.titleLabel.backgroundColor = [[UIColor purpleColor] colorWithAlphaComponent:.2f]; - alertView.contentScrollView.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:.1f]; - alertView.messageLabel.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:.2f]; - alertView.actionsScrollView.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:.2f]; -} - #pragma mark - Tests - (void)testAlertHasTextFieldAccessory { @@ -117,7 +96,6 @@ - (void)testAlertHasTextFieldAccessory { } - (void)testAlertHasCollectionViewAccessory { - // Given // Given UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; CGRect frame = CGRectMake(0.0f, 0.0f, 320.0f, 160.0f); From 4f75f7c33f8cb5e7999aa638102f04ce6de5f153 Mon Sep 17 00:00:00 2001 From: Bryan Oltman Date: Fri, 10 Apr 2020 11:17:29 -0400 Subject: [PATCH 24/31] [Buttons] Disable pointer interactions for MDCFloatingButton during animation. PiperOrigin-RevId: 305879022 --- .../Buttons/src/MDCFloatingButton+Animation.m | 35 ++++++ ...MDCFloatingButtonPointerInteractionTests.m | 109 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 components/Buttons/tests/unit/MDCFloatingButtonPointerInteractionTests.m diff --git a/components/Buttons/src/MDCFloatingButton+Animation.m b/components/Buttons/src/MDCFloatingButton+Animation.m index bd534fd4157..f09a18d87c9 100644 --- a/components/Buttons/src/MDCFloatingButton+Animation.m +++ b/components/Buttons/src/MDCFloatingButton+Animation.m @@ -88,6 +88,18 @@ + (float)fab_dragCoefficient { #endif - (void)expand:(BOOL)animated completion:(void (^_Nullable)(void))completion { +// The typical approach to adjusting the iPad's pointer frame is to invalidate the relevant +// pointer interactions. This was attempted, but, as you can see in go/mdc-fab-pointer-bug, +// invalidating the interaction while a transform animation is happening causes undesired behavior. +// Because of this, we instead temporarily disable pointer interaction for the button while it +// animates and reenable (if previously enabled) once the animation has ended. +#ifdef __IPHONE_13_4 + BOOL wasPointerInteractionEnabled = NO; + if (@available(iOS 13.4, *)) { + wasPointerInteractionEnabled = self.pointerInteractionEnabled; + self.pointerInteractionEnabled = NO; + } +#endif void (^expandActions)(void) = ^{ self.layer.transform = CATransform3DConcat(self.layer.transform, [MDCFloatingButton expandTransform]); @@ -97,6 +109,11 @@ - (void)expand:(BOOL)animated completion:(void (^_Nullable)(void))completion { [self.layer removeAnimationForKey:kMDCFloatingButtonTransformKey]; [self.layer removeAnimationForKey:kMDCFloatingButtonOpacityKey]; [self.imageView.layer removeAnimationForKey:kMDCFloatingButtonTransformKey]; +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + self.pointerInteractionEnabled = wasPointerInteractionEnabled; + } +#endif if (completion) { completion(); } @@ -163,6 +180,19 @@ - (void)expand:(BOOL)animated completion:(void (^_Nullable)(void))completion { } - (void)collapse:(BOOL)animated completion:(void (^_Nullable)(void))completion { +// The typical approach to adjusting the iPad's pointer frame is to invalidate the relevant +// pointer interactions. This was attempted, but, as you can see in go/mdc-fab-pointer-bug, +// invalidating the interaction while a transform animation is happening causes undesired behavior. +// Because of this, we instead temporarily disable pointer interaction for the button while it +// animates and reenable (if previously enabled) once the animation has ended. +#ifdef __IPHONE_13_4 + BOOL wasPointerInteractionEnabled = NO; + if (@available(iOS 13.4, *)) { + wasPointerInteractionEnabled = self.pointerInteractionEnabled; + self.pointerInteractionEnabled = NO; + } +#endif + void (^collapseActions)(void) = ^{ self.layer.transform = CATransform3DConcat(self.layer.transform, [MDCFloatingButton collapseTransform]); @@ -172,6 +202,11 @@ - (void)collapse:(BOOL)animated completion:(void (^_Nullable)(void))completion { [self.layer removeAnimationForKey:kMDCFloatingButtonTransformKey]; [self.layer removeAnimationForKey:kMDCFloatingButtonOpacityKey]; [self.imageView.layer removeAnimationForKey:kMDCFloatingButtonTransformKey]; +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + self.pointerInteractionEnabled = wasPointerInteractionEnabled; + } +#endif if (completion) { completion(); } diff --git a/components/Buttons/tests/unit/MDCFloatingButtonPointerInteractionTests.m b/components/Buttons/tests/unit/MDCFloatingButtonPointerInteractionTests.m new file mode 100644 index 00000000000..a7a8b1295e0 --- /dev/null +++ b/components/Buttons/tests/unit/MDCFloatingButtonPointerInteractionTests.m @@ -0,0 +1,109 @@ +// 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 + +#import "MaterialButtons.h" + +/** + Tests that validate collapsing and expanding MDCFloatingButton does not change + pointerInteractionEnabled. This behavior is being tested because MDCFloatingButton temporarily sets + pointerInteractionEnabled to NO when changing between collapsed and expanded states to address an + issue where the iPad pointer would remain floating-button-shaped after the button it was hovering + over collapsed. + */ +@interface MDCFloatingButtonPointerInteractionTests : XCTestCase +@end + +@implementation MDCFloatingButtonPointerInteractionTests + +- (void)testPointerInteractionIsDisabledByDefault { +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + // Given + MDCFloatingButton *button = [[MDCFloatingButton alloc] init]; + + // Then + XCTAssertFalse(button.pointerInteractionEnabled); + } +#endif +} + +- (void)testPointerInteractionRemainsEnabledAfterCollapse { +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + // Given + MDCFloatingButton *button = [[MDCFloatingButton alloc] init]; + button.pointerInteractionEnabled = YES; + + // When + [button collapse:NO completion:nil]; + + // Then + XCTAssertTrue(button.pointerInteractionEnabled); + } +#endif +} + +- (void)testPointerInteractionRemainsDisabledAfterCollapse { +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + // Given + MDCFloatingButton *button = [[MDCFloatingButton alloc] init]; + button.pointerInteractionEnabled = NO; + + // When + [button collapse:NO completion:nil]; + + // Then + XCTAssertFalse(button.pointerInteractionEnabled); + } +#endif +} + +- (void)testPointerInteractionRemainsEnabledAfterExpand { +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + // Given + MDCFloatingButton *button = [[MDCFloatingButton alloc] init]; + [button collapse:NO completion:nil]; + button.pointerInteractionEnabled = YES; + + // When + [button expand:NO completion:nil]; + + // Then + XCTAssertTrue(button.pointerInteractionEnabled); + } +#endif +} + +- (void)testPointerInteractionRemainsDisabledAfterExpand { +#ifdef __IPHONE_13_4 + if (@available(iOS 13.4, *)) { + // Given + MDCFloatingButton *button = [[MDCFloatingButton alloc] init]; + [button collapse:NO completion:nil]; + button.pointerInteractionEnabled = NO; + + // When + [button expand:NO completion:nil]; + + // Then + XCTAssertFalse(button.isPointerInteractionEnabled); + } +#endif +} + +@end From 2f0a4af1c9a965cbe76b23ca5f1f92f500d11983 Mon Sep 17 00:00:00 2001 From: Galia Kaufman Date: Fri, 10 Apr 2020 11:32:05 -0400 Subject: [PATCH 25/31] [Dialogs] Cleanup: move deprecated API to private header. PiperOrigin-RevId: 305880930 --- components/Dialogs/src/MDCAlertController+Customize.h | 6 +++--- components/Dialogs/src/MDCAlertControllerView.h | 9 --------- .../src/private/MDCAlertControllerView+Private.h | 11 +++++++++++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/components/Dialogs/src/MDCAlertController+Customize.h b/components/Dialogs/src/MDCAlertController+Customize.h index 3c087d1fa3c..e5b11c96ce6 100644 --- a/components/Dialogs/src/MDCAlertController+Customize.h +++ b/components/Dialogs/src/MDCAlertController+Customize.h @@ -22,10 +22,10 @@ @interface MDCAlertController (Customize) /** - An optional custom icon view above the title of the alert. + An optional custom view above the title of the alert. @discussion Use `titleIcon` to display icons or images. Use `titleIconView` for custom views - implementations. If both `titleIcon` are `titleIconView` are set, 'titleIcon' is + implementations. If both `titleIcon` and `titleIconView` are set, 'titleIcon' is ignored. @discussion Custom title views are aligned with the title and may be resized to fit. @@ -38,7 +38,7 @@ set its `contentMode`. @discussion Use `titleIcon` to display icons or images. Use `titleIconView` for custom views - implementations. If both `titleIcon` are `titleIconView` are set, 'titleIcon' (and + implementations. If both `titleIcon` and `titleIconView` are set, 'titleIcon' (and `titleIconImageView`) are ignored. */ @property(nonatomic, nullable, strong, readonly) UIImageView *titleIconImageView; diff --git a/components/Dialogs/src/MDCAlertControllerView.h b/components/Dialogs/src/MDCAlertControllerView.h index a97a5785584..2a833090ae0 100644 --- a/components/Dialogs/src/MDCAlertControllerView.h +++ b/components/Dialogs/src/MDCAlertControllerView.h @@ -22,15 +22,6 @@ @property(nonatomic, strong, nullable) UIImage *titleIcon; @property(nonatomic, strong, nullable) UIColor *titleIconTintColor; -@property(nonatomic, assign) NSTextAlignment titleAlignment __deprecated_msg( - "Please use MDCAlertController titleAlignment instead."); -@property(nonatomic, assign) NSTextAlignment messageAlignment __deprecated_msg( - "Please use MDCAlertController messageAlignment instead."); - -/** An optional custom icon view above the title of the alert. */ -@property(nonatomic, strong, nullable) UIView *titleIconView __deprecated_msg( - "Please use MDCAlertController+Customize titleIconView instead."); - @property(nonatomic, strong, nullable) UIFont *messageFont UI_APPEARANCE_SELECTOR; @property(nonatomic, strong, nullable) UIColor *messageColor UI_APPEARANCE_SELECTOR; diff --git a/components/Dialogs/src/private/MDCAlertControllerView+Private.h b/components/Dialogs/src/private/MDCAlertControllerView+Private.h index 055b817d6c6..f622bfe8dc4 100644 --- a/components/Dialogs/src/private/MDCAlertControllerView+Private.h +++ b/components/Dialogs/src/private/MDCAlertControllerView+Private.h @@ -21,8 +21,13 @@ @property(nonatomic, nonnull, strong) UILabel *titleLabel; @property(nonatomic, nonnull, strong) UILabel *messageLabel; + +/** An optional custom UIView that is displaed under the alert message. */ @property(nonatomic, nullable, strong) UIView *accessoryView; +/** An optional custom view above the title of the alert. */ +@property(nonatomic, strong, nullable) UIView *titleIconView; + @property(nonatomic, nullable, weak) MDCAlertActionManager *actionManager; /** The scroll view that holds the @c titleLabel. */ @@ -40,6 +45,12 @@ /** The horizontal alignment of @c titleIcon. */ @property(nonatomic, assign) NSTextAlignment titleIconAlignment; +/** The horizontal alignment of @c title. */ +@property(nonatomic, assign) NSTextAlignment titleAlignment; + +/** The horizontal alignment of @c message. */ +@property(nonatomic, assign) NSTextAlignment messageAlignment; + /** The alert actions alignment in horizontal layout. */ @property(nonatomic, assign) MDCContentHorizontalAlignment actionsHorizontalAlignment; From bff4209df216fd538dcb8d49b3941179b4a8a767 Mon Sep 17 00:00:00 2001 From: Bryan Oltman Date: Fri, 10 Apr 2020 13:32:25 -0400 Subject: [PATCH 26/31] [Buttons] Update MDCButton to use the proposed pointer effect in pointerStyleProvider. Previously, MDCButton's pointerStyleProvider was ignoring both the proposed effect and proposed shape. To better align with UIButton, we now use the proposed effect and use the button's boundingPath to provide a pointer shape. PiperOrigin-RevId: 305900307 --- components/Buttons/src/MDCButton.m | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/components/Buttons/src/MDCButton.m b/components/Buttons/src/MDCButton.m index 3020f29d64e..6e343688e84 100644 --- a/components/Buttons/src/MDCButton.m +++ b/components/Buttons/src/MDCButton.m @@ -228,14 +228,21 @@ - (void)commonMDCButtonInit { #ifdef __IPHONE_13_4 if (@available(iOS 13.4, *)) { + __weak typeof(self) weakSelf = self; UIButtonPointerStyleProvider buttonPointerStyleProvider = ^UIPointerStyle *( UIButton *buttonToStyle, UIPointerEffect *proposedEffect, UIPointerShape *proposedShape) { - UITargetedPreview *targetedPreview = [[UITargetedPreview alloc] initWithView:buttonToStyle]; - UIPointerEffect *highlightEffect = - [UIPointerHighlightEffect effectWithPreview:targetedPreview]; - return [UIPointerStyle styleWithEffect:highlightEffect shape:nil]; + typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { + return [UIPointerStyle styleWithEffect:proposedEffect shape:proposedShape]; + } + CGPathRef boundingCGPath = [strongSelf boundingPath].CGPath; + UIBezierPath *boundingBezierPath = [UIBezierPath bezierPathWithCGPath:boundingCGPath]; + UIPointerShape *shape = [UIPointerShape shapeWithPath:boundingBezierPath]; + return [UIPointerStyle styleWithEffect:proposedEffect shape:shape]; }; self.pointerStyleProvider = buttonPointerStyleProvider; + // Setting the pointerStyleProvider to a non-nil value flips pointerInteractionEnabled to YES. + // To maintain parity with UIButton's default behavior, we want it to default to NO. self.pointerInteractionEnabled = NO; } #endif From c2c1cc64b36fd93877a66e39867ec4917ad82a59 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 13 Apr 2020 07:59:37 -0400 Subject: [PATCH 27/31] [Catalog] Expose the catalog by convention tree via the AppDelegate and walk the tree with the UI test runner. This allows our UI test runner to fetch the catalog by convention tree at runtime in order to generate dynamic test runners that walk the entire catalog by convention tree without requiring maintenance of an explicit list of breadcrumbs. In this change I have replaced the explicit list of components with such a dynamic walker. In a follow-up change we will be able to dynamically walk through every node of the three and take screenshots. In a subsequent change from there we may allow certain nodes to implement "testActionStart" and "testActionStop" methods that allow the test runner to invoke certain canonical actions automatically (e.g. present a dialog). A couple examples are flaky, so a new convention has been added to those examples of providing a "flaky" tag on the example's metadata. If this flag is YES, then the example will not be snapshotted. PiperOrigin-RevId: 306214455 --- catalog/MDCCatalog/AppDelegate.swift | 8 ++++++++ .../supplemental/BottomAppBarTypicalUseSupplemental.m | 1 + .../examples/BottomDrawerExpandFullscreenExample.swift | 1 + .../examples/BottomDrawerInfiniteScrollingExample.swift | 1 - .../examples/BottomDrawerSwappingScrollViewsExample.swift | 1 - 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/catalog/MDCCatalog/AppDelegate.swift b/catalog/MDCCatalog/AppDelegate.swift index 2fafe8c9391..1728f0987b0 100644 --- a/catalog/MDCCatalog/AppDelegate.swift +++ b/catalog/MDCCatalog/AppDelegate.swift @@ -30,6 +30,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MDCAppBarNavigationContro var window: UIWindow? let navigationController = MDCAppBarNavigationController() + var tree: CBCNode? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { @@ -38,6 +39,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MDCAppBarNavigationContro // The navigation tree will only take examples that implement // and return YES to catalogIsPresentable. let tree = CBCCreatePresentableNavigationTree() + self.tree = tree navigationController.delegate = self @@ -60,6 +62,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MDCAppBarNavigationContro return true } + // This method is exposed solely for the purposes of UI test runners to be able to fetch the + // catalog by convention example tree. + @objc func navigationTree() -> CBCNode? { + return self.tree + } + @objc func themeDidChange(notification: NSNotification) { let colorScheme = AppTheme.containerScheme.colorScheme for viewController in navigationController.children { diff --git a/components/BottomAppBar/examples/supplemental/BottomAppBarTypicalUseSupplemental.m b/components/BottomAppBar/examples/supplemental/BottomAppBarTypicalUseSupplemental.m index 4db4ec6e683..5c3bb906ab7 100644 --- a/components/BottomAppBar/examples/supplemental/BottomAppBarTypicalUseSupplemental.m +++ b/components/BottomAppBar/examples/supplemental/BottomAppBarTypicalUseSupplemental.m @@ -36,6 +36,7 @@ + (NSDictionary *)catalogMetadata { @"bottom of the screen.", @"primaryDemo" : @YES, @"presentable" : @YES, + @"flaky" : @YES, }; } diff --git a/components/NavigationDrawer/examples/BottomDrawerExpandFullscreenExample.swift b/components/NavigationDrawer/examples/BottomDrawerExpandFullscreenExample.swift index 33c498f8dd3..a2e9c4c363d 100644 --- a/components/NavigationDrawer/examples/BottomDrawerExpandFullscreenExample.swift +++ b/components/NavigationDrawer/examples/BottomDrawerExpandFullscreenExample.swift @@ -139,6 +139,7 @@ extension BottomDrawerExpandFullscreenExample { "description": "Navigation Drawer", "primaryDemo": true, "presentable": true, + "flaky": true, ] } } diff --git a/components/NavigationDrawer/examples/BottomDrawerInfiniteScrollingExample.swift b/components/NavigationDrawer/examples/BottomDrawerInfiniteScrollingExample.swift index afd3fa8e5ef..6ee9ab20494 100644 --- a/components/NavigationDrawer/examples/BottomDrawerInfiniteScrollingExample.swift +++ b/components/NavigationDrawer/examples/BottomDrawerInfiniteScrollingExample.swift @@ -155,7 +155,6 @@ extension BottomDrawerInfiniteScrollingExample { return [ "breadcrumbs": ["Navigation Drawer", "Bottom Drawer Infinite Scrolling"], "description": "Navigation Drawer", - "primaryDemo": true, "presentable": true, ] } diff --git a/components/NavigationDrawer/examples/BottomDrawerSwappingScrollViewsExample.swift b/components/NavigationDrawer/examples/BottomDrawerSwappingScrollViewsExample.swift index d3a09fd7979..5f43c0fd6c0 100644 --- a/components/NavigationDrawer/examples/BottomDrawerSwappingScrollViewsExample.swift +++ b/components/NavigationDrawer/examples/BottomDrawerSwappingScrollViewsExample.swift @@ -162,7 +162,6 @@ extension BottomDrawerSwappingScrollViewsExample { return [ "breadcrumbs": ["Navigation Drawer", "Bottom Drawer Swapping ScrollViews"], "description": "Navigation Drawer", - "primaryDemo": true, "presentable": true, ] } From 1e969f0e46be66ddc8401c34e444a1378100a661 Mon Sep 17 00:00:00 2001 From: Yarden Eitan Date: Mon, 13 Apr 2020 08:11:19 -0400 Subject: [PATCH 28/31] [Ripple] Add a respondsToSelector check to resolve crashes of unrecognized selector. Adding a respondsToSelector check for "performAsCurrentTraitCollection" as there are cases where iOS 13+ devices do not recognize that API despite it being an available API since iOS 13. Tested this with iOS 13+ and iOS 11.2 simulators to see it is working as expected. PiperOrigin-RevId: 306215948 --- components/Ripple/src/MDCRippleView.m | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/components/Ripple/src/MDCRippleView.m b/components/Ripple/src/MDCRippleView.m index 027f091ff77..c95701b1714 100644 --- a/components/Ripple/src/MDCRippleView.m +++ b/components/Ripple/src/MDCRippleView.m @@ -189,23 +189,27 @@ - (void)setActiveRippleLayer:(MDCRippleLayer *)activeRippleLayer { self.activeRippleColor = self.rippleColor; } +- (void)setColorForRippleLayer:(MDCRippleLayer *)rippleLayer { +#if MDC_AVAILABLE_SDK_IOS(13_0) + if (@available(iOS 13.0, *)) { + if ([self.traitCollection respondsToSelector:@selector(performAsCurrentTraitCollection:)]) { + [self.traitCollection performAsCurrentTraitCollection:^{ + rippleLayer.fillColor = self.rippleColor.CGColor; + }]; + return; + } + } +#endif // MDC_AVAILABLE_SDK_IOS(13_0) + rippleLayer.fillColor = self.rippleColor.CGColor; +} + - (void)beginRippleTouchDownAtPoint:(CGPoint)point animated:(BOOL)animated completion:(nullable MDCRippleCompletionBlock)completion { MDCRippleLayer *rippleLayer = [MDCRippleLayer layer]; rippleLayer.rippleLayerDelegate = self; [self updateRippleStyle]; -#if MDC_AVAILABLE_SDK_IOS(13_0) - if (@available(iOS 13.0, *)) { - [self.traitCollection performAsCurrentTraitCollection:^{ - rippleLayer.fillColor = self.rippleColor.CGColor; - }]; - } else { - rippleLayer.fillColor = self.rippleColor.CGColor; - } -#else - rippleLayer.fillColor = self.rippleColor.CGColor; -#endif // MDC_AVAILABLE_SDK_IOS(13_0) + [self setColorForRippleLayer:rippleLayer]; rippleLayer.frame = self.bounds; if (self.rippleStyle == MDCRippleStyleUnbounded) { rippleLayer.maximumRadius = self.maximumRadius; From 14bf20a431221e71c92aba5949d1f0c38e93f5d7 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 13 Apr 2020 09:30:13 -0400 Subject: [PATCH 29/31] Automatic changelog preparation for release. --- .gitattributes | 28 ++++++++++++++++++--- CHANGELOG.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index c13f46c3bdc..f56e2dfb895 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,25 @@ -# Do not merge this version into `stable`. # DO NOT CHANGE THIS FILE -snapshot_test_goldens/**/*.png filter=lfs diff=lfs merge=lfs -text # DO NOT EDIT THE LINE BELOW. -.gitattributes merge=gitattributes +# DO NOT CHANGE THIS FILE +# DO NOT EDIT THE LINE BELOW. +/.gitattributes merge=gitattributes +# DO NOT EDIT THE LINE ABOVE. +# +# You can of course edit this file, but make sure you understand what you are +# doing. This file defines a custom filter driver that prevents snapshot test +# images from being merged into `stable`. Snapshot test images are only +# valuable in `develop` because they are only intended to help developers +# identify changes in the appearance of the library. +# +# Before you change this file, please carefully consider whether such a change +# is actually necessary. When you do change this file, it should almost always +# be done in a dedicated commit directly on the `stable` branch and not part +# of a release. If you see this file being changed as part of a release, +# block the release and work with the releaser to ensure that the change needs +# to be propagated from the `develop` branch to the `stable` branch. In nearly +# all cases, it should not be propagated from `develop` to `stable`. +# +# If you are a releaser and see this file change and you're not sure why, you +# might have accidentally skipped [setting the correct +# driver in your cloned +# repository](https://github.com/material-components/material-components-ios/blob/develop/contributing/releasing.md#configure-the-merge-strategy-for-gitattributes). +# If that's the case, please either revert the accidental change manually or +# restart the release with a fresh clone and the correct driver. diff --git a/CHANGELOG.md b/CHANGELOG.md index ad233a7f567..17ad89d3efc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,70 @@ +# #develop# + +Replace this text with a summarized description of this release's contents. +## Breaking changes + +Replace this explanations for how to resolve the breaking changes. +## New deprecations + +Replace this text with links to deprecation guides. +## New features + +Replace this text with example code for each new feature. +## API changes + +## Component changes + +### ActionSheet + +* [Add UIPointerInteraction support for actions..](https://github.com/material-components/material-components-ios/commit/45671537f593d07164d2d8ec15edd9a651f680a7) (Wenyu Zhang) + +### BottomSheet + +* [Deprecate the ShapeThemer.](https://github.com/material-components/material-components-ios/commit/1cf770cc40f1b29f1f89b0a3ed03f5f8899e4647) (Jeff Verkoeyen) + +### Buttons + +* [Add a snapshot test for floating buttons in normal mode with a label.](https://github.com/material-components/material-components-ios/commit/bc113b658c9a9e1befe6e3b4f83d6fa3b0e9db87) (Jeff Verkoeyen) +* [Add custom UIButtonPointerStyleProvider to MDCButton for better iPadOS pointer interaction.](https://github.com/material-components/material-components-ios/commit/5a2b2caae09680e38950dfb4c81c2c14f7a57359) (Bryan Oltman) +* [Add support to MDCFloatingButton for animating mode changes.](https://github.com/material-components/material-components-ios/commit/6ac7e6c3d87795a6ade75500c7c6af768f1463f0) (Jeff Verkoeyen) +* [Disable pointer interactions for MDCFloatingButton during animation.](https://github.com/material-components/material-components-ios/commit/4f75f7c33f8cb5e7999aa638102f04ce6de5f153) (Bryan Oltman) +* [Fix broken link in Buttons docs](https://github.com/material-components/material-components-ios/commit/8c27dcf2e53f038ba97e4e6452143a8f223c35b7) (Andrew Overton) +* [I got this build error when building on the latest Xcode: ``` "MDCFloatingButtonModeAnimator.m:150:9: Block implicitly retains 'self'; explicitly mention 'self' to indicate this is intended behavior" ``` This change adds the explicit self. If I got this other people might.](https://github.com/material-components/material-components-ios/commit/f5841c6d2fe269f4b3cadde869e1db44d1ca49cc) (Andrew Overton) +* [Standardize all examples on the same Example pattern.](https://github.com/material-components/material-components-ios/commit/3a157e1f4309fbf2c2fe4404acbbfcd34136666c) (Jeff Verkoeyen) +* [Standardize all test names on Tests.](https://github.com/material-components/material-components-ios/commit/57ec631be27332140267393711e1c06400452fb1) (Jeff Verkoeyen) +* [Update MDCButton to use the proposed pointer effect in pointerStyleProvider.](https://github.com/material-components/material-components-ios/commit/bff4209df216fd538dcb8d49b3941179b4a8a767) (Bryan Oltman) +* [Use static storage for all local consts.](https://github.com/material-components/material-components-ios/commit/4454d0be942d588fcea5a390fb78711c07513a6c) (Jeff Verkoeyen) + +### Dialogs + +* [Add UIPointerInteraction support for buttons..](https://github.com/material-components/material-components-ios/commit/d36b5e8f3d12b4123032a3a3abc9296984a617a3) (Wenyu Zhang) +* [Add a Testing target](https://github.com/material-components/material-components-ios/commit/f0747fdac58b97c1252e0c985e48913dce129c22) (Galia Kaufman) +* [Adding accessory view tests](https://github.com/material-components/material-components-ios/commit/969f6f55460312e5f16236ad0e06c3e725f1fb74) (Galia Kaufman) +* [Clean up some comments.](https://github.com/material-components/material-components-ios/commit/57a566909fb7d89247b93887262c7cb93597c75f) (Dave MacLachlan) +* [Cleanup: move deprecated API to private header.](https://github.com/material-components/material-components-ios/commit/2f0a4af1c9a965cbe76b23ca5f1f92f500d11983) (Galia Kaufman) +* [Disable broken test.](https://github.com/material-components/material-components-ios/commit/9c32878a48bc074e315f1edada3dd06b5302059e) (Jeff Verkoeyen) +* [Resolve issue with sizing a dialog's accessoryView.](https://github.com/material-components/material-components-ios/commit/579370c4d6ae72931c83dcf32f6a62ef47473464) (Nobody) +* [Using Testing target in configuration snapshots.](https://github.com/material-components/material-components-ios/commit/2e19a8aa7e363e49c7d345f76ce4b402bf245b68) (Galia Kaufman) +* [Using the new Testing target in accessory snapshots.](https://github.com/material-components/material-components-ios/commit/f7d799a9cfb50384e2f304920fcfb87d574b6a55) (Galia Kaufman) +* [Using the new Testing target in actions snapshots.](https://github.com/material-components/material-components-ios/commit/e48d79a9cc82c9187a80dcd24d513052554dbb79) (Galia Kaufman) +* [Using the new Testing target in insets snapshots.](https://github.com/material-components/material-components-ios/commit/666ed14ba0cd16bb4914f0cbbad8f022b1e7ac15) (Galia Kaufman) +* [Using the new Testing target in insets unit tests.](https://github.com/material-components/material-components-ios/commit/7346de3ce076b426cfc6f0e877ac457c255fd700) (Galia Kaufman) + +### Ripple + +* [Add a respondsToSelector check to resolve crashes of unrecognized selector.](https://github.com/material-components/material-components-ios/commit/1e969f0e46be66ddc8401c34e444a1378100a661) (Yarden Eitan) + +### TextControls + +* [This change finishes adding TextControls examples to the internal Catalog.](https://github.com/material-components/material-components-ios/commit/965cde39c85ca3e0e1e6295784321065c786dfe0) (Andrew Overton) + +## Multi-component changes + +* [Expose the catalog by convention tree via the AppDelegate and walk the tree with the UI test runner.](https://github.com/material-components/material-components-ios/commit/c2c1cc64b36fd93877a66e39867ec4917ad82a59) (Jeff Verkoeyen) +* [Fix a lot of formatting issues with material.io and some broken links](https://github.com/material-components/material-components-ios/commit/f9a72c3df80e89904fa72ad058032059e74fcf59) (Andrew Overton) + +--- + # 108.0.0 This major release removes the default `init` methods from MDCSemanticColorScheme, improves support From 237371732331f2f81add6f621464b17007a11930 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 13 Apr 2020 09:53:48 -0400 Subject: [PATCH 30/31] Update changelog. --- CHANGELOG.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17ad89d3efc..46f7ca0000e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,22 @@ -# #develop# +# 108.1.0 -Replace this text with a summarized description of this release's contents. -## Breaking changes +This minor release improves our support of the new iPadOS cursor APIs, deprecates a themer, and +adds the ability to animate a floating button's mode. -Replace this explanations for how to resolve the breaking changes. ## New deprecations -Replace this text with links to deprecation guides. +BottomSheet's ShapeThemer is now deprecated. + ## New features -Replace this text with example code for each new feature. +Improved UIPointerInteraction support for ActionSheet, Buttons, and Dialogs. + +MDCFloatingButton has a new animated API for animating the mode from normal to expanded. + +```objc +[button setMode:MDCFloatingButtonModeExpanded animated:YES]; +``` + ## API changes ## Component changes @@ -29,7 +36,7 @@ Replace this text with example code for each new feature. * [Add support to MDCFloatingButton for animating mode changes.](https://github.com/material-components/material-components-ios/commit/6ac7e6c3d87795a6ade75500c7c6af768f1463f0) (Jeff Verkoeyen) * [Disable pointer interactions for MDCFloatingButton during animation.](https://github.com/material-components/material-components-ios/commit/4f75f7c33f8cb5e7999aa638102f04ce6de5f153) (Bryan Oltman) * [Fix broken link in Buttons docs](https://github.com/material-components/material-components-ios/commit/8c27dcf2e53f038ba97e4e6452143a8f223c35b7) (Andrew Overton) -* [I got this build error when building on the latest Xcode: ``` "MDCFloatingButtonModeAnimator.m:150:9: Block implicitly retains 'self'; explicitly mention 'self' to indicate this is intended behavior" ``` This change adds the explicit self. If I got this other people might.](https://github.com/material-components/material-components-ios/commit/f5841c6d2fe269f4b3cadde869e1db44d1ca49cc) (Andrew Overton) +* [Fix build error](https://github.com/material-components/material-components-ios/commit/f5841c6d2fe269f4b3cadde869e1db44d1ca49cc) (Andrew Overton) * [Standardize all examples on the same Example pattern.](https://github.com/material-components/material-components-ios/commit/3a157e1f4309fbf2c2fe4404acbbfcd34136666c) (Jeff Verkoeyen) * [Standardize all test names on Tests.](https://github.com/material-components/material-components-ios/commit/57ec631be27332140267393711e1c06400452fb1) (Jeff Verkoeyen) * [Update MDCButton to use the proposed pointer effect in pointerStyleProvider.](https://github.com/material-components/material-components-ios/commit/bff4209df216fd538dcb8d49b3941179b4a8a767) (Bryan Oltman) From 23de723d65af40e46f48d13fb137d7b2baf08f5e Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 13 Apr 2020 09:54:03 -0400 Subject: [PATCH 31/31] Bump the release. --- MaterialComponents.podspec | 2 +- MaterialComponentsBeta.podspec | 2 +- MaterialComponentsEarlGreyTests.podspec | 2 +- MaterialComponentsExamples.podspec | 2 +- MaterialComponentsSnapshotTests.podspec | 2 +- VERSION | 2 +- catalog/MDCCatalog/Info.plist | 4 ++-- catalog/MDCDragons/Info.plist | 4 ++-- catalog/MaterialCatalog/MaterialCatalog.podspec | 2 +- components/LibraryInfo/src/MDCLibraryInfo.m | 2 +- components/LibraryInfo/tests/unit/LibraryInfoTests.m | 2 +- demos/supplemental/RemoteImageServiceForMDCDemos.podspec | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/MaterialComponents.podspec b/MaterialComponents.podspec index b529e3aa80a..98e65f9cd6e 100644 --- a/MaterialComponents.podspec +++ b/MaterialComponents.podspec @@ -2,7 +2,7 @@ load 'scripts/generated/icons.rb' Pod::Spec.new do |mdc| mdc.name = "MaterialComponents" - mdc.version = "108.0.0" + mdc.version = "108.1.0" mdc.authors = "The Material Components authors." mdc.summary = "A collection of stand-alone production-ready UI libraries focused on design details." mdc.homepage = "https://github.com/material-components/material-components-ios" diff --git a/MaterialComponentsBeta.podspec b/MaterialComponentsBeta.podspec index f7f29ec46ec..8f61ae61719 100644 --- a/MaterialComponentsBeta.podspec +++ b/MaterialComponentsBeta.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |mdc| mdc.name = "MaterialComponentsBeta" - mdc.version = "108.0.0" + mdc.version = "108.1.0" mdc.authors = "The Material Components authors." mdc.summary = "A collection of stand-alone alpha UI libraries that are not yet guaranteed to be ready for general production use. Use with caution." mdc.homepage = "https://github.com/material-components/material-components-ios" diff --git a/MaterialComponentsEarlGreyTests.podspec b/MaterialComponentsEarlGreyTests.podspec index d2c96735368..d9e8e73a9bc 100644 --- a/MaterialComponentsEarlGreyTests.podspec +++ b/MaterialComponentsEarlGreyTests.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "MaterialComponentsEarlGreyTests" - s.version = "108.0.0" + s.version = "108.1.0" s.authors = "The Material Components authors." s.summary = "This spec is an aggregate of all the Material Components EarlGrey tests." s.description = "This spec is made for use in the MDC Catalog." diff --git a/MaterialComponentsExamples.podspec b/MaterialComponentsExamples.podspec index b1de9037b9c..d4cb0639613 100644 --- a/MaterialComponentsExamples.podspec +++ b/MaterialComponentsExamples.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "MaterialComponentsExamples" - s.version = "108.0.0" + s.version = "108.1.0" s.authors = "The Material Components authors." s.summary = "This spec is an aggregate of all the Material Components examples." s.description = "This spec is made for use in the MDC Catalog. Used in conjunction with CatalogByConvention we create our Material Catalog." diff --git a/MaterialComponentsSnapshotTests.podspec b/MaterialComponentsSnapshotTests.podspec index 35c73c405fc..450e54f8584 100644 --- a/MaterialComponentsSnapshotTests.podspec +++ b/MaterialComponentsSnapshotTests.podspec @@ -53,7 +53,7 @@ end Pod::Spec.new do |s| s.name = "MaterialComponentsSnapshotTests" - s.version = "108.0.0" + s.version = "108.1.0" s.authors = "The Material Components authors." s.summary = "This spec is an aggregate of all the Material Components snapshot tests." s.homepage = "https://github.com/material-components/material-components-ios" diff --git a/VERSION b/VERSION index 37fdfb90df7..a40dc2f20e6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -108.0.0 +108.1.0 diff --git a/catalog/MDCCatalog/Info.plist b/catalog/MDCCatalog/Info.plist index e7b82121063..a10bbd3da06 100644 --- a/catalog/MDCCatalog/Info.plist +++ b/catalog/MDCCatalog/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 108.0.0 + 108.1.0 CFBundleSignature ???? CFBundleVersion - 108.0.0 + 108.1.0 LSRequiresIPhoneOS UIAppFonts diff --git a/catalog/MDCDragons/Info.plist b/catalog/MDCDragons/Info.plist index e8d34c6e77d..7c6b7b9baa9 100644 --- a/catalog/MDCDragons/Info.plist +++ b/catalog/MDCDragons/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 108.0.0 + 108.1.0 CFBundleVersion - 108.0.0 + 108.1.0 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/catalog/MaterialCatalog/MaterialCatalog.podspec b/catalog/MaterialCatalog/MaterialCatalog.podspec index 8af350aee21..cf2e668e078 100644 --- a/catalog/MaterialCatalog/MaterialCatalog.podspec +++ b/catalog/MaterialCatalog/MaterialCatalog.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "MaterialCatalog" - s.version = "108.0.0" + s.version = "108.1.0" s.summary = "Helper Objective-C classes for the MDC catalog." s.description = "This spec is made for use in the MDC Catalog." s.homepage = "https://github.com/material-components/material-components-ios" diff --git a/components/LibraryInfo/src/MDCLibraryInfo.m b/components/LibraryInfo/src/MDCLibraryInfo.m index d5a458a7182..23d79081ac0 100644 --- a/components/LibraryInfo/src/MDCLibraryInfo.m +++ b/components/LibraryInfo/src/MDCLibraryInfo.m @@ -19,7 +19,7 @@ // This string is updated automatically as a part of the release process and should not be edited // manually. Do not rename this constant or change the formatting without updating the release // scripts. -static NSString const *MDCLibraryInfoVersionString = @"108.0.0"; +static NSString const *MDCLibraryInfoVersionString = @"108.1.0"; @implementation MDCLibraryInfo diff --git a/components/LibraryInfo/tests/unit/LibraryInfoTests.m b/components/LibraryInfo/tests/unit/LibraryInfoTests.m index 6981efe8a76..4f1a7ae9d45 100644 --- a/components/LibraryInfo/tests/unit/LibraryInfoTests.m +++ b/components/LibraryInfo/tests/unit/LibraryInfoTests.m @@ -26,7 +26,7 @@ - (void)testVersionFormat { // Given // This regex pattern does the following: - // Accept: "108.0.0", etc. + // Accept: "108.1.0", etc. // Reject: "0.0.0", "1.2", "1", "-1.2.3", "Hi, I'm a version 1.2.3", "1.2.3 is my version", etc. // // Note the major version must be >= 1 since "0.0.0" is used as the version when something goes diff --git a/demos/supplemental/RemoteImageServiceForMDCDemos.podspec b/demos/supplemental/RemoteImageServiceForMDCDemos.podspec index 9212124d3fc..90e125394a4 100644 --- a/demos/supplemental/RemoteImageServiceForMDCDemos.podspec +++ b/demos/supplemental/RemoteImageServiceForMDCDemos.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "RemoteImageServiceForMDCDemos" - s.version = "108.0.0" + s.version = "108.1.0" s.summary = "A helper image class for the MDC demos." s.description = "This spec is made for use in the MDC demos. It gets images via url." s.homepage = "https://github.com/material-components/material-components-ios"