-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #569 from pennlabs/jhawk0224/contacts-swiftui-rewrite
Rewrite Contacts in SwiftUI & remove all Objective-C from codebase
- Loading branch information
Showing
57 changed files
with
344 additions
and
544 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,83 +1,122 @@ | ||
// | ||
// EmergencyContacts.swift | ||
// ContactManager.swift | ||
// PennMobile | ||
// | ||
// Created by Josh Doman on 5/12/17. | ||
// Copyright © 2017 PennLabs. All rights reserved. | ||
// Created by Jordan Hochman on 11/16/24. | ||
// Copyright © 2024 PennLabs. All rights reserved. | ||
// | ||
|
||
import Contacts | ||
|
||
extension SupportItem { | ||
|
||
extension Contact { | ||
var cnContact: CNMutableContact { | ||
let contact = CNMutableContact() | ||
contact.givenName = self.contactName | ||
contact.phoneNumbers = [CNLabeledValue( | ||
label: CNLabelPhoneNumberiPhone, | ||
label: CNLabelPhoneNumberMain, | ||
value: CNPhoneNumber(stringValue: self.phoneFiltered))] | ||
if let desc = self.descriptionText { | ||
if let desc = self.description { | ||
contact.note = desc | ||
} | ||
return contact | ||
} | ||
|
||
} | ||
|
||
class ContactManager: NSObject { | ||
|
||
static let shared = ContactManager() | ||
private let contactStore = CNContactStore() | ||
|
||
func requestAccess() async -> Bool { | ||
return await withCheckedContinuation { continuation in | ||
contactStore.requestAccess(for: .contacts) { granted, _ in | ||
continuation.resume(returning: granted) | ||
} | ||
} | ||
} | ||
|
||
func doesHaveAccess() -> Bool { | ||
let access = CNContactStore.authorizationStatus(for: .contacts) | ||
|
||
var valid: [CNAuthorizationStatus] = [.authorized] | ||
if #available(iOS 18.0, *) { | ||
valid.append(.limited) | ||
} | ||
return valid.contains(access) | ||
} | ||
|
||
func save(_ items: [SupportItem], callback: @escaping (_ success: Bool) -> Void) { | ||
func saveContacts(_ contacts: [Contact]) -> Bool { | ||
let saveRequest = CNSaveRequest() | ||
let store = CNContactStore() | ||
for item in items { | ||
saveRequest.add(item.cnContact, toContainerWithIdentifier: nil) | ||
for contact in contacts { | ||
saveRequest.add(contact.cnContact, toContainerWithIdentifier: nil) | ||
} | ||
|
||
do { | ||
try store.execute(saveRequest) | ||
callback(true) | ||
try contactStore.execute(saveRequest) | ||
return true | ||
} catch { | ||
callback(false) | ||
return false | ||
} | ||
} | ||
|
||
func delete(_ items: [SupportItem], callback: (_ success: Bool) -> Void) { | ||
var successful = true | ||
for item in items { | ||
delete(item) { (success) in | ||
successful = successful ? success : false | ||
func deleteContacts(_ contacts: [Contact]) -> Bool { | ||
var success = true | ||
for contact in contacts { | ||
if !deleteContact(contact) { | ||
success = false | ||
} | ||
} | ||
callback(successful) | ||
return success | ||
} | ||
|
||
func delete(_ item: SupportItem, callback2: (_ success: Bool) -> Void) { | ||
let store = CNContactStore() | ||
let predicate = CNContact.predicateForContacts(matchingName: item.contactName) | ||
func deleteContact(_ contact: Contact) -> Bool { | ||
let predicate = CNContact.predicateForContacts(matchingName: contact.contactName) | ||
let toFetch = [CNContactGivenNameKey] as [CNKeyDescriptor] | ||
|
||
do { | ||
let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: toFetch) | ||
guard contacts.count > 0 else { | ||
callback2(true) // no contacts found | ||
return | ||
let contacts = try contactStore.unifiedContacts(matching: predicate, keysToFetch: toFetch) | ||
guard !contacts.isEmpty else { | ||
return true // no contacts found | ||
} | ||
|
||
var success = true | ||
for contact in contacts { | ||
let req = CNSaveRequest() | ||
let mutableContact = contact.mutableCopy() as! CNMutableContact | ||
req.delete(mutableContact) | ||
|
||
let mutableContact = contact.mutableCopy() as? CNMutableContact | ||
if let mutableContact { | ||
req.delete(mutableContact) | ||
} | ||
|
||
do { | ||
try store.execute(req) | ||
callback2(true) // successfully deleted user | ||
try contactStore.execute(req) | ||
} catch { | ||
callback2(false) | ||
success = false | ||
} | ||
} | ||
return success | ||
} catch { | ||
callback2(false) | ||
return false | ||
} | ||
} | ||
|
||
func checkContactsExist(_ contacts: [Contact]) -> Bool { | ||
if !doesHaveAccess() { | ||
return false | ||
} | ||
|
||
let toFetch = [CNContactGivenNameKey] as [CNKeyDescriptor] | ||
|
||
for contact in contacts { | ||
let predicate = CNContact.predicateForContacts(matchingName: contact.contactName) | ||
|
||
do { | ||
let contacts = try contactStore.unifiedContacts(matching: predicate, keysToFetch: toFetch) | ||
if contacts.isEmpty { | ||
return false // at least one contact does not exist | ||
} | ||
} catch { | ||
return false // error occured during fetching | ||
} | ||
} | ||
return true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// | ||
// ContactModel.swift | ||
// PennMobile | ||
// | ||
// Created by Jordan Hochman on 11/16/24. | ||
// Copyright © 2024 PennLabs. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
struct Contact: Identifiable { | ||
let name: String | ||
let contactName: String | ||
let phone: String | ||
let description: String? | ||
let phoneFiltered: String | ||
|
||
var id: String { name } | ||
|
||
init(name: String, contactName: String, phoneNumber: String, desc: String? = nil) { | ||
self.name = name | ||
self.phone = phoneNumber | ||
self.contactName = contactName | ||
self.description = desc | ||
self.phoneFiltered = phoneNumber.filter { $0.isNumber } | ||
} | ||
} | ||
|
||
extension Contact { | ||
static let pennGeneral = Contact(name: "Penn Police (Non-Emergency)", contactName: "Penn Police (Non-Emergency)", phoneNumber: "(215) 898-7297", desc: "Call for all non-emergencies.") | ||
|
||
static let pennEmergency = Contact(name: "Penn Police/MERT (Emergency)", contactName: "Penn Police/MERT (Emergency)", phoneNumber: "(215) 573-3333", desc: "Call for all criminal or medical emergencies.") | ||
|
||
static let pennWalk = Contact(name: "Penn Walk", contactName: "Penn Walk", phoneNumber: "215-898-WALK (9255)", desc: "Call for a walking escort between 30th and 43rd Streets and Market Street and Baltimore Avenue.") | ||
|
||
static let pennRide = Contact(name: "Penn Ride", contactName: "Penn Ride", phoneNumber: "215-898-RIDE (7433)", desc: "Call for Penn Ride services.") | ||
|
||
static let helpLine = Contact(name: "Help Line", contactName: "Penn Help Line", phoneNumber: "215-898-HELP (4357)", desc: "24-hour phone line for navigating Penn's health and wellness resources.") | ||
|
||
static let caps = Contact(name: "CAPS", contactName: "Penn CAPS", phoneNumber: "215-898-7021", desc: "Call anytime to reach Penn's Counseling and Psychological Services Center.") | ||
|
||
static let specialServices = Contact(name: "Special Services", contactName: "Penn Special Services", phoneNumber: "215-898-4481", desc: "Call to inquire or receive support services when victimized by any type of crime.") | ||
|
||
static let womensCenter = Contact(name: "Women's Center", contactName: "Penn Women's Center", phoneNumber: "215-898-8611", desc: "The Women's Center sponsors programs on career development, stress management, parenting, violence prevention, and more.") | ||
|
||
static let shs = Contact(name: "Student Health Services", contactName: "Penn Student Health Services", phoneNumber: "215-746-3535", desc: "Call to make an appointment, contact a department, or address urgent medical issues.") | ||
|
||
static let ofa = Contact(name: "Office of Affirmative Action", contactName: "Penn Office of Affirmative Action", phoneNumber: "(215) 898-6993", desc: "Call regarding issues related to the University's obligations as an aff. action and equal opp. employer and educational institution.") | ||
|
||
static let contacts = [pennEmergency, pennGeneral, pennWalk, pennRide, helpLine, caps, specialServices, womensCenter, shs, ofa] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
// | ||
// ContactRowView.swift | ||
// PennMobile | ||
// | ||
// Created by Jordan Hochman on 11/16/24. | ||
// Copyright © 2024 PennLabs. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
|
||
struct ContactRowView: View { | ||
let contact: Contact | ||
@State var isExpanded = false | ||
|
||
var body: some View { | ||
HStack { | ||
Button(action: { | ||
call(number: contact.phoneFiltered) | ||
}) { | ||
Image("phone") | ||
.resizable() | ||
.frame(width: 30, height: 30) | ||
} | ||
.buttonStyle(.plain) | ||
|
||
Button(action: { | ||
withAnimation { | ||
isExpanded.toggle() | ||
} | ||
}) { | ||
HStack { | ||
VStack(alignment: .leading) { | ||
Text(contact.name) | ||
|
||
if isExpanded { | ||
Text(contact.phone) | ||
.font(.subheadline) | ||
|
||
if let desc = contact.description { | ||
Text(desc) | ||
.font(.subheadline) | ||
} | ||
} | ||
} | ||
|
||
Spacer() | ||
|
||
Image(systemName: isExpanded ? "chevron.up" : "chevron.down") | ||
.foregroundColor(.gray) | ||
} | ||
.contentShape(Rectangle()) | ||
} | ||
.buttonStyle(.plain) | ||
} | ||
} | ||
|
||
private func call(number: String) { | ||
guard let url = URL(string: "tel://" + number) else { | ||
return | ||
} | ||
UIApplication.shared.open(url) | ||
} | ||
} | ||
|
||
#Preview { | ||
ContactRowView(contact: Contact.pennGeneral) | ||
} |
Oops, something went wrong.