This commit is contained in:
CypherPoet 2020-01-29 21:47:10 -06:00
commit 85b7673872
43 changed files with 2554 additions and 1 deletions

View File

@ -101,12 +101,15 @@ I'm currently seeking freelance, remote opportunities as an iOS developer! If yo
- **Day 92:** [_Project 18: Layout And Geometry (Part One)_](./day-092/)
- **Day 93:** [_Project 18: Layout And Geometry (Part Two)_](./day-093/)
- **Day 94:** [_Project 18: Layout And Geometry (Part Three)_](./day-094/)
- **Day 95:** [Milestone for Projects 16-18](./day-095/)
- **Day 96:** [_Project 19: PadFinder (Part One)_](./day-096/)
- **Day 97:** [_Project 19: PadFinder (Part Two)_](./day-097/)
</details>
**Latest Day:**
- **Day 95:** [Milestone for Projects 16-18](./day-095/)
- **Day 98:** [_Project 19: PadFinder (Part Three)_](./day-098/)

View File

@ -0,0 +1,46 @@
//: [Previous](@previous)
import UIKit
//let urlString = #"https:\/\/www.google.ee\/maps\/place\/39°15'46.2\"S+177°51'52.1\"E\/"#
//let urlString = #"https://www.google.com/maps/place/30°24'08.0"N+130°58'30.0"E/"#
//let urlString = #"https:\/\/en.wikipedia.org\/wiki\/ELA-1"#
let urlString = #"https:\/\/twitter.com\/rocketlab"#
print(urlString)
print(urlString.replacingOccurrences(of: #"\"#, with: ""))
urlString
urlString.removingPercentEncoding
let url = URL(
string: urlString
.replacingOccurrences(of: #"\"#, with: "")
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
)
//
//print(url?.absoluteURL)
//print(url?.absoluteURL.path)
//print(url!.host)
//print(url!.host!.replacingOccurrences(of: "www.", with: ""))
//print(url!.host!.replacingOccurrences(of: "www.", with: "").replacingOccurrences(of: ".com", with: ""))
/// Strips the leading "sub domain" and trailing "top-level domain"
/// parts (including the ".") from a URL `host` string
print(url!.host!.split(separator: ".").count <= 2)
print(
url!.host!
.replacingOccurrences(of: "^(\\w*\\.){1}", with: "", options: .regularExpression)
.replacingOccurrences(of: "\\.(.*)", with: "", options: .regularExpression)
.capitalized
)
//: [Next](@next)

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='6.0' target-platform='ios'/>

View File

@ -0,0 +1,604 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
F32ED61823DF69E6006A5195 /* PadsListContainerView+WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32ED61723DF69E6006A5195 /* PadsListContainerView+WelcomeView.swift */; };
F32ED61A23DF91C1006A5195 /* Pad+PadType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32ED61923DF91C1006A5195 /* Pad+PadType.swift */; };
F32ED61D23DF9B46006A5195 /* PadDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32ED61C23DF9B46006A5195 /* PadDetailsView.swift */; };
F32ED61F23DF9B7C006A5195 /* PadDetailsView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32ED61E23DF9B7C006A5195 /* PadDetailsView+ViewModel.swift */; };
F32ED62223DFB836006A5195 /* NumberFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32ED62123DFB836006A5195 /* NumberFormatters.swift */; };
F32ED62423DFC7D5006A5195 /* Pad+SnapshotUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32ED62323DFC7D5006A5195 /* Pad+SnapshotUtils.swift */; };
F32ED62623E0D525006A5195 /* Pad+Computeds.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32ED62523E0D525006A5195 /* Pad+Computeds.swift */; };
F32ED62923E0F0C6006A5195 /* MapSnapshottingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32ED62823E0F0C6006A5195 /* MapSnapshottingService.swift */; };
F32ED62B23E0F0F1006A5195 /* MapSnapshotServicing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32ED62A23E0F0F1006A5195 /* MapSnapshotServicing.swift */; };
F32ED62E23E163BB006A5195 /* CypherPoetPropertyWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = F32ED62D23E163BB006A5195 /* CypherPoetPropertyWrappers */; };
F331C45C23DDB0AE0061925E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C45B23DDB0AE0061925E /* AppDelegate.swift */; };
F331C45E23DDB0AE0061925E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C45D23DDB0AE0061925E /* SceneDelegate.swift */; };
F331C46223DDB0B00061925E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F331C46123DDB0B00061925E /* Assets.xcassets */; };
F331C46523DDB0B00061925E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F331C46423DDB0B00061925E /* Preview Assets.xcassets */; };
F331C47723DDB1360061925E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F331C47523DDB1360061925E /* LaunchScreen.storyboard */; };
F331C47C23DDB1710061925E /* PadsListContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C47B23DDB1710061925E /* PadsListContainerView.swift */; };
F331C48123DDDE080061925E /* CypherPoetSwiftUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = F331C48023DDDE080061925E /* CypherPoetSwiftUIKit */; };
F331C48323DDDE570061925E /* CurrentApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C48223DDDE570061925E /* CurrentApplication.swift */; };
F331C48523DDDE710061925E /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C48423DDDE710061925E /* AppState.swift */; };
F331C48723DDDEC30061925E /* PadsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C48623DDDEC30061925E /* PadsState.swift */; };
F331C48923DDDF0E0061925E /* Pad.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C48823DDDF0E0061925E /* Pad.swift */; };
F331C48C23DDE6A90061925E /* LaunchLibraryAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C48B23DDE6A90061925E /* LaunchLibraryAPIService.swift */; };
F331C48E23DDE6C20061925E /* LaunchLibraryAPIServicing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C48D23DDE6C20061925E /* LaunchLibraryAPIServicing.swift */; };
F331C49123DDF0A30061925E /* CypherPoetNetStack in Frameworks */ = {isa = PBXBuildFile; productRef = F331C49023DDF0A30061925E /* CypherPoetNetStack */; };
F331C49323DDF0CF0061925E /* Endpoint+LaunchLibraryAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C49223DDF0CF0061925E /* Endpoint+LaunchLibraryAPI.swift */; };
F331C49623DE0FED0061925E /* PreviewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C49523DE0FED0061925E /* PreviewData.swift */; };
F331C49823DE10010061925E /* PreviewData+AppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C49723DE10010061925E /* PreviewData+AppStore.swift */; };
F331C49C23DE18650061925E /* PadsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C49B23DE18650061925E /* PadsListView.swift */; };
F331C49E23DE187A0061925E /* PadsListView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C49D23DE187A0061925E /* PadsListView+ViewModel.swift */; };
F356E61E23E25E7A008553B0 /* PadDetailsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F356E61D23E25E7A008553B0 /* PadDetailsContainerView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
F32ED61723DF69E6006A5195 /* PadsListContainerView+WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PadsListContainerView+WelcomeView.swift"; sourceTree = "<group>"; };
F32ED61923DF91C1006A5195 /* Pad+PadType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Pad+PadType.swift"; sourceTree = "<group>"; };
F32ED61C23DF9B46006A5195 /* PadDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadDetailsView.swift; sourceTree = "<group>"; };
F32ED61E23DF9B7C006A5195 /* PadDetailsView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PadDetailsView+ViewModel.swift"; sourceTree = "<group>"; };
F32ED62123DFB836006A5195 /* NumberFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatters.swift; sourceTree = "<group>"; };
F32ED62323DFC7D5006A5195 /* Pad+SnapshotUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Pad+SnapshotUtils.swift"; sourceTree = "<group>"; };
F32ED62523E0D525006A5195 /* Pad+Computeds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Pad+Computeds.swift"; sourceTree = "<group>"; };
F32ED62823E0F0C6006A5195 /* MapSnapshottingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSnapshottingService.swift; sourceTree = "<group>"; };
F32ED62A23E0F0F1006A5195 /* MapSnapshotServicing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSnapshotServicing.swift; sourceTree = "<group>"; };
F331C45823DDB0AE0061925E /* PadFinder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PadFinder.app; sourceTree = BUILT_PRODUCTS_DIR; };
F331C45B23DDB0AE0061925E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
F331C45D23DDB0AE0061925E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
F331C46123DDB0B00061925E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F331C46423DDB0B00061925E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
F331C46923DDB0B00061925E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F331C47623DDB1360061925E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = PadFinder/Base.lproj/LaunchScreen.storyboard; sourceTree = SOURCE_ROOT; };
F331C47B23DDB1710061925E /* PadsListContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadsListContainerView.swift; sourceTree = "<group>"; };
F331C48223DDDE570061925E /* CurrentApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentApplication.swift; sourceTree = "<group>"; };
F331C48423DDDE710061925E /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
F331C48623DDDEC30061925E /* PadsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadsState.swift; sourceTree = "<group>"; };
F331C48823DDDF0E0061925E /* Pad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pad.swift; sourceTree = "<group>"; };
F331C48B23DDE6A90061925E /* LaunchLibraryAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchLibraryAPIService.swift; sourceTree = "<group>"; };
F331C48D23DDE6C20061925E /* LaunchLibraryAPIServicing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchLibraryAPIServicing.swift; sourceTree = "<group>"; };
F331C49223DDF0CF0061925E /* Endpoint+LaunchLibraryAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Endpoint+LaunchLibraryAPI.swift"; sourceTree = "<group>"; };
F331C49523DE0FED0061925E /* PreviewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewData.swift; sourceTree = "<group>"; };
F331C49723DE10010061925E /* PreviewData+AppStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreviewData+AppStore.swift"; sourceTree = "<group>"; };
F331C49B23DE18650061925E /* PadsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadsListView.swift; sourceTree = "<group>"; };
F331C49D23DE187A0061925E /* PadsListView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PadsListView+ViewModel.swift"; sourceTree = "<group>"; };
F356E61D23E25E7A008553B0 /* PadDetailsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadDetailsContainerView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
F331C45523DDB0AE0061925E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F331C49123DDF0A30061925E /* CypherPoetNetStack in Frameworks */,
F32ED62E23E163BB006A5195 /* CypherPoetPropertyWrappers in Frameworks */,
F331C48123DDDE080061925E /* CypherPoetSwiftUIKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
F32ED61B23DF9B36006A5195 /* Pad Details */ = {
isa = PBXGroup;
children = (
F32ED61C23DF9B46006A5195 /* PadDetailsView.swift */,
F32ED61E23DF9B7C006A5195 /* PadDetailsView+ViewModel.swift */,
F356E61D23E25E7A008553B0 /* PadDetailsContainerView.swift */,
);
path = "Pad Details";
sourceTree = "<group>";
};
F32ED62023DFB7E9006A5195 /* Formatters */ = {
isa = PBXGroup;
children = (
F32ED62123DFB836006A5195 /* NumberFormatters.swift */,
);
path = Formatters;
sourceTree = "<group>";
};
F32ED62723E0F0B8006A5195 /* Services */ = {
isa = PBXGroup;
children = (
F32ED62823E0F0C6006A5195 /* MapSnapshottingService.swift */,
F32ED62A23E0F0F1006A5195 /* MapSnapshotServicing.swift */,
);
path = Services;
sourceTree = "<group>";
};
F331C44F23DDB0AE0061925E = {
isa = PBXGroup;
children = (
F331C45A23DDB0AE0061925E /* PadFinder */,
F331C45923DDB0AE0061925E /* Products */,
);
sourceTree = "<group>";
};
F331C45923DDB0AE0061925E /* Products */ = {
isa = PBXGroup;
children = (
F331C45823DDB0AE0061925E /* PadFinder.app */,
);
name = Products;
sourceTree = "<group>";
};
F331C45A23DDB0AE0061925E /* PadFinder */ = {
isa = PBXGroup;
children = (
F331C47423DDB10E0061925E /* App */,
F331C47323DDB1080061925E /* Data */,
F331C47223DDB1040061925E /* Networking */,
F331C47123DDB0FF0061925E /* Reusables */,
F331C47023DDB0F80061925E /* Resources */,
F331C46F23DDB0F40061925E /* Scenes */,
F331C46923DDB0B00061925E /* Info.plist */,
F331C46323DDB0B00061925E /* Preview Content */,
);
path = PadFinder;
sourceTree = "<group>";
};
F331C46323DDB0B00061925E /* Preview Content */ = {
isa = PBXGroup;
children = (
F331C49423DE0FE50061925E /* Preview Data */,
F331C46423DDB0B00061925E /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
F331C46F23DDB0F40061925E /* Scenes */ = {
isa = PBXGroup;
children = (
F331C47823DDB1430061925E /* Pads */,
);
path = Scenes;
sourceTree = "<group>";
};
F331C47023DDB0F80061925E /* Resources */ = {
isa = PBXGroup;
children = (
F331C46123DDB0B00061925E /* Assets.xcassets */,
);
path = Resources;
sourceTree = "<group>";
};
F331C47123DDB0FF0061925E /* Reusables */ = {
isa = PBXGroup;
children = (
F32ED62723E0F0B8006A5195 /* Services */,
F32ED62023DFB7E9006A5195 /* Formatters */,
);
path = Reusables;
sourceTree = "<group>";
};
F331C47223DDB1040061925E /* Networking */ = {
isa = PBXGroup;
children = (
F331C48B23DDE6A90061925E /* LaunchLibraryAPIService.swift */,
F331C48D23DDE6C20061925E /* LaunchLibraryAPIServicing.swift */,
F331C49223DDF0CF0061925E /* Endpoint+LaunchLibraryAPI.swift */,
);
path = Networking;
sourceTree = "<group>";
};
F331C47323DDB1080061925E /* Data */ = {
isa = PBXGroup;
children = (
F331C47E23DDB1D40061925E /* Models */,
F331C47D23DDB1D10061925E /* State */,
);
path = Data;
sourceTree = "<group>";
};
F331C47423DDB10E0061925E /* App */ = {
isa = PBXGroup;
children = (
F331C45B23DDB0AE0061925E /* AppDelegate.swift */,
F331C45D23DDB0AE0061925E /* SceneDelegate.swift */,
F331C47523DDB1360061925E /* LaunchScreen.storyboard */,
F331C48223DDDE570061925E /* CurrentApplication.swift */,
);
path = App;
sourceTree = "<group>";
};
F331C47823DDB1430061925E /* Pads */ = {
isa = PBXGroup;
children = (
F331C47B23DDB1710061925E /* PadsListContainerView.swift */,
F32ED61B23DF9B36006A5195 /* Pad Details */,
F32ED61723DF69E6006A5195 /* PadsListContainerView+WelcomeView.swift */,
F331C49B23DE18650061925E /* PadsListView.swift */,
F331C49D23DE187A0061925E /* PadsListView+ViewModel.swift */,
);
path = Pads;
sourceTree = "<group>";
};
F331C47D23DDB1D10061925E /* State */ = {
isa = PBXGroup;
children = (
F331C48423DDDE710061925E /* AppState.swift */,
F331C48623DDDEC30061925E /* PadsState.swift */,
);
path = State;
sourceTree = "<group>";
};
F331C47E23DDB1D40061925E /* Models */ = {
isa = PBXGroup;
children = (
F331C48A23DDDF190061925E /* Pad */,
);
path = Models;
sourceTree = "<group>";
};
F331C48A23DDDF190061925E /* Pad */ = {
isa = PBXGroup;
children = (
F331C48823DDDF0E0061925E /* Pad.swift */,
F32ED62323DFC7D5006A5195 /* Pad+SnapshotUtils.swift */,
F32ED62523E0D525006A5195 /* Pad+Computeds.swift */,
F32ED61923DF91C1006A5195 /* Pad+PadType.swift */,
);
path = Pad;
sourceTree = "<group>";
};
F331C49423DE0FE50061925E /* Preview Data */ = {
isa = PBXGroup;
children = (
F331C49523DE0FED0061925E /* PreviewData.swift */,
F331C49723DE10010061925E /* PreviewData+AppStore.swift */,
);
path = "Preview Data";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
F331C45723DDB0AE0061925E /* PadFinder */ = {
isa = PBXNativeTarget;
buildConfigurationList = F331C46C23DDB0B00061925E /* Build configuration list for PBXNativeTarget "PadFinder" */;
buildPhases = (
F331C45423DDB0AE0061925E /* Sources */,
F331C45523DDB0AE0061925E /* Frameworks */,
F331C45623DDB0AE0061925E /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = PadFinder;
packageProductDependencies = (
F331C48023DDDE080061925E /* CypherPoetSwiftUIKit */,
F331C49023DDF0A30061925E /* CypherPoetNetStack */,
F32ED62D23E163BB006A5195 /* CypherPoetPropertyWrappers */,
);
productName = PadFinder;
productReference = F331C45823DDB0AE0061925E /* PadFinder.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
F331C45023DDB0AE0061925E /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1130;
LastUpgradeCheck = 1130;
ORGANIZATIONNAME = CypherPoet;
TargetAttributes = {
F331C45723DDB0AE0061925E = {
CreatedOnToolsVersion = 11.3.1;
};
};
};
buildConfigurationList = F331C45323DDB0AE0061925E /* Build configuration list for PBXProject "PadFinder" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = F331C44F23DDB0AE0061925E;
packageReferences = (
F331C47F23DDDE080061925E /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */,
F331C48F23DDF0A30061925E /* XCRemoteSwiftPackageReference "CypherPoetNetStack" */,
F32ED62C23E163BB006A5195 /* XCRemoteSwiftPackageReference "CypherPoetPropertyWrappers" */,
);
productRefGroup = F331C45923DDB0AE0061925E /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
F331C45723DDB0AE0061925E /* PadFinder */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
F331C45623DDB0AE0061925E /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F331C47723DDB1360061925E /* LaunchScreen.storyboard in Resources */,
F331C46523DDB0B00061925E /* Preview Assets.xcassets in Resources */,
F331C46223DDB0B00061925E /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
F331C45423DDB0AE0061925E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F331C47C23DDB1710061925E /* PadsListContainerView.swift in Sources */,
F356E61E23E25E7A008553B0 /* PadDetailsContainerView.swift in Sources */,
F331C48523DDDE710061925E /* AppState.swift in Sources */,
F32ED62923E0F0C6006A5195 /* MapSnapshottingService.swift in Sources */,
F32ED61F23DF9B7C006A5195 /* PadDetailsView+ViewModel.swift in Sources */,
F331C49C23DE18650061925E /* PadsListView.swift in Sources */,
F32ED61823DF69E6006A5195 /* PadsListContainerView+WelcomeView.swift in Sources */,
F331C48923DDDF0E0061925E /* Pad.swift in Sources */,
F331C49623DE0FED0061925E /* PreviewData.swift in Sources */,
F32ED62423DFC7D5006A5195 /* Pad+SnapshotUtils.swift in Sources */,
F331C48C23DDE6A90061925E /* LaunchLibraryAPIService.swift in Sources */,
F32ED62B23E0F0F1006A5195 /* MapSnapshotServicing.swift in Sources */,
F32ED62623E0D525006A5195 /* Pad+Computeds.swift in Sources */,
F32ED62223DFB836006A5195 /* NumberFormatters.swift in Sources */,
F331C49E23DE187A0061925E /* PadsListView+ViewModel.swift in Sources */,
F331C45C23DDB0AE0061925E /* AppDelegate.swift in Sources */,
F331C48323DDDE570061925E /* CurrentApplication.swift in Sources */,
F331C49323DDF0CF0061925E /* Endpoint+LaunchLibraryAPI.swift in Sources */,
F331C48E23DDE6C20061925E /* LaunchLibraryAPIServicing.swift in Sources */,
F331C45E23DDB0AE0061925E /* SceneDelegate.swift in Sources */,
F32ED61A23DF91C1006A5195 /* Pad+PadType.swift in Sources */,
F331C49823DE10010061925E /* PreviewData+AppStore.swift in Sources */,
F331C48723DDDEC30061925E /* PadsState.swift in Sources */,
F32ED61D23DF9B46006A5195 /* PadDetailsView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
F331C47523DDB1360061925E /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
F331C47623DDB1360061925E /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
F331C46A23DDB0B00061925E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
F331C46B23DDB0B00061925E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
F331C46D23DDB0B00061925E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"PadFinder/Preview Content\"";
DEVELOPMENT_TEAM = QRXXH2RKAG;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = PadFinder/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = io.github.cypherpoet.PadFinder;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
F331C46E23DDB0B00061925E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"PadFinder/Preview Content\"";
DEVELOPMENT_TEAM = QRXXH2RKAG;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = PadFinder/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = io.github.cypherpoet.PadFinder;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
F331C45323DDB0AE0061925E /* Build configuration list for PBXProject "PadFinder" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F331C46A23DDB0B00061925E /* Debug */,
F331C46B23DDB0B00061925E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
F331C46C23DDB0B00061925E /* Build configuration list for PBXNativeTarget "PadFinder" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F331C46D23DDB0B00061925E /* Debug */,
F331C46E23DDB0B00061925E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
F32ED62C23E163BB006A5195 /* XCRemoteSwiftPackageReference "CypherPoetPropertyWrappers" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/CypherPoet/CypherPoetPropertyWrappers.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.0.1;
};
};
F331C47F23DDDE080061925E /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/CypherPoet/CypherPoetSwiftUIKit.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.0.33;
};
};
F331C48F23DDF0A30061925E /* XCRemoteSwiftPackageReference "CypherPoetNetStack" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/CypherPoet/CypherPoetNetStack.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.0.27;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
F32ED62D23E163BB006A5195 /* CypherPoetPropertyWrappers */ = {
isa = XCSwiftPackageProductDependency;
package = F32ED62C23E163BB006A5195 /* XCRemoteSwiftPackageReference "CypherPoetPropertyWrappers" */;
productName = CypherPoetPropertyWrappers;
};
F331C48023DDDE080061925E /* CypherPoetSwiftUIKit */ = {
isa = XCSwiftPackageProductDependency;
package = F331C47F23DDDE080061925E /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */;
productName = CypherPoetSwiftUIKit;
};
F331C49023DDF0A30061925E /* CypherPoetNetStack */ = {
isa = XCSwiftPackageProductDependency;
package = F331C48F23DDF0A30061925E /* XCRemoteSwiftPackageReference "CypherPoetNetStack" */;
productName = CypherPoetNetStack;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = F331C45023DDB0AE0061925E /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,34 @@
{
"object": {
"pins": [
{
"package": "CypherPoetNetStack",
"repositoryURL": "https://github.com/CypherPoet/CypherPoetNetStack.git",
"state": {
"branch": null,
"revision": "9852c07ae3c6e4294e1a2d277b6c83cb3515eb58",
"version": "0.0.27"
}
},
{
"package": "CypherPoetPropertyWrappers",
"repositoryURL": "https://github.com/CypherPoet/CypherPoetPropertyWrappers.git",
"state": {
"branch": null,
"revision": "9e1c62432f5a25a159e80dee28965e8cc20b7939",
"version": "0.0.1"
}
},
{
"package": "CypherPoetSwiftUIKit",
"repositoryURL": "https://github.com/CypherPoet/CypherPoetSwiftUIKit.git",
"state": {
"branch": null,
"revision": "dca4353f34bdf5a622fd5fc7c31dc72ad377194a",
"version": "0.0.35"
}
}
]
},
"version": 1
}

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F331C45723DDB0AE0061925E"
BuildableName = "PadFinder.app"
BlueprintName = "PadFinder"
ReferencedContainer = "container:PadFinder.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F331C45723DDB0AE0061925E"
BuildableName = "PadFinder.app"
BlueprintName = "PadFinder"
ReferencedContainer = "container:PadFinder.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F331C45723DDB0AE0061925E"
BuildableName = "PadFinder.app"
BlueprintName = "PadFinder"
ReferencedContainer = "container:PadFinder.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,37 @@
//
// AppDelegate.swift
// PadFinder
//
// Created by Brian Sipple on 1/26/20.
// Copyright © 2020 CypherPoet. All rights reserved.
//
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}

View File

@ -0,0 +1,18 @@
//
// CurrentApplication.swift
// PadFinder
//
// Created by CypherPoet on 1/26/20.
//
//
import Foundation
struct CurrentApplication {
var launchLibraryAPIService: LaunchLibraryAPIService
}
var CurrentApp = CurrentApplication(
launchLibraryAPIService: LaunchLibraryAPIService()
)

View File

@ -0,0 +1,68 @@
//
// SceneDelegate.swift
// PadFinder
//
// Created by Brian Sipple on 1/26/20.
// Copyright © 2020 CypherPoet. All rights reserved.
//
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let store = AppStore(initialState: .init(), appReducer: appReducer)
// Create the SwiftUI view that provides the window contents.
let entryView = PadsListContainerView()
.environmentObject(store)
.accentColor(.pink)
window.rootViewController = UIHostingController(rootView: entryView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// 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.
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,19 @@
//
// Pad+Computeds.swift
// PadFinder
//
// Created by CypherPoet on 1/28/20.
//
//
import Foundation
import CoreLocation
extension Pad {
var coordinate: CLLocationCoordinate2D {
.init(latitude: latitude, longitude: longitude)
}
}

View File

@ -0,0 +1,67 @@
//
// Pad+PadType.swift
// PadFinder
//
// Created by CypherPoet on 1/27/20.
//
//
import Foundation
import SwiftUI
extension Pad {
enum PadType: Int, Decodable {
case launch = 0
case landing = 1
}
}
extension Pad.PadType: CaseIterable {}
extension Pad.PadType: Identifiable {
var id: Int { self.rawValue }
}
extension Pad.PadType {
var displayName: String {
switch self {
case .launch:
return "Launch Pad"
case .landing:
return "Landing Pad"
}
}
var sfSymbolName: String {
switch self {
case .launch:
return "chevron.up"
case .landing:
return "chevron.down"
}
}
var listItemBackgroundColor: Color {
switch self {
case .launch:
return .purple
case .landing:
return .orange
}
}
var listItemImage: some View {
Image(systemName: sfSymbolName)
.font(.title)
.foregroundColor(.white)
.padding(11)
.background(listItemBackgroundColor)
.clipShape(Circle())
}
}

View File

@ -0,0 +1,27 @@
//
// Pad+SnapshotUtils.swift
// PadFinder
//
// Created by CypherPoet on 1/27/20.
//
//
import Foundation
import MapKit
extension Pad {
var baseSnapshotOptions: MKMapSnapshotter.Options {
let options = MKMapSnapshotter.Options()
options.mapType = .standard
options.scale = UIScreen.main.scale
options.showsBuildings = true
// options.pointOfInterestFilter = .init(including: [.airport, .cafe])
options.pointOfInterestFilter = .includingAll
return options
}
}

View File

@ -0,0 +1,95 @@
//
// Pad.swift
// PadFinder
//
// Created by CypherPoet on 1/26/20.
//
//
import Foundation
import CoreLocation
struct Pad {
let id: Int
var name: String
var padType: PadType
var mapURL: URL
var wikiURL: URL?
var latitude: CLLocationDegrees
var longitude: CLLocationDegrees
var isRetired: Bool
var infoURLs: [URL]?
}
extension Pad: Identifiable {}
extension Pad {
static func isRetired(int: Int) -> Bool {
int == 1
}
}
// MARK: - Decodable
extension Pad: Decodable {
struct ResultsContainer: Decodable {
var pads: [Pad]
}
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case padType = "padType"
case mapURL = "mapURL"
case wikiURL = "wikiURL"
case latitude = "latitude"
case longitude = "longitude"
case isRetired = "retired"
case infoURLs = "infoURLs"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
padType = try container.decode(PadType.self, forKey: .padType)
let mapURLString = try container.decode(String.self, forKey: .mapURL)
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
mapURL = URL(string: mapURLString)!
// if mapURL == nil {
// print("failed to make URL from string \(mapURLString)")
// }
if let wikiURLString = try? container
.decode(String.self, forKey: .wikiURL)
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
{
wikiURL = URL(string: wikiURLString)
}
let latitudeString = try container.decode(String.self, forKey: .latitude)
latitude = CLLocationDegrees(latitudeString) ?? 0.0
let longitudeString = try container.decode(String.self, forKey: .longitude)
longitude = CLLocationDegrees(longitudeString) ?? 0.0
let isRetiredInt = try container.decode(Int.self, forKey: .isRetired)
isRetired = Pad.isRetired(int: isRetiredInt)
infoURLs = try container.decodeIfPresent([URL].self, forKey: .infoURLs)
}
}
extension Pad {
static var decoder: JSONDecoder {
.init()
}
}

View File

@ -0,0 +1,36 @@
//
// AppState.swift
// PadFinder
//
// Created by CypherPoet on 1/26/20.
//
//
import Foundation
import CypherPoetSwiftUIKit_DataFlowUtils
struct AppState {
var padsState = PadsState()
}
//enum AppSideEffect: SideEffect {}
enum AppAction {
case pads(_ action: PadsAction)
}
// MARK: - Reducer
let appReducer = Reducer<AppState, AppAction> { appState, action in
switch action {
case .pads(let action):
padsReducer.reduce(&appState.padsState, action)
}
}
typealias AppStore = Store<AppState, AppAction>

View File

@ -0,0 +1,98 @@
//
// PadsState.swift
// PadFinder
//
// Created by CypherPoet on 1/26/20.
//
//
import Foundation
import Combine
import CypherPoetSwiftUIKit_DataFlowUtils
import CypherPoetPropertyWrappers_UserDefault
struct PadsState {
var dataFetchingState: DataFetchingState = .inactive
@UserDefault("pads-state-favorites", defaultValue: [Pad.ID]())
var favorites: [Pad.ID]
}
extension PadsState {
enum DataFetchingState {
case inactive
case fetching
case fetched([Pad])
case errored(Error)
}
}
extension PadsState.DataFetchingState: Equatable {
static func == (
lhs: PadsState.DataFetchingState,
rhs: PadsState.DataFetchingState
) -> Bool {
switch (lhs, rhs) {
case (.inactive, .inactive),
(.fetching, .fetching),
(.fetched, .fetched),
(.errored, .errored):
return true
default:
return false
}
}
}
enum PadsSideEffect: SideEffect {
case fetchPads
func mapToAction() -> AnyPublisher<AppAction, Never> {
switch self {
case .fetchPads:
return CurrentApp.launchLibraryAPIService
.pads()
.map { AppAction.pads(.fetchedPadsSet($0)) }
.catch { error in
Just(AppAction.pads(.fetchErrorSet(error)))
}
.eraseToAnyPublisher()
}
}
}
enum PadsAction {
case padsFetchStart
case fetchedPadsSet([Pad])
case fetchErrorSet(Error)
case favoriteAdded(Pad.ID)
case favoriteRemoved(Pad.ID)
}
// MARK: - Reducer
let padsReducer: Reducer<PadsState, PadsAction> = Reducer(
reduce: { state, action in
switch action {
case .fetchedPadsSet(let pads):
state.dataFetchingState = .fetched(pads)
case .padsFetchStart:
state.dataFetchingState = .fetching
case .fetchErrorSet(let error):
state.dataFetchingState = .errored(error)
case .favoriteAdded(let padID):
state.favorites.append(padID)
case .favoriteRemoved(let padID):
state.favorites.removeAll(where: { $0 == padID })
}
}
)

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,43 @@
//
// Endpoint+LaunchLibraryAPI.swift
// PadFinder
//
// Created by CypherPoet on 1/26/20.
//
//
import Foundation
import CypherPoetNetStack
extension Endpoint {
enum LaunchLibraryAPI {
private static let host = "launchlibrary.net"
private static let basePath = "/1.4"
public static func pads(
sizeMode: VerbosityMode = .summary
) -> Endpoint {
.init(
host: self.host,
path: "\(self.basePath)/pad",
queryItems: [
URLQueryItem(name: "mode", value: sizeMode.rawValue)
]
)
}
}
}
extension Endpoint.LaunchLibraryAPI {
/// Specifies how much data to return in each result item
enum VerbosityMode: String {
case list
case summary
case verbose
}
}

View File

@ -0,0 +1,83 @@
//
// LaunchLibraryAPIService.swift
// PadFinder
//
// Created by CypherPoet on 1/26/20.
//
//
import Foundation
import Combine
import CypherPoetNetStack
final class LaunchLibraryAPIService: LaunchLibraryAPIServicing {
var session: URLSession
var apiQueue: DispatchQueue
init(
session: URLSession = .shared,
queue: DispatchQueue = DispatchQueue(label: "LaunchLibraryAPIService", qos: .userInitiated)
) {
self.session = session
self.apiQueue = queue
}
}
extension LaunchLibraryAPIService {
private func padsContainer(
using decoder: JSONDecoder = Pad.decoder
) -> AnyPublisher<Pad.ResultsContainer, Swift.Error> {
let endpoint = Endpoint.LaunchLibraryAPI.pads()
guard let url = endpoint.url else {
fatalError("Failed to make URL for pads.")
}
return perform(
URLRequest(url: url),
parsingResponseOn: apiQueue,
with: decoder
)
.mapError { Error.network(error: $0) }
.eraseToAnyPublisher()
}
func pads(
using decoder: JSONDecoder = Pad.decoder
) -> AnyPublisher<[Pad], Swift.Error> {
padsContainer(using: decoder)
.map(\.pads)
.eraseToAnyPublisher()
}
}
// MARK: - Error
extension LaunchLibraryAPIService {
enum Error: LocalizedError {
case network(error: NetStackError)
}
}
extension LaunchLibraryAPIService.Error {
public var errorDescription: String? {
switch self {
case .network(let error):
return error.errorDescription
}
}
}
// MARK: - Error: Identifiable
extension LaunchLibraryAPIService.Error: Identifiable {
public var id: String? { errorDescription }
}

View File

@ -0,0 +1,16 @@
//
// LaunchLibraryAPIServicing.swift
// PadFinder
//
// Created by CypherPoet on 1/26/20.
//
//
import Foundation
import Combine
import CypherPoetNetStack
protocol LaunchLibraryAPIServicing: ModelTransportRequestPublishing {
func pads(using decoder: JSONDecoder) -> AnyPublisher<[Pad], Error>
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,63 @@
//
// PreviewData+AppStore.swift
// PadFinder
//
// Created by CypherPoet on 1/26/20.
//
//
import Foundation
extension PreviewData {
enum Pads {
static let pad1 = Pad(
id: 166,
name: "Rocket Lab Launch Complex 1",
padType: .landing,
mapURL: URL(
string: #"https:\/\/www.google.ee\/maps\/place\/39°15'46.2\"S+177°51'52.1\"E\/"#
.replacingOccurrences(of: #"\"#, with: "")
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
)!,
latitude: -39.262833000000000,
longitude: 177.864469000000000,
isRetired: false,
infoURLs: [
URL(string: #"http:\/\/www.rocketlabusa.com\/"#.replacingOccurrences(of: #"\"#, with: ""))!,
URL(string: #"https:\/\/twitter.com\/rocketlab"#.replacingOccurrences(of: #"\"#, with: ""))!,
URL(string: #"https:\/\/www.youtube.com\/user\/RocketLabNZ"#.replacingOccurrences(of: #"\"#, with: ""))!,
URL(string: #"https:\/\/www.facebook.com\/RocketLabUSA"#.replacingOccurrences(of: #"\"#, with: ""))!,
URL(string: #"https:\/\/www.linkedin.com\/company\/rocket-lab-limited"#.replacingOccurrences(of: #"\"#, with: ""))!,
]
)
}
enum PadsStates {
static let `default`: PadsState = {
.init(
dataFetchingState: .fetched([
PreviewData.Pads.pad1,
])
)
}()
}
enum AppStores {
static let empty: AppStore = {
AppStore(initialState: .init(), appReducer: appReducer)
}()
static let withPads: AppStore = {
AppStore(
initialState: .init(
padsState: PreviewData.PadsStates.default
),
appReducer: appReducer
)
}()
}
}

View File

@ -0,0 +1,12 @@
//
// PreviewData.swift
// PadFinder
//
// Created by CypherPoet on 1/26/20.
//
//
import Foundation
enum PreviewData {}

View File

@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "CanaveralMapSample.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,22 @@
//
// Double+CoordinateFormat.swift
// PadFinder
//
// Created by CypherPoet on 1/27/20.
//
//
import Foundation
enum NumberFormatters {
static var padCoordinateDisplay: NumberFormatter {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 4
return formatter
}
}

View File

@ -0,0 +1,64 @@
//
// MapSnapshotServicing.swift
// PadFinder
//
// Created by CypherPoet on 1/28/20.
//
//
import SwiftUI
import MapKit
import Combine
protocol MapSnapshotServicing: class {
var snapshotOptions: MKMapSnapshotter.Options { get }
var queue: DispatchQueue { get }
func takeSnapshot(
with size: CGSize,
at coordinate: CLLocationCoordinate2D,
latitudeSpan: CLLocationDegrees,
longitudeSpan: CLLocationDegrees
) -> Future<MKMapSnapshotter.Snapshot, Error>
}
extension MapSnapshotServicing {
func takeSnapshot(
with size: CGSize,
at coordinate: CLLocationCoordinate2D,
latitudeSpan: CLLocationDegrees = 0.15,
longitudeSpan: CLLocationDegrees = 0.15
) -> Future<MKMapSnapshotter.Snapshot, Error> {
let span = MKCoordinateSpan(latitudeDelta: latitudeSpan, longitudeDelta: longitudeSpan)
snapshotOptions.region = MKCoordinateRegion(
center: coordinate,
span: span
)
snapshotOptions.size = size
let snapshotter = MKMapSnapshotter(options: snapshotOptions)
// TOOD: Ideally, we'd implement some kind of cahcing here, or save the images
// as part of each pad model -- which could be persisted in Core Data.
return Future { promise in
snapshotter.start(with: self.queue) { (snapshot, error) in
guard error == nil else {
return promise(.failure(error!))
}
guard let snapshot = snapshot else {
preconditionFailure("No snapshot returned despite snapshotter completing without error.")
}
return promise(.success(snapshot))
}
}
}
}

View File

@ -0,0 +1,25 @@
//
// MapSnapshottingService.swift
// PadFinder
//
// Created by CypherPoet on 1/28/20.
//
//
import Foundation
import MapKit
final class MapSnapshottingService: MapSnapshotServicing {
var snapshotOptions: MKMapSnapshotter.Options
var queue: DispatchQueue
init(
snapshotOptions: MKMapSnapshotter.Options = .init(),
queue: DispatchQueue = DispatchQueue(label: "Map Snapshotting Service", qos: .default)
) {
self.snapshotOptions = snapshotOptions
self.queue = queue
}
}

View File

@ -0,0 +1,78 @@
//
// PadDetailsContainerView.swift
// PadFinder
//
// Created by CypherPoet on 1/29/20.
//
//
import SwiftUI
import MapKit
struct PadDetailsContainerView {
@EnvironmentObject private var store: AppStore
let pad: Pad
}
// MARK: - View
extension PadDetailsContainerView: View {
var body: some View {
PadDetailsView(
viewModel: .init(
pad: pad,
isPadFavorited: isPadFavorited,
snapshotService: MapSnapshottingService(
snapshotOptions: makeSnapshotOptions(for: pad)
)
),
onFavoriteToggled: toggleFavorite(for:)
)
}
}
// MARK: - Computeds
extension PadDetailsContainerView {
var padsState: PadsState { store.state.padsState }
var isPadFavorited: Bool { padsState.favorites.contains(pad.id) }
}
// MARK: - View Variables
extension PadDetailsContainerView {
}
// MARK: - Private Helpers
private extension PadDetailsContainerView {
func makeSnapshotOptions(for pad: Pad) -> MKMapSnapshotter.Options {
let snapshotOptions = pad.baseSnapshotOptions
return snapshotOptions
}
func toggleFavorite(for pad: Pad) {
if padsState.favorites.contains(pad.id) {
store.send(.pads(.favoriteRemoved(pad.id)))
} else {
store.send(.pads(.favoriteAdded(pad.id)))
}
}
}
//// MARK: - Preview
//struct PadDetailsContainerView_Previews: PreviewProvider {
//
// static var previews: some View {
// PadDetailsContainerView()
// }
//}

View File

@ -0,0 +1,134 @@
//
// PadDetailsView+ViewModel.swift
// PadFinder
//
// Created by CypherPoet on 1/27/20.
//
//
import SwiftUI
import MapKit
import Combine
extension PadDetailsView {
final class ViewModel: ObservableObject {
private var subscriptions = Set<AnyCancellable>()
let pad: Pad
let isPadFavorited: Bool
private let snapshotService: MapSnapshotServicing
// MARK: - Published Outputs
@Published var mapSnapshotImage: UIImage?
// MARK: - Init
init(
pad: Pad,
isPadFavorited: Bool,
snapshotService: MapSnapshotServicing
) {
self.pad = pad
self.isPadFavorited = isPadFavorited
self.snapshotService = snapshotService
// In lieu of image caching or persistance, we'll have to call
// this during init instead of onAppear. That's not the greatest.
// self.takeMapSnapshot(
// size: CGSize(
// width: UIScreen.main.bounds.width,
// height: UIScreen.main.bounds.width * 0.75
// )
// )
setupSubscribers()
}
}
}
// MARK: - Publishers
extension PadDetailsView.ViewModel {
}
// MARK: - Computeds
extension PadDetailsView.ViewModel {
var padNameText: String { pad.name }
var latitudeText: String {
NumberFormatters.padCoordinateDisplay.string(for: pad.latitude) ?? ""
}
var longitudeText: String {
NumberFormatters.padCoordinateDisplay.string(for: pad.longitude) ?? ""
}
var wikipediaURL: URL? { pad.wikiURL }
var webLinkData: [(hostName: String, url: URL)] {
([wikipediaURL] + (pad.infoURLs ?? [URL?]()))
.compactMap { url in
guard
let url = url,
let hostName = url.host
else { return nil }
return (hostName: strippedHostName(from: hostName), url: url)
}
}
var favoritesButtonText: String {
return isPadFavorited ? "Remove From Favorites" : "Add to Favorites"
}
}
// MARK: - Public Methods
extension PadDetailsView.ViewModel {
func takeMapSnapshot(size: CGSize) {
snapshotService
.takeSnapshot(with: size, at: pad.coordinate)
.assertNoFailure()
.print("takeMapSnapshot")
.map(\.image)
.receive(on: DispatchQueue.main)
.sink(
receiveValue: { self.mapSnapshotImage = $0 }
)
.store(in: &subscriptions)
}
}
// MARK: - Private Helpers
private extension PadDetailsView.ViewModel {
/// Strips the leading "sub domain" and trailing "top-level domain"
/// parts (including the ".") from a URL `host` string
func strippedHostName(from hostNameString: String) -> String {
var hostNameString = hostNameString
// Strip the leading sub domain part if it exists
if hostNameString.split(separator: ".").count > 2 {
hostNameString = hostNameString
.replacingOccurrences(of: "^(\\w*\\.){1}", with: "", options: .regularExpression)
}
// Strip the trailing top-level domain part
return hostNameString
.replacingOccurrences(of: "\\.(.*)", with: "", options: .regularExpression)
.capitalized
}
func setupSubscribers() {
}
}

View File

@ -0,0 +1,154 @@
//
// PadDetailsView.swift
// PadFinder
//
// Created by CypherPoet on 1/27/20.
//
//
import SwiftUI
import MapKit
import CypherPoetSwiftUIKit
struct PadDetailsView {
@ObservedObject var viewModel: ViewModel
var onFavoriteToggled: ((Pad) -> Void)?
}
// MARK: - View
extension PadDetailsView: View {
var body: some View {
GeometryReader { geometry in
List {
if self.viewModel.mapSnapshotImage != nil {
Image(uiImage: self.viewModel.mapSnapshotImage!)
.resizable()
.scaledToFit()
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
}
self.coordinateHeader
.padding(.vertical)
if self.viewModel.webLinkData.isEmpty == false {
self.linksSection
}
self.optionsSection
}
.embedInScrollView(axes: .vertical)
}
.navigationBarTitle(Text(viewModel.padNameText), displayMode: .inline)
.onAppear {
self.viewModel.takeMapSnapshot(
size: CGSize(
width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.width * 0.75
)
)
}
}
}
// MARK: - Computeds
extension PadDetailsView {
}
// MARK: - View Variables
extension PadDetailsView {
private var coordinateHeader: some View {
HStack(spacing: 12) {
Spacer()
Image(systemName: "mappin")
.padding()
.foregroundColor(.white)
.background(Color.red)
.clipShape(Circle())
Group {
Text("Lat: ").fontWeight(.bold)
+ Text(self.viewModel.latitudeText)
Text("Lon: ").fontWeight(.bold)
+ Text(self.viewModel.longitudeText)
}
.embedInCompactableStack()
Spacer()
}
}
private var linksSection: some View {
Section(
header: Text("Links").font(.headline)
) {
ForEach(viewModel.webLinkData, id: \.0) { linkItem in
Button(action: {
UIApplication.shared.open(linkItem.url)
}) {
Text(linkItem.hostName)
.foregroundColor(.accentColor)
}
}
}
}
private var optionsSection: some View {
Section(
header: Text("Options").font(.headline)
) {
favoritesButton
}
}
private var favoritesButton: some View {
Button(action: {
self.onFavoriteToggled?(self.viewModel.pad)
}) {
Text(viewModel.favoritesButtonText)
.foregroundColor(.accentColor)
}
}
}
// MARK: - Private Helpers
private extension PadDetailsView {
}
// MARK: - Preview
struct PadDetailsView_Previews: PreviewProvider {
static var previews: some View {
let snapshotOptions = PreviewData.Pads.pad1.baseSnapshotOptions
return NavigationView {
PadDetailsView(
viewModel: .init(
pad: PreviewData.Pads.pad1,
isPadFavorited: false,
snapshotService: MapSnapshottingService(
snapshotOptions: snapshotOptions
)
)
)
}
.navigationViewStyle(StackNavigationViewStyle())
.environmentObject(PreviewData.AppStores.withPads)
}
}

View File

@ -0,0 +1,60 @@
//
// WelcomeView.swift
// PadFinder
//
// Created by CypherPoet on 1/27/20.
//
//
import SwiftUI
extension PadsListContainerView {
struct WelcomeView {
}
}
// MARK: - View
extension PadsListContainerView.WelcomeView: View {
var body: some View {
VStack(spacing: 20.0) {
Text("Welcome to PadFinder! 🚀")
.font(.largeTitle)
Text("Select a launch pad from the side menu to get started. (Try swiping from the left edge if you don't see it!)")
.font(.headline)
.foregroundColor(.secondary)
}
.padding()
}
}
// MARK: - Computeds
extension PadsListContainerView.WelcomeView {
}
// MARK: - View Variables
extension PadsListContainerView.WelcomeView {
}
// MARK: - Private Helpers
private extension PadsListContainerView.WelcomeView {
}
// MARK: - Preview
struct PadsListContainerView_WelcomeView_Previews: PreviewProvider {
static var previews: some View {
PadsListContainerView.WelcomeView()
}
}

View File

@ -0,0 +1,90 @@
//
// PadsListContainerView.swift
// PadFinder
//
// Created by CypherPoet on 1/26/20.
//
//
import SwiftUI
import MapKit
struct PadsListContainerView {
@EnvironmentObject private var store: AppStore
}
// MARK: - View
extension PadsListContainerView: View {
var body: some View {
NavigationView {
PadsListView(
viewModel: .init(padsState: padsState),
buildDestination: buildDestination(forPad:)
)
.navigationBarTitle("Launch Pads")
.environmentObject(store)
WelcomeView()
}
.onAppear(perform: fetchPads)
}
}
// MARK: - Computeds
extension PadsListContainerView {
var padsState: PadsState { store.state.padsState }
}
// MARK: - View Variables
extension PadsListContainerView {
}
// MARK: - Private Helpers
private extension PadsListContainerView {
func fetchPads() {
store.send(PadsSideEffect.fetchPads)
}
func makeSnapshotOptions(for pad: Pad) -> MKMapSnapshotter.Options {
let snapshotOptions = pad.baseSnapshotOptions
return snapshotOptions
}
func toggleFavorite(for pad: Pad) {
if padsState.favorites.contains(pad.id) {
store.send(.pads(.favoriteRemoved(pad.id)))
} else {
store.send(.pads(.favoriteAdded(pad.id)))
}
}
func buildDestination(forPad pad: Pad) -> some View {
PadDetailsContainerView(pad: pad)
}
}
// MARK: - Preview
struct PadsListContainerView_Previews: PreviewProvider {
static var previews: some View {
let store = PreviewData.AppStores.withPads
return PadsListContainerView()
.environmentObject(store)
}
}

View File

@ -0,0 +1,88 @@
//
// PadsListView+ViewModel.swift
// PadFinder
//
// Created by CypherPoet on 1/26/20.
//
//
import SwiftUI
import Combine
extension PadsListView {
final class ViewModel: ObservableObject {
private var subscriptions = Set<AnyCancellable>()
private let padsState: PadsState
// MARK: - Published Outputs
@Published var pads: [Pad] = []
// MARK: - Init
init(
padsState: PadsState
) {
self.padsState = padsState
setupSubscribers()
}
}
}
// MARK: - Publishers
extension PadsListView.ViewModel {
private var padsStatePublisher: Publishers.Share<AnyPublisher<PadsState, Never>> {
CurrentValueSubject(padsState)
// .print("padsStatePublisher")
.eraseToAnyPublisher()
.share()
}
private var padsFetchingStatePublisher: Publishers.Share<AnyPublisher<PadsState.DataFetchingState, Never>> {
padsStatePublisher
.map(\.dataFetchingState)
.eraseToAnyPublisher()
.share()
}
}
// MARK: - Computeds
extension PadsListView.ViewModel {
}
// MARK: - Public Methods
extension PadsListView.ViewModel {
}
// MARK: - Private Helpers
private extension PadsListView.ViewModel {
func setupSubscribers() {
padsFetchingStatePublisher
.receive(on: DispatchQueue.main)
.sink(receiveValue: { (fetchingState: PadsState.DataFetchingState) in
switch fetchingState {
case .inactive:
self.pads = []
case .fetching:
self.pads = []
case .fetched(let pads):
self.pads = pads
case .errored(_):
fatalError()
}
})
.store(in: &subscriptions)
}
}

View File

@ -0,0 +1,73 @@
//
// PadsListView.swift
// PadFinder
//
// Created by CypherPoet on 1/26/20.
//
//
import SwiftUI
struct PadsListView<Destination: View> {
@EnvironmentObject private var store: AppStore
@ObservedObject var viewModel: ViewModel
let buildDestination: ((Pad) -> Destination)
}
// MARK: - View
extension PadsListView: View {
var body: some View {
List(viewModel.pads) { pad in
NavigationLink(destination: self.buildDestination(pad)) {
HStack {
pad.padType.listItemImage
VStack(alignment: .leading) {
Text(pad.name)
.foregroundColor(.primary)
.font(.headline)
Text(pad.padType.displayName)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
}
}
}
// MARK: - Computeds
extension PadsListView {
}
// MARK: - View Variables
extension PadsListView {
}
// MARK: - Private Helpers
private extension PadsListView {
}
// MARK: - Preview
struct PadsListView_Previews: PreviewProvider {
static var previews: some View {
let store = PreviewData.AppStores.withPads
return PadsListView(
viewModel: .init(padsState: store.state.padsState),
buildDestination: { _ in EmptyView() }
)
.environmentObject(store)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

48
day-096/README.md Normal file
View File

@ -0,0 +1,48 @@
# Day 96: _Project 19: SnowSeeker_ (Part One)
_Follow along at https://www.hackingwithswift.com/100/swiftui/96_.
<br/>
# 📒 Field Notes
This day covers Part One of _`Project 19`_ in the [100 Days of SwiftUI Challenge](https://www.hackingwithswift.com/100/swiftui/96).
It focuses on several specific topics:
- SnowSeeker: Introduction
- Working with two side by side views in SwiftUI
- Using alert() and sheet() with optionals
- Using groups as transparent layout containers
## SnowSeeker: Introduction
From the project description:
> In this project were going to create SnowSeeker: an app to let users browse ski resorts around the world.
>
> ...
>
> This will be the first app where we specifically aim to make something that works great on iPad by showing two views side by side, but youll also get deep into solving problematic layouts, learn a new way to show sheets and alerts, and more.
📝 Since I couldn't help myself and wanted to use data from a real API, I decided to put a twist on this project and make it search for rocket launch pads through the [Launch Library API](https://github.com/CypherPoet/100-days-of-swiftui-and-combine/commit/c6ea070356c0910398a0e70f9b5083d402038756).
I'm also thinking there's a good opportunity to integrate Map Kit snapshots for showing the location of each launch pad -- we'll see 🙂.
## Working with two side by side views in SwiftUI
SwiftUI doesn't have a direct equivalent of UIKit's `UISplitViewController`, but, instead, relies on defining the structure of the `NavigationView` and having the system handle the "splitting" depending on the layout of the device.
## Using alert() and sheet() with optionals
Sometimes we want to show an alert or sheet if a Boolean value is true. Sometimes we want to show it if a value exists altogether. (Side note: This is why Optionals can be useful in general 🙂).
Thankfully, SwiftUI gives us a version of the `alert` or `sheet` modifiers that can bind to the existence of an `Identifiable` Optional. Much more useful than driving a Boolean with side-effects 💪.

19
day-097/README.md Normal file
View File

@ -0,0 +1,19 @@
# Day 97: _Project 19: PadFinder_ (Part Two)
_Follow along at https://www.hackingwithswift.com/100/swiftui/97_.
<br/>
# 📒 Field Notes
This day covers Part Two of _`Project 19`_ in the [100 Days of SwiftUI Challenge](https://www.hackingwithswift.com/100/swiftui/97). (Project 19 files can be found in the [directory for Part One](../day-096/).)
It focuses on several specific topics:
- Building a primary list of items
- Making NavigationView work in landscape
- Creating a secondary view for NavigationView
Commits for the changes related to this day can be found in the vicinity of [this one](https://github.com/CypherPoet/100-days-of-swiftui-and-combine/commit/c6ea070356c0910398a0e70f9b5083d402038756).

18
day-098/README.md Normal file
View File

@ -0,0 +1,18 @@
# Day 98: _Project 19: PadFinder_ (Part Three)
_Follow along at https://www.hackingwithswift.com/100/swiftui/98_.
<br/>
# 📒 Field Notes
This day covers Part Three of _`Project 19`_ in the [100 Days of SwiftUI Challenge](https://www.hackingwithswift.com/100/swiftui/98). (Project 19 files can be found in the [directory for Part One](../day-096/).)
It focuses on several specific topics:
- Changing a views layout in response to size classes
- Binding an alert to an optional string
- Letting the user mark favorites