Flutter: How to avoid automatic pluggin initialization on Flutter iOS - ios

I'm working with a plugin that I would like to manually register in iOS only if some condition is meet. Right now, the autogenerated AppDelegate.swift registers all plugins included in the pubspect.yaml with this line:
GeneratedPluginRegistrant.register(with: self)
Is there any way to avoid registering a single plugin?
Thank you

Yes, there is a way (after browsing and reading Flutter iOS engine documentation).
write your plugin in Swift (this language is easier to understand. I am bad in objC ;) )
have AppDelegate.swift (in Swift language too)
create plugin in AppDelegate.swift (below AppDelegate class). This if for simplicity. In my case it is:
public class FlutterNativeTimezonePlugin: NSObject, FlutterPlugin {
public static func addManuallyToRegistry(registry: FlutterPluginRegistry) {
// https://api.flutter.dev/objcdoc/Protocols/FlutterPluginRegistry.html#/c:objc(pl)FlutterPluginRegistry(im)registrarForPlugin:
let registrar = registry.registrar(forPlugin: "flutter_native_timezone")
if let safeRegistrar = registrar {
register(with: safeRegistrar)
}
}
// this is an override of this fucnion:
// https://api.flutter.dev/objcdoc/Protocols/FlutterPlugin.html#/c:objc(pl)FlutterPlugin(cm)registerWithRegistrar:
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "flutter_native_timezone", binaryMessenger: registrar.messenger())
let instance = FlutterNativeTimezonePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: #escaping FlutterResult) {
switch call.method {
case "getLocalTimezone":
result(NSTimeZone.local.identifier)
case "getAvailableTimezones":
result(NSTimeZone.knownTimeZoneNames)
default:
result(FlutterMethodNotImplemented)
}
}
}
below GeneratedPluginRegistrant.register(with: self) method add the plugin initialisation one: FlutterNativeTimezonePlugin.addManuallyToRegistry(registry: self)
So this is manual way of adding a plugin for the iOS build. Shame there is no documentation.

Related

Add delegate to custom iOS Flutter Plugin

I'm working on integrating a custom iOS plugin into my Flutter app, problem is that I'm not getting delegate callbacks from the custom SDK Protocol.
I have to connect a bluetooth device to my app and I from the delegate calls I should receive the device's ID and pair it.
From the Flutter side, I can call the native functions from the customSdk: sdkInstance.scan() and there are even some internal (inside the sdk) prints with the scan results but my delegate calls are not in place.
I think I'm not correctly adding the delegate to the SDK, I can get this to work in a swift native app but not as a Flutter Plugin.
So here's more or less the code:
iOS Code
AppDelegate.swift
import UIKit
import Flutter
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
SwiftIosPlugin.swift
import Flutter
import UIKit
import CustomSDK
public class SwiftIosPlugin: NSObject, FlutterPlugin {
let sdkInstance = CustomSDK.shared // This returns an instance of the SDK
let channel: FlutterMethodChannel
public static func register(with registrar: FlutterPluginRegistrar)
let channel = FlutterMethodChannel(name: "ios_plugin_channel", binaryMessenger: registrar.messenger())
let instance = SwiftIosPlugin(channel)
registrar.addMethodCallDelegate(instance, channel: channel)
registrar.addApplicationDelegate(instance)
}
init (_ channel: FlutterMethodChannel) {
self.channel = channel
super.init()
// In Swift, this is done in viewDidLoad()
// Is this the correct place to do this?
sdkInstance.addDelegate(self)
}
public func handle(_ call: FlutterMethodCall, result: #escaping FlutterResult) {
switch call.method {
case "startScan":
do {
// This is being called and results printed
try sdkInstance.scan()
} catch {
result(FlutterError(code: "400", message: "\(error)", details: nil))
}
case "connect":
sdkInstance.connect(call, result)
default:
result(FlutterMethodNotImplemented)
}
}
}
// These should be called but are not
extension SwiftIosPlugin: CustomSDKDelegate {
// Isn't called when scan() is executeed!
public func onScanDevice(didScan id:String) {
// do logic
}
public func onPairedDevice(didPair id:String) {
// do logic
}
}
Update:
Silly thing that I hope nobody else has this trouble...
Two things to consider:
The problem was some of the delegate's functions public func onScanDevice(didScan id:String) was missing a parameter (even though there weren't any errors pointed out by Xcode).
sdkInstance.addDelegate(self) was called too early in the class "lifecycle".
Be mindful of these things and you won't have any trouble!

