NSUserDefaults not loading for Today Extension (Swift) - swift

I'm trying to make a Today Extension, and in the main app favorite is being correctly written and read from both ar and ar2. favorite is a property of class Book. However, when I try to access the favorite property of an array of all Book objects from the Today Extension, both ar and ar2 are empty.
main app:
var favorite: Bool {
get {
let ar = NSUserDefaults.standardUserDefaults().stringArrayForKey("favorites") ?? []
let ar2 = NSUserDefaults(suiteName: "group.com.TodayExtension")
ar2?.synchronize()
let tempVar = ar2?.stringArrayForKey("favorites") ?? []
print("ar1: \(ar)")
print("tempVar: \(tempVar)")
return ar.contains {
$0 == slug
}
}
set {
var ar = NSUserDefaults.standardUserDefaults().stringArrayForKey("favorites") ?? []
let ar2 = NSUserDefaults(suiteName: "group.com.TodayExtension")
let contains = self.favorite
if (newValue && !contains) {
ar.append(self.slug)
ar2?.setObject(ar, forKey: "favorites")
ar2?.synchronize()
} else if (!newValue && contains) {
let idx = ar.indexOf {
$0 == slug
}
if let idx = idx {
ar.removeAtIndex(idx)
}
}
NSUserDefaults.standardUserDefaults().setObject(ar, forKey: "favorites");
}
}
Output:
ar1: ["Harry Potter", "Compound", "Nefarious"]
tempVar: ["Harry Potter", "Compound", "Nefarious"]
Today Extension:
func loadData(force: Bool, completion:(() -> Void)?) {
DATA.fetchEateries(force) { (error) -> (Void) in
dispatch_async(dispatch_get_main_queue(), {() -> Void in
if let completionBlock = completion {
completionBlock()
}
self.books = self.DATA.books
if self.books != [] {
for book in self.books {
if book.favorite {
print("I like it!")
} else {
print("I don't like \(book.name)")
}
}
}
else {
print("there are no books")
}
})
}
}
Output:
ar1: []
tempVar: []
I don't like Harry Potter
ar1: []
tempVar: []
I don't like Compound
ar1: []
tempVar: []
I don't like Nefarious

standardUserDefaults doesn't return the same user defaults for both target. You must create app group and then use NSUserDefaults(suiteName: "group")

Related

HealthKit keeps updating the sample data on a simulator, but not the actual data on apple watch

