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 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 new to Siesta. How can I get the entire array and pass into my model? Besides, how can I post with params? In their documentation I couldn't find any of this.
I'm new to siesta as well. I was able to find documentation about requests here http://bustoutsolutions.github.io/siesta/guide/requests/
Basically you'll setup your resource and then call:
resource.request(.post, json: ["foo": [1,2,3]])
Your question is far too complex so I will try to explain you in a simple way.
Siesta provides a great way to map your models by using Custom Transformers. One simple way to do this is to implement the Decodable protocol provided by Swift 4+:
Lets say we want to decode this JSON response:
{
"id": 1,
"name": "Oswaldo",
"email": "omaestra#gmail.com"
}
Into our great User class which implements the Decodable protocol:
class User: Decodable {
var id: Int
var name: String
var email: String
private enum CodingKeys: String, CodingKey {
case id, name, email
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id
name = try container.decode(String.self, forKey: .name)
email = try container.decode(String.self, forKey: .email)
}
}
Awesome! Now we can decode the JSON response from our server into our awesome User class.
Next, inside our Siesta.Service class we can configure our custom transformers for specific resources:
class API: Siesta.Service {
init() {
super.init(baseURL: "https://api.example.com")
// Some initial configuration
}
let myAPI = API()
// –––––– Global configuration ––––––
let jsonDecoder = JSONDecoder()
// –––––– Mapping from specific paths to models ––––––
// These all use Swift 4’s JSONDecoder, but you can configure arbitrary transforms on arbitrary data types.
configureTransformer("rest/user/") {
// Input type inferred because the from: param takes Data.
// Output type inferred because jsonDecoder.decode() will return User
try jsonDecoder.decode(User.self, from: $0.content)
}
// MARK: - Resources
var userResource: Siesta.Resource { return resource("rest/user/") }
}
Finally, we can implement our resource inside our ViewController:
class UserViewController: UIViewController {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var emailLabel: UILabel!!
override func viewDidLoad() {
super.viewDidLoad()
myAPI.userResource
.addObserver(self)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
myAPI.userResource.loadIfNeeded()
}
}
extension UserViewController: ResourceObserver {
func resourceChanged(_ resource: Resource, event: ResourceEvent) {
let user: User? = resource.typedContent()
nameLabel.text = user?.name
emailLabel.text = user?.email
}
}
Note: Siesta is very flexible and customisable framework, you can find multiple ways to configure your services and resources. This is just one simple way to implement what you are asking for.
To make a POST request with params you can do something like this inside your Service class:
Implement an update method that makes the POST request.
func update(user: User, newName: String) -> Siesta.Request {
return usersResource
.child(user.id)
.child("update")
.request(.post, json: ["name": newName])
}
Then, in your ViewController you can call the method to submit the POST request and evaluate its response:
myAPI.update(user: user, newName: "NEW NAME")
.onSuccess({ (_) in
print("Successfully updated user's name.")
})
.onFailure({ (error) in
print("Error trying to update user's name.")
})
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.
I'm struggling a bit to figure out how to best test an app that uses Alamofire to help sync with server data.
I want to be able to test my code that uses Alamofire and processes JSON responses from a server.
I'd like to mock those tests so that I can feed the expected response data to those tests without incurring real network traffic.
This blog post (http://nshipster.com/xctestcase/) describes how easy it is to Mock an object in Swift - but I'm not sure how to do that with Alamofire and its chained responses.
Would I mock the Manager? the Request? Response? Any help would be appreciated!
I'm adding another answer since I've just found this approach that in my opinion is easier and really simple to read and use.
I've created a dummy Alamofire class that contains only the functions and the types necessary for tests.
Now I include this file in the test target instead of the real Alamofire.
For example I've created my version of the Request class where I define a couple of static variables that I valorise depending on the test, and for this class I've implemented only the init and the responseJSON function.
public class Request {
var request:String?
struct response{
static var data:NSHTTPURLResponse?
static var json:AnyObject?
static var error:NSError?
}
init (request:String){
self.request = request
}
public func responseJSON(options: NSJSONReadingOptions = .AllowFragments, completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self {
completionHandler(NSURLRequest(URL: NSURL(string:self.request!)!), Request.response.data, Request.response.json, Request.response.error)
return self
}
}
Now I can mock a response in a test:
func testMytestFunction(){
var HTMLResponse = NSHTTPURLResponse(URL: NSURL(string: "myurl")!, statusCode: 200, HTTPVersion: "HTTP/1.1", headerFields: nil)
Request.response.data = HTMLResponse
Request.response.json = LoadDataFromJSONFile("MyJsonFile")
request(.POST, "myurl", parameters: nil, encoding: ParameterEncoding.JSON).responseJSON {
(request, response, JSON, error) -> Void in
// the JSON and response variable now contains exactly the data that you have passed to Request.response.data and Request.response.json
}
}
The request function is defined here:
public func request(method: Method, URLString: URLStringConvertible, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL) -> Request {
return Request(request: URLString.URLString)
}
public func request(URLRequest: URLRequestConvertible) -> Request {
return Request(request: "fakecall")
}
This question is getting old, but I just encountered the same issue, and the solution is very easy when using OHHTTPStubs.
OHHTTPStubs just mocks the responses you get from NSURLSession, so it works well with Alamofire, and you get very good coverage of your code path.
For example, in your test case, just mock the response using:
OHHTTPStubs.stubRequestsPassingTest({
(request: NSURLRequest) -> Bool in
return request.URL!.host == "myhost.com"
}, withStubResponse: {
(request: NSURLRequest) -> OHHTTPStubsResponse in
let obj = ["status": "ok", "data": "something"]
return OHHTTPStubsResponse(JSONObject: obj, statusCode:200, headers:nil)
})
Waiting for an answer by #mattt I post an example of my code.
Let's say that we have a Client class that is responsible for calling a simple web service. This class implements a function called userSignIn that performs a sign in using the WS.
This is the code for the userSignIn function:
func userSignIn(
#email:String,
password:String,
completionHandler: (Bool, String?, NSError?) -> Void
)-> Void
{
var parameters:[String:AnyObject] = [
"email":email,
"password":password,
]
Alamofire.request(.POST, Client.urlPath, parameters: parameters, encoding: ParameterEncoding.JSON).responseJSON {
(request, response, JSON, responseError) -> Void in
// Setup callback params
// HERE WE INJECT THE "FAKE" DATA--------
var operationComplete = false
var accessToken:String?
var error:NSError?
// --------------------------------------
if let statusCode = response?.statusCode {
// Check for errors and build response data
(operationComplete, accessToken, error) = self.checkSignInResponse(statusCode, JSON: JSON)
}
// Call the completion handler
completionHandler(operationComplete, accessToken, error)
}
}
The aim of the function is to get a token from the web service if the information passed by the user are correct.
The function checkSignInResponse (I don't report its code since it's not useful for the answer) has the role to valorise the 3 variables operationComplete, accessToken and error depending on the JSON response received.
Now that the 3 variables have a value we call the completionHandler using them.
How to mock this function?!
To mock the response I override the userSignIn function directly into the test function (as explained by the NSHipster article).
func testUserSignIn_whenParamsAreInvalid(){
class MockClient:Client {
override func userSignIn(#email: String, password: String, completionHandler:
(Bool, String?, NSError?) -> Void) {
// Set callback params
var operationComplete = false
var accessToken:String? = nil
var error:NSError? = NSError(domain: "Testing", code: 99, userInfo: nil)
completionHandler(operationComplete, accessToken, error)
}
}
signInViewController!.client = MockClient()
signInViewController!.loadView()
fillRegisterFieldsWithDataAndSubmit(femail(), password: fpassword())
XCTAssertNotNil(signInViewController!.error, "Expect error to be not nil")
}
then I substitute the client inside the view controller that I'm testing using my "mocked" client. In this case I'm testing that the controller passes to the function information that are not valid so I check that the error property of the controller is not nil. To force this data I simply set operationComplete to false and I manual generate an NSError.
Does it make any sense to you? I'm not sure that this test is a good test... but at least I can verify the data flow.
I believe I have a solution to this for the newer versions of Alamofire. My Swift and DI skills are a bit noob so this can probably be improved but I thought I'd share. The most challenging part of mocking Alamofire is mocking the method chaining in the Network call (request().responseJSON).
The Network call:
let networkManager: NetworkManagerProtocol!
init(_ networkManager: NetworkManagerProtocol = NetworkManagerTest(SessionManager())) {
self.networkManager = networkManager
}
func create(_ params: [String: Any], completion: #escaping (Response<Success,Fail>) -> Void) {
self.networkManager.manager.request(self.url!, method: .post, parameters: params, encoding: URLEncoding.default, headers: nil).responseJSON {
response in
if response.result.isSuccess {
completion(Success())
} else {
completion(Fail())
}
}
}
The manager that you'll inject into the network call class:
The NetworkManagerProtocol provides the get manager functionality to the various types of network managers.
class NetworkManager: NetworkManagerProtocol {
private let sessionManager: NetworkManagerProtocol
init(_ sessionManager: NetworkManagerProtocol) {
self.sessionManager = sessionManager
}
var manager: SessionManagerProtocol {
get {
return sessionManager.manager
}
set {}
}
}
Extend Alamofire's SessionManager class:
This is where we add the protocols and custom functionality to SessionManager. Note the protocol's request method is a wrapper around Alamofire's request method .
extension SessionManager: NetworkManagerProtocol, SessionManagerProtocol {
private static var _manager = SessionManager()
var manager: SessionManagerProtocol {
get {
return SessionManager._manager
}
set {
let configuration = URLSessionConfiguration.default
SessionManager._manager = Alamofire.SessionManager(configuration: configuration, delegate: SessionManager.default.delegate)
}
}
func request(_ url: URLConvertible, method: HTTPMethod, parameters: Parameters, encoding: ParameterEncoding, headers: HTTPHeaders?) -> DataRequestProtocol {
let dataRequest: DataRequest = self.request(url, method: method, parameters: parameters, encoding: encoding, headers: headers)
return dataRequest
}
}
Create a SessionManagerMock for the mock api call:
This class creates a SessionManagerMock object and then retrieves the mock data with its request method.
class SessionManagerMock: NetworkManagerProtocol, SessionManagerProtocol {
private static var _manager = SessionManagerMock()
var manager: SessionManagerProtocol {
get {
return SessionManagerMock._manager
}
set {}
}
func request(_ url: URLConvertible, method: HTTPMethod, parameters: Parameters, encoding: ParameterEncoding, headers: HTTPHeaders?) -> DataRequestProtocol {
return DataRequestMock()
}
}
Extend Alamofire's DataRequest class:
And again, note the protocol's responseJSON class is a wrapper around DataRequests's responseJSON class.
extension DataRequest: DataRequestProtocol {
func responseJSON(completionHandler: #escaping (DataResponse<Any>) -> Void) -> Self {
return self.responseJSON(queue: nil, options: .allowFragments, completionHandler: completionHandler)
}
}
DataRequestMock Class:
This class stores the data for the mock request. It could be built out a little more (add request data, etc) but you get the idea.
class DataRequestMock: DataRequestProtocol {
static var statusCode: Int = 200
var dataResponse = DataResponse<Any>(
request: nil,
response: HTTPURLResponse(url: URL(string: "foo.baz.com")!, statusCode: DataRequestMock.statusCode, httpVersion: "1.1", headerFields: nil),
data: nil,
result: Result.success(true), // enum
timeline: Timeline()
)
func response(completionHandler: #escaping (DataResponse<Any>) -> Void) -> Self {
completionHandler(dataResponse)
return self
}
func responseJSON(completionHandler: #escaping (DataResponse<Any>) -> Void) -> Self {
return response(completionHandler: completionHandler)
}
}
The Protocol Droids:
protocol NetworkManagerProtocol {
var manager: SessionManagerProtocol { get set }
}
protocol SessionManagerProtocol {
func request(_ url: URLConvertible, method: HTTPMethod, parameters: Parameters, encoding: ParameterEncoding, headers: HTTPHeaders?) -> DataRequestProtocol
}
protocol DataRequestProtocol {
func responseJSON(completionHandler: #escaping (DataResponse<Any>) -> Void) -> Self
}
The test method:
A lot of improvements could be made to make this more dynamic but again you get the idea
var sut: UserService?
override func setUp() {
super.setUp()
sut = UserService(NetworkManagerTest(SessionManagerMock()))
}
func testCreateUser201() {
DataRequestMock.statusCode = 201
let params : [String : String] = ["name": "foo baz", "email": "foobaz#gmail.com", "password": "tester123"]
var resultCode: Int!
sut?.create(params) {(response: Response) in
switch response {
case .success(let resp):
resultCode = resp.statusCode
case .failure(let resp):
resultCode = resp.statusCode
}
}
XCTAssertEqual(resultCode, 201, "Status code is wrong")
}