Skip to content
This repository was archived by the owner on Nov 7, 2026. It is now read-only.

Commit 5b89613

Browse files
committed
Merge branch 'ci-app-build' into xcode-synced-folders
2 parents c1902bc + 0603fb4 commit 5b89613

29 files changed

Lines changed: 787 additions & 366 deletions

File tree

‎VisuStruct-iOS/VisuStructCore/Sources/VisuStructCore/VisuStructSimulationEngine.swift‎

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,11 @@ public struct VisuStructSimulationState: Equatable, Sendable {
102102
public var variables: [String: VisuStructSimulationValue]
103103
public var outputLines: [String]
104104
public var inputRequest: VisuStructSimulationInputRequest?
105-
public var inputError: String?
105+
/// Fehler zur letzten Eingabe (z. B. keine Zahl); Text in der App über `VisuStructSimulationNotice` lokalisieren.
106+
public var inputErrorNotice: VisuStructSimulationNotice?
106107
public var lastTrace: String?
107-
public var message: String?
108+
/// Hinweis nach dem letzten Schritt (Ende, Fehler); `nil` während die Simulation normal läuft.
109+
public var notice: VisuStructSimulationNotice?
108110
/// Nach einem Fehler bleibt die Simulation auf dem fehlerhaften Schritt stehen, bis sie zurückgesetzt wird.
109111
public var isHalted: Bool
110112

@@ -113,22 +115,29 @@ public struct VisuStructSimulationState: Equatable, Sendable {
113115
variables: [String: VisuStructSimulationValue] = [:],
114116
outputLines: [String] = [],
115117
inputRequest: VisuStructSimulationInputRequest? = nil,
116-
inputError: String? = nil,
118+
inputErrorNotice: VisuStructSimulationNotice? = nil,
117119
lastTrace: String? = nil,
118-
message: String? = nil,
120+
notice: VisuStructSimulationNotice? = nil,
119121
isHalted: Bool = false
120122
) {
121123
self.stepIndex = stepIndex
122124
self.variables = variables
123125
self.outputLines = outputLines
124126
self.inputRequest = inputRequest
125-
self.inputError = inputError
127+
self.inputErrorNotice = inputErrorNotice
126128
self.lastTrace = lastTrace
127-
self.message = message
129+
self.notice = notice
128130
self.isHalted = isHalted
129131
}
130132
}
131133

134+
extension VisuStructSimulationState {
135+
/// Englischer Text von `notice` (Tests, Protokolle).
136+
public var message: String? { notice?.englishText }
137+
/// Englischer Text von `inputErrorNotice`.
138+
public var inputError: String? { inputErrorNotice?.englishText }
139+
}
140+
132141
public struct VisuStructSimulationEngine: Equatable, Sendable {
133142
public private(set) var steps: [VisuStructSimulationStep]
134143
public private(set) var state: VisuStructSimulationState
@@ -157,7 +166,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
157166
public mutating func step() {
158167
guard canStep else {
159168
if state.inputRequest == nil && !state.isHalted {
160-
state.message = "Simulation finished."
169+
state.notice = .finished
161170
}
162171
return
163172
}
@@ -196,13 +205,13 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
196205
return
197206
}
198207
if state.inputRequest != nil {
199-
state.message = nil
208+
state.notice = nil
200209
return
201210
}
202211
state.stepIndex += 1
203-
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
212+
state.notice = state.stepIndex >= steps.count ? .finished : nil
204213
} catch {
205-
state.message = error.localizedDescription
214+
state.notice = VisuStructSimulationNotice(error)
206215
state.isHalted = true
207216
}
208217
}
@@ -214,13 +223,13 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
214223
try assignValue(value, to: request.variableName)
215224
state.lastTrace = "\(request.variableName) = input -> \(value.description)"
216225
} catch {
217-
state.inputError = error.localizedDescription
226+
state.inputErrorNotice = VisuStructSimulationNotice(error)
218227
return
219228
}
220229
state.inputRequest = nil
221-
state.inputError = nil
230+
state.inputErrorNotice = nil
222231
state.stepIndex += 1
223-
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
232+
state.notice = state.stepIndex >= steps.count ? .finished : nil
224233
}
225234

