繁体   English   中英

如何在 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()
            }
        }
    }
}

我已经写了关于使用 GeometryReader、视图首选项和锚点首选项的详细说明。 下面的代码使用了这些概念。 有关它们如何工作的更多信息,请查看我发布的这篇文章: https : //swiftui-lab.com/communicating-with-the-view-tree-part-1/

下面的解决方案将正确地为下划线设置动画:

在此处输入图片说明

我努力完成这项工作,我同意你的看法。 有时,您只需要能够向上或向下传递层次结构,一些框架信息。 事实上,WWDC2019 会议 237(使用 SwiftUI 构建自定义视图)解释了视图会持续传达其大小。 它基本上是说父母向孩子提出尺寸,孩子决定他们想要如何布置自己并与父母沟通。 他们是怎么做到的? 我怀疑 anchorPreference 与它有关。 然而,它非常晦涩,还没有完全记录下来。 API 是公开的,但要掌握那些长函数原型的工作原理……我现在没有时间做这件事。

我认为 Apple 没有将其记录在案,以迫使我们重新思考整个框架并忘记“旧”的 UIKit 习惯并开始声明式思考。 但是,有时仍然需要这样做。 你有没有想过背景修改器是如何工作的? 我很想看到这个实现。 它会解释很多! 我希望 Apple 能够在不久的将来记录偏好。 我一直在尝试自定义 PreferenceKey,它看起来很有趣。

现在回到您的特定需求,我设法解决了。 您需要两个维度(文本的 x 位置和宽度)。 一个我认为公平公正,另一个似乎有点黑客。 尽管如此,它还是完美的。

我通过创建自定义水平对齐方式解决了文本的 x 位置。 有关该检查会话 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 的情况下更好地实现

我的第一个解决方案有效,但我对将宽度传递给下划线视图的方式并不太感到自豪。

我找到了一种更好的方法来实现同样的目标。 事实证明,背景修改器非常强大。 它不仅仅是可以让您装饰视图背景的修饰符。

基本步骤是:

  1. 使用Text("text").background(TextGeometry()) TextGeometry 是一个自定义视图,它的父视图与文本视图的大小相同。 这就是 .background() 所做的。 很强大。
  2. 在我的TextGeometry实现中,我使用 GeometryReader 来获取父级的几何图形,这意味着我获取了 Text 视图的几何图形,这意味着我现在有了宽度。
  3. 现在将宽度传递回来,我正在使用Preferences 关于它们的文档为零,但经过一些实验,我认为偏好类似于“查看属性”,如果您愿意的话。 我创建了我的自定义PreferenceKey ,称为WidthPreferenceKey ,我在 TextGeometry 中使用它来将宽度“附加”到视图,因此可以在层次结构中读取更高的位置。
  4. 回到祖先中,我使用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] = $0 })

                Spacer()

                Text("Tweets & Replies")
                    .modifier(MagicStuff(activeIdx: $activeIdx, idx: 1))
                    .background(TextGeometry())
                    .onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[1] = $0 })

                Spacer()

                Text("Media")
                    .modifier(MagicStuff(activeIdx: $activeIdx, idx: 2))
                    .background(TextGeometry())
                    .onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[2] = $0 })

                Spacer()

                Text("Likes")
                    .modifier(MagicStuff(activeIdx: $activeIdx, idx: 3))
                    .background(TextGeometry())
                    .onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[3] = $0 })

                }
                .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 }
            }
        }
    }
}

首先,回答标题中的问题,如果你想让一个形状(视图)适合另一个视图的大小,你可以使用.overlay() .overlay()从它正在修改的视图中获得其大小。

为了在您的 Twitter 娱乐中设置偏移量和宽度,您可以使用GeometryReader GeometryReader能够找到它的.frame(in:)另一个坐标空间。

您可以使用.coordinateSpace(name:)来标识参考坐标空间。

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)
                }
            }
        }
    }
}
  1. underline视图是有2点高Rectangle ,放在一个.overlay()上的顶部HStack
  2. underline视图与.bottomLeading对齐,因此我们可以使用@State值以编程方式设置其.padding(.leading, _)
  3. 下划线视图的.frame(width:)也使用@State值设置。
  4. HStack被设置为.coordinateSpace(name: "container")所以我们可以找到相对于它的按钮的框架。
  5. MoveUnderlineButton使用GeometryReader找到它自己的widthminX ,以便为underline视图设置相应的值
  6. MoveUnderlineButton被设置为包含该按钮文本的Text视图的.overlay() ,以便其GeometryReader从该Text视图继承其大小。

使用 Underbar 进行分段

试试这个:

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)

            }
        }
    }
}

这是预览中的样子: 预览画面

让我谦虚地建议稍微修改一下这个明亮的答案:不使用首选项的版本:

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] = $0 })


            } else {
                content.onTapGesture { self.activeIdx = self.idx }.onPreferenceChange(WidthPreferenceKey.self, perform: { self.widthStorage[self.idx] = $0 })
            }
        }
    }
}

这是一个超级简单的解决方案,虽然它没有考虑到选项卡被全宽拉伸 - 但这应该只是计算填充的次要附加数学。

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] = $0 })

                Spacer()

                Text("Tweets & Replies")
                    .modifier(MagicStuff(activeIdx: $activeIdx, idx: 1))
                    .background(TextGeometry())
                    .onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[1] = $0 })

                Spacer()

                Text("Media")
                    .modifier(MagicStuff(activeIdx: $activeIdx, idx: 2))
                    .background(TextGeometry())
                    .onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[2] = $0 })

                Spacer()

                Text("Likes")
                    .modifier(MagicStuff(activeIdx: $activeIdx, idx: 3))
                    .background(TextGeometry())
                    .onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[3] = $0 })

                }
                .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()
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM