SwiftUI2.0如何向后兼容之前項目和代碼

SwiftUI如何向后兼容性

現(xiàn)在是時候開始發(fā)現(xiàn)WWDC 2020帶來的所有新SwiftUI功能了。 但是,就像每年一樣,幾毫秒后,興奮就消散了,當(dāng)您記住放棄對較早版本的OS的支持并不是您的選擇。

通常,我們求助于#available朋友。 例如,假設(shè)您有一個較長的HStack。 您可以決定使用新的LazyHStack,以利用其對長堆棧的性能改進(jìn)。 但是,如果您的應(yīng)用程序在iOS13上運(yùn)行,則可以回退到使用普通的普通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()
        }
    }
}

這很容易替代,但是在更復(fù)雜的情況下,您的后備代碼將需要更極端的措施,例如由Representable包裝的UIKit / AppKit視圖。

這種方法對于一個很小的項目來說看起來不錯,但是隨著視圖數(shù)量的增加,每次添加#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
    }
}

通過創(chuàng)建自己的LazyHStack(可在所有OS版本上使用),編譯器將不再抱怨。 這是因為現(xiàn)在LazyHStack引用MyApp.LazyHStack而不是SwiftUI.LazyHStack。 然后,在我們自己的實現(xiàn)中,檢查版本,然后決定是使用舊的SwiftUI.HStack還是新的SwiftUI.LazyHStack。

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

到目前為止,這有些瑣碎。但是,現(xiàn)在我想將重點轉(zhuǎn)移到新的SwiftUI帶來的特定問題上。我正在談?wù)撊绾问褂眯碌腁pp和Scene API,以及啟動我們的應(yīng)用程序的“舊方法”。

擁抱變化
假設(shè)您有一個應(yīng)用程序已經(jīng)在較舊的操作系統(tǒng)版本下運(yùn)行。現(xiàn)在,在見證了新的SwiftUI改進(jìn)浪潮之后,您最終決定是時候讓您的應(yīng)用包含新框架了。我不會討論SwiftUI是否足夠成熟。這在很大程度上取決于您正在編寫的應(yīng)用程序的類型,但是就本文而言,我們假設(shè)SwiftUI確實適合您的應(yīng)用程序。

對于此特定示例,我們將探討是否有可能為運(yùn)行早期OS版本的用戶保留舊的UI,并從頭開始重新設(shè)計SwiftUI界面,從而使運(yùn)行iOS14.0 + / macOS11 +的用戶受益

從Xcode 12開始,現(xiàn)在可以設(shè)計一個完全使用SwiftUI編寫的應(yīng)用程序。在過去(即去年),您仍然需要像往常一樣連接場景/窗口的層次結(jié)構(gòu)。不再需要,現(xiàn)在只需編寫幾行代碼即可編寫完整的應(yīng)用程序,如下所示

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

這雖然很棒,但在我們嘗試同時保留舊UI和新UI時都提出了一個問題。 如果嘗試使用“ if #available”來包裝@main聲明,則會收到編譯器錯誤。

不允許在頂層使用#available,因此編譯器將為我們提供以下錯誤:

#available
#available

幸運(yùn)的是,仍然有解決方法。讓我們來看看…

應(yīng)用程序入口點(@main)
Swift 5.3具有一個名為@main(SE-0281)的新屬性。由于這是該語言的一部分(即不是庫),因此只要您使用Swift 5.3+進(jìn)行編譯,它就可以與舊版OS一起使用。我們這里真正的問題是,在較早的OS版本中不存在App協(xié)議。那么,考慮到整個應(yīng)用程序中只能有一種帶@main的帶注釋類型,我們?nèi)绾谓鉀Q呢?

還要注意,@main和@ UIApplicationMain / @ NSApplicationMain與main.swift中的頂級代碼是互斥的。您只能使用一種類型的入口點。

如果我們查看文檔,就會發(fā)現(xiàn)@main為您的應(yīng)用程序提供了入口點。特別是,您的應(yīng)用程序?qū)奶D(zhuǎn)到以@main開頭的類型的main()函數(shù)開始。查看代碼,我們可以推斷出App協(xié)議必須具有main()函數(shù)的默認(rèn)實現(xiàn)。確實如此,請在此處查看Apple的文檔。

這是我們需要知道的全部信息,以便創(chuàng)建一個將新App協(xié)議用于新OS版本以及將舊UIApplication / NSApplication類型用于其他應(yīng)用程序的應(yīng)用程序??紤]到這一點,我們將需要按以下說明更改代碼。

一些注意事項

  • 后面的代碼僅作為起點。 多年來,有許多方法可以啟動應(yīng)用程序(例如,主故事板,XIB文件,場景清單,手動調(diào)用UIApplicationMain等)。 這意味著應(yīng)以不同的方式處理每種情況。 這里的代碼只會為您指明正確的方向(我希望如此)。
  • 在嘗試進(jìn)行此操作時,我發(fā)現(xiàn)有時該應(yīng)用有些固執(zhí),無法按照我的預(yù)期去做。 我發(fā)現(xiàn)從模擬器中刪除該應(yīng)用程序并重新部署解決了該問題。 這可能與Info.plist中的更改未正確更新有關(guān)……但是我不確定。 只要記住它,就應(yīng)該在您身上發(fā)生。
  • 最后一點警告:您不喜歡這個,我很抱歉…但是我們在這里處理了一些未記錄的行為,因此,風(fēng)險自負(fù)!

iOS示例

在以下示例中,我們的舊UI使用UIHostingController作為主控制器,因此不會從情節(jié)提要中加載主場景。如果您愿意,它將需要更多的工作。我尚未嘗試過,但是我為macOS應(yīng)用做了類似的操作,下面將對此進(jìn)行介紹。

首先,不要忘記刪除舊的@UIApplicationMain批注。在下面的代碼中,我將其注釋掉,以便您明白我的意思。

確保Info.plist中的“應(yīng)用程序場景清單”沒有UISceneStoryboardFile設(shè)置。由于您使用的是基于UIHostingController的應(yīng)用程序,因此不應(yīng)該…但是您的代碼可能已更新并留在了那里。如果Info.plist文件中包含UISceneStoryboardFile鍵的值,則該應(yīng)用可能默認(rèn)為該值,并忽略您的所有工作。所以要小心另外,如果更改了Info.plist,請記住,您可能需要從模擬器/設(shè)備中刪除該應(yīng)用程序,然后重新部署以確保更改生效。

在排除所有這些先決條件之后,讓我們開始更新代碼。

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結(jié)構(gòu)非常重要。 這將防止編譯器抱怨,因為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) { ... }
}

上面的代碼需要注意一些事情。 我們?yōu)锳ppDelegate創(chuàng)建了一個全局變量。 這是因為NSApp.delegate是一個弱屬性,因此我們需要保留該對象。 還要注意,應(yīng)用程序菜單是從xib文件加載的。

macOS示例(#2)

在我們的第二個macOS示例中,舊的UI將使用故事板,而不是NSHostingView。 因此,該部分已從applicationDidFinishLaunching中刪除。 其余代碼幾乎相同,但是我們需要在OldUIApp.main()函數(shù)中添加幾行:

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()函數(shù),加載情節(jié)提要,實例化窗口控制器并顯示其窗口。 請注意,菜單仍來自xib文件。 我沒有找到從情節(jié)提要中引用菜單的方法。 可能有辦法,但我沒有看。 如果您知道如何,請在下面發(fā)表評論。

更進(jìn)一步
在上面的示例中,我們根據(jù)檢測到的OS版本確定要運(yùn)行的UI。 但是,沒有什么可以阻止您基于其他事實做出該決定。 例如,在UserDefaults中保存的值,或在應(yīng)用程序啟動時按的鍵,或兩者。 例如:

@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示例中,如果應(yīng)用程序默認(rèn)設(shè)置中包含布爾鍵,或者如果在應(yīng)用程序啟動時按下SHIFT鍵,則將使用舊的UI,并且與運(yùn)行該應(yīng)用程序的OS版本無關(guān)。這對于測試非常有用。

如果您包括這種類型的有條件的UI選擇,請小心,因為這可能會違反Apple Review Guidelines。請記住,App Store不能包含Beta版軟件,因此可以選擇其他UI。特別是如果“新”設(shè)計不是默認(rèn)設(shè)計。無論如何,在App Store外部分發(fā)或進(jìn)行自己的測試時,它都是完全安全的。

總結(jié)

每年,我們都會面臨決定何時采用Apple在WWDC期間為我們帶來的新技術(shù)的挑戰(zhàn)。在前進(jìn)或保持與舊OS版本的兼容性之間找到平衡并非易事。我希望本文中的提示能使您的決策更加輕松。

原文地址

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

推薦

基礎(chǔ)文章推薦

經(jīng)典教程推薦

技術(shù)源碼推薦

推薦文章

CoreData篇

Combine篇

TextField篇

JSON文件篇


一篇文章系列

技術(shù)交流

QQ:3365059189
SwiftUI技術(shù)交流QQ群:518696470

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容