Skip to content

suguruwataru/grotesque

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 

Repository files navigation

猎奇

SwiftUI中对coordinate space的设计相当猎奇。

与这个概念最紧密相关的是ViewcoordianteSpace(name:)方法。我们先看文档

Assigns a name to the view’s coordinate space, so other code can operate on dimensions like points and sizes relative to the named space.

好,看了这个文档之后我们都对这个东西有什么作用有所期待了。接着我们看看实际效果。

import SwiftUI

struct ContentView: View {
  var body: some View {
    HStack(spacing: 0) {
      // 左侧
      VStack {
        Spacer()
        VStack {
          Spacer()
          HStack {
            Spacer()
            VStack {
              GeometryReader {
                gp in
                VStack {
                  let originWhite = gp.frame(in: .named("white")).origin
                  Text(
                    "white: \(originWhite.x), \(originWhite.y)"
                  )
                  let originGeometryReader = gp.frame(in: .named("geometry reader")).origin
                  Text(
                    "geometry reader: \(originGeometryReader.x), \(originGeometryReader.y)"
                  )
                  let originPurple = gp.frame(in: .named("purple")).origin
                  Text(
                    "purple: \(originPurple.x), \(originPurple.y)"
                  )
                  let originPink = gp.frame(in: .named("pink")).origin
                  Text(
                    "pink: \(originPink.x), \(originPink.y)"
                  )
                  let originText = gp.frame(in: .named("text")).origin
                  Text(
                    "text: \(originText.x), \(originText.y)"
                  )
                  let originBlue = gp.frame(in: .named("blue")).origin
                  Text(
                    "blue: \(originBlue.x), \(originBlue.y)"
                  )
                  let originRandom = gp.frame(in: .named(UUID())).origin
                  Text(
                    "random: \(originRandom.x), \(originRandom.y)"
                  )
                }
                .coordinateSpace(name: "text")
              }
              .coordinateSpace(name: "geometry reader")
            }
            .foregroundColor(.black)
            .background(Color.white)
            .frame(width: 300, height: 300)
            .coordinateSpace(name: "white")
          }
          .background(Color.purple)
          .coordinateSpace(name: "purple")
        }
        .background(Color.pink)
        .border(Color.gray, width: 1)
        .coordinateSpace(name: "pink")
      }
      .background(Color.pink)
      .coordinateSpace(name: "pink")
      // 右侧
      VStack {
        VStack {
          HStack {
            Rectangle().foregroundColor(.gray)
              .frame(width: 20, height: 20)
              .onDrag {
                NSItemProvider(object: "some string" as NSString)
              }
            Spacer()
          }
          Spacer()
        }
        HStack {
          ScrollView {
            VStack {
              Spacer()
                .frame(height: 20)
              Rectangle()
                .foregroundColor(.orange)
                .onDrop(of: [.text], delegate: D(target: "orange"))
              Rectangle()
                .foregroundColor(.green)
                .onDrop(of: [.text], delegate: D(target: "green"))
                .coordinateSpace(name: "green")
              Rectangle()
                .foregroundColor(.red)
                .coordinateSpace(name: "red")
                .onDrop(of: [.text], delegate: D(target: "red"))
            }
          }
          .frame(width: 300, height: 300)
          .background(Color.yellow)
          Spacer()
        }
      }
      .background(Color.blue)
      .coordinateSpace(name: "blue")
    }
    .frame(width: 800, height: 400)
    .statusBar(hidden: true)
  }
}

struct D: DropDelegate {
  let target: String
  func dropExited(info: DropInfo) {
    print("exiting \(target), location: \(info.location)")
  }
  func dropEntered(info: DropInfo) {
    print("entering \(target), location: \(info.location)")
  }
  func performDrop(info: DropInfo) -> Bool {
    print("performing \(target), location: \(info.location)")
    return true
  }
}

将上面的ContentView作为唯一View的app在11寸的iPad Pro 2020上看起来是这样的:

screenshot

看代码立刻就知道,白色方块里的那些字显示的是白色方块在不同的coordinate space中的位置。 前置知识(从coordinateSpace(named:)的文档中的举例总结出来的,算是有文档支撑吧):coordinate space中,一个View(甲)在另一个View(乙)的coordinate space中的frameoriginxy值 是甲的左上角的点在以乙的左上角为原点的坐标系中的x和y值,x为横坐标轴上的位置,此轴右为正;y为 纵坐标轴上的位置,此轴下为正。

首先是白色方块在"white" coordinate space,也就是它自己的coordinate space中的位置。 0,0。这恒河里。自己的左上角当然跟自己的左上角重合。

然后是"geometry reader"GeometryReader填满白色方块,0,0还是河里。

接下来是白色方块在"purple" coordinate space中的位置。100,0也恒河里。看看代码做点数学 就知道白色区块的左上角确实在紫色区块左上角的右侧100点(point)处。

然后事情就开始猎奇起来了。

在决定"pink" coordinate space中的位置时。pink看上去也不粉。两个pink之中,SwiftUI基于某种 原因选择了里面的一个,因而y坐标不为100。

当coordinate space变成"text"之后,我们会期待显示的数字对应白色方块相对于其中 包含文字的VStack的位置。这个VStack的左上角应该跟白色方块的左上角重合才对,但我们得到的却 不是0,0,看着反而像是相对于屏幕左上角位置。

接着是相对蓝色方块("blue")。我们期待-300,100。反正我读了文档后如此期待。结果却与上面一样。

然后,是相对于一个不存在的coordinate space。嗯。没有报错,没有输出,没有崩溃。默默地给出了 与上面一样的答案。

要是在写复杂的app时碰到这种程序不崩溃的情况人就该崩溃了。显然是有个默认行为但是文档又没写默认行为 是什么,什么时候发生。

这时候就只能靠自己研究其规律了。只有用上科学的方法:观察、假设、检验。

难怪有人说苹果是设计之神。设计出来的SwiftUI明明是人造物却需要用研究自然的方法来研究。

过程隐去,直接发表研究结果。

SwiftUI查找.namedcoordinate space的行为如下:从查找coordinate space的View(在这个场合中 是GeometryReader)或其亲View开始,确认其coordinate space是否符合用于查找的“name”。符合就直接使用。如果 不符合,就在其亲View中重复,依此循环。若始终找不到符合条件的,就使用以屏幕左上角为 原点的coordinate space。

我们看到的奇景均与这个行为相符。为什么要加个“或其亲View”呢?看到后面就清楚了。

这个猎奇的coordinate space也造就了猎奇的拖拽。拖拽中与coordinate space相关的是DropInfo.location。 我们还是先看看文档

The location of the drag in the coordinate space of the drop view.

谢谢苹果的谜语。

那么我们开拖。前述的app里灰色的方块可以拖拽,我们把它拖进橙色、绿色、红色的方块中看看打印出来的坐标。

首先是橙色。我们在橙色块上方加了一个Spacer。可以看出,虽然是橙色块调用的onDropDropDelegate使用 的coordinate space却并非以橙色左上角为原点。我们从橙色块上缘拖出拖进时,打印出的y坐标并非在0附近。 看来是使用了黄色区块,也就是ScrollView的coordinate space。

接着试试绿色。绿色区块与橙色区块的区别在于,在onDrop返回的View上调用了coordinateSpace(name:)。我们惊讶地 发现,这时DropDelegate使用的坐标系却与绿色区块相符了!我们再次观察假设检验,发现这这说明了SwiftUI另一猎奇的 行为:

coordinateSpace(name:)并不只是给一个View的coordinate space命名而已,它实际上同时“声明”了,这 个View的coordinate space“重要”。一部分API在查找应当使用的coordinate space时只会将“重要”的coordinate space纳入考虑。

这个说法不全对,不对的部分没有加粗。后面有取代不对的部分的更正确的说法。

再考虑橙色块中的行为,我们又看出:

ScrollView自带一个“重要”的coordinate space。

毕竟我们没有调用ScrollViewcoordinateSpace(name:)。有没有其它View也是如此呢?我就不知道了。

最后我们再试试红色块。红色块与绿色块的区别在于,调用onDropcoordinateSpace(name:)的顺序不一样。可以看出,红色 块的拖拽与橙色块的拖拽使用的coordinate space是一样的,都是ScrollView的coordinate space。我自然不知道SwiftUI的View Modifier都是如何实现的,但是从这里我们可以大胆猜想,

恐怕每个View Modifier返回的都是一个将其调用者作为子ViewView

并且

coordinateSpace(name:)并非为调用其的View的coordinate space命名并“声明”“重要”,而是返回一个有着有名字而“重要” 的coordinate space的,将其调用者作为唯一子View并且与其位置、大小相同的View。不由这个方法返回的View大多 没有“重要”的coordinate space,但也有例外,比如前述的ScrollView

SwiftUI在查找“重要”的coordinate space时的行为与它查找.named时的行为类似:从查找coordinate space的View或其 亲View开始,确认其coordinate space是否“重要”。若其不重要,则再在其亲View中查找,依此循环。

若始终找不到,怕还是会把屏幕左上角当原点吧,只是我懒得试了。

所以,对绿色区块的onDrop返回的View而言,coordinateSpace(name: "green")返回的View是其亲View,这个亲View有着 名为"green"的“重要”coordinate space,拖拽时要将其纳入考虑。而对红色区块的onDrop返回的View而言,有着“重要” 的"red"coordinate space的那个View是它的子View,根本不在查找coordinate space的路线之内。

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published