美文网首页
SwiftUI:views检查Mirror

SwiftUI:views检查Mirror

作者: 猪猪行天下 | 来源:发表于2021-04-19 17:26 被阅读0次

    到目前为止,我们一直接受给定的输入,通常以@ViewBuilder content: () -> Content参数的形式,作为一个黑盒:它是一个视图,这就是我们需要知道的。
    但如果我们想更多地了解这个视图呢?在本文中,让我们探索如何做到这一点。

    一个新的选择器组件

    假设我们的任务是创建一个新的picker/segmented控件:

    custom.gif

    SwiftUI的Picker开始是很好用的:

    public struct Picker<Label: View, SelectionValue: Hashable, Content: View>: View {
      public init(
        selection: Binding<SelectionValue>,
        label: Label,
        @ViewBuilder content: () -> Content
      )
    }
    

    我们可以这样使用:

    struct ContentView: View {
      @State private var selection = 0
    
      var body: some View {
        Picker(selection: $selection, label: Text("")) {
          Text("First").tag(0)
          Text("Second").tag(1)
          Text("Third").tag(2)
        }
        .pickerStyle(SegmentedPickerStyle())
      }
    }
    

    为了简单起见,我们将忽略label参数。

    original.gif

    Picker使用tag(_:)来标识哪个元素对应于哪个selection选择值。

    这一点非常重要,因为picker需要的:

    • 在任何时候突出显示所选的元素
    • 为每个元素添加一个点击手势
    • 根据用户交互更新当前选择。

    FSPickerStyle

    首先,我们可以尝试创建一个新的PickerStyle,就像我们对LabelButton所做的那样,下面是PickerStyle的定义:

    public protocol PickerStyle {
    }
    

    酷,没有要求!让我们创建我们的选择器样式:

    struct FSPickerStyle: PickerStyle {
    }
    

    这不会构建。虽然没有公共需求,但PickerStyle实际上有一些私有/内部需求,如下所示:

    protocol PickerStyle {
      static func _makeView<SelectionValue>(
        value: _GraphValue<_PickerValue<FSPickerStyle, SelectionValue>>, 
        inputs: _ViewInputs
      ) -> _ViewOutputs where SelectionValue: Hashable
    
      static func _makeViewList<SelectionValue>(
        value: _GraphValue<_PickerValue<FSPickerStyle, SelectionValue>>,
        inputs: _ViewListInputs
      ) -> _ViewListOutputs where SelectionValue: Hashable
    }
    

    _下划线的变量属于“私人的东西”,应该告诉我们那不是我们想要走的路:
    探索这样的api是留给好奇/冒险的人的。

    由于PickerStyle不是一个可行的选择,让我们开始从头构建我们自己的SwiftUI选择器。

    FSPicker

    尽管我们创建了自己的组件,我们仍然希望尽可能模仿SwiftUI的Picker选择器的API:

    public struct FSPicker<SelectionValue: Hashable, Content: View>: View {
      @Binding var selection: SelectionValue
      let content: Content
    
      public init(
        selection: Binding<SelectionValue>, 
        @ViewBuilder content: () -> Content
      ) {
        self._selection = selection
        self.content = content()
      }
    
      public var body: some View {
        ...
      }
    }
    

    到目前为止,一切都很好,多亏了这个声明,我们可以回到最初的例子,在Picker选择器中添加一个FS前缀,一切都会很好地构建:

    struct ContentView: View {
      @State private var selection = 0
    
      var body: some View {
        // 👇🏻 our picker!
        FSPicker(selection: $selection) {
          Text("First").tag(0)
          Text("Second").tag(1)
          Text("Third").tag(2)
        }
      }
    }
    

    现在让我们来实现FSPickerbody部分。

    FSPicker的body

    当我们看到像@ViewBuilder content: () -> content这样的参数时,我们通常把它当作我们放在自己的视图body中的某个地方的东西,但是我们不能这样做我们选择器。

    这是因为我们的选择器body需要获取该content,突出显示所选元素,并添加手势来响应用户的选择。
    解决这个问题的一个变通方法是用我们可以直接使用的东西来替换我们的通用Content参数。例如,我们可以用一个元组数组替换Content,其中每个元组包含一个String和一个相关联的SelectionValue:

    public struct FSPicker<SelectionValue: Hashable>: View {
      @Binding var selection: SelectionValue
      let content: [(String, SelectionValue)]
    
      public init(
        selection: Binding<SelectionValue>,
        content: [(String, SelectionValue)]
      ) {
        self._selection = selection
        self.content = content
      }
    
      public var body: some View {
        HStack {
          ForEach(content, id: \.1) { (title, value) in
            Button(title) { selection = value }
          }
        }
      }
    }
    

    然而,我们不会再遵循SwiftUI的选择器api了。
    相反,让我们让事情变得更有趣:让我们拥抱我们的“黑盒”内容,并使用Swift的反射!

    Mirror镜像反射

    虽然我们的拾取器不可能总是在构建时知道实际的content,Swift允许我们在运行时通过Mirror
    镜像检查这个值。
    让我们用以下方式更新我们的FSPicker声明:

    public struct FSPicker<SelectionValue: Hashable, Content: View>: View {
      ...
      public init(...) { ... }
    
      public var body: some View {
        let contentMirror = Mirror(reflecting: content)
        let _ = print(contentMirror)
        EmptyView()
      }
    }
    

    如果我们现在用我们最初的例子运行它,我们会在Xcode的调试区控制台中看到以下日志:

    Mirror for TupleView<
      (
        ModifiedContent<Text, _TraitWritingModifier<TagValueTraitKey<Int>>>, 
        ModifiedContent<Text, _TraitWritingModifier<TagValueTraitKey<Int>>>, 
        ModifiedContent<Text, _TraitWritingModifier<TagValueTraitKey<Int>>>
      )
    >
    

    这应该不会让我们太惊讶,除了一些不熟悉的术语。
    以下是我们最初的内容:

    Text("First").tag(0)
    Text("Second").tag(1)
    Text("Third").tag(2)
    

    我们可以看到:

    • @ViewBuilder把我们的三个Text文本,放在一个TupleView与三个block块。
    • 每个“block块”由一个ModifiedContent实例组成,这是对每个Text文本应用tag(_:)视图修饰符的结果。

    让我们接下来打印content实例:

    public struct FSPicker<SelectionValue: Hashable, Content: View>: View {
      ...
      public init(...) { ... }
    
      public var body: some View {
        let contentMirrorValue = Mirror(reflecting: content).descendant("value")!
        let _ = print(contentMirrorValue)
        EmptyView()
      }
    }
    

    为了简洁起见,我们强制展开

    这一次控制台显示:

    (
      ModifiedContent(
        content: Text(
          storage: Text.Storage.anyTextStorage(
            LocalizedTextStorage
          ), 
          modifiers: []
        ), 
        modifier: _TraitWritingModifier(
          value: TagValueTraitKey.Value.tagged(0)
        )
      ), 
      ModifiedContent(
        content: Text(
          storage: Text.Storage.anyTextStorage(
            LocalizedTextStorage
          ), 
          modifiers: []
        ), 
        modifier: _TraitWritingModifier(
          value: TagValueTraitKey.Value.tagged(1)
        )
      ), 
      ModifiedContent(
        content: Text(
          storage: Text.Storage.anyTextStorage(
            LocalizedTextStorage
          ), 
          modifiers: []
        ), 
        modifier: _TraitWritingModifier(
          value: TagValueTraitKey.Value.tagged(2)
        )
      )
    )
    

    为了清晰起见,对输出进行了格式化和稍微简化。

    这也不应该让我们太惊讶:这和以前的输出是一样的,我们现在看到的不是TupleView的类型,而是实际的值。
    请注意,我们所需要的一切都在这里:所有Text文本及其相关的.tag值。
    接下来,我们可以使用Mirror来导航并分别选择单个Text文本和tag标签值:

    public struct FSPicker<SelectionValue: Hashable, Content: View>: View {
      ...
      public init(...) { ... }
    
      public var body: some View {
        let contentMirror = Mirror(reflecting: content)
        // 👇🏻 The number of `TupleView` blocks.
        let blocksCount = Mirror(reflecting: contentMirror.descendant("value")!).children.count
    
        HStack {
          ForEach(0..<blocksCount) { index in
            // 👇🏻 A single `TupleView` block.
            let tupleBlock = contentMirror.descendant("value", ".\(index)")
            let text: Text = Mirror(reflecting: tupleBlock!).descendant("content") as! Text
            let tag: SelectionValue = Mirror(reflecting: tupleBlock!).descendant("modifier", "value", "tagged") as! SelectionValue
    
            ...
          }
        }
      }
    }
    

    此时,我们已经获得了对每个原始Text文本和tag标签值的访问权,我们可以使用它们来构建我们的视图:

    struct FSPicker<SelectionValue: Hashable, Content: View>: View {
      @Namespace var ns
      @Binding var selection: SelectionValue
      @ViewBuilder let content: Content
    
      public var body: some View {
        let contentMirror = Mirror(reflecting: content)
        let blocksCount = Mirror(reflecting: contentMirror.descendant("value")!).children.count // How many children?
        HStack {
          ForEach(0..<blocksCount) { index in
            let tupleBlock = contentMirror.descendant("value", ".\(index)")
            let text = Mirror(reflecting: tupleBlock!).descendant("content") as! Text
            let tag = Mirror(reflecting: tupleBlock!).descendant("modifier", "value", "tagged") as! SelectionValue
    
            Button {
              selection = tag
            } label: {
              text
                .padding(.vertical, 16)
            }
            .background(
              Group {
                if tag == selection {
                  Color.purple.frame(height: 2)
                    .matchedGeometryEffect(id: "selector", in: ns)
                }
              },
              alignment: .bottom
            )
            .accentColor(tag == selection ? .purple : .black)
            .animation(.easeInOut)
          }
        }
      }
    }
    
    custom.gif

    由于这个定义,FSPicker可以处理任何SelectionValue,并匹配Picker选择器的APIs。

    进一步的改善

    就目前的情况而言,只要给定的内容遵循与我们的示例相同的格式,FSPicker就非常好用。
    这实际上可能是我们想要的:与其试图支持每一个可能的SwiftUI组件,我们可以将其他组件视为错误的输入,并忽略它们。

    如果我们想要支持更多的组件(例如Images),我们可以通过扩展我们的检查来处理这样的元素,或者甚至创建我们自己的视图生成器。

    当然,这只是冰山一角:
    处理任何content内容都意味着要处理更多的组件、边界情况、多层ModifiedContent修改内容等等。

    相关文章

      网友评论

          本文标题:SwiftUI:views检查Mirror

          本文链接:https://www.haomeiwen.com/subject/vrxwlltx.html