Add functionality for updating the favorites state of a pad
This commit is contained in:
parent
8ba7a51628
commit
ad1f1b1420
|
@ -16,6 +16,7 @@
|
|||
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 */; };
|
||||
|
@ -35,6 +36,7 @@
|
|||
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 */
|
||||
|
@ -66,6 +68,7 @@
|
|||
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 */
|
||||
|
@ -74,6 +77,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F331C49123DDF0A30061925E /* CypherPoetNetStack in Frameworks */,
|
||||
F32ED62E23E163BB006A5195 /* CypherPoetPropertyWrappers in Frameworks */,
|
||||
F331C48123DDDE080061925E /* CypherPoetSwiftUIKit in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -86,6 +90,7 @@
|
|||
children = (
|
||||
F32ED61C23DF9B46006A5195 /* PadDetailsView.swift */,
|
||||
F32ED61E23DF9B7C006A5195 /* PadDetailsView+ViewModel.swift */,
|
||||
F356E61D23E25E7A008553B0 /* PadDetailsContainerView.swift */,
|
||||
);
|
||||
path = "Pad Details";
|
||||
sourceTree = "<group>";
|
||||
|
@ -270,6 +275,7 @@
|
|||
packageProductDependencies = (
|
||||
F331C48023DDDE080061925E /* CypherPoetSwiftUIKit */,
|
||||
F331C49023DDF0A30061925E /* CypherPoetNetStack */,
|
||||
F32ED62D23E163BB006A5195 /* CypherPoetPropertyWrappers */,
|
||||
);
|
||||
productName = PadFinder;
|
||||
productReference = F331C45823DDB0AE0061925E /* PadFinder.app */;
|
||||
|
@ -302,6 +308,7 @@
|
|||
packageReferences = (
|
||||
F331C47F23DDDE080061925E /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */,
|
||||
F331C48F23DDF0A30061925E /* XCRemoteSwiftPackageReference "CypherPoetNetStack" */,
|
||||
F32ED62C23E163BB006A5195 /* XCRemoteSwiftPackageReference "CypherPoetPropertyWrappers" */,
|
||||
);
|
||||
productRefGroup = F331C45923DDB0AE0061925E /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -331,6 +338,7 @@
|
|||
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 */,
|
||||
|
@ -548,6 +556,14 @@
|
|||
/* 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";
|
||||
|
@ -567,6 +583,11 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
F32ED62D23E163BB006A5195 /* CypherPoetPropertyWrappers */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = F32ED62C23E163BB006A5195 /* XCRemoteSwiftPackageReference "CypherPoetPropertyWrappers" */;
|
||||
productName = CypherPoetPropertyWrappers;
|
||||
};
|
||||
F331C48023DDDE080061925E /* CypherPoetSwiftUIKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = F331C47F23DDDE080061925E /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */;
|
||||
|
|
|
@ -10,6 +10,15 @@
|
|||
"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",
|
||||
|
|
|
@ -10,10 +10,14 @@
|
|||
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]
|
||||
}
|
||||
|
||||
|
||||
|
@ -27,6 +31,7 @@ extension PadsState {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
extension PadsState.DataFetchingState: Equatable {
|
||||
|
||||
static func == (
|
||||
|
@ -71,6 +76,8 @@ enum PadsAction {
|
|||
case padsFetchStart
|
||||
case fetchedPadsSet([Pad])
|
||||
case fetchErrorSet(Error)
|
||||
case favoriteAdded(Pad.ID)
|
||||
case favoriteRemoved(Pad.ID)
|
||||
}
|
||||
|
||||
|
||||
|
@ -84,7 +91,10 @@ let padsReducer: Reducer<PadsState, PadsAction> = Reducer(
|
|||
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,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()
|
||||
// }
|
||||
//}
|
|
@ -16,20 +16,23 @@ extension PadDetailsView {
|
|||
final class ViewModel: ObservableObject {
|
||||
private var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
private let pad: Pad
|
||||
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
|
||||
|
||||
setupSubscribers()
|
||||
|
@ -46,15 +49,7 @@ extension PadDetailsView.ViewModel {
|
|||
// MARK: - Computeds
|
||||
extension PadDetailsView.ViewModel {
|
||||
|
||||
// TODO: Replace with MapKit Snapshot
|
||||
var mapImageName: String {
|
||||
"CanaveralMapSample"
|
||||
}
|
||||
|
||||
var padNameText: String {
|
||||
pad.name
|
||||
}
|
||||
|
||||
var padNameText: String { pad.name }
|
||||
|
||||
var latitudeText: String {
|
||||
NumberFormatters.padCoordinateDisplay.string(for: pad.latitude) ?? ""
|
||||
|
@ -64,9 +59,7 @@ extension PadDetailsView.ViewModel {
|
|||
NumberFormatters.padCoordinateDisplay.string(for: pad.longitude) ?? ""
|
||||
}
|
||||
|
||||
var wikipediaURL: URL? {
|
||||
pad.wikiURL
|
||||
}
|
||||
var wikipediaURL: URL? { pad.wikiURL }
|
||||
|
||||
|
||||
var webLinkData: [(hostName: String, url: URL)] {
|
||||
|
@ -80,6 +73,11 @@ extension PadDetailsView.ViewModel {
|
|||
return (hostName: strippedHostName(from: hostName), url: url)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var favoritesButtonText: String {
|
||||
return isPadFavorited ? "Remove From Favorites" : "Add to Favorites"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,11 @@ import CypherPoetSwiftUIKit
|
|||
|
||||
|
||||
struct PadDetailsView {
|
||||
@EnvironmentObject private var store: AppStore
|
||||
|
||||
@ObservedObject var viewModel: ViewModel
|
||||
|
||||
var onFavoriteToggled: ((Pad) -> Void)?
|
||||
}
|
||||
|
||||
|
||||
|
@ -21,25 +25,22 @@ extension PadDetailsView: View {
|
|||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
VStack {
|
||||
List {
|
||||
if self.viewModel.mapSnapshotImage != nil {
|
||||
Image(uiImage: self.viewModel.mapSnapshotImage!)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
}
|
||||
|
||||
Group {
|
||||
self.coordinateHeader
|
||||
|
||||
self.coordinateHeader
|
||||
.padding(.vertical)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
if self.viewModel.webLinkData.isEmpty == false {
|
||||
self.linksSection
|
||||
}
|
||||
}
|
||||
if self.viewModel.webLinkData.isEmpty == false {
|
||||
self.linksSection
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
Spacer()
|
||||
|
||||
self.optionsSection
|
||||
}
|
||||
.onAppear {
|
||||
self.viewModel.takeMapSnapshot(
|
||||
|
@ -66,6 +67,8 @@ extension PadDetailsView {
|
|||
|
||||
private var coordinateHeader: some View {
|
||||
HStack(spacing: 12) {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "mappin")
|
||||
.padding()
|
||||
.foregroundColor(.white)
|
||||
|
@ -80,8 +83,9 @@ extension PadDetailsView {
|
|||
+ Text(self.viewModel.longitudeText)
|
||||
}
|
||||
.embedInCompactableStack()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
|
||||
|
||||
|
@ -89,7 +93,7 @@ extension PadDetailsView {
|
|||
Section(
|
||||
header: Text("Links").font(.headline)
|
||||
) {
|
||||
List(viewModel.webLinkData, id: \.0) { linkItem in
|
||||
ForEach(viewModel.webLinkData, id: \.0) { linkItem in
|
||||
Button(action: {
|
||||
UIApplication.shared.open(linkItem.url)
|
||||
}) {
|
||||
|
@ -99,6 +103,25 @@ extension PadDetailsView {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -120,6 +143,7 @@ struct PadDetailsView_Previews: PreviewProvider {
|
|||
PadDetailsView(
|
||||
viewModel: .init(
|
||||
pad: PreviewData.Pads.pad1,
|
||||
isPadFavorited: false,
|
||||
snapshotService: MapSnapshottingService(
|
||||
snapshotOptions: snapshotOptions
|
||||
)
|
||||
|
@ -127,5 +151,6 @@ struct PadDetailsView_Previews: PreviewProvider {
|
|||
)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.environmentObject(PreviewData.AppStores.withPads)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ extension PadsListContainerView: View {
|
|||
buildDestination: buildDestination(forPad:)
|
||||
)
|
||||
.navigationBarTitle("Launch Pads")
|
||||
.environmentObject(store)
|
||||
|
||||
WelcomeView()
|
||||
}
|
||||
|
@ -59,16 +60,18 @@ private extension PadsListContainerView {
|
|||
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 {
|
||||
PadDetailsView(
|
||||
viewModel: .init(
|
||||
pad: pad,
|
||||
snapshotService: MapSnapshottingService(
|
||||
snapshotOptions: makeSnapshotOptions(for: pad)
|
||||
)
|
||||
)
|
||||
)
|
||||
PadDetailsContainerView(pad: pad)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,11 +20,11 @@ extension PadsListView {
|
|||
|
||||
// MARK: - Published Outputs
|
||||
@Published var pads: [Pad] = []
|
||||
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
init(
|
||||
padsState: PadsState = .init()
|
||||
padsState: PadsState
|
||||
) {
|
||||
self.padsState = padsState
|
||||
|
||||
|
|
Loading…
Reference in New Issue