I have integrated the Pusher framework for my application in Swift 3 using cocoa pods [ pod 'PusherSwift' ].
These are the lines of code :
let pusher = Pusher(key: "XXXXXXXXXXXXXXXXXXXX")
// subscribe to channel and bind to event
let channel = pusher.subscribe("test_channel")
let _ = channel.bind(eventName: "my_event", callback: { (data: Any?) -> Void in
if let data = data as? [String : AnyObject] {
if let message = data["message"] as? String {
print(message)
}
}
})
pusher.connect()
The app crashes at pusher.connect() at the line - self.delegate?.debugLog?(message: "[PUSHER DEBUG] Network reachable"). No crash report is shown.
open lazy var reachability: Reachability? = {
let reachability = Reachability.init()
reachability?.whenReachable = { [unowned self] reachability in
self.delegate?.debugLog?(message: "[PUSHER DEBUG] Network reachable")
if self.connectionState == .disconnected || self.connectionState == .reconnectingWhenNetworkBecomesReachable {
self.attemptReconnect()
}
}
reachability?.whenUnreachable = { [unowned self] reachability in
self.delegate?.debugLog?(message: "[PUSHER DEBUG] Network unreachable")
}
return reachability
}()
This looks like you might be getting bitten by the same issue described here.
I think it's that the PusherConnection object is taken as unowned into the reachability closure but because you're not keeping a reference to the Pusher instance outside of the viewDidLoad function then the connection object gets cleaned up whereas the reachability object does not.
So, to fix this you probably need to declare the pusher object outside of the function where you instantiate it, so that it hangs around. e.g.
class ViewController: UIViewController, PusherDelegate {
var pusher: Pusher! = nil
...
and then within viewDidLoad do pusher = Pusher(... as normal.
I don't think you need to use pusher.connect().
See for example detailed docs:
let pusher = Pusher(key: "YOUR_APP_KEY")
let myChannel = pusher.subscribe("my-channel")
myChannel.bind(eventName: "new-price", callback: { (data: Any?) -> Void in
if let data = data as? [String : AnyObject] {
if let price = data["price"] as? String, company = data["company"] as? String {
print("\(company) is now priced at \(price)")
}
}
})
Alternatively try this first and see if it connects:
let pusher = Pusher(key: "XXXXXXXXXXXXXXXXXXXX")
pusher.connect()
Then bind to your channel.
Related
Hope you're doing well!
I've built an app that generates a view from a .csv file that I have hosted on my website. I've previously managed to get everything working as expected where I called the csv from the website and wrote the contents directly to a variable and then processed it from there. Obviously this wasn't good practice as the app started mis-behaving when the internet couldn't be accessed (despite writing in connectivity checks).
I've now built out the app to call the URL, save the csv with Filemanager, then when the app refreshes, it will use FileManager.default.replaceItemAt to replace the previous version if there is internet connectivity, if not the app builds from the previously stored .csv
This all works fine when the app is running, however I'm running into issues with the background processing task. It seems the app doesn't have permissions to write with FileManager when it is executed from the background task. Is there an additional step I'm missing when using this in background tasks? I've attempted to use FileManager.default.removeItem followed by FileManager.default.copyItem instead of replaceItemAt but it doesn't seem to make a difference as expected.
UPDATE 22/06 - Still scouring the internet for similar issues or examples I think I might be going down the wrong rabbit hole here. This could be issues with the way the new background task has been configured for retrieving data from my website, although the background tasks worked fine before there seems to be a bit more legwork needed for this method to work as a background task.
func handleAppRefresh(task: BGProcessingTask) {
//Schedules another refresh
scheduleAppRefresh()
DispatchQueue.global(qos: .background).async {
pullData()
print("BG Background Task fired")
}
pullData() will call loadCSV() and then do some data processing. At the moment I'm just using a print straight after loadCSV() is called to validate if the downloads etc are successful.
// Function to pass the string above into variables set in the csvevent struct
func loadCSV(from csvName: String) -> [CSVEvent] {
var csvToStruct = [CSVEvent]()
// Creates destination filepath & filename
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("testcsv.csv")
//Create URL to the source file to be downloaded
let fileURL = URL(string: "https://example.com/testcsv.csv")!
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:fileURL)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("File downloaded Successfully. Response: \(statusCode)")
}
do {
let _ = try FileManager.default.replaceItemAt(destinationFileUrl, withItemAt: tempLocalUrl)
} catch (let writeError) {
print("Error creating a file \(destinationFileUrl) : \(writeError)")
}
} else {
print("Error" )
}
}
task.resume()
let data = readCSV(inputFile: "testcsv.csv")
var rows = data.components(separatedBy: "\n")
rows.removeFirst()
// Iterates through each row and sets values
for row in rows {
let csvColumns = row.components(separatedBy: ",")
let csveventStruct = CSVEvent.init(raw: csvColumns)
csvToStruct.append(csveventStruct)
}
print("LoadCSV has run and created testcsv.csv")
return csvToStruct
}
Any help or pointers to why these files aren't being updated in background tasks but are working fine in app would be massively appreciated!
Thanks in advance.
EDIT: adding new BGProcessingTask
func handleAppRefresh(task: BGProcessingTask) {
//Schedules another refresh
print("BG Background Task fired")
scheduleAppRefresh()
Task.detached {
do {
let events = try await loadCSV(from: "Eventtest").filter { !dateInPast(value: $0.date) }
print(events)
pullData(events: events)
} catch {
print(error)
}
}
}
The problem is not the background task per se, the problem is the asynchronous behavior of downloadTask. readCSV is executed before the data is downloaded.
In Swift 5.5 and later async/await provides asynchronous behavior but the code can be written continuously.
func loadCSV(from csvName: String) async throws -> [CSVEvent] {
var csvToStruct = [CSVEvent]()
// Creates destination filepath & filename
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("testcsv.csv")
//Create URL to the source file to be downloaded
let fileURL = URL(string: "https://example.com/testcsv.csv")!
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:fileURL)
let (url, response) = try await session.download(for: request)
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("File downloaded Successfully. Response: \(statusCode)")
}
let _ = try FileManager.default.replaceItemAt(destinationFileUrl, withItemAt: url)
let data = readCSV(inputFile: "testcsv.csv")
var rows = data.components(separatedBy: "\n")
rows.removeFirst()
// Iterates through each row and sets values
for row in rows {
let csvColumns = row.components(separatedBy: ",")
let csveventStruct = CSVEvent.init(raw: csvColumns)
csvToStruct.append(csveventStruct)
}
print("LoadCSV has run and created testcsv.csv")
return csvToStruct
}
To call the function you have to wrap it in a detached Task which replaces the GCD queue
Task.detached {
do {
let events = try await loadCSV(csvName: "Foo")
print("BG Background Task fired")
} catch {
print(error)
}
}
I have a contact CKRecord with many location CKRecords ( 1 to many relationship)
Both contact CKRecord and Location CKRecord are created in public Database. I add CKReference fro contact to locaiotn via a field named owningContact on location.
ckRecord["owningContact"] = CKReference(record: contactRecord!, action: .deleteSelf)
I go to cloudKit dashboard and verify both the records exist. The location CKRecord has field owningContact that has the recordName of the contact CKRecord. I defined a function to get locations like this:
private func iCloudFetchLocations(withContactCKRecord: CKRecord, completionHandler: #escaping ([CKRecord]?, Error?) -> Void) {
var records = [CKRecord]()
let recordToMatch = CKReference(recordID: withContactCKRecord.recordID, action: .deleteSelf)
let predicate = NSPredicate(format: "owningContact == %#", recordToMatch)
// Create the query object.
let query = CKQuery(recordType: "location", predicate: predicate)
let queryOp = CKQueryOperation(query: query)
queryOp.resultsLimit = 1
queryOp.qualityOfService = .userInteractive
queryOp.recordFetchedBlock = {
records.append($0)
print($0)
}
queryOp.queryCompletionBlock = { (cursor, error) in
guard error == nil else {
if let ckerror = error as? CKError {
self.aErrorHandler.handleCkError(ckerror: ckerror)
}
return
}
if (cursor != nil) {
let newOperation = CKQueryOperation(cursor: cursor!)
newOperation.resultsLimit = queryOp.resultsLimit
newOperation.recordFetchedBlock = queryOp.recordFetchedBlock
newOperation.queryCompletionBlock = queryOp.queryCompletionBlock
self.publicDB?.add(newOperation)
}
completionHandler(records, error)
}
self.publicDB?.add(queryOp)
}
Then I call the code to fetch location CKRecord based on contact CKRecord like this:
let predicate = NSPredicate(format: "TRUEPREDICATE")
let query = CKQuery(recordType: Cloud.Entity.Contact, predicate: predicate)
publicDB?.perform(query, inZoneWith: nil, completionHandler: { (records, error) in
guard error == nil else {
if let ckerror = error as? CKError {
self.aErrorHandler.handleCkError(ckerror: ckerror)
}
return
completion(false)
}
if let contactRecords = records {
for aContactRecord in contactRecords {
// fetch Location Data
self.iCloudFetchLocations(withContactCKRecord: aContactRecord, completionHandler: { records, error in
guard error == nil else {
if let ckerror = error as? CKError {
self.aErrorHandler.handleCkError(ckerror: ckerror)
}
return
completion(false)
}
if let locationRecords = records {
}
})
}
}
})
I have two contacts the first one has been CKReferenc'ed to the location, where as the second contact is still not yet CKReferenc'ed to the location.
I think here is the problem: First time in the loop contact CKRecord information is sent by calling iCloudFetchLocations which returns immediately without waiting for cloud response, and the for loop sends the second contact and calls iCloudFetchLocations again. Since the second contact has no CKReference to the location, the call fails and I can never get to the first contact's location since it hasn't returned yet.
How to fix this?
I found that I had not set the CKReference field: owningContact as Queryable. The way I found out is printing error like this:
if let ckerror = error as? CKError {
print(ckerror.userInfo)
print(ckerror.errorUserInfo)
self.aErrorHandler.handleCkError(ckerror: ckerror)
}
As soon as I did that it started working, Since I was in a for loop it was timing out on previous fetch I think.
I do have a UIWebView included where a public URL is loaded; unfortunately, vcard and ical-Links are not handled, i.e. nothing happens when I click on them.
I tried to set all data detectors, no luck unfortunately.
In the Xcode-log, I get this here when clicking on such a link:
2017-07-14 13:43:00.982413+0200 xxx[2208:967973] WF: _userSettingsForUser mobile: {
filterBlacklist = (
);
filterWhitelist = (
);
restrictWeb = 1;
useContentFilter = 0;
useContentFilterOverrides = 0;
whitelistEnabled = 0;
}
In Safari, the same stuff works as expected.
If I use UIApplication.shared.openURL(icsOrVcardUrl) Safari gets opened and from there everything works as expected again, but I don't want the user to leave the app...
EDIT
This doesn't work either:
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
if let url = request.url {
if url.absoluteString.contains("=vcard&") || url.absoluteString.contains("/ical/") {
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:url)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
DispatchQueue.main.async {
self.documentController.url = tempLocalUrl
self.documentController.presentPreview(animated: true)
}
}
}
task.resume()
return false
}
}
return true
}
Use a UIDocumentInteractionController to preview without leaving your app.
I tested it quickly with an .ics file and it works fine.
Implement the UIDocumentInteractionControllerDelegate protocol
extension MainViewController: UIDocumentInteractionControllerDelegate {
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return self;
}
}
Create an instance of the interaction controller:
let documentController = UIDocumentInteractionController()
Intercept the clicks in your UIWebView in shouldStartLoadWithRequest, return false for links you want to handle with the in-app preview and true for all the rest. And finally:
func previewDocument(_ url: URL) {
documentController.url = url
documentController.presentPreview(animated: true)
}
Here it is in the simulator
EDIT:
In response to the comment to this answer:
The reason it doesn't work for you is because the UIDocumentInteractionController depends on the file extension. The extension of the temp file is .tmp
Renaming the file after the download solves the problem. Quick and dirty example:
let task = session.downloadTask(with: url!) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
do {
let filemgr = FileManager.default
let newUrl = tempLocalUrl.appendingPathExtension("ics")
try filemgr.moveItem(at: tempLocalUrl, to: newUrl)
DispatchQueue.main.async {
self.documentController.url = newUrl
self.documentController.presentPreview(animated: true)
}
} catch let error {
print("Error!!!: \(error.localizedDescription)")
}
}
}
task.resume()
In this case it is advisable to clean after yourself, because the file won't be deleted after the task completes although the OS will delete it eventually, when space is needed. If you often access the same urls, Library/Caches/ may be a better place for this files, just come up with good naming schema, and check if the file doesn't exist already.
I am following an outdated tutorial from the Ray Wenderlich team that walks through the repopulation of a Core Data-backed application by using a Command Line Tool application.
I have successfully prepopulated the intended entities, verified by performing an NSFetchRequest.
Now, I want to use the same prepopulated data in my unit tests to verify that my interactions with CoreData are happening correctly. I tried setting up my mocked CoreDataStack subclass to use an in-memory store, but when I attempt to verify that I have the prepopulated data for use in my unit tests, I am getting a count of 0.
The class responsible for interacting with CoreData in my application's target, named CoreDataStack, follows:
/// The object that is responsible for managing interactions with Core Data.
internal class CoreDataStack {
// MARK: - Properties
/// The name of the `NSManagedObjectModel` object used for storing information with Core Data.
private let modelName: String
/// The `NSManagedObjectContext` object that is associated with the main queue.
internal lazy var mainContext: NSManagedObjectContext = {
return self.storeContainer.viewContext
}()
/// The `NSPersistentContainer` object that encapsulates the application's Core Data stack.
internal lazy var storeContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: self.modelName)
let directory = NSPersistentContainer.defaultDirectoryURL()
let storeURL = directory.appendingPathComponent("\(self.modelName).sqlite")
if !FileManager.default.fileExists(atPath: (storeURL.path)) {
guard let populatedURL = Bundle.main.url(forResource: self.modelName, withExtension: "sqlite") else {
fatalError("Invalid populated .sqlite file URL")
}
do {
try FileManager.default.copyItem(at: populatedURL, to: storeURL)
} catch {
fatalError("Error: \(error)")
}
}
let description = NSPersistentStoreDescription()
description.url = storeURL
container.persistentStoreDescriptions = [description]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Error: \(error)")
}
})
return container
}()
// MARK: - Initialization
/// Returns an instance of `CoreDataStack`.
/// - parameter modelName: The name of the `NSManagedObjectModel` object used for storing information with Core Data.
internal init(modelName: String) {
self.modelName = modelName
}
/// Attempts to save items to Core Data by committing changes to `NSManagedObject`s in a `NSManagedObjectContext`.
/// - parameter context: The `NSManagedObjectContext` of which changes should be committed.
internal func saveContext(_ context: NSManagedObjectContext) {
context.perform {
do {
try context.save()
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
}
The subclass of CoreDataStack, MockCoreDataStack, used for testing follows:
internal class MockCoreDataStack: CoreDataStack {
// MARK: - Initialization
convenience init() {
self.init(modelName: "Currency")
}
override init(modelName: String) {
super.init(modelName: modelName)
let container = NSPersistentContainer(name: modelName)
let directory = NSPersistentContainer.defaultDirectoryURL()
let storeURL = directory.appendingPathComponent("\(modelName).sqlite")
if !FileManager.default.fileExists(atPath: (storeURL.path)) {
guard let populatedURL = Bundle(for: type(of: self)).url(forResource: modelName, withExtension: "sqlite") else {
fatalError("Invalid populated .sqlite file URL")
}
do {
try FileManager.default.copyItem(at: populatedURL, to: storeURL)
} catch {
fatalError("Error: \(error)")
}
}
let description = NSPersistentStoreDescription()
description.url = storeURL
description.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
self.storeContainer = container
}
}
The resulting count of my fetch request is 0 in my unit tests target. I expect to return a count consisting of the number of prepopulated objects, just as I get when I return the count in my application's target.
What am I doing incorrectly that is causing me to not return the expected result?
The in memory store doesn't use the store URL. It just creates an empty store in memory.
As an alternative to the in memory store, you could possibly create a parent NSManagedObjectContext between the persistent store and the contexts you actually use. (I'm not sure how that would play with NSPersistentContainer, however.)
Then you can just rollback() the parent context when you want to reset back to your initial state.
Using Alamofire 4.0 and Swift 3.0 this works:
Alamofire.request("http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json", method: .get).responseJSON {
(response) -> Void in
print("Success: \(response.result)")
}
Success: SUCCESS
However when I try to use the Sessionmanager so I can include a timeoutInterval, my requests always fail
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 15
let alamofireManager = Alamofire.SessionManager(configuration: configuration)
alamofireManager.request("http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json").validate().responseJSON {
response in
print("Success: \(response.result)")
print("Response String: \(response.result.value)")
}
Success: FAILURE
Would be grateful if someone could help point me in the right direction here.
By printing response.result.error I got:
Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLKey=http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json, NSLocalizedDescription=cancelled, NSErrorFailingURLStringKey=http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json}
Which lead me to this reference:
You need to make sure that the manager is retained. The difference
here is that the initialized manager is not owned, and is deallocated
shortly after it goes out of scope. As a result, any pending tasks are
cancelled.
Solution:
One way to solve the issue your having is by declaring the custom session manager outside of the class declaration as a global variable like so...
let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 15
return SessionManager(configuration: configuration)
}()
Now, within your class you can make the request.
class ViewController: UIViewController {
let url = "http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json"
override func viewDidLoad() {
super.viewDidLoad()
sessionManager.request(url).validate().responseJSON { response in
switch response.result {
case .success:
print(response.result.value as! NSDictionary)
break
case .failure:
print(response.result.error!)
break
}
}
}
}
Which shall give you what you're looking for. Hope that helps!