如何在 SwiftUI 中查看另一个视图的大小
How to make view the size of another view in SwiftUI
我正在尝试重新创建 Twitter iOS 应用程序的一部分以学习 SwiftUI,并且想知道如何将一个视图的宽度动态更改为另一个视图的宽度。在我的例子中,下划线的宽度与文本视图的宽度相同。
我附上了一张截图,试图更好地解释我所指的内容。任何帮助将不胜感激,谢谢!
这也是我目前的代码:
import SwiftUI
struct GridViewHeader : View {
@State var leftPadding: Length = 0.0
@State var underLineWidth: Length = 100
var body: some View {
return VStack {
HStack {
Text("Tweets")
.tapAction {
self.leftPadding = 0
}
Spacer()
Text("Tweets & Replies")
.tapAction {
self.leftPadding = 100
}
Spacer()
Text("Media")
.tapAction {
self.leftPadding = 200
}
Spacer()
Text("Likes")
}
.frame(height: 50)
.padding(.horizontal, 10)
HStack {
Rectangle()
.frame(width: self.underLineWidth, height: 2, alignment: .bottom)
.padding(.leading, leftPadding)
.animation(.basic())
Spacer()
}
}
}
}
试一试:
import SwiftUI
var titles = ["Tweets", "Tweets & Replies", "Media", "Likes"]
struct GridViewHeader : View {
@State var selectedItem: String = "Tweets"
var body: some View {
HStack(spacing: 20) {
ForEach(titles.identified(by: \.self)) { title in
HeaderTabButton(title: title, selectedItem: self.$selectedItem)
}
.frame(height: 50)
}.padding(.horizontal, 10)
}
}
struct HeaderTabButton : View {
var title: String
@Binding var selectedItem: String
var isSelected: Bool {
selectedItem == title
}
var body: some View {
VStack {
Button(action: { self.selectedItem = self.title }) {
Text(title).fixedSize(horizontal: true, vertical: false)
Rectangle()
.frame(height: 2, alignment: .bottom)
.relativeWidth(1)
.foregroundColor(isSelected ? Color.accentColor : Color.clear)
}
}
}
}
下面是它在预览中的样子:
关于使用GeometryReader、view preferences和anchor preferences我已经写了详细的解释。下面的代码使用了这些概念。有关它们如何工作的更多信息,请查看我发布的这篇文章:https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
下面的解决方案将正确地为下划线设置动画:
我努力完成这项工作,我同意你的看法。有时,您只需要能够向上或向下传递层次结构,一些框架信息。事实上,WWDC2019 session 237(使用 SwiftUI 构建自定义视图)解释了视图不断地传达它们的大小。它基本上说 Parent 向 child 提议大小,child 决定他们自己想要如何布局并与 parent 沟通。他们是怎么做到的?我怀疑 anchorPreference 与它有关。然而,它非常晦涩,根本没有记录在案。 API 已公开,但要了解那些长函数原型的工作原理......我现在没有时间去了解它。
我认为 Apple 没有记录这一点是为了迫使我们重新思考整个框架并忘记 "old" UIKit 习惯并开始以声明方式思考。但是,有时仍然需要这样做。你有没有想过背景修饰符是如何工作的?我很想看到那个实现。这会解释很多!我希望 Apple 能在不久的将来记录偏好。我一直在试验自定义 PreferenceKey,它看起来很有趣。
现在回到你的具体需求,我设法解决了。您需要两个维度(文本的 x 位置和宽度)。一个我得到它公平公正,另一个似乎有点黑客。尽管如此,它还是完美地工作。
我通过创建自定义水平对齐方式解决了文本的 x 位置。有关该检查的更多信息 session 237(在 19:00 分钟)。虽然我建议您观看整个过程,但它对布局过程的工作原理有很大帮助。
宽度,但是,我并不引以为豪... ;-) 它需要 DispatchQueue 来避免在显示时更新视图。 更新:我在下面的第二个实现中修复了它
第一次实施
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
struct GridViewHeader : View {
@State private var activeIdx: Int = 0
@State private var w: [CGFloat] = [0, 0, 0, 0]
var body: some View {
return VStack(alignment: .underlineLeading) {
HStack {
Text("Tweets").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 0))
Spacer()
Text("Tweets & Replies").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 1))
Spacer()
Text("Media").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 2))
Spacer()
Text("Likes").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 3))
}
.frame(height: 50)
.padding(.horizontal, 10)
Rectangle()
.alignmentGuide(.underlineLeading) { d in d[.leading] }
.frame(width: w[activeIdx], height: 2)
.animation(.linear)
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
@Binding var widths: [CGFloat]
let idx: Int
func body(content: Content) -> some View {
Group {
if activeIdx == idx {
content.alignmentGuide(.underlineLeading) { d in
DispatchQueue.main.async { self.widths[self.idx] = d.width }
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }
} else {
content.onTapGesture { self.activeIdx = self.idx }
}
}
}
}
更新:不使用 DispatchQueue 的更好实现
我的第一个解决方案可行,但我对将宽度传递给下划线视图的方式不太满意。
我找到了实现相同目标的更好方法。事实证明,background修饰符非常强大。它不仅仅是一个可以让您装饰视图背景的修饰符。
基本步骤是:
- 使用
Text("text").background(TextGeometry())
。 TextGeometry 是一个自定义视图,具有与文本视图相同大小的 parent。这就是 .background() 所做的。很强大。
- 在 TextGeometry 的实现中,我使用 GeometryReader 来获取 parent 的几何图形,这意味着我获取了文本视图的几何图形,这意味着我现在有宽度了。
- 现在要传回宽度,我正在使用 首选项。关于它们的文档为零,但经过一些实验后,我认为如果您愿意,首选项类似于 "view attributes"。我创建了我的自定义 PreferenceKey,称为 WidthPreferenceKey,我在 TextGeometry 中使用它来 "attach" 视图的宽度,所以它可以在层次结构中读取更高。
- 回到祖先,我使用onPreferenceChange来检测宽度的变化,并相应地设置宽度数组。
这听起来可能太复杂了,但代码最能说明问题。这是新的实现:
import SwiftUI
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
struct WidthPreferenceKey: PreferenceKey {
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
typealias Value = CGFloat
}
struct GridViewHeader : View {
@State private var activeIdx: Int = 0
@State private var w: [CGFloat] = [0, 0, 0, 0]
var body: some View {
return VStack(alignment: .underlineLeading) {
HStack {
Text("Tweets")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 0))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[0] = [=11=] })
Spacer()
Text("Tweets & Replies")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 1))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[1] = [=11=] })
Spacer()
Text("Media")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 2))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[2] = [=11=] })
Spacer()
Text("Likes")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 3))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[3] = [=11=] })
}
.frame(height: 50)
.padding(.horizontal, 10)
Rectangle()
.alignmentGuide(.underlineLeading) { d in d[.leading] }
.frame(width: w[activeIdx], height: 2)
.animation(.linear)
}
}
}
struct TextGeometry: View {
var body: some View {
GeometryReader { geometry in
return Rectangle().fill(Color.clear).preference(key: WidthPreferenceKey.self, value: geometry.size.width)
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
let idx: Int
func body(content: Content) -> some View {
Group {
if activeIdx == idx {
content.alignmentGuide(.underlineLeading) { d in
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }
} else {
content.onTapGesture { self.activeIdx = self.idx }
}
}
}
}
这是一个超级简单的解决方案,虽然它没有考虑到标签被拉伸到全宽 - 但这应该只是计算填充的次要附加数学。
import SwiftUI
struct HorizontalTabs: View {
private let tabsSpacing = CGFloat(16)
private func tabWidth(at index: Int) -> CGFloat {
let label = UILabel()
label.text = tabs[index]
let labelWidth = label.intrinsicContentSize.width
return labelWidth
}
private var leadingPadding: CGFloat {
var padding: CGFloat = 0
for i in 0..<tabs.count {
if i < selectedIndex {
padding += tabWidth(at: i) + tabsSpacing
}
}
return padding
}
let tabs: [String]
@State var selectedIndex: Int = 0
var body: some View {
VStack(alignment: .leading) {
HStack(spacing: tabsSpacing) {
ForEach(0..<tabs.count, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.tabs[index])
}
}
}
Rectangle()
.frame(width: tabWidth(at: selectedIndex), height: 3, alignment: .bottomLeading)
.foregroundColor(.blue)
.padding(.leading, leadingPadding)
.animation(Animation.spring())
}
}
}
HorizontalTabs(tabs: ["one", "two", "three"])
呈现为:
您只需要在框架内指定一个高度即可。这是一个例子:
VStack {
Text("First Text Label")
Spacer().frame(height: 50) // This line
Text("Second Text Label")
}
很精彩
但是现在变成编译错误了,改正了。 (Xcode11.1)
这是一个完整的代码。
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
struct WidthPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct HorizontalTabsView : View {
@State private var activeIdx: Int = 0
@State private var w: [CGFloat] = [0, 0, 0, 0]
var body: some View {
return VStack(alignment: .underlineLeading) {
HStack {
Text("Tweets")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 0))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[0] = [=10=] })
Spacer()
Text("Tweets & Replies")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 1))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[1] = [=10=] })
Spacer()
Text("Media")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 2))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[2] = [=10=] })
Spacer()
Text("Likes")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 3))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[3] = [=10=] })
}
.frame(height: 50)
.padding(.horizontal, 10)
Rectangle()
.alignmentGuide(.underlineLeading) { d in d[.leading] }
.frame(width: w[activeIdx], height: 2)
.animation(.default)
}
}
}
struct TextGeometry: View {
var body: some View {
GeometryReader { geometry in
return Rectangle()
.foregroundColor(.clear)
.preference(key: WidthPreferenceKey.self, value: geometry.size.width)
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
let idx: Int
func body(content: Content) -> some View {
Group {
if activeIdx == idx {
content.alignmentGuide(.underlineLeading) { d in
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }
} else {
content.onTapGesture { self.activeIdx = self.idx }
}
}
}
}
struct HorizontalTabsView_Previews: PreviewProvider {
static var previews: some View {
HorizontalTabsView()
}
}
让我谦虚地建议对 稍作修改:
不使用首选项的版本:
import SwiftUI
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
struct GridViewHeader : View {
@State private var activeIdx: Int = 0
@State private var w: [CGFloat] = [0, 0, 0, 0]
var body: some View {
return VStack(alignment: .underlineLeading) {
HStack {
Text("Tweets").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 0))
Spacer()
Text("Tweets & Replies").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 1))
Spacer()
Text("Media").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 2))
Spacer()
Text("Likes").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 3))
}
.frame(height: 50)
.padding(.horizontal, 10)
Rectangle()
.alignmentGuide(.underlineLeading) { d in d[.leading] }
.frame(width: w[activeIdx], height: 2)
.animation(.linear)
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
@Binding var widths: [CGFloat]
let idx: Int
func body(content: Content) -> some View {
var w: CGFloat = 0
return Group {
if activeIdx == idx {
content.alignmentGuide(.underlineLeading) { d in
w = d.width
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }.onAppear(perform: {self.widths[self.idx] = w})
} else {
content.onTapGesture { self.activeIdx = self.idx }
}
}
}
}
版本使用首选项和 GeometryReader
:
import SwiftUI
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
struct WidthPreferenceKey: PreferenceKey {
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
typealias Value = CGFloat
}
struct GridViewHeader : View {
@State private var activeIdx: Int = 0
@State private var w: [CGFloat] = [0, 0, 0, 0]
var body: some View {
return VStack(alignment: .underlineLeading) {
HStack {
Text("Tweets")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 0, widthStorage: $w))
Spacer()
Text("Tweets & Replies")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 1, widthStorage: $w))
Spacer()
Text("Media")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 2, widthStorage: $w))
Spacer()
Text("Likes")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 3, widthStorage: $w))
}
.frame(height: 50)
.padding(.horizontal, 10)
Rectangle()
.frame(width: w[activeIdx], height: 2)
.animation(.linear)
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
let idx: Int
@Binding var widthStorage: [CGFloat]
func body(content: Content) -> some View {
Group {
if activeIdx == idx {
content.background(GeometryReader { geometry in
return Color.clear.preference(key: WidthPreferenceKey.self, value: geometry.size.width)
})
.alignmentGuide(.underlineLeading) { d in
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.widthStorage[self.idx] = [=11=] })
} else {
content.onTapGesture { self.activeIdx = self.idx }.onPreferenceChange(WidthPreferenceKey.self, perform: { self.widthStorage[self.idx] = [=11=] })
}
}
}
}
首先,回答标题中的问题,如果你想让一个形状(视图)适合另一个视图的大小,你可以使用一个.overlay()
。 .overlay()
从它正在修改的视图中获取其大小。
为了在您的 Twitter 游戏中设置偏移量和宽度,您可以使用 GeometryReader
。 GeometryReader
有能力找到它的 .frame(in:)
另一个坐标 space.
可以使用.coordinateSpace(name:)
来识别参考坐标space。
struct ContentView: View {
@State private var offset: CGFloat = 0
@State private var width: CGFloat = 0
var body: some View {
HStack {
Text("Tweets")
.overlay(MoveUnderlineButton(offset: $offset, width: $width))
Text("Tweets & Replies")
.overlay(MoveUnderlineButton(offset: $offset, width: $width))
Text("Media")
.overlay(MoveUnderlineButton(offset: $offset, width: $width))
Text("Likes")
.overlay(MoveUnderlineButton(offset: $offset, width: $width))
}
.coordinateSpace(name: "container")
.overlay(underline, alignment: .bottomLeading)
.animation(.spring())
}
var underline: some View {
Rectangle()
.frame(height: 2)
.frame(width: width)
.padding(.leading, offset)
}
struct MoveUnderlineButton: View {
@Binding var offset: CGFloat
@Binding var width: CGFloat
var body: some View {
GeometryReader { geometry in
Button(action: {
self.offset = geometry.frame(in: .named("container")).minX
self.width = geometry.size.width
}) {
Rectangle().foregroundColor(.clear)
}
}
}
}
}
underline
观点是 2 点高 Rectangle
,在 HStack
之上放置一个 .overlay()
。
underline
视图与 .bottomLeading
对齐,因此我们可以使用 @State
值以编程方式设置其 .padding(.leading, _)
。
- 下划线视图的
.frame(width:)
也使用 @State
值设置。
HStack
设置为 .coordinateSpace(name: "container")
这样我们就可以找到相对于此的按钮框架。
-
MoveUnderlineButton
使用 GeometryReader
找到它自己的 width
和 minX
以便为 underline
视图设置各自的值
MoveUnderlineButton
被设置为包含该按钮文本的 Text
视图的 .overlay()
,因此它的 GeometryReader
继承了 [=36] 的大小=] 视图。
我正在尝试重新创建 Twitter iOS 应用程序的一部分以学习 SwiftUI,并且想知道如何将一个视图的宽度动态更改为另一个视图的宽度。在我的例子中,下划线的宽度与文本视图的宽度相同。
我附上了一张截图,试图更好地解释我所指的内容。任何帮助将不胜感激,谢谢!
这也是我目前的代码:
import SwiftUI
struct GridViewHeader : View {
@State var leftPadding: Length = 0.0
@State var underLineWidth: Length = 100
var body: some View {
return VStack {
HStack {
Text("Tweets")
.tapAction {
self.leftPadding = 0
}
Spacer()
Text("Tweets & Replies")
.tapAction {
self.leftPadding = 100
}
Spacer()
Text("Media")
.tapAction {
self.leftPadding = 200
}
Spacer()
Text("Likes")
}
.frame(height: 50)
.padding(.horizontal, 10)
HStack {
Rectangle()
.frame(width: self.underLineWidth, height: 2, alignment: .bottom)
.padding(.leading, leftPadding)
.animation(.basic())
Spacer()
}
}
}
}
试一试:
import SwiftUI
var titles = ["Tweets", "Tweets & Replies", "Media", "Likes"]
struct GridViewHeader : View {
@State var selectedItem: String = "Tweets"
var body: some View {
HStack(spacing: 20) {
ForEach(titles.identified(by: \.self)) { title in
HeaderTabButton(title: title, selectedItem: self.$selectedItem)
}
.frame(height: 50)
}.padding(.horizontal, 10)
}
}
struct HeaderTabButton : View {
var title: String
@Binding var selectedItem: String
var isSelected: Bool {
selectedItem == title
}
var body: some View {
VStack {
Button(action: { self.selectedItem = self.title }) {
Text(title).fixedSize(horizontal: true, vertical: false)
Rectangle()
.frame(height: 2, alignment: .bottom)
.relativeWidth(1)
.foregroundColor(isSelected ? Color.accentColor : Color.clear)
}
}
}
}
下面是它在预览中的样子:
关于使用GeometryReader、view preferences和anchor preferences我已经写了详细的解释。下面的代码使用了这些概念。有关它们如何工作的更多信息,请查看我发布的这篇文章:https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
下面的解决方案将正确地为下划线设置动画:
我努力完成这项工作,我同意你的看法。有时,您只需要能够向上或向下传递层次结构,一些框架信息。事实上,WWDC2019 session 237(使用 SwiftUI 构建自定义视图)解释了视图不断地传达它们的大小。它基本上说 Parent 向 child 提议大小,child 决定他们自己想要如何布局并与 parent 沟通。他们是怎么做到的?我怀疑 anchorPreference 与它有关。然而,它非常晦涩,根本没有记录在案。 API 已公开,但要了解那些长函数原型的工作原理......我现在没有时间去了解它。
我认为 Apple 没有记录这一点是为了迫使我们重新思考整个框架并忘记 "old" UIKit 习惯并开始以声明方式思考。但是,有时仍然需要这样做。你有没有想过背景修饰符是如何工作的?我很想看到那个实现。这会解释很多!我希望 Apple 能在不久的将来记录偏好。我一直在试验自定义 PreferenceKey,它看起来很有趣。
现在回到你的具体需求,我设法解决了。您需要两个维度(文本的 x 位置和宽度)。一个我得到它公平公正,另一个似乎有点黑客。尽管如此,它还是完美地工作。
我通过创建自定义水平对齐方式解决了文本的 x 位置。有关该检查的更多信息 session 237(在 19:00 分钟)。虽然我建议您观看整个过程,但它对布局过程的工作原理有很大帮助。
宽度,但是,我并不引以为豪... ;-) 它需要 DispatchQueue 来避免在显示时更新视图。 更新:我在下面的第二个实现中修复了它
第一次实施
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
struct GridViewHeader : View {
@State private var activeIdx: Int = 0
@State private var w: [CGFloat] = [0, 0, 0, 0]
var body: some View {
return VStack(alignment: .underlineLeading) {
HStack {
Text("Tweets").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 0))
Spacer()
Text("Tweets & Replies").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 1))
Spacer()
Text("Media").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 2))
Spacer()
Text("Likes").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 3))
}
.frame(height: 50)
.padding(.horizontal, 10)
Rectangle()
.alignmentGuide(.underlineLeading) { d in d[.leading] }
.frame(width: w[activeIdx], height: 2)
.animation(.linear)
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
@Binding var widths: [CGFloat]
let idx: Int
func body(content: Content) -> some View {
Group {
if activeIdx == idx {
content.alignmentGuide(.underlineLeading) { d in
DispatchQueue.main.async { self.widths[self.idx] = d.width }
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }
} else {
content.onTapGesture { self.activeIdx = self.idx }
}
}
}
}
更新:不使用 DispatchQueue 的更好实现
我的第一个解决方案可行,但我对将宽度传递给下划线视图的方式不太满意。
我找到了实现相同目标的更好方法。事实证明,background修饰符非常强大。它不仅仅是一个可以让您装饰视图背景的修饰符。
基本步骤是:
- 使用
Text("text").background(TextGeometry())
。 TextGeometry 是一个自定义视图,具有与文本视图相同大小的 parent。这就是 .background() 所做的。很强大。 - 在 TextGeometry 的实现中,我使用 GeometryReader 来获取 parent 的几何图形,这意味着我获取了文本视图的几何图形,这意味着我现在有宽度了。
- 现在要传回宽度,我正在使用 首选项。关于它们的文档为零,但经过一些实验后,我认为如果您愿意,首选项类似于 "view attributes"。我创建了我的自定义 PreferenceKey,称为 WidthPreferenceKey,我在 TextGeometry 中使用它来 "attach" 视图的宽度,所以它可以在层次结构中读取更高。
- 回到祖先,我使用onPreferenceChange来检测宽度的变化,并相应地设置宽度数组。
这听起来可能太复杂了,但代码最能说明问题。这是新的实现:
import SwiftUI
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
struct WidthPreferenceKey: PreferenceKey {
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
typealias Value = CGFloat
}
struct GridViewHeader : View {
@State private var activeIdx: Int = 0
@State private var w: [CGFloat] = [0, 0, 0, 0]
var body: some View {
return VStack(alignment: .underlineLeading) {
HStack {
Text("Tweets")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 0))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[0] = [=11=] })
Spacer()
Text("Tweets & Replies")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 1))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[1] = [=11=] })
Spacer()
Text("Media")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 2))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[2] = [=11=] })
Spacer()
Text("Likes")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 3))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[3] = [=11=] })
}
.frame(height: 50)
.padding(.horizontal, 10)
Rectangle()
.alignmentGuide(.underlineLeading) { d in d[.leading] }
.frame(width: w[activeIdx], height: 2)
.animation(.linear)
}
}
}
struct TextGeometry: View {
var body: some View {
GeometryReader { geometry in
return Rectangle().fill(Color.clear).preference(key: WidthPreferenceKey.self, value: geometry.size.width)
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
let idx: Int
func body(content: Content) -> some View {
Group {
if activeIdx == idx {
content.alignmentGuide(.underlineLeading) { d in
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }
} else {
content.onTapGesture { self.activeIdx = self.idx }
}
}
}
}
这是一个超级简单的解决方案,虽然它没有考虑到标签被拉伸到全宽 - 但这应该只是计算填充的次要附加数学。
import SwiftUI
struct HorizontalTabs: View {
private let tabsSpacing = CGFloat(16)
private func tabWidth(at index: Int) -> CGFloat {
let label = UILabel()
label.text = tabs[index]
let labelWidth = label.intrinsicContentSize.width
return labelWidth
}
private var leadingPadding: CGFloat {
var padding: CGFloat = 0
for i in 0..<tabs.count {
if i < selectedIndex {
padding += tabWidth(at: i) + tabsSpacing
}
}
return padding
}
let tabs: [String]
@State var selectedIndex: Int = 0
var body: some View {
VStack(alignment: .leading) {
HStack(spacing: tabsSpacing) {
ForEach(0..<tabs.count, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.tabs[index])
}
}
}
Rectangle()
.frame(width: tabWidth(at: selectedIndex), height: 3, alignment: .bottomLeading)
.foregroundColor(.blue)
.padding(.leading, leadingPadding)
.animation(Animation.spring())
}
}
}
HorizontalTabs(tabs: ["one", "two", "three"])
呈现为:
您只需要在框架内指定一个高度即可。这是一个例子:
VStack {
Text("First Text Label")
Spacer().frame(height: 50) // This line
Text("Second Text Label")
}
但是现在变成编译错误了,改正了。 (Xcode11.1)
这是一个完整的代码。
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
struct WidthPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct HorizontalTabsView : View {
@State private var activeIdx: Int = 0
@State private var w: [CGFloat] = [0, 0, 0, 0]
var body: some View {
return VStack(alignment: .underlineLeading) {
HStack {
Text("Tweets")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 0))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[0] = [=10=] })
Spacer()
Text("Tweets & Replies")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 1))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[1] = [=10=] })
Spacer()
Text("Media")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 2))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[2] = [=10=] })
Spacer()
Text("Likes")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 3))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[3] = [=10=] })
}
.frame(height: 50)
.padding(.horizontal, 10)
Rectangle()
.alignmentGuide(.underlineLeading) { d in d[.leading] }
.frame(width: w[activeIdx], height: 2)
.animation(.default)
}
}
}
struct TextGeometry: View {
var body: some View {
GeometryReader { geometry in
return Rectangle()
.foregroundColor(.clear)
.preference(key: WidthPreferenceKey.self, value: geometry.size.width)
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
let idx: Int
func body(content: Content) -> some View {
Group {
if activeIdx == idx {
content.alignmentGuide(.underlineLeading) { d in
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }
} else {
content.onTapGesture { self.activeIdx = self.idx }
}
}
}
}
struct HorizontalTabsView_Previews: PreviewProvider {
static var previews: some View {
HorizontalTabsView()
}
}
让我谦虚地建议对
import SwiftUI
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
struct GridViewHeader : View {
@State private var activeIdx: Int = 0
@State private var w: [CGFloat] = [0, 0, 0, 0]
var body: some View {
return VStack(alignment: .underlineLeading) {
HStack {
Text("Tweets").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 0))
Spacer()
Text("Tweets & Replies").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 1))
Spacer()
Text("Media").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 2))
Spacer()
Text("Likes").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 3))
}
.frame(height: 50)
.padding(.horizontal, 10)
Rectangle()
.alignmentGuide(.underlineLeading) { d in d[.leading] }
.frame(width: w[activeIdx], height: 2)
.animation(.linear)
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
@Binding var widths: [CGFloat]
let idx: Int
func body(content: Content) -> some View {
var w: CGFloat = 0
return Group {
if activeIdx == idx {
content.alignmentGuide(.underlineLeading) { d in
w = d.width
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }.onAppear(perform: {self.widths[self.idx] = w})
} else {
content.onTapGesture { self.activeIdx = self.idx }
}
}
}
}
版本使用首选项和 GeometryReader
:
import SwiftUI
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
struct WidthPreferenceKey: PreferenceKey {
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
typealias Value = CGFloat
}
struct GridViewHeader : View {
@State private var activeIdx: Int = 0
@State private var w: [CGFloat] = [0, 0, 0, 0]
var body: some View {
return VStack(alignment: .underlineLeading) {
HStack {
Text("Tweets")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 0, widthStorage: $w))
Spacer()
Text("Tweets & Replies")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 1, widthStorage: $w))
Spacer()
Text("Media")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 2, widthStorage: $w))
Spacer()
Text("Likes")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 3, widthStorage: $w))
}
.frame(height: 50)
.padding(.horizontal, 10)
Rectangle()
.frame(width: w[activeIdx], height: 2)
.animation(.linear)
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
let idx: Int
@Binding var widthStorage: [CGFloat]
func body(content: Content) -> some View {
Group {
if activeIdx == idx {
content.background(GeometryReader { geometry in
return Color.clear.preference(key: WidthPreferenceKey.self, value: geometry.size.width)
})
.alignmentGuide(.underlineLeading) { d in
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.widthStorage[self.idx] = [=11=] })
} else {
content.onTapGesture { self.activeIdx = self.idx }.onPreferenceChange(WidthPreferenceKey.self, perform: { self.widthStorage[self.idx] = [=11=] })
}
}
}
}
首先,回答标题中的问题,如果你想让一个形状(视图)适合另一个视图的大小,你可以使用一个.overlay()
。 .overlay()
从它正在修改的视图中获取其大小。
为了在您的 Twitter 游戏中设置偏移量和宽度,您可以使用 GeometryReader
。 GeometryReader
有能力找到它的 .frame(in:)
另一个坐标 space.
可以使用.coordinateSpace(name:)
来识别参考坐标space。
struct ContentView: View {
@State private var offset: CGFloat = 0
@State private var width: CGFloat = 0
var body: some View {
HStack {
Text("Tweets")
.overlay(MoveUnderlineButton(offset: $offset, width: $width))
Text("Tweets & Replies")
.overlay(MoveUnderlineButton(offset: $offset, width: $width))
Text("Media")
.overlay(MoveUnderlineButton(offset: $offset, width: $width))
Text("Likes")
.overlay(MoveUnderlineButton(offset: $offset, width: $width))
}
.coordinateSpace(name: "container")
.overlay(underline, alignment: .bottomLeading)
.animation(.spring())
}
var underline: some View {
Rectangle()
.frame(height: 2)
.frame(width: width)
.padding(.leading, offset)
}
struct MoveUnderlineButton: View {
@Binding var offset: CGFloat
@Binding var width: CGFloat
var body: some View {
GeometryReader { geometry in
Button(action: {
self.offset = geometry.frame(in: .named("container")).minX
self.width = geometry.size.width
}) {
Rectangle().foregroundColor(.clear)
}
}
}
}
}
underline
观点是 2 点高Rectangle
,在HStack
之上放置一个.overlay()
。underline
视图与.bottomLeading
对齐,因此我们可以使用@State
值以编程方式设置其.padding(.leading, _)
。- 下划线视图的
.frame(width:)
也使用@State
值设置。 HStack
设置为.coordinateSpace(name: "container")
这样我们就可以找到相对于此的按钮框架。-
MoveUnderlineButton
使用GeometryReader
找到它自己的width
和minX
以便为underline
视图设置各自的值 MoveUnderlineButton
被设置为包含该按钮文本的Text
视图的.overlay()
,因此它的GeometryReader
继承了 [=36] 的大小=] 视图。