Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,7 @@ As the app is still in development and there are a lot of small items on this li
### Important

- [ ] Update as needed to ensure compatibility with the new web app
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings) **--Claimed**
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings). @nathanielbscout took a look at this but couldn't reliably reproduce it - it is not fixed.
- [ ] Thoroughly test error display in different (re)load scenarios
- [ ] Ask server if there are new notifications past a certain date / fetch new notifications after a certain date (would likely require server changes)
- [ ] About screen. It is hooked up but has no meaningful content.
Expand DownExpand Up@@ -75,8 +75,8 @@ As the app is still in development and there are a lot of small items on this li
- [ ] Create an app widget (to give directions without being in the app; be cognizant of different map apps)
- [ ] Alternate event app text (in place of logo when it is missing, like in the android app)
- [ ] Swipe from edge of screen to open menu
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads) **--Claimed**
- [] Make sure prayer partner groups have the same numbers as in the android app **--Claimed**
- [] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that. **--Claimed**
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads). @nathanielbscout took a look at this, but found that it only happens in a couple of places. It may cause some trouble, but there are not any clear issues with it.
- [x] Make sure prayer partner groups have the same numbers as in the android app.
- [x] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that.
- [ ] Start refresh timer when opening the app to go off when it would have gone off, if the timer did not fire when the app was opened.
- [ ] Fix white bar above sidebar menu (very noticeable in app switcher)
4 changes: 3 additions & 1 deletion iOSEventApp.xcodeproj/project.pbxproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@
F1AF66712053292300B8820D /* ContactTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTableViewCell.swift; sourceTree = "<group>"; };
F1AF66732053433B00B8820D /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = "<group>"; };
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV2.xcdatamodel; sourceTree = "<group>"; };
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV3.xcdatamodel; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand DownExpand Up@@ -718,10 +719,11 @@
F149D6CE204DCE2F000DA81A /* iOSEventApp.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */,
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */,
F149D6CF204DCE2F000DA81A /* iOSEventApp.xcdatamodel */,
);
currentVersion = F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */;
currentVersion = F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */;
path = iOSEventApp.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
Expand Down
9 changes: 7 additions & 2 deletions iOSEventApp/MainContainerViewController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,11 @@ protocol TakesArrayData: AnyObject {
var dataArray: [Any]? { get set }
}

/// A workaround to allow sorting of data without having to do a separate if else for every data type.
/**
A workaround to allow sorting of data without having to do a separate if else for every data type.

It is important to sort for consistency across devices - although sometimes the order needs to be kept the same as in the JSON (e.g. prayer partners), which can be handled by passing the indices as strings.
*/
protocol IsComparable {
var compareString: String? { get }
}
Expand DownExpand Up@@ -168,7 +172,8 @@ class MainContainerViewController: UIViewController {
}

