【问题标题】:How to make view the size of another view in SwiftUI如何在 SwiftUI 中查看另一个视图的大小
【发布时间】:2019-10-23 14:15:27
【问题描述】:

我正在尝试重新创建 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()
            }
        }
    }
}

【问题讨论】:

  • 这是一个重要的问题。而且我通常不知道如何在 SwiftUI 中获取所有内容的大小,或者例如 ScrollView 的 x 和 y。 :(
  • 认为您错过了 SwiftUI(以及整个声明式编程)的重要内容。虽然可能是错的。您是否考虑过让 each TextRectangle 成为自己的“自定义视图” - 无论是在代码中还是在布局中 - 并将下划线作为其中的一部分? (1) 将您的View 设计为有一个带下划线的文本——即使它需要一个 ZStack 和一个矩形。相信它不会有填充。 (2) 现在,如果需要,可以将此视图放入一个 带有 填充的矩形中。这是一个单一视图。 不要担心大小,担心层次结构。
  • @Sajad_Behesti,如果你需要 size 一些东西,那就去做吧。但是 - 如果你不这样做,让 SwiftUI 为它所在的设备做这件事。至于滚动视图,请使用List。对于你们俩,我建议观看两个(或更多)WWDC 会议——介绍 SwiftUI (developer.apple.com/videos/play/wwdc2019/204) 和 SwiftUI Essentials (developer.apple.com/videos/play/wwdc2019/216)。这是与 UIKit 不同的心态,但值得花时间。

标签: ios swiftui


【解决方案1】:

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

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

我努力完成这项工作,我同意你的看法。有时,您只需要能够向上或向下传递层次结构,一些框架信息。事实上,WWDC2019 session 237(Building Custom Views with SwiftUI)解释了视图不断地传达它们的大小。它基本上说父母向孩子提出尺寸,孩子决定他们想要如何布局自己并与父母交流。他们是怎么做到的?我怀疑anchorPreference 与它有关。但是,它非常晦涩难懂,根本没有记录。 API 公开了,但要掌握那些长长的函数原型是如何工作的……这真是我现在没有时间做的地狱。

我认为 Apple 没有记录这一点,迫使我们重新思考整个框架,忘记“旧”的 UIKit 习惯,开始以声明式的方式思考。但是,仍然有需要这样做的时候。你有没有想过背景修饰符是如何工作的?我很想看到这种实施。它会解释很多!我希望苹果能在不久的将来记录偏好。我一直在尝试自定义 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 的情况下更好地实现

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

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

基本步骤是:

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

【讨论】:

  • 很好的解决方案。我仍在努力理解alignmentGuide 的工作原理。就像在 WWDC2019 第 237 场会议中一样,我还没有了解 22:50 的幕后情况。当我们说 viewDimension[.bottom] 是一个长度(CGFloat)时,这个长度代表什么,视图的高度?
  • 这很令人困惑。似乎 .bottom 间接地是高度。如果 .top == 0 那么 .bottom 将反映高度。我不知道...由于文档如此之少,需要在不同的场景中尝试一些错误,才能得出一个有把握的结论。
  • 这是一个非常聪明的技巧,我会尝试应用它。但我想知道是否可以使用 两个 自定义对齐方式,一个在左侧,一个在右端,来获得宽度?
  • @Gusutafu 我已经尝试过了,但是使用双重自定义对齐方法失败了。让我知道你是否成功。顺便说一句,从那以后我想出了如何使用锚偏好,我认为这是要走的路。我正在写一篇文章,完成后将在此处发布链接以供进一步参考。
  • 嗨@bain 将代码更新为最新的 API。将长度替换为 CGFloat。 .basic 动画和 .linear,.onTapAction 和 .tagGesture。
【解决方案2】:

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

为了在您的 Twitter 游戏中设置偏移量和宽度,您可以使用 GeometryReaderGeometryReader 能够找到它的.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,在 HStack 顶部放置一个 .overlay()
  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 视图的大小。

【讨论】:

  • 要启用默认选择,MoveUnderlineButton 应该具有 isActive 属性,并且应该将 .onAppear { if self.isActive { self.width = geometry.size.width } } 添加到按钮
【解决方案3】:

试试这个:

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)

            }
        }
    }
}

这是预览中的样子:

【讨论】:

  • 您的解决方案没有动画。这是因为您的布局方式是将下划线创建为 4 个不同的视图。 OP 希望有一个可以滑动并调整大小的下划线。我还发布了我的答案,它使用单个视图作为下划线,并相应地移动和调整大小。
  • 嗯,他们没有在他们的问题中提到这一点,但我也不使用 Twitter,所以如果你这样做也许很明显!
  • 我也不是推特用户,这只是我的解释。 OP说:“动态改变一个视图的宽度”。他谈论“一个”观点,而不是4。因此,我的假设。不过我可能错了。
【解决方案4】:

让我谦虚地建议对this bright answer 稍作修改: 不使用首选项的版本:

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

【讨论】:

    【解决方案5】:

    这是一个超级简单的解决方案,虽然它不考虑标签被拉伸全宽 - 但这应该只是计算填充的少量额外数学。

    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"]) 呈现:

    【讨论】:

      【解决方案6】:

      您只需要指定一个包含高度的框架。这是一个例子:

      VStack {
          Text("First Text Label")
      
          Spacer().frame(height: 50)    // This line
      
          Text("Second Text Label")
      }
      

      【讨论】:

        【解决方案7】:

        This solution很精彩。

        但现在它变成了编译错误,它已更正。 (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()
            }
        }
        

        【讨论】:

        • 如果他们的代码有错误,请编辑他们的原始答案,而不是自己做答案。
        猜你喜欢
        • 2021-03-16
        • 2016-03-07
        • 2020-06-21
        • 1970-01-01
        • 1970-01-01
        • 2022-08-23
        • 2021-10-15
        • 1970-01-01
        • 2020-08-11
        相关资源
        最近更新 更多