@@ -57,30 +57,32 @@ class BuildIOSCommand extends _BuildIOSSubCommand {
57
57
Directory _outputAppDirectory (String xcodeResultOutput) => globals.fs.directory (xcodeResultOutput).parent;
58
58
}
59
59
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.
62
63
@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 );
65
66
66
67
/// The idiom (iphone or ipad).
67
68
final String idiom;
68
- /// The logical size in point (e.g. 83.5).
69
- final double size;
70
69
/// The scale factor (e.g. 2).
71
70
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;
72
74
73
75
@override
74
- int get hashCode => Object .hash (idiom, size, scale );
76
+ int get hashCode => Object .hash (idiom, scale, size );
75
77
76
78
@override
77
- bool operator == (Object other) => other is _AppIconImageFileKey
79
+ bool operator == (Object other) => other is _ImageAssetFileKey
78
80
&& other.idiom == idiom
79
- && other.size == size
80
- && other.scale == scale ;
81
+ && other.scale == scale
82
+ && other.size == size ;
81
83
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.
84
86
}
85
87
86
88
/// Builds an .xcarchive and optionally .ipa for an iOS app to be generated for
@@ -159,108 +161,170 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
159
161
return super .validateCommand ();
160
162
}
161
163
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
+ {
164
171
final Directory contentsJsonDirectory = globals.fs.directory (contentsJsonDirName);
165
172
if (! contentsJsonDirectory.existsSync ()) {
166
- return < _AppIconImageFileKey , String > {};
173
+ return < _ImageAssetFileKey , String > {};
167
174
}
168
175
final File contentsJsonFile = contentsJsonDirectory.childFile ('Contents.json' );
169
176
final Map <String , dynamic > contents = json.decode (contentsJsonFile.readAsStringSync ()) as Map <String , dynamic >? ?? < String , dynamic > {};
170
177
final List <dynamic > images = contents['images' ] as List <dynamic >? ?? < dynamic > [];
171
178
final Map <String , dynamic > info = contents['info' ] as Map <String , dynamic >? ?? < String , dynamic > {};
172
179
if ((info['version' ] as int ? ) != 1 ) {
173
180
// Skips validation for unknown format.
174
- return < _AppIconImageFileKey , String > {};
181
+ return < _ImageAssetFileKey , String > {};
175
182
}
176
183
177
- final Map <_AppIconImageFileKey , String > iconInfo = < _AppIconImageFileKey , String > {};
184
+ final Map <_ImageAssetFileKey , String > iconInfo = < _ImageAssetFileKey , String > {};
178
185
for (final dynamic image in images) {
179
186
final Map <String , dynamic > imageMap = image as Map <String , dynamic >;
180
187
final String ? idiom = imageMap['idiom' ] as String ? ;
181
188
final String ? size = imageMap['size' ] as String ? ;
182
189
final String ? scale = imageMap['scale' ] as String ? ;
183
190
final String ? fileName = imageMap['filename' ] as String ? ;
184
191
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
+ {
186
196
continue ;
187
197
}
188
198
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' )
191
203
.map ((String element) => double .tryParse (element))
192
204
.whereType <double >();
193
- if (parsedSizes.isEmpty) {
194
- continue ;
205
+ if (parsedSizes.isEmpty) {
206
+ continue ;
207
+ }
208
+ parsedSize = parsedSizes.first;
209
+ } else {
210
+ parsedSize = null ;
195
211
}
196
- final double parsedSize = parsedSizes.first;
197
212
198
213
// for example, "3x".
199
214
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 >();
202
217
if (parsedScales.isEmpty) {
203
218
continue ;
204
219
}
205
220
final int parsedScale = parsedScales.first;
206
-
207
- iconInfo[_AppIconImageFileKey (idiom, parsedSize, parsedScale)] = fileName;
221
+ iconInfo[_ImageAssetFileKey (idiom, parsedScale, parsedSize)] = fileName;
208
222
}
209
-
210
223
return iconInfo;
211
224
}
212
225
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 ;
240
238
}
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
+ }
241
250
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
+ }
242
263
// validate image size is correct.
243
264
// PNG file's width is at byte [16, 20), and height is at byte [20, 24), in big endian format.
244
265
// 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 ;
253
269
}
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
+ }
255
279
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) {
257
296
messageBuffer.writeln ('\n Warning: App icon is set to the default placeholder icon. Replace with unique icons.' );
258
297
}
298
+
299
+ final List <String > filesWithWrongSize = _imageFilesWithWrongSize (
300
+ imageInfoMap: projectInfoMap,
301
+ imageDirName: app.projectAppIconDirName);
259
302
if (filesWithWrongSize.isNotEmpty) {
260
303
messageBuffer.writeln ('\n Warning: App icon is using the wrong size (e.g. ${filesWithWrongSize .first }).' );
261
304
}
262
305
}
263
306
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
+
264
328
Future <void > _validateXcodeBuildSettingsAfterArchive (StringBuffer messageBuffer) async {
265
329
final BuildableIOSApp app = await buildableIOSApp;
266
330
@@ -296,7 +360,9 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
296
360
297
361
final StringBuffer validationMessageBuffer = StringBuffer ();
298
362
await _validateXcodeBuildSettingsAfterArchive (validationMessageBuffer);
299
- await _validateIconsAfterArchive (validationMessageBuffer);
363
+ await _validateIconAssetsAfterArchive (validationMessageBuffer);
364
+ await _validateLaunchImageAssetsAfterArchive (validationMessageBuffer);
365
+
300
366
validationMessageBuffer.write ('\n To update the settings, please refer to https://docs.flutter.dev/deployment/ios' );
301
367
globals.printBox (validationMessageBuffer.toString (), title: 'App Settings' );
302
368
0 commit comments