I have the following classes that perform a network call -
import SwiftUI
import Combine
struct CoinsView: View {
private let coinsViewModel = CoinViewModel()
var body: some View {
Text("CoinsView").onAppear {
self.coinsViewModel.fetchCoins()
}
}
}
class CoinViewModel: ObservableObject {
private let networkService = NetworkService()
#Published var data = String()
var cancellable : AnyCancellable?
func fetchCoins() {
cancellable = networkService.fetchCoins().sink(receiveCompletion: { _ in
print("inside receive completion")
}, receiveValue: { value in
print("received value - \(value)")
})
}
}
class NetworkService: ObservableObject {
private var urlComponents : URLComponents {
var components = URLComponents()
components.scheme = "https"
components.host = "jsonplaceholder.typicode.com"
components.path = "/users"
return components
}
var cancelablle : AnyCancellable?
func fetchCoins() -> AnyPublisher<Any, URLError> {
return URLSession.shared.dataTaskPublisher(for: urlComponents.url!)
.map{ $0.data }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
What I want to achieve currently is just to print the JSON result.
This doesn't seem to work, and from debugging it never seems to go inside the sink{} method, therefor not executing it.
What am I missing?
After further investigation with Asperi's help I took the code to a clean project and saw that I have initialized a struct that wraps NSPersistentContainer which causes for some reason my network requests not to work. Here is the code, hopefully someone can explain why it prevented my networking to execute -
import SwiftUI
#main
struct BasicApplication: App {
let persistenceController = BasicApplciationDatabase.instance
#Environment(\.scenePhase)
var scenePhase
var body: some Scene {
WindowGroup {
CoinsView()
}
.onChange(of: scenePhase) { newScenePhase in
switch newScenePhase {
case .background:
print("Scene is background")
persistenceController.save()
case .inactive:
print("Scene is inactive")
case .active:
print("Scene is active")
#unknown default:
print("Scene is unknown default")
}
}
}
}
import CoreData
struct BasicApplciationDatabase {
static let instance = BasicApplciationDatabase()
let container : NSPersistentContainer
init() {
container = NSPersistentContainer(name: "CoreDataDatabase")
container.loadPersistentStores { NSEntityDescription, error in
if let error = error {
fatalError("Error: \(error.localizedDescription)")
}
}
}
func save(completion : #escaping(Error?) -> () = {_ in} ){
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
completion(nil)
} catch {
completion(error)
}
}
}
func delete(_ object: NSManagedObject, completion : #escaping(Error?) -> () = {_ in} ) {
let context = container.viewContext
context.delete(object)
save(completion: completion)
}
}
Related
I am very new in iOS development unit testing.
I have a view model as below
class PostsViewViewModel {
private let serviceRequest: NetworkRequestProtocol
public private(set) var requestOutput: PassthroughSubject<RequestOutput, Never> = .init()
private var cancellables = Set<AnyCancellable>()
init(
request: NetworkRequestProtocol,
user: LoginUserModel,
codeDataManager: CoreDataManagerProtocol) {
serviceRequest = request
loadPostsFromServerFor(user: user)
}
private func loadPostsFromServerFor(user: LoginUserModel) {
Task {
do {
let postsRecived = try await serviceRequest.callService(
with: ServiceEndPoint.fetchPostsForUser(id: user.userid),
model: [PostModel].self,
serviceMethod: .get
)
if postsRecived.isEmpty {
requestOutput.send(.fetchPostsDidSucceedWithEmptyList)
} else {
recievedRawPostsModel = postsRecived
createPostModelsFromPostRecieved(postsRecived)
requestOutput.send(.fetchPostsDidSucceed)
}
} catch {
requestOutput.send(.fetchPostsDidFail)
}
}
}
}
extension PostsViewViewModel {
enum RequestOutput {
case fetchPostsDidFail
case fetchPostsDidSucceed
case fetchPostsDidSucceedWithEmptyList
case reloadPost
}
}
Now I created a test class of ViewModel as below
final class PostViewViewModelTest: XCTestCase {
let userInput: PassthroughSubject<PostsViewViewModel.UserInput, Never> = .init()
private var cancellable = Set<AnyCancellable>()
private let mockUser = LoginUserModel(userid: 1)
private let coreDataManager = CoreDataStackInMemory()
private var sutPostViewModel: PostsViewViewModel!
override func setUp() {
sutPostViewModel = PostsViewViewModel(
request: MockNetworkRequestPostSuccess(),
user: mockUser, codeDataManager: coreDataManager
)
}
override func tearDown() {
sutPostViewModel = nil
}
func testPostsViewViewModel_WhenPostModelLoaded_NumberOfRowsSouldBeMoreThanZero() {
// let postViewModel = sutPostViewModel!
self.sutPostViewModel.requestOutput
//.receive(on: DispatchQueue.main)
.sink { [weak self] output in
XCTAssertTrue(output == .fetchPostsDidSucceed)
XCTAssertTrue((self?.sutPostViewModel.numberOfRowsInPostTableView)! > 0)
}
.store(in: &cancellable)
}
func testPostsViewViewModel_WhenPostModelLoaded_GetPostAtGivenIndexPathMustHaveEqualPostID() {
// let postViewModel = sutPostViewModel!
self.sutPostViewModel.requestOutput
.receive(on: DispatchQueue.main)
.sink { [weak self] output in
print(output == .reloadPost)
let post = self?.sutPostViewModel.getPost(at: IndexPath(row: 0, section: 0))
let postModel: [PostModel] = JSONLoader.load("Posts.json")
XCTAssertTrue(post.postID == postModel[0].id)
}
.store(in: &cancellable)
}
}
But test cases get crashed while access sutPostViewModel. I am unable to understand what am I doing wrong here.
While debugging I found tearDown() is being called before sink and test crash.
I think you might need to use an expectation.
func testPostsViewViewModel_WhenPostModelLoaded_NumberOfRowsSouldBeMoreThanZero() {
let expectation = expectation(description: "Sink Executed") // 1.
self.sutPostViewModel.requestOutput
.sink { [weak self] output in
XCTAssertTrue(output == .fetchPostsDidSucceed)
XCTAssertTrue((self?.sutPostViewModel.numberOfRowsInPostTableView)! > 0)
expectation.fulfill() //2. Fulfill to stop waiting
}
.store(in: &cancellable)
wait(for: [expectation], timeout: 5) // 3. Wait for 5 seconds before timeout and failure
}
You have asynchronous code so sink is executed after your test method runs. At that point tearDown has been called and your sut set to nil
I have a problem with fetching data from API. The DayViewCalendar is creating View before events data is fetched from API.
My main view is in SwiftUI
struct CalendarScreen: View {
#StateObject private var viewModel: ViewModel = ViewModel()
var body: some View {
NavigationView {
ZStack(alignment: .trailing) {
CalendarKitDisplayView(viewModel: viewModel)
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
I have a ViewModel which is fetching events data from API
import Combine
import Foundation
extension NSNotification.Name {
static let onEventLoaded = Notification.Name("onEventLoaded")
}
extension CalendarScreen {
class ViewModel: ObservableObject {
let calendarService = CalendarService()
#Published var calendarEvents: [CalendarEvent]
var cancellable: AnyCancellable?
init() {
self.calendarEvents = [CalendarEvent()]
}
func fetchCalendarEvents() {
cancellable = calendarService.getEvents()
.sink(
receiveCompletion: { _ in },
receiveValue: {
calendarEvents in self.calendarEvents = calendarEvents
NotificationCenter.default.post(name: .onEventLoaded, object: nil)
})
}
}
}
Calendar Service is just a service for singletion of repository
import Foundation
import Combine
struct CalendarService {
private var calendarRepository = CalendarRepository()
func getEvents() -> AnyPublisher<[CalendarEvent], Error> {
return calendarRepository.getEvents()
}
}
And calendarRepository is just simple URL Request for my API
import Combine
struct CalendarRepository {
private let agent = Agent()
private let calendarurl = "\(api)/calendars_events"
func getEvents() -> AnyPublisher<[CalendarEvent], Error>{
let urlString = "\(calendarurl)"
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("Bearer \(AuthManager.shared.token)", forHTTPHeaderField: "Authorization")
return agent.run(request)
}
}
Agent is handling the request
class Agent {
let session = URLSession.shared
var cancelBag: Set<AnyCancellable> = []
func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<T, Error> {
return session
.dataTaskPublisher(for: request)
.decode(type: T.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
Everything is going in CalendarViewController from CalendarKit library which stands as follow:
import SwiftUI
import UIKit
class CalendarViewController: DayViewController {
convenience init(viewModel: CalendarScreen.ViewModel) {
self.init()
self.viewModel = viewModel
}
var viewModel = CalendarScreen.ViewModel()
var refresh: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
subscribeToNotification()
}
func subscribeToNotification() {
NotificationCenter.default.addObserver(
self, selector: #selector(eventChanged(_:)), name: .onDataImported, object: nil)
}
#objc func eventChanged(_ notification: Notification) {
print("notification")
reloadData()
}
override func eventsForDate(_ date: Date) -> [EventDescriptor] {
// HOW CAN I WAIT FOR THIS LINE TO FINISH FETCH DATA FROM API
viewModel.fetchCalendarEvents()
//
let calendarKitEvents = viewModel.calendarEvents.filter {
dateTimeFormat.date(from: $0.start) ?? Date() >= date
&& dateTimeFormat.date(from: $0.end) ?? Date() <= date
}.map { item in
let event = Event()
event.dateInterval = DateInterval(
start: self.dateTimeFormat.date(from: item.start) ?? Date(),
end: self.dateTimeFormat.date(from: item.end) ?? Date())
event.color = UIColor(InvoiceColor(title: item.title))
event.isAllDay = false
event.text = item.title
return event
}
return calendarKitEvents
}
let dateTimeFormat: DateFormatter = {
let df = DateFormatter()
df.locale = Locale(identifier: "pl")
df.timeZone = TimeZone(abbreviation: "CET")
df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return df
}()
}
And the SwiftUI and UIKit is bridged by UIViewControllerRepresntable
import SwiftUI
import UIKit
struct CalendarKitDisplayView: UIViewControllerRepresentable {
#ObservedObject var viewModel: CalendarScreen.ViewModel
func makeUIViewController(context: Context) -> DayViewController {
let dayViewCalendar = CalendarViewController(viewModel: viewModel)
return dayViewCalendar
}
func updateUIViewController(_ uiViewController: DayViewController, context: Context) {
}
}
And the entity CalendarEvent which is coded to CalendarKit event
public struct CalendarEvent: Codable, Identifiable {
public var id: Int = 0
var title: String = ""
var start: String = ""
var end: String = ""
var note: String?
}
My goal is to wait for viewModel.fetchCalendarEvents() to fetch data from API and then start other tasks.
override func eventsForDate(_ date: Date) -> [EventDescriptor] {
// HOW CAN I WAIT FOR THIS LINE TO FINISH FETCH DATA FROM API
viewModel.fetchCalendarEvents()
//
I tried to implement NotificationCenter with variable refresh but when i added and changed functions
To the CalendarViewController variable var refresh: Bool = false and push notification to ViewModel
func fetchCalendarEvents() {
cancellable = calendarService.getEvents()
.sink(
receiveCompletion: { _ in },
receiveValue: {
calendarEvents in self.calendarEvents = calendarEvents
NotificationCenter.default.post(name: .eventChanged, object: nil)
})
}
After that i added subscribe to event in init() function in my CalendarViewController and #selector as follow
#objc func eventChanged(_ notification: Notification) {
print("notification")
refresh = true
reloadData()
}
I tried to add but it stay in infinite loop and variable never change
override func eventsForDate(_ date: Date) -> [EventDescriptor] {
viewModel.fetchCalendarEvents()
while refresh == true {
}
}
I was thinking about using conclusion or completion handler but i am new in Swift programming and dont really know how it should looks like.
Using a completion handler your function should look like this:
func fetchCalendarEvents(_ completion: #escaping () -> Void) {
cancellable = calendarService.getEvents()
.sink(
receiveCompletion: { _ in },
receiveValue: {
calendarEvents in self.calendarEvents = calendarEvents
NotificationCenter.default.post(name: .eventChanged, object: nil)
completion()
})
}
And when calling it:
fetchCalendarEvents {
//finished, run some code.
}
In the code below you can see the error and the code used in my xxApp.swift folder.
I was creating Sign In and a Sign Up function, and everything worked very well, but then I got this error when clicking on the register button.
If you need more code just let me know, thanks in advance!
Here is the code :
import SwiftUI
import Firebase
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}`final class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
#main -> "Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffee1a57ff0)"
struct PSMAApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
#StateObject var sessionService = SessionServiceImpl()
var body: some Scene {
WindowGroup {
NavigationView{
switch sessionService.state {
case .loggedIn:
HomeView()
.environmentObject(sessionService)
case .loggedOut:
LoginView()
}
}
}
}
}
Here is the register Button - ViewModel :
import Foundation
import Combine
enum RegistrationState {
case successfull
case failed(error: Error)
case na
}
protocol RegistrationViewModel {
func register()
var hasError: Bool { get }
var service: RegistrationService { get }
var state: RegistrationState { get }
var userDetails: RegistrationDetails { get }
init(service: RegistrationService)
}
final class RegistrationViewModelImpl: ObservableObject, RegistrationViewModel {
#Published var hasError: Bool = false
#Published var state: RegistrationState = .na
let service: RegistrationService
var userDetails: RegistrationDetails = RegistrationDetails.new
private var subscriptions = Set<AnyCancellable>()
init(service: RegistrationService) {
self.service = service
setupErrorSubscriptions()
}
func register() {
service
.register(with: userDetails)
.sink { [weak self] res in
switch res {
case .failure(let error):
self?.state = .failed(error: error)
default: break
}
} receiveValue: { [weak self] in
self?.state = .successfull
}
.store(in: &subscriptions)
}
}
private extension RegistrationViewModelImpl {
func setupErrorSubscriptions() {
$state
.map { state -> Bool in
switch state {
case .successfull,
.na:
return false
case .failed:
return true
}
}
.assign(to: &$hasError)
}
}
Here is Register - RegistrationService :
import Combine
import Foundation
import Firebase
import FirebaseDatabase
enum RegistrationKeys: String {
case username
}
protocol RegistrationService {
func register(with details: RegistrationDetails) -> AnyPublisher<Void, Error>
}
final class RegistrationServiceImpl: RegistrationService {
func register(with details: RegistrationDetails) -> AnyPublisher<Void, Error> {
Deferred {
Future { promise in
Auth.auth()
.createUser(withEmail: details.email,
password: details.password) {res, error in
if let err = error {
promise(.failure(err))
} else {
if let uid = res?.user.uid {
let values = [
RegistrationKeys.username.rawValue: details.username] as [String : Any]
Database.database()
.reference()
.child("users")
.child(uid)
.updateChildValues(values) {
error, ref in
if let err = error {
promise(.failure(err))
} else {
promise(.success(()))
}
}
} else {
promise(.failure(NSError(domain: "Invalid user ID", code: 0, userInfo: nil)))
}
}
}
}
}
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}
Here is Registraion - SessionService :
import Combine
import Foundation
import FirebaseAuth
enum SessionState {
case loggedIn
case loggedOut
}
protocol SessionService {
var state: SessionState { get }
var userDetails: SessionUserDetails? { get }
func logout()
}
final class SessionServiceImpl: ObservableObject, SessionService {
#Published var state: SessionState = .loggedOut
#Published var userDetails: SessionUserDetails?
private var handler: AuthStateDidChangeListenerHandle?
init() {
setupFirebaseAuthHandler()
}
func logout() {
try? Auth.auth().signOut()
}
}
private extension SessionServiceImpl {
func setupFirebaseAuthHandler() {
handler = Auth
.auth()
.addStateDidChangeListener { [weak self] res, user in
guard let self = self else { return }
self.state = user == nil ? .loggedOut : .loggedIn
if let uid = user?.uid {
self.handleRefresh(with: uid)
}
}
}
func handleRefresh(with uid: String) {
Database
.database()
.reference()
.child("users")
.child(uid)
.observe(.value) { [weak self] snapshot in
guard let self = self,
let value = snapshot.value as? NSDictionary,
let username = value[RegistrationKeys.username.rawValue] as? String else{
return
}
DispatchQueue.main.async {
self.userDetails = SessionUserDetails(username: username)
}
}
}
}
Here is the Register - Model :
struct RegistrationDetails {
var email: String
var username: String
var password: String
}
extension RegistrationDetails {
static var new: RegistrationDetails {
RegistrationDetails(email: "",
username: "",
password: "")
}
}
Here is the Registration - View :
import SwiftUI
struct RegisterView: View {
#StateObject private var vm = RegistrationViewModelImpl(
service: RegistrationServiceImpl()
)
var body: some View {
NavigationView {
VStack(spacing: 32) {
VStack(spacing: 16) {
InputTextFieldView(text: $vm.userDetails.email,
placeholder: "Email",
keyboardType: .emailAddress,
sfSymbol: "envelope")
InputPasswordView(password: $vm.userDetails.password,
placeholder: "Password",
sfSymbol: "lock")
Divider()
InputTextFieldView(text: $vm.userDetails.username,
placeholder: "Username",
keyboardType: .namePhonePad,
sfSymbol: nil)
}
ButtonComponentView(title: "Sign up") {
vm.register()
}
}
.padding(.horizontal, 15)
.navigationTitle("Register")
.applyClose()
.alert(isPresented: $vm.hasError,
content: {
if case .failed(let error) = vm.state {
return Alert(
title: Text("Error"),
message: Text(error.localizedDescription))
} else {
return Alert(
title: Text("Error"),
message: Text("Something went wrong"))
}
})
}
}
}
struct RegisterView_Previews: PreviewProvider {
static var previews: some View {
RegisterView()
.preferredColorScheme(.dark)
}
}
I have the following code, How can i accomplish this without changing struct into class. Escaping closure captures mutating 'self' parameter,
struct RegisterView:View {
var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
self.names = name //here is the error
}){(error) in
print("Error: \(error)")
}
init(){
LoadPerson()
}a
var body:some View{
//ui code
}
}
Firebasemanager.swift
struct FirebaseManager {
func fetchPerson(
success: #escaping (Person) -> (),
failure: #escaping (String) -> ()
) {
Database.database().reference().child("Person")
.observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
success(Person(dictionary: dictionary))
}
}) { (error) in
failure(error.localizedDescription)
}
}
}
SwiftUI view can be created (recreated) / copied many times during rendering cycle, so View.init is not appropriate place to load some external data. Use instead dedicated view model class and load explicitly only when needed.
Like
class RegisterViewModel: ObservableObject {
#Published var names = [String]()
func loadPerson() {
// probably it also worth checking if person has already loaded
// guard names.isEmpty else { return }
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
struct RegisterView: View {
// in SwiftUI 1.0 it is better to inject view model from outside
// to avoid possible recreation of vm just on parent view refresh
#ObservedObject var vm: RegisterViewModel
// #StateObject var vm = RegisterViewModel() // << only SwiftUI 2.0
var body:some View{
Some_Sub_View()
.onAppear {
self.vm.loadPerson()
}
}
}
Make the names property #State variable.
struct RegisterView: View {
#State var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success: { person in
guard let name = person.name else { return }
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
//...
}
I'm trying to understand the Combine methodology of making a JSON network call. I'm
clearly missing something basic.
The closest I get fails with the URLSession cancelled.
class NoteDataStore: ObservableObject {
#Published var notes: [MyNote] = []
init() {
getWebserviceNotes()
}
func getWebserviceNotes() {
let pub = Webservice().fetchNotes()
.sink(receiveCompletion: {_ in}, receiveValue: { (notes) in
self.notes = notes
})
}
}
}//class
The data element:
struct MyNote: Codable, Identifiable {
let id = UUID()
var title: String
var url: String
var thumbnailUrl: String
static var placeholder: MyNote {
return MyNote(title: "No Title", url: "", thumbnailUrl: "")
}
}
The network setup:
class Webservice {
func fetchNotes() -> AnyPublisher<[MyNote], Error> {
let url = "https://jsonplaceholder.typicode.com/photos"
guard let notesURL = URL(string: url) else { fatalError("The URL is broken")}
return URLSession.shared.dataTaskPublisher(for: notesURL)
.map { $0.data }
.decode(type: [MyNote].self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}
The console output is:
Task <85208F00-BC24-44AA-B644-E0398FE263A6>.<1> finished with error
[-999] Error Domain=NSURLErrorDomain Code=-999 "cancelled"
UserInfo={NSErrorFailingURLStringKey=https://jsonplaceholder.typicode.com/photos,
NSLocalizedDescription=cancelled,
NSErrorFailingURLKey=https://jsonplaceholder.typicode.com/photos}
Any guidance would be appreciated. Xcode 11.4
let pub = Webservice().fetchNotes()
this publisher is released on exit of scope, so make it member, like
private var publisher: AnyPublisher<[MyNote], Error>?
func getWebserviceNotes() {
self.publisher = Webservice().fetchNotes()
...
Based on Asperi's answer - you will also want to add:
var cancellable: AnyCancellable?
And then you can sink to get the data:
func getWebserviceNotes() {
self.publisher = Webservice().fetchNotes()
guard let pub = self.publisher else { return }
cancellable = pub
.sink(receiveCompletion: {_ in },
receiveValue: { (notes) in
self.notes = notes
})
}