VStack 中的 SwiftUI 切换未对齐
SwiftUI Toggle in a VStack is misaligned
我有一个简单的 List
,其中每一行都是 Toggle
,其 Text
和 Text
作为副标题都在 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 中切换自我
我有一个简单的 List
,其中每一行都是 Toggle
,其 Text
和 Text
作为副标题都在 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 中切换自我