I'm getting an API Result in my viewModel as below:
class HomePageViewModel {
var apiResult: CountryDataFromAPI?
//Getting API result via viewModel
public func getAPIResult(withOffset: Int, completion: #escaping () -> Void) {
APIHandler.urlRequest(with: withOffset) { result in
self.apiResult?.data.append(contentsOf: result.data)
print("api resultdata in viewmodel is \(result.data)")
completion()
}
}
}
Getting the api works fine. In the code above, I can print the statement as print("api resultdata in viewmodel is (result.data)") and see the expected api result.
When I get the data inside my viewModel as above, I use it in my mainViewController.
My mainViewController is initialized with the ViewModel
class HomePageViewController: UIViewController {
private var viewModel : HomePageViewModel
private var countryDataArray: [CountryData]?
init(with viewModel: HomePageViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
}
Everything works fine until I try to get the data from viewModel as below.
I'm trying to set the data in ViewModel to a parameter called countryDataArray. But while doing that inside the closure, my print statement "print("data in vc is (self?.viewModel.apiResult?.data)")" prints out nil even though it gets the data in viewModel.
override func viewDidLoad() {
super.viewDidLoad()
//Getting API Result in viewDidLoad. And after getting the result, reloading the tableView.
self.viewModel.getAPIResult(withOffset: 11) { [weak self] in
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self?.countryDataArray = self?.viewModel.apiResult?.data
print("data in vc is \(self?.viewModel.apiResult?.data)")
self?.countriesListTableView.reloadData()
})
}
}
Why this might be happening?
PS: My api models are as below:
struct CountryDataFromAPI: Codable {
var data: [CountryData]
}
struct CountryData: Codable,Equatable {
let name : String
let code: String
let wikiDataId: String
}
In my opinion, the problem is the self.apiResult?.data.append(contentsOf: result.data)
You didn't assign any value for the apiResult.data. Because apiResult is nil.
You have to initialize the apiResult after you get data back from API.
Related
During the development of a swiftUI project, I wish to have option to switch the way data is set, either
Allowing API calls to be made as normal which sets an #Published variable, or
Set the #published variable from mocked file and NOT make the api call.
The reason is that I am limited to the number of api calls per minute.
In my example below I load the mocked data in a model called "Person".
Current solution
Set a global variable to distinguish between the two above mentioned states.
In all places where api calls were be made, I introduce a condition to optionally use mocked data and not make the api call. See .task in MyView
struct GlobalConstants {
static let use_mock_data = true
}
class ViewModel: ObservableObject {
#Published var data: [Person] = []
#MainActor
func fetchData() async {
// ... data is set in this code
}
}
Within Person model, I set a static variable that returns the decoded mock data from a json file. The decode method below is an extension to Bundle (Thanks Paul Hudsen).
extension Person {
static var mockPersons: [Person] {
Bundle.main.decode([Person].self, from: "persons.json")
}
}
struct MyView: View {
#StateObject var vm = PersonViewModel()
var body: some View {
NavigationView {
List {
ForEach(vm.data) { d in
NavigationLink {
OtherView(prop: d.detail)
} label: {
Text(d.name)
}
}
}
.task { // -----condition--------------------- //
if GlobalConstants.use_mock_data {
vm.data = Person.mockPersons
} else {
await vm.fetchData()
}
}
}
}
}
Question
What other approaches can I consider for enabling the two states? Overriding the methods in some way?
I am still on the learning curve to swift and wondering if theres a better way to enable long term maintenance in a clean and predictable way.
As fetching the data belongs to the view model my suggestion is to put the condition into fetchData.
Marking a method as async doesn't require necessarily that the executed code is asynchronous
#MainActor
class ViewModel: ObservableObject {
#Published var people: [Person] = []
func fetchData() async {
if GlobalConstants.use_mock_data {
people = Person.mockPersons
} else {
// people = await callTheAPI()
}
}
}
and replace the task in the view with
.task {
await vm.fetchData()
}
Note: people is a more meaningful name than data 😃
You should use a Service with a protocol, and then create 2 services extending the protocol, one for mock and one for data, here are some examples from one of my apps (using alamofire, but you can modify it to use it with URLSession or anything else) :
import Alamofire
class NetworkManager {
func get<T: Decodable>(url: URLConvertible, parameters: Parameters?,_ completion: #escaping (DataResponse<T, AFError>) -> Void) {
AF.request(url, parameters: parameters)
.responseDecodable(of: T.self) { response in
completion(response)
}
}
}
protocol SearchServicing {
var parameters: Parameters? { get set }
func get(_ completion: #escaping (Result<SearchResponse, AFError>) -> Void)
}
class SearchService: NetworkManager, SearchServicing {
var parameters: Parameters?
func get(_ completion: #escaping (Result<SearchResponse, AFError>) -> Void) {
let url = "YOUR_URL"
get(url: url, parameters: parameters) { response in
completion(response.result)
}
}
}
class SearchMockService: SearchServicing {
var parameters: Parameters?
var getCallCounter = 0
func get(_ completion: #escaping (Result<SearchResponse, AFError>) -> Void) {
getCallCounter += 1
let response = SearchResponse(recipes: [
Recipe.mock,
Recipe.mock,
Recipe.mock,
])
completion(.success(response))
}
}
Then add it in the ViewModel :
#MainActor
class RecipeViewModel: ObservableObject {
...
private var service: SearchServicing
init(service: SearchServicing) {
self.service = service
}
public func fetchData() {
[...]
service.parameters = ["q": ingredients.map { $0.name }.joined(separator: " ")]
service.get { [weak self] result in
switch result {
case .success(let searchResponse):
self?.totalResults = searchResponse.count
self?.results = searchResponse.recipes
case .failure(let error):
dump(error)
self?.error = AppError(error: error)
}
}
}
}
And so it allows me to do :
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView(recipeViewModel: RecipeViewModel(service: SearchMockService()))
}
}
whenever I need to test my view model and
struct MainView: View {
#ObservedResults(RecipeEntity.self) var favorites
#StateObject var recipeViewModel = RecipeViewModel(service: SearchService())
var body: some View {
...
}
}
for non mock datas service
For more informations checkout this video, it helped me to use this structure : How to implement a Mock in Swift!
I am looking for a way to use CoreData Objects using MVVM (ditching #FetchRequest). After experimenting, I have arrived at the following implementation:
Package URL: https://github.com/TimmysApp/DataStruct
Datable.swift:
protocol Datable {
associatedtype Object: NSManagedObject
//MARK: - Mapping
static func map(from object: Object) -> Self
func map(from object: Object) -> Self
//MARK: - Entity
var object: Object {get}
//MARK: - Fetching
static var modelData: ModelData<Self> {get}
//MARK: - Writing
func save()
}
extension Datable {
static var modelData: ModelData<Self> {
return ModelData()
}
func map(from object: Object) -> Self {
return Self.map(from: object)
}
func save() {
_ = object
let viewContext = PersistenceController.shared.container.viewContext
do {
try viewContext.save()
}catch {
print(String(describing: error))
}
}
}
extension Array {
func model<T: Datable>() -> [T] {
return self.map({T.map(from: $0 as! T.Object)})
}
}
ModelData.swift:
class ModelData<T: Datable>: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
var publishedData = CurrentValueSubject<[T], Never>([])
private let fetchController: NSFetchedResultsController<NSFetchRequestResult>
override init() {
let fetchRequest = T.Object.fetchRequest()
fetchRequest.sortDescriptors = []
fetchController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: PersistenceController.shared.container.viewContext, sectionNameKeyPath: nil, cacheName: nil)
super.init()
fetchController.delegate = self
do {
try fetchController.performFetch()
publishedData.value = (fetchController.fetchedObjects as? [T.Object] ?? []).model()
}catch {
print(String(describing: error))
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let data = controller.fetchedObjects as? [T.Object] else {return}
self.publishedData.value = data.model()
}
}
Attempt.swift:
struct Attempt: Identifiable, Hashable {
var id: UUID?
var password: String
var timestamp: Date
var image: Data?
}
//MARK: - Datable
extension Attempt: Datable {
var object: AttemptData {
let viewContext = PersistenceController.shared.container.viewContext
let newAttemptData = AttemptData(context: viewContext)
newAttemptData.password = password
newAttemptData.timestamp = timestamp
newAttemptData.image = image
return newAttemptData
}
static func map(from object: AttemptData) -> Attempt {
return Attempt(id: object.aid ?? UUID(), password: object.password ?? "", timestamp: object.timestamp ?? Date(), image: object.image)
}
}
ViewModel.swift:
class HomeViewModel: BaseViewModel {
#Published var attempts = [Attempt]()
required init() {
super.init()
Attempt.modelData.publishedData.eraseToAnyPublisher()
.sink { [weak self] attempts in
self?.attempts = attempts
}.store(in: &cancellables)
}
}
So far this is working like a charm, however I wanted to check if this is the best way to do it, and improve it if possible. Please note that I have been using #FetchRequest with SwiftUI for over a year now and decided to move to MVVM since I am using it in all my Storyboard projects.
For a cutting edge way to wrap the NSFetchedResultsController in SwiftUI compatible code you might want to take a look at AsyncStream.
However, #FetchRequest currently is implemented as a DynamicProperty so if you did that too it would allow access the managed object context from the #Environment in the update func which is called on the DynamicProperty before body is called on the View. You can use an #StateObject internally as the FRC delegate.
Be careful with MVVM because it uses objects where as SwiftUI is designed to work with value types to eliminate the kinds of consistency bugs you can get with objects. See the doc Choosing Between Structures and Classes. If you build an MVVM object layer on top of SwiftUI you risk reintroducing those bugs. You're better off using the View data struct as it's designed and leave MVVM for when coding legacy view controllers. But to be perfectly honest, if you learn the child view controller pattern and understand the responder chain then there is really no need for MVVM view model objects at all.
And FYI, when using Combine's ObservableObject we don't sink the pipeline or use cancellables. Instead, assign the output of the pipeline to an #Published. However, if you aren't using CombineLatest, then perhaps reconsider if you should really be using Combine at all.
I'm new in the RxSwift development and I've an issue while presentation a view controller.
My MainViewController is just a table view and I would like to present detail when I tap on a item of the list.
My DetailViewController is modally presented and needs a ViewModel as input parameter.
I would like to avoid to dismiss the DetailViewController, I think that the responsability of dismiss belongs to the one who presented the view controller, i.e the dismiss should happen in the MainViewController.
Here is my current code
DetailsViewController
class DetailsViewController: UIViewController {
#IBOutlet weak private var doneButton: Button!
#IBOutlet weak private var label: Label!
let viewModel: DetailsViewModel
private let bag = DisposeBag()
var onComplete: Driver<Void> {
doneButton.rx.tap.take(1).asDriver(onErrorJustReturn: ())
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
bind()
}
private func bind() {
let ouput = viewModel.bind()
ouput.id.drive(idLabel.rx.text)
.disposed(by: bag)
}
}
DetailsViewModel
class DetailsViewModel {
struct Output {
let id: Driver<String>
}
let item: Observable<Item>
init(with vehicle: Observable<Item>) {
self.item = item
}
func bind() -> Output {
let id = item
.map { $0.id }
.asDriver(onErrorJustReturn: "Unknown")
return Output(id: id)
}
}
MainViewController
class MainViewController: UIViewController {
#IBOutlet weak private var tableView: TableView!
private var bag = DisposeBag()
private let viewModel: MainViewModel
private var detailsViewController: DetailsViewController?
override func viewDidLoad(_ animated: Bool) {
super.viewDidLoad(animated)
bind()
}
private func bind() {
let input = MainViewModel.Input(
selectedItem: tableView.rx.modelSelected(Item.self).asObservable()
)
let output = viewModel.bind(input: input)
showItem(output.selectedItem)
}
private func showItem(_ item: Observable<Item>) {
let viewModel = DetailsViewModel(with: vehicle)
detailsViewController = DetailsController(with: viewModel)
item.flatMapFirst { [weak self] item -> Observable<Void> in
guard let self = self,
let detailsViewController = self.detailsViewController else {
return Observable<Void>.never()
}
self.present(detailsViewController, animated: true)
return detailsViewController.onComplete.asObservable()
}
.subscribe(onNext: { [weak self] in
self?.detailsViewController?.dismiss(animated: true)
self?.detailsViewController? = nil
})
.disposed(by: bag)
}
}
MainViewModel
class MainViewModel {
struct Input {
let selectedItem: Observable<Item>
}
struct Output {
let selectedItem: Observable<Item>
}
func bind(input: Input) -> Output {
let selectedItem = input.selectedItem
.throttle(.milliseconds(500),
latest: false,
scheduler: MainScheduler.instance)
.asObservable()
return Output(selectedItem: selectedItem)
}
}
My issue is on showItem of MainViewController.
I still to think that having the DetailsViewController input as an Observable isn't working but from what I understand from Rx, we should use Observable as much as possible.
Having Item instead of Observable<Item> as input could let me use this kind of code:
item.flatMapFirst { item -> Observable<Void> in
guard let self = self else {
return Observable<Void>.never()
}
let viewModel = DetailsViewModel(with: item)
self.detailsViewController = DetailsViewController(with: viewModel)
guard let detailsViewController = self.detailsViewController else {
return Observable<Void>.never()
}
present(detailsViewController, animated: true)
return detailsViewController
}
.subscribe(onNext: { [weak self] in
self?.detailsViewController?.dismiss(animated: true)
self?.detailsViewController = nil
})
.disposed(by: bag)
What is the right way to do this?
Thanks
You should not "use Observable as much as possible." If an object is only going to ever have to deal with a single item, then just pass the item. For example if a label is only ever going to display "Hello World" then just assign the string to the label's text property. Don't bother wrapping it in a just and binding it to the label's rx.text.
Your second option is much closer to what you should have. It's a fine idea.
You might find my CLE library interesting. It takes care of the issue you are trying to handle here.
Hello I am trying to do a project with RxSwift and I am stuck trying to do in a properly way the connection between the Coordinator and the ViewModel.
Goal
Using observables, the Coordinator receives and event (in that case, when a row has been tapped) then does whatever.
Scenario
Giving a Post (String)
typealias Post = String
I have the following Coordinator:
class Coordinator {
func start() {
let selectedPostObservable = PublishSubject<Post>()
let viewController = ViewController()
let viewModel = ViewModel()
viewController.viewModel = viewModel
selectedPostObservable.subscribe { post in
//Do whatever
}
}
}
The selectedPostObservable is what I don't know how to connect it in a "clean" way with the viewModel.
As ViewModel:
class ViewModel {
struct Input {
let selectedIndexPath: Observable<IndexPath>
}
struct Output {
//UI Outputs
}
func transform(input: Input) -> Output {
let posts: [Post] = Observable.just(["1", "2", "3"])
//Connect with selectedindex
let result = input.selectedIndexPath
.withLatestFrom(posts) { $1[$0.row] }
.asDriver(onErrorJustReturn: nil)
return Output()
}
}
The result variable is what I should connect with selectedPostObservable.
And the ViewController (although I think is not relevant for the question):
class ViewController: UIViewController {
//...
var viewModel: ViewModel!
var tableView: UITableView!
//...
func bindViewModel() {
let input = ViewModel.Input(selectedIndexPath: tableView.rx.itemSelected.asObservable())
viewModel.transform(input: input)
}
}
Thank you so much.
Working with the structure you are starting with, I would put the PublishSubject in the ViewModel class instead of the Coordinator. Then something like this:
class ViewModel {
struct Input {
let selectedIndexPath: Observable<IndexPath>
}
struct Output {
//UI Outputs
}
let selectedPost = PublishSubject<Post>()
let bag = DisposeBag()
func transform(input: Input) -> Output {
let posts: [Post] = Observable.just(["1", "2", "3"])
//Connect with selectedindex
input.selectedIndexPath
.withLatestFrom(posts) { $1[$0.row] }
.bind(to: selectedPost)
.disposed(by: bag)
return Output()
}
}
class Coordinator {
func start() {
let viewController = ViewController()
let viewModel = ViewModel()
viewController.viewModel = viewModel
viewModel.selectedPost.subscribe { post in
//Do whatever
}
.disposed(by: viewModel.bag)
}
}
I am trying to create a common class for storing and retrieving data in Parse. I made the ParseProcessing class a singleton class. From my main View Controller I load the data and store it into a dictionary in the ParseProcessing. I do this by creating a shared instance of the ParseProcessing class. From another view controller I try to access the data from the dictionary. I assumed that because ParseProcessing is a singleton class that I have a single copy of the dictionary. This does not appear to be correct. How should I declare the variables inside the ParseProcessing so that they are shared? The code is shown below:
import UIKit
var gSep = ","
class QwikFileViewController: UIViewController {
var loadData = ParseProcessing.sharedInstance
override func viewDidLoad() {
super.viewDidLoad()
// load data from Parse
loadData.loadCategorySubcategoryData()
loadData.loadRecordsFromParse()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
ParseProcessing Singleton Class
import UIKit
import Parse
class ParseProcessing: Parse {
var dictMenuList = [String:String]()
var noteTitle = [String]()
var notes = [String]()
var thumbnailFiles = [PFFile]()
var objectIds = [String]()
var noteImage = UIImage()
class var sharedInstance:ParseProcessing {
struct singleton {
static let instance:ParseProcessing = ParseProcessing()
}
return singleton.instance
}
// Load Category/Subcategory data from Parse Data Base
func loadRecordsFromParse () -> Bool{
var tmpFile = [PFFile]()
var loadComplete = false
var query = PFQuery(className:"Record")
query.findObjectsInBackgroundWithBlock {
(objects, error) -> Void in
if error == nil {
// The find succeeded.
println("Successfully retrieved \(objects!.count) items.")
for object in objects! {
self.noteTitle.append(object["title"] as! String)
self.notes.append(object["notes"] as! String)
self.thumbnailFiles.append(object["thumbnail"] as! PFFile)
self.objectIds.append(String(stringInterpolationSegment: object.objectId))
}
} else {
println("\(error)")
}
loadComplete = true
}
return loadComplete
}
// Load Category/Subcategory data from Parse Data Base
func loadCategorySubcategoryData () // -> Dictionary <String,String>
{
var success : Bool = false
var d : Dictionary <String,String> = ["":""]
var menu = PFQuery(className: "Classification")
println("ParseProcessing: loadCategory...")
menu.findObjectsInBackgroundWithBlock {
(objects, error) -> Void in
if error == nil {
var category = ""
var subcategory = ""
for object in objects! {
category = object["category"] as! String
println("ParseProcessing: category = \(category)")
subcategory = object["subcategory"] as! String
println("ParseProcessing: subcategory = \(subcategory)")
d[category] = subcategory
}
success = true
self.dictMenuList = d
return
} else {
println("ParseProcessing: error = \(error)")
success = false
}
}
return
}
}
Another View Controller to examine the data
import UIKit
class TestViewController: UIViewController {
var dictMenuList = [String:String]()
var loadData = ParseProcessing.sharedInstance
override func viewDidLoad() {
super.viewDidLoad()
dictMenuList = loadData.dictMenuList
println("dictMenuList: \(dictMenuList)")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
The problem is that findObjectsInBackgroundWithBlock is asynchronous method (i.e. it returns immediately but the closure is called later when the query is done). So you cannot return loadComplete in loadRecordsFromParse, for example. This background request will almost certainly never be done by the time loadRecordsFromParse returns.
Instead, you probably want to adopt the completionHandler pattern. For example, this sample loadRecords doesn't try to return anything immediately, but rather will call the completionHandler when the request is done.
func loadRecords(completionHandler:([SomeObject]?, NSError?) -> ()) {
let query = PFQuery(className: "SomeClass")
query.findObjectsInBackgroundWithBlock { objects, error in
// build some model object
completionHandler(objectArray, error)
}
}
And you'd call it like so:
loadData.loadRecords() { objects, error in
// use `objects` (and make sure `error` is `nil`) here
}
// but do not use those variables here, as the above closure probably has not run yet!
Frankly, I'd be inclined to get rid of those properties in your singleton altogether. When you're dealing with asynchronous code, to have public properties that are updated asynchronously is going to be a source of heartache. You can do it, but it wouldn't be my first choice.
For example, when TestViewController is presented, you cannot assume that the asynchronous fetch associated with dictMenuList is done yet. I look at this and wonder if it makes sense for TestViewController to initiate the fetch itself and then use dictMenuList in the completion handler. That's going to be easiest.
If you must initiate the asynchronous request from one view controller and then have another view controller be informed when that asynchronous request is done, then you might have to use some other pattern, such as notifications (e.g. use NSNotificationCenter, and have the singleton post notifications when the various requests are done, and then any view controller that needs to be informed of this fact can add themselves as observers for that notification).