@@ -57,30 +57,32 @@ class BuildIOSCommand extends _BuildIOSSubCommand {
5757 Directory _outputAppDirectory (String xcodeResultOutput) => globals.fs.directory (xcodeResultOutput).parent;
5858}
5959
60- /// The key that uniquely identifies an image file in an app icon asset.
61- /// It consists of (idiom, size, scale).
60+ /// The key that uniquely identifies an image file in an image asset.
61+ /// It consists of (idiom, scale, size?), where size is present for app icon
62+ /// asset, and null for launch image asset.
6263@immutable
63- class _AppIconImageFileKey {
64- const _AppIconImageFileKey (this .idiom, this .size , this .scale );
64+ class _ImageAssetFileKey {
65+ const _ImageAssetFileKey (this .idiom, this .scale , this .size );
6566
6667 /// The idiom (iphone or ipad).
6768 final String idiom;
68- /// The logical size in point (e.g. 83.5).
69- final double size;
7069 /// The scale factor (e.g. 2).
7170 final int scale;
71+ /// The logical size in point (e.g. 83.5).
72+ /// Size is present for app icon, and null for launch image.
73+ final double ? size;
7274
7375 @override
74- int get hashCode => Object .hash (idiom, size, scale );
76+ int get hashCode => Object .hash (idiom, scale, size );
7577
7678 @override
77- bool operator == (Object other) => other is _AppIconImageFileKey
79+ bool operator == (Object other) => other is _ImageAssetFileKey
7880 && other.idiom == idiom
79- && other.size == size
80- && other.scale == scale ;
81+ && other.scale == scale
82+ && other.size == size ;
8183
82- /// The pixel size.
83- int get pixelSize => (size * scale).toInt (); // pixel size must be an int.
84+ /// The pixel size based on logical size and scale .
85+ int ? get pixelSize => size == null ? null : (size! * scale).toInt (); // pixel size must be an int.
8486}
8587
8688/// Builds an .xcarchive and optionally .ipa for an iOS app to be generated for
@@ -159,108 +161,170 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
159161 return super .validateCommand ();
160162 }
161163
162- // Parses Contents.json into a map, with the key to be _AppIconImageFileKey, and value to be the icon image file name.
163- Map <_AppIconImageFileKey , String > _parseIconContentsJson (String contentsJsonDirName) {
164+ // A helper function to parse Contents.json of an image asset into a map,
165+ // with the key to be _ImageAssetFileKey, and value to be the image file name.
166+ // Some assets have size (e.g. app icon) and others do not (e.g. launch image).
167+ Map <_ImageAssetFileKey , String > _parseImageAssetContentsJson (
168+ String contentsJsonDirName,
169+ { required bool requiresSize })
170+ {
164171 final Directory contentsJsonDirectory = globals.fs.directory (contentsJsonDirName);
165172 if (! contentsJsonDirectory.existsSync ()) {
166- return < _AppIconImageFileKey , String > {};
173+ return < _ImageAssetFileKey , String > {};
167174 }
168175 final File contentsJsonFile = contentsJsonDirectory.childFile ('Contents.json' );
169176 final Map <String , dynamic > contents = json.decode (contentsJsonFile.readAsStringSync ()) as Map <String , dynamic >? ?? < String , dynamic > {};
170177 final List <dynamic > images = contents['images' ] as List <dynamic >? ?? < dynamic > [];
171178 final Map <String , dynamic > info = contents['info' ] as Map <String , dynamic >? ?? < String , dynamic > {};
172179 if ((info['version' ] as int ? ) != 1 ) {
173180 // Skips validation for unknown format.
174- return < _AppIconImageFileKey , String > {};
181+ return < _ImageAssetFileKey , String > {};
175182 }
176183
177- final Map <_AppIconImageFileKey , String > iconInfo = < _AppIconImageFileKey , String > {};
184+ final Map <_ImageAssetFileKey , String > iconInfo = < _ImageAssetFileKey , String > {};
178185 for (final dynamic image in images) {
179186 final Map <String , dynamic > imageMap = image as Map <String , dynamic >;
180187 final String ? idiom = imageMap['idiom' ] as String ? ;
181188 final String ? size = imageMap['size' ] as String ? ;
182189 final String ? scale = imageMap['scale' ] as String ? ;
183190 final String ? fileName = imageMap['filename' ] as String ? ;
184191
185- if (size == null || idiom == null || scale == null || fileName == null ) {
192+ // requiresSize must match the actual presence of size in json.
193+ if (requiresSize != (size != null )
194+ || idiom == null || scale == null || fileName == null )
195+ {
186196 continue ;
187197 }
188198
189- // for example, "64x64". Parse the width since it is a square.
190- final Iterable <double > parsedSizes = size.split ('x' )
199+ final double ? parsedSize;
200+ if (size != null ) {
201+ // for example, "64x64". Parse the width since it is a square.
202+ final Iterable <double > parsedSizes = size.split ('x' )
191203 .map ((String element) => double .tryParse (element))
192204 .whereType <double >();
193- if (parsedSizes.isEmpty) {
194- continue ;
205+ if (parsedSizes.isEmpty) {
206+ continue ;
207+ }
208+ parsedSize = parsedSizes.first;
209+ } else {
210+ parsedSize = null ;
195211 }
196- final double parsedSize = parsedSizes.first;
197212
198213 // for example, "3x".
199214 final Iterable <int > parsedScales = scale.split ('x' )
200- .map ((String element) => int .tryParse (element))
201- .whereType <int >();
215+ .map ((String element) => int .tryParse (element))
216+ .whereType <int >();
202217 if (parsedScales.isEmpty) {
203218 continue ;
204219 }
205220 final int parsedScale = parsedScales.first;
206-
207- iconInfo[_AppIconImageFileKey (idiom, parsedSize, parsedScale)] = fileName;
221+ iconInfo[_ImageAssetFileKey (idiom, parsedScale, parsedSize)] = fileName;
208222 }
209-
210223 return iconInfo;
211224 }
212225
213- Future <void > _validateIconsAfterArchive (StringBuffer messageBuffer) async {
214- final BuildableIOSApp app = await buildableIOSApp;
215- final String templateIconImageDirName = await app.templateAppIconDirNameForImages;
216-
217- final Map <_AppIconImageFileKey , String > templateIconMap = _parseIconContentsJson (app.templateAppIconDirNameForContentsJson);
218- final Map <_AppIconImageFileKey , String > projectIconMap = _parseIconContentsJson (app.projectAppIconDirName);
219-
220- // validate each of the project icon images.
221- final List <String > filesWithTemplateIcon = < String > [];
222- final List <String > filesWithWrongSize = < String > [];
223- for (final MapEntry <_AppIconImageFileKey , String > entry in projectIconMap.entries) {
224- final String projectIconFileName = entry.value;
225- final String ? templateIconFileName = templateIconMap[entry.key];
226- final File projectIconFile = globals.fs.file (globals.fs.path.join (app.projectAppIconDirName, projectIconFileName));
227- if (! projectIconFile.existsSync ()) {
228- continue ;
229- }
230- final Uint8List projectIconBytes = projectIconFile.readAsBytesSync ();
231-
232- // validate conflict with template icon file.
233- if (templateIconFileName != null ) {
234- final File templateIconFile = globals.fs.file (globals.fs.path.join (
235- templateIconImageDirName, templateIconFileName));
236- if (templateIconFile.existsSync () && md5.convert (projectIconBytes) ==
237- md5.convert (templateIconFile.readAsBytesSync ())) {
238- filesWithTemplateIcon.add (entry.value);
239- }
226+ // A helper function to check if an image asset is still using template files.
227+ bool _isAssetStillUsingTemplateFiles ({
228+ required Map <_ImageAssetFileKey , String > templateImageInfoMap,
229+ required Map <_ImageAssetFileKey , String > projectImageInfoMap,
230+ required String templateImageDirName,
231+ required String projectImageDirName,
232+ }) {
233+ return projectImageInfoMap.entries.any ((MapEntry <_ImageAssetFileKey , String > entry) {
234+ final String projectFileName = entry.value;
235+ final String ? templateFileName = templateImageInfoMap[entry.key];
236+ if (templateFileName == null ) {
237+ return false ;
240238 }
239+ final File projectFile = globals.fs.file (
240+ globals.fs.path.join (projectImageDirName, projectFileName));
241+ final File templateFile = globals.fs.file (
242+ globals.fs.path.join (templateImageDirName, templateFileName));
243+
244+ return projectFile.existsSync ()
245+ && templateFile.existsSync ()
246+ && md5.convert (projectFile.readAsBytesSync ()) ==
247+ md5.convert (templateFile.readAsBytesSync ());
248+ });
249+ }
241250
251+ // A helper function to return a list of image files in an image asset with
252+ // wrong sizes (as specified in its Contents.json file).
253+ List <String > _imageFilesWithWrongSize ({
254+ required Map <_ImageAssetFileKey , String > imageInfoMap,
255+ required String imageDirName,
256+ }) {
257+ return imageInfoMap.entries.where ((MapEntry <_ImageAssetFileKey , String > entry) {
258+ final String fileName = entry.value;
259+ final File imageFile = globals.fs.file (globals.fs.path.join (imageDirName, fileName));
260+ if (! imageFile.existsSync ()) {
261+ return false ;
262+ }
242263 // validate image size is correct.
243264 // PNG file's width is at byte [16, 20), and height is at byte [20, 24), in big endian format.
244265 // Based on https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_format
245- final ByteData projectIconData = projectIconBytes.buffer.asByteData ();
246- if (projectIconData.lengthInBytes < 24 ) {
247- continue ;
248- }
249- final int width = projectIconData.getInt32 (16 );
250- final int height = projectIconData.getInt32 (20 );
251- if (width != entry.key.pixelSize || height != entry.key.pixelSize) {
252- filesWithWrongSize.add (entry.value);
266+ final ByteData imageData = imageFile.readAsBytesSync ().buffer.asByteData ();
267+ if (imageData.lengthInBytes < 24 ) {
268+ return false ;
253269 }
254- }
270+ final int width = imageData.getInt32 (16 );
271+ final int height = imageData.getInt32 (20 );
272+ // The size must not be null.
273+ final int expectedSize = entry.key.pixelSize! ;
274+ return width != expectedSize || height != expectedSize;
275+ })
276+ .map ((MapEntry <_ImageAssetFileKey , String > entry) => entry.value)
277+ .toList ();
278+ }
255279
256- if (filesWithTemplateIcon.isNotEmpty) {
280+ Future <void > _validateIconAssetsAfterArchive (StringBuffer messageBuffer) async {
281+ final BuildableIOSApp app = await buildableIOSApp;
282+
283+ final Map <_ImageAssetFileKey , String > templateInfoMap = _parseImageAssetContentsJson (
284+ app.templateAppIconDirNameForContentsJson,
285+ requiresSize: true );
286+ final Map <_ImageAssetFileKey , String > projectInfoMap = _parseImageAssetContentsJson (
287+ app.projectAppIconDirName,
288+ requiresSize: true );
289+
290+ final bool usesTemplate = _isAssetStillUsingTemplateFiles (
291+ templateImageInfoMap: templateInfoMap,
292+ projectImageInfoMap: projectInfoMap,
293+ templateImageDirName: await app.templateAppIconDirNameForImages,
294+ projectImageDirName: app.projectAppIconDirName);
295+ if (usesTemplate) {
257296 messageBuffer.writeln ('\n Warning: App icon is set to the default placeholder icon. Replace with unique icons.' );
258297 }
298+
299+ final List <String > filesWithWrongSize = _imageFilesWithWrongSize (
300+ imageInfoMap: projectInfoMap,
301+ imageDirName: app.projectAppIconDirName);
259302 if (filesWithWrongSize.isNotEmpty) {
260303 messageBuffer.writeln ('\n Warning: App icon is using the wrong size (e.g. ${filesWithWrongSize .first }).' );
261304 }
262305 }
263306
307+ Future <void > _validateLaunchImageAssetsAfterArchive (StringBuffer messageBuffer) async {
308+ final BuildableIOSApp app = await buildableIOSApp;
309+
310+ final Map <_ImageAssetFileKey , String > templateInfoMap = _parseImageAssetContentsJson (
311+ app.templateLaunchImageDirNameForContentsJson,
312+ requiresSize: false );
313+ final Map <_ImageAssetFileKey , String > projectInfoMap = _parseImageAssetContentsJson (
314+ app.projectLaunchImageDirName,
315+ requiresSize: false );
316+
317+ final bool usesTemplate = _isAssetStillUsingTemplateFiles (
318+ templateImageInfoMap: templateInfoMap,
319+ projectImageInfoMap: projectInfoMap,
320+ templateImageDirName: await app.templateLaunchImageDirNameForImages,
321+ projectImageDirName: app.projectLaunchImageDirName);
322+
323+ if (usesTemplate) {
324+ messageBuffer.writeln ('\n Warning: Launch image is set to the default placeholder. Replace with unique launch images.' );
325+ }
326+ }
327+
264328 Future <void > _validateXcodeBuildSettingsAfterArchive (StringBuffer messageBuffer) async {
265329 final BuildableIOSApp app = await buildableIOSApp;
266330
@@ -296,7 +360,9 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
296360
297361 final StringBuffer validationMessageBuffer = StringBuffer ();
298362 await _validateXcodeBuildSettingsAfterArchive (validationMessageBuffer);
299- await _validateIconsAfterArchive (validationMessageBuffer);
363+ await _validateIconAssetsAfterArchive (validationMessageBuffer);
364+ await _validateLaunchImageAssetsAfterArchive (validationMessageBuffer);
365+
300366 validationMessageBuffer.write ('\n To update the settings, please refer to https://docs.flutter.dev/deployment/ios' );
301367 globals.printBox (validationMessageBuffer.toString (), title: 'App Settings' );
302368
0 commit comments