@@ -15,6 +15,7 @@ import '../base/process.dart';
1515import '../base/utils.dart' ;
1616import '../build_info.dart' ;
1717import '../convert.dart' ;
18+ import '../doctor_validator.dart' ;
1819import '../globals.dart' as globals;
1920import '../ios/application_package.dart' ;
2021import '../ios/mac.dart' ;
@@ -277,7 +278,27 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
277278 .toList ();
278279 }
279280
280- Future <void > _validateIconAssetsAfterArchive (StringBuffer messageBuffer) async {
281+ ValidationResult ? _createValidationResult (String title, List <ValidationMessage > messages) {
282+ if (messages.isEmpty) {
283+ return null ;
284+ }
285+ final bool anyInvalid = messages.any ((ValidationMessage message) => message.type != ValidationMessageType .information);
286+ return ValidationResult (
287+ anyInvalid ? ValidationType .partial : ValidationType .success,
288+ messages,
289+ statusInfo: title,
290+ );
291+ }
292+
293+ ValidationMessage _createValidationMessage ({
294+ required bool isValid,
295+ required String message,
296+ }) {
297+ // Use "information" type for valid message, and "hint" type for invalid message.
298+ return isValid ? ValidationMessage (message) : ValidationMessage .hint (message);
299+ }
300+
301+ Future <List <ValidationMessage >> _validateIconAssetsAfterArchive () async {
281302 final BuildableIOSApp app = await buildableIOSApp;
282303
283304 final Map <_ImageAssetFileKey , String > templateInfoMap = _parseImageAssetContentsJson (
@@ -287,24 +308,35 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
287308 app.projectAppIconDirName,
288309 requiresSize: true );
289310
311+ final List <ValidationMessage > validationMessages = < ValidationMessage > [];
312+
290313 final bool usesTemplate = _isAssetStillUsingTemplateFiles (
291314 templateImageInfoMap: templateInfoMap,
292315 projectImageInfoMap: projectInfoMap,
293316 templateImageDirName: await app.templateAppIconDirNameForImages,
294317 projectImageDirName: app.projectAppIconDirName);
318+
295319 if (usesTemplate) {
296- messageBuffer.writeln ('\n Warning: App icon is set to the default placeholder icon. Replace with unique icons.' );
320+ validationMessages.add (_createValidationMessage (
321+ isValid: false ,
322+ message: 'App icon is set to the default placeholder icon. Replace with unique icons.' ,
323+ ));
297324 }
298325
299326 final List <String > filesWithWrongSize = _imageFilesWithWrongSize (
300327 imageInfoMap: projectInfoMap,
301328 imageDirName: app.projectAppIconDirName);
329+
302330 if (filesWithWrongSize.isNotEmpty) {
303- messageBuffer.writeln ('\n Warning: App icon is using the wrong size (e.g. ${filesWithWrongSize .first }).' );
331+ validationMessages.add (_createValidationMessage (
332+ isValid: false ,
333+ message: 'App icon is using the incorrect size (e.g. ${filesWithWrongSize .first }).' ,
334+ ));
304335 }
336+ return validationMessages;
305337 }
306338
307- Future <void > _validateLaunchImageAssetsAfterArchive (StringBuffer messageBuffer ) async {
339+ Future <List < ValidationMessage >> _validateLaunchImageAssetsAfterArchive () async {
308340 final BuildableIOSApp app = await buildableIOSApp;
309341
310342 final Map <_ImageAssetFileKey , String > templateInfoMap = _parseImageAssetContentsJson (
@@ -314,25 +346,32 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
314346 app.projectLaunchImageDirName,
315347 requiresSize: false );
316348
349+ final List <ValidationMessage > validationMessages = < ValidationMessage > [];
350+
317351 final bool usesTemplate = _isAssetStillUsingTemplateFiles (
318352 templateImageInfoMap: templateInfoMap,
319353 projectImageInfoMap: projectInfoMap,
320354 templateImageDirName: await app.templateLaunchImageDirNameForImages,
321355 projectImageDirName: app.projectLaunchImageDirName);
322356
323357 if (usesTemplate) {
324- messageBuffer.writeln ('\n Warning: Launch image is set to the default placeholder. Replace with unique launch images.' );
358+ validationMessages.add (_createValidationMessage (
359+ isValid: false ,
360+ message: 'Launch image is set to the default placeholder icon. Replace with unique launch image.' ,
361+ ));
325362 }
363+
364+ return validationMessages;
326365 }
327366
328- Future <void > _validateXcodeBuildSettingsAfterArchive (StringBuffer messageBuffer ) async {
367+ Future <List < ValidationMessage >> _validateXcodeBuildSettingsAfterArchive () async {
329368 final BuildableIOSApp app = await buildableIOSApp;
330369
331370 final String plistPath = app.builtInfoPlistPathAfterArchive;
332371
333372 if (! globals.fs.file (plistPath).existsSync ()) {
334373 globals.printError ('Invalid iOS archive. Does not contain Info.plist.' );
335- return ;
374+ return < ValidationMessage > [] ;
336375 }
337376
338377 final Map <String , String ?> xcodeProjectSettingsMap = < String , String ? > {};
@@ -343,17 +382,32 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
343382 xcodeProjectSettingsMap['Deployment Target' ] = globals.plistParser.getStringValueFromFile (plistPath, PlistParser .kMinimumOSVersionKey);
344383 xcodeProjectSettingsMap['Bundle Identifier' ] = globals.plistParser.getStringValueFromFile (plistPath, PlistParser .kCFBundleIdentifierKey);
345384
346- xcodeProjectSettingsMap.forEach ((String title, String ? info) {
347- messageBuffer.writeln ('$title : ${info ?? "Missing" }' );
348- });
385+ final List <ValidationMessage > validationMessages = xcodeProjectSettingsMap.entries.map ((MapEntry <String , String ?> entry) {
386+ final String title = entry.key;
387+ final String ? info = entry.value;
388+ return _createValidationMessage (
389+ isValid: info != null ,
390+ message: '$title : ${info ?? "Missing" }' ,
391+ );
392+ }).toList ();
349393
350- if (xcodeProjectSettingsMap.values.any ((String ? element) => element == null )) {
351- messageBuffer.writeln ('\n You must set up the missing settings.' );
394+ final bool hasMissingSettings = xcodeProjectSettingsMap.values.any ((String ? element) => element == null );
395+ if (hasMissingSettings) {
396+ validationMessages.add (_createValidationMessage (
397+ isValid: false ,
398+ message: 'You must set up the missing app settings.' ),
399+ );
352400 }
353401
354- if (xcodeProjectSettingsMap['Bundle Identifier' ]? .startsWith ('com.example' ) ?? false ) {
355- messageBuffer.writeln ('\n Warning: Your application still contains the default "com.example" bundle identifier.' );
402+ final bool usesDefaultBundleIdentifier = xcodeProjectSettingsMap['Bundle Identifier' ]? .startsWith ('com.example' ) ?? false ;
403+ if (usesDefaultBundleIdentifier) {
404+ validationMessages.add (_createValidationMessage (
405+ isValid: false ,
406+ message: 'Your application still contains the default "com.example" bundle identifier.' ),
407+ );
356408 }
409+
410+ return validationMessages;
357411 }
358412
359413 @override
@@ -362,13 +416,26 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
362416 displayNullSafetyMode (buildInfo);
363417 final FlutterCommandResult xcarchiveResult = await super .runCommand ();
364418
365- final StringBuffer validationMessageBuffer = StringBuffer ();
366- await _validateXcodeBuildSettingsAfterArchive (validationMessageBuffer);
367- await _validateIconAssetsAfterArchive (validationMessageBuffer);
368- await _validateLaunchImageAssetsAfterArchive (validationMessageBuffer);
369-
370- validationMessageBuffer.write ('\n To update the settings, please refer to https://docs.flutter.dev/deployment/ios' );
371- globals.printBox (validationMessageBuffer.toString (), title: 'App Settings' );
419+ final List <ValidationResult ?> validationResults = < ValidationResult ? > [];
420+ validationResults.add (_createValidationResult (
421+ 'App Settings Validation' ,
422+ await _validateXcodeBuildSettingsAfterArchive (),
423+ ));
424+ validationResults.add (_createValidationResult (
425+ 'App Icon and Launch Image Assets Validation' ,
426+ await _validateIconAssetsAfterArchive () + await _validateLaunchImageAssetsAfterArchive (),
427+ ));
428+
429+ for (final ValidationResult result in validationResults.whereType <ValidationResult >()) {
430+ globals.printStatus ('\n ${result .coloredLeadingBox } ${result .statusInfo }' );
431+ for (final ValidationMessage message in result.messages) {
432+ globals.printStatus (
433+ '${message .coloredIndicator } ${message .message }' ,
434+ indent: result.leadingBox.length + 1 ,
435+ );
436+ }
437+ }
438+ globals.printStatus ('\n To update the settings, please refer to https://docs.flutter.dev/deployment/ios\n ' );
372439
373440 // xcarchive failed or not at expected location.
374441 if (xcarchiveResult.exitStatus != ExitStatus .success) {
0 commit comments