struct DragAndDropView: View {
struct Exercise {
struct Question {
struct Drag: Codable, Hashable, Identifiable, Transferable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .data)
}
let id: String
var isText = false
}
struct Drop: Hashable, Identifiable {
var location: CGRect
var item: Drag?
func hash(into hasher: inout Hasher) {
hasher.combine(location.width)
hasher.combine(location.height)
hasher.combine(location.minX)
hasher.combine(location.minY)
}
var id: Int {
hashValue
}
}
var title: String
var longQuestion: String?
var hint: String?
var drags: [Drag]
var drops: [Drop]
var item: Hint?
}
var title: String
var questions: [Question]
}
struct Hint: Identifiable {
var message: String
var id: String {
message
}
}
@State var exercise = Exercise(
title: "A Quiz Exercise",
questions: [
.init(
title: "Drag-and-drop question",
longQuestion: "Can you find the missing pieces?",
hint: "Something",
drags: [
.init(id: "1"),
.init(id: "2"),
.init(id: "Hello, world!", isText: true)
],
drops: [
// let (anX, aY, aW, anH) = (78, 50, 21, 38)
.init(location: .init(x: 78, y: 50, width: 21, height: 38), item: nil),
.init(location: .init(x: 0, y: 0, width: 10, height: 20), item: nil),
.init(location: .init(x: 170, y: 160, width: 30, height: 40), item: nil)
])
])
var body: some View {
ScrollView {
VStack {
ForEach($exercise.questions, id: \.title, content: self.question)
}
.padding(.horizontal)
}
.navigationTitle(exercise.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem {
Button("Submit") {
//
}
}
}
}
}
private extension DragAndDropView {
func question(_ question: Binding<Exercise.Question>) -> some View {
VStack(alignment: .leading) {
HStack {
Text(question.wrappedValue.title)
.font(.title2)
Spacer()
question.wrappedValue.hint.map { hint in
Button {
question.wrappedValue.item = Hint(message: hint)
} label: {
Image(systemName: "questionmark.circle.fill")
}
.popover(item: question.item) { item in
Text(item.message)
.padding(.horizontal)
.presentationCompactAdaptation(.popover)
}
}
Text("1 P")
.font(.body.bold())
}
question.wrappedValue.longQuestion.map(Text.init)
dragAndDrop(question)
Text("Drag & Drop: Place the suitable items on the correct areas")
.font(.footnote)
}
.padding()
.background(.secondary.opacity(0.1), in: .rect(cornerRadius: 5))
}
func dragAndDrop(_ question: Binding<Exercise.Question>) -> some View {
VStack {
Image("background")
.resizable()
.aspectRatio(contentMode: .fit)
.overlay {
GeometryReader { proxy in
ZStack {
ForEach(question.drops) { drop in
self.drop(proxy: proxy, drop: drop)
}
}
}
}
HStack {
Spacer()
ForEach(question.wrappedValue.drags, content: self.drag)
.padding()
Spacer()
}
.background(
RoundedRectangle(cornerRadius: 5)
.foregroundStyle(Color.secondary.opacity(0.1)))
}
}
@ViewBuilder
func drag(drag: Exercise.Question.Drag) -> some View {
if drag.isText {
Text(drag.id)
.padding()
.background(.secondary.opacity(0.1), in: .rect(cornerRadius: 5))
.draggable(drag)
}
else {
Image("Untitled \(drag.id)")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 50)
.draggable(drag)
}
}
func drop(proxy: GeometryProxy, drop: Binding<Exercise.Question.Drop>) -> some View {
let width = drop.wrappedValue.location.width * proxy.size.width / 200
let height = drop.wrappedValue.location.height * proxy.size.height / 200
let x = drop.wrappedValue.location.minX * proxy.size.width / 200 + width / 2
let y = drop.wrappedValue.location.minY * proxy.size.height / 200 + height / 2
let _ = print(x, y, width, height)
return Rectangle()
.stroke(style: StrokeStyle(dash: [1]))
.background {
Rectangle()
.foregroundStyle(.background)
}
.overlay {
if let item = drop.wrappedValue.item {
if item.isText {
Text(item.id)
}
else {
Image("Untitled \(item.id)")
.resizable()
.aspectRatio(contentMode: .fit)
}
}
}
.frame(width: width, height: height)
.position(x: x, y: y)
.dropDestination(for: Exercise.Question.Drag.self) { items, location in
if let item = items.first {
drop.wrappedValue.item = item
}
return true
}
}
}
#Preview {
DragAndDropView()
}
Prototype
See #65
Multiple-choice question
Screen.Recording.2023-12-20.at.20.11.07.mov
Details
Short-answer question
Screen.Recording.2023-12-24.at.00.56.47.mov
Details
Drag-and-drop question
Simulator.Screen.Recording.-.iPhone.15.Pro.-.2023-12-22.at.00.15.59.mp4
Details
Android