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:
parent
b4b7f62067
commit
877c2acfa9
|
@ -49,6 +49,7 @@
|
||||||
F326140E23CFFDE2009EC215 /* ContactsListView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F326140D23CFFDE2009EC215 /* ContactsListView+ViewModel.swift */; };
|
F326140E23CFFDE2009EC215 /* ContactsListView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F326140D23CFFDE2009EC215 /* ContactsListView+ViewModel.swift */; };
|
||||||
F326141323D1073A009EC215 /* LocalNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = F326141223D1073A009EC215 /* LocalNotifications.swift */; };
|
F326141323D1073A009EC215 /* LocalNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = F326141223D1073A009EC215 /* LocalNotifications.swift */; };
|
||||||
F326141523D107FC009EC215 /* UserNotificationsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F326141423D107FC009EC215 /* UserNotificationsService.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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
@ -93,6 +94,7 @@
|
||||||
F326140D23CFFDE2009EC215 /* ContactsListView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactsListView+ViewModel.swift"; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -246,6 +248,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
F3203BB823C8467000265268 /* Contact+FilterState.swift */,
|
F3203BB823C8467000265268 /* Contact+FilterState.swift */,
|
||||||
|
F326141623D14EEF009EC215 /* Contact+SortingState.swift */,
|
||||||
F3203BBB23C8579400265268 /* Contact+CoreDataClass.swift */,
|
F3203BBB23C8579400265268 /* Contact+CoreDataClass.swift */,
|
||||||
F3203BBC23C8579400265268 /* Contact+CoreDataProperties.swift */,
|
F3203BBC23C8579400265268 /* Contact+CoreDataProperties.swift */,
|
||||||
F326140923CE689A009EC215 /* Contact+InitHelpers.swift */,
|
F326140923CE689A009EC215 /* Contact+InitHelpers.swift */,
|
||||||
|
@ -401,6 +404,7 @@
|
||||||
F3203BF423CD140100265268 /* ContactQRCodeRepresentable.swift in Sources */,
|
F3203BF423CD140100265268 /* ContactQRCodeRepresentable.swift in Sources */,
|
||||||
F3203BB723C844F100265268 /* ContactsListView.swift in Sources */,
|
F3203BB723C844F100265268 /* ContactsListView.swift in Sources */,
|
||||||
F3203BED23CC97DC00265268 /* UserQRCodeContainerView+ViewModel.swift in Sources */,
|
F3203BED23CC97DC00265268 /* UserQRCodeContainerView+ViewModel.swift in Sources */,
|
||||||
|
F326141723D14EEF009EC215 /* Contact+SortingState.swift in Sources */,
|
||||||
F3203BB523C841A700265268 /* ContactsContainerView+ViewModel.swift in Sources */,
|
F3203BB523C841A700265268 /* ContactsContainerView+ViewModel.swift in Sources */,
|
||||||
F326140A23CE689A009EC215 /* Contact+InitHelpers.swift in Sources */,
|
F326140A23CE689A009EC215 /* Contact+InitHelpers.swift in Sources */,
|
||||||
F3203BEB23CC977000265268 /* UserQRCodeContainerView.swift in Sources */,
|
F3203BEB23CC977000265268 /* UserQRCodeContainerView.swift in Sources */,
|
||||||
|
|
|
@ -13,7 +13,8 @@ import CoreData
|
||||||
|
|
||||||
extension Contact: ContactQRCodeRepresentable {
|
extension Contact: ContactQRCodeRepresentable {
|
||||||
@NSManaged public var qrCodeData: Data?
|
@NSManaged public var qrCodeData: Data?
|
||||||
|
|
||||||
|
@NSManaged public var dateAdded: Date
|
||||||
@NSManaged public var statusValue: Int16
|
@NSManaged public var statusValue: Int16
|
||||||
|
|
||||||
var status: Status {
|
var status: Status {
|
||||||
|
|
|
@ -18,16 +18,31 @@ extension Contact {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum SortDescriptors {
|
public enum SortDescriptors {
|
||||||
static let `default` = [
|
public static let byName = NSSortDescriptor(keyPath: \Contact.name, ascending: true)
|
||||||
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)
|
let keyword = NSComparisonPredicate.keyword(for: .equalTo)
|
||||||
|
|
||||||
return NSPredicate(
|
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()
|
let fetchRequest: NSFetchRequest<Contact> = Self.fetchRequest()
|
||||||
|
|
||||||
fetchRequest.sortDescriptors = Self.SortDescriptors.default
|
fetchRequest.sortDescriptors = Self.SortDescriptors.forSortingState(sortingState)
|
||||||
|
|
||||||
if let status = filterState.contactStatus {
|
if let status = filterState.contactStatus {
|
||||||
fetchRequest.predicate = Self.Predicate.contacts(for: status)
|
fetchRequest.predicate = Self.Predicate.contacts(for: status)
|
||||||
|
@ -51,5 +69,4 @@ extension Contact {
|
||||||
|
|
||||||
return fetchRequest
|
return fetchRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
extension Contact {
|
extension Contact {
|
||||||
enum FilterState: String {
|
public enum FilterState: String {
|
||||||
case all
|
case all
|
||||||
case contacted
|
case contacted
|
||||||
case uncontacted
|
case uncontacted
|
||||||
|
@ -20,14 +20,14 @@ extension Contact {
|
||||||
extension Contact.FilterState: CaseIterable {}
|
extension Contact.FilterState: CaseIterable {}
|
||||||
|
|
||||||
extension Contact.FilterState: Identifiable {
|
extension Contact.FilterState: Identifiable {
|
||||||
var id: String { self.rawValue }
|
public var id: String { self.rawValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Computeds
|
// MARK: - Computeds
|
||||||
extension Contact.FilterState {
|
extension Contact.FilterState {
|
||||||
|
|
||||||
var displayName: String {
|
public var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .all:
|
case .all:
|
||||||
return "All"
|
return "All"
|
||||||
|
|
|
@ -20,8 +20,10 @@ extension Contact {
|
||||||
let contact = Contact(context: context)
|
let contact = Contact(context: context)
|
||||||
|
|
||||||
contact.name = name
|
contact.name = name
|
||||||
contact.uuid = UUID()
|
|
||||||
contact.status = status
|
contact.status = status
|
||||||
|
|
||||||
|
contact.uuid = UUID()
|
||||||
|
contact.dateAdded = Date()
|
||||||
|
|
||||||
return contact
|
return contact
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Foundation
|
||||||
|
|
||||||
|
|
||||||
extension Contact {
|
extension Contact {
|
||||||
enum Status: Int16 {
|
public enum Status: Int16 {
|
||||||
case contacted
|
case contacted
|
||||||
case uncontacted
|
case uncontacted
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?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="">
|
<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">
|
<entity name="Contact" representedClassName="Contact" syncable="YES">
|
||||||
|
<attribute name="dateAdded" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="name" attributeType="String"/>
|
<attribute name="name" attributeType="String"/>
|
||||||
<attribute name="qrCodeData" optional="YES" attributeType="Binary"/>
|
<attribute name="qrCodeData" optional="YES" attributeType="Binary"/>
|
||||||
<attribute name="statusValue" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="statusValue" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
@ -12,6 +13,6 @@
|
||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
<elements>
|
<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>
|
</elements>
|
||||||
</model>
|
</model>
|
|
@ -25,6 +25,7 @@ extension SampleData {
|
||||||
contactUUIDs.enumerated().map { (index, uuid) in
|
contactUUIDs.enumerated().map { (index, uuid) in
|
||||||
let contact = Contact(context: context)
|
let contact = Contact(context: context)
|
||||||
|
|
||||||
|
contact.dateAdded = Date()
|
||||||
contact.status = Contact.Status.allCases.randomElement()!
|
contact.status = Contact.Status.allCases.randomElement()!
|
||||||
contact.uuid = uuid
|
contact.uuid = uuid
|
||||||
contact.name = "Contact \(index + 1)"
|
contact.name = "Contact \(index + 1)"
|
||||||
|
|
|
@ -19,6 +19,7 @@ extension ContactsContainerView {
|
||||||
|
|
||||||
// MARK: - Published Properties
|
// MARK: - Published Properties
|
||||||
@Published var filterState: Contact.FilterState = .all
|
@Published var filterState: Contact.FilterState = .all
|
||||||
|
@Published var sortingState: Contact.SortingState = .byName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ struct ContactsContainerView {
|
||||||
@ObservedObject var viewModel = ViewModel()
|
@ObservedObject var viewModel = ViewModel()
|
||||||
|
|
||||||
@State private var isShowingScannerView = false
|
@State private var isShowingScannerView = false
|
||||||
|
@State private var isShowingSortingActionSheet = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,13 +31,17 @@ extension ContactsContainerView: View {
|
||||||
contactsList
|
contactsList
|
||||||
}
|
}
|
||||||
.navigationBarTitle("Collected Contacts")
|
.navigationBarTitle("Collected Contacts")
|
||||||
.navigationBarItems(trailing: addContactButton)
|
.navigationBarItems(
|
||||||
|
leading: changeSortingButton,
|
||||||
|
trailing: addContactButton
|
||||||
|
)
|
||||||
.sheet(isPresented: $isShowingScannerView) {
|
.sheet(isPresented: $isShowingScannerView) {
|
||||||
QRCodeScannerView(
|
QRCodeScannerView(
|
||||||
simulatedData: "🚀 Rocket Man",
|
simulatedData: "🚀 Rocket Man",
|
||||||
onScanCompleted: self.codeScanCompleted(_:)
|
onScanCompleted: self.codeScanCompleted(_:)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.actionSheet(isPresented: $isShowingSortingActionSheet) { self.sortingActionSheet }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,12 +66,26 @@ extension ContactsContainerView {
|
||||||
|
|
||||||
private var contactsList: some View {
|
private var contactsList: some View {
|
||||||
ContactsListView(
|
ContactsListView(
|
||||||
viewModel: .init(filterState: viewModel.filterState),
|
viewModel: .init(
|
||||||
filterState: viewModel.filterState
|
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 {
|
private var addContactButton: some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
self.isShowingScannerView = true
|
self.isShowingScannerView = true
|
||||||
|
@ -76,6 +95,21 @@ extension ContactsContainerView {
|
||||||
Text("Scan")
|
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()]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,17 @@ struct ContactsListView {
|
||||||
|
|
||||||
init(
|
init(
|
||||||
viewModel: ViewModel = .init(),
|
viewModel: ViewModel = .init(),
|
||||||
filterState: Contact.FilterState
|
filterState: Contact.FilterState,
|
||||||
|
sortingState: Contact.SortingState
|
||||||
) {
|
) {
|
||||||
self.viewModel = viewModel
|
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 {
|
return Group {
|
||||||
ContactsListView(
|
ContactsListView(
|
||||||
viewModel: .init(filterState: .all),
|
viewModel: .init(filterState: .all),
|
||||||
filterState: .all
|
filterState: .all,
|
||||||
|
sortingState: .byName
|
||||||
)
|
)
|
||||||
.environment(\.managedObjectContext, managedObjectContext)
|
.environment(\.managedObjectContext, managedObjectContext)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue