Skip to content

Commit

Permalink
Merge pull request #569 from pennlabs/jhawk0224/contacts-swiftui-rewrite
Browse files Browse the repository at this point in the history
Rewrite Contacts in SwiftUI & remove all Objective-C from codebase
  • Loading branch information
JHawk0224 authored Nov 17, 2024
2 parents d4d1a41 + 4e2ea19 commit f8273aa
Show file tree
Hide file tree
Showing 57 changed files with 344 additions and 544 deletions.
46 changes: 16 additions & 30 deletions PennMobile.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions PennMobile/Banners/BannerDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
// Copyright © 2023 PennLabs. All rights reserved.
//

import Foundation

struct BannerDescription: Equatable, Codable {
var image: URL
var text: String
Expand Down
1 change: 1 addition & 0 deletions PennMobile/Buildings/Cells/MenuTableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import UIKit

class MenuTableView: UITableView {
override var contentSize: CGSize {
Expand Down
93 changes: 0 additions & 93 deletions PennMobile/Contacts/ContactCell.swift

This file was deleted.

111 changes: 75 additions & 36 deletions PennMobile/Contacts/ContactManager.swift
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
}
}
51 changes: 51 additions & 0 deletions PennMobile/Contacts/ContactModel.swift
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]
}
67 changes: 67 additions & 0 deletions PennMobile/Contacts/ContactRowView.swift
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)
}
Loading

0 comments on commit f8273aa

Please sign in to comment.