appium-webdriveragent 16.1.2 → 16.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/WebDriverAgent.xcodeproj/project.pbxproj +297 -0
  3. package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationApp_tvOS.xcscheme +78 -0
  4. package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_tvOS.xcscheme +86 -0
  5. package/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h +20 -8
  6. package/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m +116 -60
  7. package/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h +15 -0
  8. package/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +16 -0
  9. package/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m +4 -4
  10. package/WebDriverAgentLib/Commands/FBAlertViewCommands.m +10 -36
  11. package/WebDriverAgentLib/Commands/FBElementCommands.m +3 -3
  12. package/WebDriverAgentLib/FBAlert.h +43 -30
  13. package/WebDriverAgentLib/FBAlert.m +257 -91
  14. package/WebDriverAgentLib/Info.plist +2 -2
  15. package/WebDriverAgentLib/Routing/FBExceptionHandler.m +8 -1
  16. package/WebDriverAgentLib/Routing/FBExceptions.h +9 -0
  17. package/WebDriverAgentLib/Routing/FBExceptions.m +3 -0
  18. package/WebDriverAgentLib/Routing/FBSession.m +11 -13
  19. package/WebDriverAgentLib/Utilities/FBAlertsMonitor.m +11 -9
  20. package/WebDriverAgentLib/Utilities/FBPasteboard.m +8 -4
  21. package/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h +10 -0
  22. package/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h +29 -4
  23. package/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m +62 -27
  24. package/WebDriverAgentTests/IntegrationApp_tvOS/AppDelegate.h +13 -0
  25. package/WebDriverAgentTests/IntegrationApp_tvOS/AppDelegate.m +26 -0
  26. package/WebDriverAgentTests/IntegrationApp_tvOS/Info.plist +41 -0
  27. package/WebDriverAgentTests/IntegrationApp_tvOS/SceneDelegate.h +15 -0
  28. package/WebDriverAgentTests/IntegrationApp_tvOS/SceneDelegate.m +22 -0
  29. package/WebDriverAgentTests/IntegrationApp_tvOS/ViewController.h +13 -0
  30. package/WebDriverAgentTests/IntegrationApp_tvOS/ViewController.m +79 -0
  31. package/WebDriverAgentTests/IntegrationApp_tvOS/main.m +17 -0
  32. package/WebDriverAgentTests/IntegrationTests/FBAlertTests.m +32 -51
  33. package/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.m +9 -1
  34. package/WebDriverAgentTests/IntegrationTests/FBSafariAlertTests.m +1 -1
  35. package/WebDriverAgentTests/IntegrationTests_tvOS/FBTVFocusIntegrationTests.m +84 -0
  36. package/WebDriverAgentTests/IntegrationTests_tvOS/Info.plist +24 -0
  37. package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCAccessibilityElementDouble.h +27 -0
  38. package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCAccessibilityElementDouble.m +23 -0
  39. package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCElementSnapshotDouble.h +43 -0
  40. package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCElementSnapshotDouble.m +24 -0
  41. package/WebDriverAgentTests/UnitTests_tvOS/FBTVNavigationTrackerTests.m +48 -0
  42. package/package.json +1 -1
@@ -24,7 +24,6 @@
24
24
  #import "FBXCTestDaemonsProxy.h"
25
25
  #import "XCUIApplication+FBQuiescence.h"
26
26
  #import "XCUIElement.h"
27
- #import "XCUIElement+FBClassChain.h"
28
27
 
29
28
  /*!
30
29
  The intial value for the default application property.
@@ -56,14 +55,10 @@ NSString *const FB_SAFARI_BUNDLE_ID = @"com.apple.mobilesafari";
56
55
  NSString *autoClickAlertSelector = FBConfiguration.autoClickAlertSelector;
57
56
  if ([autoClickAlertSelector length] > 0) {
58
57
  @try {
59
- NSArray<XCUIElement*> *matches = [alert.alertElement fb_descendantsMatchingClassChain:autoClickAlertSelector
60
- shouldReturnAfterFirstMatch:YES];
61
- if (matches.count > 0) {
62
- [[matches objectAtIndex:0] tap];
63
- }
58
+ [alert clickElementMatchingClassChain:autoClickAlertSelector];
64
59
  } @catch (NSException *e) {
65
60
  [FBLogger logFmt:@"Could not click at the alert element '%@'. Original error: %@",
66
- autoClickAlertSelector, e.description];
61
+ autoClickAlertSelector, e.reason];
67
62
  }
68
63
  // This setting has priority over other settings if enabled
69
64
  return;
@@ -73,14 +68,17 @@ NSString *const FB_SAFARI_BUNDLE_ID = @"com.apple.mobilesafari";
73
68
  return;
74
69
  }
75
70
 
76
- NSError *error;
77
71
  if ([self.defaultAlertAction isEqualToString:@"accept"]) {
78
- if (![alert acceptWithError:&error]) {
79
- [FBLogger logFmt:@"Cannot accept the alert. Original error: %@", error.description];
72
+ @try {
73
+ [alert accept];
74
+ } @catch (NSException *e) {
75
+ [FBLogger logFmt:@"Cannot accept the alert. Original error: %@", e.reason];
80
76
  }
81
77
  } else if ([self.defaultAlertAction isEqualToString:@"dismiss"]) {
82
- if (![alert dismissWithError:&error]) {
83
- [FBLogger logFmt:@"Cannot dismiss the alert. Original error: %@", error.description];
78
+ @try {
79
+ [alert dismiss];
80
+ } @catch (NSException *e) {
81
+ [FBLogger logFmt:@"Cannot dismiss the alert. Original error: %@", e.reason];
84
82
  }
85
83
  } else {
86
84
  [FBLogger logFmt:@"'%@' default alert action is unsupported", self.defaultAlertAction];
@@ -213,7 +211,7 @@ static FBSession *_activeSession = nil;
213
211
  XCUIApplicationState testedAppState = self.testedApplication.state;
214
212
  if (testedAppState >= XCUIApplicationStateRunningForeground) {
215
213
  NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"%K == %@ OR %K IN {%@, %@}",
216
- @"elementType", @(XCUIElementTypeAlert),
214
+ @"elementType", @(XCUIElementTypeAlert),
217
215
  // To look for `SBTransientOverlayWindow` elements. See https://github.com/appium/WebDriverAgent/pull/946
218
216
  @"identifier", @"SBTransientOverlayWindow",
219
217
  // To look for 'criticalAlertSetting' elements https://developer.apple.com/documentation/usernotifications/unnotificationsettings/criticalalertsetting
@@ -52,26 +52,28 @@ static const NSTimeInterval FB_MONTORING_INTERVAL = 2.0;
52
52
  NSArray<XCUIApplication *> *activeApps = XCUIApplication.fb_activeApplications;
53
53
  BOOL didDetectAlert = NO;
54
54
  for (XCUIApplication *activeApp in activeApps) {
55
- XCUIElement *alertElement = nil;
56
55
  @try {
57
- alertElement = activeApp.fb_alertElement;
58
- if (nil != alertElement) {
59
- [delegate didDetectAlert:[FBAlert alertWithElement:alertElement]];
56
+ FBAlert *alert = [FBAlert alertWithApplication:activeApp];
57
+ if (alert.isPresent) {
58
+ [delegate didDetectAlert:alert];
59
+ didDetectAlert = YES;
60
60
  }
61
61
  } @catch (NSException *e) {
62
62
  [FBLogger logFmt:@"Got an unexpected exception while monitoring alerts: %@\n%@", e.reason, e.callStackSymbols];
63
63
  }
64
- if (nil != alertElement) {
65
- didDetectAlert = YES;
64
+ if (didDetectAlert) {
66
65
  break;
67
66
  }
68
67
  }
69
68
 
70
69
  if (!didDetectAlert) {
71
70
  @try {
72
- XCUIElement *alertElement = [XCUIApplication fb_limitedAccessPromptAlertElement];
73
- if (nil != alertElement) {
74
- [delegate didDetectAlert:[FBAlert alertWithElement:alertElement]];
71
+ XCUIApplication *promptApp = XCUIApplication.fb_limitedAccessPromptApplication;
72
+ if (nil != promptApp) {
73
+ FBAlert *alert = [FBAlert alertWithApplication:promptApp];
74
+ if (alert.isPresent) {
75
+ [delegate didDetectAlert:alert];
76
+ }
75
77
  }
76
78
  } @catch (NSException *e) {
77
79
  [FBLogger logFmt:@"Got an unexpected exception while monitoring alerts: %@\n%@", e.reason, e.callStackSymbols];
@@ -11,6 +11,7 @@
11
11
  #import <mach/mach_time.h>
12
12
  #import "FBAlert.h"
13
13
  #import "FBErrorBuilder.h"
14
+ #import "FBExceptions.h"
14
15
  #import "FBMacros.h"
15
16
  #import "XCUIApplication+FBHelpers.h"
16
17
  #import "XCUIApplication+FBAlert.h"
@@ -100,10 +101,13 @@
100
101
  break;
101
102
  }
102
103
 
103
- XCUIElement *alertElement = XCUIApplication.fb_systemApplication.fb_alertElement;
104
- if (nil != alertElement) {
105
- FBAlert *alert = [FBAlert alertWithElement:alertElement];
106
- [alert acceptWithError:nil];
104
+ @try {
105
+ [[FBAlert alertWithApplication:XCUIApplication.fb_systemApplication] accept];
106
+ } @catch (NSException *e) {
107
+ if (![e.name isEqualToString:FBAlertNotPresentException]) {
108
+ @throw e;
109
+ }
110
+ // No alert is present on this tick - expected on most iterations of this poll loop
107
111
  }
108
112
  uint64_t timeElapsed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted;
109
113
  if (timeElapsed / NSEC_PER_SEC > timeout) {
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  #import <Foundation/Foundation.h>
10
+ #import "FBXCElementSnapshot.h"
10
11
 
11
12
  #if TARGET_OS_TV
12
13
 
@@ -22,6 +23,15 @@
22
23
 
23
24
  - (FBTVDirection)horizontalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta;
24
25
  - (FBTVDirection)verticalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta;
26
+ - (FBTVDirection)directionWithItem:(FBTVNavigationItem *)item
27
+ delta:(CGFloat)delta
28
+ positiveDirection:(FBTVDirection)positiveDirection
29
+ negativeDirection:(FBTVDirection)negativeDirection;
30
+
31
+ // Exposed for testing: the pure direction-math core of `-pollFocusState:`,
32
+ // parameterized on the already-resolved focused element's snapshot so tests
33
+ // can supply a fake one instead of a live query result.
34
+ - (FBTVDirection)directionTowardsTargetFromFocusedElementSnapshot:(id<FBXCElementSnapshot>)focusedSnapshot;
25
35
  @end
26
36
 
27
37
  #endif
@@ -22,6 +22,19 @@ typedef NS_ENUM(NSUInteger, FBTVDirection) {
22
22
  FBTVDirectionNone = 4
23
23
  };
24
24
 
25
+ /**
26
+ Represents where the tracked target element currently stands with respect
27
+ to keyboard/remote focus, as of the last `-pollFocusState:` call.
28
+ */
29
+ typedef NS_ENUM(NSUInteger, FBTVFocusState) {
30
+ // The target element currently has focus
31
+ FBTVFocusStateFocused,
32
+ // The target element could not be located in the accessibility tree anymore
33
+ FBTVFocusStateGone,
34
+ // The target element exists, but does not have focus yet
35
+ FBTVFocusStatePending,
36
+ };
37
+
25
38
  NS_ASSUME_NONNULL_BEGIN
26
39
 
27
40
  @interface FBTVNavigationItem : NSObject
@@ -38,12 +51,24 @@ NS_ASSUME_NONNULL_BEGIN
38
51
  + (instancetype)trackerWithTargetElement: (XCUIElement *) targetElement;
39
52
 
40
53
  /**
41
- Determine the correct direction to move the focus to the tracked target
42
- element from the currently focused one
54
+ Determines whether the tracked target element currently has focus, whether
55
+ it still exists, and - if neither - which direction the focus should move
56
+ to get closer to it. Each of these is resolved with the cheapest available
57
+ accessibility round trip for what it's checking: `hasFocus`/`exists` are
58
+ single already-known-element checks, and the currently focused element (if
59
+ any) is found via a targeted `hasFocus == true` query rather than a whole-app
60
+ snapshot walk, whose cost scales with total app size/depth instead of with
61
+ the (typically 0-1) number of matches.
43
62
 
44
- @return FBTVDirection to move the focus to
63
+ @param direction Always reset to `FBTVDirectionNone` first, then set to the
64
+ suggested direction to move the focus to when the return value is
65
+ `FBTVFocusStatePending`. It is left as `FBTVDirectionNone` when the return
66
+ value is `FBTVFocusStateFocused`/`FBTVFocusStateGone`, or when the
67
+ currently focused element could not be determined yet - in the latter
68
+ case the caller should just retry on the next iteration.
69
+ @return The current focus state of the tracked target element
45
70
  */
46
- - (FBTVDirection)directionToFocusedElement;
71
+ - (FBTVFocusState)pollFocusState:(FBTVDirection *)direction;
47
72
 
48
73
  @end
49
74
 
@@ -10,6 +10,7 @@
10
10
  #import "FBTVNavigationTracker-Private.h"
11
11
 
12
12
  #import "FBMathUtils.h"
13
+ #import "FBXCElementSnapshotWrapper.h"
13
14
  #import "XCUIElement+FBCaching.h"
14
15
  #import "XCUIElement+FBUtilities.h"
15
16
  #import "XCUIElement+FBWebDriverAttributes.h"
@@ -56,17 +57,49 @@
56
57
  self = [super init];
57
58
  if (self) {
58
59
  _targetElement = targetElement;
59
- CGRect frame = targetElement.wdFrame;
60
- _targetCenter = FBRectGetCenter(frame);
60
+ _targetCenter = FBRectGetCenter(targetElement.wdFrame);
61
61
  _navigationItems = [NSMutableDictionary dictionary];
62
62
  }
63
63
  return self;
64
64
  }
65
65
 
66
- - (FBTVDirection)directionToFocusedElement
66
+ - (FBTVFocusState)pollFocusState:(FBTVDirection *)direction
67
67
  {
68
- XCUIElement *focused = XCUIApplication.fb_activeApplication.fb_focusedElement;
68
+ *direction = FBTVDirectionNone;
69
69
 
70
+ // `hasFocus`/`exists` are cheap, single-element live checks - each round
71
+ // trip only has to serialize the state of one already-known element, not
72
+ // an unbounded subtree, so there's no snapshot-walk win to be had here.
73
+ if (self.targetElement.hasFocus) {
74
+ return FBTVFocusStateFocused;
75
+ }
76
+ if (!self.targetElement.exists) {
77
+ return FBTVFocusStateGone;
78
+ }
79
+
80
+ // Likewise, a `hasFocus == true` predicate query only has to serialize its
81
+ // (typically 0-1) matches back over the round trip, unlike a whole-app
82
+ // snapshot walk, whose cost scales with total app size/depth.
83
+ XCUIElement *focusedElement = (XCUIElement *)XCUIApplication.fb_activeApplication.fb_focusedElement;
84
+ if (nil == focusedElement) {
85
+ // Nothing is focused yet (e.g. right after the app became active) - let
86
+ // the caller retry on the next iteration.
87
+ return FBTVFocusStatePending;
88
+ }
89
+
90
+ // The snapshot was already taken as a side effect of resolving the query
91
+ // above, so grab it for free instead of paying for a new round trip.
92
+ id<FBXCElementSnapshot> focusedSnapshot = focusedElement.lastSnapshot
93
+ ?: focusedElement.fb_cachedSnapshot
94
+ ?: [focusedElement fb_customSnapshot];
95
+ *direction = [self directionTowardsTargetFromFocusedElementSnapshot:focusedSnapshot];
96
+
97
+ return FBTVFocusStatePending;
98
+ }
99
+
100
+ - (FBTVDirection)directionTowardsTargetFromFocusedElementSnapshot:(id<FBXCElementSnapshot>)focusedSnapshot
101
+ {
102
+ FBXCElementSnapshotWrapper *focused = [FBXCElementSnapshotWrapper ensureWrapped:focusedSnapshot];
70
103
  CGPoint focusedCenter = FBRectGetCenter(focused.wdFrame);
71
104
  FBTVNavigationItem *item = [self navigationItemWithElement:focused];
72
105
  CGFloat yDelta = self.targetCenter.y - focusedCenter.y;
@@ -83,7 +116,6 @@
83
116
  direction = [self verticalDirectionWithItem:item andDelta:yDelta];
84
117
  }
85
118
  }
86
-
87
119
  return direction;
88
120
  }
89
121
 
@@ -99,7 +131,7 @@
99
131
  if (nil != item) {
100
132
  return item;
101
133
  }
102
-
134
+
103
135
  item = [FBTVNavigationItem itemWithUid:uid];
104
136
  [self.navigationItems setObject:item forKey:uid];
105
137
  return item;
@@ -107,32 +139,35 @@
107
139
 
108
140
  - (FBTVDirection)horizontalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta
109
141
  {
110
- // GCFloat is double in 64bit. tvOS is only for arm64
111
- if (delta > DBL_EPSILON &&
112
- ![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionRight]]) {
113
- [item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionRight]];
114
- return FBTVDirectionRight;
115
- }
116
- if (delta < -DBL_EPSILON &&
117
- ![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionLeft]]) {
118
- [item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionLeft]];
119
- return FBTVDirectionLeft;
120
- }
121
- return FBTVDirectionNone;
142
+ return [self directionWithItem:item
143
+ delta:delta
144
+ positiveDirection:FBTVDirectionRight
145
+ negativeDirection:FBTVDirectionLeft];
122
146
  }
123
147
 
124
148
  - (FBTVDirection)verticalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta
125
149
  {
126
- // GCFloat is double in 64bit. tvOS is only for arm64
127
- if (delta > DBL_EPSILON &&
128
- ![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionDown]]) {
129
- [item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionDown]];
130
- return FBTVDirectionDown;
150
+ return [self directionWithItem:item
151
+ delta:delta
152
+ positiveDirection:FBTVDirectionDown
153
+ negativeDirection:FBTVDirectionUp];
154
+ }
155
+
156
+ - (FBTVDirection)directionWithItem:(FBTVNavigationItem *)item
157
+ delta:(CGFloat)delta
158
+ positiveDirection:(FBTVDirection)positiveDirection
159
+ negativeDirection:(FBTVDirection)negativeDirection
160
+ {
161
+ // CGFloat is double in 64bit. tvOS is only for arm64
162
+ NSNumber *positiveDirectionNumber = [NSNumber numberWithInteger:positiveDirection];
163
+ NSNumber *negativeDirectionNumber = [NSNumber numberWithInteger:negativeDirection];
164
+ if (delta > DBL_EPSILON && ![item.directions containsObject:positiveDirectionNumber]) {
165
+ [item.directions addObject:positiveDirectionNumber];
166
+ return positiveDirection;
131
167
  }
132
- if (delta < -DBL_EPSILON &&
133
- ![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionUp]]) {
134
- [item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionUp]];
135
- return FBTVDirectionUp;
168
+ if (delta < -DBL_EPSILON && ![item.directions containsObject:negativeDirectionNumber]) {
169
+ [item.directions addObject:negativeDirectionNumber];
170
+ return negativeDirection;
136
171
  }
137
172
  return FBTVDirectionNone;
138
173
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ #import <UIKit/UIKit.h>
10
+
11
+ @interface AppDelegate : UIResponder <UIApplicationDelegate>
12
+
13
+ @end
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ #import "AppDelegate.h"
10
+ #import "SceneDelegate.h"
11
+
12
+ @implementation AppDelegate
13
+
14
+ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
15
+ {
16
+ return YES;
17
+ }
18
+
19
+ - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options
20
+ {
21
+ UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
22
+ configuration.delegateClass = SceneDelegate.class;
23
+ return configuration;
24
+ }
25
+
26
+ @end
@@ -0,0 +1,41 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>en</string>
7
+ <key>CFBundleExecutable</key>
8
+ <string>$(EXECUTABLE_NAME)</string>
9
+ <key>CFBundleIdentifier</key>
10
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11
+ <key>CFBundleInfoDictionaryVersion</key>
12
+ <string>6.0</string>
13
+ <key>CFBundleName</key>
14
+ <string>$(PRODUCT_NAME)</string>
15
+ <key>CFBundlePackageType</key>
16
+ <string>APPL</string>
17
+ <key>CFBundleShortVersionString</key>
18
+ <string>1.0</string>
19
+ <key>CFBundleSignature</key>
20
+ <string>????</string>
21
+ <key>CFBundleVersion</key>
22
+ <string>1</string>
23
+ <key>UILaunchScreen</key>
24
+ <dict/>
25
+ <key>UIApplicationSceneManifest</key>
26
+ <dict>
27
+ <key>UIApplicationSupportsMultipleScenes</key>
28
+ <false/>
29
+ <key>UISceneConfigurations</key>
30
+ <dict>
31
+ <key>UIWindowSceneSessionRoleApplication</key>
32
+ <array>
33
+ <dict>
34
+ <key>UISceneConfigurationName</key>
35
+ <string>Default Configuration</string>
36
+ </dict>
37
+ </array>
38
+ </dict>
39
+ </dict>
40
+ </dict>
41
+ </plist>
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ #import <UIKit/UIKit.h>
10
+
11
+ @interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
12
+
13
+ @property (strong, nonatomic) UIWindow *window;
14
+
15
+ @end
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ #import "SceneDelegate.h"
10
+ #import "ViewController.h"
11
+
12
+ @implementation SceneDelegate
13
+
14
+ - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions
15
+ {
16
+ UIWindowScene *windowScene = (UIWindowScene *)scene;
17
+ self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
18
+ self.window.rootViewController = [ViewController new];
19
+ [self.window makeKeyAndVisible];
20
+ }
21
+
22
+ @end
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ #import <UIKit/UIKit.h>
10
+
11
+ @interface ViewController : UIViewController
12
+
13
+ @end
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ #import "ViewController.h"
10
+
11
+ static NSInteger const FBGridSize = 3;
12
+ static CGFloat const FBCellSide = 200;
13
+ static CGFloat const FBCellSpacing = 60;
14
+
15
+ @interface ViewController ()
16
+ @property (nonatomic, strong) UILabel *lastSelectedLabel;
17
+ @end
18
+
19
+ @implementation ViewController
20
+
21
+ - (void)viewDidLoad
22
+ {
23
+ [super viewDidLoad];
24
+ self.view.backgroundColor = UIColor.whiteColor;
25
+
26
+ UIStackView *rows = [UIStackView new];
27
+ rows.axis = UILayoutConstraintAxisVertical;
28
+ rows.spacing = FBCellSpacing;
29
+ rows.alignment = UIStackViewAlignmentCenter;
30
+ rows.translatesAutoresizingMaskIntoConstraints = NO;
31
+ [self.view addSubview:rows];
32
+
33
+ for (NSInteger row = 0; row < FBGridSize; row++) {
34
+ UIStackView *columns = [UIStackView new];
35
+ columns.axis = UILayoutConstraintAxisHorizontal;
36
+ columns.spacing = FBCellSpacing;
37
+ for (NSInteger column = 0; column < FBGridSize; column++) {
38
+ UIButton *cell = [self newCellWithIdentifier:[NSString stringWithFormat:@"cell_%ld_%ld", (long)row, (long)column]];
39
+ [columns addArrangedSubview:cell];
40
+ }
41
+ [rows addArrangedSubview:columns];
42
+ }
43
+
44
+ UIButton *disabledButton = [self newCellWithIdentifier:@"disabledButton"];
45
+ disabledButton.enabled = NO;
46
+ [rows addArrangedSubview:disabledButton];
47
+
48
+ self.lastSelectedLabel = [UILabel new];
49
+ self.lastSelectedLabel.accessibilityIdentifier = @"lastSelectedLabel";
50
+ self.lastSelectedLabel.text = @"none";
51
+ self.lastSelectedLabel.translatesAutoresizingMaskIntoConstraints = NO;
52
+ [self.view addSubview:self.lastSelectedLabel];
53
+
54
+ [NSLayoutConstraint activateConstraints:@[
55
+ [rows.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
56
+ [rows.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor],
57
+ [self.lastSelectedLabel.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
58
+ [self.lastSelectedLabel.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-FBCellSpacing],
59
+ ]];
60
+ }
61
+
62
+ - (UIButton *)newCellWithIdentifier:(NSString *)identifier
63
+ {
64
+ UIButton *cell = [UIButton buttonWithType:UIButtonTypeSystem];
65
+ cell.accessibilityIdentifier = identifier;
66
+ [cell setTitle:identifier forState:UIControlStateNormal];
67
+ cell.translatesAutoresizingMaskIntoConstraints = NO;
68
+ [cell.widthAnchor constraintEqualToConstant:FBCellSide].active = YES;
69
+ [cell.heightAnchor constraintEqualToConstant:FBCellSide].active = YES;
70
+ [cell addTarget:self action:@selector(didSelectCell:) forControlEvents:UIControlEventPrimaryActionTriggered];
71
+ return cell;
72
+ }
73
+
74
+ - (void)didSelectCell:(UIButton *)cell
75
+ {
76
+ self.lastSelectedLabel.text = cell.accessibilityIdentifier;
77
+ }
78
+
79
+ @end
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ #import <UIKit/UIKit.h>
10
+
11
+ #import "AppDelegate.h"
12
+
13
+ int main(int argc, char * argv[]) {
14
+ @autoreleasepool {
15
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass(AppDelegate.class));
16
+ }
17
+ }