Skip to content

Commit 57bf3a5

Browse files
Harden runtime error handling
Amp-Thread-ID: https://ampcode.com/threads/T-019c3793-dd4d-73d5-8bfb-83ac69c47642 Co-authored-by: Amp <amp@ampcode.com>
1 parent 8daabec commit 57bf3a5

7 files changed

Lines changed: 82 additions & 64 deletions

File tree

IOS_PRODUCT_COMPLETION_PLAN.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ Die iOS-Version soll sich in der **Spiellogik** wie die Android-Version verhalte
6060

6161
**Android-seitig vorhanden, iOS-seitig unvollständig/fehlend:**
6262
- [x] Hauptmenü
63-
- [ ] Settings (entfernt, da aktuell kein Audio im iOS-Build aktiv ist)
6463
- [x] Credits
6564
- [x] Tutorial/Onboarding
6665
- [x] Levelmode-Completion
@@ -123,7 +122,7 @@ Die iOS-Version soll sich in der **Spiellogik** wie die Android-Version verhalte
123122
## Empfohlene Reihenfolge
124123
1. Crash-Fixes + 100-Level-Parität
125124
2. JustPlay vollständig (Timer/Score/Lost/Score/Highscore)
126-
3. Fehlende Flows (Menü/Settings/Tutorial/Credits/Completion)
125+
3. Fehlende Flows (Menü/Tutorial/Credits/Completion)
127126
4. Release-Härtung + Testausbau
128127
5. Build-/Store-Modernisierung und finaler Release-Check
129128

SOPA/AppDelegate.swift

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88

99
import UIKit
1010
import CoreData
11+
import OSLog
1112

1213
@UIApplicationMain
1314
class AppDelegate: UIResponder, UIApplicationDelegate {
15+
private let logger = Logger(subsystem: "SOPA", category: "AppDelegate")
1416

1517
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
1618
// Override point for customization after application launch.
@@ -56,20 +58,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
5658
error conditions that could cause the creation of the store to fail.
5759
*/
5860
let container = NSPersistentContainer(name: "SOPA")
59-
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
61+
container.loadPersistentStores(completionHandler: { [self] (storeDescription, error) in
6062
if let error = error as NSError? {
61-
// Replace this implementation with code to handle the error appropriately.
62-
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
63-
64-
/*
65-
Typical reasons for an error here include:
66-
* The parent directory does not exist, cannot be created, or disallows writing.
67-
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
68-
* The device is out of space.
69-
* The store could not be migrated to the current model version.
70-
Check the error message to determine what the actual problem was.
71-
*/
72-
fatalError("Unresolved error \(error), \(error.userInfo)")
63+
logger.error("Failed to load persistent store: \(error.localizedDescription)")
7364
}
7465
})
7566
return container
@@ -83,10 +74,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
8374
do {
8475
try context.save()
8576
} catch {
86-
// Replace this implementation with code to handle the error appropriately.
87-
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
88-
let nserror = error as NSError
89-
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
77+
logger.error("Failed to save CoreData context: \(error.localizedDescription)")
9078
}
9179
}
9280
}

SOPA/Info.plist

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,8 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5-
<key>LSSupportsOpeningDocumentsInPlace</key>
6-
<true/>
7-
<key>UIFileSharingEnabled</key>
8-
<true/>
9-
<key>CFBundleDevelopmentRegion</key>
10-
<string>$(DEVELOPMENT_LANGUAGE)</string>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>$(DEVELOPMENT_LANGUAGE)</string>
117
<key>CFBundleExecutable</key>
128
<string>$(EXECUTABLE_NAME)</string>
139
<key>CFBundleIdentifier</key>
@@ -47,12 +43,8 @@
4743
</array>
4844
</dict>
4945
</dict>
50-
<key>UIRequiredDeviceCapabilities</key>
51-
<array>
52-
<string>armv7</string>
53-
</array>
54-
<key>UIStatusBarHidden</key>
55-
<true/>
46+
<key>UIStatusBarHidden</key>
47+
<true/>
5648
<key>UISupportedInterfaceOrientations</key>
5749
<array>
5850
<string>UIInterfaceOrientationPortrait</string>

SOPA/database/LevelInfoDataSource.swift

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,27 @@
77
//
88
import CoreData
99
import Foundation
10+
import OSLog
1011
public class LevelInfoDataSource {
1112
let appDelegate: AppDelegate
1213
let context: NSManagedObjectContext
14+
private let logger = Logger(subsystem: "SOPA", category: "LevelInfoDataSource")
1315
init(appDelegate: AppDelegate){
1416
self.appDelegate = appDelegate
1517
self.context = appDelegate.persistentContainer.viewContext
1618
}
1719

1820
func createLevelInfo(levelInfo: LevelInfo) -> LevelInfo {
19-
if(getLevelInfoById(id: levelInfo.levelId) != nil) {
20-
fatalError()
21+
if getLevelInfoById(id: levelInfo.levelId) != nil {
22+
logger.error("LevelInfo already exists for id \(levelInfo.levelId)")
23+
return levelInfo
2124
}
2225
let entity = NSEntityDescription.entity(forEntityName: "LevelInfo", in: context)
23-
let newLevelInfo = NSManagedObject(entity: entity!, insertInto: context)
26+
guard let entity = entity else {
27+
logger.error("Missing LevelInfo entity description")
28+
return levelInfo
29+
}
30+
let newLevelInfo = NSManagedObject(entity: entity, insertInto: context)
2431
newLevelInfo.setValue(levelInfo.levelId, forKey: "id")
2532
newLevelInfo.setValue(levelInfo.fewestMoves, forKey: "fewest_moves")
2633
newLevelInfo.setValue(levelInfo.locked, forKey: "locked")
@@ -36,17 +43,22 @@ public class LevelInfoDataSource {
3643
do {
3744
let fetchedLevelInfos = try context.fetch(oldLevelFetch)
3845
if(fetchedLevelInfos.count != 1) {
39-
fatalError()
46+
logger.error("Expected 1 LevelInfo, got \(fetchedLevelInfos.count) for id \(levelInfo.levelId)")
47+
return levelInfo
48+
}
49+
guard let oldLevel = fetchedLevelInfos[0] as? LevelInfoMO else {
50+
logger.error("LevelInfo fetch returned unexpected type")
51+
return levelInfo
4052
}
41-
let oldLevel = fetchedLevelInfos[0] as! LevelInfoMO
4253
oldLevel.stars = Int16(levelInfo.stars)
4354
oldLevel.fewest_moves = Int16(levelInfo.fewestMoves)
4455
oldLevel.locked = levelInfo.locked
4556
oldLevel.time = levelInfo.time
4657
appDelegate.saveContext()
4758
return levelInfo
4859
} catch {
49-
fatalError()
60+
logger.error("Failed to update LevelInfo for id \(levelInfo.levelId): \(error.localizedDescription)")
61+
return levelInfo
5062
}
5163
}
5264

@@ -61,7 +73,8 @@ public class LevelInfoDataSource {
6173
}
6274
return levelInfos
6375
} catch {
64-
fatalError()
76+
logger.error("Failed to fetch LevelInfo list: \(error.localizedDescription)")
77+
return []
6578
}
6679
}
6780

@@ -77,7 +90,8 @@ public class LevelInfoDataSource {
7790
let levelInfo = LevelInfo(levelInfoMO: levelInfoMO)
7891
return levelInfo
7992
} catch {
80-
fatalError()
93+
logger.error("Failed to fetch LevelInfo for id \(id): \(error.localizedDescription)")
94+
return nil
8195
}
8296
}
8397

@@ -97,7 +111,8 @@ public class LevelInfoDataSource {
97111
}
98112
return LevelInfo(levelInfoMO: unlockedLevelsMO[0])
99113
} catch {
100-
fatalError()
114+
logger.error("Failed to fetch last unlocked LevelInfo: \(error.localizedDescription)")
115+
return nil
101116
}
102117
}
103118

@@ -109,7 +124,7 @@ public class LevelInfoDataSource {
109124
context.delete(levelInfoMO)
110125
}
111126
} catch {
112-
fatalError()
127+
logger.error("Failed to delete LevelInfos: \(error.localizedDescription)")
113128
}
114129
}
115130

@@ -134,7 +149,7 @@ public class LevelInfoDataSource {
134149
}
135150
appDelegate.saveContext()
136151
} catch {
137-
fatalError()
152+
logger.error("Failed to save JustPlay score: \(error.localizedDescription)")
138153
}
139154
}
140155

@@ -152,7 +167,8 @@ public class LevelInfoDataSource {
152167
let solvedLevels = Int(currentBest.value(forKey: "solvedLevels") as? Int64 ?? 0)
153168
return JustPlayScore(points: points, solvedLevels: solvedLevels)
154169
} catch {
155-
fatalError()
170+
logger.error("Failed to fetch JustPlay high score: \(error.localizedDescription)")
171+
return nil
156172
}
157173
}
158174
}

SOPA/helper/LevelCreator.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,13 @@ class LevelCreator {
8989
startY = Int.random(in: 1..<height - 1)
9090
direction = 3
9191
tiles[startX][startY] = startTile
92-
92+
9393
default:
94-
fatalError()
94+
startTile = Tile(top: false, bottom: true, left: false, right: false, tileType: TileType.START, shortcut: "s")
95+
startX = 1
96+
startY = 0
97+
direction = 0
98+
tiles[startX][startY] = startTile
9599
}
96100

97101
var x = startX
@@ -137,9 +141,9 @@ class LevelCreator {
137141
case 3:
138142
tiles[x][y].left = true
139143
tiles[xNew][yNew].right = true
140-
144+
141145
default:
142-
fatalError()
146+
break
143147
}
144148

145149
x = xNew

SOPA/helper/LevelServiceImp.swift

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@
77
//
88

99
import Foundation
10+
import OSLog
1011
class LevelServiceImpl: LevelService {
12+
private let logger = Logger(subsystem: "SOPA", category: "LevelService")
1113
func getLevelById(id: Int) -> Level? {
1214
let level: Level = levelFileService.getLevel(id: id)
13-
level.levelInfo = levelInfoDataSource.getLevelInfoById(id: level.id!)
15+
guard let levelId = level.id else {
16+
logger.error("Loaded level without id for requested id \(id)")
17+
return nil
18+
}
19+
level.levelInfo = levelInfoDataSource.getLevelInfoById(id: levelId)
1420
return level
1521
}
1622

@@ -75,26 +81,41 @@ class LevelServiceImpl: LevelService {
7581
}
7682

7783
func calculateLevelResult(level: Level) -> LevelResult {
78-
let stars = starCalculator.getStars(neededMoves: level.movesCounter, minimumMoves: level.minimumMovesToSolve!)
79-
return LevelResult(levelId: level.id!, moveCount: level.movesCounter, stars: stars, time: Double.nan)
84+
guard let minimumMoves = level.minimumMovesToSolve else {
85+
logger.error("Missing minimum moves for level \(level.id ?? -1)")
86+
return LevelResult(levelId: level.id ?? 0, moveCount: level.movesCounter, stars: 0, time: Double.nan)
87+
}
88+
guard let levelId = level.id else {
89+
logger.error("Missing level id when calculating result")
90+
return LevelResult(levelId: 0, moveCount: level.movesCounter, stars: 0, time: Double.nan)
91+
}
92+
let stars = starCalculator.getStars(neededMoves: level.movesCounter, minimumMoves: minimumMoves)
93+
return LevelResult(levelId: levelId, moveCount: level.movesCounter, stars: stars, time: Double.nan)
8094
}
8195

8296
func persistLevelResult(levelResult: LevelResult) -> LevelInfo {
83-
let levelInfo = levelInfoDataSource.getLevelInfoById(id: levelResult.levelId)
84-
85-
if levelInfo!.fewestMoves == -1 || levelInfo!.fewestMoves > levelResult.moveCount {
86-
levelInfo?.fewestMoves = levelResult.moveCount
87-
levelInfo?.stars = levelResult.stars
97+
guard let levelInfo = levelInfoDataSource.getLevelInfoById(id: levelResult.levelId) else {
98+
logger.error("Missing LevelInfo for level \(levelResult.levelId); creating fallback entry")
99+
let newLevelInfo = LevelInfo(levelId: levelResult.levelId, locked: false, fewestMoves: levelResult.moveCount, stars: levelResult.stars, time: levelResult.time)
100+
return levelInfoDataSource.createLevelInfo(levelInfo: newLevelInfo)
88101
}
89-
levelInfo?.time = levelResult.time
90102

91-
return levelInfoDataSource.updateLevelInfo(levelInfo: levelInfo!)
103+
if levelInfo.fewestMoves == -1 || levelInfo.fewestMoves > levelResult.moveCount {
104+
levelInfo.fewestMoves = levelResult.moveCount
105+
levelInfo.stars = levelResult.stars
106+
}
107+
levelInfo.time = levelResult.time
108+
109+
return levelInfoDataSource.updateLevelInfo(levelInfo: levelInfo)
92110
}
93111

94112
func unlockLevel(levelId: Int) {
95-
let levelInfo = levelInfoDataSource.getLevelInfoById(id: levelId)
96-
levelInfo?.locked = false
97-
_ = levelInfoDataSource.updateLevelInfo(levelInfo: levelInfo!)
113+
guard let levelInfo = levelInfoDataSource.getLevelInfoById(id: levelId) else {
114+
logger.error("Missing LevelInfo when unlocking level \(levelId)")
115+
return
116+
}
117+
levelInfo.locked = false
118+
_ = levelInfoDataSource.updateLevelInfo(levelInfo: levelInfo)
98119
}
99120

100121
func deleteAllLevelInfos() {

SOPA/model/game/Level.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,8 @@ class Level {
2323
init(level: Level) {
2424
copyTilesFrom(tiles: level.tiles)
2525
self.id = level.id
26-
if level.levelInfo == nil {
27-
levelInfo = nil;
28-
} else {
29-
levelInfo = LevelInfo(levelInfo: level.levelInfo!);
26+
if let info = level.levelInfo {
27+
levelInfo = LevelInfo(levelInfo: info)
3028
}
3129
minimumMovesToSolve = level.minimumMovesToSolve;
3230
startX = level.startX;

0 commit comments

Comments
 (0)