Complete Challenge 3

> Use an action sheet to customize the way users are sorted in each screen – by name or by most recent.
This commit is contained in:
CypherPoet 2020-01-16 21:32:42 -06:00
parent b4b7f62067
commit 877c2acfa9
12 changed files with 133 additions and 22 deletions

View File

@ -49,6 +49,7 @@
F326140E23CFFDE2009EC215 /* ContactsListView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F326140D23CFFDE2009EC215 /* ContactsListView+ViewModel.swift */; };
F326141323D1073A009EC215 /* LocalNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = F326141223D1073A009EC215 /* LocalNotifications.swift */; };
F326141523D107FC009EC215 /* UserNotificationsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F326141423D107FC009EC215 /* UserNotificationsService.swift */; };
F326141723D14EEF009EC215 /* Contact+SortingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F326141623D14EEF009EC215 /* Contact+SortingState.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -93,6 +94,7 @@
F326140D23CFFDE2009EC215 /* ContactsListView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactsListView+ViewModel.swift"; sourceTree = "<group>"; };
F326141223D1073A009EC215 /* LocalNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotifications.swift; sourceTree = "<group>"; };
F326141423D107FC009EC215 /* UserNotificationsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationsService.swift; sourceTree = "<group>"; };
F326141623D14EEF009EC215 /* Contact+SortingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Contact+SortingState.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -246,6 +248,7 @@
isa = PBXGroup;
children = (
F3203BB823C8467000265268 /* Contact+FilterState.swift */,
F326141623D14EEF009EC215 /* Contact+SortingState.swift */,
F3203BBB23C8579400265268 /* Contact+CoreDataClass.swift */,
F3203BBC23C8579400265268 /* Contact+CoreDataProperties.swift */,
F326140923CE689A009EC215 /* Contact+InitHelpers.swift */,
@ -401,6 +404,7 @@
F3203BF423CD140100265268 /* ContactQRCodeRepresentable.swift in Sources */,
F3203BB723C844F100265268 /* ContactsListView.swift in Sources */,
F3203BED23CC97DC00265268 /* UserQRCodeContainerView+ViewModel.swift in Sources */,
F326141723D14EEF009EC215 /* Contact+SortingState.swift in Sources */,
F3203BB523C841A700265268 /* ContactsContainerView+ViewModel.swift in Sources */,
F326140A23CE689A009EC215 /* Contact+InitHelpers.swift in Sources */,
F3203BEB23CC977000265268 /* UserQRCodeContainerView.swift in Sources */,

View File

