如何在SwiftUI的List中让Section内的Divider拉伸至最左侧?
iOS 17健康App心理健康测评界面复刻问题
近期我体验了iOS 17中Apple Health app的心理健康测评功能:

我很喜欢该应用完成题目后自动滚动至下一题的交互,UI设计也十分赏心悦目,于是尝试用SwiftUI复刻该界面,已经做出了相近的效果:

但有一处细节一直困扰我:问题与选项之间的水平分隔线(红色圈出部分)无法像原应用那样拉伸至最左侧,希望实现这一效果。我在QuestionView中使用了Section,QuizView中使用了List,恳请提供SwiftUI实现分隔线拉伸的方法,同时也期待对复刻版本的反馈。以下是我的源码:
// // QuizView.swift // Swole // // Created by Mashrafi Rahman on 18/8/23. // import SwiftUI import IdentifiedCollections class QuizViewModel: ObservableObject, Codable { /// Question in the quiz. @Published var questions: IdentifiedArrayOf<Question> private enum CodingKeys: CodingKey { case questions } init(questions: IdentifiedArrayOf<Question> = .init()) { self.questions = questions } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.questions = try container.decode( IdentifiedArrayOf<Question>.self, forKey: .questions ) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.questions, forKey: .questions) } /// Whether any changes have been made. var is_changed: Bool { self.questions.contains { $0.is_answered } } /// Whether the quiz is ready to be submitted. var is_ready: Bool { self.questions.allSatisfy { $0.is_answered } } /// Index of the first question that has not been filled. var next: UUID? { self.questions.first { !$0.is_answered }?.id } /// Returns data for submission in the form of an array containing arrays of selected elements. func submit() -> [[Int]]? { return is_ready ? questions.map { $0.selected } : nil } } struct QuizView: View { @StateObject var quiz: QuizViewModel = .init() var body: some View { ScrollViewReader { proxy in List { ForEach($quiz.questions) { $q in let i: Int = quiz.questions.index(id: q.id)! + 1 let l: Int = quiz.questions.count QuestionView(question: $q, index: (i, l)) } .onChange(of: quiz.next) { _, new in withAnimation { if let new { proxy.scrollTo(new, anchor: .top) } } } } } } } /// Type of question. enum QuestionType: String, Codable { /// Multiple-choice question. case multiple /// Single-choice question. case single } struct Question: Identifiable, Equatable, Codable { let id: UUID = UUID() /// Question or statement. let question: String /// Type of question. let type: QuestionType /// Options for the question. var options: IdentifiedArrayOf<Option> private enum CodingKeys: String, CodingKey { case question, type, options } /// Indices of options that have been selected. var selected: [Int] { self.options.enumerated().compactMap { index, element in element.selected ? index : nil } } /// Whether changes have been made to the question. var is_answered: Bool { self.options.contains { $0.selected } } /// Callback function when an option is pressed. mutating func select(id: UUID) { guard let selected: Bool = self.options[id: id]?.selected else { return } switch self.type { case .multiple: break case .single: if !selected { self.options.indices.forEach { x in self.options[x].selected = false } } } self.options[id: id]!.selected.toggle() } /// Checks if two questions are equal.. static func == (lhs: Question, rhs: Question) -> Bool { return lhs.id == rhs.id } } struct QuestionView: View { /// Question to display. @Binding var question: Question /// Index of question. var index: (Int, Int)? = nil var body: some View { Section { VStack { if let index { Text("Question \(index.0) of \(index.1)") .padding(.top, 7.5) .padding(.bottom, 1) .frame(maxWidth: .infinity, alignment: .leading) .font(.caption) .foregroundStyle(.secondary) .bold() } else { Spacer(minLength: 8) } Text(question.question) .padding(.bottom, 7.5) .font(.headline) .frame(maxWidth: .infinity, alignment: .leading) } ForEach(question.options) { o in OptionView(option: o, type: question.type) .onTapGesture { question.select(id: o.id) } } } } } /// An option for a question. Can be used in multiple-choice and single-choice questions. struct Option: Identifiable, Codable { let id: UUID = UUID() /// Description of option. let desc: String /// Whether the option is selected. var selected: Bool = false private enum CodingKeys: String, CodingKey { case desc, selected } } struct OptionView: View { /// Option to display. let option: Option /// Name of System Image (SF Symbol) when checked. var symbolChecked: String var symbolUnchecked: String init(option: Option, type: QuestionType) { self.option = option switch type { case .single: self.symbolChecked = "checkmark.circle.fill" self.symbolUnchecked = "circle" case .multiple: self.symbolChecked = "checkmark.square.fill" self.symbolUnchecked = "square" } } var body: some View { ZStack { Text(option.desc) .frame(maxWidth: .infinity, alignment: .leading) if option.selected { Image(systemName: symbolChecked) .symbolRenderingMode(.multicolor) .frame(maxWidth: .infinity, alignment: .trailing) } else { Image(systemName: symbolUnchecked) .symbolRenderingMode(.palette) .foregroundStyle(.tertiary) .frame(maxWidth: .infinity, alignment: .trailing) } } .contentShape(Rectangle()) } } struct PreviewView: View { var quizz: QuizViewModel = .init(questions: .init()) init() { for i in 1...20 { quizz.questions.append( .init(question: "Question \(i)", type: .single, options: [ .init(desc: "Yes"), .init(desc: "No"), ]) ) } } var body: some View { QuizView(quiz: quizz) } } #Preview { PreviewView() }
内容的提问来源于stack exchange,提问作者Mycelium
相关产品推荐
相关产品推荐

