Add functionality for updating the favorites state of a pad

This commit is contained in:
CypherPoet 2020-01-29 19:24:59 -06:00
parent 8ba7a51628
commit ad1f1b1420
8 changed files with 184 additions and 40 deletions

View File

@ -16,6 +16,7 @@
F32ED62623E0D525006A5195 /* Pad+Computeds.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32ED62523E0D525006A5195 /* Pad+Computeds.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 */; }; F32ED62923E0F0C6006A5195 /* MapSnapshottingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32ED62823E0F0C6006A5195 /* MapSnapshottingService.swift */; };
F32ED62B23E0F0F1006A5195 /* MapSnapshotServicing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32ED62A23E0F0F1006A5195 /* MapSnapshotServicing.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 */; }; F331C45C23DDB0AE0061925E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C45B23DDB0AE0061925E /* AppDelegate.swift */; };
F331C45E23DDB0AE0061925E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C45D23DDB0AE0061925E /* SceneDelegate.swift */; }; F331C45E23DDB0AE0061925E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C45D23DDB0AE0061925E /* SceneDelegate.swift */; };
F331C46223DDB0B00061925E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F331C46123DDB0B00061925E /* Assets.xcassets */; }; 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 */; }; F331C49823DE10010061925E /* PreviewData+AppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C49723DE10010061925E /* PreviewData+AppStore.swift */; };
F331C49C23DE18650061925E /* PadsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F331C49B23DE18650061925E /* PadsListView.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 */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@ -66,6 +68,7 @@
F331C49723DE10010061925E /* PreviewData+AppStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreviewData+AppStore.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>"; }; 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>"; }; 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 */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -74,6 +77,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
F331C49123DDF0A30061925E /* CypherPoetNetStack in Frameworks */, F331C49123DDF0A30061925E /* CypherPoetNetStack in Frameworks */,
F32ED62E23E163BB006A5195 /* CypherPoetPropertyWrappers in Frameworks */,
F331C48123DDDE080061925E /* CypherPoetSwiftUIKit in Frameworks */, F331C48123DDDE080061925E /* CypherPoetSwiftUIKit in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -86,6 +90,7 @@
children = ( children = (
F32ED61C23DF9B46006A5195 /* PadDetailsView.swift */, F32ED61C23DF9B46006A5195 /* PadDetailsView.swift */,
F32ED61E23DF9B7C006A5195 /* PadDetailsView+ViewModel.swift */, F32ED61E23DF9B7C006A5195 /* PadDetailsView+ViewModel.swift */,
F356E61D23E25E7A008553B0 /* PadDetailsContainerView.swift */,
); );
path = "Pad Details"; path = "Pad Details";
sourceTree = "<group>"; sourceTree = "<group>";
@ -270,6 +275,7 @@
packageProductDependencies = ( packageProductDependencies = (
F331C48023DDDE080061925E /* CypherPoetSwiftUIKit */, F331C48023DDDE080061925E /* CypherPoetSwiftUIKit */,
F331C49023DDF0A30061925E /* CypherPoetNetStack */, F331C49023DDF0A30061925E /* CypherPoetNetStack */,
F32ED62D23E163BB006A5195 /* CypherPoetPropertyWrappers */,
); );
productName = PadFinder; productName = PadFinder;
productReference = F331C45823DDB0AE0061925E /* PadFinder.app */; productReference = F331C45823DDB0AE0061925E /* PadFinder.app */;
@ -302,6 +308,7 @@
packageReferences = ( packageReferences = (
F331C47F23DDDE080061925E /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */, F331C47F23DDDE080061925E /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */,
F331C48F23DDF0A30061925E /* XCRemoteSwiftPackageReference "CypherPoetNetStack" */, F331C48F23DDF0A30061925E /* XCRemoteSwiftPackageReference "CypherPoetNetStack" */,
F32ED62C23E163BB006A5195 /* XCRemoteSwiftPackageReference "CypherPoetPropertyWrappers" */,
); );
productRefGroup = F331C45923DDB0AE0061925E /* Products */; productRefGroup = F331C45923DDB0AE0061925E /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -331,6 +338,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
F331C47C23DDB1710061925E /* PadsListContainerView.swift in Sources */, F331C47C23DDB1710061925E /* PadsListContainerView.swift in Sources */,
F356E61E23E25E7A008553B0 /* PadDetailsContainerView.swift in Sources */,
F331C48523DDDE710061925E /* AppState.swift in Sources */, F331C48523DDDE710061925E /* AppState.swift in Sources */,
F32ED62923E0F0C6006A5195 /* MapSnapshottingService.swift in Sources */, F32ED62923E0F0C6006A5195 /* MapSnapshottingService.swift in Sources */,
F32ED61F23DF9B7C006A5195 /* PadDetailsView+ViewModel.swift in Sources */, F32ED61F23DF9B7C006A5195 /* PadDetailsView+ViewModel.swift in Sources */,
@ -548,6 +556,14 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference 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" */ = { F331C47F23DDDE080061925E /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/CypherPoet/CypherPoetSwiftUIKit.git"; repositoryURL = "https://github.com/CypherPoet/CypherPoetSwiftUIKit.git";
@ -567,6 +583,11 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
F32ED62D23E163BB006A5195 /* CypherPoetPropertyWrappers */ = {
isa = XCSwiftPackageProductDependency;
package = F32ED62C23E163BB006A5195 /* XCRemoteSwiftPackageReference "CypherPoetPropertyWrappers" */;
productName = CypherPoetPropertyWrappers;
};
F331C48023DDDE080061925E /* CypherPoetSwiftUIKit */ = { F331C48023DDDE080061925E /* CypherPoetSwiftUIKit */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = F331C47F23DDDE080061925E /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */; package = F331C47F23DDDE080061925E /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */;

View File

@ -10,6 +10,15 @@
"version": "0.0.27" "version": "0.0.27"
} }
}, },
{
"package": "CypherPoetPropertyWrappers",
"repositoryURL": "https://github.com/CypherPoet/CypherPoetPropertyWrappers.git",
"state": {
"branch": null,
"revision": "9e1c62432f5a25a159e80dee28965e8cc20b7939",
"version": "0.0.1"
}
},
{ {
"package": "CypherPoetSwiftUIKit", "package": "CypherPoetSwiftUIKit",
"repositoryURL": "https://github.com/CypherPoet/CypherPoetSwiftUIKit.git", "repositoryURL": "https://github.com/CypherPoet/CypherPoetSwiftUIKit.git",

View File

@ -10,10 +10,14 @@
import Foundation import Foundation
import Combine import Combine
import CypherPoetSwiftUIKit_DataFlowUtils import CypherPoetSwiftUIKit_DataFlowUtils
import CypherPoetPropertyWrappers_UserDefault
struct PadsState { struct PadsState {
var dataFetchingState: DataFetchingState = .inactive 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 { extension PadsState.DataFetchingState: Equatable {
static func == ( static func == (
@ -71,6 +76,8 @@ enum PadsAction {
case padsFetchStart case padsFetchStart
case fetchedPadsSet([Pad]) case fetchedPadsSet([Pad])
case fetchErrorSet(Error) case fetchErrorSet(Error)
case favoriteAdded(Pad.ID)
case favoriteRemoved(Pad.ID)
} }
@ -84,7 +91,10 @@ let padsReducer: Reducer<PadsState, PadsAction> = Reducer(
state.dataFetchingState = .fetching state.dataFetchingState = .fetching
case .fetchErrorSet(let error): case .fetchErrorSet(let error):
state.dataFetchingState = .errored(error) state.dataFetchingState = .errored(error)
case .favoriteAdded(let padID):
state.favorites.append(padID)
case .favoriteRemoved(let padID):
state.favorites.removeAll(where: { $0 == padID })
} }
} }
) )

View File

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

View File

@ -16,7 +16,8 @@ extension PadDetailsView {
final class ViewModel: ObservableObject { final class ViewModel: ObservableObject {
private var subscriptions = Set<AnyCancellable>() private var subscriptions = Set<AnyCancellable>()
private let pad: Pad let pad: Pad
let isPadFavorited: Bool
private let snapshotService: MapSnapshotServicing private let snapshotService: MapSnapshotServicing
@ -27,9 +28,11 @@ extension PadDetailsView {
// MARK: - Init // MARK: - Init
init( init(
pad: Pad, pad: Pad,
isPadFavorited: Bool,
snapshotService: MapSnapshotServicing snapshotService: MapSnapshotServicing
) { ) {
self.pad = pad self.pad = pad
self.isPadFavorited = isPadFavorited
self.snapshotService = snapshotService self.snapshotService = snapshotService
setupSubscribers() setupSubscribers()
@ -46,15 +49,7 @@ extension PadDetailsView.ViewModel {
// MARK: - Computeds // MARK: - Computeds
extension PadDetailsView.ViewModel { extension PadDetailsView.ViewModel {
// TODO: Replace with MapKit Snapshot var padNameText: String { pad.name }
var mapImageName: String {
"CanaveralMapSample"
}
var padNameText: String {
pad.name
}
var latitudeText: String { var latitudeText: String {
NumberFormatters.padCoordinateDisplay.string(for: pad.latitude) ?? "" NumberFormatters.padCoordinateDisplay.string(for: pad.latitude) ?? ""
@ -64,9 +59,7 @@ extension PadDetailsView.ViewModel {
NumberFormatters.padCoordinateDisplay.string(for: pad.longitude) ?? "" NumberFormatters.padCoordinateDisplay.string(for: pad.longitude) ?? ""
} }
var wikipediaURL: URL? { var wikipediaURL: URL? { pad.wikiURL }
pad.wikiURL
}
var webLinkData: [(hostName: String, url: URL)] { var webLinkData: [(hostName: String, url: URL)] {
@ -80,6 +73,11 @@ extension PadDetailsView.ViewModel {
return (hostName: strippedHostName(from: hostName), url: url) return (hostName: strippedHostName(from: hostName), url: url)
} }
} }
var favoritesButtonText: String {
return isPadFavorited ? "Remove From Favorites" : "Add to Favorites"
}
} }

View File

@ -12,7 +12,11 @@ import CypherPoetSwiftUIKit
struct PadDetailsView { struct PadDetailsView {
@EnvironmentObject private var store: AppStore
@ObservedObject var viewModel: ViewModel @ObservedObject var viewModel: ViewModel
var onFavoriteToggled: ((Pad) -> Void)?
} }
@ -21,25 +25,22 @@ extension PadDetailsView: View {
var body: some View { var body: some View {
GeometryReader { geometry in GeometryReader { geometry in
VStack { List {
if self.viewModel.mapSnapshotImage != nil { if self.viewModel.mapSnapshotImage != nil {
Image(uiImage: self.viewModel.mapSnapshotImage!) Image(uiImage: self.viewModel.mapSnapshotImage!)
.resizable() .resizable()
.scaledToFit() .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 { if self.viewModel.webLinkData.isEmpty == false {
self.linksSection self.linksSection
} }
}
}
.padding(.horizontal)
Spacer() self.optionsSection
} }
.onAppear { .onAppear {
self.viewModel.takeMapSnapshot( self.viewModel.takeMapSnapshot(
@ -66,6 +67,8 @@ extension PadDetailsView {
private var coordinateHeader: some View { private var coordinateHeader: some View {
HStack(spacing: 12) { HStack(spacing: 12) {
Spacer()
Image(systemName: "mappin") Image(systemName: "mappin")
.padding() .padding()
.foregroundColor(.white) .foregroundColor(.white)
@ -80,8 +83,9 @@ extension PadDetailsView {
+ Text(self.viewModel.longitudeText) + Text(self.viewModel.longitudeText)
} }
.embedInCompactableStack() .embedInCompactableStack()
Spacer()
} }
.padding(.top)
} }
@ -89,7 +93,7 @@ extension PadDetailsView {
Section( Section(
header: Text("Links").font(.headline) header: Text("Links").font(.headline)
) { ) {
List(viewModel.webLinkData, id: \.0) { linkItem in ForEach(viewModel.webLinkData, id: \.0) { linkItem in
Button(action: { Button(action: {
UIApplication.shared.open(linkItem.url) 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( PadDetailsView(
viewModel: .init( viewModel: .init(
pad: PreviewData.Pads.pad1, pad: PreviewData.Pads.pad1,
isPadFavorited: false,
snapshotService: MapSnapshottingService( snapshotService: MapSnapshottingService(
snapshotOptions: snapshotOptions snapshotOptions: snapshotOptions
) )
@ -127,5 +151,6 @@ struct PadDetailsView_Previews: PreviewProvider {
) )
} }
.navigationViewStyle(StackNavigationViewStyle()) .navigationViewStyle(StackNavigationViewStyle())
.environmentObject(PreviewData.AppStores.withPads)
} }
} }

View File

@ -25,6 +25,7 @@ extension PadsListContainerView: View {
buildDestination: buildDestination(forPad:) buildDestination: buildDestination(forPad:)
) )
.navigationBarTitle("Launch Pads") .navigationBarTitle("Launch Pads")
.environmentObject(store)
WelcomeView() WelcomeView()
} }
@ -60,15 +61,17 @@ private extension PadsListContainerView {
} }
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 { func buildDestination(forPad pad: Pad) -> some View {
PadDetailsView( PadDetailsContainerView(pad: pad)
viewModel: .init(
pad: pad,
snapshotService: MapSnapshottingService(
snapshotOptions: makeSnapshotOptions(for: pad)
)
)
)
} }
} }

View File

@ -24,7 +24,7 @@ extension PadsListView {
// MARK: - Init // MARK: - Init
init( init(
padsState: PadsState = .init() padsState: PadsState
) { ) {
self.padsState = padsState self.padsState = padsState