hi i want to test CNContacts Store since this is my first time doing test, i don't have any idea how to conduct a unit test. This the code i want to test.
private func fetchContacts() {
var contacts: [Contact] = []
let keys: [CNKeyDescriptor] = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactPhoneNumbersKey as CNKeyDescriptor]
let request = CNContactFetchRequest(keysToFetch: keys)
do {
try contactStore.enumerateContacts(with: request) {
(contact, stop) in
let name: String = CNContactFormatter.string(from: contact, style: .fullName) ?? contact.nickname
contacts.append(contentsOf: contact.phoneNumbers.compactMap({ phoneNumber in
let phoneNumberString: String = phoneNumber.value.stringValue
return .init(name: name, phoneNumber: phoneNumberString)
}))
}
allContacts = contacts
isContactsFetched = true
filterContacts()
}
catch {
print("unable to fetch contacts")
}
}
I'm using sourcery to generate mock from CNContactStore this is the enumerated mock i generate using sorcery
//MARK: - enumerateContacts
var enumerateContactsWithUsingBlockThrowableError: Error?
var enumerateContactsWithUsingBlockCallsCount = 0
var enumerateContactsWithUsingBlockCalled: Bool {
return enumerateContactsWithUsingBlockCallsCount > 0
}
var enumerateContactsWithUsingBlockReceivedArguments: (fetchRequest: CNContactFetchRequest, block: (CNContact, UnsafeMutablePointer<ObjCBool>) -> Void)?
var enumerateContactsWithUsingBlockReceivedInvocations: [(fetchRequest: CNContactFetchRequest, block: (CNContact, UnsafeMutablePointer<ObjCBool>) -> Void)] = []
var enumerateContactsWithUsingBlockClosure: ((CNContactFetchRequest, #escaping (CNContact, UnsafeMutablePointer<ObjCBool>) -> Void) throws -> Void)?
func enumerateContacts(with fetchRequest: CNContactFetchRequest, usingBlock block: #escaping (CNContact, UnsafeMutablePointer<ObjCBool>) -> Void) throws {
if let error = enumerateContactsWithUsingBlockThrowableError {
throw error
}
enumerateContactsWithUsingBlockCallsCount += 1
enumerateContactsWithUsingBlockReceivedArguments = (fetchRequest: fetchRequest, block: block)
enumerateContactsWithUsingBlockReceivedInvocations.append((fetchRequest: fetchRequest, block: block))
try enumerateContactsWithUsingBlockClosure?(fetchRequest, block)
}
what i did so far for unit test is this
it("should fetch contacts") {
let contact = CNContact()
let stop = UnsafeMutablePointer<ObjCBool>.allocate(capacity: 1)
stop[0] = true
// When
viewModel.onViewDidAppear()
// Then
mockContactStore.enumerateContactsWithUsingBlockClosure = { (_, args) in
args(contact, stop)
expect(mockContactStore.enumerateContactsWithUsingBlockCallsCount).to(equal(1))
}
}
Please help
if you want to test this ->
let request = CNContactFetchRequest(keysToFetch: keys)
you can do like this ->
protocol CNContactFetchRequestProtocol {
}
extension CNContactFetchRequest: CNContactFetchRequestProtocol {
}
let request: CNContactFetchRequestProtocol = CNContactFetchRequest(keysToFetch: keys)
and finally you can create mock
class MockContact: CNContactFetchRequestProtocol {
}
and then you can tests like this:
let request: CNContactFetchRequestProtocol = MockContact()
Related
New to swiftui and don't understand why the JSONDecoder() line in the first code throws
[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
This to me is not updating ui so why is this showing?
do {
// pass the request type, bearer token, and email / password ( which are sent as POST body params )
let request = L.getRequest(requestType:"POST", token: token, email: self.username, password: self.psw)
L.fetchData(from: request) { result in
switch result {
case .success(let data):
// covert the binary data to a swift dictionary
do {
let response = try JSONDecoder().decode(WpJson.self, from: data)
for (key, title) in response.allowedReadings {
let vimeoId = Int( key )!
let vimeoUri = self.buildVimeoUri(vimeoId: key)
self.addReadingEntity(vimeoUri: vimeoUri, vimeoId: vimeoId, title: title)
}
self.writeToKeychain(jwt:response.jwt, displayName: response.displayName)
readings = self.fetchReadings()
}
catch {
self.error = error.localizedDescription
}
case .failure(let error):
self.error = error.localizedDescription
}
}
}
I tried wrapping a main queue around the do-catch in the L.fetchData(from: request) { result in but this did not help
DispatchQueue.main.async { [weak self] in
Here is the Login protocol, again without any ui work:
import Foundation
import SwiftUI
struct Login: Endpoint {
var url: URL?
init(url: URL?) {
self.url = url
}
}
protocol Endpoint {
var url: URL? { get set }
init(url: URL?)
}
extension Endpoint {
func getRequestUrl() -> URLRequest {
guard let requestUrl = url else { fatalError() }
// Prepare URL Request Object
return URLRequest(url: requestUrl)
}
func getRequest(requestType:String="POST", token:String, email:String="", password:String="") -> URLRequest {
var request = self.getRequestUrl()
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
if ( "" != email && "" != password && requestType == "POST") {
let parameters:[String:String?] = [
"email": email,
"password": password
]
// Run the request
do {
// pass dictionary to nsdata object and set it as request body
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
} catch let error {
print(error.localizedDescription)
}
}
return request;
}
func fetchData(from request: URLRequest, completion: #escaping (Result<Data, NetworkError>) -> Void) {
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
completion(.success(data))
} else if error != nil {
// any sort of network failure
completion(.failure(.requestFailed))
} else {
// this ought not to be possible, yet here we are
completion(.failure(.unknown))
}
}.resume()
}
}
extension URLSession {
func dataTask(with request: URLRequest, completionHandler: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> URLSessionDataTask {
return dataTask(with: request, completionHandler: { (data, urlResponse, error) in
if let error = error {
completionHandler(.failure(error))
} else if let data = data, let urlResponse = urlResponse as? HTTPURLResponse {
completionHandler(.success((data, urlResponse)))
}
})
}
}
Do you have any idea on how to fix this?
Wrap it right in place of assignment
catch {
DispatchQueue.main.async {
self.error = error.localizedDescription
}
}
case .failure(let error):
DispatchQueue.main.async {
self.error = error.localizedDescription
}
}
I am writing a Hacker News iOS application using SwiftUI/Combine. They have an API call for getting the ids of top posts and then you are supposed to request each story by itself. For this I have created storyIds(:) -> AnyPublisher<[Int], Error> and story(for:) -> AnyPublisher<Post, Error> for those calls.
Now I want to combine them into one function, getStories() which first download the identifiers and then goes through them fetching the stories one by one. I suppose you can use MergeMany or something else in the API for achieving this but I am not sure
The last thing I need is a function that combine these into stories() -> AnyPublisher<[Post], Error>. I found another question doing almost that. What I miss however, is a way to report progress of the task. I would like to update a counter for each fetched story to show the user how much is left of the download. How can I do this?
struct Agent {
struct Response<T> {
let value: T
let response: URLResponse
}
func run<T: Decodable>(_ request: URLRequest, _ decoder: JSONDecoder = JSONDecoder()) -> AnyPublisher<Response<T>, Error> {
return URLSession.shared
.dataTaskPublisher(for: request)
.tryMap { result -> Response<T> in
let value = try decoder.decode(T.self, from: result.data)
return Response(value: value, response: result.response)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
enum HackerNewsAPI {
static let agent = Agent()
static let base = URL(string: "https://hacker-news.firebaseio.com/v0/")!
}
extension HackerNewsAPI {
static func storyIds() -> AnyPublisher<[Int], Error> {
let request = URLRequest(url: base.appendingPathComponent("topstories.json"))
return agent.run(request)
.print()
.map(\.value)
.eraseToAnyPublisher()
}
}
extension HackerNewsAPI {
static func story(for id: Int) -> AnyPublisher<Post, Error> {
let request = URLRequest(url: base.appendingPathComponent("item/\(id).json"))
return agent.run(request)
.map(\.value)
.eraseToAnyPublisher()
}
}
extension HackerNewsAPI {
static func stories() -> AnyPublisher<[Post], Error> {
HackerNewsAPI.storyIds()
.flatMap { storyIDs -> AnyPublisher<[Post], Error> in
let stories = storyIDs.map { story(for: $0) }
return Publishers.MergeMany(stories)
.collect()
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
I expect something like the below code would work.
You end up with a count publisher that emits the number of articles that have been downloaded so far and a array publisher that emits the entire array of Posts after they have all been downloaded.
func getStories() -> (count: AnyPublisher<Int, Error>, stories: AnyPublisher<[Post], Error>) {
let posts = HackerNewsAPI.storyIds()
.map { $0.map { HackerNewsAPI.story(for: $0) } }
.flatMap { Publishers.Sequence(sequence: $0) }
.flatMap { $0 }
.share()
let count = posts
.scan(0) { count, _ in
return count + 1
}
let array = posts
.reduce([Post]()) { current, post in
current + [post]
}
return (count.eraseToAnyPublisher(), array.eraseToAnyPublisher())
}
I'm trying to migrate an extension to Alamofire.Request but am getting the error Cannot call value of non-function type 'HTTPURLResponse?'.
I know the compiler thinks I'm referring to the member response and not the function.
I've already replaced Request.JSONResponseSerializer with DataRequest.jsonResponseSerializer.
Can anyone see what I'm missing?
extension Alamofire.Request {
public func responseSwiftyJSON(_ queue: DispatchQueue? = nil, options: JSONSerialization.ReadingOptions = .allowFragments, completionHandler: #escaping (NSURLRequest, HTTPURLResponse?, SwiftyJSON.JSON, Error?) -> Void) -> Self {
return response(responseSerializer: Alamofire.DataRequest.jsonResponseSerializer(options: options)) { response in
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
var responseJSON: JSON
if response.result.isFailure {
responseJSON = JSON.null
} else {
responseJSON = SwiftyJSON.JSON(response.result.value!)
}
dispatch_async(queue ?? dispatch_get_main_queue(), {
completionHandler(self.request!, self.response, responseJSON, response.result.error)
})
}
}
}
}
I'm not sure if this is compatible with older versions, but I suggest you rewrite your extensions as this:
extension DataRequest {
public func responseSwiftyJSON(queue: DispatchQueue? = nil, options: JSONSerialization.ReadingOptions = .allowFragments, completionHandler: #escaping (URLRequest?, HTTPURLResponse?, SwiftyJSON.JSON, Error?) -> Void) -> Self {
return responseJSON(queue: queue, options: options) {
let responseJSON: JSON
switch result {
case let .success(json): responseJSON = JSON(json)
case .failure: responseJSON = JSON.null
}
completionHandler(request, response, responseJSON, error)
}
}
}
I have very-very strange things.
In my simple function I create variable which contains dictionary of settings parameters. It is set as 'let', so inner loop just reads it.
In a random moment of loop time it crashes with "unresolved settings".
It seems like smth makes it nil. Who does it?
private static func preferencesFilter(userIDs: [Int], access: String) -> [User] {
self.sharedInstance.delegate?.updateActionLabel(label: "Filter")
var result = [VKUser]()
let settings = self.parseSettings()
let progressFraction = 1.00 / Float(userIDs.count)
var n = 0
for userID in userIDs {
if sharedInstance.stopped {
return []
}
n += 1
let user = VKUser.getUser(id: userID, access_token: access_token)
if settings["gender"] != nil {
if user.sex == settings["gender"] as! String {
if (user.born?.isBetweeen(date1: settings["minAge"] as! Date, date2: settings["maxAge"] as! Date))! {
if settings["country"] != nil {
if user.country == settings["country"] as! String {
result.append(user)
}
}
else {
result.append(user)
}
}
}
}
else {
if (user.born?.isBetweeen(date1: settings["minAge"] as! Date, date2: settings["maxAge"] as! Date))! {
if settings["country"] != nil {
if user.country == settings["country"] as! String {
result.append(user)
}
}
else {
result.append(user)
}
}
}
self.sharedInstance.delegate?.updateProgress(value: Float(n) * progressFraction)
}
return result
}
I refactored your code into something more swift like:
private static func preferencesFilter(userIDs: [Int], access_token: String) -> [User]? {
guard userIDs.count > 0 else {
return [User]() // no input, return empty list
}
let settings = self.parseSettings()
guard let minAge = settings["minAge"] as? Date,
let maxAge = settings["maxAge"] as? Date
else {
return nil
}
let country = settings["country"] as? String // specified or nil
let gender = settings["gender"] as? String // specified or nil
sharedInstance.delegate?.updateActionLabel(label: "Filter")
var result = [VKUser]()
let progressFraction = 1.00 / Float(userIDs.count)
var n = 0
for userID in userIDs {
if !sharedInstance.stopped {
n += 1
let user = VKUser.getUser(id: userID, access_token: access_token)
var shouldInclude = true
if user.sex != gender { // wrong sex or no required gender specified
shouldInclude = false
}
if user.country != country { // wrong country or no required country specified
shouldInclude = false
}
if let born = user.born {
if !born.isBetweeen(date1: minAge, date2: maxAge) {
shouldInclude = false
}
} else { // no user.born date, cant check if in range
shouldInclude = false
}
if shouldInclude {
result.append(user)
}
sharedInstance.delegate?.updateProgress(value: Float(n) * progressFraction)
}
}
return result
}
Is that what you intended to write? How is that running for you?
Can you change this into a non-static method? Makes more sense to me.
You can see it returns an optional now, since the method might fail with a nil. Your calling code should handle that correctly.
I have a data loading function that sometimes needs to update the ui via callback:
class Middleware {
...
func loadChannels(callback: #escaping (_ middleware: Middleware) -> Void) {
let url = URL(string: uri + "/entity.json")!
let task = URLSession.shared.dataTask(with: url) {
(data, response, error) in
guard let data = data, let _:URLResponse = response , error == nil else {
print("> loadChannels " + error.debugDescription)
return
}
let json = JSON(data: data)
if (json["entities"].array != nil) {
self.channels.removeAll()
for (_, json) in json["entities"] {
if let channel = Channel(json: json) {
self.channels += [channel]
}
}
if (callback != nil) {
callback!(self)
}
}
}
task.resume()
}
}
When I don't need the callback I'm calling it like this:
middleware.loadChannels(callback: { (middleware: Middleware) -> Void in })
Using
middleware.loadChannels(callback: nil)
doesn't work:
Nil is not compatible with expected argument type '(Middleware) -> Void'
Is there a less verbose way than my current version?
You need to make the callback optional:
func loadChannels(callback: ((_ middleware: Middleware) -> Void)?) {
}
Now you can pass nil to the callback parameter.