226235
private static func parseInputValue(_ rawValue: String, as valueType: VisuStructSimulationValueType) throws -> VisuStructSimulationValue {
@@ -230,12 +239,12 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
230239
switch valueType {
231240
case .int:
232241
guard let value = Int(trimmed) else {
233-
throw SimulationError.invalidInput("Please enter a whole number.")
242+
throw SimulationError.invalidInput(.wholeNumberExpected)
234243
}
235244
return .int(value)
236245
case .double:
237246
guard let value = Double(trimmed) else {
238-
throw SimulationError.invalidInput("Please enter a number.")
247+
throw SimulationError.invalidInput(.numberExpected)
239248
}
240249
return .double(value)
241250
case .string:
@@ -346,7 +355,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
346355
steps.remove(at: state.stepIndex)
347356
steps.insert(contentsOf: branchSteps, at: state.stepIndex)
348357
state.lastTrace = "\(condition) -> \(result ? "true" : "false")"
349-
state.message = !branchSteps.isEmpty ? nil : (state.stepIndex >= steps.count ? "Simulation finished." : nil)
358+
state.notice = !branchSteps.isEmpty ? nil : (state.stepIndex >= steps.count ? .finished : nil)
350359
}
351360

352361
private mutating func executeSwitchSelection(_ step: VisuStructSimulationStep) throws {
@@ -366,7 +375,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
366375
steps.remove(at: state.stepIndex)
367376
steps.insert(contentsOf: caseSteps, at: state.stepIndex)
368377
state.lastTrace = "\(selector) -> \(selectedCase.name)"
369-
state.message = !caseSteps.isEmpty ? nil : (state.stepIndex >= steps.count ? "Simulation finished." : nil)
378+
state.notice = !caseSteps.isEmpty ? nil : (state.stepIndex >= steps.count ? .finished : nil)
370379
}
371380

372381
private mutating func executeForLoopStart(_ step: VisuStructSimulationStep) throws {
@@ -419,7 +428,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
419428
steps.insert(contentsOf: bodySteps + [incrementStep, conditionStep], at: state.stepIndex)
420429
} else {
421430
steps.remove(at: state.stepIndex)
422-
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
431+
state.notice = state.stepIndex >= steps.count ? .finished : nil
423432
}
424433
}
425434

@@ -449,10 +458,10 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
449458
)
450459
steps.remove(at: state.stepIndex)
451460
steps.insert(contentsOf: bodySteps + [conditionStep], at: state.stepIndex)
452-
state.message = !bodySteps.isEmpty ? nil : (state.stepIndex >= steps.count ? "Simulation finished." : nil)
461+
state.notice = !bodySteps.isEmpty ? nil : (state.stepIndex >= steps.count ? .finished : nil)
453462
} else {
454463
steps.remove(at: state.stepIndex)
455-
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
464+
state.notice = state.stepIndex >= steps.count ? .finished : nil
456465
}
457466
}
458467

@@ -469,7 +478,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
469478
)
470479
steps.remove(at: state.stepIndex)
471480
steps.insert(contentsOf: bodySteps + [conditionStep], at: state.stepIndex)
472-
state.message = !bodySteps.isEmpty ? nil : (state.stepIndex >= steps.count ? "Simulation finished." : nil)
481+
state.notice = !bodySteps.isEmpty ? nil : (state.stepIndex >= steps.count ? .finished : nil)
473482
}
474483

475484
private mutating func executeDoUntilCondition(_ step: VisuStructSimulationStep) throws {
@@ -491,7 +500,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
491500
steps.insert(contentsOf: bodySteps + [conditionStep], at: state.stepIndex)
492501
} else {
493502
steps.remove(at: state.stepIndex)
494-
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
503+
state.notice = state.stepIndex >= steps.count ? .finished : nil
495504
}
496505
}
497506

@@ -509,14 +518,14 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
509518
steps.remove(at: state.stepIndex)
510519
steps.insert(contentsOf: bodySteps + [loopStep], at: state.stepIndex)
511520
state.lastTrace = "true -> true"
512-
state.message = !bodySteps.isEmpty ? nil : "Infinite loop has no body."
521+
state.notice = !bodySteps.isEmpty ? nil : .infiniteLoopWithoutBody
513522
}
514523