@ -13,7 +13,8 @@ import CoreData
extension Contact: ContactQRCodeRepresentable {
@NSManaged public var qrCodeData: Data?
@NSManaged public var dateAdded: Date
@NSManaged public var statusValue: Int16
var status: Status {

View File

@ -18,16 +18,31 @@ extension Contact {
}
enum SortDescriptors {
static let `default` = [
NSSortDescriptor(keyPath: \Contact.name, ascending: true)
]
public enum SortDescriptors {
public static let byName = NSSortDescriptor(keyPath: \Contact.name, ascending: true)
public static let byMostRecent = NSSortDescriptor(keyPath: \Contact.dateAdded, ascending: false)
public static let byOldestAdded = NSSortDescriptor(keyPath: \Contact.dateAdded, ascending: true)
public static let `default` = Self.byName
public static func forSortingState(_ sortingState: Contact.SortingState) -> [NSSortDescriptor] {
switch sortingState {
case .byName:
return [Self.byName]
case .byMostRecent:
return [Self.byMostRecent, byName]
case .byOldestAdded:
return [Self.byOldestAdded, byName]
}
}
}
enum Predicate {
public enum Predicate {
static func contacts(for status: Status) -> NSPredicate {
public static func contacts(for status: Contact.Status) -> NSPredicate {
let keyword = NSComparisonPredicate.keyword(for: .equalTo)
return NSPredicate(
@ -40,10 +55,13 @@ extension Contact {
static func fetchRequest(for filterState: FilterState) -> NSFetchRequest<Contact> {
@nonobjc public class func fetchRequest(
filteringOn filterState: FilterState,
sortingBy sortingState: SortingState
) -> NSFetchRequest<Contact> {
let fetchRequest: NSFetchRequest<Contact> = Self.fetchRequest()
fetchRequest.sortDescriptors = Self.SortDescriptors.default
fetchRequest.sortDescriptors = Self.SortDescriptors.forSortingState(sortingState)
if let status = filterState.contactStatus {
fetchRequest.predicate = Self.Predicate.contacts(for: status)
@ -51,5 +69,4 @@ extension Contact {
return fetchRequest
}
}

View File

@ -10,7 +10,7 @@ import SwiftUI
extension Contact {
enum FilterState: String {
public enum FilterState: String {
case all
case contacted
case uncontacted
@ -20,14 +20,14 @@ extension Contact {
extension Contact.FilterState: CaseIterable {}
extension Contact.FilterState: Identifiable {
var id: String { self.rawValue }
public var id: String { self.rawValue }
}
// MARK: - Computeds
extension Contact.FilterState {
var displayName: String {
public var displayName: String {
switch self {
case .all:
return "All"

View File

@ -20,8 +20,10 @@ extension Contact {
let contact = Contact(context: context)
contact.name = name
contact.uuid = UUID()
contact.status = status
contact.uuid = UUID()
contact.dateAdded = Date()
return contact
}

View File

@ -0,0 +1,42 @@
//
// Contact+SortingState.swift
// QRConnections
//
// Created by CypherPoet on 1/16/20.
//
//
import Foundation
extension Contact {
public enum SortingState: String {
case byName
case byMostRecent
case byOldestAdded
}
}
extension Contact.SortingState: CaseIterable {}
extension Contact.SortingState: Identifiable {
public var id: String { self.rawValue }
}
// MARK: - Computeds
extension Contact.SortingState {
public var displayName: String {
switch self {
case .byName:
return "By Name"
case .byMostRecent:
return "By Most Recent"
case .byOldestAdded:
return "By First Added"
}
}
}

View File

@ -10,7 +10,7 @@ import Foundation
extension Contact {
enum Status: Int16 {
public enum Status: Int16 {
case contacted
case uncontacted
}

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Contact" representedClassName="Contact" syncable="YES">
<attribute name="dateAdded" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="qrCodeData" optional="YES" attributeType="Binary"/>
<attribute name="statusValue" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
@ -12,6 +13,6 @@
</uniquenessConstraints>
</entity>
<elements>
<element name="Contact" positionX="6.3984375" positionY="-108.1640625" width="128" height="103"/>
<element name="Contact" positionX="6.3984375" positionY="-108.1640625" width="128" height="118"/>
</elements>
</model>

View File

@ -25,6 +25,7 @@ extension SampleData {
contactUUIDs.enumerated().map { (index, uuid) in
let contact = Contact(context: context)
contact.dateAdded = Date()
contact.status = Contact.Status.allCases.randomElement()!
contact.uuid = uuid
contact.name = "Contact \(index + 1)"

View File

@ -19,6 +19,7 @@ extension ContactsContainerView {
// MARK: - Published Properties
@Published var filterState: Contact.FilterState = .all
@Published var sortingState: Contact.SortingState = .byName
}
}

View File

@ -15,6 +15,7 @@ struct ContactsContainerView {
@ObservedObject var viewModel = ViewModel()
@State private var isShowingScannerView = false
@State private var isShowingSortingActionSheet = false
}
@ -30,13 +31,17 @@ extension ContactsContainerView: View {
contactsList
}
.navigationBarTitle("Collected Contacts")
.navigationBarItems(trailing: addContactButton)
.navigationBarItems(
leading: changeSortingButton,
trailing: addContactButton
)
.sheet(isPresented: $isShowingScannerView) {
QRCodeScannerView(
simulatedData: "🚀 Rocket Man",
onScanCompleted: self.codeScanCompleted(_:)
)
}
.actionSheet(isPresented: $isShowingSortingActionSheet) { self.sortingActionSheet }
}
}
}
@ -61,12 +66,26 @@ extension ContactsContainerView {
private var contactsList: some View {
ContactsListView(
viewModel: .init(filterState: viewModel.filterState),
filterState: viewModel.filterState
viewModel: .init(
filterState: viewModel.filterState
),
filterState: viewModel.filterState,
sortingState: viewModel.sortingState
)
}
private var changeSortingButton: some View {
Button(action: {
self.isShowingSortingActionSheet = true
}) {
Image(systemName: "arrow.up.and.down")
.imageScale(.large)
Text("Sorting")
}
}
private var addContactButton: some View {
Button(action: {
self.isShowingScannerView = true
@ -76,6 +95,21 @@ extension ContactsContainerView {
Text("Scan")
}
}
private var sortingActionSheet: ActionSheet {
let actionButtons: [ActionSheet.Button] = Contact.SortingState.allCases.map { sortingState in
.default(
Text(sortingState.displayName),
action: { self.viewModel.sortingState = sortingState }
)
}
return ActionSheet(
title: Text("Sort contacts in the list"),
buttons: actionButtons + [.cancel()]
)
}
}

View File

@ -17,10 +17,17 @@ struct ContactsListView {
init(
viewModel: ViewModel = .init(),
filterState: Contact.FilterState
filterState: Contact.FilterState,
sortingState: Contact.SortingState
) {
self.viewModel = viewModel
self.fetchRequest = FetchRequest(fetchRequest: Contact.fetchRequest(for: filterState))
self.fetchRequest = FetchRequest(
fetchRequest: Contact.fetchRequest(
filteringOn: filterState,
sortingBy: sortingState
)
)
}
}
@ -73,7 +80,8 @@ struct ContactsListView_Previews: PreviewProvider {
return Group {
ContactsListView(
viewModel: .init(filterState: .all),
filterState: .all
filterState: .all,
sortingState: .byName
)
.environment(\.managedObjectContext, managedObjectContext)