I just started learning swift using WWDC open sources. Im learning on how to create watch os workout application. When I run this on the simulator it will keep updating sample data, but on my Apple Watch, when I run this, it doesn't keep updating the live workout data. I am sure that I have to deal with code below but
extension WorkoutManager: HKLiveWorkoutBuilderDelegate {
func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
}
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
for type in collectedTypes {
guard let quantityType = type as? HKQuantityType else {
return // Nothing to do.
}
let statistics = workoutBuilder.statistics(for: quantityType)
// Update the published values.
updateForStatistics(statistics)
}
}
}
I don't know exactly what goes on when invoking HealthKit, I took most of the code from the WWDC example.
import Foundation
import HealthKit
class WorkoutManager: NSObject, ObservableObject {
var selectedWorkout: HKWorkoutActivityType? {
didSet {
guard let selectedWorkout = selectedWorkout else { return }
startWorkout(workoutType: selectedWorkout)
}
}
#Published var showingSummaryView: Bool = false {
didSet {
if showingSummaryView == false {
resetWorkout()
}
}
}
let healthStore = HKHealthStore()
var session: HKWorkoutSession?
var builder: HKLiveWorkoutBuilder?
func startWorkout(workoutType: HKWorkoutActivityType) {
let configuration = HKWorkoutConfiguration()
configuration.activityType = workoutType
configuration.locationType = .outdoor
// Create the session and obtain the workout builder.
do {
session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
builder = session?.associatedWorkoutBuilder()
} catch {
// Handle any exceptions.
return
}
// Set the workout builder's data source.
builder?.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore,
workoutConfiguration: configuration)
session?.delegate = self
builder?.delegate = self
// Start the workout session and begin data collection.
let startDate = Date()
session?.startActivity(with: startDate)
builder?.beginCollection(withStart: startDate) { (success, error) in
// The workout has started.
}
}
func requestAuthorization() {
// The quantity type to write to the health store.
let typesToShare: Set = [
HKQuantityType.workoutType()
]
// The quantity types to read from the health store.
let typesToRead: Set = [
HKQuantityType.quantityType(forIdentifier: .heartRate)!,
HKObjectType.activitySummaryType()
]
// Request authorization for those quantity types.
healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead) { (success, error) in
// Handle error.
}
}
// MARK: - Session State Control
// The app's workout state.
#Published var running = false
func togglePause() {
if running == true {
self.pause()
} else {
resume()
}
}
func pause() {
session?.pause()
}
func resume() {
session?.resume()
}
func endWorkout() {
session?.end()
showingSummaryView = true
}
// MARK: - Workout Metrics
#Published var averageHeartRate: Double = 0
#Published var heartRate: Double = 0
#Published var workout: HKWorkout?
func updateForStatistics(_ statistics: HKStatistics?) {
guard let statistics = statistics else { return }
DispatchQueue.main.async {
switch statistics.quantityType {
case HKQuantityType.quantityType(forIdentifier: .heartRate):
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
self.heartRate = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) ?? 0
self.averageHeartRate = statistics.averageQuantity()?.doubleValue(for: heartRateUnit) ?? 0
default:
return
}
}
}
func resetWorkout() {
selectedWorkout = nil
builder = nil
workout = nil
session = nil
averageHeartRate = 0
heartRate = 0
}
}
extension WorkoutManager: HKWorkoutSessionDelegate {
func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState,
from fromState: HKWorkoutSessionState, date: Date) {
DispatchQueue.main.async {
self.running = toState == .running
}
// Wait for the session to transition states before ending the builder.
if toState == .ended {
builder?.endCollection(withEnd: date) { (success, error) in
self.builder?.finishWorkout { (workout, error) in
DispatchQueue.main.async {
self.workout = workout
}
}
}
}
}
func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
}
}
extension WorkoutManager: HKLiveWorkoutBuilderDelegate {
func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
}
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
for type in collectedTypes {
guard let quantityType = type as? HKQuantityType else {
return // Nothing to do.
}
let statistics = workoutBuilder.statistics(for: quantityType)
// Update the published values.
updateForStatistics(statistics)
}
}
}
Do you have the Workout processing background mode enabled in the Info.plist?

Async Future Promise not working in Swift 5