if let takesArrayData = vc as? TakesArrayData {
if let sortable = data as? [IsComparable] { // All objects in the array are the same type
// Sort the data if possible - it is simpler to sort here and not in each individual sidebar view controller.
if let sortable = data as? [IsComparable] {
takesArrayData.dataArray = sortable.sorted(by: { (obj1, obj2) -> Bool in
guard let str1 = obj1.compareString, let str2 = obj2.compareString else {
return false
Expand Down
11 changes: 7 additions & 4 deletions iOSEventApp/Model/DataController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ struct MalformedDataInformation: CustomStringConvertible {

DataController also starts the refresh timer in the refreshController, which it keeps a static reference to. The refresh timer triggers a reload of notifications.

Passing a context around different threads is against recommended practice. It will remain as is until changed.
The context should not be carelessly accessed from a thread different from the one it was created on. The only time a context is potentially passed between threads in this class is when a data task is performed - and it may be improperly handled in `loadNotificationsFromURL` or methods that call it, but since there are no clear issues right now it is being left alone.
*/
class DataController: NSObject {

Expand DownExpand Up@@ -150,6 +150,8 @@ class DataController: NSObject {
return
}

// This is in a performBackgroundTask block.
// I still don't think the context should *really* be passed like this, but there are no apparent issues with this at the moment.
self.loadNotificationsFromURL(context: context, url: URL(string: notificationsURLString)!) { (success, nErrors, newNotifications) in
if let additionalErrors = nErrors {
if case .unableToSave(_)? = additionalErrors.first {
Expand DownExpand Up@@ -391,10 +393,11 @@ extension DataController {
var sidebarKVPairs = [String: String]()
let groupDicts = partnerGroups as! [[String: Any]]

for obj in groupDicts {
// The prayer partners come in an array, and so the order should be preserved.
for (index, obj) in groupDicts.enumerated() {
if let partnerNames = obj["students"] {
if partnerNames is String {
groupstoCreate.append(["students": partnerNames])
groupstoCreate.append(["students": partnerNames, "order": Int16(index)])
}
else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "PrayerPartnerGroup", propertyName: "students", missingProperty: nil)))
Expand DownExpand Up@@ -638,7 +641,7 @@ extension DataController {
sidebarNum += 1

let pageIdentifier = key
// Ideally this wouldn't be hardcoded for an index of 0
// The event data has the sidebar info at the first index of each page array
guard let sidebarName = value[0][sidebarNameKey] as? String else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "Information Page \(pageNum)", propertyName: pageIdentifier, missingProperty: sidebarNameKey)))
continue
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>iOSEventAppV2.xcdatamodel</string>
<string>iOSEventAppV3.xcdatamodel</string>
</dict>
</plist>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Contact" representedClassName="Contact" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="phone" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ContactPageSection" representedClassName="ContactPageSection" syncable="YES" codeGenerationType="class">
<attribute name="content" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="header" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="id" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="key" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="General" representedClassName="General" syncable="YES" codeGenerationType="class">
<attribute name="custom_time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="event_name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="logo" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
<attribute name="notifications_url" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh_expire" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh_rate" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="remote_viewing" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="version_num" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="welcome_message" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="year" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="HousingUnit" representedClassName="HousingUnit" syncable="YES" codeGenerationType="class">
<attribute name="driver" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="hostName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="InformationPage" representedClassName="InformationPage" syncable="YES" codeGenerationType="class">
<attribute name="pageName" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoNav" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SidebarAppearance" inverseName="infoPage" inverseEntity="SidebarAppearance" syncable="YES"/>
<relationship name="infoSections" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="InformationPageSection" inverseName="infoPage" inverseEntity="InformationPageSection" syncable="YES"/>
</entity>
<entity name="InformationPageSection" representedClassName="InformationPageSection" syncable="YES" codeGenerationType="class">
<attribute name="information" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoSections" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Notification" representedClassName="Notification" syncable="YES" codeGenerationType="class">
<attribute name="body" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="notificationNumber" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="PrayerPartnerGroup" representedClassName="PrayerPartnerGroup" syncable="YES" codeGenerationType="class">
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ScheduleDay" representedClassName="ScheduleDay" syncable="YES" codeGenerationType="class">
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ScheduleItem" inverseName="day" inverseEntity="ScheduleItem" syncable="YES"/>
</entity>
<entity name="ScheduleItem" representedClassName="ScheduleItem" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="itemDescription" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="length" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="location" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="startTime" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="day" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ScheduleDay" inverseName="items" inverseEntity="ScheduleDay" syncable="YES"/>
</entity>
<entity name="SidebarAppearance" representedClassName="SidebarAppearance" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="icon" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="nav" attributeType="String" syncable="YES"/>
<attribute name="optionalIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoNav" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Theme" representedClassName="Theme" syncable="YES" codeGenerationType="class">
<attribute name="themeName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="themeValue" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<elements>
<element name="Contact" positionX="27" positionY="126" width="128" height="90"/>
<element name="ContactPageSection" positionX="-9" positionY="90" width="128" height="105"/>
<element name="General" positionX="18" positionY="63" width="128" height="225"/>
<element name="HousingUnit" positionX="18" positionY="63" width="128" height="90"/>
<element name="InformationPage" positionX="18" positionY="126" width="128" height="90"/>
<element name="InformationPageSection" positionX="27" positionY="135" width="128" height="105"/>
<element name="Notification" positionX="9" positionY="126" width="128" height="120"/>
<element name="PrayerPartnerGroup" positionX="0" positionY="45" width="128" height="75"/>
<element name="ScheduleDay" positionX="-9" positionY="81" width="128" height="75"/>
<element name="ScheduleItem" positionX="0" positionY="90" width="128" height="135"/>
<element name="SidebarAppearance" positionX="9" positionY="54" width="128" height="135"/>
<element name="Theme" positionX="36" positionY="135" width="128" height="75"/>
</elements>
</model>
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import UIKit

extension PrayerPartnerGroup: IsComparable {
var compareString: String? {
return students
return String(order)
}
}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,7 @@ As the app is still in development and there are a lot of small items on this li
### Important

- [ ] Update as needed to ensure compatibility with the new web app
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings) **--Claimed**
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings). @nathanielbscout took a look at this but couldn't reliably reproduce it - it is not fixed.
- [ ] Thoroughly test error display in different (re)load scenarios
- [ ] Ask server if there are new notifications past a certain date / fetch new notifications after a certain date (would likely require server changes)
- [ ] About screen. It is hooked up but has no meaningful content.
Expand DownExpand Up@@ -75,8 +75,8 @@ As the app is still in development and there are a lot of small items on this li
- [ ] Create an app widget (to give directions without being in the app; be cognizant of different map apps)
- [ ] Alternate event app text (in place of logo when it is missing, like in the android app)
- [ ] Swipe from edge of screen to open menu
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads) **--Claimed**
- [] Make sure prayer partner groups have the same numbers as in the android app **--Claimed**
- [] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that. **--Claimed**
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads). @nathanielbscout took a look at this, but found that it only happens in a couple of places. It may cause some trouble, but there are not any clear issues with it.
- [x] Make sure prayer partner groups have the same numbers as in the android app.
- [x] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that.
- [ ] Start refresh timer when opening the app to go off when it would have gone off, if the timer did not fire when the app was opened.
- [ ] Fix white bar above sidebar menu (very noticeable in app switcher)
4 changes: 3 additions & 1 deletion iOSEventApp.xcodeproj/project.pbxproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@
F1AF66712053292300B8820D /* ContactTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTableViewCell.swift; sourceTree = "<group>"; };
F1AF66732053433B00B8820D /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = "<group>"; };
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV2.xcdatamodel; sourceTree = "<group>"; };
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV3.xcdatamodel; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand DownExpand Up@@ -718,10 +719,11 @@
F149D6CE204DCE2F000DA81A /* iOSEventApp.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */,
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */,
F149D6CF204DCE2F000DA81A /* iOSEventApp.xcdatamodel */,
);
currentVersion = F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */;
currentVersion = F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */;
path = iOSEventApp.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
Expand Down
9 changes: 7 additions & 2 deletions iOSEventApp/MainContainerViewController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,11 @@ protocol TakesArrayData: AnyObject {
var dataArray: [Any]? { get set }
}

/// A workaround to allow sorting of data without having to do a separate if else for every data type.
/**
A workaround to allow sorting of data without having to do a separate if else for every data type.

It is important to sort for consistency across devices - although sometimes the order needs to be kept the same as in the JSON (e.g. prayer partners), which can be handled by passing the indices as strings.
*/
protocol IsComparable {
var compareString: String? { get }
}
Expand DownExpand Up@@ -168,7 +172,8 @@ class MainContainerViewController: UIViewController {
}

if let takesArrayData = vc as? TakesArrayData {
if let sortable = data as? [IsComparable] { // All objects in the array are the same type
// Sort the data if possible - it is simpler to sort here and not in each individual sidebar view controller.
if let sortable = data as? [IsComparable] {
takesArrayData.dataArray = sortable.sorted(by: { (obj1, obj2) -> Bool in
guard let str1 = obj1.compareString, let str2 = obj2.compareString else {
return false
Expand Down
11 changes: 7 additions & 4 deletions iOSEventApp/Model/DataController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ struct MalformedDataInformation: CustomStringConvertible {

DataController also starts the refresh timer in the refreshController, which it keeps a static reference to. The refresh timer triggers a reload of notifications.

Passing a context around different threads is against recommended practice. It will remain as is until changed.
The context should not be carelessly accessed from a thread different from the one it was created on. The only time a context is potentially passed between threads in this class is when a data task is performed - and it may be improperly handled in `loadNotificationsFromURL` or methods that call it, but since there are no clear issues right now it is being left alone.
*/
class DataController: NSObject {

Expand DownExpand Up@@ -150,6 +150,8 @@ class DataController: NSObject {
return
}

// This is in a performBackgroundTask block.
// I still don't think the context should *really* be passed like this, but there are no apparent issues with this at the moment.
self.loadNotificationsFromURL(context: context, url: URL(string: notificationsURLString)!) { (success, nErrors, newNotifications) in
if let additionalErrors = nErrors {
if case .unableToSave(_)? = additionalErrors.first {
Expand DownExpand Up@@ -391,10 +393,11 @@ extension DataController {
var sidebarKVPairs = [String: String]()
let groupDicts = partnerGroups as! [[String: Any]]

for obj in groupDicts {
// The prayer partners come in an array, and so the order should be preserved.
for (index, obj) in groupDicts.enumerated() {
if let partnerNames = obj["students"] {
if partnerNames is String {
groupstoCreate.append(["students": partnerNames])
groupstoCreate.append(["students": partnerNames, "order": Int16(index)])
}
else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "PrayerPartnerGroup", propertyName: "students", missingProperty: nil)))
Expand DownExpand Up@@ -638,7 +641,7 @@ extension DataController {
sidebarNum += 1

let pageIdentifier = key
// Ideally this wouldn't be hardcoded for an index of 0
// The event data has the sidebar info at the first index of each page array
guard let sidebarName = value[0][sidebarNameKey] as? String else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "Information Page \(pageNum)", propertyName: pageIdentifier, missingProperty: sidebarNameKey)))
continue
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>iOSEventAppV2.xcdatamodel</string>
<string>iOSEventAppV3.xcdatamodel</string>
</dict>
</plist>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Contact" representedClassName="Contact" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="phone" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ContactPageSection" representedClassName="ContactPageSection" syncable="YES" codeGenerationType="class">
<attribute name="content" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="header" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="id" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="key" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="General" representedClassName="General" syncable="YES" codeGenerationType="class">
<attribute name="custom_time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="event_name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="logo" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
<attribute name="notifications_url" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh_expire" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh_rate" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="remote_viewing" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="version_num" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="welcome_message" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="year" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="HousingUnit" representedClassName="HousingUnit" syncable="YES" codeGenerationType="class">
<attribute name="driver" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="hostName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="InformationPage" representedClassName="InformationPage" syncable="YES" codeGenerationType="class">
<attribute name="pageName" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoNav" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SidebarAppearance" inverseName="infoPage" inverseEntity="SidebarAppearance" syncable="YES"/>
<relationship name="infoSections" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="InformationPageSection" inverseName="infoPage" inverseEntity="InformationPageSection" syncable="YES"/>
</entity>
<entity name="InformationPageSection" representedClassName="InformationPageSection" syncable="YES" codeGenerationType="class">
<attribute name="information" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoSections" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Notification" representedClassName="Notification" syncable="YES" codeGenerationType="class">
<attribute name="body" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="notificationNumber" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="PrayerPartnerGroup" representedClassName="PrayerPartnerGroup" syncable="YES" codeGenerationType="class">
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ScheduleDay" representedClassName="ScheduleDay" syncable="YES" codeGenerationType="class">
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ScheduleItem" inverseName="day" inverseEntity="ScheduleItem" syncable="YES"/>
</entity>
<entity name="ScheduleItem" representedClassName="ScheduleItem" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="itemDescription" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="length" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="location" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="startTime" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="day" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ScheduleDay" inverseName="items" inverseEntity="ScheduleDay" syncable="YES"/>
</entity>
<entity name="SidebarAppearance" representedClassName="SidebarAppearance" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="icon" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="nav" attributeType="String" syncable="YES"/>
<attribute name="optionalIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoNav" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Theme" representedClassName="Theme" syncable="YES" codeGenerationType="class">
<attribute name="themeName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="themeValue" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<elements>
<element name="Contact" positionX="27" positionY="126" width="128" height="90"/>
<element name="ContactPageSection" positionX="-9" positionY="90" width="128" height="105"/>
<element name="General" positionX="18" positionY="63" width="128" height="225"/>
<element name="HousingUnit" positionX="18" positionY="63" width="128" height="90"/>
<element name="InformationPage" positionX="18" positionY="126" width="128" height="90"/>
<element name="InformationPageSection" positionX="27" positionY="135" width="128" height="105"/>
<element name="Notification" positionX="9" positionY="126" width="128" height="120"/>
<element name="PrayerPartnerGroup" positionX="0" positionY="45" width="128" height="75"/>
<element name="ScheduleDay" positionX="-9" positionY="81" width="128" height="75"/>
<element name="ScheduleItem" positionX="0" positionY="90" width="128" height="135"/>
<element name="SidebarAppearance" positionX="9" positionY="54" width="128" height="135"/>
<element name="Theme" positionX="36" positionY="135" width="128" height="75"/>
</elements>
</model>
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import UIKit

extension PrayerPartnerGroup: IsComparable {
var compareString: String? {
return students
return String(order)
}
}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,7 @@ As the app is still in development and there are a lot of small items on this li
### Important

- [ ] Update as needed to ensure compatibility with the new web app
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings) **--Claimed**
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings). @nathanielbscout took a look at this but couldn't reliably reproduce it - it is not fixed.
- [ ] Thoroughly test error display in different (re)load scenarios
- [ ] Ask server if there are new notifications past a certain date / fetch new notifications after a certain date (would likely require server changes)
- [ ] About screen. It is hooked up but has no meaningful content.
Expand DownExpand Up@@ -75,8 +75,8 @@ As the app is still in development and there are a lot of small items on this li
- [ ] Create an app widget (to give directions without being in the app; be cognizant of different map apps)
- [ ] Alternate event app text (in place of logo when it is missing, like in the android app)
- [ ] Swipe from edge of screen to open menu
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads) **--Claimed**
- [] Make sure prayer partner groups have the same numbers as in the android app **--Claimed**
- [] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that. **--Claimed**
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads). @nathanielbscout took a look at this, but found that it only happens in a couple of places. It may cause some trouble, but there are not any clear issues with it.
- [x] Make sure prayer partner groups have the same numbers as in the android app.
- [x] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that.
- [ ] Start refresh timer when opening the app to go off when it would have gone off, if the timer did not fire when the app was opened.
- [ ] Fix white bar above sidebar menu (very noticeable in app switcher)
4 changes: 3 additions & 1 deletion iOSEventApp.xcodeproj/project.pbxproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@
F1AF66712053292300B8820D /* ContactTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTableViewCell.swift; sourceTree = "<group>"; };
F1AF66732053433B00B8820D /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = "<group>"; };
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV2.xcdatamodel; sourceTree = "<group>"; };
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV3.xcdatamodel; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand DownExpand Up@@ -718,10 +719,11 @@
F149D6CE204DCE2F000DA81A /* iOSEventApp.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */,
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */,
F149D6CF204DCE2F000DA81A /* iOSEventApp.xcdatamodel */,
);
currentVersion = F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */;
currentVersion = F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */;
path = iOSEventApp.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
Expand Down
9 changes: 7 additions & 2 deletions iOSEventApp/MainContainerViewController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,11 @@ protocol TakesArrayData: AnyObject {
var dataArray: [Any]? { get set }
}

/// A workaround to allow sorting of data without having to do a separate if else for every data type.
/**
A workaround to allow sorting of data without having to do a separate if else for every data type.

It is important to sort for consistency across devices - although sometimes the order needs to be kept the same as in the JSON (e.g. prayer partners), which can be handled by passing the indices as strings.
*/
protocol IsComparable {
var compareString: String? { get }
}
Expand DownExpand Up@@ -168,7 +172,8 @@ class MainContainerViewController: UIViewController {
}

if let takesArrayData = vc as? TakesArrayData {
if let sortable = data as? [IsComparable] { // All objects in the array are the same type
// Sort the data if possible - it is simpler to sort here and not in each individual sidebar view controller.
if let sortable = data as? [IsComparable] {
takesArrayData.dataArray = sortable.sorted(by: { (obj1, obj2) -> Bool in
guard let str1 = obj1.compareString, let str2 = obj2.compareString else {
return false
Expand Down
11 changes: 7 additions & 4 deletions iOSEventApp/Model/DataController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ struct MalformedDataInformation: CustomStringConvertible {

DataController also starts the refresh timer in the refreshController, which it keeps a static reference to. The refresh timer triggers a reload of notifications.

Passing a context around different threads is against recommended practice. It will remain as is until changed.
The context should not be carelessly accessed from a thread different from the one it was created on. The only time a context is potentially passed between threads in this class is when a data task is performed - and it may be improperly handled in `loadNotificationsFromURL` or methods that call it, but since there are no clear issues right now it is being left alone.
*/
class DataController: NSObject {

Expand DownExpand Up@@ -150,6 +150,8 @@ class DataController: NSObject {
return
}

// This is in a performBackgroundTask block.
// I still don't think the context should *really* be passed like this, but there are no apparent issues with this at the moment.
self.loadNotificationsFromURL(context: context, url: URL(string: notificationsURLString)!) { (success, nErrors, newNotifications) in
if let additionalErrors = nErrors {
if case .unableToSave(_)? = additionalErrors.first {
Expand DownExpand Up@@ -391,10 +393,11 @@ extension DataController {
var sidebarKVPairs = [String: String]()
let groupDicts = partnerGroups as! [[String: Any]]

for obj in groupDicts {
// The prayer partners come in an array, and so the order should be preserved.
for (index, obj) in groupDicts.enumerated() {
if let partnerNames = obj["students"] {
if partnerNames is String {
groupstoCreate.append(["students": partnerNames])
groupstoCreate.append(["students": partnerNames, "order": Int16(index)])
}
else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "PrayerPartnerGroup", propertyName: "students", missingProperty: nil)))
Expand DownExpand Up@@ -638,7 +641,7 @@ extension DataController {
sidebarNum += 1

let pageIdentifier = key
// Ideally this wouldn't be hardcoded for an index of 0
// The event data has the sidebar info at the first index of each page array
guard let sidebarName = value[0][sidebarNameKey] as? String else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "Information Page \(pageNum)", propertyName: pageIdentifier, missingProperty: sidebarNameKey)))
continue
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>iOSEventAppV2.xcdatamodel</string>
<string>iOSEventAppV3.xcdatamodel</string>
</dict>
</plist>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Contact" representedClassName="Contact" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="phone" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ContactPageSection" representedClassName="ContactPageSection" syncable="YES" codeGenerationType="class">
<attribute name="content" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="header" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="id" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="key" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="General" representedClassName="General" syncable="YES" codeGenerationType="class">
<attribute name="custom_time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="event_name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="logo" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
<attribute name="notifications_url" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh_expire" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh_rate" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="remote_viewing" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="version_num" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="welcome_message" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="year" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="HousingUnit" representedClassName="HousingUnit" syncable="YES" codeGenerationType="class">
<attribute name="driver" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="hostName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="InformationPage" representedClassName="InformationPage" syncable="YES" codeGenerationType="class">
<attribute name="pageName" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoNav" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SidebarAppearance" inverseName="infoPage" inverseEntity="SidebarAppearance" syncable="YES"/>
<relationship name="infoSections" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="InformationPageSection" inverseName="infoPage" inverseEntity="InformationPageSection" syncable="YES"/>
</entity>
<entity name="InformationPageSection" representedClassName="InformationPageSection" syncable="YES" codeGenerationType="class">
<attribute name="information" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoSections" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Notification" representedClassName="Notification" syncable="YES" codeGenerationType="class">
<attribute name="body" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="notificationNumber" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="PrayerPartnerGroup" representedClassName="PrayerPartnerGroup" syncable="YES" codeGenerationType="class">
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ScheduleDay" representedClassName="ScheduleDay" syncable="YES" codeGenerationType="class">
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ScheduleItem" inverseName="day" inverseEntity="ScheduleItem" syncable="YES"/>
</entity>
<entity name="ScheduleItem" representedClassName="ScheduleItem" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="itemDescription" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="length" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="location" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="startTime" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="day" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ScheduleDay" inverseName="items" inverseEntity="ScheduleDay" syncable="YES"/>
</entity>
<entity name="SidebarAppearance" representedClassName="SidebarAppearance" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="icon" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="nav" attributeType="String" syncable="YES"/>
<attribute name="optionalIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoNav" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Theme" representedClassName="Theme" syncable="YES" codeGenerationType="class">
<attribute name="themeName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="themeValue" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<elements>
<element name="Contact" positionX="27" positionY="126" width="128" height="90"/>
<element name="ContactPageSection" positionX="-9" positionY="90" width="128" height="105"/>
<element name="General" positionX="18" positionY="63" width="128" height="225"/>
<element name="HousingUnit" positionX="18" positionY="63" width="128" height="90"/>
<element name="InformationPage" positionX="18" positionY="126" width="128" height="90"/>
<element name="InformationPageSection" positionX="27" positionY="135" width="128" height="105"/>
<element name="Notification" positionX="9" positionY="126" width="128" height="120"/>
<element name="PrayerPartnerGroup" positionX="0" positionY="45" width="128" height="75"/>
<element name="ScheduleDay" positionX="-9" positionY="81" width="128" height="75"/>
<element name="ScheduleItem" positionX="0" positionY="90" width="128" height="135"/>
<element name="SidebarAppearance" positionX="9" positionY="54" width="128" height="135"/>
<element name="Theme" positionX="36" positionY="135" width="128" height="75"/>
</elements>
</model>
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import UIKit

extension PrayerPartnerGroup: IsComparable {
var compareString: String? {
return students
return String(order)
}
}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,7 @@ As the app is still in development and there are a lot of small items on this li
### Important

- [ ] Update as needed to ensure compatibility with the new web app
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings) **--Claimed**
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings). @nathanielbscout took a look at this but couldn't reliably reproduce it - it is not fixed.
- [ ] Thoroughly test error display in different (re)load scenarios
- [ ] Ask server if there are new notifications past a certain date / fetch new notifications after a certain date (would likely require server changes)
- [ ] About screen. It is hooked up but has no meaningful content.
Expand DownExpand Up@@ -75,8 +75,8 @@ As the app is still in development and there are a lot of small items on this li
- [ ] Create an app widget (to give directions without being in the app; be cognizant of different map apps)
- [ ] Alternate event app text (in place of logo when it is missing, like in the android app)
- [ ] Swipe from edge of screen to open menu
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads) **--Claimed**
- [] Make sure prayer partner groups have the same numbers as in the android app **--Claimed**
- [] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that. **--Claimed**
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads). @nathanielbscout took a look at this, but found that it only happens in a couple of places. It may cause some trouble, but there are not any clear issues with it.
- [x] Make sure prayer partner groups have the same numbers as in the android app.
- [x] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that.
- [ ] Start refresh timer when opening the app to go off when it would have gone off, if the timer did not fire when the app was opened.
- [ ] Fix white bar above sidebar menu (very noticeable in app switcher)
4 changes: 3 additions & 1 deletion iOSEventApp.xcodeproj/project.pbxproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@
F1AF66712053292300B8820D /* ContactTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTableViewCell.swift; sourceTree = "<group>"; };
F1AF66732053433B00B8820D /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = "<group>"; };
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV2.xcdatamodel; sourceTree = "<group>"; };
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV3.xcdatamodel; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand DownExpand Up@@ -718,10 +719,11 @@
F149D6CE204DCE2F000DA81A /* iOSEventApp.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */,
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */,
F149D6CF204DCE2F000DA81A /* iOSEventApp.xcdatamodel */,
);
currentVersion = F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */;
currentVersion = F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */;
path = iOSEventApp.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
Expand Down
9 changes: 7 additions & 2 deletions iOSEventApp/MainContainerViewController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,11 @@ protocol TakesArrayData: AnyObject {
var dataArray: [Any]? { get set }
}

/// A workaround to allow sorting of data without having to do a separate if else for every data type.
/**
A workaround to allow sorting of data without having to do a separate if else for every data type.

It is important to sort for consistency across devices - although sometimes the order needs to be kept the same as in the JSON (e.g. prayer partners), which can be handled by passing the indices as strings.
*/
protocol IsComparable {
var compareString: String? { get }
}
Expand DownExpand Up@@ -168,7 +172,8 @@ class MainContainerViewController: UIViewController {
}

if let takesArrayData = vc as? TakesArrayData {
if let sortable = data as? [IsComparable] { // All objects in the array are the same type
// Sort the data if possible - it is simpler to sort here and not in each individual sidebar view controller.
if let sortable = data as? [IsComparable] {
takesArrayData.dataArray = sortable.sorted(by: { (obj1, obj2) -> Bool in
guard let str1 = obj1.compareString, let str2 = obj2.compareString else {
return false
Expand Down
11 changes: 7 additions & 4 deletions iOSEventApp/Model/DataController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ struct MalformedDataInformation: CustomStringConvertible {

DataController also starts the refresh timer in the refreshController, which it keeps a static reference to. The refresh timer triggers a reload of notifications.

Passing a context around different threads is against recommended practice. It will remain as is until changed.
The context should not be carelessly accessed from a thread different from the one it was created on. The only time a context is potentially passed between threads in this class is when a data task is performed - and it may be improperly handled in `loadNotificationsFromURL` or methods that call it, but since there are no clear issues right now it is being left alone.
*/
class DataController: NSObject {

Expand DownExpand Up@@ -150,6 +150,8 @@ class DataController: NSObject {
return
}

// This is in a performBackgroundTask block.
// I still don't think the context should *really* be passed like this, but there are no apparent issues with this at the moment.
self.loadNotificationsFromURL(context: context, url: URL(string: notificationsURLString)!) { (success, nErrors, newNotifications) in
if let additionalErrors = nErrors {
if case .unableToSave(_)? = additionalErrors.first {
Expand DownExpand Up@@ -391,10 +393,11 @@ extension DataController {
var sidebarKVPairs = [String: String]()
let groupDicts = partnerGroups as! [[String: Any]]

for obj in groupDicts {
// The prayer partners come in an array, and so the order should be preserved.
for (index, obj) in groupDicts.enumerated() {
if let partnerNames = obj["students"] {
if partnerNames is String {
groupstoCreate.append(["students": partnerNames])
groupstoCreate.append(["students": partnerNames, "order": Int16(index)])
}
else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "PrayerPartnerGroup", propertyName: "students", missingProperty: nil)))
Expand DownExpand Up@@ -638,7 +641,7 @@ extension DataController {
sidebarNum += 1

let pageIdentifier = key
// Ideally this wouldn't be hardcoded for an index of 0
// The event data has the sidebar info at the first index of each page array
guard let sidebarName = value[0][sidebarNameKey] as? String else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "Information Page \(pageNum)", propertyName: pageIdentifier, missingProperty: sidebarNameKey)))
continue
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>iOSEventAppV2.xcdatamodel</string>
<string>iOSEventAppV3.xcdatamodel</string>
</dict>
</plist>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Contact" representedClassName="Contact" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="phone" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ContactPageSection" representedClassName="ContactPageSection" syncable="YES" codeGenerationType="class">
<attribute name="content" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="header" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="id" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="key" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="General" representedClassName="General" syncable="YES" codeGenerationType="class">
<attribute name="custom_time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="event_name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="logo" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
<attribute name="notifications_url" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh_expire" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh_rate" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="remote_viewing" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="version_num" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="welcome_message" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="year" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="HousingUnit" representedClassName="HousingUnit" syncable="YES" codeGenerationType="class">
<attribute name="driver" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="hostName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="InformationPage" representedClassName="InformationPage" syncable="YES" codeGenerationType="class">
<attribute name="pageName" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoNav" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SidebarAppearance" inverseName="infoPage" inverseEntity="SidebarAppearance" syncable="YES"/>
<relationship name="infoSections" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="InformationPageSection" inverseName="infoPage" inverseEntity="InformationPageSection" syncable="YES"/>
</entity>
<entity name="InformationPageSection" representedClassName="InformationPageSection" syncable="YES" codeGenerationType="class">
<attribute name="information" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoSections" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Notification" representedClassName="Notification" syncable="YES" codeGenerationType="class">
<attribute name="body" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="notificationNumber" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="PrayerPartnerGroup" representedClassName="PrayerPartnerGroup" syncable="YES" codeGenerationType="class">
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ScheduleDay" representedClassName="ScheduleDay" syncable="YES" codeGenerationType="class">
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ScheduleItem" inverseName="day" inverseEntity="ScheduleItem" syncable="YES"/>
</entity>
<entity name="ScheduleItem" representedClassName="ScheduleItem" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="itemDescription" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="length" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="location" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="startTime" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="day" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ScheduleDay" inverseName="items" inverseEntity="ScheduleDay" syncable="YES"/>
</entity>
<entity name="SidebarAppearance" representedClassName="SidebarAppearance" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="icon" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="nav" attributeType="String" syncable="YES"/>
<attribute name="optionalIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoNav" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Theme" representedClassName="Theme" syncable="YES" codeGenerationType="class">
<attribute name="themeName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="themeValue" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<elements>
<element name="Contact" positionX="27" positionY="126" width="128" height="90"/>
<element name="ContactPageSection" positionX="-9" positionY="90" width="128" height="105"/>
<element name="General" positionX="18" positionY="63" width="128" height="225"/>
<element name="HousingUnit" positionX="18" positionY="63" width="128" height="90"/>
<element name="InformationPage" positionX="18" positionY="126" width="128" height="90"/>
<element name="InformationPageSection" positionX="27" positionY="135" width="128" height="105"/>
<element name="Notification" positionX="9" positionY="126" width="128" height="120"/>
<element name="PrayerPartnerGroup" positionX="0" positionY="45" width="128" height="75"/>
<element name="ScheduleDay" positionX="-9" positionY="81" width="128" height="75"/>
<element name="ScheduleItem" positionX="0" positionY="90" width="128" height="135"/>
<element name="SidebarAppearance" positionX="9" positionY="54" width="128" height="135"/>
<element name="Theme" positionX="36" positionY="135" width="128" height="75"/>
</elements>
</model>
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import UIKit

extension PrayerPartnerGroup: IsComparable {
var compareString: String? {
return students
return String(order)
}
}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,7 @@ As the app is still in development and there are a lot of small items on this li
### Important

- [ ] Update as needed to ensure compatibility with the new web app
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings) **--Claimed**
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings). @nathanielbscout took a look at this but couldn't reliably reproduce it - it is not fixed.
- [ ] Thoroughly test error display in different (re)load scenarios
- [ ] Ask server if there are new notifications past a certain date / fetch new notifications after a certain date (would likely require server changes)
- [ ] About screen. It is hooked up but has no meaningful content.
Expand DownExpand Up@@ -75,8 +75,8 @@ As the app is still in development and there are a lot of small items on this li
- [ ] Create an app widget (to give directions without being in the app; be cognizant of different map apps)
- [ ] Alternate event app text (in place of logo when it is missing, like in the android app)
- [ ] Swipe from edge of screen to open menu
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads) **--Claimed**
- [] Make sure prayer partner groups have the same numbers as in the android app **--Claimed**
- [] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that. **--Claimed**
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads). @nathanielbscout took a look at this, but found that it only happens in a couple of places. It may cause some trouble, but there are not any clear issues with it.
- [x] Make sure prayer partner groups have the same numbers as in the android app.
- [x] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that.
- [ ] Start refresh timer when opening the app to go off when it would have gone off, if the timer did not fire when the app was opened.
- [ ] Fix white bar above sidebar menu (very noticeable in app switcher)
4 changes: 3 additions & 1 deletion iOSEventApp.xcodeproj/project.pbxproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@
F1AF66712053292300B8820D /* ContactTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTableViewCell.swift; sourceTree = "<group>"; };
F1AF66732053433B00B8820D /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = "<group>"; };
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV2.xcdatamodel; sourceTree = "<group>"; };
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV3.xcdatamodel; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand DownExpand Up@@ -718,10 +719,11 @@
F149D6CE204DCE2F000DA81A /* iOSEventApp.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */,
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */,
F149D6CF204DCE2F000DA81A /* iOSEventApp.xcdatamodel */,
);
currentVersion = F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */;
currentVersion = F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */;
path = iOSEventApp.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
Expand Down
9 changes: 7 additions & 2 deletions iOSEventApp/MainContainerViewController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,11 @@ protocol TakesArrayData: AnyObject {
var dataArray: [Any]? { get set }
}

/// A workaround to allow sorting of data without having to do a separate if else for every data type.
/**
A workaround to allow sorting of data without having to do a separate if else for every data type.

It is important to sort for consistency across devices - although sometimes the order needs to be kept the same as in the JSON (e.g. prayer partners), which can be handled by passing the indices as strings.
*/
protocol IsComparable {
var compareString: String? { get }
}
Expand DownExpand Up@@ -168,7 +172,8 @@ class MainContainerViewController: UIViewController {
}

if let takesArrayData = vc as? TakesArrayData {
if let sortable = data as? [IsComparable] { // All objects in the array are the same type
// Sort the data if possible - it is simpler to sort here and not in each individual sidebar view controller.
if let sortable = data as? [IsComparable] {
takesArrayData.dataArray = sortable.sorted(by: { (obj1, obj2) -> Bool in
guard let str1 = obj1.compareString, let str2 = obj2.compareString else {
return false
Expand Down
11 changes: 7 additions & 4 deletions iOSEventApp/Model/DataController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ struct MalformedDataInformation: CustomStringConvertible {

DataController also starts the refresh timer in the refreshController, which it keeps a static reference to. The refresh timer triggers a reload of notifications.

Passing a context around different threads is against recommended practice. It will remain as is until changed.
The context should not be carelessly accessed from a thread different from the one it was created on. The only time a context is potentially passed between threads in this class is when a data task is performed - and it may be improperly handled in `loadNotificationsFromURL` or methods that call it, but since there are no clear issues right now it is being left alone.
*/
class DataController: NSObject {

Expand DownExpand Up@@ -150,6 +150,8 @@ class DataController: NSObject {
return
}

// This is in a performBackgroundTask block.
// I still don't think the context should *really* be passed like this, but there are no apparent issues with this at the moment.
self.loadNotificationsFromURL(context: context, url: URL(string: notificationsURLString)!) { (success, nErrors, newNotifications) in
if let additionalErrors = nErrors {
if case .unableToSave(_)? = additionalErrors.first {
Expand DownExpand Up@@ -391,10 +393,11 @@ extension DataController {
var sidebarKVPairs = [String: String]()
let groupDicts = partnerGroups as! [[String: Any]]

for obj in groupDicts {
// The prayer partners come in an array, and so the order should be preserved.
for (index, obj) in groupDicts.enumerated() {
if let partnerNames = obj["students"] {
if partnerNames is String {
groupstoCreate.append(["students": partnerNames])
groupstoCreate.append(["students": partnerNames, "order": Int16(index)])
}
else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "PrayerPartnerGroup", propertyName: "students", missingProperty: nil)))
Expand DownExpand Up@@ -638,7 +641,7 @@ extension DataController {
sidebarNum += 1

let pageIdentifier = key
// Ideally this wouldn't be hardcoded for an index of 0
// The event data has the sidebar info at the first index of each page array
guard let sidebarName = value[0][sidebarNameKey] as? String else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "Information Page \(pageNum)", propertyName: pageIdentifier, missingProperty: sidebarNameKey)))
continue
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>iOSEventAppV2.xcdatamodel</string>
<string>iOSEventAppV3.xcdatamodel</string>
</dict>
</plist>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Contact" representedClassName="Contact" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="phone" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ContactPageSection" representedClassName="ContactPageSection" syncable="YES" codeGenerationType="class">
<attribute name="content" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="header" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="id" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="key" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="General" representedClassName="General" syncable="YES" codeGenerationType="class">
<attribute name="custom_time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="event_name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="logo" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
<attribute name="notifications_url" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh_expire" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh_rate" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="remote_viewing" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="version_num" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="welcome_message" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="year" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="HousingUnit" representedClassName="HousingUnit" syncable="YES" codeGenerationType="class">
<attribute name="driver" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="hostName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="InformationPage" representedClassName="InformationPage" syncable="YES" codeGenerationType="class">
<attribute name="pageName" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoNav" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SidebarAppearance" inverseName="infoPage" inverseEntity="SidebarAppearance" syncable="YES"/>
<relationship name="infoSections" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="InformationPageSection" inverseName="infoPage" inverseEntity="InformationPageSection" syncable="YES"/>
</entity>
<entity name="InformationPageSection" representedClassName="InformationPageSection" syncable="YES" codeGenerationType="class">
<attribute name="information" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoSections" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Notification" representedClassName="Notification" syncable="YES" codeGenerationType="class">
<attribute name="body" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="notificationNumber" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="PrayerPartnerGroup" representedClassName="PrayerPartnerGroup" syncable="YES" codeGenerationType="class">
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ScheduleDay" representedClassName="ScheduleDay" syncable="YES" codeGenerationType="class">
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ScheduleItem" inverseName="day" inverseEntity="ScheduleItem" syncable="YES"/>
</entity>
<entity name="ScheduleItem" representedClassName="ScheduleItem" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="itemDescription" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="length" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="location" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="startTime" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="day" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ScheduleDay" inverseName="items" inverseEntity="ScheduleDay" syncable="YES"/>
</entity>
<entity name="SidebarAppearance" representedClassName="SidebarAppearance" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="icon" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="nav" attributeType="String" syncable="YES"/>
<attribute name="optionalIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoNav" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Theme" representedClassName="Theme" syncable="YES" codeGenerationType="class">
<attribute name="themeName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="themeValue" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<elements>
<element name="Contact" positionX="27" positionY="126" width="128" height="90"/>
<element name="ContactPageSection" positionX="-9" positionY="90" width="128" height="105"/>
<element name="General" positionX="18" positionY="63" width="128" height="225"/>
<element name="HousingUnit" positionX="18" positionY="63" width="128" height="90"/>
<element name="InformationPage" positionX="18" positionY="126" width="128" height="90"/>
<element name="InformationPageSection" positionX="27" positionY="135" width="128" height="105"/>
<element name="Notification" positionX="9" positionY="126" width="128" height="120"/>
<element name="PrayerPartnerGroup" positionX="0" positionY="45" width="128" height="75"/>
<element name="ScheduleDay" positionX="-9" positionY="81" width="128" height="75"/>
<element name="ScheduleItem" positionX="0" positionY="90" width="128" height="135"/>
<element name="SidebarAppearance" positionX="9" positionY="54" width="128" height="135"/>
<element name="Theme" positionX="36" positionY="135" width="128" height="75"/>
</elements>
</model>
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import UIKit

extension PrayerPartnerGroup: IsComparable {
var compareString: String? {
return students
return String(order)
}
}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,7 @@ As the app is still in development and there are a lot of small items on this li
### Important

- [ ] Update as needed to ensure compatibility with the new web app
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings) **--Claimed**
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings). @nathanielbscout took a look at this but couldn't reliably reproduce it - it is not fixed.
- [ ] Thoroughly test error display in different (re)load scenarios
- [ ] Ask server if there are new notifications past a certain date / fetch new notifications after a certain date (would likely require server changes)
- [ ] About screen. It is hooked up but has no meaningful content.
Expand DownExpand Up@@ -75,8 +75,8 @@ As the app is still in development and there are a lot of small items on this li
- [ ] Create an app widget (to give directions without being in the app; be cognizant of different map apps)
- [ ] Alternate event app text (in place of logo when it is missing, like in the android app)
- [ ] Swipe from edge of screen to open menu
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads) **--Claimed**
- [] Make sure prayer partner groups have the same numbers as in the android app **--Claimed**
- [] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that. **--Claimed**
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads). @nathanielbscout took a look at this, but found that it only happens in a couple of places. It may cause some trouble, but there are not any clear issues with it.
- [x] Make sure prayer partner groups have the same numbers as in the android app.
- [x] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that.
- [ ] Start refresh timer when opening the app to go off when it would have gone off, if the timer did not fire when the app was opened.
- [ ] Fix white bar above sidebar menu (very noticeable in app switcher)
4 changes: 3 additions & 1 deletion iOSEventApp.xcodeproj/project.pbxproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@
F1AF66712053292300B8820D /* ContactTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTableViewCell.swift; sourceTree = "<group>"; };
F1AF66732053433B00B8820D /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = "<group>"; };
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV2.xcdatamodel; sourceTree = "<group>"; };
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV3.xcdatamodel; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand DownExpand Up@@ -718,10 +719,11 @@
F149D6CE204DCE2F000DA81A /* iOSEventApp.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */,
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */,
F149D6CF204DCE2F000DA81A /* iOSEventApp.xcdatamodel */,
);
currentVersion = F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */;
currentVersion = F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */;
path = iOSEventApp.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
Expand Down
9 changes: 7 additions & 2 deletions iOSEventApp/MainContainerViewController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,11 @@ protocol TakesArrayData: AnyObject {
var dataArray: [Any]? { get set }
}

/// A workaround to allow sorting of data without having to do a separate if else for every data type.
/**
A workaround to allow sorting of data without having to do a separate if else for every data type.

It is important to sort for consistency across devices - although sometimes the order needs to be kept the same as in the JSON (e.g. prayer partners), which can be handled by passing the indices as strings.
*/
protocol IsComparable {
var compareString: String? { get }
}
Expand DownExpand Up@@ -168,7 +172,8 @@ class MainContainerViewController: UIViewController {
}

if let takesArrayData = vc as? TakesArrayData {
if let sortable = data as? [IsComparable] { // All objects in the array are the same type
// Sort the data if possible - it is simpler to sort here and not in each individual sidebar view controller.
if let sortable = data as? [IsComparable] {
takesArrayData.dataArray = sortable.sorted(by: { (obj1, obj2) -> Bool in
guard let str1 = obj1.compareString, let str2 = obj2.compareString else {
return false
Expand Down
11 changes: 7 additions & 4 deletions iOSEventApp/Model/DataController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ struct MalformedDataInformation: CustomStringConvertible {

DataController also starts the refresh timer in the refreshController, which it keeps a static reference to. The refresh timer triggers a reload of notifications.

Passing a context around different threads is against recommended practice. It will remain as is until changed.
The context should not be carelessly accessed from a thread different from the one it was created on. The only time a context is potentially passed between threads in this class is when a data task is performed - and it may be improperly handled in `loadNotificationsFromURL` or methods that call it, but since there are no clear issues right now it is being left alone.
*/
class DataController: NSObject {

Expand DownExpand Up@@ -150,6 +150,8 @@ class DataController: NSObject {
return
}

// This is in a performBackgroundTask block.
// I still don't think the context should *really* be passed like this, but there are no apparent issues with this at the moment.
self.loadNotificationsFromURL(context: context, url: URL(string: notificationsURLString)!) { (success, nErrors, newNotifications) in
if let additionalErrors = nErrors {
if case .unableToSave(_)? = additionalErrors.first {
Expand DownExpand Up@@ -391,10 +393,11 @@ extension DataController {
var sidebarKVPairs = [String: String]()
let groupDicts = partnerGroups as! [[String: Any]]

for obj in groupDicts {
// The prayer partners come in an array, and so the order should be preserved.
for (index, obj) in groupDicts.enumerated() {
if let partnerNames = obj["students"] {
if partnerNames is String {
groupstoCreate.append(["students": partnerNames])
groupstoCreate.append(["students": partnerNames, "order": Int16(index)])
}
else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "PrayerPartnerGroup", propertyName: "students", missingProperty: nil)))
Expand DownExpand Up@@ -638,7 +641,7 @@ extension DataController {
sidebarNum += 1

let pageIdentifier = key
// Ideally this wouldn't be hardcoded for an index of 0
// The event data has the sidebar info at the first index of each page array
guard let sidebarName = value[0][sidebarNameKey] as? String else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "Information Page \(pageNum)", propertyName: pageIdentifier, missingProperty: sidebarNameKey)))
continue
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>iOSEventAppV2.xcdatamodel</string>
<string>iOSEventAppV3.xcdatamodel</string>
</dict>
</plist>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Contact" representedClassName="Contact" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="phone" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ContactPageSection" representedClassName="ContactPageSection" syncable="YES" codeGenerationType="class">
<attribute name="content" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="header" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="id" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="key" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="General" representedClassName="General" syncable="YES" codeGenerationType="class">
<attribute name="custom_time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="event_name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="logo" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
<attribute name="notifications_url" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh_expire" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh_rate" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="remote_viewing" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="version_num" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="welcome_message" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="year" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="HousingUnit" representedClassName="HousingUnit" syncable="YES" codeGenerationType="class">
<attribute name="driver" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="hostName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="InformationPage" representedClassName="InformationPage" syncable="YES" codeGenerationType="class">
<attribute name="pageName" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoNav" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SidebarAppearance" inverseName="infoPage" inverseEntity="SidebarAppearance" syncable="YES"/>
<relationship name="infoSections" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="InformationPageSection" inverseName="infoPage" inverseEntity="InformationPageSection" syncable="YES"/>
</entity>
<entity name="InformationPageSection" representedClassName="InformationPageSection" syncable="YES" codeGenerationType="class">
<attribute name="information" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoSections" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Notification" representedClassName="Notification" syncable="YES" codeGenerationType="class">
<attribute name="body" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="notificationNumber" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="PrayerPartnerGroup" representedClassName="PrayerPartnerGroup" syncable="YES" codeGenerationType="class">
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ScheduleDay" representedClassName="ScheduleDay" syncable="YES" codeGenerationType="class">
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ScheduleItem" inverseName="day" inverseEntity="ScheduleItem" syncable="YES"/>
</entity>
<entity name="ScheduleItem" representedClassName="ScheduleItem" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="itemDescription" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="length" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="location" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="startTime" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="day" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ScheduleDay" inverseName="items" inverseEntity="ScheduleDay" syncable="YES"/>
</entity>
<entity name="SidebarAppearance" representedClassName="SidebarAppearance" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="icon" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="nav" attributeType="String" syncable="YES"/>
<attribute name="optionalIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoNav" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Theme" representedClassName="Theme" syncable="YES" codeGenerationType="class">
<attribute name="themeName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="themeValue" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<elements>
<element name="Contact" positionX="27" positionY="126" width="128" height="90"/>
<element name="ContactPageSection" positionX="-9" positionY="90" width="128" height="105"/>
<element name="General" positionX="18" positionY="63" width="128" height="225"/>
<element name="HousingUnit" positionX="18" positionY="63" width="128" height="90"/>
<element name="InformationPage" positionX="18" positionY="126" width="128" height="90"/>
<element name="InformationPageSection" positionX="27" positionY="135" width="128" height="105"/>
<element name="Notification" positionX="9" positionY="126" width="128" height="120"/>
<element name="PrayerPartnerGroup" positionX="0" positionY="45" width="128" height="75"/>
<element name="ScheduleDay" positionX="-9" positionY="81" width="128" height="75"/>
<element name="ScheduleItem" positionX="0" positionY="90" width="128" height="135"/>
<element name="SidebarAppearance" positionX="9" positionY="54" width="128" height="135"/>
<element name="Theme" positionX="36" positionY="135" width="128" height="75"/>
</elements>
</model>
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import UIKit

extension PrayerPartnerGroup: IsComparable {
var compareString: String? {
return students
return String(order)
}
}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,7 @@ As the app is still in development and there are a lot of small items on this li
### Important

- [ ] Update as needed to ensure compatibility with the new web app
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings) **--Claimed**
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings). @nathanielbscout took a look at this but couldn't reliably reproduce it - it is not fixed.
- [ ] Thoroughly test error display in different (re)load scenarios
- [ ] Ask server if there are new notifications past a certain date / fetch new notifications after a certain date (would likely require server changes)
- [ ] About screen. It is hooked up but has no meaningful content.
Expand DownExpand Up@@ -75,8 +75,8 @@ As the app is still in development and there are a lot of small items on this li
- [ ] Create an app widget (to give directions without being in the app; be cognizant of different map apps)
- [ ] Alternate event app text (in place of logo when it is missing, like in the android app)
- [ ] Swipe from edge of screen to open menu
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads) **--Claimed**
- [] Make sure prayer partner groups have the same numbers as in the android app **--Claimed**
- [] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that. **--Claimed**
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads). @nathanielbscout took a look at this, but found that it only happens in a couple of places. It may cause some trouble, but there are not any clear issues with it.
- [x] Make sure prayer partner groups have the same numbers as in the android app.
- [x] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that.
- [ ] Start refresh timer when opening the app to go off when it would have gone off, if the timer did not fire when the app was opened.
- [ ] Fix white bar above sidebar menu (very noticeable in app switcher)
4 changes: 3 additions & 1 deletion iOSEventApp.xcodeproj/project.pbxproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@
F1AF66712053292300B8820D /* ContactTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTableViewCell.swift; sourceTree = "<group>"; };
F1AF66732053433B00B8820D /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = "<group>"; };
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV2.xcdatamodel; sourceTree = "<group>"; };
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV3.xcdatamodel; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand DownExpand Up@@ -718,10 +719,11 @@
F149D6CE204DCE2F000DA81A /* iOSEventApp.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */,
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */,
F149D6CF204DCE2F000DA81A /* iOSEventApp.xcdatamodel */,
);
currentVersion = F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */;
currentVersion = F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */;
path = iOSEventApp.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
Expand Down
9 changes: 7 additions & 2 deletions iOSEventApp/MainContainerViewController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,11 @@ protocol TakesArrayData: AnyObject {
var dataArray: [Any]? { get set }
}

/// A workaround to allow sorting of data without having to do a separate if else for every data type.
/**
A workaround to allow sorting of data without having to do a separate if else for every data type.

It is important to sort for consistency across devices - although sometimes the order needs to be kept the same as in the JSON (e.g. prayer partners), which can be handled by passing the indices as strings.
*/
protocol IsComparable {
var compareString: String? { get }
}
Expand DownExpand Up@@ -168,7 +172,8 @@ class MainContainerViewController: UIViewController {
}

if let takesArrayData = vc as? TakesArrayData {
if let sortable = data as? [IsComparable] { // All objects in the array are the same type
// Sort the data if possible - it is simpler to sort here and not in each individual sidebar view controller.
if let sortable = data as? [IsComparable] {
takesArrayData.dataArray = sortable.sorted(by: { (obj1, obj2) -> Bool in
guard let str1 = obj1.compareString, let str2 = obj2.compareString else {
return false
Expand Down
11 changes: 7 additions & 4 deletions iOSEventApp/Model/DataController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ struct MalformedDataInformation: CustomStringConvertible {

DataController also starts the refresh timer in the refreshController, which it keeps a static reference to. The refresh timer triggers a reload of notifications.

Passing a context around different threads is against recommended practice. It will remain as is until changed.
The context should not be carelessly accessed from a thread different from the one it was created on. The only time a context is potentially passed between threads in this class is when a data task is performed - and it may be improperly handled in `loadNotificationsFromURL` or methods that call it, but since there are no clear issues right now it is being left alone.
*/
class DataController: NSObject {

Expand DownExpand Up@@ -150,6 +150,8 @@ class DataController: NSObject {
return
}

// This is in a performBackgroundTask block.
// I still don't think the context should *really* be passed like this, but there are no apparent issues with this at the moment.
self.loadNotificationsFromURL(context: context, url: URL(string: notificationsURLString)!) { (success, nErrors, newNotifications) in
if let additionalErrors = nErrors {
if case .unableToSave(_)? = additionalErrors.first {
Expand DownExpand Up@@ -391,10 +393,11 @@ extension DataController {
var sidebarKVPairs = [String: String]()
let groupDicts = partnerGroups as! [[String: Any]]

for obj in groupDicts {
// The prayer partners come in an array, and so the order should be preserved.
for (index, obj) in groupDicts.enumerated() {
if let partnerNames = obj["students"] {
if partnerNames is String {
groupstoCreate.append(["students": partnerNames])
groupstoCreate.append(["students": partnerNames, "order": Int16(index)])
}
else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "PrayerPartnerGroup", propertyName: "students", missingProperty: nil)))
Expand DownExpand Up@@ -638,7 +641,7 @@ extension DataController {
sidebarNum += 1

let pageIdentifier = key
// Ideally this wouldn't be hardcoded for an index of 0
// The event data has the sidebar info at the first index of each page array
guard let sidebarName = value[0][sidebarNameKey] as? String else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "Information Page \(pageNum)", propertyName: pageIdentifier, missingProperty: sidebarNameKey)))
continue
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>iOSEventAppV2.xcdatamodel</string>
<string>iOSEventAppV3.xcdatamodel</string>
</dict>
</plist>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Contact" representedClassName="Contact" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="phone" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ContactPageSection" representedClassName="ContactPageSection" syncable="YES" codeGenerationType="class">
<attribute name="content" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="header" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="id" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="key" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="General" representedClassName="General" syncable="YES" codeGenerationType="class">
<attribute name="custom_time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="event_name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="logo" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
<attribute name="notifications_url" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh_expire" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh_rate" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="remote_viewing" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="version_num" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="welcome_message" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="year" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="HousingUnit" representedClassName="HousingUnit" syncable="YES" codeGenerationType="class">
<attribute name="driver" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="hostName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="InformationPage" representedClassName="InformationPage" syncable="YES" codeGenerationType="class">
<attribute name="pageName" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoNav" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SidebarAppearance" inverseName="infoPage" inverseEntity="SidebarAppearance" syncable="YES"/>
<relationship name="infoSections" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="InformationPageSection" inverseName="infoPage" inverseEntity="InformationPageSection" syncable="YES"/>
</entity>
<entity name="InformationPageSection" representedClassName="InformationPageSection" syncable="YES" codeGenerationType="class">
<attribute name="information" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoSections" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Notification" representedClassName="Notification" syncable="YES" codeGenerationType="class">
<attribute name="body" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="notificationNumber" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="PrayerPartnerGroup" representedClassName="PrayerPartnerGroup" syncable="YES" codeGenerationType="class">
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ScheduleDay" representedClassName="ScheduleDay" syncable="YES" codeGenerationType="class">
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ScheduleItem" inverseName="day" inverseEntity="ScheduleItem" syncable="YES"/>
</entity>
<entity name="ScheduleItem" representedClassName="ScheduleItem" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="itemDescription" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="length" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="location" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="startTime" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="day" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ScheduleDay" inverseName="items" inverseEntity="ScheduleDay" syncable="YES"/>
</entity>
<entity name="SidebarAppearance" representedClassName="SidebarAppearance" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="icon" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="nav" attributeType="String" syncable="YES"/>
<attribute name="optionalIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoNav" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Theme" representedClassName="Theme" syncable="YES" codeGenerationType="class">
<attribute name="themeName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="themeValue" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<elements>
<element name="Contact" positionX="27" positionY="126" width="128" height="90"/>
<element name="ContactPageSection" positionX="-9" positionY="90" width="128" height="105"/>
<element name="General" positionX="18" positionY="63" width="128" height="225"/>
<element name="HousingUnit" positionX="18" positionY="63" width="128" height="90"/>
<element name="InformationPage" positionX="18" positionY="126" width="128" height="90"/>
<element name="InformationPageSection" positionX="27" positionY="135" width="128" height="105"/>
<element name="Notification" positionX="9" positionY="126" width="128" height="120"/>
<element name="PrayerPartnerGroup" positionX="0" positionY="45" width="128" height="75"/>
<element name="ScheduleDay" positionX="-9" positionY="81" width="128" height="75"/>
<element name="ScheduleItem" positionX="0" positionY="90" width="128" height="135"/>
<element name="SidebarAppearance" positionX="9" positionY="54" width="128" height="135"/>
<element name="Theme" positionX="36" positionY="135" width="128" height="75"/>
</elements>
</model>
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import UIKit

extension PrayerPartnerGroup: IsComparable {
var compareString: String? {
return students
return String(order)
}
}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,7 @@ As the app is still in development and there are a lot of small items on this li
### Important

- [ ] Update as needed to ensure compatibility with the new web app
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings) **--Claimed**
- [ ] Fix the core data crash (results occasionally from the "Refresh event data now" button in settings). @nathanielbscout took a look at this but couldn't reliably reproduce it - it is not fixed.
- [ ] Thoroughly test error display in different (re)load scenarios
- [ ] Ask server if there are new notifications past a certain date / fetch new notifications after a certain date (would likely require server changes)
- [ ] About screen. It is hooked up but has no meaningful content.
Expand DownExpand Up@@ -75,8 +75,8 @@ As the app is still in development and there are a lot of small items on this li
- [ ] Create an app widget (to give directions without being in the app; be cognizant of different map apps)
- [ ] Alternate event app text (in place of logo when it is missing, like in the android app)
- [ ] Swipe from edge of screen to open menu
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads) **--Claimed**
- [] Make sure prayer partner groups have the same numbers as in the android app **--Claimed**
- [] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that. **--Claimed**
- [ ] Improve managed object context usage (the data controller appears to violate Apple documentation's concurrency section by passing contexts between threads). @nathanielbscout took a look at this, but found that it only happens in a couple of places. It may cause some trouble, but there are not any clear issues with it.
- [x] Make sure prayer partner groups have the same numbers as in the android app.
- [x] Add some kind of order key to objects received in an array (not a dictionary) and update sorting to use that.
- [ ] Start refresh timer when opening the app to go off when it would have gone off, if the timer did not fire when the app was opened.
- [ ] Fix white bar above sidebar menu (very noticeable in app switcher)
4 changes: 3 additions & 1 deletion iOSEventApp.xcodeproj/project.pbxproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@
F1AF66712053292300B8820D /* ContactTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTableViewCell.swift; sourceTree = "<group>"; };
F1AF66732053433B00B8820D /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = "<group>"; };
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV2.xcdatamodel; sourceTree = "<group>"; };
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iOSEventAppV3.xcdatamodel; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand DownExpand Up@@ -718,10 +719,11 @@
F149D6CE204DCE2F000DA81A /* iOSEventApp.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */,
F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */,
F149D6CF204DCE2F000DA81A /* iOSEventApp.xcdatamodel */,
);
currentVersion = F1DB720A20A6655C00B30C54 /* iOSEventAppV2.xcdatamodel */;
currentVersion = F1EE2DC422453D88006E9A1A /* iOSEventAppV3.xcdatamodel */;
path = iOSEventApp.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
Expand Down
9 changes: 7 additions & 2 deletions iOSEventApp/MainContainerViewController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,11 @@ protocol TakesArrayData: AnyObject {
var dataArray: [Any]? { get set }
}

/// A workaround to allow sorting of data without having to do a separate if else for every data type.
/**
A workaround to allow sorting of data without having to do a separate if else for every data type.

It is important to sort for consistency across devices - although sometimes the order needs to be kept the same as in the JSON (e.g. prayer partners), which can be handled by passing the indices as strings.
*/
protocol IsComparable {
var compareString: String? { get }
}
Expand DownExpand Up@@ -168,7 +172,8 @@ class MainContainerViewController: UIViewController {
}

if let takesArrayData = vc as? TakesArrayData {
if let sortable = data as? [IsComparable] { // All objects in the array are the same type
// Sort the data if possible - it is simpler to sort here and not in each individual sidebar view controller.
if let sortable = data as? [IsComparable] {
takesArrayData.dataArray = sortable.sorted(by: { (obj1, obj2) -> Bool in
guard let str1 = obj1.compareString, let str2 = obj2.compareString else {
return false
Expand Down
11 changes: 7 additions & 4 deletions iOSEventApp/Model/DataController.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ struct MalformedDataInformation: CustomStringConvertible {

DataController also starts the refresh timer in the refreshController, which it keeps a static reference to. The refresh timer triggers a reload of notifications.

Passing a context around different threads is against recommended practice. It will remain as is until changed.
The context should not be carelessly accessed from a thread different from the one it was created on. The only time a context is potentially passed between threads in this class is when a data task is performed - and it may be improperly handled in `loadNotificationsFromURL` or methods that call it, but since there are no clear issues right now it is being left alone.
*/
class DataController: NSObject {

Expand DownExpand Up@@ -150,6 +150,8 @@ class DataController: NSObject {
return
}

// This is in a performBackgroundTask block.
// I still don't think the context should *really* be passed like this, but there are no apparent issues with this at the moment.
self.loadNotificationsFromURL(context: context, url: URL(string: notificationsURLString)!) { (success, nErrors, newNotifications) in
if let additionalErrors = nErrors {
if case .unableToSave(_)? = additionalErrors.first {
Expand DownExpand Up@@ -391,10 +393,11 @@ extension DataController {
var sidebarKVPairs = [String: String]()
let groupDicts = partnerGroups as! [[String: Any]]

for obj in groupDicts {
// The prayer partners come in an array, and so the order should be preserved.
for (index, obj) in groupDicts.enumerated() {
if let partnerNames = obj["students"] {
if partnerNames is String {
groupstoCreate.append(["students": partnerNames])
groupstoCreate.append(["students": partnerNames, "order": Int16(index)])
}
else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "PrayerPartnerGroup", propertyName: "students", missingProperty: nil)))
Expand DownExpand Up@@ -638,7 +641,7 @@ extension DataController {
sidebarNum += 1

let pageIdentifier = key
// Ideally this wouldn't be hardcoded for an index of 0
// The event data has the sidebar info at the first index of each page array
guard let sidebarName = value[0][sidebarNameKey] as? String else {
errors.append(.partiallyMalformed(MalformedDataInformation(objectName: "Information Page \(pageNum)", propertyName: pageIdentifier, missingProperty: sidebarNameKey)))
continue
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>iOSEventAppV2.xcdatamodel</string>
<string>iOSEventAppV3.xcdatamodel</string>
</dict>
</plist>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Contact" representedClassName="Contact" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="phone" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ContactPageSection" representedClassName="ContactPageSection" syncable="YES" codeGenerationType="class">
<attribute name="content" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="header" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="id" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="key" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="General" representedClassName="General" syncable="YES" codeGenerationType="class">
<attribute name="custom_time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="event_name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="logo" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
<attribute name="notifications_url" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh_expire" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refresh_rate" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="remote_viewing" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="time_zone" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="version_num" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="welcome_message" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="year" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="HousingUnit" representedClassName="HousingUnit" syncable="YES" codeGenerationType="class">
<attribute name="driver" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="hostName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="InformationPage" representedClassName="InformationPage" syncable="YES" codeGenerationType="class">
<attribute name="pageName" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoNav" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SidebarAppearance" inverseName="infoPage" inverseEntity="SidebarAppearance" syncable="YES"/>
<relationship name="infoSections" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="InformationPageSection" inverseName="infoPage" inverseEntity="InformationPageSection" syncable="YES"/>
</entity>
<entity name="InformationPageSection" representedClassName="InformationPageSection" syncable="YES" codeGenerationType="class">
<attribute name="information" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoSections" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Notification" representedClassName="Notification" syncable="YES" codeGenerationType="class">
<attribute name="body" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="notificationNumber" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="PrayerPartnerGroup" representedClassName="PrayerPartnerGroup" syncable="YES" codeGenerationType="class">
<attribute name="order" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="students" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<entity name="ScheduleDay" representedClassName="ScheduleDay" syncable="YES" codeGenerationType="class">
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ScheduleItem" inverseName="day" inverseEntity="ScheduleItem" syncable="YES"/>
</entity>
<entity name="ScheduleItem" representedClassName="ScheduleItem" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="itemDescription" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="length" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="location" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="startTime" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="day" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ScheduleDay" inverseName="items" inverseEntity="ScheduleDay" syncable="YES"/>
</entity>
<entity name="SidebarAppearance" representedClassName="SidebarAppearance" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="icon" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="nav" attributeType="String" syncable="YES"/>
<attribute name="optionalIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="order" attributeType="String" syncable="YES"/>
<relationship name="infoPage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InformationPage" inverseName="infoNav" inverseEntity="InformationPage" syncable="YES"/>
</entity>
<entity name="Theme" representedClassName="Theme" syncable="YES" codeGenerationType="class">
<attribute name="themeName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="themeValue" optional="YES" attributeType="String" syncable="YES"/>
</entity>
<elements>
<element name="Contact" positionX="27" positionY="126" width="128" height="90"/>
<element name="ContactPageSection" positionX="-9" positionY="90" width="128" height="105"/>
<element name="General" positionX="18" positionY="63" width="128" height="225"/>
<element name="HousingUnit" positionX="18" positionY="63" width="128" height="90"/>
<element name="InformationPage" positionX="18" positionY="126" width="128" height="90"/>
<element name="InformationPageSection" positionX="27" positionY="135" width="128" height="105"/>
<element name="Notification" positionX="9" positionY="126" width="128" height="120"/>
<element name="PrayerPartnerGroup" positionX="0" positionY="45" width="128" height="75"/>
<element name="ScheduleDay" positionX="-9" positionY="81" width="128" height="75"/>
<element name="ScheduleItem" positionX="0" positionY="90" width="128" height="135"/>
<element name="SidebarAppearance" positionX="9" positionY="54" width="128" height="135"/>
<element name="Theme" positionX="36" positionY="135" width="128" height="75"/>
</elements>
</model>
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import UIKit

extension PrayerPartnerGroup: IsComparable {
var compareString: String? {
return students
return String(order)
}
}

Expand Down