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

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/

推荐

基础文章推荐

经典教程推荐

技术源码推荐

推荐文章

CoreData篇

Combine篇

TextField篇

JSON文件篇


一篇文章系列

技术交流

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351