diff --git a/README.md b/README.md index 68541f9..3afae1b 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,11 @@ Now your project files should look like this. 1. First Open Unity Project. -2. Click Menu: File => Build Settings => Player Settings +2. Click Menu: File => Build Settings -3. Change `Product Name` to Name of the Xcode project, You can find it follow `ios/${XcodeProjectName}.xcodeproj`. +Be sure you have at least one scene added to your build. + +3. => Player Settings **Android Platform**: 1. Make sure your `Graphics APIs` are set to OpenGLES3 with a fallback to OpenGLES2 (no Vulkan) @@ -72,8 +74,8 @@ Now your project files should look like this. - x86 ✅ - **IOS Platform**: - 1. Other Settings find the Rendering part, uncheck the `Auto Graphics API` and select only `OpenGLES2`. + **iOS Platform**: + 1. Other Settings find the Rendering part, uncheck the `Auto Graphics API` and select only `OpenGLES3`. 2. Depending on where you want to test or run your app, (simulator or physical device), you should select the appropriate SDK on `Target SDK`.
@@ -84,7 +86,7 @@ Now your project files should look like this. ### Add Unity Build Scripts and Export -Copy [`Build.cs`](https://github.com/f111fei/react-native-unity-demo/blob/master/unity/Cube/Assets/Scripts/Editor/Build.cs) and [`XCodePostBuild.cs`](https://github.com/f111fei/react-native-unity-demo/blob/master/unity/Cube/Assets/Scripts/Editor/XCodePostBuild.cs) to `unity//Assets/Scripts/Editor/` +Copy [`Build.cs`](https://github.com/snowballdigital/flutter-unity-view-widget/tree/master/scripts/Editor/Build.cs) and [`XCodePostBuild.cs`](https://github.com/snowballdigital/flutter-unity-view-widget/tree/master/scripts/Editor/XCodePostBuild.cs) to `unity//Assets/Scripts/Editor/` Open your unity project in Unity Editor. Now you can export unity project with `Flutter/Export Android` or `Flutter/Export IOS` menu. @@ -127,6 +129,21 @@ IOS will export unity project to `ios/UnityExport`. } ``` +**iOS Platform Only** + + 1. open your xcode workspace and add the exported project (with File -> Add Files to “Runner” -> add the UnityExport/Unity-Iphone.xcodeproj file + 2. Select the Unity-iPhone/Data folder and change the Target Membership for Data folder to UnityFramework + + 3. Add this to your Runner/Runner/Runner-Bridging-Header.h + ```h + #import "UnityUtils.h" + ``` + 4. Add to AppDelegate.swift before the GeneratePluginRegistrant call: + ```swift + InitArgs(CommandLine.argc, CommandLine.unsafeArgv) + ``` + 5. Opt-in to the embedded views preview by adding a boolean property to the app's `Info.plist` file with the key `io.flutter.embedded_views_preview` and the value `YES`. +
### AR Foundation (ANDROID only at the moment) @@ -277,7 +294,6 @@ class _UnityDemoScreenState extends State{ - pause() ## Known issues - - no iOS support yet - Android Export requires several manual changes - Using AR will make the activity run in full screen (hiding status and navigation bar). diff --git a/change_target_membership_data_folder.png b/change_target_membership_data_folder.png new file mode 100644 index 0000000..1373a57 Binary files /dev/null and b/change_target_membership_data_folder.png differ diff --git a/ios/flutter_unity_widget.podspec b/ios/flutter_unity_widget.podspec index 852183f..f61f5e8 100644 --- a/ios/flutter_unity_widget.podspec +++ b/ios/flutter_unity_widget.podspec @@ -18,7 +18,7 @@ Flutter unity 3D widget for embedding unity in flutter s.ios.deployment_target = '8.0' s.xcconfig = { - 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}/../.symlinks/flutter/ios-release" "${PODS_ROOT}/../Unity3Export" "${PODS_CONFIGURATION_BUILD_DIR}"', + 'FRAMEWORK_SEARCH_PATHS' => '"${PODS_ROOT}/../UnityExport" "${PODS_ROOT}/../.symlinks/flutter/ios-release" "${PODS_CONFIGURATION_BUILD_DIR}"', 'OTHER_LDFLAGS' => '$(inherited) -framework UnityFramework ${PODS_LIBRARIES}' } end diff --git a/scripts/Editor/XCodePostBuild.cs b/scripts/Editor/XCodePostBuild.cs new file mode 100644 index 0000000..1100234 --- /dev/null +++ b/scripts/Editor/XCodePostBuild.cs @@ -0,0 +1,480 @@ +/* +MIT License +Copyright (c) 2017 Jiulong Wang +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#if UNITY_IOS + +using System; +using System.Linq; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using UnityEngine; +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEditor.iOS.Xcode; +using Application = UnityEngine.Application; + +/// +/// Adding this post build script to Unity project enables the flutter-unity-widget to access it +/// +public static class XcodePostBuild +{ + /// + /// Path to the root directory of Xcode project. + /// This should point to the directory of '${XcodeProjectName}.xcodeproj'. + /// It is recommended to use relative path here. + /// Current directory is the root directory of this Unity project, i.e. the directory of 'Assets' folder. + /// Sample value: "../xcode" + /// + private const string XcodeProjectRoot = "../../ios"; + + /// + /// Name of the Xcode project. + /// This script looks for '${XcodeProjectName} + ".xcodeproj"' under '${XcodeProjectRoot}'. + /// Sample value: "DemoApp" + /// + private static string XcodeProjectName = Application.productName; + + /// + /// Directories, relative to the root directory of the Xcode project, to put generated Unity iOS build output. + /// + private static string ClassesProjectPath = "UnityExport/Classes"; + private static string LibrariesProjectPath = "UnityExport/Libraries"; + private static string DataProjectPath = "UnityExport/Data"; + + /// + /// Path, relative to the root directory of the Xcode project, to put information about generated Unity output. + /// + private static string ExportsConfigProjectPath = "UnityExport/Exports.xcconfig"; + + private static string PbxFilePath = XcodeProjectName + ".xcodeproj/project.pbxproj"; + + private const string BackupExtension = ".bak"; + + /// + /// The identifier added to touched file to avoid double edits when building to existing directory without + /// replace existing content. + /// + private const string TouchedMarker = "https://github.com/snowballdigital/flutter-unity-view-widget"; + + [PostProcessBuild] + public static void OnPostBuild(BuildTarget target, string pathToBuiltProject) + { + if (target != BuildTarget.iOS) + { + return; + } + + PatchUnityNativeCode(pathToBuiltProject); + + } + + /// + /// Update pbx project file by adding src files and removing extra files that + /// exists in dest but not in src any more. + /// + /// This method only updates the pbx project file. It does not copy or delete + /// files in Swift Xcode project. The Swift Xcode project will do copy and delete + /// during build, and it should copy files if contents are different, regardless + /// of the file time. + /// + /// The pbx project. + /// The directory where Unity project is built. + /// The directory of the Swift Xcode project where the + /// Unity project is embedded into. + /// The prefix of project path in Swift Xcode + /// project for Unity code files. E.g. "DempApp/Unity/Classes" for all files + /// under Classes folder from Unity iOS build output. + private static void ProcessUnityDirectory(PBXProject pbx, string src, string dest, string projectPathPrefix) + { + var targetGuid = pbx.TargetGuidByName(XcodeProjectName); + if (string.IsNullOrEmpty(targetGuid)) { + throw new Exception(string.Format("TargetGuid could not be found for '{0}'", XcodeProjectName)); + } + + // newFiles: array of file names in build output that do not exist in project.pbx manifest. + // extraFiles: array of file names in project.pbx manifest that do not exist in build output. + // Build output files that already exist in project.pbx manifest will be skipped to minimize + // changes to project.pbx file. + string[] newFiles, extraFiles; + CompareDirectories(src, dest, out newFiles, out extraFiles); + + foreach (var f in newFiles) + { + if (ShouldExcludeFile(f)) + { + continue; + } + + var projPath = Path.Combine(projectPathPrefix, f); + if (!pbx.ContainsFileByProjectPath(projPath)) + { + var guid = pbx.AddFile(projPath, projPath); + pbx.AddFileToBuild(targetGuid, guid); + + Debug.LogFormat("Added file to pbx: '{0}'", projPath); + } + } + + foreach (var f in extraFiles) + { + var projPath = Path.Combine(projectPathPrefix, f); + if (pbx.ContainsFileByProjectPath(projPath)) + { + var guid = pbx.FindFileGuidByProjectPath(projPath); + pbx.RemoveFile(guid); + + Debug.LogFormat("Removed file from pbx: '{0}'", projPath); + } + } + } + + /// + /// Compares the directories. Returns files that exists in src and + /// extra files that exists in dest but not in src any more. + /// + private static void CompareDirectories(string src, string dest, out string[] srcFiles, out string[] extraFiles) + { + srcFiles = GetFilesRelativePath(src); + + var destFiles = GetFilesRelativePath(dest); + var extraFilesSet = new HashSet(destFiles); + + extraFilesSet.ExceptWith(srcFiles); + extraFiles = extraFilesSet.ToArray(); + } + + private static string[] GetFilesRelativePath(string directory) + { + var results = new List(); + + if (Directory.Exists(directory)) + { + foreach (var path in Directory.GetFiles(directory, "*", SearchOption.AllDirectories)) + { + var relative = path.Substring(directory.Length).TrimStart('/'); + results.Add(relative); + } + } + + return results.ToArray(); + } + + private static bool ShouldExcludeFile(string fileName) + { + if (fileName.EndsWith(".bak", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + + /// + /// Make necessary changes to Unity build output that enables it to be embedded into existing Xcode project. + /// + private static void PatchUnityNativeCode(string pathToBuiltProject) + { + EditMainMM(Path.Combine(pathToBuiltProject, "Classes/main.mm")); + EditUnityAppControllerH(Path.Combine(pathToBuiltProject, "Classes/UnityAppController.h")); + EditUnityAppControllerMM(Path.Combine(pathToBuiltProject, "Classes/UnityAppController.mm")); + + if (Application.unityVersion == "2017.1.1f1") + { + EditMetalHelperMM(Path.Combine(pathToBuiltProject, "Classes/Unity/MetalHelper.mm")); + } + + // TODO: Parse unity version number and do range comparison. + if (Application.unityVersion.StartsWith("2017.3.0f") || Application.unityVersion.StartsWith("2017.3.1f")) + { + EditSplashScreenMM(Path.Combine(pathToBuiltProject, "Classes/UI/SplashScreen.mm")); + } + } + + /// + /// Edit 'main.mm': removes 'main' entry that would conflict with the Xcode project it embeds into. + /// + private static void EditMainMM(string path) + { + EditCodeFile(path, line => + { + if (line.TrimStart().StartsWith("int main", StringComparison.Ordinal)) + { + return line.Replace("int main", "int old_main"); + } + + return line; + }); + } + + /// + /// Edit 'UnityAppController.h': returns 'UnityAppController' from 'AppDelegate' class. + /// + private static void EditUnityAppControllerH(string path) + { + var inScope = false; + var markerDetected = false; + var markerAdded = false; + + // Add static GetAppController + EditCodeFile(path, line => + { + inScope |= line.Contains("- (void)startUnity:"); + + if (inScope) + { + if (line.Trim() == "") + { + inScope = false; + + return new string[] + { + "", + "// Added by " + TouchedMarker, + "+ (UnityAppController*)GetAppController;", + "" + }; + } + } + + return new string[] { line }; + }); + + inScope = false; + markerDetected = false; + + // Modify inline GetAppController + EditCodeFile(path, line => + { + inScope |= line.Contains("inline UnityAppController"); + + if (inScope && !markerDetected) + { + if (line.Trim() == "}") + { + inScope = false; + markerDetected = true; + + return new string[] + { + "// }", + "", + "static inline UnityAppController* GetAppController()", + "{", + " return [UnityAppController GetAppController];", + "}", + }; + } + + if (!markerAdded) + { + markerAdded = true; + return new string[] + { + "// Modified by " + TouchedMarker, + "// " + line, + }; + } + + return new string[] { "// " + line }; + } + + return new string[] { line }; + }); + } + + /// + /// Edit 'UnityAppController.mm': triggers 'UnityReady' notification after Unity is actually started. + /// + private static void EditUnityAppControllerMM(string path) + { + var inScope = false; + var markerDetected = false; + + EditCodeFile(path, line => + { + if (line.Trim() == "@end") + { + return new string[] + { + "", + "// Added by " + TouchedMarker, + "static UnityAppController *unityAppController = nil;", + "", + @"+ (UnityAppController*)GetAppController", + "{", + " static dispatch_once_t onceToken;", + " dispatch_once(&onceToken, ^{", + " unityAppController = [[self alloc] init];", + " });", + " return unityAppController;", + "}", + "", + line, + }; + } + + inScope |= line.Contains("- (void)startUnity:"); + markerDetected |= inScope && line.Contains(TouchedMarker); + + if (inScope && line.Trim() == "}") + { + inScope = false; + + if (markerDetected) + { + return new string[] { line }; + } + else + { + return new string[] + { + " // Modified by " + TouchedMarker, + @" [[NSNotificationCenter defaultCenter] postNotificationName: @""UnityReady"" object:self];", + "}", + }; + } + } + + return new string[] { line }; + }); + } + + /// + /// Edit 'MetalHelper.mm': fixes a bug (only in 2017.1.1f1) that causes crash. + /// + private static void EditMetalHelperMM(string path) + { + var markerDetected = false; + + EditCodeFile(path, line => + { + markerDetected |= line.Contains(TouchedMarker); + + if (!markerDetected && line.Trim() == "surface->stencilRB = [surface->device newTextureWithDescriptor: stencilTexDesc];") + { + return new string[] + { + "", + " // Modified by " + TouchedMarker, + " // Default stencilTexDesc.usage has flag 1. In runtime it will cause assertion failure:", + " // validateRenderPassDescriptor:589: failed assertion `Texture at stencilAttachment has usage (0x01) which doesn't specify MTLTextureUsageRenderTarget (0x04)'", + " // Adding MTLTextureUsageRenderTarget seems to fix this issue.", + " stencilTexDesc.usage |= MTLTextureUsageRenderTarget;", + line, + }; + } + + return new string[] { line }; + }); + } + + /// + /// Edit 'SplashScreen.mm': Unity introduces its own 'LaunchScreen.storyboard' since 2017.3.0f3. + /// Disable it here and use Swift project's launch screen instead. + /// + private static void EditSplashScreenMM(string path) { + var markerDetected = false; + var markerAdded = false; + var inScope = false; + var level = 0; + + EditCodeFile(path, line => + { + inScope |= line.Trim() == "void ShowSplashScreen(UIWindow* window)"; + markerDetected |= line.Contains(TouchedMarker); + + if (inScope && !markerDetected) + { + if (line.Trim() == "{") + { + level++; + } + else if (line.Trim() == "}") + { + level--; + } + + if (line.Trim() == "}" && level == 0) + { + inScope = false; + } + + if (level > 0 && line.Trim().StartsWith("bool hasStoryboard")) + { + return new string[] + { + " // " + line, + " bool hasStoryboard = false;", + }; + } + + if (!markerAdded) + { + markerAdded = true; + return new string[] + { + "// Modified by " + TouchedMarker, + line, + }; + } + } + + return new string[] { line }; + }); + } + + private static void EditCodeFile(string path, Func lineHandler) + { + EditCodeFile(path, line => + { + return new string[] { lineHandler(line) }; + }); + } + + private static void EditCodeFile(string path, Func> lineHandler) + { + var bakPath = path + ".bak"; + if (File.Exists(bakPath)) + { + File.Delete(bakPath); + } + + File.Move(path, bakPath); + + using (var reader = File.OpenText(bakPath)) + using (var stream = File.Create(path)) + using (var writer = new StreamWriter(stream)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + var outputs = lineHandler(line); + foreach (var o in outputs) + { + writer.WriteLine(o); + } + } + } + } +} + +#endif \ No newline at end of file