如何在 SwiftUI 中查看另一个视图的大小 [英] How to make view the size of another view in SwiftUI

查看:21
本文介绍了如何在 SwiftUI 中查看另一个视图的大小的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试重新创建 Twitter iOS 应用程序的一部分以学习 SwiftUI,并且想知道如何将一个视图的宽度动态更改为另一个视图的宽度.就我而言,下划线的宽度与文本视图的宽度相同.

我附上了一张截图,试图更好地解释我所指的内容.任何帮助将不胜感激,谢谢!

还有我目前的代码:

导入 SwiftUI结构 GridViewHeader :查看 {@State var leftPadding:长度 = 0.0@State var underLineWidth: 长度 = 100var主体:一些视图{返回 VStack {堆栈{文本(推文").tapAction {self.leftPadding = 0}垫片()文本(推文和回复").tapAction {self.leftPadding = 100}垫片()文本(媒体").tapAction {self.leftPadding = 200}垫片()文本(喜欢")}.frame(高度:50).padding(.horizo​​ntal, 10)堆栈{长方形().frame(宽度:self.underLineWidth,高度:2,对齐方式:.bottom).padding(.leader, leftPadding).animation(.basic())垫片()}}}}

解决方案

我已经写了一篇关于使用 GeometryReader、视图首选项和锚点首选项的详细说明.下面的代码使用了这些概念.有关它们如何工作的更多信息,请查看我发布的这篇文章:

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

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

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

文本的 x 位置我通过创建自定义水平对齐方式解决了它.有关该检查会话 237(在 19:00 分钟)的更多信息.虽然我建议您观看整个过程,但它对布局过程的工作原理有很多了解.

宽度,然而,我并不那么自豪...... ;-) 它需要 DispatchQueue 来避免在显示时更新视图.更新:我在下面的第二个实现中修复了它

首次实现

