Swift 5.5新增功能:现在可以使用Swift的#if编译器指令有条件地编译后缀成员表达式。让我们看看这个新特性在哪些情况下真正有用。

1. Working around platform differences

尽管许多内置api和框架在苹果平台上的工作方式完全相同,但我们可能需要解决某些差异。例如,当使用SwiftUI构建一个同时在iOS和Mac上运行的应用程序时,我们可能会发现自己处于以下类型的情况——当我们试图将iOS特有的InsetGroupedListStyle应用到List时,我们会得到一个错误:

struct ItemList: View {
    var items: [Item]

    var body: some View {
        List {
            ...
        }
        // Error: 'InsetGroupedListStyle' is unavailable in macOS
        .listStyle(InsetGroupedListStyle())
    }
}

一般来说,这类问题可以通过使用编译时平台检查来解决——但在Swift 5.5之前,我们必须首先将List分解成一个单独的表达式,然后使用基于#if的操作系统条件分别应用不同的listStyle修饰符:

struct ItemList: View {
    var items: [Item]

    var body: some View {
        let list = List {
            ...
        }

        #if os(iOS)
        list.listStyle(InsetGroupedListStyle())
        #else
        list.listStyle(InsetListStyle())
        #endif
    }
}

单独来看,上面的代码看起来并没有那么糟糕,但是如果我们的ItemList视图要获得额外的子视图(或者如果我们需要在其中执行额外的编译时检查),那么它的主体很快就会变得非常难以阅读。

因此,解决这个问题的一个更健壮的解决方案可能是将上述平台检查提取到一个专用的修改器方法中——例如:

extension View {
    func defaultListStyle() -> some View {
        #if os(iOS)
        listStyle(InsetGroupedListStyle())
        #else
        listStyle(InsetListStyle())
        #endif
    }
}

有了上面的内容,我们可以简单地在ItemList视图中应用新的defaultListStyle修饰符,并且在构造实际的UI时,我们不再需要处理任何平台差异:

struct ItemList: View {
    var items: [Item]

    var body: some View {
        List {
            ...
        }
        .defaultListStyle()
    }
}

然而,尽管在处理我们希望在项目中多次重用的修饰符和样式时,上述方法确实是一种整洁的技术,但每次遇到平台差异时都必须定义专用方法,这可能会变得非常乏味。

这就是Swift 5.5在后缀成员表达式中支持#if条件的地方。

2. Inline checks within expressions

当使用Swift 5.5(在编写本文时,作为Xcode 13的一部分正在测试)时,我们现在可以选择在表达式中内联#if指令。那么,回到我们的ItemList,我们现在可以有条件地完全内联地应用每个listStyle修饰符,而不必首先将表达式分成多个部分:

struct ItemList: View {
    var items: [Item]

    var body: some View {
        List {
            ...
        }
        #if os(iOS)
        .listStyle(.insetGrouped)
        #else
        .listStyle(.inset)
        #endif
    }
}

很不错! 当然,这个新功能也适用于其他类型的基于#if的编译时检查——包括使用标准DEBUG标志来检查我们的应用程序是否正在使用它的调试构建配置进行编译,以及任何自定义编译器标志。

作为一个例子,下面是我们如何使用自定义标记来有条件地可视化一个视图的最终渲染大小:

struct DynamicIcon: View {
    var name: String

    var body: some View {
        Image(systemName: name)
            .resizable()
            .aspectRatio(contentMode: .fit)
            #if SHOW_ICON_SIZES
            .overlay(GeometryReader { geo in
                Text("\(Int(geo.size.width)) x \(Int(geo.size.height))")
                    .font(.footnote)
                    .background(Color.blue)
                    .foregroundColor(.white)
            })
            #endif
    }
}

3. Conclusion

那么,我们应该如何在这些新的内联#if条件和创建专门的修饰符来处理平台差异和执行其他类型的编译时检查之间进行选择呢? 虽然每个开发人员肯定都有自己的偏好——对我来说,这完全取决于给定的模式是否会在代码库中重复,或者它是否只执行一次。