I'm trying to implement an async call to a function loadUserFromFirebase and populate and return an array based on user attributes that I then use to write to a firebase collection.
I'm trying to use the Combine framework from Swift using future and promises but for some reason, the receiveCompletion and receiveValue don't get called and thus I get an empty array from my async function call.
Here is my code:
var cancellable = Set<AnyCancellable>()
func loadUserFromFirebase(groupUserIds: [String], handler: #escaping ([groupMate]) -> Void) {
var groupmateID = 0
var groupMates : [groupMate] = []
print("groupUserIds: \(groupUserIds)" )
for groupUserUID in groupUserIds {
print("groupUserUID: \(groupUserUID)" )
let future = Future<groupMate, Never> { promise in
self.ref.collection("Users").document(groupUserUID).getDocument(){
(friendDocument, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
// print("friendDocument: \(String(describing: friendDocument?.data()))" )
let groupUsername = (friendDocument?.data()?["username"]) as! String
let groupUID = (friendDocument?.data()?["uid"]) as! String
let groupName = (friendDocument?.data()?["name"]) as! String
let groupPic = (friendDocument?.data()?["imageurl"]) as! String
promise(.success(groupMate(id: groupmateID, uid: groupUID , name: groupName , username: groupUsername, pic: groupPic)))
}
groupmateID += 1
}
}
print("in receiveCompletion")
future.sink(receiveCompletion: { completion in
print("in receiveCompletion")
print(1, completion)
switch completion {
case .failure(let error):
print(3, error)
handler([])
return
case .finished:
break
}
},
receiveValue: {
print("in receiveValue")
groupMates.append($0)
print(groupMates)
handler(groupMates)
}).store(in: &cancellable)
}
}
func creategroup(groupName: String){
addedTogroupUsers.append(self.uid)
print("here111")
loadUserFromFirebase(groupUserIds: addedTogroupUsers) { groupMates in
print("here222")
let groupData: [String: Any] = [
"groupName": "\(groupName)",
"groupmates": groupMates
]
print("here333 \(groupData)")
print("groupMates are \(self.groupMates)")
var groupref: DocumentReference? = nil
groupref = self.ref.collection("groups").addDocument(data: groupData) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document added with ID: \(groupref!.documentID)")
for addedgroupUser in self.addedTogroupUsers {
self.ref.collection("Users").document(addedgroupUser).updateData([
"groups": FieldValue.arrayUnion([groupref!.documentID])
])
}
}
}
print("groupName is \(groupName) and addedTogroup are \(self.addedTogroupUsers)")
}
}
I'm trying to see if AnyCancellable is the way to go but since I'm using a chained array of future promises, I'm not sure how to implement it. Please let me know how you'd solve this problem so that the array does get populated since the documents do exist and the print inside the method call work but the groupMates array in the createGroup function prints an empty array afterwards. Thanks!
Edit: Added AnyCancallable to Code along with completion handler as suggested
dealing with async functions can be tricky. You are getting an empty array, because you are returning too early, in loadUserFromFirebase. Try this approach (untested) using the old style closure:
func loadUserFromFirebase(groupUserIds: [String], handler: #escaping ([groupMate]) -> Void) { // <-- here
var groupmateID = 0
var groupMates : [String] = []
print("groupUserIds: \(groupUserIds)" )
for groupUserUID in groupUserIds {
print("groupUserUID: \(groupUserUID)" )
let future = Future<groupMate, Never> { promise in
self.ref.collection("Users").document(groupUserUID).getDocument(){ (friendDocument, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
print("friendDocument: \(String(describing: friendDocument?.data()))" )
let groupUsername = (friendDocument?.data()?["username"]) as! String
let groupUID = (friendDocument?.data()?["uid"]) as! String
let groupName = (friendDocument?.data()?["name"]) as! String
let groupPic = (friendDocument?.data()?["imageurl"]) as! String
promise(.success(groupMate(id: groupmateID, uid: groupUID , name: groupName , username: groupUsername, pic: groupPic)))
}
groupmateID += 1
}
}
future.sink(receiveCompletion: { completion in
print("in receiveCompletion")
print(1, completion)
switch completion {
case .failure(let error):
print(3, error)
handler([]) // <-- here
return // <-- here
case .finished:
break
}
},
receiveValue: {
print("in receiveValue")
groupMates.append($0)
print(groupMates)
handler(groupMates) // <-- here
})
}
// <-- do not return here
}
func creategroup(groupName: String) {
addedTogroupUsers.append(self.uid)
// -- here wait until you get the data
loadUserFromFirebase(groupUserIds: addedTogroupUsers) { groupMates in
let groupData: [String: Any] = [
"groupName": "\(groupName)",
"groupmates": groupMates // <-- here
]
print("groupMates are \(self.groupMates)")
var groupref: DocumentReference? = nil
groupref = ref.collection("groups").addDocument(data: groupData) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document added with ID: \(groupref!.documentID)")
for addedgroupUser in self.addedTogroupUsers {
self.ref.collection("Users").document(addedgroupUser).updateData([
"groups": FieldValue.arrayUnion([groupref!.documentID])
])
}
}
}
print("groupName is \(groupName) and addedTogroup are \(addedTogroupUsers)")
}
}
Note, if you are targeting ios 15, macos 12, you are far better served if you use the swift 5.5 async/await/task features. They really work well.
EDIT: trying to return all results
func loadUserFromFirebase(groupUserIds: [String], handler: #escaping ([groupMate]) -> Void) {
var groupmateID = 0
var groupMates : [String] = []
print("groupUserIds: \(groupUserIds)" )
var arr: [Future<groupMate, Never>] = [Future<groupMate, Never>]()
var cancellable = Set<AnyCancellable>()
for groupUserUID in groupUserIds {
print("groupUserUID: \(groupUserUID)" )
let future = Future<groupMate, Never> { promise in
self.ref.collection("Users").document(groupUserUID).getDocument(){ (friendDocument, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
print("friendDocument: \(String(describing: friendDocument?.data()))" )
let groupUsername = (friendDocument?.data()?["username"]) as! String
let groupUID = (friendDocument?.data()?["uid"]) as! String
let groupName = (friendDocument?.data()?["name"]) as! String
let groupPic = (friendDocument?.data()?["imageurl"]) as! String
promise(.success(groupMate(id: groupmateID, uid: groupUID , name: groupName , username: groupUsername, pic: groupPic)))
}
groupmateID += 1
}
}
arr.append(future)
}
Publishers.MergeMany(arr)
.collect()
.sink { _ in
print("-----> merging ")
} receiveValue: { value in
print("-----> value: \(value)")
groupMates = value // <--- maybe append?
print(groupMates)
handler(groupMates)
}
.store(in: &cancellable)
}

