Refactor app to use Core Data.

This commit is contained in:
CypherPoet 2019-11-27 16:15:42 -06:00
parent 508e8e28de
commit 46f3a93100
36 changed files with 1036 additions and 427 deletions

View File

@ -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 58:** [_Project 12: Core Data_ (Part Two)](./day-058/)
- **Day 59:** [_Project 12: Core Data_ (Part Three)](./day-059/)
- **Day 60:** [Milestone for Projects 10-12 (Part One)](./day-060/)
</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>
- [Milestone 1: RockPaperQuizzers](./day-025/RockPaperQuizzers/)
- [Milestone Project 1: RockPaperQuizzers](./day-025/RockPaperQuizzers/)
<div style="text-align: center;">
<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;">
<img src="./day-053/Projects/Bookworm/Screenshots/day-56-recording-1.gif" width="300px"/>
</div>
- [Project 12: Exploring Core Data](./day-057/Projects/ExploringCoreData)
- [Milestone Project 4: SpaceX Payload Stats](./day-060/Project/SpaceXPayloadStats/)

View File

@ -7,6 +7,19 @@
objects = {
/* 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 */; };
F3F9F3A723892C0300864243 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3A623892C0300864243 /* SceneDelegate.swift */; };
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 */; };
F3F9F3CD2389556700864243 /* MissionsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3CC2389556700864243 /* MissionsState.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 */; };
F3F9F3DC2389743600864243 /* MissionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3DB2389743600864243 /* MissionsListView.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 */; };
F3F9F3F7238B001600864243 /* InfoHeaderTextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F9F3F5238B001600864243 /* InfoHeaderTextStyle.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 */; };
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 */
/* 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; };
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>"; };
@ -50,9 +73,6 @@
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>"; };
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>"; };
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>"; };
@ -65,9 +85,10 @@
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>"; };
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>"; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@ -76,6 +97,7 @@
buildActionMask = 2147483647;
files = (
F3F9F3C2238952F200864243 /* CypherPoetNetStack in Frameworks */,
F3524240238EE983009DF1F9 /* CypherPoetCoreDataKit in Frameworks */,
F3F9F3BF238952C500864243 /* CypherPoetSwiftUIKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -83,6 +105,23 @@
/* End PBXFrameworksBuildPhase 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 = {
isa = PBXGroup;
children = (
@ -130,6 +169,8 @@
children = (
F3F9F3C42389531800864243 /* Models */,
F3F9F3C32389531400864243 /* State */,
F3F9F3FF238C357E00864243 /* CoreDataManager.swift */,
F3F9F401238C78F500864243 /* SpaceXPayloadStats.xcdatamodeld */,
);
path = Data;
sourceTree = "<group>";
@ -137,6 +178,7 @@
F3F9F3B923894FE700864243 /* Reusables */ = {
isa = PBXGroup;
children = (
F30B3644238D88340041AC59 /* Extensions */,
F3F9F3F3238AFFEB00864243 /* Styles */,
F3F9F3EE238AB00900864243 /* Formatters */,
F3F9F3BA23894FEB00864243 /* Views */,
@ -182,6 +224,7 @@
F3F9F3C42389531800864243 /* Models */ = {
isa = PBXGroup;
children = (
F3A61A82238C994E00279192 /* Orbit Params */,
F3F9F3D22389565900864243 /* Mission */,
F3F9F3D32389565D00864243 /* Payload */,
);
@ -199,7 +242,10 @@
F3F9F3D22389565900864243 /* Mission */ = {
isa = PBXGroup;
children = (
F3F9F3D02389565000864243 /* Mission.swift */,
F3A61A7A238C98EE00279192 /* Mission+CoreDataClass.swift */,
F3A61A7B238C98EE00279192 /* Mission+CoreDataProperties.swift */,
F352423A238EE7B2009DF1F9 /* Mission+FetchHelpers.swift */,
F3A61A87238C9D8A00279192 /* Mission+Computeds.swift */,
);
path = Mission;
sourceTree = "<group>";
@ -207,9 +253,11 @@
F3F9F3D32389565D00864243 /* Payload */ = {
isa = PBXGroup;
children = (
F3F9F3D42389567D00864243 /* Payload.swift */,
F3F9F3F9238B036E00864243 /* Payload+Computeds.swift */,
F3F9F3D62389611800864243 /* Payload+OrbitParams.swift */,
F3A61A7E238C994900279192 /* Payload+CoreDataClass.swift */,
F3A61A7F238C994900279192 /* Payload+CoreDataProperties.swift */,
F352423C238EE80C009DF1F9 /* Payload+FetchHelpers.swift */,
F3A61A89238C9E1100279192 /* Payload+Comparable.swift */,
F3A61A8B238D67DF00279192 /* Payload+Computeds.swift */,
);
path = Payload;
sourceTree = "<group>";
@ -297,6 +345,7 @@
packageProductDependencies = (
F3F9F3BE238952C500864243 /* CypherPoetSwiftUIKit */,
F3F9F3C1238952F200864243 /* CypherPoetNetStack */,
F352423F238EE983009DF1F9 /* CypherPoetCoreDataKit */,
);
productName = SpaceXPayloadStats;
productReference = F3F9F3A123892C0300864243 /* SpaceXPayloadStats.app */;
@ -329,6 +378,7 @@
packageReferences = (
F3F9F3BD238952C500864243 /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */,
F3F9F3C0238952F200864243 /* XCRemoteSwiftPackageReference "CypherPoetNetStack" */,
F352423E238EE983009DF1F9 /* XCRemoteSwiftPackageReference "CypherPoetCoreDataKit" */,
);
productRefGroup = F3F9F3A223892C0300864243 /* Products */;
projectDirPath = "";
@ -361,25 +411,35 @@
F3F9F3F2238AF96600864243 /* NumberFormatters.swift in Sources */,
F3F9F3F0238AB02B00864243 /* DateFormatter+iso8601Full.swift in Sources */,
F3F9F3A523892C0300864243 /* AppDelegate.swift in Sources */,
F3F9F3D72389611800864243 /* Payload+OrbitParams.swift in Sources */,
F3F9F3CD2389556700864243 /* MissionsState.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 */,
F3F9F3FA238B036F00864243 /* Payload+Computeds.swift in Sources */,
F3F9F3E62389828D00864243 /* Endpoint+SpaceXAPI.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 */,
F3A61A8A238C9E1100279192 /* Payload+Comparable.swift in Sources */,
F3A61A7C238C98EE00279192 /* Mission+CoreDataClass.swift in Sources */,
F3F9F3C7238953B600864243 /* SampleData.swift in Sources */,
F3A61A80238C994900279192 /* Payload+CoreDataClass.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 */,
F3F9F3F7238B001600864243 /* InfoHeaderTextStyle.swift in Sources */,
F3F9F3F8238B001600864243 /* Text+InfoHeaderStyle.swift in Sources */,
F3F9F3FE238BF79A00864243 /* MissionDetailsViewModel.swift in Sources */,
F3F9F3CF2389558000864243 /* PayloadsState.swift in Sources */,
F3F9F3D12389565000864243 /* Mission.swift in Sources */,
F3A61A7D238C98EE00279192 /* Mission+CoreDataProperties.swift in Sources */,
F3F9F3DC2389743600864243 /* MissionsListView.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 */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -576,6 +636,14 @@
/* End XCConfigurationList 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" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/CypherPoet/CypherPoetSwiftUIKit.git";
@ -595,6 +663,11 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
F352423F238EE983009DF1F9 /* CypherPoetCoreDataKit */ = {
isa = XCSwiftPackageProductDependency;
package = F352423E238EE983009DF1F9 /* XCRemoteSwiftPackageReference "CypherPoetCoreDataKit" */;
productName = CypherPoetCoreDataKit;
};
F3F9F3BE238952C500864243 /* CypherPoetSwiftUIKit */ = {
isa = XCSwiftPackageProductDependency;
package = F3F9F3BD238952C500864243 /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */;
@ -606,6 +679,19 @@
productName = CypherPoetNetStack;
};
/* 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 */;
}

View File

@ -1,6 +1,15 @@
{
"object": {
"pins": [
{
"package": "CypherPoetCoreDataKit",
"repositoryURL": "https://github.com/CypherPoet/CypherPoetCoreDataKit.git",
"state": {
"branch": null,
"revision": "4a628bc53793aa8868e1fa9bb12850c0b9331f38",
"version": "0.0.1"
}
},
{
"package": "CypherPoetNetStack",
"repositoryURL": "https://github.com/CypherPoet/CypherPoetNetStack.git",

View File

@ -50,6 +50,16 @@
ReferencedContainer = "container:SpaceXPayloadStats.xcodeproj">
</BuildableReference>
</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>
<ProfileAction
buildConfiguration = "Release"

View File

@ -15,6 +15,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
CoreDataManager.shared.setup()
return true
}

View File

@ -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"
}
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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
}()
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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?
}

View File

@ -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
}
}

View File

@ -2,7 +2,7 @@
// Payload+Computeds.swift
// SpaceXPayloadStats
//
// Created by CypherPoet on 11/24/19.
// Created by CypherPoet on 11/26/19.
//
//
@ -12,7 +12,7 @@ import Foundation
extension Payload {
var payloadTypeEmoji: String? {
let type = payloadType.lowercased()
guard let type = payloadType?.lowercased() else { return nil }
if type.starts(with: "satellite") {
return "🛰"
@ -22,5 +22,14 @@ extension Payload {
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
}()
}

View File

@ -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)
}
}

View File

@ -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?
}

View File

@ -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
}
}

View File

@ -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"
}
}

View File

@ -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
}()
}

View File

@ -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>

View File

@ -10,7 +10,7 @@ import CypherPoetSwiftUIKit
struct MissionsState: Codable {
var missions: [Mission] = []
var missionsFetchErrorMessage: String?
}
@ -20,11 +20,17 @@ enum MissionsSideEffect: SideEffect {
func mapToAction() -> AnyPublisher<AppAction, Never> {
switch self {
case .fetch:
let context = CoreDataManager.shared.backgroundContext
return Dependencies.spaceXAPIService
.fetchMissions()
.breakpointOnError()
.replaceError(with: [])
.map { AppAction.missions(.set(missions: $0)) }
.fetchMissions(using: context)
.map { _ in
CoreDataManager.shared.save(context)
return AppAction.missions(.set(fetchErrorMessage: nil))
}
.catch { error in
Just(AppAction.missions(.set(fetchErrorMessage: error.errorDescription)))
}
.eraseToAnyPublisher()
}
}
@ -33,7 +39,7 @@ enum MissionsSideEffect: SideEffect {
enum MissionsAction {
case set(missions: [Mission])
case set(fetchErrorMessage: String?)
}
@ -41,7 +47,7 @@ enum MissionsAction {
// MARK: - Reducer
let missionsReducer = Reducer<MissionsState, MissionsAction> { state, action in
switch action {
case let .set(missions):
state.missions = missions
case .set(let fetchErrorMessage):
state.missionsFetchErrorMessage = fetchErrorMessage
}
}

View File

@ -14,22 +14,27 @@ import CypherPoetSwiftUIKit
struct PayloadsState: Codable {
var payloadFetchErrorMessage: String?
var payloadsByID: [Payload.ID: Payload] = [:]
var payloadsByMissionID: [Mission.ID: [Payload]] = [:]
}
enum PayloadsSideEffect: SideEffect {
case fetchPayload(withID: Payload.ID)
case fetchPayloads(for: Mission)
// case fetchPayload(withID: String)
func mapToAction() -> AnyPublisher<AppAction, Never> {
let context = CoreDataManager.shared.backgroundContext
switch self {
case let .fetchPayload(payloadID):
case let .fetchPayloads(mission):
return Dependencies.spaceXAPIService
.fetchPayload(for: payloadID)
.map { AppAction.payloads(.setPayload($0)) }
.catch {
Just(AppAction.payloads(.setFetchError(message: $0.localizedDescription)))
.fetchPayloads(with: mission.payloadIDs ?? [], using: context)
.map { _ in
CoreDataManager.shared.save(context)
return AppAction.payloads(.set(fetchErrorMessage: nil))
}
.catch { error in
Just(AppAction.payloads(.set(fetchErrorMessage: error.errorDescription)))
}
.eraseToAnyPublisher()
}
@ -38,9 +43,7 @@ enum PayloadsSideEffect: SideEffect {
enum PayloadsAction {
case setPayload(Payload)
case setPayloadsForMission([Payload], missionID: Mission.ID)
case setFetchError(message: String)
case set(fetchErrorMessage: String?)
}
@ -48,11 +51,7 @@ enum PayloadsAction {
// MARK: - Reducer
let payloadsReducer = Reducer<PayloadsState, PayloadsAction> { state, action in
switch action {
case .setPayload(let payload):
state.payloadsByID[payload.id] = payload
case .setPayloadsForMission(let payloads, let missionID):
state.payloadsByMissionID[missionID] = payloads
case .setFetchError(let message):
state.payloadFetchErrorMessage = message
case .set(let fetchErrorMessage):
state.payloadFetchErrorMessage = fetchErrorMessage
}
}

View File

@ -20,7 +20,7 @@ extension Endpoint {
path: "/v3/missions"
)
public static func payload(for payloadID: Payload.ID) -> Endpoint {
public static func payload(for payloadID: String) -> Endpoint {
Endpoint(
host: Self.host,
path: "/v3/payloads/\(payloadID)"

View File

@ -8,6 +8,7 @@
import Foundation
import Combine
import CoreData
import CypherPoetNetStack
@ -29,40 +30,68 @@ public final class SpaceXAPIService: ModelTransportRequestPublishing {
// MARK: - Core Methods
extension SpaceXAPIService {
func fetchMissions() -> AnyPublisher<[Mission], SpaceXAPIService.Error> {
let endpoint = Endpoint.SpaceXAPI.missions
guard let url = endpoint.url else {
func fetchMissions(using context: NSManagedObjectContext) -> AnyPublisher<[Mission], SpaceXAPIService.Error> {
guard let url = Endpoint.SpaceXAPI.missions.url else {
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(
URLRequest(url: url),
parsingResponseOn: apiQueue,
with: JSONDecoder()
with: decoder
)
.mapError { .network(error: $0) }
.eraseToAnyPublisher()
}
func fetchPayload(for payloadID: Payload.ID) -> AnyPublisher<Payload, SpaceXAPIService.Error> {
let endpoint = Endpoint.SpaceXAPI.payload(for: payloadID)
guard let url = endpoint.url else {
func fetchPayload(
for payloadID: String,
using context: NSManagedObjectContext
) -> AnyPublisher<Payload, SpaceXAPIService.Error> {
guard let url = Endpoint.SpaceXAPI.payload(for: payloadID).url else {
preconditionFailure("Unable to make url for endpoint")
}
let decoder = Payload.decoder
decoder.userInfo[.managedObjectContext] = context
print("Fetching payload at URL path: \(url.absoluteString)")
return perform(
URLRequest(url: url),
parsingResponseOn: apiQueue,
with: Payload.decoder
with: decoder
)
.mapError { .network(error: $0) }
.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()
}
}

View File

@ -10,31 +10,42 @@ import SwiftUI
import CoreData
enum SampleMOC {
static let mainContext = CoreDataManager.shared.mainContext
static let backgroundContext = CoreDataManager.shared.backgroundContext
}
enum SampleMissions {
static let telstar = Mission(
missionID: "F4F83DE",
name: "Telstar",
manufacturers: [
static let telstar: Mission = {
let mission = Mission(context: SampleMOC.mainContext)
mission.missionID = "F4F83DE"
mission.name = "Telstar"
mission.manufacturers = [
"SSL",
],
payloadIDs: [
]
mission.payloadIDs = [
SamplePayloads.telstar18V.payloadID,
SamplePayloads.telstar19V.payloadID,
],
wikipediaURL: URL(string: "https://en.wikipedia.org/wiki/Telesat"),
websiteURL: URL(string: "https://www.telesat.com/"),
twitterURL: 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."
)
].compactMap { $0 }
mission.wikipediaURLString = "https://en.wikipedia.org/wiki/Telesat"
mission.websiteURLString = "https://www.telesat.com/"
mission.twitterURLString = nil
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."
return mission
}()
static let idridiumNext = 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",
],
payloadIDs: [
]
mission.payloadIDs = [
"Iridium NEXT 1",
"Iridium NEXT 2",
"Iridium NEXT 3",
@ -43,92 +54,101 @@ enum SampleMissions {
"Iridium NEXT 6",
"Iridium NEXT 7",
"Iridium NEXT 8",
],
wikipediaURL: URL(string: "https://en.wikipedia.org/wiki/Iridium_satellite_constellation"),
websiteURL: URL(string: "https://www.iridiumnext.com/"),
twitterURL: 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.wikipediaURLString = "https://en.wikipedia.org/wiki/Iridium_satellite_constellation"
mission.websiteURLString = "https://www.iridiumnext.com/"
mission.twitterURLString = nil
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 {
static let telstar18V = Payload(
payloadID: "Telstar 18V",
payloadType: "Satellite",
isReused: false,
manufacturer: "SSL",
customers: [
static let telstar18V: Payload = {
let payload = Payload(context: SampleMOC.mainContext)
payload.payloadID = "Telstar 18V"
payload.payloadType = "Satellite"
payload.isReused = false
payload.manufacturer = "SSL"
payload.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
)
)
]
payload.nationality = "Canada"
payload.mass = 7060
payload.orbit = "GTO"
payload.orbitParams = SampleOrbitParams.fullParams1
try? SampleMOC.mainContext.save()
return payload
}()
static let telstar19V = Payload(
payloadID: "Telstar 19V",
payloadType: "Satellite",
isReused: false,
manufacturer: "SSL",
customers: [
static let telstar19V: Payload = {
let payload = Payload(context: SampleMOC.mainContext)
payload.payloadID = "Telstar 19V"
payload.payloadType = "Satellite"
payload.isReused = false
payload.manufacturer = "SSL"
payload.customers = [
"Telesat",
],
nationality: "Canada",
mass: 7076,
orbit: "GTO",
orbitParams: Payload.OrbitParams(
referenceSystem: "geocentric",
regime: "geostationary",
longitude: -65,
periapsis: 35778.442,
apoapsis: 35797.08,
epoch: Date(),
meanAnomaly: 69.209,
lifeSpanYears: 15,
periodMinutes: 35778.442
)
)
]
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 {
static let noModels = 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
]
]
)
)
static let `default` = AppState()
}
enum SampleStore {
static let noModels = AppStore(initialState: SampleAppState.noModels, appReducer: appReducer)
static let withModels = AppStore(initialState: SampleAppState.withModels, appReducer: appReducer)
static let `default` = AppStore(initialState: SampleAppState.default, appReducer: appReducer)
}

View File

@ -27,10 +27,10 @@
"color" : {
"color-space" : "display-p3",
"components" : {
"red" : "0.697",
"red" : "0.620",
"alpha" : "1.000",
"blue" : "0.697",
"green" : "0.697"
"blue" : "0.620",
"green" : "0.620"
}
}
},

View File

@ -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")!
}

View File

@ -9,6 +9,7 @@
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
@ -24,10 +25,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let window = UIWindow(windowScene: windowScene)
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.
let entryView = MissionsListContainerView()
.accentColor(.purple)
.environmentObject(store)
.environment(\.managedObjectContext, managedObjectContext)
// Use a UIHostingController as window root view controller.
window.rootViewController = UIHostingController(rootView: entryView)
@ -62,8 +67,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// 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
// to restore the scene back to its current state.
CoreDataManager.shared.saveContexts()
}
}

View File

@ -10,14 +10,19 @@ import SwiftUI
struct MissionDetailsView<Destination: View>: View {
let buildDestination: ((Payload.ID) -> Destination)
private let viewModel: MissionDetailsViewModel
@EnvironmentObject var store: AppStore
let mission: Mission
let buildDestination: ((Payload) -> Destination)
@ObservedObject private var viewModel: MissionDetailsViewModel
init(
mission: Mission,
buildDestination: @escaping ((Payload.ID) -> Destination)
buildDestination: @escaping ((Payload) -> Destination)
) {
self.mission = mission
self.buildDestination = buildDestination
self.viewModel = MissionDetailsViewModel(mission: mission)
}
@ -44,7 +49,10 @@ extension MissionDetailsView {
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 {
Section(header: SectionHeader(title: "🛰 Payload History")) {
ForEach(viewModel.payloadIDs, id: \.self) { payloadID in
NavigationLink(destination: self.buildDestination(payloadID)) {
Text(payloadID)
ForEach(viewModel.payloads, id: \.self) { payload in
NavigationLink(destination: self.buildDestination(payload)) {
Text(payload.payloadID ?? "")
}
}
}
@ -97,6 +105,8 @@ struct MissionDetailsView_Previews: PreviewProvider {
mission: SampleMissions.telstar,
buildDestination: { _ in EmptyView() }
)
.environmentObject(SampleStore.default)
.environment(\.managedObjectContext, SampleMOC.mainContext)
}
NavigationView {
@ -104,6 +114,8 @@ struct MissionDetailsView_Previews: PreviewProvider {
mission: SampleMissions.idridiumNext,
buildDestination: { _ in EmptyView() }
)
.environmentObject(SampleStore.default)
.environment(\.managedObjectContext, SampleMOC.mainContext)
}
}
}

View File

@ -7,10 +7,40 @@
//
import SwiftUI
import CoreData
import Combine
struct MissionDetailsViewModel {
final class MissionDetailsViewModel: NSObject, ObservableObject {
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 hasWebLinks: Bool { !webLinks.isEmpty }
var payloadIDs: [Payload.ID] { mission.payloadIDs }
var wikipediaURL: URL? { mission.wikipediaURL }
var twitterURL: URL? { mission.twitterURL }
var websiteURL: URL? { mission.websiteURL }
var payloadIDs: [String] { mission.payloadIDs ?? [] }
var webLinks: [(linkName: String, url: URL)] {
[
("Website", websiteURL),
("Wikipedia", wikipediaURL),
("Twitter", twitterURL),
].compactMap { labelAndURLPair in
guard let url = labelAndURLPair.1 else { return nil }
("Website", mission.wikipediaURLString),
("Wikipedia", mission.twitterURLString),
("Twitter", mission.websiteURLString),
].compactMap { labelAndURLStringPair in
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 missionDescription: String { mission.description }
var missionName: String { mission.name ?? "" }
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
}
}

View File

@ -14,6 +14,7 @@ struct MissionsListContainerView: View {
}
// MARK: - Body
extension MissionsListContainerView {
@ -36,7 +37,6 @@ extension MissionsListContainerView {
private var missionsList: some View {
MissionsListView(
missions: store.state.missionsState.missions,
buildDestination: buildDestination(forMission:)
)
}
@ -54,16 +54,13 @@ private extension MissionsListContainerView {
func buildDestination(forMission mission: Mission) -> some View {
MissionDetailsView(
mission: mission,
buildDestination: buildDestination(forPayloadID:)
buildDestination: buildDestination(forPayload:)
)
}
func buildDestination(forPayloadID payloadID: Payload.ID) -> some View {
PayloadDetailsView(
payloadID: payloadID,
store: store
)
func buildDestination(forPayload payload: Payload) -> some View {
PayloadDetailsView(payload: payload)
}
}
@ -75,6 +72,7 @@ struct MissionsListContainerView_Previews: PreviewProvider {
static var previews: some View {
MissionsListContainerView()
.environmentObject(SampleStore.noModels)
.environment(\.managedObjectContext, SampleMOC.mainContext)
.environmentObject(SampleStore.default)
}
}

View File

@ -10,8 +10,9 @@ import SwiftUI
struct MissionsListView<Destination: View>: View {
let missions: [Mission]
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 {
List {
ForEach(missions) { mission in
ForEach(missions, id: \.self) { mission in
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 {
MissionsListView(
missions: [
SampleMissions.telstar,
],
buildDestination: { _ in EmptyView() }
)
.environment(\.managedObjectContext, SampleMOC.mainContext)
}
}

View File

@ -7,21 +7,15 @@
//
import SwiftUI
import Combine
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
// UINavigationBar.appearance().largeTitleTextAttributes = [
// .foregroundColor: UIColor(named: "LightGray1") ?? .systemGray
// ]
init(payload: Payload) {
self.viewModel = PayloadDetailsViewModel(payload: payload)
}
}
@ -59,7 +53,6 @@ extension PayloadDetailsView {
.foregroundColor(.white)
.padding(.horizontal)
.padding(.bottom, 20)
.onAppear(perform: viewModel.loadPayload)
}
}
}
@ -167,21 +160,13 @@ extension PayloadDetailsView {
}
// MARK: - Private Helpers
private extension PayloadDetailsView {
}
// MARK: - Preview
struct PayloadDetailsView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
PayloadDetailsView(
payloadID: SamplePayloads.telstar18V.id,
store: SampleStore.withModels
)
PayloadDetailsView(payload: SamplePayloads.telstar18V)
.environment(\.managedObjectContext, SampleMOC.mainContext)
}
}
}

View File

@ -10,61 +10,38 @@ import SwiftUI
import Combine
final class PayloadDetailsViewModel: ObservableObject {
private var subscriptions = Set<AnyCancellable>()
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()
}
struct PayloadDetailsViewModel {
private(set) var payload: Payload
}
// MARK: - Computeds
extension PayloadDetailsViewModel {
var payloadManufacturerText: String { payload?.manufacturer ?? "" }
var payloadNationalityText: String { payload?.nationality ?? "" }
var payloadOrbitText: String { payload?.orbit ?? "" }
var payloadManufacturerText: String { payload.manufacturer ?? "" }
var payloadNationalityText: String { payload.nationality ?? "" }
var payloadOrbitText: String { payload.orbit ?? "" }
var payloadNameText: String {
guard let id = payload?.id else { return "" }
guard let id = payload.payloadID else { return "" }
return "🛰 \(id)"
}
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: " ")
}
var payloadMassText: String {
guard let mass = payload?.mass else { return "" }
return "\(mass) kg"
}
var payloadMassText: String { "\(payload.mass) kg" }
var periapsisText: String {
guard
let periapsis = payload?.orbitParams.periapsis,
let periapsis = payload.orbitParams?.periapsis,
let formattedValue = NumberFormatters.apsisLength.string(for: periapsis)
else { return "" }
@ -74,7 +51,7 @@ extension PayloadDetailsViewModel {
var apoapsisText: String {
guard
let apoapsis = payload?.orbitParams.apoapsis,
let apoapsis = payload.orbitParams?.apoapsis,
let formattedValue = NumberFormatters.apsisLength.string(for: apoapsis)
else { return "" }
@ -82,10 +59,10 @@ extension PayloadDetailsViewModel {
}
var orbitalDiameter: Payload.OrbitParams.Kilometers? {
var orbitalDiameter: OrbitParams.Kilometers? {
guard
let apoapsis = payload?.orbitParams.apoapsis,
let periapsis = payload?.orbitParams.periapsis
let apoapsis = payload.orbitParams?.apoapsis,
let periapsis = payload.orbitParams?.periapsis
else { return nil }
return apoapsis + periapsis
@ -97,7 +74,7 @@ extension PayloadDetailsViewModel {
var apoapsisPct: CGFloat {
guard
let apoapsis = payload?.orbitParams.apoapsis,
let apoapsis = payload.orbitParams?.apoapsis,
let orbitalDiameter = orbitalDiameter
else { return 0 }
@ -107,7 +84,7 @@ extension PayloadDetailsViewModel {
var periapsisPct: CGFloat {
guard
let periapsis = payload?.orbitParams.periapsis,
let periapsis = payload.orbitParams?.periapsis,
let orbitalDiameter = orbitalDiameter
else { return 0 }
@ -118,36 +95,14 @@ extension PayloadDetailsViewModel {
// MARK: - Public Methods
extension PayloadDetailsViewModel {
func loadPayload() {
if let payload = store.state.payloadsState.payloadsByID[payloadID] {
self.payload = payload
} else {
store.send(PayloadsSideEffect.fetchPayload(withID: payloadID))
}
}
}
// MARK: - Publishers
extension PayloadDetailsViewModel {
private var payloadPublisher: AnyPublisher<Payload?, Never> {
store.$state
.map(\.payloadsState.payloadsByID)
.map { payloadsByID in payloadsByID[self.payloadID] }
.eraseToAnyPublisher()
}
}
// MARK: - Private Helpers
private extension PayloadDetailsViewModel {
func setupSubscribers() {
payloadPublisher
.receive(on: DispatchQueue.main)
.assign(to: \.payload, on: self)
.store(in: &subscriptions)
}
}

View File

@ -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_.