SwiftUI展开列表一次一个,自动折叠

lzfw57am  于 2023-06-21  发布在  Swift
关注(0)|答案(2)|浏览(217)

我试图建立一个视图与几个折叠列表,只有标题显示在第一。当你点击一个标题时,它的列表应该会展开。然后,随着第一个列表的展开,如果你点击另一个列表的标题,第一个列表应该自动折叠,而第二个列表展开。这样,一次只能看到一个列表。
下面的代码可以很好地同时显示多个列表,它们都可以通过点击来展开和折叠,但是我不知道当我点击展开折叠列表时,如何使已经打开的列表折叠。
下面是代码(抱歉,有点长):

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 = $0
      tempVar.isExpanded = false
      return tempVar
    }
    self.tasks = tasks
  }

就这样!完全按要求工作!

bnl4lu3b

bnl4lu3b1#

我认为实现这一点的最好方法是将逻辑移到视图模型中。

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 = $0
        tempVar.isExpanded = $0.id == task.id
        return tempVar
    }
    
    // assign array to update view
    self.tasks = tasks
}
}

注意事项:

  • 重命名你的任务模型,因为用一个已经被语言使用的名字来命名是一个非常糟糕的主意
  • 这只处理扩展。但是实现崩溃应该不难:)

编辑:
如果你不需要视图模型,你可以使用绑定作为替代:
添加到您的containerview:

@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
      }
    }
  }
}
3pvhb19x

3pvhb19x2#

**iOS 17+**支持代码扩展。

init(_:isExpanded:content:)
isExpanded是一个绑定到布尔值的值,该值确定节的展开状态(展开或折叠)。
https://developer.apple.com/documentation/swiftui/section/init(_:isexpanded:content:)-153np

struct Sidebar: View {
    @Binding var selection: Int?
    @State private var isSection1Expanded = true
    @State private var isSection2Expanded = false

    var body: some View {
        List(selection: $selection) {
            Section("First Section", isExpanded: $isSection1Expanded) {
                ForEach(1..<6, id: \.self) {
                    Text("Item \($0)")
                }
            }
            Section("Second Section", isExpanded: $isSection2Expanded) {
                ForEach(6..<11, id: \.self) {
                    Text("Item \($0)")
                }
            }
        }
    }
}

相关问题