[英]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 }
}
}
}
}
我的第一個解決方案有效,但我對將寬度傳遞給下划線視圖的方式並不太感到自豪。
我找到了一種更好的方法來實現同樣的目標。 事實證明,背景修改器非常強大。 它不僅僅是可以讓您裝飾視圖背景的修飾符。
基本步驟是:
Text("text").background(TextGeometry())
。 TextGeometry 是一個自定義視圖,它的父視圖與文本視圖的大小相同。 這就是 .background() 所做的。 很強大。這聽起來可能太復雜了,但代碼最好地說明了這一點。 這是新的實現:
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)
}
}
}
}
}
underline
視圖是有2點高Rectangle
,放在一個.overlay()
上的頂部HStack
。underline
視圖與.bottomLeading
對齊,因此我們可以使用@State
值以編程方式設置其.padding(.leading, _)
。.frame(width:)
也使用@State
值設置。HStack
被設置為.coordinateSpace(name: "container")
所以我們可以找到相對於它的按鈕的框架。MoveUnderlineButton
使用GeometryReader
找到它自己的width
和minX
,以便為underline
視圖設置相應的值MoveUnderlineButton
被設置為包含該按鈕文本的Text
視圖的.overlay()
,以便其GeometryReader
從該Text
視圖繼承其大小。試試這個:
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.