SwiftUI 自定义对齐方式推动视图比父视图更宽

Posted

技术标签:

【中文标题】SwiftUI 自定义对齐方式推动视图比父视图更宽【英文标题】:SwiftUI custom alignment pushes views wider than parent 【发布时间】:2020-01-31 21:16:15 【问题描述】:

我从这个开始。一些带有两个滑块的 UI。

由此代码定义:

struct ContentView: View 
    @State private var num1: Double = 0.5
    @State private var num2: Double = 0.5
    let blueGreen = Color(red: 0.2, green: 0.6, blue: 0.6)
    var body: some View 
        VStack 
            Circle().fill(blueGreen).border(Color.blue, width: 1.0).padding(4.0)
            VStack 
                HStack 
                    Text("Value:")
                    Slider(value: $num1, in: 0...1)
                
                HStack 
                    Text("Opacity:")
                    Slider(value: $num2, in: 0...1)
                
            
            Spacer()
        .border(Color.green, width: 1.0).padding()
    

我希望“值:”和“不透明度:”标签在其后沿对齐,使滑块对齐且宽度相同。我听说自定义对齐可以对齐这样的视图,不是兄弟的视图。

所以我添加了自定义对齐方式:

extension HorizontalAlignment 
    private enum MyAlignment : AlignmentID 
        static func defaultValue(in d: ViewDimensions) -> CGFloat 
            return d[.trailing]
        
    
    static let myAlignment = HorizontalAlignment(MyAlignment.self)


struct ContentView: View 
    @State private var num1: Double = 0.5
    @State private var num2: Double = 0.5
    let blueGreen = Color(red: 0.2, green: 0.6, blue: 0.6)
    var body: some View 
        VStack 
            Circle().fill(blueGreen).border(Color.blue, width: 1.0).padding(4.0)
            VStack(alignment: .myAlignment) 
                HStack 
                    Text("Value:").alignmentGuide(.myAlignment)  d in d[.trailing] 
                    Slider(value: $num1, in: 0...1)
                
                HStack 
                    Text("Opacity:").alignmentGuide(.myAlignment)  d in d[.trailing] 
                    Slider(value: $num2, in: 0...1)
                
            
            Spacer()
        .border(Color.green, width: 1.0).padding()
    

它部分有效。标签的后沿已对齐,但现在 UI 已部分移出屏幕右侧。

使用 UIKit 和自动布局,我会将这两个标签的后沿限制为相等。这不会将任何东西推离屏幕(假设事情像往常一样被限制在屏幕边缘)。这里出了什么问题?以及如何通过对齐或更好的方式使用 SwiftUI 来实现这一点?

【问题讨论】:

【参考方案1】:

一种方法是使用首选项(了解更多here)。

struct ContentView: View 
    @State private var num1: Double = 0.5
    @State private var num2: Double = 0.5
    @State private var sliderWidth: CGFloat = 0

    private let spacing: CGFloat = 5
    let blueGreen = Color(red: 0.2, green: 0.6, blue: 0.6)

    var body: some View 
        VStack 
            Circle().fill(blueGreen).border(Color.blue, width: 1.0).padding(4.0)

            VStack(alignment: .trailing) 
                HStack(spacing: self.spacing) 
                    Text("Value:")
                        .fixedSize()
                        .anchorPreference(key: MyPrefKey.self, value: .bounds, transform:  [$0] )

                    Slider(value: $num1, in: 0...1)
                        .frame(width: sliderWidth)
                
                .frame(maxWidth: .infinity, alignment: .trailing)


                HStack(spacing: self.spacing) 
                    Text("Opacity:")
                        .fixedSize()
                        .anchorPreference(key: MyPrefKey.self, value: .bounds, transform:  [$0] )

                    Slider(value: $num2, in: 0...1)
                        .frame(width: sliderWidth)
                
                .frame(maxWidth: .infinity, alignment: .trailing)

            
            .backgroundPreferenceValue(MyPrefKey.self)  prefs -> GeometryReader<AnyView> in

               GeometryReader  proxy -> AnyView in
                    let vStackWidth = proxy.size.width

                    let maxAnchor = prefs.max 
                        return proxy[$0].size.width < proxy[$1].size.width

                    

                    DispatchQueue.main.async 
                        if let a = maxAnchor 
                            self.sliderWidth = vStackWidth - (proxy[a].size.width + self.spacing)

                        
                    

                    return AnyView(EmptyView())

                
            

            Spacer()

        .border(Color.green, width: 1.0).padding(20)
    



struct MyPrefKey: PreferenceKey 
    typealias Value = [Anchor<CGRect>]

    static var defaultValue: [Anchor<CGRect>] = []

    static func reduce(value: inout [Anchor<CGRect>], nextValue: () -> [Anchor<CGRect>]) 
        value.append(contentsOf: nextValue())
    

【讨论】:

谢谢!我对此进行了一些调整以设置标签宽度并将滑块排除在外,并将.backgroundPreferenceValue 块隐藏在可重复使用的.setWidth(MyKey.self, $widthBinding) 修饰符后面。我怀疑这不是他们想要使用“首选项”的方式,但您已经找到了一种解决 SwiftUI 当前缺点的好方法。 我选择设置滑块的宽度,因为根据标签的宽度设置标签的宽度可能会带来麻烦。如果 SwiftUI 将来更改它调用闭包和刷新视图的顺序,如果这会产生某种循环,我不会感到惊讶。 @kontiki 这可以通过将 PreferenceKey.Value 定义为字典来解决,其中键为每个视图(又名标签)唯一分配【参考方案2】:

好吧,我同意 Apple 似乎忘记在 SwiftUI 中使用/来自自动布局引擎给我们一些东西,比如内容拥抱优先级、抗压缩、大小相等等......

在 SwiftUI 中对齐指南不会更改视图大小,它们只是“按原样”移动视图以对齐到指定的锚点(如果容器的大小不受限制,则扩展容器,因为默认情况下它会紧贴内容,这就是您所看到的) .因此,要解决考虑的情况,需要以某种方式限制(或更改)大小。

下面的代码不是完整的解决方案(它没有考虑可能的l10n),但显示了方向 - 因为Slider没有默认大小,应该限制一些如何不扩展容器。

无论如何,我会考虑以下作为解决方法...(宽度的计算可能更准确和复杂,例如获取文本框架,例如通过锚首选项,然后考虑到填充从可用几何代理宽度中减去,但这仍然是相同的 - 明确指定视图框架宽度)。

struct HueSaturationView : View 
    @State private var num1: Double = 0.5
    @State private var num2: Double = 0.5
    let blueGreen = Color(red: 0.2, green: 0.6, blue: 0.6)
    var body: some View 
        GeometryReader  gp in
            VStack 
                Circle().fill(self.blueGreen).border(Color.blue, width: 1.0).padding(4.0)
                VStack(alignment: .myAlignment) 
                    HStack 
                        Text("Value:").alignmentGuide(.myAlignment)  d in d[.trailing] 
                        Slider(value: self.$num1, in: 0...1)
                            .frame(width: gp.size.width * 2/3)
                    
                    HStack 
                        Text("Opacity:").alignmentGuide(.myAlignment)  d in d[.trailing] 
                        Slider(value: self.$num2, in: 0...1)
                            .frame(width: gp.size.width * 2/3)
                    
                
                Spacer()
            
            .border(Color.green, width: 1.0).padding()
        
    

【讨论】:

感谢您的回答。您说:“在 SwiftUI 对齐指南中,它们不会更改视图大小,它们只是按原样移动视图以对齐到指定的锚点”。您是否在文档或 WWDC 谈话中找到了这个?或者你是基于你在使用 SwiftUI 时观察到的?因为从理论上讲,它可以满足对齐方式,就像约束一样,然后然后计算(或重新计算)视图大小。很难知道他们什么时候没有记录任何东西!

以上是关于SwiftUI 自定义对齐方式推动视图比父视图更宽的主要内容,如果未能解决你的问题,请参考以下文章

如何阻止 SwiftUI 视图变得比 superview 更宽

带有自定义 selectedBackgroundView 的 UITableViewCell,它比 UISplitViewController 上的主视图控制器更宽

如何使用 swiftUI 沿自定义路径移动视图/形状?

在 SwiftUI 中对齐包含的视图

在 SwiftUI 中显示键盘上方的视图 [关闭]

如何在 SwiftUI 中添加自定义容器视图