-1

This SwiftUI-Kit is a open source project, which acts as a way for showcasing all SwiftUI components and it supports all Apple platforms.

The project was created in Xcode 12 beta, with the new SwiftUI App protocol for handling the app's lifecycle and the deployment target being iOS 14.

Now, I want to add Support for iOS 13 to the project. And I can't find a way to have both App protocol for iOS 14 and other platforms and use AppDelegate for iOS 13 in this project.

I tried different combinations of App Delegate and Scene Delegate methods. The end result is a crash in iOS 13 device with the following error.

dyld: Symbol not found: _$s7SwiftUI4ViewPAAE18navigationBarTitle_11displayModeQrqd___AA010NavigationE4ItemV0f7DisplayH0OtSyRd__lFQOMQ
  Referenced from: /private/var/containers/Bundle/Application/0813D699-9718-4106-BBC6/SwiftUI Kit iOS.app/SwiftUI Kit iOS
  Expected in: /System/Library/Frameworks/SwiftUI.framework/SwiftUI
 in /private/var/containers/Bundle/Application/0813D699-9718-4106-BBC6/SwiftUI Kit iOS.app/SwiftUI Kit iOS
dyld: launch, loading dependent libraries
DYLD_LIBRARY_PATH=/usr/lib/system/introspection
DYLD_INSERT_LIBRARIES=/Developer/usr/lib/libBacktraceRecording.dylib:/Developer/usr/lib/libMainThreadChecker.dylib:/Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib

Here is the code. You can find full project code for iOS 13 in this branch.

import UIKit
import SwiftUI

#if os(iOS)

class AppDelegate: UIResponder, UIApplicationDelegate {...}

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // ...
        let contentView = ContentView()

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

    func sceneDidDisconnect(_ scene: UIScene) {...}

    func sceneDidBecomeActive(_ scene: UIScene) {...}

    func sceneWillResignActive(_ scene: UIScene) {...}

    func sceneWillEnterForeground(_ scene: UIScene) {...}

    func sceneDidEnterBackground(_ scene: UIScene) {...}
}

@main
struct MainApp {
    static func main() {
        if #available(iOS 14.0, *) {
            SwiftUI_Kit_iOS_App.main()
        } else {
            UIApplicationMain(
                CommandLine.argc,
                CommandLine.unsafeArgv,
                nil,
                NSStringFromClass(AppDelegate.self)
            )
        }
    }
}

@available(iOS 14.0, *)
struct SwiftUI_Kit_iOS_App: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

#else

@main
struct SwiftUI_KitApp: App {
    var body: some Scene {
        WindowGroup {
            #if os(macOS)
            ContentView().frame(minWidth: 100, idealWidth: 300, maxWidth: .infinity, minHeight: 100, idealHeight: 200, maxHeight: .infinity)
            #else
            ContentView()
            #endif
        }
    }
}

#endif

I looked up this question, but the answers require iOS 14 as target. I want to make it work with iOS 13 as target.

imthath
  • 1,353
  • 1
  • 13
  • 35
  • You cannot use SwiftUI Life-cycle on pre-iOS14, the same as you cannot use SwiftUI on pre-iOS13. – Asperi Jul 13 '20 at 16:56
  • 1
    @Asperi I'm not trying to use SwiftUI Life-cycle on pre iOS 14, I just want to know how to use both in the same project, supporting different targets. – imthath Jul 13 '20 at 17:03
  • Hm, I just came across an article about that! https://swiftui-lab.com/backward-compatibility/ – cbjeukendrup Jul 13 '20 at 17:40
  • @cbjeukendrup Thanks for the link. It is working with iOS 14. But I'm still getting the same crash in iOS 13. – imthath Jul 14 '20 at 01:10
  • I believe you can't have two `@main` attributes, maybe that is the point. But your case is a little more complex, because you want to support multiple platforms. I will have a closer look and let you know! – cbjeukendrup Jul 14 '20 at 10:23

1 Answers1

1

Okay, I finally found a solution!

No, it's not beautiful, and no, it's not nice, but it's quite correct and it works as far as I can check. So, here it is:

import SwiftUI

@main
struct MainApp {
    static func main() {
        if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
            SwiftUIApp.main()
        } else {
            #if os(iOS) // iOS 13.0 or lower
            UIApplicationMain(CommandLine.argc,
                              CommandLine.unsafeArgv,
                              nil,
                              NSStringFromClass(AppDelegate.self))
            #else
            // So we are on macOS 10.15, tvOS 13.0, watchOS 6.0 or someting lower.
            // By correctly setting the deployment target in your project,
            // you won't need to do someting here, as this situation will
            // never occur.
            print("This app doesn't run (yet) on this OS, so Bye")
            return
            #endif
        }
    }
}

@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
struct SwiftUIApp: App {
    var body: some Scene {
        return WindowGroup {
            #if os(macOS)
            ContentView().frame(minWidth: 100, idealWidth: 300, maxWidth: .infinity, minHeight: 100, idealHeight: 200, maxHeight: .infinity)
            #else
            ContentView()
            #endif
        }
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello world!")
    }
}

#if os(iOS)
import UIKit
// @UIApplicationMain <- remove that!
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        return true
    }
    
    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }

}

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        let contentView = ContentView()

        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

    func sceneDidDisconnect(_ scene: UIScene) { /*...*/ }
    func sceneDidBecomeActive(_ scene: UIScene) { /*...*/ }
    func sceneWillResignActive(_ scene: UIScene) { /*...*/ }
    func sceneWillEnterForeground(_ scene: UIScene) { /*...*/ }
    func sceneDidEnterBackground(_ scene: UIScene) { /*...*/ }
}
#endif

Quite a lot of code, huh?

But, (in my case) this was not all. We also need to dive into the Info.plist file for the iOS target.

  1. Find the key called Application Scene Manifest or UIApplicationSceneManifest and expand it (by clicking the gray triangle)

  2. Add the following things so that it looks like in the picture below: Info.plist

    Make sure that at "Default configuration" you fill in exactly the same as in this line of the code:

            return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    

    Furthermore, "SceneDelegate" should be the name of the SceneDelegate class and "LaunchScreen" should be the name of your launch screen storyboard (sometimes it's written with a space, sometimes without, so be careful!).

    If you use the code I provided, and don't rename one of these things, this won't probably be a problem.

  3. Finally, delete the app from the device. This will make sure that the new Info.plist is copied when re-running and installing. (only necessary when making changes in Info.plist)

cbjeukendrup
  • 3,216
  • 2
  • 12
  • 28
  • Thanks for the detailed answer. However, I am getting the `EXC_BAD_ACCESS` error at the line, where you have called `UIApplicationMain(CommandLine.argc, ...)`. – imthath Jul 15 '20 at 01:10
  • Oh, that's not a good sign... and I don't immediately have a solution. You could try pressing Cmd+Shift+K to 'clean' the project and then run again. Sometimes that helps. – cbjeukendrup Jul 15 '20 at 09:22
  • I have cleaned the project multiple times. Restarted Xcode, Mac and Phone. Nothing worked. – imthath Jul 16 '20 at 06:44
  • I will try what I can do! – cbjeukendrup Jul 16 '20 at 06:55
  • Two suggestions: 1. It seems that you don't actually use Main.storyboard, so maybe it's a good idea to delete it. Make sure that the "Main interface" field in `Project -> SwiftUI Kit iOS -> Deployment Info` is empty. 2. There was a typo in the Info.plist file for iOS: `LaunchScren` instead of `LaunchScreen`. Maybe one of these things helps? – cbjeukendrup Jul 16 '20 at 07:44
  • Thanks for you efforts, but I have to report that I'm facing crash at the same line. – imthath Jul 16 '20 at 08:28