如何在SwiftUI中使视图的大小成为另一个视图

时间:2019-06-08 09:17:06

标签: ios 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()
            }
        }
    }
}

7 个答案:

答案 0 :(得分:18)

首先,要回答标题中的问题,如果要使形状(视图)适合另一个视图的大小,可以使用.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,放在.overlay()上方的HStack中。
  2. underline视图与.bottomLeading对齐,因此我们可以使用.padding(.leading, _)值以编程方式设置其@State
  3. 下划线视图的.frame(width:)也是使用@State值设置的。
  4. HStack被设置为.coordinateSpace(name: "container"),因此我们可以找到与此相对的按钮框架。
  5. MoveUnderlineButton使用GeometryReader查找自己的widthminX以便为underline视图设置各自的值
  6. MoveUnderlineButton设置为包含该按钮文本的.overlay()视图的Text,以便其GeometryReader从该Text继承其大小视图。

Segmented with Underbar in action

答案 1 :(得分:6)

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

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

enter image description here

我努力做到这一点,我同意你的看法。有时,您只需要能够向上或向下传递层次结构以及一些框架信息。实际上,WWDC2019会议237(使用SwiftUI构建自定义视图)说明了视图会不断传达其大小。它基本上说父母建议孩子的大小,孩子决定他们要如何布局自己并与父母沟通。他们是如何做到的?我怀疑anchorPreference与它有关。但是,它非常晦涩,尚未完全记录在案。该API公开了,但是掌握了这些长函数原型的工作原理……这真是太糟糕了,我现在没有时间。

我认为Apple对此文件未作记录,迫使我们重新考虑整个框架,而忘记了“旧的” UIKit习惯,并开始进行声明式思考。但是,有时仍然需要这样做。您是否想知道背景修饰符如何工作?我很乐意看到该实现。这会解释很多!我希望苹果公司能在不久的将来记录下偏好设置。我一直在尝试自定义PreferenceKey,它看起来很有趣。

现在回到您的特定需求,我设法解决了。您需要两个尺寸(文本的x位置和宽度)。一个让我公平公正,另一个似乎有点骇人听闻。尽管如此,它仍然可以正常工作。

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

宽度,但是,我并不为此感到骄傲... ;-)它需要DispatchQueue避免在显示时更新视图。 更新:我在下面的第二个实现中对其进行了修复

首次实施

import SwiftUI

extension HorizontalAlignment {
    private enum UnderlineLeading: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> Length {
            return d[.leading]
        }
    }

    static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}


struct GridViewHeader : View {

    @State private var activeIdx: Int = 0
    @State private var w: [Length] = [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(.basic())
        }
    }
}

struct MagicStuff: ViewModifier {
    @Binding var activeIdx: Int
    @Binding var widths: [Length]
    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]
                }.tapAction { self.activeIdx = self.idx }

            } else {
                content.tapAction { self.activeIdx = self.idx }
            }
        }
    }
}

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

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

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

基本步骤是:

  1. 使用Text("text").background(TextGeometry())。 TextGeometry是一个自定义视图,其父视图的大小与文本视图相同。那就是.background()所做的。非常强大。
  2. 在实现 TextGeometry 的过程中,我使用GeometryReader来获取父级的几何,这意味着,我将获得Text视图的几何,这意味着我现在具有宽度。
  3. 现在要将宽度传回,我正在使用首选项。关于它们的文档为零,但是经过一些试验,如果您愿意,我认为首选项类似于“视图属性”。我创建了名为 WidthPreferenceKey 的自定义 PreferenceKey ,并在TextGeometry中使用它来将宽度“附加”到视图,以便可以在层次结构中更高地阅读。
  4. 回到祖先,我使用 onPreferenceChange 来检测宽度的变化,并相应地设置widths数组。

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

import SwiftUI

extension HorizontalAlignment {
    private enum UnderlineLeading: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> Length {
            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: [Length] = [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(.basic())
        }
    }
}

struct TextGeometry: View {
    var body: some View {
        GeometryReader { geometry in
            return Rectangle().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]
                }.tapAction { self.activeIdx = self.idx }

            } else {
                content.tapAction { self.activeIdx = self.idx }
            }
        }
    }
}

答案 2 :(得分:3)

让我谦虚地建议对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 })
            }
        }
    }
}

答案 3 :(得分:1)

尝试一下:

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)

            }
        }
    }
}

这是预览中的样子: Preview screen

答案 4 :(得分: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"])呈现以下内容:

screenshot

答案 5 :(得分:0)

您只需要指定一个框架,框架内的高度即可。这是一个示例:

VStack {
    Text("First Text Label")

    Spacer().frame(height: 50)    // This line

    Text("Second Text Label")
}

答案 6 :(得分:0)

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