diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart index 8a99ffaab49..99940ce7958 100644 --- a/packages/flutter_tools/lib/src/application_package.dart +++ b/packages/flutter_tools/lib/src/application_package.dart @@ -82,7 +82,7 @@ class AndroidApk extends ApplicationPackage { class IOSApp extends ApplicationPackage { static const String _defaultId = 'io.flutter.runner.Runner'; - static const String _defaultPath = 'ios'; + static const String _defaultPath = 'ios/Generated'; IOSApp({ String localPath: _defaultPath, diff --git a/packages/flutter_tools/lib/src/commands/ios.dart b/packages/flutter_tools/lib/src/commands/ios.dart index 160b085c0b9..1075584a0c0 100644 --- a/packages/flutter_tools/lib/src/commands/ios.dart +++ b/packages/flutter_tools/lib/src/commands/ios.dart @@ -59,6 +59,11 @@ class IOSCommand extends FlutterCommand { tempFile.writeAsBytesSync(archiveBytes); try { + // Remove the old generated project if one is present + runCheckedSync(['/bin/rm', '-rf', directory]); + // Create the directory so unzip can write to it + runCheckedSync(['/bin/mkdir', '-p', directory]); + // Unzip the Xcode project into the new empty directory runCheckedSync(['/usr/bin/unzip', tempFile.path, '-d', directory]); } catch (error) { return false; @@ -88,6 +93,41 @@ class IOSCommand extends FlutterCommand { return true; } + + void _writeUserEditableFilesIfNecessary(String directory) { + printStatus("Checking if user editable files need updates"); + + // Step 1: Check if the Info.plist exists and write one if not + File infoPlist = new File(path.join(directory, "Info.plist")); + if (!infoPlist.existsSync()) { + printStatus("Did not find an existing Info.plist. Creating one."); + infoPlist.writeAsStringSync(_infoPlistInitialContents); + } else { + printStatus("Info.plist present. Using existing."); + } + + // Step 2: Check if the LaunchScreen.storyboard exists and write one if not + File launchScreen = new File(path.join(directory, "LaunchScreen.storyboard")); + if (!launchScreen.existsSync()) { + printStatus("Did not find an existing LaunchScreen.storyboard. Creating one."); + launchScreen.writeAsStringSync(_launchScreenInitialContents); + } else { + printStatus("LaunchScreen.storyboard present. Using existing."); + } + + // Step 3: Check if the Assets.xcassets exists and write one if not + Directory xcassets = new Directory(path.join(directory, "Assets.xcassets")); + if (!xcassets.existsSync()) { + printStatus("Did not find an existing Assets.xcassets. Creating one."); + Directory iconsAssetsDir = new Directory(path.join(xcassets.path, "AppIcon.appiconset")); + iconsAssetsDir.createSync(recursive: true); + File iconContents = new File(path.join(iconsAssetsDir.path, "Contents.json")); + iconContents.writeAsStringSync(_iconAssetInitialContents); + } else { + printStatus("Assets.xcassets present. Using existing."); + } + } + void _setupXcodeProjXcconfig(String filePath) { StringBuffer localsBuffer = new StringBuffer(); @@ -111,7 +151,8 @@ class IOSCommand extends FlutterCommand { Future _runInitCommand() async { // Step 1: Fetch the archive from the cloud - String xcodeprojPath = path.join(Directory.current.path, "ios"); + String iosFilesPath = path.join(Directory.current.path, "ios"); + String xcodeprojPath = path.join(iosFilesPath, "Generated"); List archiveBytes = await _fetchXcodeArchive(); if (archiveBytes.isEmpty) { @@ -122,21 +163,31 @@ class IOSCommand extends FlutterCommand { // Step 2: Inflate the archive into the user project directory bool result = await _inflateXcodeArchive(xcodeprojPath, archiveBytes); if (!result) { - printError("Error: Could not init the Xcode project: the 'ios' directory already exists."); - printError("To proceed, remove the 'ios' directory and try again."); - printError("Warning: You may have made manual changes to files in the 'ios' directory."); + printError("Could not inflate the Xcode project archive."); return 1; } - // Step 3: Populate the Local.xcconfig with project specific paths + // Step 3: The generated project should NOT be checked into the users + // version control system. Be nice and write a gitignore for them if + // one does not exist. + File generatedGitignore = new File(path.join(iosFilesPath, ".gitignore")); + if (!generatedGitignore.existsSync()) { + generatedGitignore.writeAsStringSync("Generated/\n"); + } + + // Step 4: Setup default user editable files if this is the first run of + // the init command. + _writeUserEditableFilesIfNecessary(iosFilesPath); + + // Step 5: Populate the Local.xcconfig with project specific paths _setupXcodeProjXcconfig(path.join(xcodeprojPath, "Local.xcconfig")); - // Step 4: Write the REVISION file + // Step 6: Write the REVISION file File revisionFile = new File(path.join(xcodeprojPath, "REVISION")); revisionFile.createSync(); revisionFile.writeAsStringSync(ArtifactStore.engineRevision); - // Step 5: Tell the user the location of the generated project. + // Step 7: Tell the user the location of the generated project. printStatus("An Xcode project has been placed in 'ios/'."); printStatus("You may edit it to modify iOS specific configuration."); return 0; @@ -156,3 +207,159 @@ class IOSCommand extends FlutterCommand { return 1; } } + +final String _infoPlistInitialContents = ''' + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + Runner + CFBundleIdentifier + io.flutter.runner.Runner + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Flutter + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + +'''; + +final String _launchScreenInitialContents = ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + +'''; + +final String _iconAssetInitialContents = ''' +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} +''';