Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exercises: Add participation in quiz exercises #68

Open
nityanandaz opened this issue Jan 9, 2024 · 0 comments
Open

Exercises: Add participation in quiz exercises #68

nityanandaz opened this issue Jan 9, 2024 · 0 comments
Labels
enhancement New feature or request

Comments

@nityanandaz
Copy link
Collaborator

nityanandaz commented Jan 9, 2024

Prototype

See #65

Multiple-choice question

Screen.Recording.2023-12-20.at.20.11.07.mov
Details

import SwiftUI

struct MultipleChoiceView: View {

    struct Exercise {

        struct Question {

            struct Choice {
                var title: String
                var hint: String?
                var explanation: String?
                var isOn: Bool

                var item: Hint?
            }

            var title: String

            var longQuestion: String?
            var hint: String?

            var choices: [Choice]

            var item: Hint?
        }

        var title: String
        var questions: [Question]
    }

    struct Hint: Identifiable {
        var message: String

        var id: String {
            message
        }
    }

    struct MultipleChoiceToggleStyle: ToggleStyle {
        func makeBody(configuration: Configuration) -> some View {
            HStack {
                configuration.label
                Button {
                    configuration.isOn.toggle()
                } label: {
                    if configuration.isOn {
                        Image(systemName: "checkmark.square.fill")
                    } else {
                        Image(systemName: "square")
                    }
                }
                .foregroundStyle(.foreground)
            }
            .padding()
            .background(.secondary.opacity(0.2), in: .rect(cornerRadius: 5))
        }
    }

    @Environment(\.isEnabled) var isEnabled

    @State var exercise = Exercise(
        title: "A Quiz Exercise",
        questions: [
            .init(
                title: "Multiple-choice question",
                longQuestion: "A very very very very very very very very very very very very very very very very very long question?",
                hint: "Something",
                choices: [
                    .init(
                        title: "Enter a correct answer option here",
                        hint: "This is correct",
                        explanation: "Add an explanation here (only visible in feedback after quiz has ended)",
                        isOn: false),
                    .init(title: "Maybe this is correct, too", isOn: false),
                    .init(title: "Enter a wrong answer option here", isOn: false)
                ]),
            .init(
                title: "What does every program say first?",
                hint: "Nothing",
                choices: [
                    .init(title: "Hello, world!", isOn: false)
                ])
        ])

    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 MultipleChoiceView {
    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)
            ForEach(question.choices, id: \.title, content: self.choice)
            Text("Please check all correct answer options")
                .font(.footnote)
        }
        .padding()
        .background(.secondary.opacity(0.1), in: .rect(cornerRadius: 5))
    }

    func choice(_ choice: Binding<Exercise.Question.Choice>) -> some View {
        VStack {
            Toggle(isOn: choice.isOn) {
                HStack {
                    Text(choice.wrappedValue.title)
                    Spacer()
                    if isEnabled {
                        choice.wrappedValue.hint.map { hint in
                            Button {
                                choice.wrappedValue.item = Hint(message: hint)
                            } label: {
                                Image(systemName: "questionmark.circle.fill")
                            }
                            .popover(item: choice.item) { item in
                                Text(item.message)
                                    .padding(.horizontal)
                                    .presentationCompactAdaptation(.popover)
                            }
                        }
                    }
                }
            }
            .toggleStyle(MultipleChoiceToggleStyle())
            if !isEnabled {
                if let hint = choice.wrappedValue.hint {
                    HStack {
                        Image(systemName: "questionmark.circle.fill")
                        Text(hint)
                        Spacer()
                    }
                }
                if let explanation = choice.wrappedValue.explanation {
                    HStack(alignment: .top) {
                        Image(systemName: "exclamationmark.circle.fill")
                        Text(explanation)
                        Spacer()
                    }
                }
            }
        }
    }
}

#Preview {
    NavigationStack {
        MultipleChoiceView()
            .disabled(false)
    }
}

#Preview {
    NavigationStack {
        MultipleChoiceView()
            .disabled(true)
    }
    .preferredColorScheme(.dark)
}

Short-answer question

Screen.Recording.2023-12-24.at.00.56.47.mov
Details

import SwiftUI

struct ShortAnswerView: View {

    static func makeNodes(_ string: String) -> [Exercise.Question.Node_] {
        let pattern = #/\[-spot \d+\]/#
        let split = string
            .split(separator: pattern)
            .map { substring in
                Exercise.Question.Node_.init(id: .init(), text: String(substring), isSpot: false)
            }

        let matches = string.matches(of: pattern)
        let match = matches[0].output
        print(match)

        assert(split.count == matches.count + 1)

        // https://stackoverflow.com/questions/34951824/how-can-i-interleave-two-arrays
        func mergeFunction<T>(_ one: [T], _ two: [T]) -> [T] {
            let commonLength = min(one.count, two.count)
            return zip(one, two).flatMap { [$0, $1] }
                   + one.suffix(from: commonLength)
                   + two.suffix(from: commonLength)
        }

        return mergeFunction(split,
                             matches.map { x in Exercise.Question.Node_.init(id: .init(), text: "", isSpot: true) })
    }

    struct Exercise {

        struct Question {

            struct Node_: Identifiable {
                let id: UUID
                var text: String
                var isSpot: Bool
            }

            var title: String

            var longQuestion: String?
            var hint: String?

            var nodes: [Node_]

            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: "Short-answer question",
                longQuestion: nil,
                hint: "Something",
                nodes: makeNodes("Enter your long question if needed\n\nSelect a part of the text and click on Add Spot to automatically create an input field and the corresponding mapping\n\nYou can define a input field like this: This [-spot 1] an [-spot 2] field.\n\nTo define the solution for the input fields you need to create a mapping (multiple mapping also possible):")),
//            .init(
//                title: "What does every program say first?",
//                hint: "Nothing",
//                choices: [
//                    .init(title: "Hello, world!", isOn: false)
//                ])
        ])

    var body: some View {
        NavigationStack {
            ScrollView {
                VStack {
                    ForEach($exercise.questions, id: \.title, content: self.question)
                }
                .padding(.horizontal)
            }
            .navigationTitle(exercise.title)
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem {
                    Button("Submit") {
                        //
                    }
                }
            }
        }
    }
}

private extension ShortAnswerView {
    func question(_ question: Binding<Exercise.Question>) -> some View {
        VStack(alignment: .leading) {
            HStack/*(alignment: .firstTextBaseline)*/ {
                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())
            }
            VStack(alignment: .leading) {
                ForEach(question.nodes) { node in
                    if node.wrappedValue.isSpot {
                        TextField("", text: node.text)
                            .textFieldStyle(.roundedBorder)
                    } else {
                        Text(node.wrappedValue.text)
                    }
                }
            }
            .padding()
            .background(.background, in: .rect(cornerRadius: 5))
        }
        .padding()
        .background(.secondary.opacity(0.1), in: .rect(cornerRadius: 5))
    }
}

#Preview {
    ShortAnswerView()
}

Drag-and-drop question

Simulator.Screen.Recording.-.iPhone.15.Pro.-.2023-12-22.at.00.15.59.mp4
Details

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

Android

a b c
Screenshot_20231210_014315 Screenshot_20231210_014332 Screenshot_20231210_014346
Screenshot_20231210_014410 Screenshot_20231210_014428
Screenshot_20231210_014441 Screenshot_20231210_014507
Screenshot_20231210_014551 Screenshot_20231210_014602 Screenshot_20231210_014607
@nityanandaz nityanandaz added the enhancement New feature or request label Jun 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant