SWIFT 5.3增强了SWIFT UI的DSL

2020-09-14 02:46:40

提供的基础文章:SwiftUI当SwiftUI首次在WWDC 2019上推出时,通过大量使用泛型、基于闭包的API和全新功能(如属性包装器和函数构建器),它无疑将SWIFT和Xcode的许多方面推向了极限。

因此,即将发布的SWIFT 5.3新版的一大重点是继续扩展使用SWIFT构建SwiftUI样式域特定语言(或DSL)的方式,并消除许多开发人员在SWIFT 5.2和更早版本中使用SwiftUI时遇到的一些“粗糙边缘”,这并不令人惊讶。

本周,让我们来看看其中的一些改进,以及它们如何共同增强使用SwiftUI构建视图的整体体验。

这则广告让所有人都可以免费享受Swift by Sundell的所有服务。如果可以,请查看此赞助商,因为这将直接帮助您支持此网站:

:我最喜欢的持续集成服务。根据每个拉取请求自动构建、测试和分发您的应用程序-这非常适合现在远程工作的团队,因为您将快速获得有关您所做的每个更改的反馈。尝试他们新的、改进的免费层来开始吧。

从一开始,SWIFT就要求我们在访问转义闭包中的实例方法或属性时显式指定self,这在某种程度上相当于让闭包捕获封闭对象或值的“选择加入”-因为这样做在某些情况下可能最终导致保留循环。

但是,由于在使用值类型时保留周期的风险通常可以忽略不计,因此在SWIFT 5.3中,通过使编译器能够隐式捕获结构实例(SwiftUI视图和修饰符几乎是以独占方式实现的),始终必须指定self的要求有所放宽。

例如,假设我们使用SWIFT 5.2构建了以下FavoriteButton,这要求我们在其底层按钮的action闭包内引用其ISON属性时使用Self:

Struct FavoriteButton:view{@binding var ison:bool var body:Some View{Button(action:{sel.。伊森。切换()},标签:{Image(systemName:";HARD";+(ISON?";.Fill";:";";))})}。

但是,当升级到SWIFT 5.3时,现在可以完全删除该自我引用-这为我们提供了稍微简单的实施:

Struct FavoriteButton:view{@binding var ison:bool var body:Some View{Button(action:{iso.。切换()},标签:{Image(systemName:";HARD";+(ISON?";.Fill";:";";))})}。

虽然上面的改变在总体上可能是一个很小的改变,但它确实让SwiftUI的DSL感觉更轻巧、更容易使用。

此外,作为一个副作用,因为现在只有在真正重要的情况下才需要显式指定self(例如在处理捕获的引用类型时),这应该会使代码库的这些部分更加“突出”一点,这反过来可以更容易地发现此类代码中潜在的与保留周期相关的问题。

通常,在构建UI时,根据给定应用程序或功能当前所处的状态,想要使用单独的视图实现是非常常见的。

例如,假设我们当前正在构建一个应用程序,该应用程序使用AppState对象来跟踪其整体状态,其中包括用户是否已通过应用程序的入网流程等属性。然后,我们检查应用程序根视图中的状态,以确定是否应该显示HomeView或OnboardingView-如下所示:

Struct RootView:view{@ObservedObject var state:AppState var body:Some View{if state.。IsOnboardingCompleted{Return AnyView(HomeView(state:state))}Else{Return AnyView(OnboardingView(isCompleted:$stat.。IsOnboardingCompleted))}

请注意,我们是如何使用SwiftUI的AnyView类型在上述两个视图实例上执行类型擦除的-这样做是为了给Body属性一个单一的、统一的返回类型。然而,这样使用AnyView不仅会给我们的代码增加相当多的“混乱”,还会降低SwiftUI的基于类型的差异算法的效率,因为包含在视图正文中的所有类型信息当前都被完全擦除了。

值得庆幸的是,有一种更好的方法来实现上述条件-即使在使用SWIFT 5.2或更早版本时也是如此-那就是手动将@ViewBuilder属性添加到视图的Body属性中,这样我们就可以在该属性的实现中直接充分利用SwiftUI的Function Builder驱动的DSL:

Struct RootView:view{@ObservedObject var state:AppState@ViewBuilder var body:Some View{if state.。IsOnboardingCompleted{HomeView(STATE:STATE)}ELSE{OnboardingView(isCompleted:$STATE。IsOnboardingCompleted)}。

SWIFT 5.3中的新功能是,所有视图现在都自动获得上述功能,因为视图现在直接从View协议本身的声明继承@ViewBuilder属性-这意味着我们可以继续使用上述方法,而不必向视图正文添加任何附加属性:

Struct RootView:view{@ObservedObject var state:AppState var body:Some View{if state.。IsOnboardingCompleted{HomeView(STATE:STATE)}ELSE{OnboardingView(isCompleted:$STATE。IsOnboardingCompleted)}。

以上可能也是一个相对较小的改变,但它肯定会让有条件地创建单独的视图类型变得更简单、更直观-这反过来应该会导致AnyView实例更少,从而在许多基于SwiftUI的应用程序中更简单的代码和更好的整体性能。

如上所述,SwiftUI的整体API在很大程度上是由SWIFT的函数构建器功能提供支持的-这使得我们可以简单地表达我们希望呈现的各种视图,然后SwiftUI会自动组合这些表达式以形成我们的最终UI。

然而,这种强大和便利也伴随着一定的限制和缺陷。例如,在SWIFT 5.2及更早版本中,在函数生成器上下文中只能使用一组非常有限的控制流机制,如基本的if和Else语句。

因此,如果我们想使用稍微复杂一点的方法来处理多个状态,例如使用Switch语句,那么我们必须再次求助于将AnyView包装的视图作为主体实现中的单独表达式显式返回-如下所示:

Struct ContentView<;Content:View>;:View{枚举状态{案例加载案例加载(内容)案例失败(错误)}var state:state var body:Some View{Switch State{Case.。正在加载:返回AnyView(LoadingSpner())case。已加载(Let Content):返回AnyView(Content)大小写。失败(LET错误):返回AnyView(ErrorView(ErrorView(Error:Error))}。

但是,在SWIFT 5.3中,函数构建器上下文中现在完全支持Switch语句-这意味着我们可以再次移除AnyView包装器,只需表达我们希望在每个代码分支中呈现的视图:

Struct ContentView<;内容:View>;:View{...。变量正文:某个视图{开关状态{案例。正在加载:LoadingSpner()case。已加载(Let Content):内容案例。失败(LET ERROR):ErrorView(Error:Error)}。

沿着同样的思路,可选的-现在也完全支持展开if let条件-这意味着我们不再需要想出我们自己的技术来呈现依赖于某种形式的可选数据的视图。SWIFT 5.2及更早版本中常用的一种这样的技术是将常规IF语句与强制展开相结合-例如,如下所示:

Struct HomeView:view{@ObservedObject var userController:UserController var body:Some View{VStack{if userController.。LoggedInUser!=nil{ProfileView(User:userControl.。LoggedInUser!)}...}。

现在,一旦我们准备好升级到SWIFT 5.3,我们就可以简单地使用标准的if let条件来编写上述类型的表达式-这既使这类代码变得简单得多,又删除了强制展开选项(大赢家!):

Struct HomeView:view{@ObservedObject var userController:UserController var body:Some View{VStack{if let user=userController.。LoggedInUser{ProfileView(User:User)}...}

SWIFT 5.3还引入了一个名为多个尾随闭包的新功能(有些有争议),顾名思义,它使我们能够在调用接受多个闭包的函数或初始化式时附加多个尾随闭包。

虽然该功能的确切语法自首次引入以来就在SWIFT论坛上引起了激烈的讨论,但可以说,它确实使某些API的调用点稍微更干净、更易于阅读。例如,如果我们在创建底层Button实例时使用多个尾随闭包,则前面的FavoriteButton实现将是什么样子:

Struct FavoriteButton:view{@binding var ison:bool var body:Some View{Button{iso.。切换()}标签:{Image(systemName:";HARD";+(ISON?";.Fill";:";";))}}。

上述语法的主要优点是,它使使用多个闭包的API(几乎所有提供某种形式的事件处理的SwiftUI视图都这样做)在SwiftUis DSL中感觉更“自在”,并且使我们能够使用附加的尾随闭包逐渐扩展给定的调用,而不必重写整个表达式。

然而,特别是在上面这样的情况下,也可以争辩说,不再清楚第一个拖尾(现在没有标签)闭包的作用-所以我们可能仍然希望在某些情况下显式地标记每个闭包,这当然仍然是一种选择。

最后,让我们看看Xcode 12附带的SwiftUI版本如何利用SWIFT 5.3的新@main属性来声明应用程序的主要入口点,其方式与我们定义各种视图的方式非常相似。

作为一项语言功能,@Main属性使任何SWIFT程序都可以定义基于类型的入口点-即实现用于运行程序根逻辑的静态Main方法的类型:

@main struct MyApp{static func main(){//运行我们程序的根逻辑}}

虽然上面定义应用程序主要入口点的方法对于完全自定义的程序(如脚本和命令行工具)可能真的很有效,但当涉及到iOS和MacOS应用程序时,我们可能不想完全控制应用程序的启动和运行所涉及的一切-多亏了SwiftUI的新App协议,我们不需要这样做。

通过将@main属性与新协议相结合,我们可以简单地使用SwiftUI的DSL来定义我们应用程序的各种场景,以及这些场景中包含的根视图-这意味着现在可以直接使用SwiftUI构建整个应用程序-例如:

@main struct MyApp:app{@StateObject var state=AppState()var body:Some Scene{WindowGroup{if state.。IsOnboardingCompleted{HomeView(STATE:STATE)}ELSE{OnboardingView(isCompleted:$STATE。IsOnboardingCompleted)}。

虽然与UIKit的UIApplicationDelegate必须提供的所有功能相比,上面的新API(在撰写本文时)相当有限,但好消息是,我们还可以使用UIApplicationDelegateAdaptor属性包装器轻松连接这两个领域。要了解更多信息,请查看这篇迷你文章。

这则广告让所有人都可以免费享受Swift by Sundell的所有服务。如果可以,请查看此赞助商,因为这将直接帮助您支持此网站:

:我最喜欢的持续集成服务。根据每个拉取请求自动构建、测试和分发您的应用程序-这非常适合现在远程工作的团队,因为您将快速获得有关您所做的每个更改的反馈。尝试他们新的、改进的免费层来开始吧。

SWIFT 5.3为SWIFT UI的整体API带来了许多非常受欢迎的增强,虽然它可能不会从根本上改变我们使用SWIFT的方式(考虑到这只是一个小的版本提升,这会很奇怪),但它显示出SWIFT和SwiftUI继续同步发展的紧密程度。

然而,作为语言本身的功能,而不是任何特定的SDK,我们可以同时使用SwiftUI和苹果平台之外的这些新功能,而且我们也可以使用它们,而不必将我们的应用程序的最低部署目标增加到iOS 14或MacOS Big Sur。我们所要做的就是使用Xcode12构建我们的项目,这样我们就可以充分利用SWIFT 5.3提供的所有功能。

唯一的例外是App协议,它只在Apple的2020版操作系统上可用,因为它是更通用(并且向后兼容)@main属性的具体SwiftUI特定实现。

有问题、评论或反馈吗?随时欢迎您与我联系。你可以通过推特或电子邮件联系我。