Merge branch 'master' of https://github.com/CypherPoet/100-days-of-swiftui
This commit is contained in:
commit
85b7673872
|
@ -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/)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='6.0' target-platform='ios'/>
|
|
@ -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 */;
|
||||
}
|
7
day-096/Projects/PadFinder/PadFinder.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
day-096/Projects/PadFinder/PadFinder.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
|
@ -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>
|
|
@ -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
|
||||
}
|
|
@ -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>
|
|
@ -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.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
)
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// PreviewData.swift
|
||||
// PadFinder
|
||||
//
|
||||
// Created by CypherPoet on 1/26/20.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
enum PreviewData {}
|
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
// }
|
||||
//}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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 we’re 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 you’ll 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 💪.
|
|
@ -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).
|
|
@ -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 view’s layout in response to size classes
|
||||
- Binding an alert to an optional string
|
||||
- Letting the user mark favorites
|
||||
|
||||
|
Loading…
Reference in New Issue