ViewController.m 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. // Copyright 2019 Google
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import "ViewController.h"
  15. #import <FirebaseAnalytics/FirebaseAnalytics.h>
  16. #import <FirebaseCore/FIROptions.h>
  17. #import <FirebaseCore/FirebaseCore.h>
  18. #import <FirebaseInstanceID/FIRInstanceID+Private.h>
  19. #import <FirebaseRemoteConfig/FIRRemoteConfig_Private.h>
  20. #import <FirebaseRemoteConfig/FirebaseRemoteConfig.h>
  21. #import "FRCLog.h"
  22. static NSString *const FIRPerfNamespace = @"fireperf";
  23. static NSString *const FIRDefaultFIRAppName = @"__FIRAPP_DEFAULT";
  24. static NSString *const FIRSecondFIRAppName = @"secondFIRApp";
  25. @interface ViewController ()
  26. @property(nonatomic, strong) IBOutlet UIButton *fetchButton;
  27. @property(nonatomic, strong) IBOutlet UIButton *applyButton;
  28. @property(nonatomic, strong) IBOutlet UIButton *refreshButton;
  29. @property(nonatomic, strong) IBOutlet UIButton *clearLogsButton;
  30. @property(nonatomic, strong) IBOutlet UITextView *mainTextView;
  31. /// Key of custom variable to be added by user.
  32. @property(nonatomic, weak) IBOutlet UITextField *keyLabel;
  33. /// Value of custom variable to be added by user.
  34. @property(nonatomic, weak) IBOutlet UITextField *valueLabel;
  35. /// Expiration in seconds to be set by user.
  36. @property(nonatomic, weak) IBOutlet UITextField *expirationLabel;
  37. /// Config Defaults.
  38. @property(nonatomic, strong) NSMutableDictionary *configDefaults;
  39. /// developer mode switch.
  40. @property(strong, nonatomic) IBOutlet UISwitch *developerModeEnabled;
  41. /// Current selected namespace.
  42. @property(nonatomic, copy) NSString *currentNamespace;
  43. /// Curret selected FIRApp instance name.
  44. @property(nonatomic, copy) NSString *FIRAppName;
  45. /// Selected namespace picker control view.
  46. @property(nonatomic, strong) IBOutlet UIPickerView *namespacePicker;
  47. /// Selected app picker control view.
  48. @property(nonatomic, strong) IBOutlet UIPickerView *appPicker;
  49. /// Array of prepopulated namespaces supported by this app.
  50. @property(nonatomic, strong) NSArray<NSString *> *namespacePickerData;
  51. /// Array of prepopulated FIRApp names supported by this app.
  52. @property(nonatomic, strong) NSArray<NSString *> *appPickerData;
  53. /// Array of Remote Config instances.
  54. @property(nonatomic, strong) NSMutableDictionary *RCInstances;
  55. @end
  56. @implementation ViewController
  57. - (void)viewDidLoad {
  58. [super viewDidLoad];
  59. [[FRCLog sharedInstance] setLogView:self.mainTextView];
  60. [[FRCLog sharedInstance] logToConsole:@"Viewcontroller loaded"];
  61. [[FRCLog sharedInstance] logToConsole:[[NSBundle mainBundle] bundleIdentifier]];
  62. // Setup UI
  63. self.expirationLabel.text = [NSString stringWithFormat:@"0"];
  64. self.configDefaults = [[NSMutableDictionary alloc] init];
  65. self.keyLabel.delegate = self;
  66. self.valueLabel.delegate = self;
  67. self.expirationLabel.delegate = self;
  68. self.mainTextView.editable = NO;
  69. // TODO(mandard): Add support for deleting and adding namespaces in the app.
  70. self.namespacePickerData =
  71. [[NSArray alloc] initWithObjects:FIRNamespaceGoogleMobilePlatform, FIRPerfNamespace, nil];
  72. self.appPickerData =
  73. [[NSArray alloc] initWithObjects:FIRDefaultFIRAppName, FIRSecondFIRAppName, nil];
  74. self.RCInstances = [[NSMutableDictionary alloc] init];
  75. for (NSString *namespaceString in self.namespacePickerData) {
  76. for (NSString *appString in self.appPickerData) {
  77. // Check for the default instance.
  78. if (!self.RCInstances[namespaceString]) {
  79. self.RCInstances[namespaceString] = [[NSMutableDictionary alloc] init];
  80. }
  81. if ([namespaceString isEqualToString:FIRNamespaceGoogleMobilePlatform] &&
  82. [appString isEqualToString:FIRDefaultFIRAppName]) {
  83. self.RCInstances[namespaceString][appString] = [FIRRemoteConfig remoteConfig];
  84. } else {
  85. FIRApp *firebaseApp = ([appString isEqualToString:FIRDefaultFIRAppName])
  86. ? [FIRApp defaultApp]
  87. : [FIRApp appNamed:appString];
  88. self.RCInstances[namespaceString][appString] =
  89. [FIRRemoteConfig remoteConfigWithFIRNamespace:namespaceString app:firebaseApp];
  90. }
  91. FIRRemoteConfigSettings *settings = [[FIRRemoteConfigSettings alloc] init];
  92. settings.fetchTimeout = 300;
  93. settings.minimumFetchInterval = 0;
  94. ((FIRRemoteConfig *)(self.RCInstances[namespaceString][appString])).configSettings = settings;
  95. }
  96. }
  97. [[FRCLog sharedInstance] logToConsole:@"RC instances inited"];
  98. self.namespacePicker.dataSource = self;
  99. self.namespacePicker.delegate = self;
  100. self.appPicker.dataSource = self;
  101. self.appPicker.delegate = self;
  102. [self.developerModeEnabled setOn:true animated:false];
  103. }
  104. - (IBAction)fetchButtonPressed:(id)sender {
  105. [[FRCLog sharedInstance] logToConsole:@"Fetch button pressed"];
  106. // fetchConfig api callback, this is triggered when client receives response from server
  107. ViewController *__weak weakSelf = self;
  108. FIRRemoteConfigFetchCompletion completion = ^(FIRRemoteConfigFetchStatus status, NSError *error) {
  109. ViewController *strongSelf = weakSelf;
  110. if (!strongSelf) {
  111. return;
  112. }
  113. [[FRCLog sharedInstance]
  114. logToConsole:[NSString stringWithFormat:@"Fetch completed. Status=%@",
  115. [strongSelf statusString:status]]];
  116. if (error) {
  117. [[FRCLog sharedInstance] logToConsole:[NSString stringWithFormat:@"Fetch Error=%@", error]];
  118. }
  119. NSMutableString *output = [NSMutableString
  120. stringWithFormat:@"Fetch status : %@.\n\n", [strongSelf statusString:status]];
  121. if (error) {
  122. [output appendFormat:@"%@\n", error];
  123. }
  124. if (status == FIRRemoteConfigFetchStatusFailure) {
  125. [output appendString:[NSString stringWithFormat:@"Fetch Error :%@.\n",
  126. [strongSelf errorString:error.code]]];
  127. if (error.code == FIRRemoteConfigErrorThrottled) {
  128. NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  129. NSNumber *throttledTime =
  130. (NSNumber *)error.userInfo[FIRRemoteConfigThrottledEndTimeInSecondsKey];
  131. NSDate *date = [NSDate dateWithTimeIntervalSince1970:[throttledTime doubleValue]];
  132. [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
  133. NSString *timeString = [dateFormatter stringFromDate:date];
  134. [output appendString:[NSString stringWithFormat:@"Throttled end at: %@ \n", timeString]];
  135. }
  136. }
  137. [[FRCLog sharedInstance] logToConsole:output];
  138. };
  139. // fetchConfig api call
  140. [[FRCLog sharedInstance] logToConsole:@"Calling fetchWithExpirationDuration.."];
  141. [self.RCInstances[self.currentNamespace][self.FIRAppName]
  142. fetchWithExpirationDuration:self.expirationLabel.text.integerValue
  143. completionHandler:completion];
  144. }
  145. - (IBAction)fetchAndActivateButtonPressed:(id)sender {
  146. // fetchConfig api callback, this is triggered when client receives response from server
  147. ViewController *__weak weakSelf = self;
  148. FIRRemoteConfigFetchAndActivateCompletion fetchAndActivateCompletion = ^(
  149. FIRRemoteConfigFetchAndActivateStatus status, NSError *error) {
  150. ViewController *strongSelf = weakSelf;
  151. if (!strongSelf) {
  152. return;
  153. }
  154. NSMutableString *output = [NSMutableString
  155. stringWithFormat:@"Fetch and activate status : %@.\n\n",
  156. (status == FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote ||
  157. status == FIRRemoteConfigFetchAndActivateStatusSuccessUsingPreFetchedData)
  158. ? @"Success"
  159. : [NSString
  160. stringWithFormat:@"Failure: %@", [error localizedDescription]]];
  161. if (error) {
  162. [output appendFormat:@"%@\n", error];
  163. }
  164. if (status == FIRRemoteConfigFetchAndActivateStatusError) {
  165. [output appendString:[NSString stringWithFormat:@"Fetch And Activate Error :%@.\n",
  166. [strongSelf errorString:error.code]]];
  167. if (error.code == FIRRemoteConfigErrorThrottled) {
  168. [output appendString:[NSString stringWithFormat:@"Throttled.\n"]];
  169. }
  170. }
  171. // activate status
  172. [[FRCLog sharedInstance] logToConsole:output];
  173. if (status == FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote ||
  174. status == FIRRemoteConfigFetchAndActivateStatusSuccessUsingPreFetchedData) {
  175. [strongSelf printResult:[[NSMutableString alloc] init]];
  176. }
  177. };
  178. // fetchConfig api call
  179. [self.RCInstances[self.currentNamespace][self.FIRAppName]
  180. fetchAndActivateWithCompletionHandler:fetchAndActivateCompletion];
  181. }
  182. - (IBAction)activateButtonPressed:(id)sender {
  183. [self apply];
  184. }
  185. - (IBAction)refreshButtonPressed:(id)sender {
  186. NSMutableString *output = [[NSMutableString alloc] init];
  187. [self printResult:output];
  188. }
  189. - (IBAction)setDefaultFromPlistButtonPressed:(id)sender {
  190. [self.RCInstances[self.currentNamespace][self.FIRAppName]
  191. setDefaultsFromPlistFileName:@"Defaults"];
  192. [self printDefaultConfigs];
  193. }
  194. - (IBAction)setDefaultButtonPressed:(id)sender {
  195. if (self.configDefaults.count) {
  196. [self.RCInstances[self.currentNamespace][self.FIRAppName] setDefaults:self.configDefaults];
  197. [self.configDefaults removeAllObjects];
  198. [self printDefaultConfigs];
  199. } else {
  200. [[FRCLog sharedInstance] logToConsole:@"Nothing to set for defaults."];
  201. }
  202. }
  203. - (IBAction)onClearLogsButtonPressed:(id)sender {
  204. self.mainTextView.text = @"";
  205. }
  206. - (void)printDefaultConfigs {
  207. NSMutableString *output = [[NSMutableString alloc] init];
  208. [output appendString:@"\n-------Default config------\n"];
  209. NSArray<NSString *> *result = [self.RCInstances[self.currentNamespace][self.FIRAppName]
  210. allKeysFromSource:FIRRemoteConfigSourceDefault];
  211. if (result) {
  212. NSString *stringPerNs = @"";
  213. for (NSString *key in result) {
  214. FIRRemoteConfigValue *value =
  215. [self.RCInstances[self.currentNamespace][self.FIRAppName] defaultValueForKey:key];
  216. stringPerNs = [NSString stringWithFormat:@"%@%@ : %@ : %@\n", stringPerNs,
  217. self.currentNamespace, key, value.stringValue];
  218. }
  219. [output appendString:stringPerNs];
  220. }
  221. [[FRCLog sharedInstance] logToConsole:output];
  222. }
  223. - (IBAction)getValueButtonPressed:(id)sender {
  224. [[FRCLog sharedInstance] logToConsole:[self.RCInstances[self.currentNamespace][self.FIRAppName]
  225. configValueForKey:self.keyLabel.text]
  226. .debugDescription];
  227. }
  228. - (IBAction)logValueButtonPressed:(id)sender {
  229. [[FRCLog sharedInstance]
  230. logToConsole:[NSString stringWithFormat:@"key: %@ logged", self.keyLabel.text]];
  231. }
  232. // add default variable button pressed
  233. - (IBAction)addButtonPressed:(id)sender {
  234. [self addNewEntryToVariables:self.configDefaults isDefaults:YES];
  235. }
  236. - (IBAction)developerModeSwitched:(id)sender {
  237. FIRRemoteConfigSettings *configSettings =
  238. [[FIRRemoteConfigSettings alloc] initWithDeveloperModeEnabled:self.developerModeEnabled.isOn];
  239. ((FIRRemoteConfig *)(self.RCInstances[self.currentNamespace][self.FIRAppName])).configSettings =
  240. configSettings;
  241. [[FRCLog sharedInstance]
  242. logToConsole:[NSString
  243. stringWithFormat:@"Developer Mode Enabled: %d\n",
  244. ((FIRRemoteConfig *)(self.RCInstances[self.currentNamespace]
  245. [self.FIRAppName]))
  246. .configSettings.isDeveloperModeEnabled]];
  247. }
  248. - (void)addNewEntryToVariables:(NSMutableDictionary *)variables isDefaults:(BOOL)isDefaults {
  249. if ([self.keyLabel.text length]) {
  250. variables[self.keyLabel.text] = self.valueLabel.text;
  251. NSString *showText = @"custom variables ";
  252. if (isDefaults) {
  253. showText = @"config defaults";
  254. }
  255. [[FRCLog sharedInstance]
  256. logToConsole:[NSString stringWithFormat:@"New %@ added %@ : %@\n", showText,
  257. self.keyLabel.text, self.valueLabel.text]];
  258. self.keyLabel.text = @"";
  259. self.valueLabel.text = @"";
  260. }
  261. }
  262. - (void)apply {
  263. [self.RCInstances[self.currentNamespace][self.FIRAppName]
  264. activateWithCompletionHandler:^(NSError *_Nullable error) {
  265. NSMutableString *output = [[NSMutableString alloc] init];
  266. [output appendString:[NSString stringWithFormat:@"ActivateFetched = %@\n",
  267. error ? @"NO" : @"YES"]];
  268. [[FRCLog sharedInstance] logToConsole:output];
  269. if (!error) {
  270. [self printResult:output];
  271. } else {
  272. [self printResult:[[NSString stringWithFormat:@"Activate failed. Error: %@",
  273. error.localizedDescription] mutableCopy]];
  274. }
  275. }];
  276. }
  277. // print out fetch result
  278. - (void)printResult:(NSMutableString *)output {
  279. FIRRemoteConfig *currentRCInstance = self.RCInstances[self.currentNamespace][self.FIRAppName];
  280. NSString *namespace_p = self.currentNamespace;
  281. [output appendString:@"-------Active config------\n"];
  282. NSArray<NSString *> *result = [self.RCInstances[self.currentNamespace][self.FIRAppName]
  283. allKeysFromSource:FIRRemoteConfigSourceRemote];
  284. if (result) {
  285. NSString *stringPerNs = @"";
  286. for (NSString *key in result) {
  287. FIRRemoteConfigValue *value = [currentRCInstance configValueForKey:key];
  288. stringPerNs = [NSString
  289. stringWithFormat:@"%@%@ : %@ : %@\n", stringPerNs, namespace_p, key, value.stringValue];
  290. }
  291. [output appendString:stringPerNs];
  292. }
  293. [output appendString:@"\n-------Default config------\n"];
  294. result = [currentRCInstance allKeysFromSource:FIRRemoteConfigSourceDefault];
  295. if (result) {
  296. NSString *stringPerNs = @"";
  297. for (NSString *key in result) {
  298. FIRRemoteConfigValue *value = [currentRCInstance defaultValueForKey:key];
  299. stringPerNs = [NSString
  300. stringWithFormat:@"%@%@ : %@ : %@\n", stringPerNs, namespace_p, key, value.stringValue];
  301. }
  302. [output appendString:stringPerNs];
  303. }
  304. [output appendString:@"\n--------Custom Variables--------\n"];
  305. [output
  306. appendString:[NSString
  307. stringWithFormat:@"Developer Mode Enabled: %d\n",
  308. currentRCInstance.configSettings.isDeveloperModeEnabled]];
  309. [output appendString:@"\n----------Last fetch time----------------\n"];
  310. NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  311. [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
  312. [output
  313. appendString:[NSString stringWithFormat:@"%@\n",
  314. [dateFormatter
  315. stringFromDate:currentRCInstance.lastFetchTime]]];
  316. [output appendString:@"\n-----------Last fetch status------------\n"];
  317. [output appendString:[NSString
  318. stringWithFormat:@"%@\n",
  319. [self statusString:currentRCInstance.lastFetchStatus]]];
  320. FIRInstanceID *instanceID = [FIRInstanceID instanceID];
  321. [instanceID getIDWithHandler:^(NSString *_Nullable identity, NSError *_Nullable error) {
  322. [output appendString:@"\n-----------Instance ID------------------\n"];
  323. [output appendString:[NSString stringWithFormat:@"%@\n", identity]];
  324. [output appendString:@"\n-----------Instance ID token------------\n"];
  325. [output
  326. appendString:[NSString stringWithFormat:@"%@\n",
  327. currentRCInstance.settings.configInstanceIDToken]];
  328. [output appendString:@"\n-----------Android ID------------\n"];
  329. [output
  330. appendString:[NSString stringWithFormat:@"%@\n", currentRCInstance.settings.deviceAuthID]];
  331. [[FRCLog sharedInstance] logToConsole:output];
  332. }];
  333. }
  334. - (BOOL)textFieldShouldReturn:(UITextField *)textField {
  335. [textField resignFirstResponder];
  336. return YES;
  337. }
  338. - (NSString *)statusString:(FIRRemoteConfigFetchStatus)status {
  339. switch (status) {
  340. case FIRRemoteConfigFetchStatusSuccess:
  341. return @"Success";
  342. case FIRRemoteConfigFetchStatusNoFetchYet:
  343. return @"NotFetchYet";
  344. case FIRRemoteConfigFetchStatusFailure:
  345. return @"Failure";
  346. case FIRRemoteConfigFetchStatusThrottled:
  347. return @"Throttled";
  348. default:
  349. return @"Unknown";
  350. }
  351. return @"";
  352. }
  353. - (NSString *)errorString:(FIRRemoteConfigError)error {
  354. switch (error) {
  355. case FIRRemoteConfigErrorInternalError:
  356. return @"Internal Error";
  357. case FIRRemoteConfigErrorUnknown:
  358. return @"Unknown Error";
  359. case FIRRemoteConfigErrorThrottled:
  360. return @"Throttled";
  361. default:
  362. return @"unknown";
  363. }
  364. return @"";
  365. }
  366. - (IBAction)fetchIIDButtonClicked:(id)sender {
  367. FIRInstanceID *instanceID = [FIRInstanceID instanceID];
  368. FIRInstanceIDTokenHandler instanceIDHandler = ^(NSString *token, NSError *error) {
  369. if (error) {
  370. [[FRCLog sharedInstance] logToConsole:[NSString stringWithFormat:@"%@", error]];
  371. }
  372. if (token) {
  373. ((FIRRemoteConfig *)self.RCInstances[self.currentNamespace][self.FIRAppName])
  374. .settings.configInstanceIDToken = token;
  375. [instanceID getIDWithHandler:^(NSString *_Nullable identity, NSError *_Nullable error) {
  376. [[FRCLog sharedInstance]
  377. logToConsole:[NSString
  378. stringWithFormat:
  379. @"Successfully getting InstanceID : \n\n%@\n\nToken : \n\n%@\n",
  380. identity, token]];
  381. }];
  382. }
  383. };
  384. [instanceID tokenWithAuthorizedEntity:[FIRApp appNamed:self.FIRAppName].options.GCMSenderID
  385. scope:@"*"
  386. options:nil
  387. handler:instanceIDHandler];
  388. }
  389. - (IBAction)searchButtonClicked:(id)sender {
  390. NSString *output = @"-------Active Config------\n";
  391. for (NSString *key in [self.RCInstances[self.currentNamespace][self.FIRAppName]
  392. keysWithPrefix:self.keyLabel.text]) {
  393. FIRRemoteConfigValue *value =
  394. ((FIRRemoteConfig *)(self.RCInstances[self.currentNamespace][self.FIRAppName]))[key];
  395. output = [NSString stringWithFormat:@"%@%@ : %@ : %@\n", output, self.currentNamespace, key,
  396. value.stringValue];
  397. }
  398. [[FRCLog sharedInstance] logToConsole:output];
  399. }
  400. - (IBAction)userPropertyButtonClicked:(id)sender {
  401. [FIRAnalytics setUserPropertyString:self.valueLabel.text forName:self.keyLabel.text];
  402. NSString *output = [NSString
  403. stringWithFormat:@"Set User Property => %@ : %@\n", self.keyLabel.text, self.valueLabel.text];
  404. [[FRCLog sharedInstance] logToConsole:output];
  405. }
  406. #pragma mark - picker
  407. // The number of columns of data
  408. - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
  409. // App and currentNamespace pickers.
  410. return 2;
  411. }
  412. // The number of rows of data
  413. - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
  414. NSInteger rowCount = (component == 0) ? self.namespacePickerData.count : self.appPickerData.count;
  415. return rowCount;
  416. }
  417. // The data to return for the row and component (column) that's being passed in
  418. - (NSString *)pickerView:(UIPickerView *)pickerView
  419. titleForRow:(NSInteger)row
  420. forComponent:(NSInteger)component {
  421. if (component == 0) {
  422. self.currentNamespace = self.namespacePickerData[row];
  423. return self.namespacePickerData[row];
  424. } else {
  425. self.FIRAppName = self.appPickerData[row];
  426. return self.appPickerData[row];
  427. }
  428. }
  429. - (UIView *)pickerView:(UIPickerView *)pickerView
  430. viewForRow:(NSInteger)row
  431. forComponent:(NSInteger)component
  432. reusingView:(UIView *)view {
  433. UILabel *tView = (UILabel *)view;
  434. if (!tView) {
  435. tView = [[UILabel alloc] init];
  436. [tView setFont:[UIFont fontWithName:@"Helvetica" size:15]];
  437. tView.numberOfLines = 3;
  438. }
  439. if (component == 0) {
  440. self.currentNamespace = self.namespacePickerData[row];
  441. tView.text = self.namespacePickerData[row];
  442. } else {
  443. self.FIRAppName = self.appPickerData[row];
  444. tView.text = self.appPickerData[row];
  445. }
  446. return tView;
  447. }
  448. @end