ios 如何在VStack中创建一个带有Overlay Highlight的SwiftUI视图

x4shl7ld  于 12个月前  发布在  iOS
关注(0)|答案(1)|浏览(118)

考虑以下简化的代码片段

import SwiftUI

struct ContentView: View {
  var body: some View {
    ScrollView {
      // Some other views here that should also be covered
      VStack {
        ForEach(0..<8) { i in
          ListElement()
            .padding(.horizontal)
        }
      }
    }
  }
  
}

struct ListElement: View {
  @State private var isFocused: Bool = false
  
  var body: some View {
    Button(action: {
      self.isFocused.toggle()
    }, label: {
      Text(isFocused ? "I am focused" : "I am not focused")
        .foregroundColor(.white)
        .frame(maxWidth: .infinity)
        .padding()
        .background(.gray)
        .background(
          isFocused ?
          Color.pink
            .opacity(0.5)
            .frame(width: 10000, height: 10000)
            .ignoresSafeArea()
            .onTapGesture {
              self.isFocused = false
            }
          : nil
        )
    })
  }
}

#Preview {
  ContentView()
}

其结果如下

通常ScrollView包含更多的ListElements。如果用户点击一个ListElement,我希望它调整大小并显示附加信息,这是一个简单的按钮。但是,我也希望除了ListElement之外的所有内容都有一个透明的覆盖层,以便更加强调所选的ListElement,而不强调其他内容。如果用户点击除了所选ListElement之外的任何内容(在这种情况下,是其背景),则相应的ListElement不应再被聚焦。有几个解决方案(如“滚动”类视图),但据我所知,由于依赖于GeometryReader,没有一个在ScrollViews中工作。
在示例代码中,我尝试在每个ListElement上使用一个大得离谱的background-修饰符来实现这一点,它将覆盖整个屏幕。它看起来像这样:

万岁!VStack在视觉上不再被强调,所选的ListElement清楚地处于焦点位置。但是,如果选择了除最后一个ListElement之外的任何ListElement,则会发生以下情况:

正如您所看到的,background-修饰符并没有覆盖任何在所选元素之后的ListElement。我假设这是因为SwiftUI中的视图层次结构。有没有什么方法可以将当前选中的ListElement推到层次结构的顶部,这样我就可以覆盖它后面的任何内容?

kmynzznz

kmynzznz1#

要回答你的问题,你必须将.zIndex(isFocused ? 1 : -1)修饰符添加到ListElement的Button中。但是你也必须将焦点状态存储在一个全局位置,因为-我猜-如果一个列表元素是焦点,我们应该从之前焦点的元素(如果有的话)中删除焦点。
因此,代码看起来像这样:

import SwiftUI
import Foundation

struct ContentView: View {

  @State var focusedIndex: Int?

  var body: some View {
    ScrollView {
      Text("Some other views here that should also be covered")
      VStack {
        ForEach(0..<8) { i in
          ListElement(
            isFocused: focusedIndex == i,
            action: { focusedIndex = i }
          )
          .padding(.horizontal)
        }
      }
    }
  }

}

struct ListElement: View {
  let isFocused: Bool
  let action: () -> Void

  var body: some View {
    Button(action: {
      action()
    }, label: {
      Text(isFocused ? "I am focused" : "I am not focused")
        .foregroundColor(.white)
        .frame(maxWidth: .infinity)
        .padding()
        .background(.gray)
        .background(
          isFocused ?
          Color.pink
            .opacity(0.5)
            .frame(width: 10000, height: 10000)
            .ignoresSafeArea()
          : nil
        )

    })
    .zIndex(isFocused ? 1 : -1)
  }
}

#Preview {
  ContentView()
}

虽然它的工作,我并不完全满意这个解决方案。我觉得它很笨拙,我没有100%的信心,它将在每一个场景中工作。
所以我认为一个更好的解决方案是在我们的全部内容上方画一个“封面”,并在特定位置剪一个洞,使屏幕的一部分可见。为了实现这一点,我们必须确定一个给定的元素在屏幕上的确切位置,然后我们必须绘制我们的封面,并使用“reversemask”在该位置切一个洞。这个解决方案有很多优点,比如它不仅可以处理列表元素,还可以处理屏幕上的任何类型的视图。我们也可以将其扩展到“切割”不是矩形,而是任何形状的孔。
此解决方案还有一个未解决的问题:如果你有很多元素,比如说你滚动你的内容,那么孔的位置将是不正确的。如果检查scrollview的contentOffset和subscrollview的offset.x与测量的CGRect的origin. x,就可以解决这个问题。我没有实现它,因为我的代码使用它会太复杂,但是您可以使用this作为帮助轻松地扩展此代码。

import SwiftUI

struct ContentView: View {

  @State var focusedRect: CGRect?
  @State var focusedIndex: Int?

  var body: some View {
    TutorialContent(
      highlightRect: focusedRect,
      action: {
        focusedRect = nil
        focusedIndex = nil
      },
      content: {
        ScrollView {
          VStack {
            VStack {
              Text("Another view which will be covered!")
            }
            ForEach(0 ..< 8) { i in
              let isFocused = i == focusedIndex
              ListElement(
                text: isFocused ? "I am focused" : "I am not focused",
                action: { rect in
                  focusedRect = rect
                  focusedIndex = i
                }
              )
              .padding(.horizontal)
            }
          }
        }
      }
    )
    .animation(.default, value: focusedIndex)
  }
}

struct ListElement: View {
  let text: String
  let action: (CGRect) -> Void

  @State var rect: CGRect = .zero

  var body: some View {
    Button(
      action: {
        action(rect)
      },
      label: {
        Text(text)
          .padding()
          .foregroundColor(.white)
          .frame(maxWidth: .infinity)
          .background(.gray)
      })
    .measure { rect = $0 }
  }
}

struct TutorialContent<Content: View>: View {
  let highlightRect: CGRect?
  let action: () -> Void
  let content: () -> Content

  var body: some View {
    ZStack {
      if let rect = highlightRect {
        ZStack {
          Color.pink.opacity(0.5)
          Rectangle()
            .frame(
              width: rect.width,
              height: rect.height
            )
            .position(CGPoint(x: rect.midX, y: rect.midY))
            .blendMode(.destinationOut)
        }
        .ignoresSafeArea(.all)
        .compositingGroup()
        .zIndex(highlightRect != nil ? 1 : -1)
        .onTapGesture {
          action()
        }
      }

      content()
    }
  }
}

extension View {
  @ViewBuilder func measure(
    in coordinateSpace: CoordinateSpace = .global,
    _ block: @escaping (CGRect) -> Void
  ) -> some View {
    MeasuredView(coordinateSpace: coordinateSpace, onMeasure: block, content: self)
  }
}

struct MeasuredView<Content: View>: View {
  let coordinateSpace: CoordinateSpace
  let onMeasure: (CGRect) -> Void
  let content: Content

  var body: some View {
    content
      .overlay {
        GeometryReader { proxy in
          Rectangle().fill(.clear)
            .task {
              onMeasure(proxy.frame(in: coordinateSpace))
            }
        }
      }
  }
}

#Preview {
  ContentView()
}

相关问题