HomeiOS Developmentios - Learn how to notify UIViewController's UINavigationController to push one other...

ios – Learn how to notify UIViewController’s UINavigationController to push one other UIViewController on navigation stack utilizing Mix?


I am making an attempt to inform my TUCSearchRepoListViewController to push one of many controllers on high of navigation stack based mostly on motion that occur within the TUCRepositoryListView.

The TUCRepositoryListView holds a UITableView with a customized cell of kind TUCRepositoryListTableViewCell that has two motion.
One is for opening the TUCRepositoryDetailsViewController by urgent on the textual content, and one other one is for opening TUCUserDetailsViewController by urgent the UIImageView.

Situation: By tapping on the cells textual content or picture, new UIViewController is just not being pushed to navigation stack.

I am utilizing the MVVM design sample and I separated the View from the UIViewController.

I perceive that this can be a lot of code, so if it is simpler for you, right here is the hyperlink to the Git repository with full code.

Right here is my code of TUCSearchRepoListViewController:

import SnapKit
import Mix

/// Preliminary controller for the app. This controller presents TURepositoryListView which helps trying to find repos and sorting them.
ultimate class TUCSearchRepoListViewController: UIViewController {
    personal let repoListView = TUCRepositoryListView(body: .zero)
    personal var cancellables = Set<AnyCancellable>()

    // MARK: - Implementation
    override func viewDidLoad() {
        tremendous.viewDidLoad()
        title = "Repositories"
        setUpViews()
    }

    personal func setUpViews() {
        navigationItem.searchController = repoListView.searchController
        view.backgroundColor = .systemBackground
        view.addSubview(repoListView)
        repoListView.snp.makeConstraints { make in
            make.edges.equalTo(view.safeAreaLayoutGuide)
        }
    }

    personal func bind() {
        repoListView.openRepositoryDetails
            .obtain(on: DispatchQueue.essential)
            .sink { [weak self] repository in
                let viewModel = TUCRepositoryDetailsViewModel(repository: repository)
                let vc = TUCRepositoryDetailsViewController(viewModel: viewModel)
                self?.navigationController?.pushViewController(vc, animated: true)
            }.retailer(in: &cancellables)
        repoListView.openUserDetails
            .obtain(on: DispatchQueue.essential)
            .sink { [weak self] url in
                let viewModel = TUCUserDetailsViewModel(userUrl: url)
                let vc = TUCUserDetailsViewController(viewModel: viewModel)
                self?.navigationController?.pushViewController(vc, animated: true)
            }.retailer(in: &cancellables)
    }
}

Right here is the TUCRepositoryListView:

import UIKit
import SnapKit
import Mix

