在同一个多平台(iOS、macOS、watchOS、tvOS)应用程序中支持不同的生命周期方法

Posted

技术标签:

【中文标题】在同一个多平台(iOS、macOS、watchOS、tvOS)应用程序中支持不同的生命周期方法【英文标题】:Supporting different Lifecycle methods in same multiplatform(iOS, macOS, watchOS, tvOS) app 【发布时间】:2020-07-13 16:50:35 【问题描述】:

这个SwiftUI-Kit 是一个开源项目,作为展示所有 SwiftUI 组件的一种方式,它支持所有 Apple 平台。

该项目是在 Xcode 12 测试版中创建的,使用新的 SwiftUI App 协议来处理应用的生命周期,部署目标是 ios 14。

现在,我想在项目中添加对 iOS 13 的支持。而且我找不到在这个项目中同时拥有适用于 iOS 14 和其他平台的 App 协议并使用适用于 iOS 13 的 AppDelegate 的方法。

我尝试了 App Delegate 和 Scene Delegate 方法的不同组合。最终结果是 iOS 13 设备崩溃并出现以下错误。

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

这里是代码。您可以在 this branch 找到 iOS 13 的完整项目代码。

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

我查了this question,但答案需要 iOS 14 作为目标。我想让它以 iOS 13 为目标。

【问题讨论】:

iOS14之前不能使用SwiftUI Life-cycle,iOS13之前不能使用SwiftUI。 @Asperi 我不是想在 iOS 14 之前使用 SwiftUI 生命周期,我只是想知道如何在同一个项目中使用这两者,支持不同的目标。 嗯,我刚刚看到一篇关于这个的文章! swiftui-lab.com/backward-compatibility @cbjeukendrup 感谢您的链接。它适用于 iOS 14。但我在 iOS 13 中仍然遇到同样的崩溃。 我相信你不能有两个@main 属性,也许这就是重点。但是你的情况有点复杂,因为你想支持多个平台。我会仔细看看,让你知道! 【参考方案1】:

好的,我终于找到了解决办法!

不,它不漂亮,不,它不好,但它非常正确,而且我可以检查它是否有效。所以,这里是:

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

相当多的代码,对吧?

但是,(就我而言)这还不是全部。我们还需要深入研究 iOS 目标的 Info.plist 文件。

    找到名为Application Scene ManifestUIApplicationSceneManifest 的键并将其展开(通过单击灰色三角形)

    添加以下内容,使其如下图所示:

    确保在“默认配置”中填写的内容与这行代码完全相同:

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

    此外,“SceneDelegate”应该是 SceneDelegate 类的名称,“LaunchScreen”应该是您的启动屏幕故事板的名称(有时它写有空格,有时没有,所以要小心!)。

    如果您使用我提供的代码,并且不重命名这些内容之一,这可能不会成为问题。

    最后,从设备中删除该应用。这将确保在重新运行和安装时复制新的 Info.plist。 (仅在 Info.plist 中进行更改时需要)

【讨论】:

感谢您的详细回答。但是,我在您调用 UIApplicationMain(CommandLine.argc, ...) 的线路上收到了 EXC_BAD_ACCESS 错误。 哦,这不是一个好兆头......而且我没有立即找到解决方案。您可以尝试按 Cmd+Shift+K 来“清理”项目,然后再次运行。有时这会有所帮助。 我已经多次清理项目。重新启动 Xcode、Mac 和 Phone。没有任何效果。 我会尽我所能! 两个建议: 1. 看来你并没有真正使用 Main.storyboard,所以也许删除它是个好主意。确保Project -&gt; SwiftUI Kit iOS -&gt; Deployment Info 中的“主界面”字段为空。 2. iOS 的 Info.plist 文件中有错字:LaunchScren 而不是LaunchScreen。也许其中一件事情有帮助?

以上是关于在同一个多平台(iOS、macOS、watchOS、tvOS)应用程序中支持不同的生命周期方法的主要内容,如果未能解决你的问题,请参考以下文章

iOS 10 都有什么改变?

「系统更新」iOS,macOS,watchOS,tvOS

苹果同步发布 macOS & watchOS 最新系统,增加不少新功能

WWDC20 总结|MacOS 有史以来最大的更新变动WatchOS 7 发布

Swift介绍

苹果发布 macOS 12——Monterey