extension Horizo​​ntalAlignment {私人枚举 UnderlineLeading:AlignmentID {static func defaultValue(in d: ViewDimensions) ->CGFloat {返回 d[.leading]}}静态让 underlineLeading = Horizo​​ntalAlignment(UnderlineLeading.self)}结构 GridViewHeader :查看 {@State 私有变量 activeIdx: Int = 0@State 私有变量 w: [CGFloat] = [0, 0, 0, 0]var主体:一些视图{返回 VStack(对齐:.underlineLeading){堆栈{文本(推文").modifier(MagicStuff(activeIdx:$activeIdx,宽度:$w,idx:0))垫片()Text("Tweets & Replies").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 1))垫片()文本(媒体").修改器(MagicStuff(activeIdx:$activeIdx,宽度:$w,idx:2))垫片()文本(喜欢").修改器(MagicStuff(activeIdx:$activeIdx,宽度:$w,idx:3))}.frame(高度:50).padding(.horizo​​ntal, 10)长方形().alignmentGuide(.underlineLeading) { d in d[.leading] }.frame(宽度:w[activeIdx],高度:2).动画(.线性)}}}struct MagicStuff:ViewModifier {@Binding var activeIdx: Int@Binding var 宽度:[CGFloat]让 idx: 整数func 正文(内容:内容)->一些视图{团体 {如果 activeIdx == idx {content.alignmentGuide(.underlineLeading) { d inDispatchQueue.main.async { self.widths[self.idx] = d.width }返回 d[.leading]}.onTapGesture { self.activeIdx = self.idx }} 别的 {content.onTapGesture { self.activeIdx = self.idx }}}}}

更新:无需使用 DispatchQueue 即可实现更好的实现

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

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

基本步骤是:

  1. 使用Text("text").background(TextGeometry()).TextGeometry 是一个自定义视图,它的父视图与文本视图的大小相同.这就是 .background() 所做的.非常强大.
  2. 在我的 TextGeometry 实现中,我使用 GeometryReader 来获取父视图的几何图形,这意味着我获取了 Text 视图的几何图形,这意味着我现在有了宽度.
  3. 现在为了将宽度传递回来,我使用了首选项.关于它们的文档为零,但经过一些实验后,我认为偏好类似于查看属性",如果您愿意的话.我创建了我的自定义 PreferenceKey,称为 WidthPreferenceKey,我在 TextGeometry 中使用它来将宽度附加"到视图,因此可以在层次结构中读取更高的宽度.莉>
  4. 回到祖先中,我使用 onPreferenceChange 来检测宽度的变化,并相应地设置宽度数组.

这听起来可能太复杂了,但代码最好地说明了这一点.这是新的实现:

import SwiftUI扩展水平对齐{私人枚举 UnderlineLeading:AlignmentID {static func defaultValue(in d: ViewDimensions) ->CGFloat {返回 d[.leading]}}静态让 underlineLeading = Horizo​​ntalAlignment(UnderlineLeading.self)}struct WidthPreferenceKey:PreferenceKey {静态变量 defaultValue = CGFloat(0)static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {值 = 下一个值()}类型别名值 = CGFloat}结构 GridViewHeader :查看 {@State 私有变量 activeIdx: Int = 0@State 私有变量 w: [CGFloat] = [0, 0, 0, 0]var主体:一些视图{返回 VStack(对齐:.underlineLeading){堆栈{文本(推文").modifier(MagicStuff(activeIdx: $activeIdx, idx: 0)).background(TextGeometry()).onPreferenceChange(WidthPreferenceKey.self, 执行: { self.w[0] = $0 })垫片()文本(推文和回复").modifier(MagicStuff(activeIdx: $activeIdx, idx: 1)).background(TextGeometry()).onPreferenceChange(WidthPreferenceKey.self, 执行: { self.w[1] = $0 })垫片()文本(媒体").modifier(MagicStuff(activeIdx: $activeIdx, idx: 2)).background(TextGeometry()).onPreferenceChange(WidthPreferenceKey.self, 执行: { self.w[2] = $0 })垫片()文本(喜欢").modifier(MagicStuff(activeIdx: $activeIdx, idx: 3)).background(TextGeometry()).onPreferenceChange(WidthPreferenceKey.self, 执行: { self.w[3] = $0 })}.frame(高度:50).padding(.horizo​​ntal, 10)长方形().alignmentGuide(.underlineLeading) { d in d[.leading] }.frame(宽度:w[activeIdx],高度:2).动画(.线性)}}}struct TextGeometry:查看{var主体:一些视图{GeometryReader { 几何输入return Rectangle().fill(Color.clear).preference(key: WidthPreferenceKey.self, value: geometry.size.width)}}}struct MagicStuff:ViewModifier {@Binding var activeIdx: Int让 idx: 整数func 正文(内容:内容)->一些视图{团体 {如果 activeIdx == idx {content.alignmentGuide(.underlineLeading) { d in返回 d[.leading]}.onTapGesture { self.activeIdx = self.idx }} 别的 {content.onTapGesture { self.activeIdx = self.idx }}}}}

I'm trying to recreate a portion of the Twitter iOS app to learn SwiftUI and am wondering how to dynamically change the width of one view to be the width of another view. In my case, to have the underline be the same width as the Text view.

I have attached a screenshot to try and better explain what I'm referring to. Any help would be greatly appreciated, thanks!

Also here is the code I have so far:

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

解决方案

I have written a detailed explanation about using GeometryReader, view preferences and anchor preferences. The code below uses those concepts. For further information on how they work, check this article I posted: https://swiftui-lab.com/communicating-with-the-view-tree-part-1/

The solution below, will properly animate the underline:

I struggled to make this work and I agree with you. Sometimes, you just need to be able to pass up or down the hierarchy, some framing information. In fact, the WWDC2019 session 237 (Building Custom Views with SwiftUI), explains that views communicate their sizing continuously. It basically says Parent proposes size to child, childen decide how they want to layout theirselves and communicate back to the parent. How they do that? I suspect the anchorPreference has something to do with it. However it is very obscure and not at all documented yet. The API is exposed, but grasping how those long function prototypes work... that's a hell I do not have time for right now.

I think Apple has left this undocumented to force us rethink the whole framework and forget about "old" UIKit habits and start thinking declaratively. However, there are still times when this is needed. Have you ever wonder how the background modifier works? I would love to see that implementation. It would explain a lot! I'm hoping Apple will document preferences in the near future. I have been experimenting with custom PreferenceKey and it looks interesting.

Now back to your specific need, I managed to work it out. There are two dimensions you need (the x position and width of the text). One I get it fair and square, the other seems a bit of a hack. Nevertheless, it works perfectly.

The x position of the text I solved it by creating a custom horizontal alignment. More information on that check session 237 (at minute 19:00). Although I recommend you watch the whole thing, it sheds a lot of light on how the layout process works.

The width, however, I'm not so proud of... ;-) It requires DispatchQueue to avoid updating the view while being displayed. UPDATE: I fixed it in the second implementation down below

First implementation

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

Update: Better implementation without using DispatchQueue

My first solution works, but I was not too proud of the way the width is passed to the underline view.

I found a better way of achieving the same thing. It turns out, the background modifier is very powerful. It is much more than a modifier that can let you decorate the background of a view.

The basic steps are:

  1. Use Text("text").background(TextGeometry()). TextGeometry is a custom view that has a parent with the same size as the text view. That is what .background() does. Very powerful.
  2. In my implementation of TextGeometry I use GeometryReader, to get the geometry of the parent, which means, I get the geometry of the Text view, which means I now have the width.
  3. Now to pass the width back, I am using Preferences. There's zero documentation about them, but after a little experimentation, I think preferences are something like "view attributes" if you like. I created my custom PreferenceKey, called WidthPreferenceKey and I use it in TextGeometry to "attach" the width to the view, so it can be read higher in the hierarchy.
  4. Back in the ancestor, I use onPreferenceChange to detect changes in the width, and set the widths array accordingly.

It may all sound too complex, but the code illustrates it best. Here's the new implementation:

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

这篇关于如何在 SwiftUI 中查看另一个视图的大小的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