SwiftUI CKShare Set Default Permission to Read Only

I have a couple of apps that include CloudKit record sharing and it now seems to work ok in iOS 16.
I only include Read Write and Read Only permission options and require the share recipient be in the user contacts.
This does indeed seem to work as expected, however the default choice in the share sheet options is Read Write.
The vast majority of the time I want Read Only. I have not been able to find a way to change
the default permission from "Can make changes" to "View only"
My CloudSharingController is pretty straightforward:
struct CloudSharingView: UIViewControllerRepresentable {
let share: CKShare
let container: CKContainer
let recipe: Recipe
func makeCoordinator() -> CloudSharingCoordinator {
CloudSharingCoordinator(recipe: recipe)
}
func makeUIViewController(context: Context) -> UICloudSharingController {
share[CKShare.SystemFieldKey.title] = recipe.rName
let controller = UICloudSharingController(share: share, container: container)
controller.modalPresentationStyle = .formSheet
controller.delegate = context.coordinator
controller.availablePermissions = [.allowReadWrite, .allowReadOnly]
return controller
}
func updateUIViewController(_ uiViewController: UICloudSharingController, context: Context) {
}
}//cloud sharing view
final class CloudSharingCoordinator: NSObject, UICloudSharingControllerDelegate {
let stack = CoreDataStack.shared
let recipe: Recipe
init(recipe: Recipe) {
self.recipe = recipe
}
func itemTitle(for csc: UICloudSharingController) -> String? {
recipe.rName
}
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
print("Failed to save share: \(error)")
}
func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
print("Saved the share")
}
func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) {
if !stack.isOwner(object: recipe) {
stack.delete(recipe)
}
}
}//cloud sharing coordinator
The screen:
Any guidance would be appreciated. Xcode 14.0, iOS 16.0
The availablePermissions seems to have two pairs of options:
allowPrivate
allowPublic
and
allowReadOnly
allowReadWrite
You need to specify one from each pair, so in your case it looks like you need
controller.availablePermissions = [.allowPrivate, .allowReadWrite]
If you don't specify one from a pair, it looks like it takes the most permissive option.

Capacitor plugin with ASWebAuthenticationSession: must be used from main thread only

I'm trying to get rid off an annoying warning/error in the xcode console.
I've implemented a custom plugin to open Keycloak using ASWebAuthenticationSession and I'm having issue figuring out how to call the main thread window.
This is the code:
#available(iOS 13.0, *)
#objc(KeycloakPlugin)
public class KeycloakPlugin: CAPPlugin, ObservableObject, ASWebAuthenticationPresentationContextProviding {
var webAuthSession: ASWebAuthenticationSession?
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return self.bridge?.webView?.window ?? ASPresentationAnchor()
}
This line compains when I open the external url for the authentication:
return self.bridge?.webView?.window ?? ASPresentationAnchor()
in this case I get:
UIView.window must be used from main thread only
Do you have any idea how to fix this?
Maybe this helps:
https://capacitorjs.com/docs/core-apis/ios
If you wrap your code in DispatchQueue.main.async, it should remove the warning. It also works with DispatchQueue.main.sync, depending on the implementation.
Something like this:
#objc(MyPlugin)
public class MyPlugin: CAPPlugin, ASWebAuthenticationPresentationContextProviding {
#objc func myPluginMethod(_ call: CAPPluginCall) {
// do something here
}
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
var view: ASPresentationAnchor?
DispatchQueue.main.sync {
view = self.bridge?.webView?.window
// or use async and do something here, e.g. create an implementation instance and pass the view
}
return view ?? ASPresentationAnchor()
}
}

How to write a return type of a method as a class that conforms to a protocol + swift

