-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
iOS fill in the blanks with inputs (#720)
^ALTAPPS-1016
- Loading branch information
1 parent
e1ddecc
commit 368d166
Showing
23 changed files
with
1,321 additions
and
15 deletions.
There are no files selected for viewing
112 changes: 112 additions & 0 deletions
112
iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj
Large diffs are not rendered by default.
Oops, something went wrong.
12 changes: 12 additions & 0 deletions
12
iosHyperskillApp/iosHyperskillApp/Sources/Extensions/UIKit/UIFont+SizeOfString.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import UIKit | ||
|
||
extension UIFont { | ||
func sizeOfString(string: String, constrainedToWidth width: Double) -> CGSize { | ||
NSString(string: string).boundingRect( | ||
with: CGSize(width: width, height: Double.greatestFiniteMagnitude), | ||
options: NSStringDrawingOptions.usesLineFragmentOrigin, | ||
attributes: [NSAttributedString.Key.font: self], | ||
context: nil | ||
).size | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
...pp/Sources/Modules/StepQuizSubmodules/StepQuizFillBlanks/StepQuizFillBlanksAssembly.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import Highlightr | ||
import shared | ||
import SwiftUI | ||
|
||
final class StepQuizFillBlanksAssembly: StepQuizChildQuizAssembly { | ||
var moduleInput: StepQuizChildQuizInputProtocol? | ||
weak var moduleOutput: StepQuizChildQuizOutputProtocol? | ||
|
||
private let provideModuleInputCallback: (StepQuizChildQuizInputProtocol?) -> Void | ||
|
||
private let step: Step | ||
private let dataset: Dataset | ||
private let reply: Reply? | ||
|
||
init( | ||
step: Step, | ||
dataset: Dataset, | ||
reply: Reply?, | ||
provideModuleInputCallback: @escaping (StepQuizChildQuizInputProtocol?) -> Void, | ||
moduleOutput: StepQuizChildQuizOutputProtocol? | ||
) { | ||
self.step = step | ||
self.dataset = dataset | ||
self.reply = reply | ||
self.provideModuleInputCallback = provideModuleInputCallback | ||
self.moduleOutput = moduleOutput | ||
} | ||
|
||
func makeModule() -> StepQuizFillBlanksView { | ||
let viewModel = StepQuizFillBlanksViewModel( | ||
step: step, | ||
dataset: dataset, | ||
reply: reply, | ||
viewDataMapper: StepQuizFillBlanksViewDataMapper( | ||
fillBlanksItemMapper: FillBlanksItemMapper(), | ||
highlightr: Highlightr().require(), | ||
codeEditorThemeService: CodeEditorThemeService(), | ||
cache: StepQuizFillBlanksViewDataMapperCache.shared | ||
), | ||
provideModuleInputCallback: provideModuleInputCallback | ||
) | ||
|
||
moduleInput = viewModel | ||
viewModel.moduleOutput = moduleOutput | ||
|
||
return StepQuizFillBlanksView(viewModel: viewModel) | ||
} | ||
} |
73 changes: 73 additions & 0 deletions
73
...p/Sources/Modules/StepQuizSubmodules/StepQuizFillBlanks/StepQuizFillBlanksViewModel.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import Combine | ||
import Foundation | ||
import shared | ||
|
||
final class StepQuizFillBlanksViewModel: ObservableObject { | ||
weak var moduleOutput: StepQuizChildQuizOutputProtocol? | ||
private let provideModuleInputCallback: (StepQuizChildQuizInputProtocol?) -> Void | ||
|
||
@Published private(set) var viewData: StepQuizFillBlanksViewData | ||
|
||
init( | ||
step: Step, | ||
dataset: Dataset, | ||
reply: Reply?, | ||
viewDataMapper: StepQuizFillBlanksViewDataMapper, | ||
provideModuleInputCallback: @escaping (StepQuizChildQuizInputProtocol?) -> Void | ||
) { | ||
self.provideModuleInputCallback = provideModuleInputCallback | ||
self.viewData = viewDataMapper.mapToViewData(dataset: dataset, reply: reply) | ||
} | ||
|
||
func doProvideModuleInput() { | ||
provideModuleInputCallback(self) | ||
} | ||
|
||
func doInputTextUpdate(_ inputText: String, for component: StepQuizFillBlankComponent) { | ||
guard let index = viewData.components.firstIndex( | ||
where: { $0.id == component.id } | ||
) else { | ||
return | ||
} | ||
|
||
viewData.components[index].inputText = inputText | ||
outputCurrentReply() | ||
} | ||
|
||
func doSelectComponent(at indexPath: IndexPath) { | ||
setIsFirstResponder(true, forComponentAt: indexPath) | ||
} | ||
|
||
func doDeselectComponent(at indexPath: IndexPath) { | ||
setIsFirstResponder(false, forComponentAt: indexPath) | ||
} | ||
|
||
private func setIsFirstResponder(_ isFirstResponder: Bool, forComponentAt indexPath: IndexPath) { | ||
guard viewData.components[indexPath.row].type == .input else { | ||
return | ||
} | ||
|
||
viewData.components[indexPath.row].isFirstResponder = isFirstResponder | ||
} | ||
} | ||
|
||
// MARK: - StepQuizFillBlanksViewModel: StepQuizChildQuizInputProtocol - | ||
|
||
extension StepQuizFillBlanksViewModel: StepQuizChildQuizInputProtocol { | ||
func createReply() -> Reply { | ||
let blanks: [String] = viewData.components.compactMap { component in | ||
switch component.type { | ||
case .text, .lineBreak: | ||
return nil | ||
case .input: | ||
return component.inputText ?? "" | ||
} | ||
} | ||
|
||
return Reply.companion.fillBlanks(blanks: blanks) | ||
} | ||
|
||
private func outputCurrentReply() { | ||
moduleOutput?.handleChildQuizSync(reply: createReply()) | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
...s/Modules/StepQuizSubmodules/StepQuizFillBlanks/ViewData/StepQuizFillBlanksViewData.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import Foundation | ||
|
||
struct StepQuizFillBlanksViewData: Hashable { | ||
var components: [StepQuizFillBlankComponent] | ||
} | ||
|
||
struct StepQuizFillBlankComponent: Hashable, Identifiable { | ||
var id: Int = 0 | ||
let type: ComponentType | ||
// text | ||
var attributedText: NSAttributedString? | ||
// input | ||
var inputText: String? | ||
var isFirstResponder = false | ||
|
||
enum ComponentType { | ||
case text | ||
case input | ||
case lineBreak | ||
} | ||
} |
89 changes: 89 additions & 0 deletions
89
...les/StepQuizSubmodules/StepQuizFillBlanks/ViewData/StepQuizFillBlanksViewDataMapper.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import Foundation | ||
import Highlightr | ||
import shared | ||
|
||
final class StepQuizFillBlanksViewDataMapper { | ||
private let fillBlanksItemMapper: FillBlanksItemMapper | ||
private let highlightr: Highlightr | ||
private let codeEditorThemeService: CodeEditorThemeServiceProtocol | ||
private let cache: StepQuizFillBlanksViewDataMapperCacheProtocol | ||
|
||
init( | ||
fillBlanksItemMapper: FillBlanksItemMapper, | ||
highlightr: Highlightr, | ||
codeEditorThemeService: CodeEditorThemeServiceProtocol, | ||
cache: StepQuizFillBlanksViewDataMapperCacheProtocol | ||
) { | ||
let theme = codeEditorThemeService.theme | ||
highlightr.setTheme(to: theme.name) | ||
highlightr.theme.setCodeFont(theme.font) | ||
|
||
self.highlightr = highlightr | ||
self.codeEditorThemeService = codeEditorThemeService | ||
self.cache = cache | ||
self.fillBlanksItemMapper = fillBlanksItemMapper | ||
} | ||
|
||
func mapToViewData(dataset: Dataset, reply: Reply?) -> StepQuizFillBlanksViewData { | ||
guard let fillBlanksData = fillBlanksItemMapper.map(dataset: dataset, reply: reply) else { | ||
return .init(components: []) | ||
} | ||
|
||
return mapFillBlanksDataToViewData(fillBlanksData) | ||
} | ||
|
||
private func mapFillBlanksDataToViewData(_ fillBlanksData: FillBlanksData) -> StepQuizFillBlanksViewData { | ||
let language = fillBlanksData.language | ||
|
||
var components = fillBlanksData.fillBlanks | ||
.map { mapFillBlanksItem($0, language: language) } | ||
.flatMap { $0 } | ||
for index in components.indices { | ||
components[index].id = index | ||
} | ||
|
||
return StepQuizFillBlanksViewData(components: components) | ||
} | ||
|
||
private func mapFillBlanksItem( | ||
_ fillBlanksItem: FillBlanksItem, | ||
language: String? | ||
) -> [StepQuizFillBlankComponent] { | ||
switch FillBlanksItemKs(fillBlanksItem) { | ||
case .text(let data): | ||
var result = [StepQuizFillBlankComponent]() | ||
|
||
if data.startsWithNewLine { | ||
result.append(StepQuizFillBlankComponent(type: .lineBreak)) | ||
} | ||
|
||
let hash = data.text.hashValue ^ UITraitCollection.current.userInterfaceStyle.hashValue | ||
|
||
if let cachedCode = cache.getHighlightedCode(for: hash) { | ||
result.append(StepQuizFillBlankComponent(type: .text, attributedText: cachedCode)) | ||
} else { | ||
let unescaped = HTMLString.unescape(string: data.text) | ||
|
||
if let highlightedCode = highlight(code: unescaped, language: language) { | ||
cache.setHighlightedCode(highlightedCode, for: hash) | ||
result.append(StepQuizFillBlankComponent(type: .text, attributedText: highlightedCode)) | ||
} else { | ||
let attributedText = NSAttributedString( | ||
string: unescaped, | ||
attributes: [.font: codeEditorThemeService.theme.font] | ||
) | ||
cache.setHighlightedCode(attributedText, for: hash) | ||
result.append(StepQuizFillBlankComponent(type: .text, attributedText: attributedText)) | ||
} | ||
} | ||
|
||
return result | ||
case .input(let data): | ||
return [StepQuizFillBlankComponent(type: .input, inputText: data.inputText)] | ||
} | ||
} | ||
|
||
private func highlight(code: String, language: String?) -> NSAttributedString? { | ||
highlightr.highlight(code, as: language, fastRender: true) | ||
} | ||
} |
Oops, something went wrong.