Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ struct KeyboardAdaptiveViewModifier: ViewModifier {
}

extension View {
func keyboardAdaptive() -> some View {
self.modifier(KeyboardAdaptiveViewModifier())
func keyboardAdaptive(isEnabled: Bool = true) -> some View {
if isEnabled {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I set this flag because it caused a problem when opening the keyboard in the Quiz.

AnyView(self.modifier(KeyboardAdaptiveViewModifier()))
} else {
AnyView(self)
}
}
}
37 changes: 37 additions & 0 deletions Horizon/Horizon/Sources/Common/View/KeyboardObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// This file is part of Canvas.
// Copyright (C) 2025-present Instructure, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//

import SwiftUI
import Combine

@Observable
final class KeyboardObserver {

private(set) var isKeyboardVisible: Bool = false
private var cancellables: Set<AnyCancellable> = []

init() {
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
.sink { [weak self] _ in self?.isKeyboardVisible = true }
.store(in: &cancellables)

NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
.sink { [weak self] _ in self?.isKeyboardVisible = false }
.store(in: &cancellables)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ struct AssignmentDetails: View {
}
.hidden(viewModel.isInitialLoading)
.overlay { loaderView }
.keyboardAdaptive()
.keyboardAdaptive(isEnabled: viewModel.selectedSubmission == .text)
.ignoresSafeArea(.keyboard, edges: .bottom)
.scrollDismissesKeyboard(isShowHeader ? .immediately : .never)
.preference(key: AssignmentPreferenceKey.self, value: viewModel.assignmentPreference)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ final class AssignmentDetailsViewModel {
private(set) var submission: HSubmission?
private(set) var isSegmentControlVisible: Bool = false
private(set) var selectedSubmission: AssignmentSubmissionType = .text
private(set) var externalURL: URL?
var isStartTyping = false
var assignmentPreference: AssignmentPreferenceKeyType?

Expand Down Expand Up @@ -244,6 +245,25 @@ final class AssignmentDetailsViewModel {
selectedSubmission = hasSubmittedBefore == true ? latestSubmission : selectedSubmission
submission = submissions.first
didLoadAttemptCount(response.attemptCount)
if selectedSubmission == .externalTool {
fetchExternalURL()
}
}

private func fetchExternalURL() {
let tools = LTITools(
context: .course(courseID),
id: assignment?.externalToolContentID,
launchType: .assessment,
isQuizLTI: assignment?.isQuizLTI,
assignmentID: assignmentID
)

isLoaderVisible = true
tools.getSessionlessLaunch { [weak self] value in
self?.isLoaderVisible = false
self?.externalURL = value?.url
}
}

private func bindSubmissionAssignmentEvents() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import SwiftUI
enum AssignmentPreferenceKeyType: Equatable {
case confirmation(viewModel: SubmissionAlertViewModel)
case toastViewModel(viewModel: ToastViewModel)
case moduleNavBarButton(isVisible: Bool)
}

struct HeaderVisibilityKey: PreferenceKey {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ struct AssignmentSubmissionView: View {
}
case .externalTool:
externalToolView
.frame(height: 322)
case .fileUpload:
VStack(spacing: .huiSpaces.space8) {
HorizonUI.FileDrop(
Expand Down Expand Up @@ -153,7 +152,6 @@ struct AssignmentSubmissionView: View {
.id(rceID)
.focused($focusedInput)
.onChange(of: focusedInput) { _, newValue in
viewModel.assignmentPreference = .moduleNavBarButton(isVisible: !newValue)
viewModel.isStartTyping = newValue
if newValue {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
Expand All @@ -167,14 +165,18 @@ struct AssignmentSubmissionView: View {

@ViewBuilder
private var externalToolView: some View {
let tools = LTITools(
context: .course(viewModel.courseID),
id: viewModel.assignment?.externalToolContentID,
launchType: .assessment,
isQuizLTI: viewModel.assignment?.isQuizLTI,
assignmentID: viewModel.assignmentID
let padding: CGFloat = .huiSpaces.space24
let isLTI = viewModel.assignment?.isQuizLTI == true
WebView(
url: viewModel.externalURL,
features: [
.invertColorsInDarkMode,
.hideReturnButtonInQuizLTI
]
)
LTIViewRepresentable(environment: .shared, tools: tools, name: nil)
.padding(.horizontal, isLTI ? -padding : 0)
/// I set 1000 as a temporary value until the front-end team removes the ScrollView embedded in the WebView in quiz.
.frame(height: isLTI ? 1000 : 400, alignment: .top)
}

private func makeFileUploadButtons() -> [HorizonUI.Overlay.ButtonAttribute] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ public struct ModuleItemSequenceView: View {
@State private var isShowModuleNavBar = true
@State private var submissionAlertModel = SubmissionAlertViewModel()
@State private var draftToastViewModel = ToastViewModel()
@State private var isHeaderAnimationEnabled = true
@Environment(\.viewController) private var viewController
private let keyboardObserver = KeyboardObserver()

// MARK: - Dependencies

Expand All @@ -54,6 +56,7 @@ public struct ModuleItemSequenceView: View {
.offset(x: viewModel.offsetX)
.huiCornerRadius(level: .level5, corners: [.topRight, .topLeft])
.onPreferenceChange(HeaderVisibilityKey.self) { isShow in
isHeaderAnimationEnabled = true
if isShowModuleNavBar {
isShowHeader = isShow
} else {
Expand All @@ -67,17 +70,14 @@ public struct ModuleItemSequenceView: View {
submissionAlertModel = viewModel
case .toastViewModel(viewModel: let viewModel):
draftToastViewModel = viewModel
case .moduleNavBarButton(isVisible: let isVisible):
isShowModuleNavBar = isVisible
isShowHeader = isVisible
}
}
}
}
.overlay { loaderView }
.safeAreaInset(edge: .top, spacing: .zero) { introBlock }
.safeAreaInset(edge: .bottom, spacing: .zero) { moduleNavBarView }
.animation(.linear, value: isShowHeader)
.animation(isHeaderAnimationEnabled ? .linear : nil, value: isShowHeader)
.confirmationDialog("", isPresented: $isShowMakeAsDoneSheet, titleVisibility: .hidden) {
makeAsDoneSheetButtons
}
Expand All @@ -101,7 +101,11 @@ public struct ModuleItemSequenceView: View {
isShowCancelButton: submissionAlertModel.type == .confirmation,
confirmButton: submissionAlertModel.button,
isPresented: $submissionAlertModel.isPresented) { assignmentConfirmationView }

.onChange(of: keyboardObserver.isKeyboardVisible) { _, newValue in
isHeaderAnimationEnabled = false
self.isShowHeader = !newValue
self.isShowModuleNavBar = !newValue
}
}

private var assignmentConfirmationView: some View {
Expand Down