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
      }
    }
  }
}