VStack 中的 SwiftUI 切换未对齐

SwiftUI Toggle in a VStack is misaligned

我有一个简单的 List,其中每一行都是 Toggle,其 TextText 作为副标题都在 VStack 中。一切正常,直到我开始显示或隐藏一些行。 Toggle 视图的开关以某种方式错位并放置在其标题上。这只发生在设备上,而不是在模拟器上 运行 时。

设备上的 XCode 13.3 和 13.4 beta 运行 iOS 13.3.1

都会发生这种情况

完整的例子是

import SwiftUI

struct ContentView: View {

    @State var showDetails = false
    @State var firstToggle = false
    @State var secondToggle = false
    var body: some View {
        NavigationView {
            Form {
                ToggleSubtitleRow(title: "Show Advanced",
                                     text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, se",
                                     isOn: $showDetails)

                if showDetails {
                    ToggleSubtitleRow(title: "Lorem ipsum dolor sit amet",
                                         text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam",
                                         isOn: $firstToggle)

                    ToggleSubtitleRow(title: "Lorem ipsum dolor sit amet",
                                         text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam",
                                         isOn: $secondToggle)
                }

            }
            .listStyle(GroupedListStyle())
            .navigationBarTitle("Settings", displayMode: .inline)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

public struct ToggleSubtitleRow: View {
    let title: String
    let text: String
    @Binding var isOn: Bool

    public init(title: String, text: String,
                isOn: Binding<Bool>) {
        self.text = text
        self.title = title
        self._isOn = isOn

    }

    public var body: some View {
        VStack(alignment: .leading) {
            Toggle(isOn: $isOn) {
                Text(title)
            }
            Text(text)
                .fixedSize(horizontal: false, vertical: true)
                .foregroundColor(Color(.secondaryLabel))
                .frame(alignment: .leading)
        }
        .foregroundColor(Color(.label))
    }
}

这应该有效

import SwiftUI

struct ContentView: View {

    @State var showDetails = false
    @State var firstToggle = false
    @State var secondToggle = false
    var body: some View {
        NavigationView {
            Form {
                ToggleSubtitleRow(title: "Show Advanced",
                                     text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, se",
                                     isOn: $showDetails)

                if showDetails {
                    ToggleSubtitleRow(title: "Lorem ipsum dolor sit amet",
                                         text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam",
                        isOn: $firstToggle).id(UUID())

                    ToggleSubtitleRow(title: "Lorem ipsum dolor sit amet",
                                         text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam",
                                         isOn: $secondToggle)
                }

            }
            .listStyle(GroupedListStyle())
            .navigationBarTitle("Settings", displayMode: .inline)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

public struct ToggleSubtitleRow: View {
    let title: String
    let text: String
    @Binding var isOn: Bool

    public var body: some View {
        VStack(alignment: .leading) {
            Toggle(isOn: $isOn) {
                Text(title)
            }
            Text(text)
                .fixedSize(horizontal: false, vertical: true)
                .foregroundColor(Color(.secondaryLabel))
                .frame(alignment: .leading)
        }
        .foregroundColor(Color(.label))
    }
}

我把它放在这里是为了表明,如果至少有一个元素需要重新创建,整个条件部分将被重新创建。

为了解释,稍微更改一下代码(不带任何 .id 修饰符)

if showDetails {
    Text("\(showDetails.description)")
    ToggleSubtitleRow(title: "Lorem ipsum dolor sit amet",
                                  text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam",
                                  isOn: $firstToggle)

     ToggleSubtitleRow(title: "Lorem ipsum dolor sit amet",
                                  text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam",
                                  isOn: $secondToggle)
 }

有效,"as expected",因为 SwiftUI 识别的条件部分 "something" 已更改。

Text("\(showDetails.description)")

效果相同

.id修饰符呢?为什么有效?

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension View {

    /// Returns a view whose identity is explicitly bound to the proxy
    /// value `id`. When `id` changes the identity of the view (for
    /// example, its state) is reset.
    @inlinable public func id<ID>(_ id: ID) -> some View where ID : Hashable

}

基于书面

ToggleSubtitleRow(title: "Lorem ipsum dolor sit amet",
                                  text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam",
                    isOn: $firstToggle).id(showDetails)

同样有效!

让我们以这种方式重新排列代码

struct ContentView: View {

    @State var showDetails = false
    @State var firstToggle = false
    @State var secondToggle = false
    var body: some View {
        let g = Group {
            if showDetails {
                ToggleSubtitleRow(title: "Lorem ipsum dolor sit amet",
                                  text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam",
                                  isOn: $firstToggle).id(UUID())

                ToggleSubtitleRow(title: "Lorem ipsum dolor sit amet",
                                  text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam",
                                  isOn: $secondToggle)
            }
        }
        let f = Form {
            ToggleSubtitleRow(title: "Show Advanced",
                                 text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, se",
                                 isOn: $showDetails)
            g

        }
        .listStyle(GroupedListStyle())
        .navigationBarTitle("Settings", displayMode: .inline)
        let v = NavigationView {
            f
        }
        return v
    }
}

并检查 g

的类型
let g: Group<TupleView<(some View, ToggleSubtitleRow)>?>

我们可以看到 SwiftUI 如何处理我们的 "conditional"。事实上

TupleView<(some View, ToggleSubtitleRow)>?

根据讨论更新,在多个 ToggleSubtitleRow 上应用 .id 修饰符根本不起作用

最好的选择,解决这个bug的方法是重新定义

public struct ToggleSubtitleRow: View {
    let title: String
    let text: String
    @Binding var isOn: Bool

    public var body: some View {
        VStack(alignment: .leading) {
            Toggle(isOn: $isOn) {
                Text(title)
            }.id(UUID())
            Text(text)
                .fixedSize(horizontal: false, vertical: true)
                .foregroundColor(Color(.secondaryLabel))
                .frame(alignment: .leading)
        }
        .foregroundColor(Color(.label))
    }
}

不修改 ContentView 中的任何内容,而是直接在 ToggleSubtitleRow 中切换自我