515524
private mutating func execute(statement rawText: String) throws {
516525
let text = rawText.trimmingCharacters(in: .whitespacesAndNewlines)
517526
if text.lowercased().hasPrefix("input:") {
518527
state.inputRequest = try parseInputRequest(text)
519-
state.inputError = nil
528+
state.inputErrorNotice = nil
520529
return
521530
}
522531

@@ -1343,20 +1352,53 @@ private enum SimulationError: LocalizedError, Equatable {
13431352
case invalidExpression(String)
13441353
case unknownVariable(String)
13451354
case divisionByZero
1346-
case invalidInput(String)
1355+
case invalidInput(VisuStructSimulationNotice)
1356+
1357+
var notice: VisuStructSimulationNotice {
1358+
switch self {
1359+
case .unsupportedStatement(let text): return .unsupportedStatement(text)
1360+
case .invalidExpression(let text): return .invalidExpression(text)
1361+
case .unknownVariable(let name): return .unknownVariable(name)
1362+
case .divisionByZero: return .divisionByZero
1363+
case .invalidInput(let notice): return notice
1364+
}
1365+
}
1366+
1367+
var errorDescription: String? { notice.englishText }
1368+
}
1369+
1370+
/// Meldungen der Simulation, unabhängig von der Sprache; die App übersetzt sie.
1371+
public enum VisuStructSimulationNotice: Equatable, Sendable {
1372+
case finished
1373+
case infiniteLoopWithoutBody
1374+
case unsupportedStatement(String)
1375+
case invalidExpression(String)
1376+
case unknownVariable(String)
1377+
case divisionByZero
1378+
case wholeNumberExpected
1379+
case numberExpected
1380+
/// Fehler aus einer anderen Quelle, nur als Text bekannt.
1381+
case other(String)
1382+
1383+
init(_ error: Error) {
1384+
if let simulationError = error as? SimulationError {
1385+
self = simulationError.notice
1386+
} else {
1387+
self = .other(error.localizedDescription)
1388+
}
1389+
}
13471390

1348-
var errorDescription: String? {
1391+
public var englishText: String {
13491392
switch self {
1350-
case .unsupportedStatement(let text):
1351-
return "Unsupported statement: \(text)"
1352-
case .invalidExpression(let text):
1353-
return "Invalid expression: \(text)"
1354-
case .unknownVariable(let name):
1355-
return "Unknown variable: \(name)"
1356-
case .divisionByZero:
1357-
return "Division by zero."
1358-
case .invalidInput(let message):
1359-
return message
1393+
case .finished: return "Simulation finished."
1394+
case .infiniteLoopWithoutBody: return "Infinite loop has no body."
1395+
case .unsupportedStatement(let text): return "Unsupported statement: \(text)"
1396+
case .invalidExpression(let text): return "Invalid expression: \(text)"
1397+
case .unknownVariable(let name): return "Unknown variable: \(name)"
1398+
case .divisionByZero: return "Division by zero."
1399+
case .wholeNumberExpected: return "Please enter a whole number."
1400+
case .numberExpected: return "Please enter a number."
1401+
case .other(let text): return text
13601402
}
13611403
}
13621404
}

‎VisuStruct-iOS/VisuStructCore/Tests/VisuStructCoreTests/SimulationEngineTests.swift‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ final class SimulationEngineTests: XCTestCase {
3232
XCTAssertEqual(engine.state.inputRequest?.variableName, "n")
3333

3434
engine.provideInput("abc")
35-
XCTAssertNotNil(engine.state.inputError)
35+
XCTAssertEqual(engine.state.inputErrorNotice, .wholeNumberExpected)
3636
XCTAssertNotNil(engine.state.inputRequest)
3737

3838
engine.provideInput("21")
@@ -107,7 +107,18 @@ final class SimulationEngineTests: XCTestCase {
107107
])
108108
var engine = VisuStructSimulationEngine(document: document)
109109
engine.step()
110+
XCTAssertEqual(engine.state.notice, .divisionByZero)
110111
XCTAssertEqual(engine.state.message, "Division by zero.")
111112
XCTAssertNil(engine.state.variables["x"])
112113
}
114+
115+
func testFinishedNoticeAfterLastStep() {
116+
let document = VisuStructDocument(elements: [
117+
VisuStructElement(type: .anweisung, textLines: ["int x = 1"]),
118+
])
119+
var engine = VisuStructSimulationEngine(document: document)
120+
engine.step()
121+
XCTAssertEqual(engine.state.notice, .finished)
122+
XCTAssertEqual(engine.state.message, "Simulation finished.")
123+
}
113124
}

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/AppLanguage.swift‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,37 @@ enum AppLanguage: String, CaseIterable, Identifiable {
5151
return b
5252
}
5353