I am working on a project where I need to replicate the functionality developed with android in iOS using swift.
In the android app, there is a place where the return type of a method inside an abstract class Device is mentioned as,
abstract fun getDT(): Class<out DeviceType>
where DeviceType is itself another abstract class. So I heard from the android developer that, in the actual implementation of this method, it will return a class that inherits the DeviceType as below,
override fun getDT(): Class<out DeviceType> {
return type1DeviceType::class.java
}
where type1DeviceType actually inherits DeviceType abstract class as below
public class type1DeviceType extends DeviceType {
So in our iOS terms, the equivalent for the abstract class is protocol.
So in iOS, I have written a protocol in place of the abstract class in Android. And for the return type of the abstract function inside it, I need to mention the return type as something that conforms to the DeviceType protocol. Any idea how to achieve this?
I tried with the following code in swift.
public func getDT() -> DeviceTypeProtocol.Type {
return type1DeviceType as! DeviceTypeProtocol.Type
}
But during runtime, I get the error,
Swift runtime failure: type cast failed
Did you mean something like this?
protocol DeviceType {
func getDeviceType() -> DeviceType.Type
}
extension DeviceType {
func getDeviceType() -> DeviceType.Type { Self.self }
}
class AudioDevice: DeviceType {
func getDeviceType() -> DeviceType.Type { AudioDevice.self }
}
class Microphone: AudioDevice {
}
class Speaker: AudioDevice {
override func getDeviceType() -> DeviceType.Type { Speaker.self }
}
class VideoDevice: DeviceType {}
class Camera: VideoDevice {}
class Monitor: VideoDevice {
func getDeviceType() -> DeviceType.Type { VideoDevice.self }
}
func test() {
print(AudioDevice().getDeviceType()) // prints AudioDevice
print(Microphone().getDeviceType()) // prints AudioDevice
print(Speaker().getDeviceType()) // prints Speaker
print(VideoDevice().getDeviceType()) // prints VideoDevice
print(Camera().getDeviceType()) // prints Camera
print(Monitor().getDeviceType()) // prints VideoDevice
}
A protocol is defined for a DeviceType which has a capability of returning a type with getDeviceType that is also a type of DeviceType.
Extension of a protocol is not needed for what you are describing but I wanted to demonstrate it either way. It is used in VideoDevice.
So the AudioDevice inherits the protocol and explicitly defines a method to get a device type. Since it returns type of AudioDevice that is what it prints out. The Microphone inherits from AudioDevice (not from DeviceType) and does not override the method so it also returns AudioDevice. And Speaker also inherits from AudioDevice but does override the method and so does return Speaker.
The VideoDevice is a bit more fun. It inherits the protocol but does not explicitly define the method needed. Therefore it uses the extension which has a funny syntax of Self.self. It basically just means "return whatever is a static type of a dynamic self" if that makes more sense... This is only possible because extension of a protocol is defined. Removing the extension will create a compile time error that will let you know that you DO need to define that method. Now because the extension is nicely defined the VideoDevice already prints out itself. Same goes for Camera which inherits from VideoDevice (not from DeviceType). And then Monitor again overrides the method and prints out VideoDevice instead of Monitor.
Naturally you could define device categories (in this case video and audio) as protocols that inherit device type. And you could also put extensions on those protocols. Take a look at this example:
protocol DeviceType {
func getDeviceType() -> DeviceType.Type
}
protocol AudioDevice: DeviceType { }
class Microphone: AudioDevice {
func getDeviceType() -> DeviceType.Type { Microphone.self }
}
class Speaker: AudioDevice {
func getDeviceType() -> DeviceType.Type { Speaker.self }
}
protocol VideoDevice: DeviceType { }
extension VideoDevice {
func getDeviceType() -> DeviceType.Type { Self.self }
}
class Camera: VideoDevice {
}
class Monitor: VideoDevice {
func getDeviceType() -> DeviceType.Type { Camera.self }
}
func test() {
print(Microphone().getDeviceType()) // prints Microphone
print(Speaker().getDeviceType()) // prints Speaker
print(Camera().getDeviceType()) // prints Camera
print(Monitor().getDeviceType()) // prints Camera
}
Well a Swift protocol is not the same as a Kotlin abstract class. A closer comparison would be Kotlin interface and Swift protocol.
I'm a little confused on what your requirements are here. However, based on what I see here it seems like a good case for Protocol Oriented Programming, which can mitigate the need for multiple nested abstract classes or subclasses.
I'm also a little confused why you would need to get the DeviceType of a DeviceType.... is the DeviceType itself not the DeviceType?
In my head it seems something like this would be a lot more simple:
Kotlin
See in Kotlin playground
interface Device {
fun doSomething()
}
interface MobileDevice: Device {
fun onTap()
}
interface DesktopDevice: Device {
fun onClick()
}
interface WearableDevice: Device {
fun onButtonPush()
}
class AndroidPhone: MobileDevice {
override fun doSomething() {
println("I'm an Android phone.")
}
override fun onTap() {
println("Tap the screen.")
}
}
class MacDesktop: DesktopDevice {
override fun doSomething() {
println("I'm a Mac desktop.")
}
override fun onClick() {
println("Click the magic mouse.")
}
}
class SmartNecklace: WearableDevice {
override fun doSomething() {
println("I'm a smart necklace.")
}
override fun onButtonPush() {
println("Help! I've fallen and I can't get up!")
}
}
Which could be used like:
fun exampleFunction() {
val mobile = AndroidPhone()
val desktop = MacDesktop()
val wearable = SmartNecklace()
mobile.doSomething()
desktop.doSomething()
wearable.doSomething()
val devices = listOf(mobile, desktop, wearable)
devices.forEach { device ->
when (device) {
is MobileDevice -> device.onTap()
is DesktopDevice -> device.onClick()
is WearableDevice -> device.onButtonPush()
else -> println("Unknown Type.")
}
}
}
Swift
In your Swift version you can also add some default behaviors to the protocols (see the protocol extension examples below).
protocol Device {
func doSomething()
}
protocol MobileDevice: Device {
func onTap()
}
protocol DesktopDevice: Device {
func onClick()
}
protocol WearableDevice: Device {
func onButtonPush()
}
extension Device {
func doSomething() {
print("Doing default thing.")
}
}
extension WearableDevice {
func onButtonPush() {
print("Help! I've defaulted and I can't get up!")
}
}
class AndroidPhone: MobileDevice {
func onTap() {
print("Tap the screen.")
}
}
class MacDesktop: DesktopDevice {
func doSomething() {
print("I'm a Mac desktop.")
}
func onClick() {
print("Click the magic mouse.")
}
}
class SmartNecklace: WearableDevice {
func doSomething() {
print("I'm a smart necklace.")
}
}
Which could be used like:
func exampleFunction() {
let mobile = AndroidPhone()
let desktop = MacDesktop()
let wearable = SmartNecklace()
mobile.doSomething()
desktop.doSomething()
wearable.doSomething()
let devices: Array<Device> = [mobile, desktop, wearable]
devices.forEach { device in
switch (device) {
case let device as MobileDevice:
device.onTap()
case let device as DesktopDevice:
device.onClick()
case let device as WearableDevice:
device.onButtonPush()
default:
print("Uknown type")
}
}
}
Output
Doing default thing.
I'm a Mac desktop.
I'm a smart necklace.
Tap the screen.
Click the magic mouse.
Help! I've defaulted and I can't get up!
If you did something like this, you would already know the type by nature of the object (as seen in switch/when blocks). You wouldn't need a method to get the device type. You would just have it.
If there is something I'm missing from your question, let me know.
As for your error:
Swift runtime failure: type cast failed
If the cast is failing, I'm guessing type1DeviceType is a DeviceTypeProtocol, not a DeviceTypeProtocol.Type.
public func getDT() -> DeviceTypeProtocol.Type {
return type1DeviceType as! DeviceTypeProtocol
}
Check what the type shows up as when you try to type out or use type1DeviceType, or check the quick help on it to see the type. What do you see as the actual type when you get to that point?
I would also ask how important is it to do this the exact same way as the Android version? I always recommend proper planning cross-platform to keep things as similar as possible. It always helps, especially when debugging and building things together as a team.
And as you can see from the above code, it can be almost exact. That's how I do things as well. But not at the expense of being able to get the job done.
class type1DeviceType: DeviceTypeProtocol {
public func getDT() -> DeviceTypeProtocol.Type {
// return type1DeviceType as! DeviceTypeProtocol.Type
return type1DeviceType.self
}
}

Embed Unity inside iOS in own ViewController

Using Unity 2019.3.0f3 and its Unity as a library feature I'm trying to embed a Unity project inside my iOS application.
Unity officially only supports full screen rendering. Nevertheless I'm looking for a way around that restriction.
In previous versions of Unity i successfully used swift-unity to do the integration. Within this approach it is easy to just get the View where Unity is rendering to (using UnityGetGLView()). I had no problems regarding stability or resources.
Using the new library approach, every time I try to access the UnityView, unity forces it's complete Window as keyWindow.
I tried accessing the UnityView in my own ViewController using
if let unityView = UnityFramework.getInstance()?.appController()?.rootViewController.view {
// insert subview at index 0 ensures unity view is behind current UI view
view?.insertSubview(unityView, at: 0)
}
But that immediately activates the complete unity-window and hides my parenting UITabBarController.
Trying to make the UnityFramework.getInstance()?.appController()?.rootViewController a child of my UITabBarController failed with the same result.
Furthermore it is not possible to add a child ViewController. Only adding subviews seems possible.
Does anybody know where that window-behaviour is located or how i can access the UnityView (or the RootViewController) and use it freely?
I found a solution to the problem based on this approach from the unity forum. Using this approach I'm able to use the UnityViewController as a child in my own TabBarController.
The approach is working for Unity 2019.3.0f3, but I'm not sure if it will work in future versions. It feels like Unity tries to actively prevent such use. Then again I found hints in comments in the library-code that would suggest that a modified ViewController-Hierarchy was at least contemplated e.g. in UnityAppController+ViewHandling.h. But the instructions are unclear and methods with the hinted names don't exist.
Solution
1. Create UnityEmbeddedSwift.swift
The official example App provided by Unity is a real mess. I ended up using the UnityEmbeddedSwift.swift from the linked forum post with additions for pausing. This class encapsulates all Unity-related functionality in one clean class.
//
// UnityEmbeddedSwift.swift
// Native
//
// Created by NSWell on 2019/12/19.
// Copyright © 2019 WEACW. All rights reserved.
//
//
// Created by Simon Tysland on 19/08/2019.
// Copyright © 2019 Simon Tysland. All rights reserved.
//
import Foundation
import UnityFramework
class UnityEmbeddedSwift: UIResponder, UIApplicationDelegate, UnityFrameworkListener {
private struct UnityMessage {
let objectName : String?
let methodName : String?
let messageBody : String?
}
private static var instance : UnityEmbeddedSwift!
private var ufw : UnityFramework!
private static var hostMainWindow : UIWindow! // Window to return to when exiting Unity window
private static var launchOpts : [UIApplication.LaunchOptionsKey: Any]?
private static var cachedMessages = [UnityMessage]()
// MARK: - Static functions (that can be called from other scripts)
static func getUnityRootViewController() -> UIViewController! {
return instance.ufw.appController()?.rootViewController
}
static func getUnityView() -> UIView! {
return instance.ufw.appController()?.rootViewController?.view
}
static func setHostMainWindow(_ hostMainWindow : UIWindow?) {
UnityEmbeddedSwift.hostMainWindow = hostMainWindow
let value = UIInterfaceOrientation.landscapeLeft.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
}
static func setLaunchinOptions(_ launchingOptions : [UIApplication.LaunchOptionsKey: Any]?) {
UnityEmbeddedSwift.launchOpts = launchingOptions
}
static func showUnity() {
if(UnityEmbeddedSwift.instance == nil || UnityEmbeddedSwift.instance.unityIsInitialized() == false) {
UnityEmbeddedSwift().initUnityWindow()
}
else {
UnityEmbeddedSwift.instance.showUnityWindow()
}
}
static func hideUnity() {
UnityEmbeddedSwift.instance?.hideUnityWindow()
}
static func pauseUnity() {
UnityEmbeddedSwift.instance?.pauseUnityWindow()
}
static func unpauseUnity() {
UnityEmbeddedSwift.instance?.unpauseUnityWindow()
}
static func unloadUnity() {
UnityEmbeddedSwift.instance?.unloadUnityWindow()
}
static func sendUnityMessage(_ objectName : String, methodName : String, message : String) {
let msg : UnityMessage = UnityMessage(objectName: objectName, methodName: methodName, messageBody: message)
// Send the message right away if Unity is initialized, else cache it
if(UnityEmbeddedSwift.instance != nil && UnityEmbeddedSwift.instance.unityIsInitialized()) {
UnityEmbeddedSwift.instance.ufw.sendMessageToGO(withName: msg.objectName, functionName: msg.methodName, message: msg.messageBody)
}
else {
UnityEmbeddedSwift.cachedMessages.append(msg)
}
}
// MARK - Callback from UnityFrameworkListener
func unityDidUnload(_ notification: Notification!) {
ufw.unregisterFrameworkListener(self)
ufw = nil
UnityEmbeddedSwift.hostMainWindow?.makeKeyAndVisible()
}
// MARK: - Private functions (called within the class)
private func unityIsInitialized() -> Bool {
return ufw != nil && (ufw.appController() != nil)
}
private func initUnityWindow() {
if unityIsInitialized() {
showUnityWindow()
return
}
ufw = UnityFrameworkLoad()!
ufw.setDataBundleId("com.unity3d.framework")
ufw.register(self)
// NSClassFromString("FrameworkLibAPI")?.registerAPIforNativeCalls(self)
ufw.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: UnityEmbeddedSwift.launchOpts)
sendUnityMessageToGameObject()
UnityEmbeddedSwift.instance = self
}
private func showUnityWindow() {
if unityIsInitialized() {
ufw.showUnityWindow()
sendUnityMessageToGameObject()
}
}
private func hideUnityWindow() {
if(UnityEmbeddedSwift.hostMainWindow == nil) {
print("WARNING: hostMainWindow is nil! Cannot switch from Unity window to previous window")
}
else {
UnityEmbeddedSwift.hostMainWindow?.makeKeyAndVisible()
}
}
private func pauseUnityWindow() {
ufw.pause(true)
}
private func unpauseUnityWindow() {
ufw.pause(false)
}
private func unloadUnityWindow() {
if unityIsInitialized() {
UnityEmbeddedSwift.cachedMessages.removeAll()
ufw.unloadApplication()
}
}
private func sendUnityMessageToGameObject() {
if (UnityEmbeddedSwift.cachedMessages.count >= 0 && unityIsInitialized())
{
for msg in UnityEmbeddedSwift.cachedMessages {
ufw.sendMessageToGO(withName: msg.objectName, functionName: msg.methodName, message: msg.messageBody)
}
UnityEmbeddedSwift.cachedMessages.removeAll()
}
}
private func UnityFrameworkLoad() -> UnityFramework? {
let bundlePath: String = Bundle.main.bundlePath + "/Frameworks/UnityFramework.framework"
let bundle = Bundle(path: bundlePath )
if bundle?.isLoaded == false {
bundle?.load()
}
let ufw = bundle?.principalClass?.getInstance()
if ufw?.appController() == nil {
// unity is not initialized
// ufw?.executeHeader = &mh_execute_header
let machineHeader = UnsafeMutablePointer<MachHeader>.allocate(capacity: 1)
machineHeader.pointee = _mh_execute_header
ufw!.setExecuteHeader(machineHeader)
}
return ufw
}
}
2. Modify AppDelegate.swift
Sets window and launch options needed by UnityEmbeddedSwift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UnityEmbeddedSwift.setHostMainWindow(window)
UnityEmbeddedSwift.setLaunchinOptions(launchOptions)
return true
}
3. Create RootTabBarController.swift
This class sets up the hierarchy.
It is important to use the UnityRootViewController right after calling UnityEmbeddedSwift.showUnity().
The Tab-Switching is not nice, but if it is missing Unity will pause (or freeze?) during loading. The timing seems to depend on the Unity-Projects loading time. It can be faster for small projects and needs more time for larger projects.
import UIKit
class RootTabBarController: UITabBarController, UITabBarControllerDelegate {
var unityNC: UINavigationController?
var nativeNC: UINavigationController?
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
// start unity and immediatly set as rootViewController
// this loophole makes it possible to run unity in the same window
UnityEmbeddedSwift.showUnity()
let unityViewController = UnityEmbeddedSwift.getUnityRootViewController()!
unityViewController.navigationItem.title = "Unity"
unityNC = UINavigationController.init(rootViewController: unityViewController)
unityNC?.tabBarItem.title = "Unity"
let nativeViewController = UIViewController.init()
nativeViewController.view.backgroundColor = UIColor.darkGray
nativeViewController.navigationItem.title = "Native"
nativeNC = UINavigationController.init(rootViewController: nativeViewController)
nativeNC?.tabBarItem.title = "Native"
viewControllers = [unityNC!, nativeNC!]
// select other tab and reselect first tab to unfreeze unity-loading
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
self.selectedIndex = 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01, execute: {
self.selectedIndex = 0
})
})
}
// MARK: - UITabBarControllerDelegate
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
// pause unity if unity-tab is not selected
if viewController != unityNC {
UnityEmbeddedSwift.pauseUnity()
} else {
UnityEmbeddedSwift.unpauseUnity()
}
}
}
4. Modify Main.storyboard
Modify the storyboard to start with the RootTabBarController.
For anyone who is still interested in preventing the freezing, I am building on top of aalmigthy's answer:
You do not need to add a TabBar controller and switch between the tabs. All you need to do is:
Add the Unity view as a subview
Send the subview to back
Here's the modified ViewController class (no need for a tab bar):
import UIKit
class HybridViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
UnityEmbeddedSwift.showUnity()
let uView = UnityEmbeddedSwift.getUnityView()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
self.view.addSubview(uView!)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
self.view.sendSubviewToBack(uView!)
})
})
}
}

Resources