I have an application written in the MVVM-C pattern, using RxSwift
After adding a new view programmatically, the application crashes with a
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an
Optional value
error. I am at a complete loss, the implementation is almost exactly the same, minus the fact one view controller is a storyboard and one is not.
This is my new ViewController
import UIKit
import RxSwift
import RxCocoa
final class FeedViewController: TableViewController, ViewModelAttaching {
var viewModel: Attachable<FeedViewModel>!
var bindings: FeedViewModel.Bindings {
let viewWillAppear = rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
.mapToVoid()
.asDriverOnErrorJustComplete()
let refresh = tableView.refreshControl!.rx
.controlEvent(.valueChanged)
.asDriver()
return FeedViewModel.Bindings(
fetchTrigger: Driver.merge(viewWillAppear, refresh),
selection: tableView.rx.itemSelected.asDriver()
)
}
override func viewDidLoad() {
super.viewDidLoad()
}
func bind(viewModel: FeedViewModel) -> FeedViewModel {
viewModel.posts
.drive(tableView.rx.items(cellIdentifier: FeedTableViewCell.reuseID, cellType: FeedTableViewCell.self)) { _, viewModel, cell in
cell.bind(to: viewModel)
}
.disposed(by: disposeBag)
viewModel.fetching
.drive(tableView.refreshControl!.rx.isRefreshing)
.disposed(by: disposeBag)
viewModel.errors
.delay(0.1)
.map { $0.localizedDescription }
.drive(errorAlert)
.disposed(by: disposeBag)
return viewModel
}
}
This is an existing one, that works but uses storyboards
final class PostsListViewController: TableViewController, ViewModelAttaching {
var viewModel: Attachable<PostsListViewModel>!
var bindings: PostsListViewModel.Bindings {
let viewWillAppear = rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
.mapToVoid()
.asDriverOnErrorJustComplete()
let refresh = tableView.refreshControl!.rx
.controlEvent(.valueChanged)
.asDriver()
return PostsListViewModel.Bindings(
fetchTrigger: Driver.merge(viewWillAppear, refresh),
selection: tableView.rx.itemSelected.asDriver()
)
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
// MARK: - View Methods
private func setupView() {
title = "Posts"
}
func bind(viewModel: PostsListViewModel) -> PostsListViewModel {
viewModel.posts
.drive(tableView.rx.items(cellIdentifier: PostTableViewCell.reuseID, cellType: PostTableViewCell.self)) { _, viewModel, cell in
cell.bind(to: viewModel)
}
.disposed(by: disposeBag)
viewModel.fetching
.drive(tableView.refreshControl!.rx.isRefreshing)
.disposed(by: disposeBag)
viewModel.errors
.delay(0.1)
.map { $0.localizedDescription }
.drive(errorAlert)
.disposed(by: disposeBag)
return viewModel
}
}
They are basically exactly the same. The exception is thrown on the let refresh = tableView.refreshControl!.rx line.
The working coordinator, using a Storyboard is
import RxSwift
class PostsCoordinator: BaseCoordinator<Void> {
typealias Dependencies = HasPostService
private let navigationController: UINavigationController
private let dependencies: Dependencies
init(navigationController: UINavigationController, dependencies: Dependencies) {
self.navigationController = navigationController
self.dependencies = dependencies
}
override func start() -> Observable<Void> {
let viewController = PostsListViewController.instance()
navigationController.viewControllers = [viewController]
let avm: Attachable<PostsListViewModel> = .detached(dependencies)
let viewModel = viewController.attach(wrapper: avm)
viewModel.selectedPost
.drive(onNext: { [weak self] selection in
self?.showDetailView(with: selection)
})
.disposed(by: viewController.disposeBag)
// View will never be dismissed
return Observable.never()
}
private func showDetailView(with post: Post) {
let viewController = PostDetailViewController.instance()
viewController.viewModel = PostDetailViewModel(post: post)
navigationController.showDetailViewController(viewController, sender: nil)
}
}
I have an extension to allow me to instantiate it also
protocol Reusable {
static var reuseID: String { get }
}
extension Reusable {
static var reuseID: String {
return String(describing: self)
}
}
// MARK: - View Controller
extension UIViewController: Reusable {
class func instance() -> Self {
let storyboard = UIStoryboard(name: reuseID, bundle: nil)
return storyboard.instantiateViewController()
}
}
extension UIStoryboard {
func instantiateViewController<T: UIViewController>() -> T {
guard let viewController = self.instantiateViewController(withIdentifier: T.reuseID) as? T else {
fatalError("Unable to instantiate view controller: \(T.self)")
}
return viewController
}
}
The 'broken' coordinator is exactly the same, except I swapped
let viewController = PostsListViewController.instance()
for
let viewController = FeedViewController()
I am at a complete loss at to why this is throwing. Print statements and breakpoints at various points haven't turned up a nil on any values.
Please let me know if it would be easier to share a sample app as I appreciate the code snippets may not be the most obvious.
tableView.refreshControl is nil. You are trying to force access the nil refreshControl.
The Refreshing property is Enabled for the UITableViewController in your storyboard that works. In the programmatic version, the refreshControl is not created automatically.
The default value of the refreshControl property is nil. You need to instantiate and assign a UIRefreshControl to self.refreshControl before it exists.
When you create your view using a Storyboard and enable it, this is taken care of behind the scenes for you. Programmatically you will be required to implement this yourself.
Related
I'm making an app with SwiftUI and UIkit, I use UIkit for the main app controller and navigation, and I use SwiftUI for app design.
The app works very well, but I'm worried about the memory leaks. This is because the ViewModels I use to pass data between views don't call desinit whene the view disappears. I know that in SwiftUI views are not disposed immediately, but since I'm using UIKit to navigate I don't know what the problem is.
//The ViewModel for each user fetched
internal class UserViewModel: ObservableObject, Identifiable {
//MARK: - Propeties var currentListener: ListenerRegistration?
#Published var request: Request?
#Published var user: User
init(user: User) {
self.user = user
getRequest()
fetchAdmins()
}
deinit {
//Dosnt get called removeListener()
}
func getRequest() {
guard let uid = Auth.auth().currentUser?.uid else {return}
guard let id = id else {return}
self.currentListener = Collections.requests(id).document(uid).addSnapshotListener { snapshot, error in
if let error = error {
print(error.localizedDescription)
return
}
if ((snapshot?.exists) != nil) {
if let request = try? snapshot!.data(as: Request.self) {
DispatchQueue.main.async {
self.request = request
}
}
}
}
}
func removeListener() {
self.currentListener?.remove()
}
}
}
//The ViewModel to fetch all the users ViewModels
class UsersViewModel: ObservableObject {
#Published var users = [UserViewModel]()
func fetch() {
DispatchQueue.global(qos: .background).async {
Collections.users.getDocuments(completion: { snapshot, err in
guard let documents = snapshot?.documents else { return } let users = documents.compactMap({ try? $0.data(as: User.self) })
users.forEach { user in
let vm = UserViewModel(user: user)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.users.append(vm)
}
}
})
}
} }
//Show the users cells with the ViewModel
struct HomeView: View {
#ObservedObject var usersViewModels: UsersViewModel
//MARK: - Init
init() {
self.usersViewModels = UsersViewModel()
}
var body: some View {
ListView(content: {
ForEach(usersViewModels) { usersViewModel in
UserCell(viewModel: usersViewModel).id(user.id)
}
})
}
}
This is how I navigate between controllers and views of my app. I don't use NavigationLinks:
public static func push<Content: View>(view: Content) {
DispatchQueue.main.async {
guard let tabBarController = UIApplication.rootViewController as? UITabBarController, let navigationController = tabBarController.selectedViewController as? UINavigationController else { return nil }
if let navigationController = UIApplication.getCurrentNavigationController() {
navigationController.pushViewController(HostingController(content: view), animated: true)
}
}
}
Does anyone know if this method that I am using to navigate can cause me memory problems? And you know why my app doesn't reduce its memory every time I close a window, it just increases more and more.
The disappearing does not mean it is no longer in memory.
It looks like you keep pushing them onto the navigation stack which increases their retain count.
You've got a memory leak here:
struct HomeView: View {
#ObservedObject var usersViewModels: UsersViewModel
//MARK: - Init
init() {
self.usersViewModels = UsersViewModel() // here
}
View structs must not init objects because the View struct is recreated every state change thus the object is being constantly init.
SwiftUI is all about taking advantage of value semantics, try to use #State with value types (or group them in a struct) in the View struct for your view data.
Model data structs go in a singleton ObservableObject supplied to the Views using .environmentObject.
I'm struggling with specific use-case incorporating RxSwift's PublishSubject.
For sake of simplicity unimportant details were omitted.
There is a MVVM setup. In VC I have a UIButton, on tap of which a network call should dispatch. In ViewModel I have a buttonDidTapSubject: PublishSubject<Void>.
class ViewModel {
let disposeBag = DisposeBag()
let buttonDidTapSubject = PublishSubject<Void>()
let service: Service
typealias Credentials = (String, String)
var credentials: Observable<Credentials> {
return Observable.just(("testEmail", "testPassword"))
}
init(_ service: Service) {
self.service = service
buttonDidTapSubject
.withLatestFrom(credentials)
.flatMap(service.login) // login method has signature func login(_ creds: Credentials) -> Observable<User>
.subscribe(onNext: { user in print("Logged in \(user)") },
onError: { error in print("Received error") })
.disposed(by: disposeBag)
}
}
class ViewController: UIViewController {
let viewModel: ViewModel
let button = UIButton()
init(_ viewModel: ViewModel) {
self.viewModel = viewModel
}
}
In controller's viewDidLoad I make a binding:
override func viewDidLoad() {
button.rx.tap.asObservable()
.subscribe(viewModel.buttonDidTapSubject)
.disposed(by: disposeBag)
}
The problem is, since network request can fail and Observable that is returned from login(_:) method will produce an error, the whole subscription to buttonDidTapSubject in ViewModel will be disposed. And all other taps on a button will not trigger sequence to login in ViewModel.
Is there any way to avoid this kind of behavior?
You can use retry to prevent finishing the subcription. If you only want to retry in specific cases or errors you can also use retryWhen operator
In the view model:
lazy var retrySubject: Observable<Void> = {
return viewModel.buttonDidTapSubject
.retryWhen { error in
if (error == .networkError){ //check here your error
return .just(Void())
} else {
return .never() // Do not retry
}
}
}()
In the view controller I would have done it in another way:
override func viewDidLoad() {
super.viewDidLoad()
button.rx.tap.asObservable()
.flatMap { [weak self] _ in
return self?.viewModel.retrySubject
}
.subscribe(onNext: {
//do whatever
})
.disposed(by: disposeBag)
}
Not sure if still relevant - Use PublishRelay ( although it is RxCocoa )
This question already has answers here:
Passing data between view controllers
(45 answers)
Closed 5 years ago.
I'm trying to go back to my las viewController with sending data, but it doesn't work.
When I just use popViewController, I can go back to the page, but I can't move my datas from B to A.
Here is my code :
func goToLastViewController() {
let vc = self.navigationController?.viewControllers[4] as! OnaylarimTableViewController
vc.onayCode.userId = taskInfo.userId
vc.onayCode.systemCode = taskInfo.systemCode
self.navigationController?.popToViewController(vc, animated: true)
}
To pass data from Child to parent Controller, you have to pass data using Delegate pattern.
Steps to implement delegation pattern, Suppose A is Parent viewController and B is Child viewController.
Create protocol, and create delegate variable in B
Extend protocol in A
pass reference to B of A when Push or Present viewcontroller
Define delegate Method in A, receive action.
After that, According to your condition you can call delegate method from B.
You should do it using delegate protocol
class MyClass: NSUserNotificationCenterDelegate
The implementation will be like following:
func userDidSomeAction() {
//implementation
}
And ofcourse you have to implement delegete in your parent class like
childView.delegate = self
Check this for more information
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html
You have to send back to last ViewController with 2 options.
1. Unwind segue. (With use of storyboard)
You can refer this link.
2. Use of delegate/protocol.
You can refer this link.
Also this link will be useful for you.
You can use Coordinator Pattern
For example, I have 2 screens. The first displays information about the user, and from there, he goes to the screen for selecting his city. Information about the changed city should be displayed on the first screen.
final class CitiesViewController: UITableViewController {
// MARK: - Output -
var onCitySelected: ((City) -> Void)?
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
onCitySelected?(cities[indexPath.row])
}
...
}
UserEditViewController:
final class UserEditViewController: UIViewController, UpdateableWithUser {
// MARK: - Input -
var user: User? { didSet { updateView() } }
#IBOutlet private weak var userLabel: UILabel?
private func updateView() {
userLabel?.text = "User: \(user?.name ?? ""), \n"
+ "City: \(user?.city?.name ?? "")"
}
}
And Coordinator:
protocol UpdateableWithUser: class {
var user: User? { get set }
}
final class UserEditCoordinator {
// MARK: - Properties
private var user: User { didSet { updateInterfaces() } }
private weak var navigationController: UINavigationController?
// MARK: - Init
init(user: User, navigationController: UINavigationController) {
self.user = user
self.navigationController = navigationController
}
func start() {
showUserEditScreen()
}
// MARK: - Private implementation
private func showUserEditScreen() {
let controller = UIStoryboard.makeUserEditController()
controller.user = user
controller.onSelectCity = { [weak self] in
self?.showCitiesScreen()
}
navigationController?.pushViewController(controller, animated: false)
}
private func showCitiesScreen() {
let controller = UIStoryboard.makeCitiesController()
controller.onCitySelected = { [weak self] city in
self?.user.city = city
_ = self?.navigationController?.popViewController(animated: true)
}
navigationController?.pushViewController(controller, animated: true)
}
private func updateInterfaces() {
navigationController?.viewControllers.forEach {
($0 as? UpdateableWithUser)?.user = user
}
}
}
Then we just need to start coordinator:
coordinator = UserEditCoordinator(user: user, navigationController: navigationController)
coordinator.start()
I'm beginning with MVVM in order to well separate logic code from the view. But I have some concern about where to put the progressHUD related code when tapping a button that makes a request.
Before, I used to do that:
//Before
#IBAction func startRequestTapped() {
SVProgressHUD.show()
self.apiClient.requestObservable().subscribe(onError: { (error) in
SVProgressHUD.hide()
}, onCompleted: {
SVProgressHUD.hide()
})
}
But when I use mvvm, I do like that:
//In the viewModel
public var validateButtonDidTap = PublishSubject<Void>()
init() {
validateButtonDidTap.flatMap { (_)
return self.apiClient.requestObservable()
}
}
// In the viewController
viewDidLoad() {
let tap = self.validateButton.rx.tap
tap.bindTo(self.viewModel.validateButtonDidTap)
}
And amongst that, I don't know where to put the the ProgressHUD hide or show.
Mark answer is right, but I am going to guide you step by step.
Let's supose you're going to try signing in.
Copy ActivityIndicator.swift file in your project.
In the viewModel:
//MARK: - Properties
/// The http client
private let apiClient: YourApiClient
/// Clousure when button is tapped
var didTappedButton: () -> Void = {}
/// The user
var user: Observable<User>
/// Is signing process in progress
let signingIn: Observable<Bool> = ActivityIndicator().asObservable()
//MARK: - Initialization
init(client: YourApiClient) {
self.client = client
self.didTappedButton = { [weak self] in
self.user = self.apiClient
.yourSignInRequest()
.trackActivity(self.signingIn)
.observeOn(MainScheduler.instance)
}
}
Create an extension of SVProgressHUD: (I don't know SVProgressHUD library, but it would be something like this. Please fix it if needed)
extension Reactive where Base: SVProgressHUD {
/// Bindable sink for `show()`, `hide()` methods.
public static var isAnimating: UIBindingObserver<Base, Bool> {
return UIBindingObserver(UIElement: self.base) { progressHUD, isVisible in
if isVisible {
progressHUD.show() // or other show methods
} else {
progressHUD.dismiss() // or other hide methods
}
}
}
}
In your viewController:
#IBAction func startRequestTapped() {
viewModel.didTappedButton()
}
override func viewDidLoad() {
// ...
viewModel.signingIn
.bindTo(SVProgressHUD.rx.isAnimating)
.addDisposableTo(disposeBag)
}
Accepted answer updated to Swift 4, RxSwift 4.0.0 and SVProgressHUD 2.2.2:
3- Extension:
extension Reactive where Base: SVProgressHUD {
public static var isAnimating: Binder<Bool> {
return Binder(UIApplication.shared) {progressHUD, isVisible in
if isVisible {
SVProgressHUD.show()
} else {
SVProgressHUD.dismiss()
}
}
}
}
4- Controller:
viewModel.signingIn.asObservable().bind(to: SVProgressHUD.rx.isAnimating).disposed(by: disposeBag)
You could try using an ActivityIndicator.
See the example here:
https://github.com/RxSwiftCommunity/RxSwiftUtilities
Recently I watched this talk (link, see section Networking) and I decided to do some excerise in protocol-oriented programming. So I thought about this simple example: View Controller for displaying list of files. Of course, protocol-oriented way, with following constraints:
FilesViewController - containts table view & FilesTableViewAdapter. Table view delegate.
FilesTableViewAdapter - initializable with table view & FilesProvider: Gettable,
so that in tests I can inject FilesProviderMock: Gettable.
FilesTableViewAdapter is a data source of table view and uses FilesProvider for fetching files.
final class FilesTableViewController: UIViewController {
var filesTableView: FilesTableView! { return view as! FilesTableView }
private var tableViewAdapter: FilesTableViewAdapter<FilesProvider>!
// MARK: Subclassing
override func loadView() {
view = FilesTableView(frame: UIScreen.main.bounds)
}
override func viewDidLoad() {
tableViewAdapter = FilesTableViewAdapter(filesTableView.tableView, provider: FilesProvider())
// Actually I would like to have this method in Adapter
// so that VC isn't handling networking.
tableViewAdapter.provider.get { result in
// result type: (Result<[File]>)
switch result {
case .success(let files): print(files)
case .failure(let error): print(error)
}
}
filesTableView.tableView.delegate = self
filesTableView.tableView.dataSource = tableViewAdapter
}
}
extension FilesTableViewController: UITableViewDelegate {
//
}
final class FilesTableViewAdapter<T: Gettable>: NSObject, UITableViewDataSource {
let provider: T
private let tableView: UITableView
init(_ tableView: UITableView, provider: T) {
self.tableView = tableView
self.provider = provider
super.init()
}
func problem() {
provider.get { result in
// Result type is (Result<T.T>) - :(
switch result {
case .success(let files): print(files)
case .failure(let error): print(error)
}
}
}
struct FilesProvider {
private let Files = [File]()
}
extension FilesProvider: Gettable {
func get(completionHandler: (Result<[File]>) -> Void) {
//
}
}
protocol Gettable {
associatedtype T
func get(completionHandler: (Result<T>) -> Void)
}
I know I went too far with generalizing this piece of code. Now I'm stuck and I have this questions I can't answer myself:
How to make it in protocol-oriented way, with networking code in class different than VC (say Adapter)?
How to make it easily testable and extendable in the future?