/// A view holding a desk view that presents cells with repository info,
/// and UISearchController with it is ScopeButtons.
ultimate class TUCRepositoryListView: UIView {
    personal let viewModel = TUCRepositroyListViewViewModel()
    personal let enter = PassthroughSubject<TUCRepositroyListViewViewModel.Enter, By no means>()
    personal var cancellables = Set<AnyCancellable>()

    public let openUserDetails = PassthroughSubject<URL, By no means>()
    public let openRepositoryDetails = PassthroughSubject<TUCRepository, By no means>()

    public let searchController: UISearchController = {
        let searchController = UISearchController()
        searchController.searchBar.placeholder = "Repository identify"
        searchController.searchBar.showsScopeBar = true
        searchController.searchBar.scopeButtonTitles = ["Stars", "Forks", "Updated"]
        searchController.searchBar.backgroundColor = .systemBackground
        return searchController
    }()
    personal let tableView: UITableView = {
        let desk = UITableView()
        desk.register(TUCRepositoryListTableViewCell.self,
                       forCellReuseIdentifier: TUCRepositoryListTableViewCell.identifier)
        desk.keyboardDismissMode = .onDrag
        desk.separatorStyle = .none
        return desk
    }()
    personal let spinner: UIActivityIndicatorView = {
        let spinner = UIActivityIndicatorView()
        spinner.hidesWhenStopped = true
        spinner.type = .giant
        return spinner
    }()

    // MARK: - Init
    override init(body: CGRect) {
        tremendous.init(body: body)
        setUpViews()
        setUpConstraints()
        configureView()
        bind()
    }

    required init?(coder: NSCoder) {
        fatalError("Unsupported")
    }

    // MARK: - Implementation
    personal func bind() {
        let output = viewModel.rework(enter: enter.eraseToAnyPublisher())
        output
            .obtain(on: DispatchQueue.essential)
            .sink { occasion in
                swap occasion {
                case .didBeginLoading:
                    self.beginLoadingRepositories()
                case .failedToLoadSearchRepositories:
                    self.failedToLoadSearchRepositories()
                case .finishedLoadingOrSortingRepositories:
                    self.finishedLoadingOrSortingRepositories()
                case .openUserDetails(userUrl: let userUrl):
                    self.openUserDetails(userUrl: userUrl)
                case .openRepositoryDetils(repository: let repository):
                    self.openRepositoryDetails(repository: repository)
                }
            }.retailer(in: &cancellables)
    }

    personal func setUpViews() {
        addSubviews(tableView, spinner)
    }

    personal func setUpConstraints() {
        tableView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        spinner.snp.makeConstraints { make in
            make.centerY.centerX.equalToSuperview()
        }
    }

    personal func configureView() {
        searchController.searchResultsUpdater = self
        searchController.searchBar.delegate = self
        tableView.delegate = viewModel
        tableView.dataSource = viewModel
    }

    // MARK: - Output
    personal func beginLoadingRepositories() {
        spinner.startAnimating()
        tableView.backgroundView = nil
    }

    personal func failedToLoadSearchRepositories() {
        spinner.stopAnimating()
        tableView.reloadData()
        tableView.backgroundView = TUCEmptyTableViewBackground()
    }

    personal func finishedLoadingOrSortingRepositories() {
        spinner.stopAnimating()
        tableView.reloadData()
    }

    personal func openUserDetails(userUrl: URL) {
        openUserDetails.ship(userUrl)
    }

    personal func openRepositoryDetails(repository: TUCRepository) {
        openRepositoryDetails.ship(repository)
    }
}

// MARK: - UISearchResultsUpdating, UISearchBarDelegate
extension TUCRepositoryListView: UISearchResultsUpdating, UISearchBarDelegate {
    func updateSearchResults(for searchController: UISearchController) {
        enter.ship(.searchButtonPress(withText: searchController.searchBar.textual content))
    }

    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        enter.ship(.searchButtonPress(withText: searchBar.textual content))
    }

    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        enter.ship(.cancelButtonPressed)
    }

    func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
        enter.ship(.sortPressed(selectedIndex: selectedScope))
    }
}

Right here is the TUCRepositroyListViewViewModel:

import UIKit
import Mix

/// A viewModel that fetches all repositories and searches for them utilizing searchController.
/// The viewModel can also be answerable for sorting/filtering outcomes based mostly on chosen index of ScopeButton.
ultimate class TUCRepositroyListViewViewModel: NSObject {
    enum Enter {
        case searchButtonPress(withText: String?)
        case cancelButtonPressed
        case sortPressed(selectedIndex: Int)
    }
    enum Output {
        case didBeginLoading
        case failedToLoadSearchRepositories
        case finishedLoadingOrSortingRepositories
        case openUserDetails(userUrl: URL)
        case openRepositoryDetils(repository: TUCRepository)
    }

    enum SortType {
        case stars
        case forks
        case up to date

        init(_ index: Int) {
            swap index {
            case 0: self = .stars
            case 1: self = .forks
            case 2: self = .up to date
            default: self = .stars
            }
        }
    }

    personal var sortType: SortType = .stars
    personal var lastSearchName = ""
    personal var isLoadingSearchRepositories = false
    personal var shouldInitialScreenPresent = true

    personal var cellViewModels: [TUCRepositoryListTableViewCellViewModel] = []
    personal var cancellables = Set<AnyCancellable>()
    personal let output = PassthroughSubject<Output, By no means>()

    personal var repositories: [TUCRepository] = [] {
        didSet {
            sortRepositories()
        }
    }
    personal var sortedRepositories: [TUCRepository] = [] {
        didSet {
            cellViewModels.removeAll()
            for repository in sortedRepositories {
                let viewModel = TUCRepositoryListTableViewCellViewModel(repository: repository)
                cellViewModels.append(viewModel)
            }
        }
    }

    // MARK: - Implementation
    func rework(enter: AnyPublisher<Enter, By no means>) -> AnyPublisher<Output, By no means> {
        enter.sink { [weak self] occasion in
            swap occasion {
            case .searchButtonPress(withText: let identify):
                self?.fetchRepositories(utilizing: identify)
            case .sortPressed(selectedIndex: let index):
                self?.sortType = .init(index)
                self?.sortRepositories()
            case .cancelButtonPressed:
                self?.cancelButtonPressed()
            }
        }.retailer(in: &cancellables)
        return output.eraseToAnyPublisher()
    }

    personal func sortRepositories() {
        swap sortType {
        case .stars:
            sortedRepositories = repositories.sorted(by: { $0.stargazersCount > $1.stargazersCount })
        case .forks:
            sortedRepositories = repositories.sorted(by: { $0.forksCount > $1.forksCount })
        case .up to date:
            let dateFormatter = ISO8601DateFormatter()
            sortedRepositories = repositories.sorted {
                guard let firstDate = dateFormatter.date(from: $0.updatedAt),
                      let secondDate = dateFormatter.date(from: $1.updatedAt) else {
                    return false
                }
                return firstDate > secondDate
            }
            // MARK: [TEST] - Uncomment to print sorted array of dates since "TURepository.updatedAt" is just not preseneted on UI.
            // print(sortedRepositories.compactMap { return $0.updatedAt })
        }
        output.ship(.finishedLoadingOrSortingRepositories)
    }

    personal func cancelButtonPressed() {
        shouldInitialScreenPresent = true
        repositories.removeAll()
    }

    personal func fetchRepositories(utilizing searchName: String?) {
        guard let identify = searchName, !identify.isEmpty else { return }
        if !isLoadingSearchRepositories && lastSearchName != identify {
            let queryParams = [
                URLQueryItem(name: "q", value: name)
            ]
            let tucRequest = TUCRequest(enpoint: .searchRepositories, queryParams: queryParams)
            lastSearchName = identify
            isLoadingSearchRepositories = true
            shouldInitialScreenPresent = false
            output.ship(.didBeginLoading)
            TUCService.shared.execute(tucRequest, anticipated: TUCRepositoriesResponse.self)
                .obtain(on: DispatchQueue.essential)
                .sink(receiveCompletion: { [weak self] completion in
                    if case .failure(let error) = completion {
                        self?.repositories.removeAll()
                        self?.isLoadingSearchRepositories = false
                        self?.output.ship(.failedToLoadSearchRepositories)
                        print(error.localizedDescription)
                    }
                }, receiveValue: { [weak self] lead to
                    self?.repositories = outcome.objects
                    self?.isLoadingSearchRepositories = false
                    self?.output.ship(.finishedLoadingOrSortingRepositories)
                }).retailer(in: &cancellables)
        }
    }
}

// MARK: - UITableViewDelegate, UITableViewDataSource
extension TUCRepositroyListViewViewModel: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection part: Int) -> Int {
        if cellViewModels.isEmpty && shouldInitialScreenPresent {
            tableView.backgroundView = TUCEmptyTableViewBackground(message: "Attempt trying to find repo.")
            return 0
        }
        tableView.backgroundView = nil
        return cellViewModels.rely
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: TUCRepositoryListTableViewCell.identifier) as? TUCRepositoryListTableViewCell else {
            return UITableViewCell()
        }

        cell.userTapAction.obtain(on: DispatchQueue.essential).sink { [weak self] userUrl in
            self?.output.ship(.openUserDetails(userUrl: userUrl))
        }.retailer(in: &cancellables)
        cell.repositoryTapAction.sink { [weak self] repository in
            self?.output.ship(.openRepositoryDetils(repository: repository))
        }.retailer(in: &cancellables)

        cell.configure(with: cellViewModels[indexPath.row])
        return cell
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 120
    }
}

Right here is the TUCRepositoryListTableViewCell:

import UIKit
import Kingfisher
import SnapKit
import Mix

/// A desk view cell that represents a repository merchandise in a TURepositoryListView.
/// Press on picture opens person particulars, press on textual content open repo particulars.
class TUCRepositoryListTableViewCell: UITableViewCell {
    static let identifier = "TURepositoryListTableViewCell"
    personal var viewModel: TUCRepositoryListTableViewCellViewModel?

    public let userTapAction = PassthroughSubject<URL, By no means>()
    public let repositoryTapAction = PassthroughSubject<TUCRepository, By no means>()

    personal let containerView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    personal let textContainerView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    personal let repositoryNameLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 18, weight: .semibold)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    personal let authorNameLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 14, weight: .medium)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    personal let numberOfWatchersLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 12, weight: .mild)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    personal let numberOfForksLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 12, weight: .mild)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    personal let numberOfIssuesLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 12, weight: .mild)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    personal let numberOfStarsLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 12, weight: .mild)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    personal let authorAvatarImageView: UIImageView = {
        let picture = UIImageView()
        picture.contentMode = .scaleAspectFill
        picture.translatesAutoresizingMaskIntoConstraints = false
        return picture
    }()

    // MARK: - Init
    override init(type: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        tremendous.init(type: type, reuseIdentifier: reuseIdentifier)
        setUpViews()
        setUpConstraints()
    }

    required init?(coder: NSCoder) {
        fatalError("Unsupported")
    }

    override func layoutSubviews() {}

    // MARK: - Implementation
    personal func setUpViews() {
        contentView.addSubview(containerView)
        containerView.addSubviews(authorAvatarImageView,
                                  textContainerView)
        textContainerView.addSubviews(repositoryNameLabel,
                                      authorNameLabel,
                                      numberOfForksLabel,
                                      numberOfIssuesLabel,
                                      numberOfWatchersLabel,
                                      numberOfStarsLabel)

        let tapImage = UITapGestureRecognizer(goal: self, motion: #selector(openUserDetails))
        let tapText = UITapGestureRecognizer(goal: self, motion: #selector(openRepositoryDetails))
        authorAvatarImageView.addGestureRecognizer(tapImage)
        authorAvatarImageView.isUserInteractionEnabled = true
        textContainerView.addGestureRecognizer(tapText)
        textContainerView.isUserInteractionEnabled = true
    }

    personal func setUpConstraints() {
        containerView.backgroundColor = .cyan.withAlphaComponent(0.4)
        containerView.layer.cornerRadius = 20
        containerView.clipsToBounds = true
        containerView.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(10)
        }
        authorAvatarImageView.snp.makeConstraints { make in
            make.left.centerY.equalToSuperview()
            make.peak.width.equalTo(containerView.snp.peak)
        }
        textContainerView.snp.makeConstraints { make in
            make.left.equalTo(authorAvatarImageView.snp.proper).offset(10)
            make.high.proper.backside.equalToSuperview()
        }
        repositoryNameLabel.snp.makeConstraints { make in
            make.high.proper.equalToSuperview().inset(10)
            make.left.equalTo(authorAvatarImageView.snp.proper).offset(10)
        }
        authorNameLabel.snp.makeConstraints { make in
            make.proper.equalToSuperview().inset(10)
            make.high.equalTo(repositoryNameLabel.snp.backside).offset(2)
            make.left.equalTo(authorAvatarImageView.snp.proper).offset(10)
        }
        numberOfWatchersLabel.snp.makeConstraints { make in
            make.high.equalTo(authorNameLabel.snp.backside).offset(5)
            make.left.equalTo(authorAvatarImageView.snp.proper).offset(10)
        }
        numberOfForksLabel.snp.makeConstraints { make in
            make.high.equalTo(numberOfWatchersLabel.snp.high)
            make.left.equalTo(numberOfWatchersLabel.snp.proper).offset(10)
        }
        numberOfIssuesLabel.snp.makeConstraints { make in
            make.high.equalTo(numberOfWatchersLabel.snp.backside).offset(2)
            make.left.equalTo(authorAvatarImageView.snp.proper).offset(10)
        }
        numberOfStarsLabel.snp.makeConstraints { make in
            make.high.equalTo(numberOfForksLabel.snp.backside).offset(2)
            make.left.equalTo(numberOfIssuesLabel.snp.proper).offset(10)
        }
    }

    public func configure(with viewModel: TUCRepositoryListTableViewCellViewModel) {
        self.viewModel = viewModel
        repositoryNameLabel.textual content = viewModel.repositoryTitle
        authorNameLabel.textual content = viewModel.authorName
        numberOfWatchersLabel.textual content = viewModel.watchersCountText
        numberOfForksLabel.textual content = viewModel.forksCountText
        numberOfIssuesLabel.textual content = viewModel.issuesCountText
        numberOfStarsLabel.textual content = viewModel.starsCountText

        let placeholder = UIImage(systemName: "individual.fill")
        authorAvatarImageView.kf.indicatorType = .exercise
        authorAvatarImageView.kf.setImage(with: viewModel.avatarURL,
                                          placeholder: placeholder,
                                          choices: [.transition(.flipFromLeft(0.2))])
    }

    // MARK: - Actions
    @objc personal func openUserDetails() {
        guard let url = viewModel?.userUrl else { return }
        userTapAction.ship(url)
    }

    @objc personal func openRepositoryDetails() {
        guard let repository = viewModel?.detailedRepository else { return }
        repositoryTapAction.ship(repository)
    }
}

Right here is the TUCRepositoryListTableViewCellViewModel:

import Basis

/// A viewModel answerable for managing knowledge of TURepositoryListTableViewCell.
ultimate class TUCRepositoryListTableViewCellViewModel {
    personal let repository: TUCRepository

    // MARK: - Public calculated properties
    public var id: Int {
        return repository.id
    }
    public var avatarURL: URL? {
        return URL(string: repository.ownerUser.avatarImageString)
    }
    public var repositoryTitle: String {
        return repository.identify
    }
    public var authorName: String {
        return repository.ownerUser.identify
    }
    public var userUrl: URL? {
        return URL(string: repository.ownerUser.userUrl)
    }
    public var repositoryUrl: String {
        return repository.repositoryUrl
    }
    public var starsCountText: String {
        return "Stars: (repository.stargazersCount)"
    }
    public var watchersCountText: String {
        return "Watchers: (repository.watchersCount)"
    }
    public var forksCountText: String {
        return "Forks: (repository.forksCount)"
    }
    public var issuesCountText: String {
        return "Open points: (repository.openIssuesCount)"
    }
    public var detailedRepository: TUCRepository {
        return repository
    }

    // MARK: - Init
    init(repository: TUCRepository) {
        self.repository = repository
    }
}

I attempted defining a protocol

protocol TUCRepositoryListViewDelegate: AnyObject {
    func openUserDetails(url: URL)
    func openRepositoryDetails(repository: TUCRepository)
}

ultimate class TUCRepositoryListView: UIView {
    weak var delegate: TUCRepositoryListViewDelegate?
.
.
.

and utilizing the delegate sample within the TUCRepositoryListView which labored, however I want to do away with the delegates for good.

Right here is an instance that labored:

ultimate class TUCRepositoryListView: UIView {
.
.
.
    personal func openUserDetails(userUrl: URL) {
        delegate?.openUserDetails(url: userUrl)
    }

    personal func openRepositoryDetails(repository: TUCRepository) {
        delegate?.openRepositoryDetails(repository: repository)
    }

and lengthening the TUCSearchRepoListViewController like this:

extension TUCSearchRepoListViewController: TUCRepositoryListViewDelegate {
    func openRepositoryDetails(repository: TUCRepository) {
        let viewModel = TUCRepositoryDetailsViewModel(repository: repository)
        let vc = TUCRepositoryDetailsViewController(viewModel: viewModel)
        navigationController?.pushViewController(vc, animated: true)
    }

    func openUserDetails(url: URL) {
        let viewModel = TUCUserDetailsViewModel(userUrl: url)
        let vc = TUCUserDetailsViewController(viewModel: viewModel)
        navigationController?.pushViewController(vc, animated: true)
    }
}

Any concepts are welcome. Thanks upfront!

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments