Refactor app to use Core Data.
This commit is contained in:
parent
508e8e28de
commit
46f3a93100
11
README.md
11
README.md
|
@ -66,10 +66,11 @@ I'm currently seeking freelance, remote opportunities as an iOS developer! If yo
|
||||||
- **Day 57:** [_Project 12: Core Data_ (Part One)](./day-057/)
|
- **Day 57:** [_Project 12: Core Data_ (Part One)](./day-057/)
|
||||||
- **Day 58:** [_Project 12: Core Data_ (Part Two)](./day-058/)
|
- **Day 58:** [_Project 12: Core Data_ (Part Two)](./day-058/)
|
||||||
- **Day 59:** [_Project 12: Core Data_ (Part Three)](./day-059/)
|
- **Day 59:** [_Project 12: Core Data_ (Part Three)](./day-059/)
|
||||||
|
- **Day 60:** [Milestone for Projects 10-12 (Part One)](./day-060/)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
- **Day 60:** [Milestone for Projects 10-12)](./day-060/)
|
- **Day 61:** [Milestone for Projects 10-12 (Part Two)](./day-061/)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,7 +97,7 @@ I'm currently seeking freelance, remote opportunities as an iOS developer! If yo
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
- [Milestone 1: RockPaperQuizzers](./day-025/RockPaperQuizzers/)
|
- [Milestone Project 1: RockPaperQuizzers](./day-025/RockPaperQuizzers/)
|
||||||
|
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<img src="./day-025/RockPaperQuizzers/Screenshots/recording-1.gif" width="300px"/>
|
<img src="./day-025/RockPaperQuizzers/Screenshots/recording-1.gif" width="300px"/>
|
||||||
|
@ -161,3 +162,9 @@ I'm currently seeking freelance, remote opportunities as an iOS developer! If yo
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<img src="./day-053/Projects/Bookworm/Screenshots/day-56-recording-1.gif" width="300px"/>
|
<img src="./day-053/Projects/Bookworm/Screenshots/day-56-recording-1.gif" width="300px"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
- [Project 12: Exploring Core Data](./day-057/Projects/ExploringCoreData)
|
||||||
|
|
||||||
|
|
||||||
|
- [Milestone Project 4: SpaceX Payload Stats](./day-060/Project/SpaceXPayloadStats/)
|
||||||
|
|
|
@ -7,6 +7,19 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
F30B3646238D88480041AC59 /* CodingUserInfoKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30B3645238D88480041AC59 /* CodingUserInfoKey+Extensions.swift */; };
|
||||||
|
F352423B238EE7B2009DF1F9 /* Mission+FetchHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F352423A238EE7B2009DF1F9 /* Mission+FetchHelpers.swift */; };
|
||||||
|
F352423D238EE80C009DF1F9 /* Payload+FetchHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F352423C238EE80C009DF1F9 /* Payload+FetchHelpers.swift */; };
|
||||||
|
F3524240238EE983009DF1F9 /* CypherPoetCoreDataKit in Frameworks */ = {isa = PBXBuildFile; productRef = F352423F238EE983009DF1F9 /* CypherPoetCoreDataKit */; };
|
||||||
|
F3A61A7C238C98EE00279192 /* Mission+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A61A7A238C98EE00279192 /* Mission+CoreDataClass.swift */; };
|
||||||
|
F3A61A7D238C98EE00279192 /* Mission+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A61A7B238C98EE00279192 /* Mission+CoreDataProperties.swift */; };
|
||||||
|
F3A61A80238C994900279192 /* Payload+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A61A7E238C994900279192 /* Payload+CoreDataClass.swift */; };
|
||||||
|
F3A61A81238C994900279192 /* Payload+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A61A7F238C994900279192 /* Payload+CoreDataProperties.swift */; };
|
||||||
|
F3A61A85238C996C00279192 /* OrbitParams+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A61A83238C996C00279192 /* OrbitParams+CoreDataClass.swift */; };
|
||||||
|
F3A61A86238C996C00279192 /* OrbitParams+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A61A84238C996C00279192 /* OrbitParams+CoreDataProperties.swift */; };
|
||||||
|
F3A61A88238C9D8A00279192 /* Mission+Computeds.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A61A87238C9D8A00279192 /* Mission+Computeds.swift */; };
|
||||||
|
F3A61A8A238C9E1100279192 /* Payload+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A61A89238C9E1100279192 /* Payload+Comparable.swift */; };
|
||||||
|
F3A61A8C238D67DF00279192 /* Payload+Computeds.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A61A8B238D67DF00279192 /* Payload+Computeds.swift */; };
|
||||||
F3F9F3A523892C0300864243 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3A423892C0300864243 /* AppDelegate.swift */; };
|
F3F9F3A523892C0300864243 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3A423892C0300864243 /* AppDelegate.swift */; };
|
||||||
F3F9F3A723892C0300864243 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3A623892C0300864243 /* SceneDelegate.swift */; };
|
F3F9F3A723892C0300864243 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3A623892C0300864243 /* SceneDelegate.swift */; };
|
||||||
F3F9F3AB23892C0800864243 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F3F9F3AA23892C0800864243 /* Assets.xcassets */; };
|
F3F9F3AB23892C0800864243 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F3F9F3AA23892C0800864243 /* Assets.xcassets */; };
|
||||||
|
@ -18,9 +31,6 @@
|
||||||
F3F9F3CA238953C200864243 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3C8238953C200864243 /* AppState.swift */; };
|
F3F9F3CA238953C200864243 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3C8238953C200864243 /* AppState.swift */; };
|
||||||
F3F9F3CD2389556700864243 /* MissionsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3CC2389556700864243 /* MissionsState.swift */; };
|
F3F9F3CD2389556700864243 /* MissionsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3CC2389556700864243 /* MissionsState.swift */; };
|
||||||
F3F9F3CF2389558000864243 /* PayloadsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3CE2389558000864243 /* PayloadsState.swift */; };
|
F3F9F3CF2389558000864243 /* PayloadsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3CE2389558000864243 /* PayloadsState.swift */; };
|
||||||
F3F9F3D12389565000864243 /* Mission.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3D02389565000864243 /* Mission.swift */; };
|
|
||||||
F3F9F3D52389567D00864243 /* Payload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3D42389567D00864243 /* Payload.swift */; };
|
|
||||||
F3F9F3D72389611800864243 /* Payload+OrbitParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3D62389611800864243 /* Payload+OrbitParams.swift */; };
|
|
||||||
F3F9F3DA238966E000864243 /* MissionsListContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3D9238966E000864243 /* MissionsListContainerView.swift */; };
|
F3F9F3DA238966E000864243 /* MissionsListContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3D9238966E000864243 /* MissionsListContainerView.swift */; };
|
||||||
F3F9F3DC2389743600864243 /* MissionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3DB2389743600864243 /* MissionsListView.swift */; };
|
F3F9F3DC2389743600864243 /* MissionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3DB2389743600864243 /* MissionsListView.swift */; };
|
||||||
F3F9F3DF238976E200864243 /* MissionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3DE238976E200864243 /* MissionDetailsView.swift */; };
|
F3F9F3DF238976E200864243 /* MissionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3DE238976E200864243 /* MissionDetailsView.swift */; };
|
||||||
|
@ -33,12 +43,25 @@
|
||||||
F3F9F3F2238AF96600864243 /* NumberFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3F1238AF96500864243 /* NumberFormatters.swift */; };
|
F3F9F3F2238AF96600864243 /* NumberFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3F1238AF96500864243 /* NumberFormatters.swift */; };
|
||||||
F3F9F3F7238B001600864243 /* InfoHeaderTextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3F5238B001600864243 /* InfoHeaderTextStyle.swift */; };
|
F3F9F3F7238B001600864243 /* InfoHeaderTextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3F5238B001600864243 /* InfoHeaderTextStyle.swift */; };
|
||||||
F3F9F3F8238B001600864243 /* Text+InfoHeaderStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3F6238B001600864243 /* Text+InfoHeaderStyle.swift */; };
|
F3F9F3F8238B001600864243 /* Text+InfoHeaderStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3F6238B001600864243 /* Text+InfoHeaderStyle.swift */; };
|
||||||
F3F9F3FA238B036F00864243 /* Payload+Computeds.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3F9238B036E00864243 /* Payload+Computeds.swift */; };
|
|
||||||
F3F9F3FC238B115A00864243 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F3F9F3FB238B115A00864243 /* Colors.xcassets */; };
|
F3F9F3FC238B115A00864243 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F3F9F3FB238B115A00864243 /* Colors.xcassets */; };
|
||||||
F3F9F3FE238BF79A00864243 /* MissionDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3FD238BF79900864243 /* MissionDetailsViewModel.swift */; };
|
F3F9F3FE238BF79A00864243 /* MissionDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3FD238BF79900864243 /* MissionDetailsViewModel.swift */; };
|
||||||
|
F3F9F400238C357E00864243 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3FF238C357E00864243 /* CoreDataManager.swift */; };
|
||||||
|
F3F9F403238C78F600864243 /* SpaceXPayloadStats.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F401238C78F500864243 /* SpaceXPayloadStats.xcdatamodeld */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
F30B3645238D88480041AC59 /* CodingUserInfoKey+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodingUserInfoKey+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
F352423A238EE7B2009DF1F9 /* Mission+FetchHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mission+FetchHelpers.swift"; sourceTree = "<group>"; };
|
||||||
|
F352423C238EE80C009DF1F9 /* Payload+FetchHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Payload+FetchHelpers.swift"; sourceTree = "<group>"; };
|
||||||
|
F3A61A7A238C98EE00279192 /* Mission+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mission+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
|
F3A61A7B238C98EE00279192 /* Mission+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mission+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||||
|
F3A61A7E238C994900279192 /* Payload+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Payload+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
|
F3A61A7F238C994900279192 /* Payload+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Payload+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||||
|
F3A61A83238C996C00279192 /* OrbitParams+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrbitParams+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
|
F3A61A84238C996C00279192 /* OrbitParams+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrbitParams+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||||
|
F3A61A87238C9D8A00279192 /* Mission+Computeds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mission+Computeds.swift"; sourceTree = "<group>"; };
|
||||||
|
F3A61A89238C9E1100279192 /* Payload+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Payload+Comparable.swift"; sourceTree = "<group>"; };
|
||||||
|
F3A61A8B238D67DF00279192 /* Payload+Computeds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Payload+Computeds.swift"; sourceTree = "<group>"; };
|
||||||
F3F9F3A123892C0300864243 /* SpaceXPayloadStats.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SpaceXPayloadStats.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
F3F9F3A123892C0300864243 /* SpaceXPayloadStats.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SpaceXPayloadStats.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
F3F9F3A423892C0300864243 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
F3F9F3A423892C0300864243 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
F3F9F3A623892C0300864243 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
F3F9F3A623892C0300864243 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
@ -50,9 +73,6 @@
|
||||||
F3F9F3C8238953C200864243 /* AppState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
|
F3F9F3C8238953C200864243 /* AppState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
|
||||||
F3F9F3CC2389556700864243 /* MissionsState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MissionsState.swift; sourceTree = "<group>"; };
|
F3F9F3CC2389556700864243 /* MissionsState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MissionsState.swift; sourceTree = "<group>"; };
|
||||||
F3F9F3CE2389558000864243 /* PayloadsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayloadsState.swift; sourceTree = "<group>"; };
|
F3F9F3CE2389558000864243 /* PayloadsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayloadsState.swift; sourceTree = "<group>"; };
|
||||||
F3F9F3D02389565000864243 /* Mission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mission.swift; sourceTree = "<group>"; };
|
|
||||||
F3F9F3D42389567D00864243 /* Payload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Payload.swift; sourceTree = "<group>"; };
|
|
||||||
F3F9F3D62389611800864243 /* Payload+OrbitParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Payload+OrbitParams.swift"; sourceTree = "<group>"; };
|
|
||||||
F3F9F3D9238966E000864243 /* MissionsListContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissionsListContainerView.swift; sourceTree = "<group>"; };
|
F3F9F3D9238966E000864243 /* MissionsListContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissionsListContainerView.swift; sourceTree = "<group>"; };
|
||||||
F3F9F3DB2389743600864243 /* MissionsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissionsListView.swift; sourceTree = "<group>"; };
|
F3F9F3DB2389743600864243 /* MissionsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissionsListView.swift; sourceTree = "<group>"; };
|
||||||
F3F9F3DE238976E200864243 /* MissionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissionDetailsView.swift; sourceTree = "<group>"; };
|
F3F9F3DE238976E200864243 /* MissionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissionDetailsView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -65,9 +85,10 @@
|
||||||
F3F9F3F1238AF96500864243 /* NumberFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatters.swift; sourceTree = "<group>"; };
|
F3F9F3F1238AF96500864243 /* NumberFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatters.swift; sourceTree = "<group>"; };
|
||||||
F3F9F3F5238B001600864243 /* InfoHeaderTextStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoHeaderTextStyle.swift; sourceTree = "<group>"; };
|
F3F9F3F5238B001600864243 /* InfoHeaderTextStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoHeaderTextStyle.swift; sourceTree = "<group>"; };
|
||||||
F3F9F3F6238B001600864243 /* Text+InfoHeaderStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+InfoHeaderStyle.swift"; sourceTree = "<group>"; };
|
F3F9F3F6238B001600864243 /* Text+InfoHeaderStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+InfoHeaderStyle.swift"; sourceTree = "<group>"; };
|
||||||
F3F9F3F9238B036E00864243 /* Payload+Computeds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Payload+Computeds.swift"; sourceTree = "<group>"; };
|
|
||||||
F3F9F3FB238B115A00864243 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = "<group>"; };
|
F3F9F3FB238B115A00864243 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = "<group>"; };
|
||||||
F3F9F3FD238BF79900864243 /* MissionDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissionDetailsViewModel.swift; sourceTree = "<group>"; };
|
F3F9F3FD238BF79900864243 /* MissionDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissionDetailsViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
F3F9F3FF238C357E00864243 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = "<group>"; };
|
||||||
|
F3F9F402238C78F600864243 /* SpaceXPayloadStats.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SpaceXPayloadStats.xcdatamodel; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -76,6 +97,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
F3F9F3C2238952F200864243 /* CypherPoetNetStack in Frameworks */,
|
F3F9F3C2238952F200864243 /* CypherPoetNetStack in Frameworks */,
|
||||||
|
F3524240238EE983009DF1F9 /* CypherPoetCoreDataKit in Frameworks */,
|
||||||
F3F9F3BF238952C500864243 /* CypherPoetSwiftUIKit in Frameworks */,
|
F3F9F3BF238952C500864243 /* CypherPoetSwiftUIKit in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -83,6 +105,23 @@
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
F30B3644238D88340041AC59 /* Extensions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
F30B3645238D88480041AC59 /* CodingUserInfoKey+Extensions.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
F3A61A82238C994E00279192 /* Orbit Params */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
F3A61A83238C996C00279192 /* OrbitParams+CoreDataClass.swift */,
|
||||||
|
F3A61A84238C996C00279192 /* OrbitParams+CoreDataProperties.swift */,
|
||||||
|
);
|
||||||
|
path = "Orbit Params";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
F3F9F39823892C0300864243 = {
|
F3F9F39823892C0300864243 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -130,6 +169,8 @@
|
||||||
children = (
|
children = (
|
||||||
F3F9F3C42389531800864243 /* Models */,
|
F3F9F3C42389531800864243 /* Models */,
|
||||||
F3F9F3C32389531400864243 /* State */,
|
F3F9F3C32389531400864243 /* State */,
|
||||||
|
F3F9F3FF238C357E00864243 /* CoreDataManager.swift */,
|
||||||
|
F3F9F401238C78F500864243 /* SpaceXPayloadStats.xcdatamodeld */,
|
||||||
);
|
);
|
||||||
path = Data;
|
path = Data;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -137,6 +178,7 @@
|
||||||
F3F9F3B923894FE700864243 /* Reusables */ = {
|
F3F9F3B923894FE700864243 /* Reusables */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
F30B3644238D88340041AC59 /* Extensions */,
|
||||||
F3F9F3F3238AFFEB00864243 /* Styles */,
|
F3F9F3F3238AFFEB00864243 /* Styles */,
|
||||||
F3F9F3EE238AB00900864243 /* Formatters */,
|
F3F9F3EE238AB00900864243 /* Formatters */,
|
||||||
F3F9F3BA23894FEB00864243 /* Views */,
|
F3F9F3BA23894FEB00864243 /* Views */,
|
||||||
|
@ -182,6 +224,7 @@
|
||||||
F3F9F3C42389531800864243 /* Models */ = {
|
F3F9F3C42389531800864243 /* Models */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
F3A61A82238C994E00279192 /* Orbit Params */,
|
||||||
F3F9F3D22389565900864243 /* Mission */,
|
F3F9F3D22389565900864243 /* Mission */,
|
||||||
F3F9F3D32389565D00864243 /* Payload */,
|
F3F9F3D32389565D00864243 /* Payload */,
|
||||||
);
|
);
|
||||||
|
@ -199,7 +242,10 @@
|
||||||
F3F9F3D22389565900864243 /* Mission */ = {
|
F3F9F3D22389565900864243 /* Mission */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
F3F9F3D02389565000864243 /* Mission.swift */,
|
F3A61A7A238C98EE00279192 /* Mission+CoreDataClass.swift */,
|
||||||
|
F3A61A7B238C98EE00279192 /* Mission+CoreDataProperties.swift */,
|
||||||
|
F352423A238EE7B2009DF1F9 /* Mission+FetchHelpers.swift */,
|
||||||
|
F3A61A87238C9D8A00279192 /* Mission+Computeds.swift */,
|
||||||
);
|
);
|
||||||
path = Mission;
|
path = Mission;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -207,9 +253,11 @@
|
||||||
F3F9F3D32389565D00864243 /* Payload */ = {
|
F3F9F3D32389565D00864243 /* Payload */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
F3F9F3D42389567D00864243 /* Payload.swift */,
|
F3A61A7E238C994900279192 /* Payload+CoreDataClass.swift */,
|
||||||
F3F9F3F9238B036E00864243 /* Payload+Computeds.swift */,
|
F3A61A7F238C994900279192 /* Payload+CoreDataProperties.swift */,
|
||||||
F3F9F3D62389611800864243 /* Payload+OrbitParams.swift */,
|
F352423C238EE80C009DF1F9 /* Payload+FetchHelpers.swift */,
|
||||||
|
F3A61A89238C9E1100279192 /* Payload+Comparable.swift */,
|
||||||
|
F3A61A8B238D67DF00279192 /* Payload+Computeds.swift */,
|
||||||
);
|
);
|
||||||
path = Payload;
|
path = Payload;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -297,6 +345,7 @@
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
F3F9F3BE238952C500864243 /* CypherPoetSwiftUIKit */,
|
F3F9F3BE238952C500864243 /* CypherPoetSwiftUIKit */,
|
||||||
F3F9F3C1238952F200864243 /* CypherPoetNetStack */,
|
F3F9F3C1238952F200864243 /* CypherPoetNetStack */,
|
||||||
|
F352423F238EE983009DF1F9 /* CypherPoetCoreDataKit */,
|
||||||
);
|
);
|
||||||
productName = SpaceXPayloadStats;
|
productName = SpaceXPayloadStats;
|
||||||
productReference = F3F9F3A123892C0300864243 /* SpaceXPayloadStats.app */;
|
productReference = F3F9F3A123892C0300864243 /* SpaceXPayloadStats.app */;
|
||||||
|
@ -329,6 +378,7 @@
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
F3F9F3BD238952C500864243 /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */,
|
F3F9F3BD238952C500864243 /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */,
|
||||||
F3F9F3C0238952F200864243 /* XCRemoteSwiftPackageReference "CypherPoetNetStack" */,
|
F3F9F3C0238952F200864243 /* XCRemoteSwiftPackageReference "CypherPoetNetStack" */,
|
||||||
|
F352423E238EE983009DF1F9 /* XCRemoteSwiftPackageReference "CypherPoetCoreDataKit" */,
|
||||||
);
|
);
|
||||||
productRefGroup = F3F9F3A223892C0300864243 /* Products */;
|
productRefGroup = F3F9F3A223892C0300864243 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -361,25 +411,35 @@
|
||||||
F3F9F3F2238AF96600864243 /* NumberFormatters.swift in Sources */,
|
F3F9F3F2238AF96600864243 /* NumberFormatters.swift in Sources */,
|
||||||
F3F9F3F0238AB02B00864243 /* DateFormatter+iso8601Full.swift in Sources */,
|
F3F9F3F0238AB02B00864243 /* DateFormatter+iso8601Full.swift in Sources */,
|
||||||
F3F9F3A523892C0300864243 /* AppDelegate.swift in Sources */,
|
F3F9F3A523892C0300864243 /* AppDelegate.swift in Sources */,
|
||||||
F3F9F3D72389611800864243 /* Payload+OrbitParams.swift in Sources */,
|
|
||||||
F3F9F3CD2389556700864243 /* MissionsState.swift in Sources */,
|
F3F9F3CD2389556700864243 /* MissionsState.swift in Sources */,
|
||||||
F3F9F3E223897B7000864243 /* Dependencies.swift in Sources */,
|
F3F9F3E223897B7000864243 /* Dependencies.swift in Sources */,
|
||||||
|
F3F9F400238C357E00864243 /* CoreDataManager.swift in Sources */,
|
||||||
|
F352423B238EE7B2009DF1F9 /* Mission+FetchHelpers.swift in Sources */,
|
||||||
|
F3A61A86238C996C00279192 /* OrbitParams+CoreDataProperties.swift in Sources */,
|
||||||
F3F9F3ED238A6F7800864243 /* PayloadDetailsViewModel.swift in Sources */,
|
F3F9F3ED238A6F7800864243 /* PayloadDetailsViewModel.swift in Sources */,
|
||||||
F3F9F3FA238B036F00864243 /* Payload+Computeds.swift in Sources */,
|
|
||||||
F3F9F3E62389828D00864243 /* Endpoint+SpaceXAPI.swift in Sources */,
|
F3F9F3E62389828D00864243 /* Endpoint+SpaceXAPI.swift in Sources */,
|
||||||
F3F9F3E423897CA800864243 /* SpaceXAPIService.swift in Sources */,
|
F3F9F3E423897CA800864243 /* SpaceXAPIService.swift in Sources */,
|
||||||
F3F9F3D52389567D00864243 /* Payload.swift in Sources */,
|
F3A61A8C238D67DF00279192 /* Payload+Computeds.swift in Sources */,
|
||||||
|
F3F9F403238C78F600864243 /* SpaceXPayloadStats.xcdatamodeld in Sources */,
|
||||||
F3F9F3EB23899C0B00864243 /* PayloadDetailsView.swift in Sources */,
|
F3F9F3EB23899C0B00864243 /* PayloadDetailsView.swift in Sources */,
|
||||||
|
F3A61A8A238C9E1100279192 /* Payload+Comparable.swift in Sources */,
|
||||||
|
F3A61A7C238C98EE00279192 /* Mission+CoreDataClass.swift in Sources */,
|
||||||
F3F9F3C7238953B600864243 /* SampleData.swift in Sources */,
|
F3F9F3C7238953B600864243 /* SampleData.swift in Sources */,
|
||||||
|
F3A61A80238C994900279192 /* Payload+CoreDataClass.swift in Sources */,
|
||||||
F3F9F3DA238966E000864243 /* MissionsListContainerView.swift in Sources */,
|
F3F9F3DA238966E000864243 /* MissionsListContainerView.swift in Sources */,
|
||||||
|
F30B3646238D88480041AC59 /* CodingUserInfoKey+Extensions.swift in Sources */,
|
||||||
|
F3A61A88238C9D8A00279192 /* Mission+Computeds.swift in Sources */,
|
||||||
F3F9F3A723892C0300864243 /* SceneDelegate.swift in Sources */,
|
F3F9F3A723892C0300864243 /* SceneDelegate.swift in Sources */,
|
||||||
F3F9F3F7238B001600864243 /* InfoHeaderTextStyle.swift in Sources */,
|
F3F9F3F7238B001600864243 /* InfoHeaderTextStyle.swift in Sources */,
|
||||||
F3F9F3F8238B001600864243 /* Text+InfoHeaderStyle.swift in Sources */,
|
F3F9F3F8238B001600864243 /* Text+InfoHeaderStyle.swift in Sources */,
|
||||||
F3F9F3FE238BF79A00864243 /* MissionDetailsViewModel.swift in Sources */,
|
F3F9F3FE238BF79A00864243 /* MissionDetailsViewModel.swift in Sources */,
|
||||||
F3F9F3CF2389558000864243 /* PayloadsState.swift in Sources */,
|
F3F9F3CF2389558000864243 /* PayloadsState.swift in Sources */,
|
||||||
F3F9F3D12389565000864243 /* Mission.swift in Sources */,
|
F3A61A7D238C98EE00279192 /* Mission+CoreDataProperties.swift in Sources */,
|
||||||
F3F9F3DC2389743600864243 /* MissionsListView.swift in Sources */,
|
F3F9F3DC2389743600864243 /* MissionsListView.swift in Sources */,
|
||||||
F3F9F3DF238976E200864243 /* MissionDetailsView.swift in Sources */,
|
F3F9F3DF238976E200864243 /* MissionDetailsView.swift in Sources */,
|
||||||
|
F3A61A81238C994900279192 /* Payload+CoreDataProperties.swift in Sources */,
|
||||||
|
F352423D238EE80C009DF1F9 /* Payload+FetchHelpers.swift in Sources */,
|
||||||
|
F3A61A85238C996C00279192 /* OrbitParams+CoreDataClass.swift in Sources */,
|
||||||
F3F9F3CA238953C200864243 /* AppState.swift in Sources */,
|
F3F9F3CA238953C200864243 /* AppState.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -576,6 +636,14 @@
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
F352423E238EE983009DF1F9 /* XCRemoteSwiftPackageReference "CypherPoetCoreDataKit" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/CypherPoet/CypherPoetCoreDataKit.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 0.0.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
F3F9F3BD238952C500864243 /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */ = {
|
F3F9F3BD238952C500864243 /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/CypherPoet/CypherPoetSwiftUIKit.git";
|
repositoryURL = "https://github.com/CypherPoet/CypherPoetSwiftUIKit.git";
|
||||||
|
@ -595,6 +663,11 @@
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
F352423F238EE983009DF1F9 /* CypherPoetCoreDataKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = F352423E238EE983009DF1F9 /* XCRemoteSwiftPackageReference "CypherPoetCoreDataKit" */;
|
||||||
|
productName = CypherPoetCoreDataKit;
|
||||||
|
};
|
||||||
F3F9F3BE238952C500864243 /* CypherPoetSwiftUIKit */ = {
|
F3F9F3BE238952C500864243 /* CypherPoetSwiftUIKit */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = F3F9F3BD238952C500864243 /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */;
|
package = F3F9F3BD238952C500864243 /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */;
|
||||||
|
@ -606,6 +679,19 @@
|
||||||
productName = CypherPoetNetStack;
|
productName = CypherPoetNetStack;
|
||||||
};
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|
||||||
|
/* Begin XCVersionGroup section */
|
||||||
|
F3F9F401238C78F500864243 /* SpaceXPayloadStats.xcdatamodeld */ = {
|
||||||
|
isa = XCVersionGroup;
|
||||||
|
children = (
|
||||||
|
F3F9F402238C78F600864243 /* SpaceXPayloadStats.xcdatamodel */,
|
||||||
|
);
|
||||||
|
currentVersion = F3F9F402238C78F600864243 /* SpaceXPayloadStats.xcdatamodel */;
|
||||||
|
path = SpaceXPayloadStats.xcdatamodeld;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
versionGroupType = wrapper.xcdatamodel;
|
||||||
|
};
|
||||||
|
/* End XCVersionGroup section */
|
||||||
};
|
};
|
||||||
rootObject = F3F9F39923892C0300864243 /* Project object */;
|
rootObject = F3F9F39923892C0300864243 /* Project object */;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
{
|
{
|
||||||
"object": {
|
"object": {
|
||||||
"pins": [
|
"pins": [
|
||||||
|
{
|
||||||
|
"package": "CypherPoetCoreDataKit",
|
||||||
|
"repositoryURL": "https://github.com/CypherPoet/CypherPoetCoreDataKit.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "4a628bc53793aa8868e1fa9bb12850c0b9331f38",
|
||||||
|
"version": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "CypherPoetNetStack",
|
"package": "CypherPoetNetStack",
|
||||||
"repositoryURL": "https://github.com/CypherPoet/CypherPoetNetStack.git",
|
"repositoryURL": "https://github.com/CypherPoet/CypherPoetNetStack.git",
|
||||||
|
|
|
@ -50,6 +50,16 @@
|
||||||
ReferencedContainer = "container:SpaceXPayloadStats.xcodeproj">
|
ReferencedContainer = "container:SpaceXPayloadStats.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<CommandLineArguments>
|
||||||
|
<CommandLineArgument
|
||||||
|
argument = "-com.apple.CoreData.Logging.stderr 1"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</CommandLineArgument>
|
||||||
|
<CommandLineArgument
|
||||||
|
argument = "-com.apple.CoreData.SQLDebug 1"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</CommandLineArgument>
|
||||||
|
</CommandLineArguments>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|
|
@ -15,6 +15,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
// Override point for customization after application launch.
|
// Override point for customization after application launch.
|
||||||
|
|
||||||
|
CoreDataManager.shared.setup()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
//
|
||||||
|
// CoreDataManager.swift
|
||||||
|
// SpaceXPayloadStats
|
||||||
|
//
|
||||||
|
// Created by CypherPoet on 11/25/19.
|
||||||
|
// ✌️
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
|
||||||
|
final class CoreDataManager {
|
||||||
|
typealias LoadCompletionHandler = (() -> Void)
|
||||||
|
|
||||||
|
private var storeType: String = NSSQLiteStoreType
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - PersistentContainer
|
||||||
|
|
||||||
|
lazy var persistentContainer: NSPersistentContainer = {
|
||||||
|
let container = NSPersistentContainer(name: ManagedObjectModelName.application)
|
||||||
|
let description = container.persistentStoreDescriptions.first
|
||||||
|
|
||||||
|
description?.type = storeType
|
||||||
|
|
||||||
|
return container
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Managed Object Contexts
|
||||||
|
|
||||||
|
lazy var backgroundContext: NSManagedObjectContext = {
|
||||||
|
let context = self.persistentContainer.newBackgroundContext()
|
||||||
|
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||||
|
|
||||||
|
return context
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
lazy var mainContext: NSManagedObjectContext = {
|
||||||
|
let context = self.persistentContainer.viewContext
|
||||||
|
context.automaticallyMergesChangesFromParent = true
|
||||||
|
|
||||||
|
return context
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Private Methods
|
||||||
|
extension CoreDataManager {
|
||||||
|
|
||||||
|
private func loadPersistentStore(then completionHandler: @escaping LoadCompletionHandler) {
|
||||||
|
persistentContainer.loadPersistentStores { description, error in
|
||||||
|
guard error == nil else {
|
||||||
|
fatalError("Error while loading store: \(error!)")
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Public Methods
|
||||||
|
extension CoreDataManager {
|
||||||
|
|
||||||
|
func setup(storeType: String = NSSQLiteStoreType, then completionHandler: LoadCompletionHandler? = nil) {
|
||||||
|
self.storeType = storeType
|
||||||
|
|
||||||
|
loadPersistentStore() {
|
||||||
|
completionHandler?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func save(_ context: NSManagedObjectContext) {
|
||||||
|
context.performAndWait {
|
||||||
|
if context.hasChanges {
|
||||||
|
do {
|
||||||
|
try context.save()
|
||||||
|
} catch {
|
||||||
|
// TODO: Better error handling would be needed here for a production app
|
||||||
|
let nsError = error as NSError
|
||||||
|
print("Error while attempting to save Core Data context named: \"\(context.name ?? "")\": \(nsError), \(nsError.userInfo)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func saveContexts() {
|
||||||
|
[mainContext, backgroundContext].forEach(save(_:))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension CoreDataManager {
|
||||||
|
static let shared = CoreDataManager()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CoreDataManager {
|
||||||
|
enum ManagedObjectModelName {
|
||||||
|
static let application = "SpaceXPayloadStats"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// Mission+Computeds.swift
|
||||||
|
// SpaceXPayloadStats
|
||||||
|
//
|
||||||
|
// Created by CypherPoet on 11/25/19.
|
||||||
|
// ✌️
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
extension Mission {
|
||||||
|
|
||||||
|
var payloadsArray: [Payload] {
|
||||||
|
guard let payloads = payloads as? Set<Payload> else { return [] }
|
||||||
|
|
||||||
|
return payloads.sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static var decoder = JSONDecoder()
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
//
|
||||||
|
// Mission+CoreDataClass.swift
|
||||||
|
// SpaceXPayloadStats
|
||||||
|
//
|
||||||
|
// Created by Brian Sipple on 11/25/19.
|
||||||
|
// Copyright © 2019 CypherPoet. All rights reserved.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
@objc(Mission)
|
||||||
|
public class Mission: NSManagedObject, Decodable {
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case missionID = "mission_id"
|
||||||
|
case name = "mission_name"
|
||||||
|
case manufacturers
|
||||||
|
case payloadIDs = "payload_ids"
|
||||||
|
case wikipediaURLString = "wikipedia"
|
||||||
|
case websiteURLString = "website"
|
||||||
|
case twitterURLString = "twitter"
|
||||||
|
case missionDescription = "description"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Decodable
|
||||||
|
public required convenience init(from decoder: Decoder) throws {
|
||||||
|
guard
|
||||||
|
let managedObjectContext = decoder.userInfo[.managedObjectContext] as? NSManagedObjectContext,
|
||||||
|
let entity = NSEntityDescription.entity(forEntityName: "Mission", in: managedObjectContext)
|
||||||
|
else {
|
||||||
|
fatalError("Unable to initialize entity during decoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(entity: entity, insertInto: managedObjectContext)
|
||||||
|
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
missionID = try container.decode(String.self, forKey: .missionID)
|
||||||
|
name = try container.decode(String.self, forKey: .name)
|
||||||
|
manufacturers = try container.decode([String].self, forKey: .manufacturers)
|
||||||
|
payloadIDs = try container.decode([String].self, forKey: .payloadIDs)
|
||||||
|
wikipediaURLString = try? container.decode(String.self, forKey: .wikipediaURLString)
|
||||||
|
websiteURLString = try? container.decode(String.self, forKey: .websiteURLString)
|
||||||
|
twitterURLString = try? container.decode(String.self, forKey: .twitterURLString)
|
||||||
|
missionDescription = try container.decode(String.self, forKey: .missionDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
//
|
||||||
|
// Mission+CoreDataProperties.swift
|
||||||
|
// SpaceXPayloadStats
|
||||||
|
//
|
||||||
|
// Created by Brian Sipple on 11/25/19.
|
||||||
|
// Copyright © 2019 CypherPoet. All rights reserved.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
|
||||||
|
extension Mission {
|
||||||
|
|
||||||
|
@nonobjc public class func fetchRequest() -> NSFetchRequest<Mission> {
|
||||||
|
return NSFetchRequest<Mission>(entityName: "Mission")
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged public var manufacturers: [String]?
|
||||||
|
@NSManaged public var missionDescription: String?
|
||||||
|
@NSManaged public var missionID: String?
|
||||||
|
@NSManaged public var name: String?
|
||||||
|
@NSManaged public var payloadIDs: [String]?
|
||||||
|
@NSManaged public var twitterURLString: String?
|
||||||
|
@NSManaged public var websiteURLString: String?
|
||||||
|
@NSManaged public var wikipediaURLString: String?
|
||||||
|
|
||||||
|
@NSManaged public var payloads: NSSet?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Generated accessors for payloads
|
||||||
|
extension Mission {
|
||||||
|
|
||||||
|
@objc(addPayloadsObject:)
|
||||||
|
@NSManaged public func addToPayloads(_ value: Payload)
|
||||||
|
|
||||||
|
@objc(removePayloadsObject:)
|
||||||
|
@NSManaged public func removeFromPayloads(_ value: Payload)
|
||||||
|
|
||||||
|
@objc(addPayloads:)
|
||||||
|
@NSManaged public func addToPayloads(_ values: NSSet)
|
||||||
|
|
||||||
|
@objc(removePayloads:)
|
||||||
|
@NSManaged public func removeFromPayloads(_ values: NSSet)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// Mission+FetchHelpers.swift
|
||||||
|
// SpaceXPayloadStats
|
||||||
|
//
|
||||||
|
// Created by CypherPoet on 11/27/19.
|
||||||
|
// ✌️
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
|
||||||
|
extension Mission {
|
||||||
|
|
||||||
|
enum SortDescriptors {
|
||||||
|
|
||||||
|
static let `default` = [
|
||||||
|
NSSortDescriptor(keyPath: \Mission.name, ascending: true)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum FetchRequest {
|
||||||
|
|
||||||
|
static let all: NSFetchRequest<Mission> = {
|
||||||
|
let fetchRequest: NSFetchRequest<Mission> = Mission.fetchRequest()
|
||||||
|
|
||||||
|
fetchRequest.sortDescriptors = Mission.SortDescriptors.default
|
||||||
|
|
||||||
|
return fetchRequest
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
//
|
|
||||||
// Mission.swift
|
|
||||||
// SpaceXPayloadStats
|
|
||||||
//
|
|
||||||
// Created by CypherPoet on 11/23/19.
|
|
||||||
// ✌️
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
|
|
||||||
struct Mission {
|
|
||||||
var missionID: String
|
|
||||||
var name: String
|
|
||||||
var manufacturers: [String]
|
|
||||||
var payloadIDs: [String]
|
|
||||||
var wikipediaURL: URL?
|
|
||||||
var websiteURL: URL?
|
|
||||||
var twitterURL: URL?
|
|
||||||
var description: String
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extension Mission: Identifiable {
|
|
||||||
var id: String { missionID }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extension Mission: Codable {
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case missionID = "mission_id"
|
|
||||||
case name = "mission_name"
|
|
||||||
case manufacturers
|
|
||||||
case payloadIDs = "payload_ids"
|
|
||||||
case wikipediaURL = "wikipedia"
|
|
||||||
case websiteURL = "website"
|
|
||||||
case twitterURL = "twitter"
|
|
||||||
case description
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
//
|
||||||
|
// OrbitParams+CoreDataClass.swift
|
||||||
|
// SpaceXPayloadStats
|
||||||
|
//
|
||||||
|
// Created by Brian Sipple on 11/25/19.
|
||||||
|
// Copyright © 2019 CypherPoet. All rights reserved.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
@objc(OrbitParams)
|
||||||
|
public class OrbitParams: NSManagedObject, Decodable {
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case referenceSystem = "reference_system"
|
||||||
|
case regime
|
||||||
|
case longitude
|
||||||
|
case periapsis = "periapsis_km"
|
||||||
|
case apoapsis = "apoapsis_km"
|
||||||
|
case epoch
|
||||||
|
case meanAnomaly = "mean_anomaly"
|
||||||
|
case lifespanYears = "lifespan_years"
|
||||||
|
case periodMinutes = "period_min"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Decodable
|
||||||
|
public required convenience init(from decoder: Decoder) throws {
|
||||||
|
guard
|
||||||
|
let managedObjectContext = decoder.userInfo[.managedObjectContext] as? NSManagedObjectContext,
|
||||||
|
let entity = NSEntityDescription.entity(forEntityName: "OrbitParams", in: managedObjectContext)
|
||||||
|
else {
|
||||||
|
fatalError("Unable to initialize entity during decoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(entity: entity, insertInto: managedObjectContext)
|
||||||
|
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
referenceSystem = try? container.decode(String.self, forKey: .referenceSystem)
|
||||||
|
regime = try? container.decode(String.self, forKey: .regime)
|
||||||
|
longitude = (try? container.decode(Double.self, forKey: .longitude)) ?? 0
|
||||||
|
periapsis = (try? container.decode(Kilometers.self, forKey: .periapsis)) ?? 0
|
||||||
|
apoapsis = (try? container.decode(Kilometers.self, forKey: .apoapsis)) ?? 0
|
||||||
|
epoch = try? container.decode(Date.self, forKey: .epoch)
|
||||||
|
meanAnomaly = (try? container.decode(Double.self, forKey: .meanAnomaly)) ?? 0
|
||||||
|
lifespanYears = (try? container.decode(Double.self, forKey: .lifespanYears)) ?? 0
|
||||||
|
periodMinutes = (try? container.decode(Double.self, forKey: .periodMinutes)) ?? 0
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
//
|
||||||
|
// OrbitParams+CoreDataProperties.swift
|
||||||
|
// SpaceXPayloadStats
|
||||||
|
//
|
||||||
|
// Created by Brian Sipple on 11/25/19.
|
||||||
|
// Copyright © 2019 CypherPoet. All rights reserved.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
|
||||||
|
extension OrbitParams {
|
||||||
|
|
||||||
|
@nonobjc public class func fetchRequest() -> NSFetchRequest<OrbitParams> {
|
||||||
|
return NSFetchRequest<OrbitParams>(entityName: "OrbitParams")
|
||||||
|
}
|
||||||
|
|
||||||
|
public typealias Kilometers = Double
|
||||||
|
|
||||||
|
@NSManaged public var apoapsis: Kilometers
|
||||||
|
@NSManaged public var epoch: Date?
|
||||||
|
@NSManaged public var lifespanYears: Double
|
||||||
|
@NSManaged public var longitude: Double
|
||||||
|
@NSManaged public var meanAnomaly: Double
|
||||||
|
@NSManaged public var periapsis: Kilometers
|
||||||
|
@NSManaged public var periodMinutes: Double
|
||||||
|
@NSManaged public var referenceSystem: String?
|
||||||
|
@NSManaged public var regime: String?
|
||||||
|
|
||||||
|
@NSManaged public var payload: Payload?
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
//
|
||||||
|
// Payload+Comparable.swift
|
||||||
|
// SpaceXPayloadStats
|
||||||
|
//
|
||||||
|
// Created by CypherPoet on 11/25/19.
|
||||||
|
// ✌️
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
extension Payload: Comparable {
|
||||||
|
|
||||||
|
public static func < (lhs: Payload, rhs: Payload) -> Bool {
|
||||||
|
// Assign a lower sort priority if `payloadID` doesn't exist
|
||||||
|
guard let lhsID = lhs.payloadID else { return false }
|
||||||
|
guard let rhsID = rhs.payloadID else { return true }
|
||||||
|
|
||||||
|
return lhsID < rhsID
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
// Payload+Computeds.swift
|
// Payload+Computeds.swift
|
||||||
// SpaceXPayloadStats
|
// SpaceXPayloadStats
|
||||||
//
|
//
|
||||||
// Created by CypherPoet on 11/24/19.
|
// Created by CypherPoet on 11/26/19.
|
||||||
// ✌️
|
// ✌️
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import Foundation
|
||||||
extension Payload {
|
extension Payload {
|
||||||
|
|
||||||
var payloadTypeEmoji: String? {
|
var payloadTypeEmoji: String? {
|
||||||
let type = payloadType.lowercased()
|
guard let type = payloadType?.lowercased() else { return nil }
|
||||||
|
|
||||||
if type.starts(with: "satellite") {
|
if type.starts(with: "satellite") {
|
||||||
return "🛰"
|
return "🛰"
|
||||||
|
@ -22,5 +22,14 @@ extension Payload {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
static var decoder: JSONDecoder = {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
/// Use a custom date decoding strategy to handle `orbitParams.epoch`, which contain
|
||||||
|
/// milliseconds (aka "fractional seconds")
|
||||||
|
decoder.dateDecodingStrategy = .iso8601Full
|
||||||
|
|
||||||
|
return decoder
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
//
|
||||||
|
// Payload+CoreDataClass.swift
|
||||||
|
// SpaceXPayloadStats
|
||||||
|
//
|
||||||
|
// Created by Brian Sipple on 11/25/19.
|
||||||
|
// Copyright © 2019 CypherPoet. All rights reserved.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
@objc(Payload)
|
||||||
|
public class Payload: NSManagedObject, Decodable {
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case payloadID = "payload_id"
|
||||||
|
case payloadType = "payload_type"
|
||||||
|
case isReused = "reused"
|
||||||
|
case manufacturer
|
||||||
|
case customers
|
||||||
|
case nationality
|
||||||
|
case mass = "payload_mass_kg"
|
||||||
|
case orbit
|
||||||
|
case orbitParams = "orbit_params"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Decodable
|
||||||
|
public required convenience init(from decoder: Decoder) throws {
|
||||||
|
guard
|
||||||
|
let managedObjectContext = decoder.userInfo[.managedObjectContext] as? NSManagedObjectContext,
|
||||||
|
let entity = NSEntityDescription.entity(forEntityName: "Payload", in: managedObjectContext)
|
||||||
|
else {
|
||||||
|
fatalError("Unable to initialize entity during decoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(entity: entity, insertInto: managedObjectContext)
|
||||||
|
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
payloadID = try container.decode(String.self, forKey: .payloadID)
|
||||||
|
payloadType = try? container.decode(String.self, forKey: .payloadType)
|
||||||
|
isReused = try container.decode(Bool.self, forKey: .isReused)
|
||||||
|
manufacturer = try? container.decode(String.self, forKey: .manufacturer)
|
||||||
|
customers = try? container.decode([String].self, forKey: .customers)
|
||||||
|
nationality = try? container.decode(String.self, forKey: .nationality)
|
||||||
|
mass = (try? container.decode(Int64.self, forKey: .mass)) ?? 0
|
||||||
|
orbit = try? container.decode(String.self, forKey: .orbit)
|
||||||
|
orbitParams = try container.decode(OrbitParams.self, forKey: .orbitParams)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
//
|
||||||
|
// Payload+CoreDataProperties.swift
|
||||||
|
// SpaceXPayloadStats
|
||||||
|
//
|
||||||
|
// Created by Brian Sipple on 11/25/19.
|
||||||
|
// Copyright © 2019 CypherPoet. All rights reserved.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
|
||||||
|
extension Payload {
|
||||||
|
|
||||||
|
@nonobjc public class func fetchRequest() -> NSFetchRequest<Payload> {
|
||||||
|
return NSFetchRequest<Payload>(entityName: "Payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged public var customers: [String]?
|
||||||
|
@NSManaged public var isReused: Bool
|
||||||
|
@NSManaged public var manufacturer: String?
|
||||||
|
@NSManaged public var mass: Int64
|
||||||
|
@NSManaged public var nationality: String?
|
||||||
|
@NSManaged public var orbit: String?
|
||||||
|
@NSManaged public var payloadID: String?
|
||||||
|
@NSManaged public var payloadType: String?
|
||||||
|
|
||||||
|
@NSManaged public var mission: Mission?
|
||||||
|
@NSManaged public var orbitParams: OrbitParams?
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
//
|
||||||
|
// Payload+FetchHelpers.swift
|
||||||
|
// SpaceXPayloadStats
|
||||||
|
//
|
||||||
|
// Created by CypherPoet on 11/27/19.
|
||||||
|
// ✌️
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import CypherPoetCoreDataKit_PredicateUtils
|
||||||
|
|
||||||
|
|
||||||
|
extension Payload {
|
||||||
|
|
||||||
|
enum SortDescriptors {
|
||||||
|
|
||||||
|
static let `default` = [
|
||||||
|
NSSortDescriptor(keyPath: \Payload.payloadID, ascending: true)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum Predicate {
|
||||||
|
|
||||||
|
static func payloads(for mission: Mission) -> NSPredicate {
|
||||||
|
let keyword = NSComparisonPredicate.keyword(for: .in)
|
||||||
|
|
||||||
|
return NSPredicate(
|
||||||
|
format: "%K \(keyword) %@",
|
||||||
|
#keyPath(Payload.payloadID),
|
||||||
|
mission.payloadIDs ?? []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func fetchRequest(for mission: Mission) -> NSFetchRequest<Payload> {
|
||||||
|
let fetchRequest: NSFetchRequest<Payload> = Self.fetchRequest()
|
||||||
|
|
||||||
|
fetchRequest.sortDescriptors = Self.SortDescriptors.default
|
||||||
|
fetchRequest.predicate = Self.Predicate.payloads(for: mission)
|
||||||
|
|
||||||
|
return fetchRequest
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,52 +0,0 @@
|
||||||
//
|
|
||||||
// Payload+OrbitParams.swift
|
|
||||||
// SpaceXPayloadStats
|
|
||||||
//
|
|
||||||
// Created by CypherPoet on 11/23/19.
|
|
||||||
// ✌️
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
extension Payload {
|
|
||||||
|
|
||||||
struct OrbitParams {
|
|
||||||
typealias Kilometers = Double
|
|
||||||
|
|
||||||
var referenceSystem: String?
|
|
||||||
var regime: String?
|
|
||||||
var longitude: Double?
|
|
||||||
|
|
||||||
var periapsis: Kilometers?
|
|
||||||
var apoapsis: Kilometers?
|
|
||||||
|
|
||||||
/// https://en.wikipedia.org/wiki/Epoch_(astronomy)
|
|
||||||
var epoch: Date?
|
|
||||||
|
|
||||||
/// https://en.wikipedia.org/wiki/Mean_anomaly
|
|
||||||
var meanAnomaly: Double?
|
|
||||||
|
|
||||||
var lifeSpanYears: Double?
|
|
||||||
|
|
||||||
/// https://en.wikipedia.org/wiki/Geosynchronous_orbit#Properties
|
|
||||||
var periodMinutes: Double?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extension Payload.OrbitParams: Codable {
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case referenceSystem = "reference_system"
|
|
||||||
case regime
|
|
||||||
case longitude
|
|
||||||
case periapsis = "periapsis_km"
|
|
||||||
case apoapsis = "apoapsis_km"
|
|
||||||
case epoch
|
|
||||||
case meanAnomaly = "mean_anomaly"
|
|
||||||
case lifeSpanYears = "lifespan_years"
|
|
||||||
case periodMinutes = "period_min"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
//
|
|
||||||
// Payload.swift
|
|
||||||
// SpaceXPayloadStats
|
|
||||||
//
|
|
||||||
// Created by CypherPoet on 11/23/19.
|
|
||||||
// ✌️
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
struct Payload {
|
|
||||||
typealias Kilograms = Int
|
|
||||||
|
|
||||||
var payloadID: String
|
|
||||||
var payloadType: String
|
|
||||||
var isReused: Bool
|
|
||||||
var manufacturer: String?
|
|
||||||
var customers: [String]
|
|
||||||
var nationality: String?
|
|
||||||
var mass: Kilograms?
|
|
||||||
var orbit: String
|
|
||||||
var orbitParams: OrbitParams
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extension Payload: Identifiable {
|
|
||||||
var id: String { payloadID }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extension Payload: Codable {
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case payloadID = "payload_id"
|
|
||||||
case payloadType = "payload_type"
|
|
||||||
case isReused = "reused"
|
|
||||||
case manufacturer
|
|
||||||
case customers
|
|
||||||
case nationality
|
|
||||||
case mass = "payload_mass_kg"
|
|
||||||
case orbit
|
|
||||||
case orbitParams = "orbit_params"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extension Payload {
|
|
||||||
static var decoder: JSONDecoder = {
|
|
||||||
let decoder = JSONDecoder()
|
|
||||||
|
|
||||||
/// Use a custom date decoding strategy to handle `orbitParams.epoch`, which contain
|
|
||||||
/// milliseconds (aka "fractional seconds")
|
|
||||||
decoder.dateDecodingStrategy = .iso8601Full
|
|
||||||
|
|
||||||
return decoder
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15508" systemVersion="19B88" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
|
<entity name="Mission" representedClassName="Mission" syncable="YES">
|
||||||
|
<attribute name="manufacturers" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
|
||||||
|
<attribute name="missionDescription" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="missionID" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="name" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="payloadIDs" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
|
||||||
|
<attribute name="twitterURLString" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="websiteURLString" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="wikipediaURLString" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="payloads" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Payload" inverseName="mission" inverseEntity="Payload"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="missionID"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="OrbitParams" representedClassName="OrbitParams" syncable="YES">
|
||||||
|
<attribute name="apoapsis" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="epoch" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="lifespanYears" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="longitude" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="meanAnomaly" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="periapsis" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="periodMinutes" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="referenceSystem" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="regime" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="payload" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Payload" inverseName="orbitParams" inverseEntity="Payload"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="Payload" representedClassName="Payload" syncable="YES">
|
||||||
|
<attribute name="customers" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
|
||||||
|
<attribute name="isReused" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="manufacturer" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="mass" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="nationality" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="orbit" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="payloadID" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="payloadType" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="mission" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Mission" inverseName="payloads" inverseEntity="Mission"/>
|
||||||
|
<relationship name="orbitParams" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="OrbitParams" inverseName="payload" inverseEntity="OrbitParams"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="payloadID"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<elements>
|
||||||
|
<element name="Mission" positionX="-63" positionY="-18" width="128" height="178"/>
|
||||||
|
<element name="OrbitParams" positionX="-54" positionY="-9" width="128" height="193"/>
|
||||||
|
<element name="Payload" positionX="-54" positionY="-9" width="128" height="193"/>
|
||||||
|
</elements>
|
||||||
|
</model>
|
|
@ -10,7 +10,7 @@ import CypherPoetSwiftUIKit
|
||||||
|
|
||||||
|
|
||||||
struct MissionsState: Codable {
|
struct MissionsState: Codable {
|
||||||
var missions: [Mission] = []
|
var missionsFetchErrorMessage: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,11 +20,17 @@ enum MissionsSideEffect: SideEffect {
|
||||||
func mapToAction() -> AnyPublisher<AppAction, Never> {
|
func mapToAction() -> AnyPublisher<AppAction, Never> {
|
||||||
switch self {
|
switch self {
|
||||||
case .fetch:
|
case .fetch:
|
||||||
|
let context = CoreDataManager.shared.backgroundContext
|
||||||
|
|
||||||
return Dependencies.spaceXAPIService
|
return Dependencies.spaceXAPIService
|
||||||
.fetchMissions()
|
.fetchMissions(using: context)
|
||||||
.breakpointOnError()
|
.map { _ in
|
||||||
.replaceError(with: [])
|
CoreDataManager.shared.save(context)
|
||||||
.map { AppAction.missions(.set(missions: $0)) }
|
return AppAction.missions(.set(fetchErrorMessage: nil))
|
||||||
|
}
|
||||||
|
.catch { error in
|
||||||
|
Just(AppAction.missions(.set(fetchErrorMessage: error.errorDescription)))
|
||||||
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +39,7 @@ enum MissionsSideEffect: SideEffect {
|
||||||
|
|
||||||
|
|
||||||
enum MissionsAction {
|
enum MissionsAction {
|
||||||
case set(missions: [Mission])
|
case set(fetchErrorMessage: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,7 +47,7 @@ enum MissionsAction {
|
||||||
// MARK: - Reducer
|
// MARK: - Reducer
|
||||||
let missionsReducer = Reducer<MissionsState, MissionsAction> { state, action in
|
let missionsReducer = Reducer<MissionsState, MissionsAction> { state, action in
|
||||||
switch action {
|
switch action {
|
||||||
case let .set(missions):
|
case .set(let fetchErrorMessage):
|
||||||
state.missions = missions
|
state.missionsFetchErrorMessage = fetchErrorMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,22 +14,27 @@ import CypherPoetSwiftUIKit
|
||||||
|
|
||||||
struct PayloadsState: Codable {
|
struct PayloadsState: Codable {
|
||||||
var payloadFetchErrorMessage: String?
|
var payloadFetchErrorMessage: String?
|
||||||
var payloadsByID: [Payload.ID: Payload] = [:]
|
|
||||||
var payloadsByMissionID: [Mission.ID: [Payload]] = [:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum PayloadsSideEffect: SideEffect {
|
enum PayloadsSideEffect: SideEffect {
|
||||||
case fetchPayload(withID: Payload.ID)
|
case fetchPayloads(for: Mission)
|
||||||
|
// case fetchPayload(withID: String)
|
||||||
|
|
||||||
|
|
||||||
func mapToAction() -> AnyPublisher<AppAction, Never> {
|
func mapToAction() -> AnyPublisher<AppAction, Never> {
|
||||||
|
let context = CoreDataManager.shared.backgroundContext
|
||||||
|
|
||||||
switch self {
|
switch self {
|
||||||
case let .fetchPayload(payloadID):
|
case let .fetchPayloads(mission):
|
||||||
return Dependencies.spaceXAPIService
|
return Dependencies.spaceXAPIService
|
||||||
.fetchPayload(for: payloadID)
|
.fetchPayloads(with: mission.payloadIDs ?? [], using: context)
|
||||||
.map { AppAction.payloads(.setPayload($0)) }
|
.map { _ in
|
||||||
.catch {
|
CoreDataManager.shared.save(context)
|
||||||
Just(AppAction.payloads(.setFetchError(message: $0.localizedDescription)))
|
return AppAction.payloads(.set(fetchErrorMessage: nil))
|
||||||
|
}
|
||||||
|
.catch { error in
|
||||||
|
Just(AppAction.payloads(.set(fetchErrorMessage: error.errorDescription)))
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -38,9 +43,7 @@ enum PayloadsSideEffect: SideEffect {
|
||||||
|
|
||||||
|
|
||||||
enum PayloadsAction {
|
enum PayloadsAction {
|
||||||
case setPayload(Payload)
|
case set(fetchErrorMessage: String?)
|
||||||
case setPayloadsForMission([Payload], missionID: Mission.ID)
|
|
||||||
case setFetchError(message: String)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,11 +51,7 @@ enum PayloadsAction {
|
||||||
// MARK: - Reducer
|
// MARK: - Reducer
|
||||||
let payloadsReducer = Reducer<PayloadsState, PayloadsAction> { state, action in
|
let payloadsReducer = Reducer<PayloadsState, PayloadsAction> { state, action in
|
||||||
switch action {
|
switch action {
|
||||||
case .setPayload(let payload):
|
case .set(let fetchErrorMessage):
|
||||||
state.payloadsByID[payload.id] = payload
|
state.payloadFetchErrorMessage = fetchErrorMessage
|
||||||
case .setPayloadsForMission(let payloads, let missionID):
|
|
||||||
state.payloadsByMissionID[missionID] = payloads
|
|
||||||
case .setFetchError(let message):
|
|
||||||
state.payloadFetchErrorMessage = message
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ extension Endpoint {
|
||||||
path: "/v3/missions"
|
path: "/v3/missions"
|
||||||
)
|
)
|
||||||
|
|
||||||
public static func payload(for payloadID: Payload.ID) -> Endpoint {
|
public static func payload(for payloadID: String) -> Endpoint {
|
||||||
Endpoint(
|
Endpoint(
|
||||||
host: Self.host,
|
host: Self.host,
|
||||||
path: "/v3/payloads/\(payloadID)"
|
path: "/v3/payloads/\(payloadID)"
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
import CoreData
|
||||||
import CypherPoetNetStack
|
import CypherPoetNetStack
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,40 +30,68 @@ public final class SpaceXAPIService: ModelTransportRequestPublishing {
|
||||||
// MARK: - Core Methods
|
// MARK: - Core Methods
|
||||||
extension SpaceXAPIService {
|
extension SpaceXAPIService {
|
||||||
|
|
||||||
func fetchMissions() -> AnyPublisher<[Mission], SpaceXAPIService.Error> {
|
func fetchMissions(using context: NSManagedObjectContext) -> AnyPublisher<[Mission], SpaceXAPIService.Error> {
|
||||||
let endpoint = Endpoint.SpaceXAPI.missions
|
guard let url = Endpoint.SpaceXAPI.missions.url else {
|
||||||
|
|
||||||
guard let url = endpoint.url else {
|
|
||||||
preconditionFailure("Unable to make url for endpoint")
|
preconditionFailure("Unable to make url for endpoint")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let decoder = Mission.decoder
|
||||||
|
decoder.userInfo[.managedObjectContext] = context
|
||||||
|
|
||||||
|
print("Fetching missions at URL path: \(url.absoluteString)")
|
||||||
|
|
||||||
return perform(
|
return perform(
|
||||||
URLRequest(url: url),
|
URLRequest(url: url),
|
||||||
parsingResponseOn: apiQueue,
|
parsingResponseOn: apiQueue,
|
||||||
with: JSONDecoder()
|
with: decoder
|
||||||
)
|
)
|
||||||
.mapError { .network(error: $0) }
|
.mapError { .network(error: $0) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func fetchPayload(for payloadID: Payload.ID) -> AnyPublisher<Payload, SpaceXAPIService.Error> {
|
func fetchPayload(
|
||||||
let endpoint = Endpoint.SpaceXAPI.payload(for: payloadID)
|
for payloadID: String,
|
||||||
|
using context: NSManagedObjectContext
|
||||||
guard let url = endpoint.url else {
|
) -> AnyPublisher<Payload, SpaceXAPIService.Error> {
|
||||||
|
guard let url = Endpoint.SpaceXAPI.payload(for: payloadID).url else {
|
||||||
preconditionFailure("Unable to make url for endpoint")
|
preconditionFailure("Unable to make url for endpoint")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let decoder = Payload.decoder
|
||||||
|
decoder.userInfo[.managedObjectContext] = context
|
||||||
|
|
||||||
print("Fetching payload at URL path: \(url.absoluteString)")
|
print("Fetching payload at URL path: \(url.absoluteString)")
|
||||||
|
|
||||||
return perform(
|
return perform(
|
||||||
URLRequest(url: url),
|
URLRequest(url: url),
|
||||||
parsingResponseOn: apiQueue,
|
parsingResponseOn: apiQueue,
|
||||||
with: Payload.decoder
|
with: decoder
|
||||||
)
|
)
|
||||||
.mapError { .network(error: $0) }
|
.mapError { .network(error: $0) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func mergedPayloads(
|
||||||
|
for payloadIDs: [String],
|
||||||
|
using context: NSManagedObjectContext
|
||||||
|
) -> AnyPublisher<Payload, SpaceXAPIService.Error> {
|
||||||
|
Publishers.MergeMany(payloadIDs.map { fetchPayload(for: $0, using: context) })
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func fetchPayloads(
|
||||||
|
with payloadIDs: [String],
|
||||||
|
using context: NSManagedObjectContext
|
||||||
|
) -> AnyPublisher<[Payload], SpaceXAPIService.Error> {
|
||||||
|
mergedPayloads(for: payloadIDs, using: context)
|
||||||
|
.scan([], { (accumulated, payload) -> [Payload] in
|
||||||
|
accumulated + [payload]
|
||||||
|
})
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,31 +10,42 @@ import SwiftUI
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
|
|
||||||
|
enum SampleMOC {
|
||||||
|
static let mainContext = CoreDataManager.shared.mainContext
|
||||||
|
static let backgroundContext = CoreDataManager.shared.backgroundContext
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
enum SampleMissions {
|
enum SampleMissions {
|
||||||
static let telstar = Mission(
|
static let telstar: Mission = {
|
||||||
missionID: "F4F83DE",
|
let mission = Mission(context: SampleMOC.mainContext)
|
||||||
name: "Telstar",
|
|
||||||
manufacturers: [
|
mission.missionID = "F4F83DE"
|
||||||
|
mission.name = "Telstar"
|
||||||
|
mission.manufacturers = [
|
||||||
"SSL",
|
"SSL",
|
||||||
],
|
]
|
||||||
payloadIDs: [
|
mission.payloadIDs = [
|
||||||
SamplePayloads.telstar18V.payloadID,
|
SamplePayloads.telstar18V.payloadID,
|
||||||
SamplePayloads.telstar19V.payloadID,
|
SamplePayloads.telstar19V.payloadID,
|
||||||
],
|
].compactMap { $0 }
|
||||||
wikipediaURL: URL(string: "https://en.wikipedia.org/wiki/Telesat"),
|
mission.wikipediaURLString = "https://en.wikipedia.org/wiki/Telesat"
|
||||||
websiteURL: URL(string: "https://www.telesat.com/"),
|
mission.websiteURLString = "https://www.telesat.com/"
|
||||||
twitterURL: nil,
|
mission.twitterURLString = nil
|
||||||
description: "Telstar 19V (Telstar 19 Vantage) is a communication satellite in the Telstar series of the Canadian satellite communications company Telesat. It was built by Space Systems Loral (MAXAR) and is based on the SSL-1300 bus. As of 26 July 2018, Telstar 19V is the heaviest commercial communications satellite ever launched, weighing at 7,076 kg (15,600 lbs) and surpassing the previous record, set by TerreStar-1 (6,910 kg/15230lbs), launched by Ariane 5ECA on 1 July 2009."
|
mission.missionDescription = "Telstar 19V (Telstar 19 Vantage) is a communication satellite in the Telstar series of the Canadian satellite communications company Telesat. It was built by Space Systems Loral (MAXAR) and is based on the SSL-1300 bus. As of 26 July 2018, Telstar 19V is the heaviest commercial communications satellite ever launched, weighing at 7,076 kg (15,600 lbs) and surpassing the previous record, set by TerreStar-1 (6,910 kg/15230lbs), launched by Ariane 5ECA on 1 July 2009."
|
||||||
)
|
|
||||||
|
|
||||||
static let idridiumNext = Mission(
|
return mission
|
||||||
missionID: "F3364BF",
|
}()
|
||||||
name: "Iridium NEXT",
|
|
||||||
manufacturers: [
|
static let idridiumNext: Mission = {
|
||||||
|
let mission = Mission(context: SampleMOC.mainContext)
|
||||||
|
|
||||||
|
mission.missionID = "F3364BF"
|
||||||
|
mission.name = "Iridium NEXT"
|
||||||
|
mission.manufacturers = [
|
||||||
"Orbital ATK",
|
"Orbital ATK",
|
||||||
],
|
]
|
||||||
payloadIDs: [
|
mission.payloadIDs = [
|
||||||
"Iridium NEXT 1",
|
"Iridium NEXT 1",
|
||||||
"Iridium NEXT 2",
|
"Iridium NEXT 2",
|
||||||
"Iridium NEXT 3",
|
"Iridium NEXT 3",
|
||||||
|
@ -43,92 +54,101 @@ enum SampleMissions {
|
||||||
"Iridium NEXT 6",
|
"Iridium NEXT 6",
|
||||||
"Iridium NEXT 7",
|
"Iridium NEXT 7",
|
||||||
"Iridium NEXT 8",
|
"Iridium NEXT 8",
|
||||||
],
|
]
|
||||||
wikipediaURL: URL(string: "https://en.wikipedia.org/wiki/Iridium_satellite_constellation"),
|
mission.wikipediaURLString = "https://en.wikipedia.org/wiki/Iridium_satellite_constellation"
|
||||||
websiteURL: URL(string: "https://www.iridiumnext.com/"),
|
mission.websiteURLString = "https://www.iridiumnext.com/"
|
||||||
twitterURL: nil,
|
mission.twitterURLString = nil
|
||||||
description: "In 2017, Iridium began launching Iridium NEXT, a second-generation worldwide network of telecommunications satellites, consisting of 66 active satellites, with another nine in-orbit spares and six on-ground spares. These satellites will incorporate features such as data transmission that were not emphasized in the original design. The constellation will provide L-band data speeds of up to 128 kbit/s to mobile terminals, up to 1.5 Mbit/s to Iridium Pilot marine terminals, and high-speed Ka-band service of up to 8 Mbit/s to fixed/transportable terminals. The next-generation terminals and service are expected to be commercially available by the end of 2018. However, Iridium's proposed use of its next-generation satellites has raised concerns the service will harmfully interfere with GPS devices. The satellites will incorporate a secondary payload for Aireon, a space-qualified ADS-B data receiver. This is for use by air traffic control and, via FlightAware, for use by airlines. A tertiary payload on 58 satellites is a marine AIS ship-tracker receiver, for Canadian company exactEarth Ltd. Iridium can also be used to provide a data link to other satellites in space, enabling command and control of other space assets regardless of the position of ground stations and gateways."
|
mission.missionDescription = "In 2017, Iridium began launching Iridium NEXT, a second-generation worldwide network of telecommunications satellites, consisting of 66 active satellites, with another nine in-orbit spares and six on-ground spares. These satellites will incorporate features such as data transmission that were not emphasized in the original design. The constellation will provide L-band data speeds of up to 128 kbit/s to mobile terminals, up to 1.5 Mbit/s to Iridium Pilot marine terminals, and high-speed Ka-band service of up to 8 Mbit/s to fixed/transportable terminals. The next-generation terminals and service are expected to be commercially available by the end of 2018. However, Iridium's proposed use of its next-generation satellites has raised concerns the service will harmfully interfere with GPS devices. The satellites will incorporate a secondary payload for Aireon, a space-qualified ADS-B data receiver. This is for use by air traffic control and, via FlightAware, for use by airlines. A tertiary payload on 58 satellites is a marine AIS ship-tracker receiver, for Canadian company exactEarth Ltd. Iridium can also be used to provide a data link to other satellites in space, enabling command and control of other space assets regardless of the position of ground stations and gateways."
|
||||||
)
|
|
||||||
|
return mission
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum SamplePayloads {
|
enum SamplePayloads {
|
||||||
static let telstar18V = Payload(
|
static let telstar18V: Payload = {
|
||||||
payloadID: "Telstar 18V",
|
let payload = Payload(context: SampleMOC.mainContext)
|
||||||
payloadType: "Satellite",
|
|
||||||
isReused: false,
|
|
||||||
manufacturer: "SSL",
|
|
||||||
customers: [
|
|
||||||
"Telesat",
|
|
||||||
],
|
|
||||||
nationality: "Canada",
|
|
||||||
mass: 7060,
|
|
||||||
orbit: "GTO",
|
|
||||||
orbitParams: Payload.OrbitParams(
|
|
||||||
referenceSystem: "geocentric",
|
|
||||||
regime: "geostationary",
|
|
||||||
longitude: 138,
|
|
||||||
periapsis: 5780.342,
|
|
||||||
apoapsis: 35793.581,
|
|
||||||
epoch: Date(),
|
|
||||||
meanAnomaly: 206.577,
|
|
||||||
lifeSpanYears: 15,
|
|
||||||
periodMinutes: 1436.115
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
static let telstar19V = Payload(
|
payload.payloadID = "Telstar 18V"
|
||||||
payloadID: "Telstar 19V",
|
payload.payloadType = "Satellite"
|
||||||
payloadType: "Satellite",
|
payload.isReused = false
|
||||||
isReused: false,
|
payload.manufacturer = "SSL"
|
||||||
manufacturer: "SSL",
|
payload.customers = [
|
||||||
customers: [
|
|
||||||
"Telesat",
|
"Telesat",
|
||||||
],
|
]
|
||||||
nationality: "Canada",
|
payload.nationality = "Canada"
|
||||||
mass: 7076,
|
payload.mass = 7060
|
||||||
orbit: "GTO",
|
payload.orbit = "GTO"
|
||||||
orbitParams: Payload.OrbitParams(
|
payload.orbitParams = SampleOrbitParams.fullParams1
|
||||||
referenceSystem: "geocentric",
|
|
||||||
regime: "geostationary",
|
try? SampleMOC.mainContext.save()
|
||||||
longitude: -65,
|
|
||||||
periapsis: 35778.442,
|
return payload
|
||||||
apoapsis: 35797.08,
|
}()
|
||||||
epoch: Date(),
|
|
||||||
meanAnomaly: 69.209,
|
|
||||||
lifeSpanYears: 15,
|
static let telstar19V: Payload = {
|
||||||
periodMinutes: 35778.442
|
let payload = Payload(context: SampleMOC.mainContext)
|
||||||
)
|
|
||||||
)
|
payload.payloadID = "Telstar 19V"
|
||||||
|
payload.payloadType = "Satellite"
|
||||||
|
payload.isReused = false
|
||||||
|
payload.manufacturer = "SSL"
|
||||||
|
payload.customers = [
|
||||||
|
"Telesat",
|
||||||
|
]
|
||||||
|
payload.nationality = "Canada"
|
||||||
|
payload.mass = 7076
|
||||||
|
payload.orbit = "GTO"
|
||||||
|
payload.orbitParams = SampleOrbitParams.fullParams2
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum SampleOrbitParams {
|
||||||
|
static let fullParams1: OrbitParams = {
|
||||||
|
let params = OrbitParams(context: SampleMOC.mainContext)
|
||||||
|
|
||||||
|
params.referenceSystem = "geocentric"
|
||||||
|
params.regime = "geostationary"
|
||||||
|
params.longitude = 138
|
||||||
|
params.periapsis = 5780.342
|
||||||
|
params.apoapsis = 35793.581
|
||||||
|
params.epoch = Date()
|
||||||
|
params.meanAnomaly = 206.577
|
||||||
|
params.lifespanYears = 15
|
||||||
|
params.periodMinutes = 1436.11
|
||||||
|
|
||||||
|
return params
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let fullParams2: OrbitParams = {
|
||||||
|
let params = OrbitParams(context: SampleMOC.mainContext)
|
||||||
|
|
||||||
|
params.referenceSystem = "geocentric"
|
||||||
|
params.regime = "geostationary"
|
||||||
|
params.longitude = -65
|
||||||
|
params.periapsis = 35778.442
|
||||||
|
params.apoapsis = 35797.08
|
||||||
|
params.epoch = Date()
|
||||||
|
params.meanAnomaly = 69.209
|
||||||
|
params.lifespanYears = 15
|
||||||
|
params.periodMinutes = 35778.44
|
||||||
|
|
||||||
|
return params
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
enum SampleAppState {
|
enum SampleAppState {
|
||||||
static let noModels = AppState()
|
static let `default` = AppState()
|
||||||
|
|
||||||
static let withModels = AppState(
|
|
||||||
missionsState: MissionsState(missions: [SampleMissions.telstar]),
|
|
||||||
payloadsState: PayloadsState(
|
|
||||||
payloadsByID: [
|
|
||||||
SamplePayloads.telstar18V.id: SamplePayloads.telstar18V,
|
|
||||||
SamplePayloads.telstar19V.id: SamplePayloads.telstar19V,
|
|
||||||
],
|
|
||||||
payloadsByMissionID: [
|
|
||||||
SampleMissions.telstar.id: [
|
|
||||||
SamplePayloads.telstar18V,
|
|
||||||
SamplePayloads.telstar19V
|
|
||||||
]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum SampleStore {
|
enum SampleStore {
|
||||||
static let noModels = AppStore(initialState: SampleAppState.noModels, appReducer: appReducer)
|
static let `default` = AppStore(initialState: SampleAppState.default, appReducer: appReducer)
|
||||||
static let withModels = AppStore(initialState: SampleAppState.withModels, appReducer: appReducer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -27,10 +27,10 @@
|
||||||
"color" : {
|
"color" : {
|
||||||
"color-space" : "display-p3",
|
"color-space" : "display-p3",
|
||||||
"components" : {
|
"components" : {
|
||||||
"red" : "0.697",
|
"red" : "0.620",
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.697",
|
"blue" : "0.620",
|
||||||
"green" : "0.697"
|
"green" : "0.620"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
//
|
||||||
|
// CodingUserInfoKey+Extensions.swift
|
||||||
|
// SpaceXPayloadStats
|
||||||
|
//
|
||||||
|
// Created by CypherPoet on 11/26/19.
|
||||||
|
// ✌️
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
public extension CodingUserInfoKey {
|
||||||
|
|
||||||
|
/// Use for retrieving a Core Data managed object context from the `userInfo` dictionary
|
||||||
|
/// of a decoder instance.
|
||||||
|
static let managedObjectContext = CodingUserInfoKey(rawValue: "ManagedObjectContext")!
|
||||||
|
}
|
|
@ -9,6 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
@ -24,10 +25,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
let window = UIWindow(windowScene: windowScene)
|
let window = UIWindow(windowScene: windowScene)
|
||||||
let store = AppStore(initialState: AppState(), appReducer: appReducer)
|
let store = AppStore(initialState: AppState(), appReducer: appReducer)
|
||||||
|
|
||||||
|
// Get the managed object context from the shared persistent container.
|
||||||
|
let managedObjectContext = CoreDataManager.shared.mainContext
|
||||||
|
|
||||||
// Create the SwiftUI view that provides the window contents.
|
// Create the SwiftUI view that provides the window contents.
|
||||||
let entryView = MissionsListContainerView()
|
let entryView = MissionsListContainerView()
|
||||||
.accentColor(.purple)
|
.accentColor(.purple)
|
||||||
.environmentObject(store)
|
.environmentObject(store)
|
||||||
|
.environment(\.managedObjectContext, managedObjectContext)
|
||||||
|
|
||||||
// Use a UIHostingController as window root view controller.
|
// Use a UIHostingController as window root view controller.
|
||||||
window.rootViewController = UIHostingController(rootView: entryView)
|
window.rootViewController = UIHostingController(rootView: entryView)
|
||||||
|
@ -62,8 +67,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
// Called as the scene transitions from the foreground to the background.
|
// Called as the scene transitions from the foreground to the background.
|
||||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||||
// to restore the scene back to its current state.
|
// to restore the scene back to its current state.
|
||||||
|
CoreDataManager.shared.saveContexts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,14 +10,19 @@ import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
struct MissionDetailsView<Destination: View>: View {
|
struct MissionDetailsView<Destination: View>: View {
|
||||||
let buildDestination: ((Payload.ID) -> Destination)
|
@EnvironmentObject var store: AppStore
|
||||||
private let viewModel: MissionDetailsViewModel
|
|
||||||
|
let mission: Mission
|
||||||
|
let buildDestination: ((Payload) -> Destination)
|
||||||
|
|
||||||
|
@ObservedObject private var viewModel: MissionDetailsViewModel
|
||||||
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
mission: Mission,
|
mission: Mission,
|
||||||
buildDestination: @escaping ((Payload.ID) -> Destination)
|
buildDestination: @escaping ((Payload) -> Destination)
|
||||||
) {
|
) {
|
||||||
|
self.mission = mission
|
||||||
self.buildDestination = buildDestination
|
self.buildDestination = buildDestination
|
||||||
self.viewModel = MissionDetailsViewModel(mission: mission)
|
self.viewModel = MissionDetailsViewModel(mission: mission)
|
||||||
}
|
}
|
||||||
|
@ -44,7 +49,10 @@ extension MissionDetailsView {
|
||||||
payloadsSection
|
payloadsSection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitle(Text(viewModel.missionName), displayMode: .automatic)
|
.navigationBarTitle(Text(viewModel.missionName))
|
||||||
|
.onAppear {
|
||||||
|
self.store.send(PayloadsSideEffect.fetchPayloads(for: self.mission))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,9 +75,9 @@ extension MissionDetailsView {
|
||||||
|
|
||||||
private var payloadsSection: some View {
|
private var payloadsSection: some View {
|
||||||
Section(header: SectionHeader(title: "🛰 Payload History")) {
|
Section(header: SectionHeader(title: "🛰 Payload History")) {
|
||||||
ForEach(viewModel.payloadIDs, id: \.self) { payloadID in
|
ForEach(viewModel.payloads, id: \.self) { payload in
|
||||||
NavigationLink(destination: self.buildDestination(payloadID)) {
|
NavigationLink(destination: self.buildDestination(payload)) {
|
||||||
Text(payloadID)
|
Text(payload.payloadID ?? "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,6 +105,8 @@ struct MissionDetailsView_Previews: PreviewProvider {
|
||||||
mission: SampleMissions.telstar,
|
mission: SampleMissions.telstar,
|
||||||
buildDestination: { _ in EmptyView() }
|
buildDestination: { _ in EmptyView() }
|
||||||
)
|
)
|
||||||
|
.environmentObject(SampleStore.default)
|
||||||
|
.environment(\.managedObjectContext, SampleMOC.mainContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationView {
|
NavigationView {
|
||||||
|
@ -104,6 +114,8 @@ struct MissionDetailsView_Previews: PreviewProvider {
|
||||||
mission: SampleMissions.idridiumNext,
|
mission: SampleMissions.idridiumNext,
|
||||||
buildDestination: { _ in EmptyView() }
|
buildDestination: { _ in EmptyView() }
|
||||||
)
|
)
|
||||||
|
.environmentObject(SampleStore.default)
|
||||||
|
.environment(\.managedObjectContext, SampleMOC.mainContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,40 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import CoreData
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
|
||||||
struct MissionDetailsViewModel {
|
final class MissionDetailsViewModel: NSObject, ObservableObject {
|
||||||
let mission: Mission
|
let mission: Mission
|
||||||
|
|
||||||
|
|
||||||
|
@Published var payloads: [Payload] = []
|
||||||
|
|
||||||
|
|
||||||
|
init(mission: Mission) {
|
||||||
|
self.mission = mission
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.fetchedResultsController.delegate = self
|
||||||
|
|
||||||
|
fetchPayloads()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
lazy var fetchRequest: NSFetchRequest<Payload> = {
|
||||||
|
Payload.fetchRequest(for: mission)
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
lazy var fetchedResultsController: NSFetchedResultsController<Payload> = {
|
||||||
|
NSFetchedResultsController(
|
||||||
|
fetchRequest: fetchRequest,
|
||||||
|
managedObjectContext: CoreDataManager.shared.mainContext,
|
||||||
|
sectionNameKeyPath: nil,
|
||||||
|
cacheName: nil
|
||||||
|
)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,29 +49,65 @@ extension MissionDetailsViewModel {
|
||||||
var hasPayloads: Bool { !payloadIDs.isEmpty }
|
var hasPayloads: Bool { !payloadIDs.isEmpty }
|
||||||
var hasWebLinks: Bool { !webLinks.isEmpty }
|
var hasWebLinks: Bool { !webLinks.isEmpty }
|
||||||
|
|
||||||
var payloadIDs: [Payload.ID] { mission.payloadIDs }
|
var payloadIDs: [String] { mission.payloadIDs ?? [] }
|
||||||
|
|
||||||
var wikipediaURL: URL? { mission.wikipediaURL }
|
|
||||||
var twitterURL: URL? { mission.twitterURL }
|
|
||||||
var websiteURL: URL? { mission.websiteURL }
|
|
||||||
|
|
||||||
|
|
||||||
var webLinks: [(linkName: String, url: URL)] {
|
var webLinks: [(linkName: String, url: URL)] {
|
||||||
[
|
[
|
||||||
("Website", websiteURL),
|
("Website", mission.wikipediaURLString),
|
||||||
("Wikipedia", wikipediaURL),
|
("Wikipedia", mission.twitterURLString),
|
||||||
("Twitter", twitterURL),
|
("Twitter", mission.websiteURLString),
|
||||||
].compactMap { labelAndURLPair in
|
].compactMap { labelAndURLStringPair in
|
||||||
guard let url = labelAndURLPair.1 else { return nil }
|
guard
|
||||||
|
let urlString = labelAndURLStringPair.1,
|
||||||
|
let url = URL(string: urlString)
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
return (labelAndURLPair.0, url)
|
return (labelAndURLStringPair.0, url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var missionName: String { mission.name }
|
var missionName: String { mission.name ?? "" }
|
||||||
var missionDescription: String { mission.description }
|
var missionDescription: String { mission.missionDescription ?? "" }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Public Methods
|
||||||
|
extension MissionDetailsViewModel {
|
||||||
|
|
||||||
|
func fetchPayloads() {
|
||||||
|
// TODO: Better error handling here?
|
||||||
|
try? fetchedResultsController.performFetch()
|
||||||
|
setPayloads(from: fetchedResultsController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - NSFetchedResultsControllerDelegate
|
||||||
|
extension MissionDetailsViewModel: NSFetchedResultsControllerDelegate {
|
||||||
|
|
||||||
|
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||||
|
guard let controller = controller as? NSFetchedResultsController<Payload> else { return }
|
||||||
|
|
||||||
|
setPayloads(from: controller)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Private Helpers
|
||||||
|
private extension MissionDetailsViewModel {
|
||||||
|
|
||||||
|
func setPayloads(from fetchedResultsController: NSFetchedResultsController<Payload>) {
|
||||||
|
guard
|
||||||
|
let section = fetchedResultsController.sections?.first,
|
||||||
|
let fetchedPayloads = section.objects as? [Payload]
|
||||||
|
else {
|
||||||
|
payloads = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payloads = fetchedPayloads
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ struct MissionsListContainerView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Body
|
// MARK: - Body
|
||||||
extension MissionsListContainerView {
|
extension MissionsListContainerView {
|
||||||
|
|
||||||
|
@ -36,7 +37,6 @@ extension MissionsListContainerView {
|
||||||
|
|
||||||
private var missionsList: some View {
|
private var missionsList: some View {
|
||||||
MissionsListView(
|
MissionsListView(
|
||||||
missions: store.state.missionsState.missions,
|
|
||||||
buildDestination: buildDestination(forMission:)
|
buildDestination: buildDestination(forMission:)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -54,16 +54,13 @@ private extension MissionsListContainerView {
|
||||||
func buildDestination(forMission mission: Mission) -> some View {
|
func buildDestination(forMission mission: Mission) -> some View {
|
||||||
MissionDetailsView(
|
MissionDetailsView(
|
||||||
mission: mission,
|
mission: mission,
|
||||||
buildDestination: buildDestination(forPayloadID:)
|
buildDestination: buildDestination(forPayload:)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func buildDestination(forPayloadID payloadID: Payload.ID) -> some View {
|
func buildDestination(forPayload payload: Payload) -> some View {
|
||||||
PayloadDetailsView(
|
PayloadDetailsView(payload: payload)
|
||||||
payloadID: payloadID,
|
|
||||||
store: store
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +72,7 @@ struct MissionsListContainerView_Previews: PreviewProvider {
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
MissionsListContainerView()
|
MissionsListContainerView()
|
||||||
.environmentObject(SampleStore.noModels)
|
.environment(\.managedObjectContext, SampleMOC.mainContext)
|
||||||
|
.environmentObject(SampleStore.default)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,9 @@ import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
struct MissionsListView<Destination: View>: View {
|
struct MissionsListView<Destination: View>: View {
|
||||||
let missions: [Mission]
|
|
||||||
let buildDestination: ((Mission) -> Destination)
|
let buildDestination: ((Mission) -> Destination)
|
||||||
|
|
||||||
|
@FetchRequest(fetchRequest: Mission.FetchRequest.all, animation: nil) var missions: FetchedResults<Mission>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,13 +21,13 @@ extension MissionsListView {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
ForEach(missions) { mission in
|
ForEach(missions, id: \.self) { mission in
|
||||||
NavigationLink(destination: self.buildDestination(mission)) {
|
NavigationLink(destination: self.buildDestination(mission)) {
|
||||||
Text(mission.name)
|
Text(mission.name ?? "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitle("SpaceX Missions", displayMode: .automatic)
|
.navigationBarTitle("SpaceX Missions")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,10 +48,8 @@ struct MissionsListView_Previews: PreviewProvider {
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
MissionsListView(
|
MissionsListView(
|
||||||
missions: [
|
|
||||||
SampleMissions.telstar,
|
|
||||||
],
|
|
||||||
buildDestination: { _ in EmptyView() }
|
buildDestination: { _ in EmptyView() }
|
||||||
)
|
)
|
||||||
|
.environment(\.managedObjectContext, SampleMOC.mainContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,21 +7,15 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
|
||||||
struct PayloadDetailsView: View {
|
struct PayloadDetailsView: View {
|
||||||
@ObservedObject private var viewModel: PayloadDetailsViewModel
|
private let viewModel: PayloadDetailsViewModel
|
||||||
|
|
||||||
init(payloadID: Payload.ID, store: AppStore) {
|
|
||||||
self.viewModel = PayloadDetailsViewModel(
|
|
||||||
payloadID: payloadID,
|
|
||||||
store: store
|
|
||||||
)
|
|
||||||
|
|
||||||
// UINavigationBar.appearance().backgroundColor = .clear
|
init(payload: Payload) {
|
||||||
// UINavigationBar.appearance().largeTitleTextAttributes = [
|
self.viewModel = PayloadDetailsViewModel(payload: payload)
|
||||||
// .foregroundColor: UIColor(named: "LightGray1") ?? .systemGray
|
|
||||||
// ]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +53,6 @@ extension PayloadDetailsView {
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.padding(.bottom, 20)
|
.padding(.bottom, 20)
|
||||||
.onAppear(perform: viewModel.loadPayload)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,21 +160,13 @@ extension PayloadDetailsView {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Private Helpers
|
|
||||||
private extension PayloadDetailsView {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Preview
|
// MARK: - Preview
|
||||||
struct PayloadDetailsView_Previews: PreviewProvider {
|
struct PayloadDetailsView_Previews: PreviewProvider {
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
PayloadDetailsView(
|
PayloadDetailsView(payload: SamplePayloads.telstar18V)
|
||||||
payloadID: SamplePayloads.telstar18V.id,
|
.environment(\.managedObjectContext, SampleMOC.mainContext)
|
||||||
store: SampleStore.withModels
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,61 +10,38 @@ import SwiftUI
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
|
|
||||||
final class PayloadDetailsViewModel: ObservableObject {
|
struct PayloadDetailsViewModel {
|
||||||
private var subscriptions = Set<AnyCancellable>()
|
private(set) var payload: Payload
|
||||||
private let store: AppStore
|
|
||||||
private let payloadID: Payload.ID
|
|
||||||
|
|
||||||
|
|
||||||
@Published var payload: Payload?
|
|
||||||
|
|
||||||
|
|
||||||
init(
|
|
||||||
payloadID: Payload.ID,
|
|
||||||
store: AppStore
|
|
||||||
) {
|
|
||||||
self.payloadID = payloadID
|
|
||||||
self.store = store
|
|
||||||
|
|
||||||
setupSubscribers()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Computeds
|
// MARK: - Computeds
|
||||||
extension PayloadDetailsViewModel {
|
extension PayloadDetailsViewModel {
|
||||||
var payloadManufacturerText: String { payload?.manufacturer ?? "" }
|
var payloadManufacturerText: String { payload.manufacturer ?? "" }
|
||||||
var payloadNationalityText: String { payload?.nationality ?? "" }
|
var payloadNationalityText: String { payload.nationality ?? "" }
|
||||||
var payloadOrbitText: String { payload?.orbit ?? "" }
|
var payloadOrbitText: String { payload.orbit ?? "" }
|
||||||
|
|
||||||
|
|
||||||
var payloadNameText: String {
|
var payloadNameText: String {
|
||||||
guard let id = payload?.id else { return "" }
|
guard let id = payload.payloadID else { return "" }
|
||||||
return "🛰 \(id)"
|
return "🛰 \(id)"
|
||||||
}
|
}
|
||||||
|
|
||||||
var payloadTypeText: String {
|
var payloadTypeText: String {
|
||||||
guard let type = payload?.payloadType else { return "" }
|
guard let type = payload.payloadType else { return "" }
|
||||||
|
|
||||||
let emoji = payload?.payloadTypeEmoji ?? ""
|
let emoji = payload.payloadTypeEmoji ?? ""
|
||||||
|
|
||||||
return [emoji, type].joined(separator: " ")
|
return [emoji, type].joined(separator: " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var payloadMassText: String {
|
var payloadMassText: String { "\(payload.mass) kg" }
|
||||||
guard let mass = payload?.mass else { return "" }
|
|
||||||
|
|
||||||
return "\(mass) kg"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var periapsisText: String {
|
var periapsisText: String {
|
||||||
guard
|
guard
|
||||||
let periapsis = payload?.orbitParams.periapsis,
|
let periapsis = payload.orbitParams?.periapsis,
|
||||||
let formattedValue = NumberFormatters.apsisLength.string(for: periapsis)
|
let formattedValue = NumberFormatters.apsisLength.string(for: periapsis)
|
||||||
else { return "" }
|
else { return "" }
|
||||||
|
|
||||||
|
@ -74,7 +51,7 @@ extension PayloadDetailsViewModel {
|
||||||
|
|
||||||
var apoapsisText: String {
|
var apoapsisText: String {
|
||||||
guard
|
guard
|
||||||
let apoapsis = payload?.orbitParams.apoapsis,
|
let apoapsis = payload.orbitParams?.apoapsis,
|
||||||
let formattedValue = NumberFormatters.apsisLength.string(for: apoapsis)
|
let formattedValue = NumberFormatters.apsisLength.string(for: apoapsis)
|
||||||
else { return "" }
|
else { return "" }
|
||||||
|
|
||||||
|
@ -82,10 +59,10 @@ extension PayloadDetailsViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var orbitalDiameter: Payload.OrbitParams.Kilometers? {
|
var orbitalDiameter: OrbitParams.Kilometers? {
|
||||||
guard
|
guard
|
||||||
let apoapsis = payload?.orbitParams.apoapsis,
|
let apoapsis = payload.orbitParams?.apoapsis,
|
||||||
let periapsis = payload?.orbitParams.periapsis
|
let periapsis = payload.orbitParams?.periapsis
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
return apoapsis + periapsis
|
return apoapsis + periapsis
|
||||||
|
@ -97,7 +74,7 @@ extension PayloadDetailsViewModel {
|
||||||
|
|
||||||
var apoapsisPct: CGFloat {
|
var apoapsisPct: CGFloat {
|
||||||
guard
|
guard
|
||||||
let apoapsis = payload?.orbitParams.apoapsis,
|
let apoapsis = payload.orbitParams?.apoapsis,
|
||||||
let orbitalDiameter = orbitalDiameter
|
let orbitalDiameter = orbitalDiameter
|
||||||
else { return 0 }
|
else { return 0 }
|
||||||
|
|
||||||
|
@ -107,7 +84,7 @@ extension PayloadDetailsViewModel {
|
||||||
|
|
||||||
var periapsisPct: CGFloat {
|
var periapsisPct: CGFloat {
|
||||||
guard
|
guard
|
||||||
let periapsis = payload?.orbitParams.periapsis,
|
let periapsis = payload.orbitParams?.periapsis,
|
||||||
let orbitalDiameter = orbitalDiameter
|
let orbitalDiameter = orbitalDiameter
|
||||||
else { return 0 }
|
else { return 0 }
|
||||||
|
|
||||||
|
@ -118,36 +95,14 @@ extension PayloadDetailsViewModel {
|
||||||
|
|
||||||
// MARK: - Public Methods
|
// MARK: - Public Methods
|
||||||
extension PayloadDetailsViewModel {
|
extension PayloadDetailsViewModel {
|
||||||
|
|
||||||
func loadPayload() {
|
|
||||||
if let payload = store.state.payloadsState.payloadsByID[payloadID] {
|
|
||||||
self.payload = payload
|
|
||||||
} else {
|
|
||||||
store.send(PayloadsSideEffect.fetchPayload(withID: payloadID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Publishers
|
// MARK: - Publishers
|
||||||
extension PayloadDetailsViewModel {
|
extension PayloadDetailsViewModel {
|
||||||
|
|
||||||
private var payloadPublisher: AnyPublisher<Payload?, Never> {
|
|
||||||
store.$state
|
|
||||||
.map(\.payloadsState.payloadsByID)
|
|
||||||
.map { payloadsByID in payloadsByID[self.payloadID] }
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Private Helpers
|
// MARK: - Private Helpers
|
||||||
private extension PayloadDetailsViewModel {
|
private extension PayloadDetailsViewModel {
|
||||||
|
|
||||||
func setupSubscribers() {
|
|
||||||
payloadPublisher
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.assign(to: \.payload, on: self)
|
|
||||||
.store(in: &subscriptions)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Day 60: Milestone for Projects 10-12
|
# Day 60: Milestone for Projects 10-12 (Part One)
|
||||||
|
|
||||||
_Follow along at https://www.hackingwithswift.com/100/swiftui/60_.
|
_Follow along at https://www.hackingwithswift.com/100/swiftui/60_.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue