Skip to content

Commit 3b456ac

Browse files
Merge pull request #23 from Ditectrev/develop
Develop
2 parents a672ff7 + 2fcab3c commit 3b456ac

File tree

7 files changed

+364
-278
lines changed

7 files changed

+364
-278
lines changed

CloudMaster.xcodeproj/project.pbxproj

+10-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
/* Begin PBXBuildFile section */
1010
8D26A31C2C0EA9C100E9B015 /* QuestionNavbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D26A31B2C0EA9C100E9B015 /* QuestionNavbar.swift */; };
11+
8D26A31E2C0EE3F400E9B015 /* QuestionImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D26A31D2C0EE3F400E9B015 /* QuestionImages.swift */; };
12+
8D26A3202C0EE4A000E9B015 /* QuestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D26A31F2C0EE4A000E9B015 /* QuestionView.swift */; };
1113
8D8D8A862C05A23600ACC61C /* CloudMasterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8D8A852C05A23600ACC61C /* CloudMasterTests.swift */; };
1214
8D8D8A902C05A23600ACC61C /* CloudMasterUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8D8A8F2C05A23600ACC61C /* CloudMasterUITests.swift */; };
1315
8D8D8A922C05A23600ACC61C /* CloudMasterUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8D8A912C05A23600ACC61C /* CloudMasterUITestsLaunchTests.swift */; };
@@ -63,6 +65,8 @@
6365

6466
/* Begin PBXFileReference section */
6567
8D26A31B2C0EA9C100E9B015 /* QuestionNavbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionNavbar.swift; sourceTree = "<group>"; };
68+
8D26A31D2C0EE3F400E9B015 /* QuestionImages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionImages.swift; sourceTree = "<group>"; };
69+
8D26A31F2C0EE4A000E9B015 /* QuestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionView.swift; sourceTree = "<group>"; };
6670
8D8D8A712C05A23400ACC61C /* CloudMaster Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "CloudMaster Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; };
6771
8D8D8A812C05A23600ACC61C /* CloudMasterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CloudMasterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
6872
8D8D8A852C05A23600ACC61C /* CloudMasterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudMasterTests.swift; sourceTree = "<group>"; };
@@ -132,6 +136,8 @@
132136
children = (
133137
8D8D8A9F2C05A27800ACC61C /* ConfirmPopup.swift */,
134138
8D26A31B2C0EA9C100E9B015 /* QuestionNavbar.swift */,
139+
8D26A31D2C0EE3F400E9B015 /* QuestionImages.swift */,
140+
8D26A31F2C0EE4A000E9B015 /* QuestionView.swift */,
135141
);
136142
path = Components;
137143
sourceTree = "<group>";
@@ -586,9 +592,11 @@
586592
8D8D8ACF2C05A27800ACC61C /* DownloadOverlayView.swift in Sources */,
587593
8D8D8AD32C05A27800ACC61C /* UserExamData.swift in Sources */,
588594
8D8D8AD82C05A27800ACC61C /* PreviousExamsView.swift in Sources */,
595+
8D26A31E2C0EE3F400E9B015 /* QuestionImages.swift in Sources */,
589596
8D8D8AE42C05A27800ACC61C /* QuestionLoader.swift in Sources */,
590597
8D8D8ACE2C05A27800ACC61C /* ConfirmPopup.swift in Sources */,
591598
8D8D8AE32C05A27800ACC61C /* FavoriteStorage.swift in Sources */,
599+
8D26A3202C0EE4A000E9B015 /* QuestionView.swift in Sources */,
592600
8D8D8AD62C05A27800ACC61C /* ExamSummaryView.swift in Sources */,
593601
8D8D8AD92C05A27800ACC61C /* HomeView.swift in Sources */,
594602
8D8D8AD42C05A27800ACC61C /* ExamModesView.swift in Sources */,
@@ -774,7 +782,7 @@
774782
"$(inherited)",
775783
"@executable_path/Frameworks",
776784
);
777-
MARKETING_VERSION = 1.0.2;
785+
MARKETING_VERSION = 1.0.3;
778786
PRODUCT_BUNDLE_IDENTIFIER = com.ditectrev.cloudmasterswift;
779787
PRODUCT_NAME = "CloudMaster Swift";
780788
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -809,7 +817,7 @@
809817
"$(inherited)",
810818
"@executable_path/Frameworks",
811819
);
812-
MARKETING_VERSION = 1.0.2;
820+
MARKETING_VERSION = 1.0.3;
813821
PRODUCT_BUNDLE_IDENTIFIER = com.ditectrev.cloudmasterswift;
814822
PRODUCT_NAME = "CloudMaster Swift";
815823
PROVISIONING_PROFILE_SPECIFIER = "";

