SwiftUI2.0如何向后兼容之前项目和代码

    技术2022-07-13  86

    SwiftUI如何向后兼容性

    现在是时候开始发现WWDC 2020带来的所有新SwiftUI功能了。 但是,就像每年一样,几毫秒后,兴奋就消散了,当您记住放弃对较早版本的OS的支持并不是您的选择。

    通常,我们求助于#available朋友。 例如,假设您有一个较长的HStack。 您可以决定使用新的LazyHStack,以利用其对长堆栈的性能改进。 但是,如果您的应用程序在iOS13上运行,则可以回退到使用普通的普通HStack:

    Group { Text("A long vertical view is below!") if #available(iOS 14.0, *) { LazyHStack { View1() View2() } } else { // Fallback on earlier versions HStack { View1() View2() } } }

    这很容易替代,但是在更复杂的情况下,您的后备代码将需要更极端的措施,例如由Representable包装的UIKit / AppKit视图。

    这种方法对于一个很小的项目来说看起来不错,但是随着视图数量的增加,每次添加#available检查都会使您感到很烦。 而且,代码的可读性将遭受极大的损害。 对于这些情况,我们可以利用Swift可以在不同作用域中处理相同类型名称的事实。 让我用一个例子来说明:

    // Now, the compiler will no longer complain about LazyHStack not being available on iOS13. struct ContentView: View { var body: some View { LazyHStack(spacing: 30) { View1() View2() } } } struct LazyHStack<Content> : View where Content : View { let alignment: VerticalAlignment let spacing: CGFloat? let content: () -> Content var body: some View { Group { if #available(OSX 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) { SwiftUI.LazyHStack(alignment: alignment, spacing: spacing, content: content) } else { // Fallback on earlier versions HStack(alignment: alignment, spacing: spacing, content: content) } } } init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: @escaping () -> Content) { self.alignment = alignment self.spacing = spacing self.content = content } }

    通过创建自己的LazyHStack(可在所有OS版本上使用),编译器将不再抱怨。 这是因为现在LazyHStack引用MyApp.LazyHStack而不是SwiftUI.LazyHStack。 然后,在我们自己的实现中,检查版本,然后决定是使用旧的SwiftUI.HStack还是新的SwiftUI.LazyHStack。

    @available 参考资料: https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID583.

    到目前为止,这有些琐碎。但是,现在我想将重点转移到新的SwiftUI带来的特定问题上。我正在谈论如何使用新的App和Scene API,以及启动我们的应用程序的“旧方法”。

    拥抱变化 假设您有一个应用程序已经在较旧的操作系统版本下运行。现在,在见证了新的SwiftUI改进浪潮之后,您最终决定是时候让您的应用包含新框架了。我不会讨论SwiftUI是否足够成熟。这在很大程度上取决于您正在编写的应用程序的类型,但是就本文而言,我们假设SwiftUI确实适合您的应用程序。

    对于此特定示例,我们将探讨是否有可能为运行早期OS版本的用户保留旧的UI,并从头开始重新设计SwiftUI界面,从而使运行iOS14.0 + / macOS11 +的用户受益

    从Xcode 12开始,现在可以设计一个完全使用SwiftUI编写的应用程序。在过去(即去年),您仍然需要像往常一样连接场景/窗口的层次结构。不再需要,现在只需编写几行代码即可编写完整的应用程序,如下所示

    @main struct MyApp: App { var body: some Scene { WindowGroup { Text("Hello, world!") } } }

    这虽然很棒,但在我们尝试同时保留旧UI和新UI时都提出了一个问题。 如果尝试使用“ if #available”来包装@main声明,则会收到编译器错误。

    不允许在顶层使用#available,因此编译器将为我们提供以下错误:

    #available #available

    幸运的是,仍然有解决方法。让我们来看看…

    应用程序入口点(@main) Swift 5.3具有一个名为@main(SE-0281)的新属性。由于这是该语言的一部分(即不是库),因此只要您使用Swift 5.3+进行编译,它就可以与旧版OS一起使用。我们这里真正的问题是,在较早的OS版本中不存在App协议。那么,考虑到整个应用程序中只能有一种带@main的带注释类型,我们如何解决呢?

    还要注意,@main和@ UIApplicationMain / @ NSApplicationMain与main.swift中的顶级代码是互斥的。您只能使用一种类型的入口点。

    如果我们查看文档,就会发现@main为您的应用程序提供了入口点。特别是,您的应用程序将从跳转到以@main开头的类型的main()函数开始。查看代码,我们可以推断出App协议必须具有main()函数的默认实现。确实如此,请在此处查看Apple的文档。

    这是我们需要知道的全部信息,以便创建一个将新App协议用于新OS版本以及将旧UIApplication / NSApplication类型用于其他应用程序的应用程序。考虑到这一点,我们将需要按以下说明更改代码。

    一些注意事项

    后面的代码仅作为起点。 多年来,有许多方法可以启动应用程序(例如,主故事板,XIB文件,场景清单,手动调用UIApplicationMain等)。 这意味着应以不同的方式处理每种情况。 这里的代码只会为您指明正确的方向(我希望如此)。在尝试进行此操作时,我发现有时该应用有些固执,无法按照我的预期去做。 我发现从模拟器中删除该应用程序并重新部署解决了该问题。 这可能与Info.plist中的更改未正确更新有关……但是我不确定。 只要记住它,就应该在您身上发生。最后一点警告:您不喜欢这个,我很抱歉…但是我们在这里处理了一些未记录的行为,因此,风险自负!

    iOS示例

    在以下示例中,我们的旧UI使用UIHostingController作为主控制器,因此不会从情节提要中加载主场景。如果您愿意,它将需要更多的工作。我尚未尝试过,但是我为macOS应用做了类似的操作,下面将对此进行介绍。

    首先,不要忘记删除旧的@UIApplicationMain批注。在下面的代码中,我将其注释掉,以便您明白我的意思。

    确保Info.plist中的“应用程序场景清单”没有UISceneStoryboardFile设置。由于您使用的是基于UIHostingController的应用程序,因此不应该…但是您的代码可能已更新并留在了那里。如果Info.plist文件中包含UISceneStoryboardFile键的值,则该应用可能默认为该值,并忽略您的所有工作。所以要小心另外,如果更改了Info.plist,请记住,您可能需要从模拟器/设备中删除该应用程序,然后重新部署以确保更改生效。

    在排除所有这些先决条件之后,让我们开始更新代码。

    import SwiftUI @main struct MainApp { static func main() { if #available(iOS 14.0, *) { MyNewUI.main() } else { UIApplicationMain( CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(AppDelegate.self)) } } } @available(iOS 14.0, *) struct MyNewUI: App { var body: some Scene { WindowGroup { Text("This is my new UI! Pretty basic, huh?") } } } //@UIApplicationMain 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() 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) { ... } }

    使用@available(iOS 14.0,*)注释App结构非常重要。 这将防止编译器抱怨,因为App在较早的OS版本中不存在。

    类似的逻辑适用于macOS,让我们来看另一个示例。

    macOS示例(#1) 在第一个macOS示例中,旧的UI使用的是NSHostingView。 第二个示例将使用故事板。

    与iOS示例一样,有一些先决条件:

    从您的Info.plist文件中删除NSMainStoryboardFile条目。从Info.plist文件中删除NSPrincipalClass条目。删除@NSApplicationMain批注。

    如果不从Info.plist文件中删除这些条目,则在启动时可能会覆盖您的逻辑。 因此,请勿跳过该部分。

    import SwiftUI var appDelegate = AppDelegate() @main struct AppUserInterfaceSelector { static func main() { if #available(OSX 11.0, *) { NewUIApp.main() } else { OldUIApp.main() } } } @available(OSX 11.0, *) struct NewUIApp: App { var body: some Scene { WindowGroup() { NewContentView() } } } struct OldUIApp { static func main() { NSApplication.shared.setActivationPolicy(.regular) let nib = NSNib(nibNamed: NSNib.Name("MainMenu"), bundle: Bundle.main) nib?.instantiate(withOwner: NSApplication.shared, topLevelObjects: nil) NSApp.delegate = appDelegate NSApp.activate(ignoringOtherApps: true) NSApp.run() } } class AppDelegate: NSObject, NSApplicationDelegate { var window: NSWindow! func applicationDidFinishLaunching(_ aNotification: Notification) { // Create the SwiftUI view that provides the window contents. let contentView = OldContentView() // Create the window and set the content view. window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false) window.title = "Test Application" window.isReleasedWhenClosed = false window.center() window.setFrameAutosaveName("Main Window") window.contentView = NSHostingView(rootView: contentView) window.makeKeyAndOrderFront(nil) } func applicationWillTerminate(_ aNotification: Notification) { ... } }

    上面的代码需要注意一些事情。 我们为AppDelegate创建了一个全局变量。 这是因为NSApp.delegate是一个弱属性,因此我们需要保留该对象。 还要注意,应用程序菜单是从xib文件加载的。

    macOS示例(#2)

    在我们的第二个macOS示例中,旧的UI将使用故事板,而不是NSHostingView。 因此,该部分已从applicationDidFinishLaunching中删除。 其余代码几乎相同,但是我们需要在OldUIApp.main()函数中添加几行:

    struct OldUIApp { static func main() { NSApplication.shared.setActivationPolicy(.regular) // Load MainMenu, from MainMenu.xib let nib = NSNib(nibNamed: NSNib.Name("MainMenu"), bundle: Bundle.main) nib?.instantiate(withOwner: NSApplication.shared, topLevelObjects: nil) // Load Main storyboard and show main window let sb = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: .main) let windowController = sb.instantiateInitialController() as? NSWindowController windowController?.window?.makeKeyAndOrderFront(nil) NSApp.delegate = appDelegate NSApp.activate(ignoringOtherApps: true) NSApp.run() } }

    这个新版本的main()函数,加载情节提要,实例化窗口控制器并显示其窗口。 请注意,菜单仍来自xib文件。 我没有找到从情节提要中引用菜单的方法。 可能有办法,但我没有看。 如果您知道如何,请在下面发表评论。

    更进一步 在上面的示例中,我们根据检测到的OS版本确定要运行的UI。 但是,没有什么可以阻止您基于其他事实做出该决定。 例如,在UserDefaults中保存的值,或在应用程序启动时按的键,或两者。 例如:

    @main struct AppUserInterfaceSelector { static func main() { if #available(OSX 11.0, *) { // Use old interface, if SHIFT key is pressed during app launch, or // if UseOldUI is set to true in UserDefaults. if NSEvent.modifierFlags.contains(.shift) || UserDefaults.standard.bool(forKey: "UseOldUI") { OldUIApp.main() } else { NewUIApp.main() } } else { OldUIApp.main() } } }

    在上面的macOS示例中,如果应用程序默认设置中包含布尔键,或者如果在应用程序启动时按下SHIFT键,则将使用旧的UI,并且与运行该应用程序的OS版本无关。这对于测试非常有用。

    如果您包括这种类型的有条件的UI选择,请小心,因为这可能会违反Apple Review Guidelines。请记住,App Store不能包含Beta版软件,因此可以选择其他UI。特别是如果“新”设计不是默认设计。无论如何,在App Store外部分发或进行自己的测试时,它都是完全安全的。

    总结

    每年,我们都会面临决定何时采用Apple在WWDC期间为我们带来的新技术的挑战。在前进或保持与旧OS版本的兼容性之间找到平衡并非易事。我希望本文中的提示能使您的决策更加轻松。

    原文地址

    https://swiftui-lab.com/backward-compatibility/

    推荐

    基础文章推荐

    《SwiftUI是什么,听听大牛们如何说》

    经典教程推荐

    更新近百篇SwiftUI教程《SwiftUI2020教程》帮您突破数据存储难关《SwiftUI vs CoreData数据存储解决方案》

    技术源码推荐

    推荐文章

    CoreData篇

    SwiftUI数据存储之做个笔记App 新增与查询(CoreData)SwiftUI进阶之存储用户状态实现登录与登出SwiftUI 数据之List显示Sqlite数据库内容(2020年教程)

    Combine篇

    一篇文章学懂弄通SwiftUI与Combine(含轮播动画App源码)

    TextField篇

    《SwiftUI 一篇文章全面掌握TextField文本框 (教程和全部源码)》《SwiftUI实战之TextField风格自定义与formatters》《SwiftUI实战之TextField如何给键盘增加个返回按钮(隐藏键盘)》《SwiftUI 当键盘出现时避免TextField被遮挡自动向上移动》《SwiftUI实战之TextField如何给键盘增加个返回按钮(隐藏键盘)》

    JSON文件篇

    SwiftUI JSON文件下载、存储、解析和展示(代码大全)

    一篇文章系列

    SwiftUI一篇文章全面掌握List(教程和源码)《SwiftUI 一篇文章全面掌握TextField文本框 (教程和全部源码)》SwiftUI一篇文章全面掌握Picker,解决数据选择(教程和源码)SwiftUI一篇文章全面掌握Form(教程和源码)SwiftUI Color 颜色一篇文章全解决

    技术交流

    QQ:3365059189 SwiftUI技术交流QQ群:518696470

    请关注我的专栏icloudend, SwiftUI教程与源码 https://www.jianshu.com/c/7b3e3b671970
    Processed: 0.022, SQL: 9