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 */; };
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" */;

View File

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

View File

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

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

View File

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

View File

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

View File

@ -20,11 +20,11 @@ extension PadsListView {
// MARK: - Published Outputs
@Published var pads: [Pad] = []
// MARK: - Init
init(
padsState: PadsState = .init()
padsState: PadsState
) {
self.padsState = padsState