I have the following, but it completes using a closure. The question is whether the code will cause a memory cycle without the use of [weak self].
class ViewModel {
init() {}
var completion: ((Users) -> Void)?
func downloadFiles() {
guard let url = URL(string: "https://reqres.in/api/users?page=2") else {return}
let task = URLSession.shared.dataTask(
with: url,
completionHandler: { data, response, _ in // add [weak self]
guard let data = data,
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else { return }
if let decoded = try? JSONDecoder().decode(Users.self, from: data) {
self.completion?(decoded)
}
})
task.resume()
}
}
using the following viewcontroller
class ViewController: UIViewController {
private let viewModel: ViewModel
override func viewDidLoad() {
super.viewDidLoad()
}
override func loadView() {
let view = UIView()
view.backgroundColor = .red
self.view = view
}
init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewModel.completion = {
print($0)
}
viewModel.downloadFiles()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and model
struct Users: Decodable {
let page: Int
let per_page: Int
let total: Int
let total_pages: Int
let data: [UserData]
}
struct UserData: Decodable {
let id: Int
let email: String
let first_name: String
let last_name: String
let avatar: String
}
So should [weak self] be used, and if not why not. This is a minimum example, and as such the memory debugger doesn't help me out here, but I want to know for the general case.
You need to use [weak self]. There are times when you won't end up with a memory leak not using [weak self], but you can't rely on that. Suppose your request takes too long to load, and the user clicks the back button. Your ViewController will wan to deinitalize, and clear itself from memory, but it can't. It can't because it has a strong reference to your ViewModel which is still downloading and has a strong reference there also. So basically you end up with a memory leak, that is your ViewController remains in memory, which you can't clear anymore.
In your code URLSessionDataTask completion block holds reference to view model. This means that it won't be released until task completes. There is no memory leak because you don't have situation where object A holds object B and object B holds object A.
However, we can ask a question: Do you need results of your data download when view controller is released and thus should be view model. If not you should go with [weak self] for view model
Related
I'm adding a bunch of annotations to a map and as the user moves and pans around to different countries I remove the annotations and add in some more. The problem I'm facing is the new annotations don't show until I've interacted with the map, either tap, pinch, pan or zoom.
I've tried placing the map.addAnnotations() into a DispatchQueue but that didn't work and I'm also offsetting the built method loadNewCountry(country: String) into a dispatchGroup. None of these are working!
Note: I've got several thousand annotations of varying types so loading them all in memory won't work for older devices :)
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
checkIfLoadNewCountry()
}
func checkIfLoadNewCountry() {
let visible = map.centerCoordinate
geocode(latitude: visible.latitude, longitude: visible.longitude) { placemark, error in
if let error = error {
print("\(error)")
return
} else if let placemark = placemark?.first {
if let isoCountry = placemark.isoCountryCode?.lowercased() {
self.loadNewCountry(with: isoCountry)
}
}
}
}
func loadNewCountry(with country: String) {
let annotationsArray = [
self.viewModel1.array,
self.viewModel2.array,
self.viewModel3.array
] as [[MKAnnotation]]
let annotations = map.annotations
autoreleasepool {
annotations.forEach {
if !($0 is CustomAnnotationOne), !($0 is CustomAnnotationTwo) {
self.map.removeAnnotation($0)
}
}
}
let group = DispatchGroup()
let queue = DispatchQueue(label: "reload-annotations", attributes: .concurrent)
group.enter()
queue.async {
self.viewModel1.load(country: country)
group.leave()
}
group.enter()
queue.async {
self.viewModel2.load(country: country)
group.leave()
}
group.wait()
DispatchQueue.main.async {
for annoArray in annotationsArray {
self.map.addAnnotations(annoArray)
}
}
}
The key issue is that the code is initializing the [[MKAnnotation]] with the current view model results, then starting the load of the view models models for a new country, and then adding the old view model annotations to the map view.
Instead, grab the [[MKAnnotation]] after the reloading is done:
func loadNewCountry(with country: String) {
let annotations = map.annotations
annotations
.filter { !($0 is CustomAnnotationOne || $0 is CustomAnnotationTwo || $0 is MKUserLocation) }
.forEach { map.removeAnnotation($0) }
let group = DispatchGroup()
let queue = DispatchQueue(label: "reload-annotations", attributes: .concurrent)
queue.async(group: group) {
self.viewModel1.load(country: country)
}
queue.async(group: group) {
self.viewModel2.load(country: country)
}
group.notify(queue: .main) {
let annotationsArrays: [[MKAnnotation]] = [
self.viewModel1.array,
self.viewModel2.array,
self.viewModel3.array
]
for annotations in annotationsArrays {
self.map.addAnnotations(annotations)
}
}
}
Unrelated to the problem at hand, I have also:
simplified the DispatchGroup group syntax;
eliminated the wait as you should never block the main thread;
eliminated the unnecessary autoreleasepool;
added MKUserLocation to the types of annotations to exclude (even if you're not showing the user location right now, you might at some future date) ... you never want to manually remove MKUserLocation or else you can get weird UX;
renamed annotationArrays to make it clear that you’re dealing with an array of arrays.
As an aside, the above raises thread-safety concerns. You appear to be updating your view models on a background queue. If you are interacting with these view models elsewhere, make sure to synchronize your access. And, besides, the motivating idea of “view models” (as opposed to a “presenter” pattern, for example) is that you hook them up so that they inform the view of changes themselves.
So, you might consider:
Give the view models asynchronous startLoad methods;
Give the view models some mechanism to inform the view (on the main queue) of changes when a load is done (whether observers, delegate protocol, closures, etc.).
Make sure the view models synchronize interaction with their properties (e.g., array).
E.g., let us imagine that the view model is updating the view via closures:
typealias AnnotationBlock = ([MKAnnotation]) -> Void
protocol CountryLoader {
var didAdd: AnnotationBlock? { get set }
var didRemove: AnnotationBlock? { get set }
}
class ViewModel1: CountryLoader {
var array: [CustomAnnotationX] = []
var didAdd: AnnotationBlock?
var didRemove: AnnotationBlock?
func startLoad(country: String, completion: (() -> Void)? = nil) {
DispatchQueue.global().async {
let newArray: [CustomAnnotationX] = ... // computationally expensive load process here (on background queue)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.didRemove?(self.array) // tell view what was removed
self.array = newArray // update model on main queue
self.didAdd?(newArray) // tell view what was added
completion?() // tell caller that we're done
}
}
}
}
That is a thread-safe implementation that abstracts the view and view controller from any of the complicated asynchronous processes. Then the view controller needs to configure the view model:
class ViewController: UIViewController {
#IBOutlet weak var map: MKMapView!
let viewModel1 = ViewModel1()
let viewModel2 = ViewModel2()
let viewModel3 = ViewModel3()
override func viewDidLoad() {
super.viewDidLoad()
configureViewModels()
}
func configureViewModels() {
viewModel1.didRemove = { [weak self] annotations in
self?.map?.removeAnnotations(annotations)
}
viewModel1.didAdd = { [weak self] annotations in
self?.map?.addAnnotations(annotations)
}
...
}
}
Then, the “reload for country” becomes:
func loadNewCountry(with country: String) {
viewModel1.startLoad(country: country)
viewModel2.startLoad(country: country)
viewModel3.startLoad(country: country)
}
Or
func loadNewCountry(with country: String) {
showLoadingIndicator()
let group = DispatchGroup()
group.enter()
viewModel1.startLoad(country: country) {
group.leave()
}
group.enter()
viewModel2.startLoad(country: country) {
group.leave()
}
group.enter()
viewModel3.startLoad(country: country) {
group.leave()
}
group.notify(queue: .main) { [weak self] in
self?.hideLoadingIndicator()
}
}
Now that’s just one pattern. The implementation details could vary wildly, based upon how you have implemented your view model. But the idea is that you should:
make sure the view model is thread-safe;
abstract the complicated threading logic out of the view and keep it in the view model; and
have some process whereby the view model informs the view of the relevant changes.
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.
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.
I'm trying to figure out Core Data. I've been following some different tutorials and they all do things a bit differently.
I have a CoreDataStack and it's initialized in SceneDelegate
lazy var coreDataStack = CoreDataStack(modelName: "model")
I believe I then use dependency injection? to set a corresponding property in the viewControllers
guard let tabController = window?.rootViewController as? UITabBarController,
let viewController = navigationController.topViewController as? ViewController else {
fatalError("Application storyboard mis-configuration. Application is mis-configured")
}
viewController.coreDataStack = coreDataStack
viewController.context = coreDataStack.ManagedObjectContext
My questions is should I pass the entire coreDataStack object to the next view? Or just the context?
Initially I was passing the entire coreDataStack, Everything seemed to work just fine. But I wasn't sure if that was correct since most tutorials seem to only reference the context. (But even then, most tutorials are vastly different, even when they are made by the same author.)
import UIKit
import CoreData
class CoreDataStack {
private let modelName: String
init(modelName: String) {
self.modelName = modelName
setupNotificationHandling()
}
lazy var managedContext: NSManagedObjectContext = {
return self.storeContainer.viewContext
}()
private lazy var storeContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: self.modelName)
container.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? {
print("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}()
// MARK: - Notification Handling
func saveForDidEnterBackground() {
saveContext()
}
#objc func saveChanges(_ notification: Notification) {
saveContext()
}
// MARK: - Helper Methods
private func setupNotificationHandling() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
selector: #selector(saveChanges(_:)),
name: UIApplication.willTerminateNotification,
object: nil)
}
// MARK: -
private func saveContext() {
guard managedContext.hasChanges else { return }
do {
try managedContext.save()
} catch {
print("Unable to Save Managed Object Context")
print("\(error), \(error.localizedDescription)")
}
}
}
I am building a simple currency converter app. When ViewController gets opened it calls a function from CoinManager.swift:
class ViewController: UIViewController {
var coinManager = CoinManager()
override func viewDidLoad() {
super.viewDidLoad()
coinManager.delegate = self
coinManager.getCoinPrice(for: "AUD", "AZN", firstCall: true)
}
...
}
CoinManager.swift:
protocol CoinManagerDelegate {
func didUpdatePrice(price1: Double, currency1: String, price2: Double, currency2: String)
func tellTableView(descriptions: [String], symbols: [String])
func didFailWithError(error: Error)
}
struct CoinManager {
var delegate: CoinManagerDelegate?
let baseURL = "https://www.cbr-xml-daily.ru/daily_json.js"
func getCoinPrice (for currency1: String,_ currency2: String, firstCall: Bool) {
if let url = URL(string: baseURL) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
self.delegate?.didFailWithError(error: error!)
return
}
if let safeData = data {
if let coinData = self.parseJSON(safeData) {
if firstCall {
var descriptions = [""]
let listOfCoins = Array(coinData.keys)
for key in listOfCoins {
descriptions.append(coinData[key]!.Name)
}
descriptions.removeFirst()
self.delegate?.tellTableView(descriptions: descriptions, symbols: listOfCoins)
}
if let coinInfo1 = coinData[currency1] {
let value1 = coinInfo1.Value
if let coinInfo2 = coinData[currency2] {
let value2 = coinInfo2.Value
//this line does not do anything the second time I call getCoinPrice:
self.delegate?.didUpdatePrice(price1: value1, currency1: currency1, price2: value2, currency2: currency2)
//And this one does work
print("delegate:\(currency1)")
} else {
print("no name matches currency2")
}
} else {
print("no name matches currency1")
}
}
}
}
task.resume()
}
}
func ParseJSON....
}
The method it calls (ViewController.swift):
extension ViewController: CoinManagerDelegate {
func didUpdatePrice(price1: Double, currency1: String, price2: Double, currency2: String) {
print("didUpdatePrice called")
DispatchQueue.main.async {
let price1AsString = String(price1)
let price2AsString = String(price2)
self.leftTextField.text = price1AsString
self.rightTextField.text = price2AsString
self.leftLabel.text = currency1
self.rightLabel.text = currency2
}
}
...
}
and finally, CurrencyViewController.swift:
var coinManager = CoinManager()
#IBAction func backButtonPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
coinManager.getCoinPrice(for: "USD", "AZN", firstCall: false)
}
So when I launch the app i get following in my debug console:
didUpdatePrice called
delegate:AUD
And when I call getCoinPrice() from CurrencyViewController the delegate method does not get called. I know that my code goes through the delegate function line as I get this in debug console:
delegate:USD
I just can't wrap my head around it. The delegate method does not work when gets called second time. Even though it is called by the same algorithm
It's because you're creating a new object of CoinManager in CurrencyViewController where the delegate is not set. So you've to set the delegate every time you create a new instance of CoinManager.
#IBAction func backButtonPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
coinManager.delegate = self
coinManager.getCoinPrice(for: "USD", "AZN", firstCall: false)
}
Update: So, the above solution would require for you to make the delegate conformance in CurrencyViewController. If you're looking for an alternate solution you should probably pass the instance of coinManager in ViewController to CurrencyViewController. For that here are the things you need to update.
In CurrencyViewController:
class CurrencyViewController: UIViewController {
var coinManager: CoinManager! // you can optional unwrap if you intent to use CurrencyViewController without coinManager
//...
And in ViewController:
currencyViewController.coinManager = coinManager // passing the instance of coinManager
Can you share the full code of CoinManager? I see this part
if firstCall {
...
}
Maybe some block logic here or unhandled cases? And can you share the full code of protocol?
Also try to print something before this code:
if error != nil {
self.delegate?.didFailWithError(error: error!)
return
}