SwiftUI 展开列表 One-At-A-Time,自动折叠
SwiftUI Expand Lists One-At-A-Time, Auto Collapse
我正在尝试构建一个包含多个折叠列表的视图,最初只显示 header。当您点击 header 时,它的列表应该展开。然后,随着第一个列表的展开,如果您点击另一个列表的 header,第一个列表应该会自动折叠,而第二个列表会展开。依此类推,一次只能看到一个列表。
下面的代码非常适合同时显示多个列表,它们都可以通过点击展开和折叠,但是我不知道如何在我点击展开时折叠已经打开的列表折叠列表。
这是代码(抱歉,有点长):
import SwiftUI
struct Task: Identifiable {
let id: String = UUID().uuidString
let title: String
let subtask: [Subtask]
}
struct Subtask: Identifiable {
let id: String = UUID().uuidString
let title: String
}
struct SubtaskCell: View {
let task: Subtask
var body: some View {
HStack {
Image(systemName: "circle")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
}
}
struct TaskCell: View {
var task: Task
@State private var isExpanded = false
var body: some View {
content
.padding(.leading)
.frame(maxWidth: .infinity)
}
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if isExpanded {
Group {
List(task.subtask) { subtask in
SubtaskCell(task: subtask)
}
}
.padding(.leading)
}
Divider()
}
}
private var header: some View {
HStack {
Image(systemName: "square")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
.padding(.vertical, 4)
.onTapGesture {
withAnimation {
isExpanded.toggle()
}
}
}
}
struct ContentView: View {
//sample data
private let tasks: [Task] = [
Task(
title: "Create playground",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
Task(
title: "Write article",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
Task(
title: "Prepare assets",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
Task(
title: "Publish article",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
]
var body: some View {
NavigationView {
VStack(alignment: .leading) {
ForEach(tasks) { task in
TaskCell(task: task)
.animation(.default)
}
Spacer()
}
}
}
}
在此先感谢您的帮助!
编辑:这是与下面接受的解决方案一起使用的折叠功能:
将 private var header: some View
中的 onTapGesture
更新为如下所示:
.onTapGesture {
withAnimation {
if task.isExpanded {
viewmodel.collapse(task)
} else {
viewmodel.expand(task)
}
}
}
然后将collapse
函数添加到class Viewmodel
func collapse(_ taks: TaskModel) {
var tasks = self.tasks
tasks = tasks.map {
var tempVar = [=13=]
tempVar.isExpanded = false
return tempVar
}
self.tasks = tasks
}
就是这样!完全按要求工作!
我认为实现此目的的最佳方法是将逻辑移至视图模型。
struct TaskModel: Identifiable {
let id: String = UUID().uuidString
let title: String
let subtask: [Subtask]
var isExpanded: Bool = false // moved state variable to the model
}
struct Subtask: Identifiable {
let id: String = UUID().uuidString
let title: String
}
struct SubtaskCell: View {
let task: Subtask
var body: some View {
HStack {
Image(systemName: "circle")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
}
}
struct TaskCell: View {
var task: TaskModel
@EnvironmentObject private var viewmodel: Viewmodel //removed state here and added viewmodel from environment
var body: some View {
content
.padding(.leading)
.frame(maxWidth: .infinity)
}
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if task.isExpanded {
Group {
List(task.subtask) { subtask in
SubtaskCell(task: subtask)
}
}
.padding(.leading)
}
Divider()
}
}
private var header: some View {
HStack {
Image(systemName: "square")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
.padding(.vertical, 4)
.onTapGesture {
withAnimation {
viewmodel.expand(task) //handle expand / collapse here
}
}
}
}
struct ContentView: View {
@StateObject private var viewmodel: Viewmodel = Viewmodel() //Create viewmodel here
var body: some View {
NavigationView {
VStack(alignment: .leading) {
ForEach(viewmodel.tasks) { task in //use viewmodel tasks here
TaskCell(task: task)
.animation(.default)
.environmentObject(viewmodel)
}
Spacer()
}
}
}
}
class Viewmodel: ObservableObject{
@Published var tasks: [TaskModel] = [
TaskModel(
title: "Create playground",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
TaskModel(
title: "Write article",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
TaskModel(
title: "Prepare assets",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
TaskModel(
title: "Publish article",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
]
func expand(_ task: TaskModel){
//copy tasks to local variable to avoid refreshing multiple times
var tasks = self.tasks
//create new task array with isExpanded set
tasks = tasks.map{
var tempVar = [=10=]
tempVar.isExpanded = [=10=].id == task.id
return tempVar
}
// assign array to update view
self.tasks = tasks
}
}
备注:
- 重命名了您的任务模型,因为使用语言已经使用的名称来命名某物是一个非常糟糕的主意
- 这只处理扩展。但是实现折叠应该不会太难:)
编辑:
如果您不想要视图模型,您可以使用绑定作为替代:
添加到您的容器视图:
@State private var selectedId: String?
将正文更改为:
NavigationView {
VStack(alignment: .leading) {
ForEach(tasks) { task in
TaskCell(task: task, selectedId: $selectedId)
.animation(.default)
}
Spacer()
}
}
并将您的 TaskCell 更改为:
struct TaskCell: View {
var task: TaskModel
@Binding var selectedId: String?
var body: some View {
content
.padding(.leading)
.frame(maxWidth: .infinity)
}
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if selectedId == task.id {
Group {
List(task.subtask) { subtask in
SubtaskCell(task: subtask)
}
}
.padding(.leading)
}
Divider()
}
}
private var header: some View {
HStack {
Image(systemName: "square")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
.padding(.vertical, 4)
.onTapGesture {
withAnimation {
selectedId = selectedId == task.id ? nil : task.id
}
}
}
}
我正在尝试构建一个包含多个折叠列表的视图,最初只显示 header。当您点击 header 时,它的列表应该展开。然后,随着第一个列表的展开,如果您点击另一个列表的 header,第一个列表应该会自动折叠,而第二个列表会展开。依此类推,一次只能看到一个列表。
下面的代码非常适合同时显示多个列表,它们都可以通过点击展开和折叠,但是我不知道如何在我点击展开时折叠已经打开的列表折叠列表。
这是代码(抱歉,有点长):
import SwiftUI
struct Task: Identifiable {
let id: String = UUID().uuidString
let title: String
let subtask: [Subtask]
}
struct Subtask: Identifiable {
let id: String = UUID().uuidString
let title: String
}
struct SubtaskCell: View {
let task: Subtask
var body: some View {
HStack {
Image(systemName: "circle")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
}
}
struct TaskCell: View {
var task: Task
@State private var isExpanded = false
var body: some View {
content
.padding(.leading)
.frame(maxWidth: .infinity)
}
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if isExpanded {
Group {
List(task.subtask) { subtask in
SubtaskCell(task: subtask)
}
}
.padding(.leading)
}
Divider()
}
}
private var header: some View {
HStack {
Image(systemName: "square")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
.padding(.vertical, 4)
.onTapGesture {
withAnimation {
isExpanded.toggle()
}
}
}
}
struct ContentView: View {
//sample data
private let tasks: [Task] = [
Task(
title: "Create playground",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
Task(
title: "Write article",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
Task(
title: "Prepare assets",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
Task(
title: "Publish article",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
]
var body: some View {
NavigationView {
VStack(alignment: .leading) {
ForEach(tasks) { task in
TaskCell(task: task)
.animation(.default)
}
Spacer()
}
}
}
}
在此先感谢您的帮助!
编辑:这是与下面接受的解决方案一起使用的折叠功能:
将 private var header: some View
中的 onTapGesture
更新为如下所示:
.onTapGesture {
withAnimation {
if task.isExpanded {
viewmodel.collapse(task)
} else {
viewmodel.expand(task)
}
}
}
然后将collapse
函数添加到class Viewmodel
func collapse(_ taks: TaskModel) {
var tasks = self.tasks
tasks = tasks.map {
var tempVar = [=13=]
tempVar.isExpanded = false
return tempVar
}
self.tasks = tasks
}
就是这样!完全按要求工作!
我认为实现此目的的最佳方法是将逻辑移至视图模型。
struct TaskModel: Identifiable {
let id: String = UUID().uuidString
let title: String
let subtask: [Subtask]
var isExpanded: Bool = false // moved state variable to the model
}
struct Subtask: Identifiable {
let id: String = UUID().uuidString
let title: String
}
struct SubtaskCell: View {
let task: Subtask
var body: some View {
HStack {
Image(systemName: "circle")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
}
}
struct TaskCell: View {
var task: TaskModel
@EnvironmentObject private var viewmodel: Viewmodel //removed state here and added viewmodel from environment
var body: some View {
content
.padding(.leading)
.frame(maxWidth: .infinity)
}
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if task.isExpanded {
Group {
List(task.subtask) { subtask in
SubtaskCell(task: subtask)
}
}
.padding(.leading)
}
Divider()
}
}
private var header: some View {
HStack {
Image(systemName: "square")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
.padding(.vertical, 4)
.onTapGesture {
withAnimation {
viewmodel.expand(task) //handle expand / collapse here
}
}
}
}
struct ContentView: View {
@StateObject private var viewmodel: Viewmodel = Viewmodel() //Create viewmodel here
var body: some View {
NavigationView {
VStack(alignment: .leading) {
ForEach(viewmodel.tasks) { task in //use viewmodel tasks here
TaskCell(task: task)
.animation(.default)
.environmentObject(viewmodel)
}
Spacer()
}
}
}
}
class Viewmodel: ObservableObject{
@Published var tasks: [TaskModel] = [
TaskModel(
title: "Create playground",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
TaskModel(
title: "Write article",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
TaskModel(
title: "Prepare assets",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
TaskModel(
title: "Publish article",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
]
func expand(_ task: TaskModel){
//copy tasks to local variable to avoid refreshing multiple times
var tasks = self.tasks
//create new task array with isExpanded set
tasks = tasks.map{
var tempVar = [=10=]
tempVar.isExpanded = [=10=].id == task.id
return tempVar
}
// assign array to update view
self.tasks = tasks
}
}
备注:
- 重命名了您的任务模型,因为使用语言已经使用的名称来命名某物是一个非常糟糕的主意
- 这只处理扩展。但是实现折叠应该不会太难:)
编辑:
如果您不想要视图模型,您可以使用绑定作为替代:
添加到您的容器视图:
@State private var selectedId: String?
将正文更改为:
NavigationView {
VStack(alignment: .leading) {
ForEach(tasks) { task in
TaskCell(task: task, selectedId: $selectedId)
.animation(.default)
}
Spacer()
}
}
并将您的 TaskCell 更改为:
struct TaskCell: View {
var task: TaskModel
@Binding var selectedId: String?
var body: some View {
content
.padding(.leading)
.frame(maxWidth: .infinity)
}
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if selectedId == task.id {
Group {
List(task.subtask) { subtask in
SubtaskCell(task: subtask)
}
}
.padding(.leading)
}
Divider()
}
}
private var header: some View {
HStack {
Image(systemName: "square")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
.padding(.vertical, 4)
.onTapGesture {
withAnimation {
selectedId = selectedId == task.id ? nil : task.id
}
}
}
}