Custom Property Wrapper that Updates View Swift

Xcode 11.3, Swift 5.1.3
I am trying currently to create a custom property wrapper that allows me to link variables to a Firebase database. When doing this, to make it update the view, I at first tried to use the #ObservedObject #Bar var foo = []. But I get an error that multiple property wrappers are not supported. Next thing I tried to do, which would honestly be ideal, was try to make my custom property wrapper update the view itself upon being changed, just like #State and #ObservedObject. This both avoids needing to go down two layers to access the underlying values and avoid the use of nesting property wrappers. To do this, I checked the SwiftUI documentation and found out that they both implement the DynamicProperty protocol. I tried to use this too but failed because I need to be able to update the view (call update()) from within my Firebase database observers, which I cannot do since .update() is mutating.
Here is my current attempt at this:
import SwiftUI
import Firebase
import CodableFirebase
import Combine
#propertyWrapper
final class DatabaseBackedArray<Element>: ObservableObject where Element: Codable & Identifiable {
typealias ObserverHandle = UInt
typealias Action = RealtimeDatabase.Action
typealias Event = RealtimeDatabase.Event
private(set) var reference: DatabaseReference
private var currentValue: [Element]
private var childAddedObserverHandle: ObserverHandle?
private var childChangedObserverHandle: ObserverHandle?
private var childRemovedObserverHandle: ObserverHandle?
private var childAddedActions: [Action<[Element]>] = []
private var childChangedActions: [Action<[Element]>] = []
private var childRemovedActions: [Action<[Element]>] = []
init(wrappedValue: [Element], _ path: KeyPath<RealtimeDatabase, RealtimeDatabase>, events: Event = .all,
actions: [Action<[Element]>] = []) {
currentValue = wrappedValue
reference = RealtimeDatabase()[keyPath: path].reference
for action in actions {
if action.event.contains(.childAdded) {
childAddedActions.append(action)
}
if action.event.contains(.childChanged) {
childChangedActions.append(action)
}
if action.event.contains(.childRemoved) {
childRemovedActions.append(action)
}
}
if events.contains(.childAdded) {
childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.append(decodedValue)
self.childAddedActions.forEach { $0.action(&self.currentValue) }
}
}
if events.contains(.childChanged) {
childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
guard let changeIndex = self.currentValue.firstIndex(where: { $0.id == decodedValue.id }) else {
return
}
self.objectWillChange.send()
self.currentValue[changeIndex] = decodedValue
self.childChangedActions.forEach { $0.action(&self.currentValue) }
}
}
if events.contains(.childRemoved) {
childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.removeAll { $0.id == decodedValue.id }
self.childRemovedActions.forEach { $0.action(&self.currentValue) }
}
}
}
private func setValue(to value: [Element]) {
guard let encodedValue = try? FirebaseEncoder().encode(currentValue) else {
fatalError("Could not encode value to Firebase.")
}
reference.setValue(encodedValue)
}
var wrappedValue: [Element] {
get {
return currentValue
}
set {
self.objectWillChange.send()
setValue(to: newValue)
}
}
var projectedValue: Binding<[Element]> {
return Binding(get: {
return self.wrappedValue
}) { newValue in
self.wrappedValue = newValue
}
}
var hasActiveObserver: Bool {
return childAddedObserverHandle != nil || childChangedObserverHandle != nil || childRemovedObserverHandle != nil
}
var hasChildAddedObserver: Bool {
return childAddedObserverHandle != nil
}
var hasChildChangedObserver: Bool {
return childChangedObserverHandle != nil
}
var hasChildRemovedObserver: Bool {
return childRemovedObserverHandle != nil
}
func connectObservers(for event: Event) {
if event.contains(.childAdded) && childAddedObserverHandle == nil {
childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.append(decodedValue)
self.childAddedActions.forEach { $0.action(&self.currentValue) }
}
}
if event.contains(.childChanged) && childChangedObserverHandle == nil {
childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
guard let changeIndex = self.currentValue.firstIndex(where: { $0.id == decodedValue.id }) else {
return
}
self.objectWillChange.send()
self.currentValue[changeIndex] = decodedValue
self.childChangedActions.forEach { $0.action(&self.currentValue) }
}
}
if event.contains(.childRemoved) && childRemovedObserverHandle == nil {
childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.removeAll { $0.id == decodedValue.id }
self.childRemovedActions.forEach { $0.action(&self.currentValue) }
}
}
}
func removeObserver(for event: Event) {
if event.contains(.childAdded), let handle = childAddedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childAddedObserverHandle = nil
}
if event.contains(.childChanged), let handle = childChangedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childChangedObserverHandle = nil
}
if event.contains(.childRemoved), let handle = childRemovedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childRemovedObserverHandle = nil
}
}
func removeAction(_ action: Action<[Element]>) {
if action.event.contains(.childAdded) {
childAddedActions.removeAll { $0.id == action.id }
}
if action.event.contains(.childChanged) {
childChangedActions.removeAll { $0.id == action.id }
}
if action.event.contains(.childRemoved) {
childRemovedActions.removeAll { $0.id == action.id }
}
}
func removeAllActions(for event: Event) {
if event.contains(.childAdded) {
childAddedActions = []
}
if event.contains(.childChanged) {
childChangedActions = []
}
if event.contains(.childRemoved) {
childRemovedActions = []
}
}
}
struct School: Codable, Identifiable {
/// The unique id of the school.
var id: String
/// The name of the school.
var name: String
/// The city of the school.
var city: String
/// The province of the school.
var province: String
/// Email domains for student emails from the school.
var domains: [String]
}
#dynamicMemberLookup
struct RealtimeDatabase {
private var path: [String]
var reference: DatabaseReference {
var ref = Database.database().reference()
for component in path {
ref = ref.child(component)
}
return ref
}
init(previous: Self? = nil, child: String? = nil) {
if let previous = previous {
path = previous.path
} else {
path = []
}
if let child = child {
path.append(child)
}
}
static subscript(dynamicMember member: String) -> Self {
return Self(child: member)
}
subscript(dynamicMember member: String) -> Self {
return Self(child: member)
}
static subscript(dynamicMember keyPath: KeyPath<Self, Self>) -> Self {
return Self()[keyPath: keyPath]
}
static let reference = Database.database().reference()
struct Event: OptionSet, Hashable {
let rawValue: UInt
static let childAdded = Event(rawValue: 1 << 0)
static let childChanged = Event(rawValue: 1 << 1)
static let childRemoved = Event(rawValue: 1 << 2)
static let all: Event = [.childAdded, .childChanged, .childRemoved]
static let constructive: Event = [.childAdded, .childChanged]
static let destructive: Event = .childRemoved
}
struct Action<Value>: Identifiable {
let id = UUID()
let event: Event
let action: (inout Value) -> Void
private init(on event: Event, perform action: #escaping (inout Value) -> Void) {
self.event = event
self.action = action
}
static func on<Value>(_ event: RealtimeDatabase.Event, perform action: #escaping (inout Value) -> Void) -> Action<Value> {
return Action<Value>(on: event, perform: action)
}
}
}
Usage example:
struct ContentView: View {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
var body: some View {
Text("School: ").bold() +
Text(schools.isEmpty ? "Loading..." : schools.first!.name)
}
}
When I try to use this though, the view never updates with the value from Firebase even though I am positive that the .childAdded observer is being called.
One of my attempts at fixing this was to store all of these variables in a singleton that itself conforms to ObservableObject. This solution is also ideal as it allows the variables being observed to be shared throughout my application, preventing multiples instances of the same date and allowing for a single source of truth. Unfortunately, this too did not update the view with the fetched value of currentValue.
class Session: ObservableObject {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
private init() {
//Send `objectWillChange` when `schools` property changes
_schools.objectWillChange.sink {
self.objectWillChange.send()
}
}
static let current = Session()
}
struct ContentView: View {
#ObservedObject
var session = Session.current
var body: some View {
Text("School: ").bold() +
Text(session.schools.isEmpty ? "Loading..." : session.schools.first!.name)
}
}
Is there any way to make a custom property wrapper that also updates a view in SwiftUI?
Making use of the DynamicProperty protocol we can easily trigger view updates by making use of SwiftUI's existing property wrappers. (DynamicProperty tells SwiftUI to look for these within our type)
#propertyWrapper
struct OurPropertyWrapper: DynamicProperty {
// A state object that we notify of updates
#StateObject private var updater = Updater()
var wrappedValue: T {
get {
// Your getter code here
}
nonmutating set {
// Tell SwiftUI we're going to change something
updater.notifyUpdate()
// Your setter code here
}
}
class Updater: ObservableObject {
func notifyUpdate() {
objectWillChange.send()
}
}
}
The solution to this is to make a minor tweak to the solution of the singleton. Credits to #user1046037 for pointing this out to me. The problem with the singleton fix mentioned in the original post, is that it does not retain the canceller for the sink in the initializer. Here is the correct code:
class Session: ObservableObject {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
private var cancellers = [AnyCancellable]()
private init() {
_schools.objectWillChange.sink {
self.objectWillChange.send()
}.assign(to: &cancellers)
}
static let current = Session()
}

Typecasting causing struct values to change (Swift)

After downcasting an array of structs, my Variables View window shows that all of the values in my struct have shifted "down" (will explain in a second). But when I print(structName), the values are fine. However, when I run an equality check on the struct, it once again behaves as though my values have shifted.
For example, I am trying to downcast Model A to ModelProtocol. var m = Model A and has the values {id: "1234", name: "Cal"}. When I downcast, m now has the values { id:"\0\0", name:"1234" }.
Actual Example Below:
Models that I want to downcast:
struct PrivateSchoolModel: Decodable, SchoolProtocol {
var id: String
var name: String
var city: String
var state: String
}
struct PublicSchoolModel: Decodable, SchoolProtocol {
var id: String
var name: String
var city: String
var state: String
var latitude: String
var longitude: String
}
Protocol I want to downcast to:
protocol SchoolProtocol {
var id: String { get set }
var name: String { get set }
var city: String { get set }
var state: String { get set }
var longitude: Float { get set }
var latitude: Float { get set }
}
extension SchoolProtocol {
var longitude: Float {
get { return -1.0 }
set {}
}
var latitude: Float {
get { return -1.0 }
set {}
}
}
Downcasting:
guard let downcastedArr = privateSchoolArray as? [SchoolProtocol] else { return [] }
Result (item at index 0) or originalArr:
id = "1234"
name = "Leo High School"
city = "Bellview"
state = "WA"
Result (item at index 0) of downcastedArr:
id = "\0\0"
name = "1234"
city = "Leo High School"
state = "Bellview"
But if I print(downcastArr[0]), it will show:
id = "1234"
name = "Leo High School"
city = "Bellview"
state = "WA"
But if I try originalArray[0].id == downcastArr[0].id, it returns false
My Code with the problem:
class SchoolJSONHandler {
private enum JSONFile: String {
case publicSchool = "publicSchool"
case privateSchool = "privateSchool"
}
private lazy var privateSchoolArray = getPrivateSchools()
private lazy var publicSchoolArray = getPublicSchools()
func getSchoolArray(sorted: Bool, filtered: Bool, by stateAbbreviation: String?) -> [SchoolProtocol] {
var schools = combineArrays()
if sorted {
schools.sort(by: { $0.name < $1.name })
}
if filtered {
guard let abbr = stateAbbreviation else { return [] }
schools = schools.filter {
return $0.state == abbr
}
}
return schools
}
private func combineArrays() -> [SchoolProtocol] {
// EVERYTHING IS FINE IN NEXT LINE
let a = privateSchoolArray
// PROBLEM OCCURS IN NEXT 2 LINES WHEN DOWNCASTING
let b = privateSchoolArray as [SchoolProtocol]
let c = publicSchoolArray as [SchoolProtocol]
return b + c
}
private func getPublicSchools() -> [PublicSchoolModel] {
guard let jsonData = getJSONData(from: .publicSchool) else { return [] }
guard let schools = decode(jsonData: jsonData, using: [PublicSchoolModel].self) else { return [] }
return schools
}
private func getPrivateSchools() -> [PrivateSchoolModel] {
guard let jsonData = getJSONData(from: .privateSchool) else { return [] }
guard let schools = decode(jsonData: jsonData, using: [PrivateSchoolModel].self) else { return [] }
return schools
}
private func getJSONData(from resource: JSONFile) -> Data? {
let url = Bundle.main.url(forResource: resource.rawValue, withExtension: "json")!
do {
let jsonData = try Data(contentsOf: url)
return jsonData
}
catch {
print(error)
}
return nil
}
private func decode<M: Decodable>(jsonData: Data, using modelType: M.Type) -> M? {
do {
//here dataResponse received from a network request
let decoder = JSONDecoder()
let model = try decoder.decode(modelType, from:
jsonData) //Decode JSON Response Data
return model
} catch let parsingError {
print("Error", parsingError)
}
return nil
}
}
And then it is just called in another class by schoolJSONHandler.getSchoolArray(sorted: true, filtered: true, by: "WA")

Swift 4 - Struct static variables not showing values

I am new to swift but learning as I go. I have a function to get data from AWS DynamoDB table, using a for..loop. I am capturing field values of table items in struct variables within the loop, but when I call the struct variables in a tableview, it is showing blank array like [] in the log screen in Xcode. Here is the code:
import Foundation
import SQLite3
import UIKit
import AWSDynamoDB
struct MyVariables {
static var empDict: [String: String] = [:]
static var arrayEmployees = Array<Dictionary<String, String>>()
}
// This is a class defined to hold various methods to be used throughout the app.
class DatabaseClass: UIViewController, UITextFieldDelegate
{
// other methods within DatabaseClass
// ........
//
//
// Scan DynamoDB table for employees
func scanEmployees () {
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.default()
let scanExpression = AWSDynamoDBScanExpression()
scanExpression.limit = 20
dynamoDBObjectMapper.scan(Employees.self, expression: scanExpression).continueWith(block: { (task:AWSTask<AWSDynamoDBPaginatedOutput>!) -> Any? in
if let error = task.error as NSError? {
print("The request failed. Error: \(error)")
}
let paginatedOutput = task.result!
for emp in paginatedOutput.items as! [Employees] {
if emp._empid != nil {
MyVariables.empDict["Empid"] = emp._empid
} else {
MyVariables.empDict["Empid"] = nil
}
if emp._email != nil {
MyVariables.empDict["email"] = emp._email
} else {
MyVariables.empDict["email"] = nil
}
if emp._firstname != nil {
MyVariables.empDict["firstname"] = emp._firstname
} else {
MyVariables.empDict["firstname"] = nil
}
if emp._lastname != nil {
MyVariables.empDict["lastname"] = emp._lastname
} else {
MyVariables.empDict["lastname"] = nil
}
if emp._location != nil {
MyVariables.empDict["location"] = emp._location
} else {
MyVariables.empDict["location"] = nil
}
if emp._mobile != nil {
MyVariables.empDict["mobile"] = emp._mobile
} else {
MyVariables.empDict["mobile"] = nil
}
if emp._work != nil {
MyVariables.empDict["work"] = emp._work
} else {
MyVariables.empDict["work"] = nil
}
if emp._site != nil {
MyVariables.empDict["site"] = emp._site
} else {
MyVariables.empDict["site"] = nil
}
MyVariables.arrayEmployees.append(MyVariables.empDict)
//print(MyVariables.arrayEmployees)
} // for loop
print ("printing array employees just after for loop")
print(MyVariables.arrayEmployees)
// The above print works
return nil
})
// print(MyVariables.arrayEmployees)
// Above doesn’t print
}
}
Later, I call this DatabaseClass in a tableview as follows:
class JsonParseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let createEmp = DatabaseClass()
createEmp.scanEmployees()
print("in JsonParseViewController, calling scan function")
print(MyVariables.arrayEmployees)
When running in simulator, the log file shows the following:
in JsonParseViewController, calling scan function
[]
printing array employees just after for loop
[["location": "XXXXXX", "email": "johnSmith1#email.com", "firstname": "John1", "lastname": "Smith1", "mobile": "123-456-7890", "work": "123-456-7890", "Empid": "12345", "site": "KKKKKK"], ["location": "YYYYY", "email": "johnSmith8#email.com", "firstname": "John8", "lastname": "Smith8", "mobile": "123-456-7890", "work": "123-456-7890", "Empid": "12415", "site": "ZZZZZ"], ......etc]]
While executing the function scanEmployees, it seems to skip the DynamoDB scan command and returns [] in tableview, but goes to the function somehow and executes the data. It fails to capture the data in struct variables. I have bee trying to fix this for many days.
You have a problem with static memory. I would suggest not to use static values, its a very bad practice.
The solution could be to create a structure inside The class DatabaseClass. For example var myVariables = MyVariables() and fill it in the same way. Now You have the option for return it or use it from the DatabaseClass object. I will show the example now how it should look.
struct MyVariables {
var empDict: [String: String] = [:]
var arrayEmployees = Array<Dictionary<String, String>>()
}
// This is a class defined to hold various methods to be used throughout the app.
class DatabaseClass: UIViewController, UITextFieldDelegate
{
// other methods within DatabaseClass
// ........
//
//
// Scan DynamoDB table for employees
public var myVariables = MyVariables()
func scanEmployees () {
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.default()
let scanExpression = AWSDynamoDBScanExpression()
scanExpression.limit = 20
dynamoDBObjectMapper.scan(Employees.self, expression: scanExpression).continueWith(block: { (task:AWSTask<AWSDynamoDBPaginatedOutput>!) -> Any? in
if let error = task.error as NSError? {
print("The request failed. Error: \(error)")
}
let paginatedOutput = task.result!
for emp in paginatedOutput.items as! [Employees] {
myVariables.empDict["Empid"] = emp._empid
myVariables.empDict["email"] = emp._email
myVariables.empDict["firstname"] = emp._firstname
myVariables.empDict["lastname"] = emp._lastname
myVariables.empDict["location"] = emp._location
myVariables.empDict["mobile"] = emp._mobile
myVariables.empDict["work"] = emp._work
myVariables.empDict["site"] = emp._site
myVariables.arrayEmployees.append(myVariables.empDict)
//print(myVariables.arrayEmployees)
} // for loop
print ("printing array employees just after for loop")
print(myVariables.arrayEmployees)
// The above print works
return nil
})
// print(myVariables.arrayEmployees)
// Above doesn’t print
}
}
//then use it in this way
class JsonParseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let createEmp = DatabaseClass()
createEmp.scanEmployees()
print("in JsonParseViewController, calling scan function")
print(createEmp.myVariables.arrayEmployees)
}
}
You miss asynchronous call
func scanEmployees (completion:#escaping ()-> Void) {
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.default()
let scanExpression = AWSDynamoDBScanExpression()
scanExpression.limit = 20
dynamoDBObjectMapper.scan(Employees.self, expression: scanExpression).continueWith(block: { (task:AWSTask<AWSDynamoDBPaginatedOutput>!) -> Any? in
if let error = task.error as NSError? {
print("The request failed. Error: \(error)")
}
let paginatedOutput = task.result!
for emp in paginatedOutput.items as! [Employees] {
myVariables.empDict["Empid"] = emp._empid
myVariables.empDict["email"] = emp._email
myVariables.empDict["firstname"] = emp._firstname
myVariables.empDict["lastname"] = emp._lastname
myVariables.empDict["location"] = emp._location
myVariables.empDict["mobile"] = emp._mobile
myVariables.empDict["work"] = emp._work
myVariables.empDict["site"] = emp._site
MyVariables.arrayEmployees.append(MyVariables.empDict)
//print(MyVariables.arrayEmployees)
} // for loop
print ("printing array employees just after for loop")
print(MyVariables.arrayEmployees)
// The above print works
completion()
return nil
}
)
// print(MyVariables.arrayEmployees)
// Above doesn’t print
}
//
when calling
createEmp.scanEmployees {
print(MyVariables.arrayEmployees)
}
Note : I recommend using SwiftyJson or JSONDecoder intstead of manually parsing the keys