Unity as a Library rendered on partial screen with SwiftUI

I tweaked some of the Unity iOS support code to make it work with SwiftUI as a UIViewRepresentable. Will share the process of getting there if people are interested.

7 Likes

Yes Please!
Can you put this into a github and share it? Iā€™ve done a bunch of work to get this to work as a window above a swift UI, but it has itā€™s issues. This seems much more seamless.

@Neonlyte maybe also post it into the UAAL forum page. But please show us a github for it!

I checked my changes and itā€™s bits and pieces everywhere that I donā€™t think I can just share a git repo and itā€™ll be clear by itself. Iā€™ll do a write-up here instead and update the main post.

The anatomy of an iOS app is essential to understand what we need to do. An iOS app would consist of one or more UIWindow, in each window, one or more UIViewController will be presented, and in each view controller, one or more UIView will be presented.

Unityā€™s UaaL initialization for iOS is essentially the same as if it was running as a standalone application, where on initialization, Unityā€™s iOS ā€œtrampolineā€ code would create one of each UI components internally so that it can have exclusive control over those components, rather than reusing any of those from the host application.

What we need to do is to hijack the creation of these UI components, and along the way, we will also need to move around some code to make things visible under Swift, so that we can embed the Unity View into SwiftUI.

  1. Prevent Unity creating its own window and view controller. A newly UIWindow will cause it to be placed on top of the host application, and you would have to manually place the SwiftUI app window in front of it again, or all touch events will be blocked by that new window. The UnityViewControllerBase contains code related to auto screen orientation, status bar and home bar, which we want to avoid letting Unity changing those.

The following changes also removes snapshots VC and splash screen usage. My anecdotal observation is that removing the splash screen code actually makes the app launches perceivably faster even for non-UaaL purpose, as it seems that Unity would manually present the splash screen VC, even though iOS would present those automatically.

2 Likes

This flag enabled auto-rotation which would try to access Unityā€™s own view controller that would not exist, plus we automatically get auto-rotation when embedded into other view controllers.

This change may be optional as itā€™s part of the auto-rotation code.

2 Likes
  1. Make Unity View accessible in Swift. By default, Unity does not export the UnityView class, and its header file structure prevents Swift from importing the relevant methods and symbols.

Create a new header file called ā€œUnityView+Private.hā€, and move the 3 private fields deleted in the previous image into here.

Then, update header includes

2 Likes

Finally, export the header file in the framework umbrella header, and configure Xcode project to copy the header into the framework:

1 Like
  1. Integrate with SwiftUI in a way that SwiftUI Preview still works.
import SwiftUI

#if targetEnvironment(simulator)
#else
import UnityFramework
import MachO

private var isUnityStarted: Bool = false
func initializeUnityIfNeeded() {
    guard !isUnityStarted else {
        return
    }
 
    UnityInstance.setDataBundleId("com.unity3d.framework")
    UnityInstance.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: [:])
 
    isUnityStarted = true
}

private let UnityBundle = Bundle(path: Bundle.main.privateFrameworksPath! + "/UnityFramework.framework")!
let UnityInstance = {
    guard UnityBundle.load() else {
        fatalError("Unity Bundle failed to load")
    }
 
    let unityFrameworkInstance = UnityBundle.principalClass!.getInstance()!

    if unityFrameworkInstance.appController() == nil {
        let machineHeader = UnsafeMutablePointer<MachHeader>.allocate(capacity: 1)
        machineHeader.pointee = _mh_execute_header
        unityFrameworkInstance.setExecuteHeader(machineHeader)
    }

    return unityFrameworkInstance
}()
#endif

@main
struct Partial_View_TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
import SwiftUI

#if targetEnvironment(simulator)
// Does not import UnityFramework for simulator
#else
import UnityFramework
#endif

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello from Swift UI!")
                .padding()
            UnityWrapperView()
                .aspectRatio(16/9, contentMode: .fit)
            Button("Render") {
#if targetEnvironment(simulator)
                // Does nothing if simulator
#else
                UnityInstance.sendMessageToGO(
                    withName: "Message Receiver",
                    functionName: "Receive",
                    message: "C"
                )
#endif
            }
            .padding()
        }
        .padding()
    }
}

#if targetEnvironment(simulator)
struct UnityWrapperView: View {
    var body: some View {
        Rectangle()
            .foregroundColor(.blue)
    }
}
#else
struct UnityWrapperView: UIViewRepresentable {
    func makeUIView(context: Context) -> UIView {
        initializeUnityIfNeeded()
     
        let view = UnityInstance.appController().unityView!
        view.removeFromSuperview()
     
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {
    }
}
#endif

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Make sure that the framework is not linked via the build phase.

Linker flag:

1 Like

@Neonlyte ! Thereā€™s some great info there!
This is a completely different setup than how I have it. Iā€™ll give it a spin soon and see if that helps solving some things.
One question though, how does your UnityInstance object / file looks like? Iā€™m using a version of the UnityBridge idea from the other thread. or is UnityInstance at that point imported from the library?
I feel just by reading this that Iā€™m missing something.
Thank you for taking the time to write this up!

Iā€™ve tried replicating this in my own project, but iā€™m getting some odd error in an unrelated class (lifecycle).
There are also some lines that werenā€™t in my project, so my questions are:

  1. Can you please share this as a repo? This will be easier to compare where discrepencies are.
  2. Which version of Unity is this based off? Iā€™m using 2021.3

This is based off Unity 2022.2.10.

I cannot share this as a repository because of the changes I have made is very fragmented, and since I canā€™t reasonably bundle the IL2CPP sources and Unity library, you wonā€™t be able to build it to run anyway. All the screenshots above contained all of my changes from a clean export.

The UnityInstance global variable is an instance of UnityFramework. You get that by calling [UnityFramework getInstance] in Objective-C. The UnityFramework class is the principal class of the UnityFramework.framework. See the first code chunk of this post on Wednesday at 6:26 AM .

1 Like

Ok thanks. Might have to try a clean export out into a new project or something. I got all the changes down yet it wonā€™t compile due to some wierd error happening on the LifeCycleListener protocol. This happens after I add the last parts, of the view / view+private. All of a sudden Iā€™m getting some errors in that class.

8925917--1223096--upload_2023-4-4_12-23-13.png

But, unfortunatly those errors donā€™t make any sense, especially as this is a class that hasnā€™t even been edited, and compiles correctly without the other changesā€¦

@Neonlyte . Thanks again for this write up and your help. hopefully this will benefit other users in the future. Iā€™ll post updates if I manage to get this working.

Update : Did a clean XCode 14.3 project, with a clean Unity Project 2022.2.11, getting the same error. Looks like a dead cause atm unfortunately.

You may have messed up a header file somewhere which can cause this kind of bogus errors. These changes are indeed not easy to replicate due to all the moving pieces and I was only comfortable doing it because my day job is iOS Development.

1 Like

Yeah we use this in an IOS app ourselves however Iā€™m new to it and never worked with ObjC. What irks me is that it happens with a file that isnā€™t releated and Iā€™ve double and triple checked. It has to be some odd order of includes that breaking it or something along those lines.

A clean project and build also didnā€™t work and iā€™ve gone through this multiple times, commiting each step and seeing where it breaks.

Could be some config thing, xcode issue or god knows what, and at this point, I canā€™t spend anymore time troubleshooting it. Unity really needs to step up and update their codebase at this point so we arenā€™t relying on the community to solve these issues.

In any case, @Neonlyte , thanks for the effort, maybe someone else will have better success and will be able to get it working.

Itā€™s alive! It is working. @Neonlyte Thank you many times.
One thing is that you missed, you have to add UnityFramework in General settings Frameworks, Libraries, and Embedded Content.
And probably there a bit more things that can be cleaned up before making Framework, I will try and maybe post a github link with changes on empty project

1 Like

Oh. I forgot to mention that I have a small problem, probably it is related to headers as well. When I follow exact steps from your instruction I am getting an error (on the screen) 9031195--1246702--Screenshot 2023-05-23 at 16.38.18.png

If I comment that line - everything builds fine.
BUT in Native iOS project where I am trying to use resulted framework I am getting an opposite error in that file that Screenorientation canā€™t be found. I have to go inside the framework (fortunately itā€™s just a folder basically) and uncomment that line manually.

May be the order of headers or quirks of compiler idk. But for future followers - worth mentioning

1 Like

Hello, I followed along and got stuck on the LifeCycleListener.h warnings identical to @SKArctop , how were you able to get past those?

Glad you were able to make it work. I hope my posts have served the necessary inspiration.

One comment on ā€œFrameworks, Libraries, and Embedded Contentā€. The reason that I did not set it there is because I manually configured the build settings to achieve the same effect. That setting is usually convenience, but in my case it interferes with building SwiftUI in Xcode Preview, since the Unity Xcode project can only link with either Simulator or Device SDK at one time, so if I export the framework with Device SDK, it canā€™t link with Simulator targets, which is what Xcode Preview is underneath.

I am not sure. I just followed the steps ĀÆ_(惄)_/ĀÆ
The only difference is that I made a universal framework (xcframework) which includes a build for simulator and device.
I made two separate builds and after that, I joined the results via command:
xcodebuild -create-xcframework -framework <deviceFrameworkPath> -framework <simulatorFrameworkPath> -output <xcframeworkPath>

Hi, Can you please share the github link for the working copy of this development