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