Wait for module UI test buttons to be hittable before tapping them (#87607)

diff --git a/dev/integration_tests/ios_host_app/FlutterUITests/FlutterUITests.m b/dev/integration_tests/ios_host_app/FlutterUITests/FlutterUITests.m
index 72f16bc..d47e5a9 100644
--- a/dev/integration_tests/ios_host_app/FlutterUITests/FlutterUITests.m
+++ b/dev/integration_tests/ios_host_app/FlutterUITests/FlutterUITests.m
@@ -5,24 +5,26 @@
 @import XCTest;
 
 @interface FlutterUITests : XCTestCase
+@property (strong) XCUIApplication *app;
 @end
 
 @implementation FlutterUITests
 
 - (void)setUp {
+    [super setUp];
     self.continueAfterFailure = NO;
+
+    XCUIApplication *app = [[XCUIApplication alloc] init];
+    [app launch];
+    self.app = app;
 }
 
 - (void)testFullScreenColdPop {
-    XCUIApplication *app = [[XCUIApplication alloc] init];
-    [app launch];
-
-    XCUIElement *coldButton = app.buttons[@"Full Screen (Cold)"];
-    XCTAssertTrue([coldButton waitForExistenceWithTimeout:60.0]);
-    [coldButton tap];
-
+    XCUIApplication *app = self.app;
+    [self waitForAndTapElement:app.buttons[@"Full Screen (Cold)"]];
     XCTAssertTrue([app.staticTexts[@"Button tapped 0 times."] waitForExistenceWithTimeout:60.0]);
-    [app.otherElements[@"Increment via Flutter"] tap];
+
+    [self waitForAndTapElement:app.otherElements[@"Increment via Flutter"]];
     XCTAssertTrue([app.staticTexts[@"Button tapped 1 time."] waitForExistenceWithTimeout:60.0]);
 
     // Back navigation.
@@ -31,15 +33,12 @@
 }
 
 - (void)testFullScreenWarm {
-    XCUIApplication *app = [[XCUIApplication alloc] init];
-    [app launch];
+    XCUIApplication *app = self.app;
 
-    XCUIElement *warmButton = app.buttons[@"Full Screen (Warm)"];
-    XCTAssertTrue([warmButton waitForExistenceWithTimeout:60.0]);
-    [warmButton tap];
-
+    [self waitForAndTapElement:app.buttons[@"Full Screen (Warm)"]];
     XCTAssertTrue([app.staticTexts[@"Button tapped 0 times."] waitForExistenceWithTimeout:60.0]);
-    [app.otherElements[@"Increment via Flutter"] tap];
+
+    [self waitForAndTapElement:app.otherElements[@"Increment via Flutter"]];
     XCTAssertTrue([app.staticTexts[@"Button tapped 1 time."] waitForExistenceWithTimeout:60.0]);
 
     // Back navigation.
@@ -48,15 +47,12 @@
 }
 
 - (void)testFlutterViewWarm {
-    XCUIApplication *app = [[XCUIApplication alloc] init];
-    [app launch];
+    XCUIApplication *app = self.app;
 
-    XCUIElement *warmButton = app.buttons[@"Flutter View (Warm)"];
-    XCTAssertTrue([warmButton waitForExistenceWithTimeout:60.0]);
-    [warmButton tap];
-
+    [self waitForAndTapElement:app.buttons[@"Flutter View (Warm)"]];
     XCTAssertTrue([app.staticTexts[@"Button tapped 0 times."] waitForExistenceWithTimeout:60.0]);
-    [app.otherElements[@"Increment via Flutter"] tap];
+
+    [self waitForAndTapElement:app.otherElements[@"Increment via Flutter"]];
     XCTAssertTrue([app.staticTexts[@"Button tapped 1 time."] waitForExistenceWithTimeout:60.0]);
 
     // Back navigation.
@@ -65,17 +61,14 @@
 }
 
 - (void)testHybridViewWarm {
-    XCUIApplication *app = [[XCUIApplication alloc] init];
-    [app launch];
+    XCUIApplication *app = self.app;
 
-    XCUIElement *warmButton = app.buttons[@"Hybrid View (Warm)"];
-    XCTAssertTrue([warmButton waitForExistenceWithTimeout:60.0]);
-    [warmButton tap];
+    [self waitForAndTapElement:app.buttons[@"Hybrid View (Warm)"]];
 
     XCTAssertTrue([app.staticTexts[@"Flutter button tapped 0 times."] waitForExistenceWithTimeout:60.0]);
     XCTAssertTrue(app.staticTexts[@"Platform button tapped 0 times."].exists);
 
-    [app.otherElements[@"Increment via Flutter"] tap];
+    [self waitForAndTapElement:app.otherElements[@"Increment via Flutter"]];
     XCTAssertTrue([app.staticTexts[@"Flutter button tapped 1 time."] waitForExistenceWithTimeout:60.0]);
     XCTAssertTrue(app.staticTexts[@"Platform button tapped 0 times."].exists);
 
@@ -89,10 +82,9 @@
 }
 
 - (void)testDualCold {
-    XCUIApplication *app = [[XCUIApplication alloc] init];
-    [app launch];
+    XCUIApplication *app = self.app;
 
-    [app.buttons[@"Dual Flutter View (Cold)"] tap];
+    [self waitForAndTapElement:app.buttons[@"Dual Flutter View (Cold)"]];
 
     // There are two marquees.
     XCUIElementQuery *marqueeQuery = [app.staticTexts matchingIdentifier:@"This is Marquee"];
@@ -104,4 +96,11 @@
     XCTAssertTrue([app.navigationBars[@"Flutter iOS Demos Home"] waitForExistenceWithTimeout:60.0]);
 }
 
+- (void)waitForAndTapElement:(XCUIElement *)element {
+    NSPredicate *hittable = [NSPredicate predicateWithFormat:@"exists == YES AND hittable == YES"];
+    [self expectationForPredicate:hittable evaluatedWithObject:element handler:nil];
+    [self waitForExpectationsWithTimeout:30.0 handler:nil];
+    [element tap];
+}
+
 @end