54+
/// Übersetzung zu `key`; fehlt sie in dieser Sprache, der englische Text (nie der Schlüssel selbst).
5455
func tr(_ key: String) -> String {
56+
let text: String
5557
if self == .javaLike {
56-
return NSLocalizedString(key, tableName: Self.javaStringsTable, bundle: Bundle.main, value: key, comment: "")
58+
text = NSLocalizedString(key, tableName: Self.javaStringsTable, bundle: Bundle.main, value: key, comment: "")
59+
} else {
60+
text = NSLocalizedString(key, tableName: nil, bundle: bundle, value: key, comment: "")
5761
}
58-
return NSLocalizedString(key, tableName: nil, bundle: bundle, value: key, comment: "")
62+
if text == key, self != .english {
63+
return AppLanguage.english.tr(key)
64+
}
65+
return text
66+
}
67+
68+
/// Sprache des Geräts (bzw. die für VisuStruct in den Systemeinstellungen gewählte), sofern die App sie anbietet; sonst Englisch.
69+
static var deviceDefault: AppLanguage {
70+
for code in Bundle.main.preferredLocalizations {
71+
if let lang = AppLanguage(rawValue: code), lang != .javaLike { return lang }
72+
}
73+
return .english
74+
}
75+
76+
/// Erster Start ohne gespeicherte Sprache: Gerätesprache übernehmen, damit auch die Systemeinstellungen sie anzeigen.
77+
static func storeDeviceDefaultIfNeeded() {
78+
guard UserDefaults.standard.string(forKey: storageKey) == nil else { return }
79+
UserDefaults.standard.set(deviceDefault.rawValue, forKey: storageKey)
5980
}
6081

6182
/// Für `ContentView.init`, bevor `Environment` steht.
6283
static func currentFromDefaults() -> AppLanguage {
63-
let code = UserDefaults.standard.string(forKey: storageKey) ?? AppLanguage.english.rawValue
84+
let code = UserDefaults.standard.string(forKey: storageKey) ?? AppLanguage.deviceDefault.rawValue
6485
return AppLanguage(rawValue: code) ?? .english
6586
}
6687
}

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/ContentView.swift‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,10 @@ struct ContentView: View {
771771
@ViewBuilder
772772
private var simulationDetailView: some View {
773773
GeometryReader { geo in
774-
let useColumns = geo.size.width >= 760
774+
// Seitenspalte nur im Querformat; hochkant wäre sie gequetscht und unter dem Diagramm bliebe Platz frei.
775+
let useColumns = geo.size.width >= 760 && geo.size.width > geo.size.height
776+
// iPad hochkant: Diagramm oben, Steuerfeld darunter über die volle Breite.
777+
let useWideStack = !useColumns && geo.size.width >= 600 && geo.size.height >= 700
775778
let panelWidth = min(max(geo.size.width * 0.32, 280), 360)
776779

777780
if useColumns {
@@ -792,6 +795,24 @@ struct ContentView: View {
792795
.frame(width: panelWidth)
793796
.frame(maxHeight: .infinity, alignment: .top)
794797
}
798+
} else if useWideStack {
799+
VStack(alignment: .leading, spacing: 16) {
800+
simulationCanvasBlock
801+
.frame(height: geo.size.height * 0.5)
802+
SimulationStatePanel(
803+
engine: simulationEngine,
804+
isPlaying: isSimulationPlaying,
805+
inputText: $simulationInputText,
806+
highlightModeRaw: $simulationHighlightModeRaw,
807+
onPlay: playSimulation,
808+
onPause: pauseSimulation,
809+
onStep: stepSimulation,
810+
onSubmitInput: submitSimulationInput,
811+
onStop: resetSimulation,
812+
usesWideLayout: true
813+
)
814+
.frame(maxHeight: .infinity)
815+
}
795816
} else {
796817
ScrollView {
797818
VStack(alignment: .leading, spacing: 16) {
@@ -2007,7 +2028,9 @@ struct ContentView: View {
20072028
tabs[i].lineEditorText = el.textLines.joined(separator: "\n")
20082029
tabsModel.tabs = tabs
20092030

2010-
if !isPhone, focusedField == nil, el.type != .forSchleife {
2031+
if DiagramInspectorAutoFocus.shouldFocusOnSelection,
2032+
focusedField == nil,
2033+
el.type != .forSchleife {
20112034
focusedField = .textEditor
20122035
editorFocusToken = UUID()
20132036
}

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/Editing/CodeLineEditor.swift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ private struct CodeLineEditorUIKit: UIViewRepresentable {
110110
tv.commitOnReturn = commitOnReturn
111111
tv.onCommit = onCommit
112112
tv.delegate = context.coordinator
113+
// Beim Erzeugen nicht fokussieren: Nur ein späterer Token-Wechsel holt die Tastatur.
114+
context.coordinator.lastFocusToken = focusToken
113115
return tv
114116
}
115117

0 commit comments

Comments
 (0)