CloudMaster/Features/Exam/Views/ExamView.swift

+5-95
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ struct ExamView: View {
4444
}
4545
.padding(.horizontal)
4646

47-
ExamQuestion(
47+
QuestionView(
48+
mode: .exam,
4849
question: questions[currentQuestionIndex],
4950
selectedChoices: selectedChoices[questions[currentQuestionIndex].id] ?? [],
5051
isMultipleResponse: questions[currentQuestionIndex].multipleResponse,
52+
isResultShown: false, // Exam mode does not show result immediately
5153
onChoiceSelected: { choiceId in
5254
if questions[currentQuestionIndex].multipleResponse {
5355
if selectedChoices[questions[currentQuestionIndex].id]?.contains(choiceId) == true {
@@ -60,6 +62,7 @@ struct ExamView: View {
6062
}
6163
}
6264
)
65+
6366
Button(action: {
6467
if currentQuestionIndex < questions.count - 1 {
6568
currentQuestionIndex += 1
@@ -86,7 +89,7 @@ struct ExamView: View {
8689
}
8790
.padding()
8891
} else {
89-
Text("No que")
92+
Text("No Questions available! Please download course")
9093
}
9194
}
9295
.navigationDestination(isPresented: $navigateToSummary) {
@@ -164,96 +167,3 @@ struct ExamView: View {
164167
return String(format: "%02d:%02d", minutes, seconds)
165168
}
166169
}
167-
168-
struct ExamQuestion: View {
169-
let question: Question
170-
let selectedChoices: Set<UUID>?
171-
let isMultipleResponse: Bool
172-
let onChoiceSelected: (UUID) -> Void
173-
174-
var body: some View {
175-
ScrollView {
176-
VStack(alignment: .leading, spacing: 16) {
177-
Text(question.question)
178-
.font(.system(size: adjustedFontSize(for: question.question), weight: .bold))
179-
.minimumScaleFactor(0.5)
180-
.lineLimit(nil) // Allow text to wrap as needed
181-
.fixedSize(horizontal: false, vertical: true)
182-
.padding(.horizontal)
183-
.multilineTextAlignment(.leading) // Justify the text
184-
.lineSpacing(2)
185-
186-
if let imagePath = question.imagePath,
187-
let image = loadImage(from: imagePath) {
188-
Image(uiImage: image)
189-
.resizable()
190-
.cornerRadius(2)
191-
.aspectRatio(contentMode: .fit)
192-
.frame(maxWidth: .infinity)
193-
.padding(.horizontal)
194-
}
195-
196-
if isMultipleResponse {
197-
VStack {
198-
Text("Multiple response - Pick \(question.responseCount)")
199-
.font(.subheadline)
200-
.multilineTextAlignment(.center)
201-
.opacity(0.7)
202-
.padding(.vertical, 5)
203-
.frame(minWidth: 0, maxWidth: .infinity)
204-
}
205-
.background(Color.gray.opacity(0.2))
206-
.cornerRadius(10)
207-
.padding(.horizontal)
208-
}
209-
210-
ForEach(question.choices) { choice in
211-
ExamChoice(choice: choice, isSelected: selectedChoices?.contains(choice.id) == true, onChoiceSelected: onChoiceSelected)
212-
}
213-
}
214-
.padding()
215-
}
216-
}
217-
218-
private func loadImage(from imagePath: String) -> UIImage? {
219-
let fileManager = FileManager.default
220-
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
221-
let imageURL = documentsURL.appendingPathComponent(imagePath)
222-
return UIImage(contentsOfFile: imageURL.path)
223-
}
224-
225-
private func adjustedFontSize(for text: String) -> CGFloat {
226-
let maxWidth = UIScreen.main.bounds.width - 32
227-
let baseFontSize: CGFloat = 24
228-
let minFontSize: CGFloat = 14
229-
230-
// Scale the font size based on the text length
231-
let lengthFactor = CGFloat(text.count) / 100.0
232-
let scaledFontSize = max(baseFontSize - lengthFactor, minFontSize)
233-
234-
return scaledFontSize
235-
}
236-
}
237-
238-
struct ExamChoice: View {
239-
let choice: Choice
240-
let isSelected: Bool
241-
let onChoiceSelected: (UUID) -> Void
242-
243-
var body: some View {
244-
Button(action: {
245-
onChoiceSelected(choice.id)
246-
}) {
247-
Text(choice.text)
248-
.padding()
249-
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
250-
.multilineTextAlignment(.center)
251-
}
252-
.background(isSelected ? Color.gray.opacity(0.3) : Color.clear)
253-
.cornerRadius(10)
254-
.padding(.horizontal)
255-
.foregroundColor(.white)
256-
257-
Divider()
258-
}
259-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import SwiftUI
2+
3+
struct QuestionImages: View {
4+
let images: [Question.ImageInfo]
5+
@Binding var currentImageIndex: Int
6+
@Binding var isFullscreenImageShown: Bool
7+
@Binding var selectedImageIndex: Int
8+
9+
var body: some View {
10+
if !images.isEmpty {
11+
TabView(selection: $currentImageIndex) {
12+
ForEach(images.indices, id: \.self) { index in
13+
let imageInfo = images[index]
14+
if let image = loadImage(from: imageInfo.path) {
15+
Image(uiImage: image)
16+
.resizable()
17+
.cornerRadius(2)
18+
.aspectRatio(contentMode: .fit)
19+
.frame(maxWidth: .infinity)
20+
.padding(.horizontal)
21+
.tag(index)
22+
.onTapGesture {
23+
selectedImageIndex = index
24+
isFullscreenImageShown = true
25+
}
26+
} else if let url = imageInfo.url, let urlImage = loadImage(from: url) {
27+
Image(uiImage: urlImage)
28+
.resizable()
29+
.cornerRadius(2)
30+
.aspectRatio(contentMode: .fit)
31+
.frame(maxWidth: .infinity)
32+
.padding(.horizontal)
33+
.tag(index)
34+
.onTapGesture {
35+
selectedImageIndex = index
36+
isFullscreenImageShown = true
37+
}
38+
}
39+
}
40+
}
41+
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
42+
.frame(height: 300)
43+
}
44+
}
45+
46+
private func loadImage(from imagePath: String) -> UIImage? {
47+
let fileManager = FileManager.default
48+
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
49+
let imageURL = documentsURL.appendingPathComponent(imagePath)
50+
return UIImage(contentsOfFile: imageURL.path)
51+
}
52+
}
53+
54+
55+
struct FullscreenImageView: View {
56+
let images: [Question.ImageInfo]
57+
@Binding var selectedImageIndex: Int
58+
@Binding var isShown: Bool
59+
60+
@State private var scale: CGFloat = 1.0
61+
@State private var lastScale: CGFloat = 1.0
62+
63+
var body: some View {
64+
ZStack {
65+
Color.black.opacity(0.8)
66+
.edgesIgnoringSafeArea(.all)
67+
68+
if let imageInfo = images[safe: selectedImageIndex],
69+
let uiImage = loadImage(from: imageInfo.path) ?? loadImage(from: imageInfo.url) {
70+
Image(uiImage: uiImage)
71+
.resizable()
72+
.aspectRatio(contentMode: .fit)
73+
.scaleEffect(scale)
74+
.gesture(
75+
MagnificationGesture()
76+
.onChanged { value in
77+
self.scale = self.lastScale * value
78+
}
79+
.onEnded { _ in
80+
self.lastScale = self.scale
81+
}
82+
)
83+
.onTapGesture {
84+
isShown = false
85+
}
86+
}
87+
}
88+
}
89+
90+
private func loadImage(from imagePath: String?) -> UIImage? {
91+
guard let imagePath = imagePath else { return nil }
92+
let fileManager = FileManager.default
93+
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
94+
let imageURL = documentsURL.appendingPathComponent(imagePath)
95+
return UIImage(contentsOfFile: imageURL.path)
96+
}
97+
}
98+
99+
// extension to safely access array elements to avoid out-of-bounds
100+
extension Array {
101+
subscript(safe index: Int) -> Element? {
102+
return indices.contains(index) ? self[index] : nil
103+
}
104+
}

0 commit comments

Comments
 (0)