I am attempting to inform my TUCSearchRepoListViewController
to push one of many controllers on prime 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 just isn’t being pushed to navigation stack.
I am utilizing the MVVM design sample and I separated the View from the UIViewController.
I perceive that it is 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.important)
.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.important)
.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 data,
/// 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.remodel(enter: enter.eraseToAnyPublisher())
output
.obtain(on: DispatchQueue.important)
.sink { occasion in
change 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 be liable 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) {
change 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 remodel(enter: AnyPublisher<Enter, By no means>) -> AnyPublisher<Output, By no means> {
enter.sink { [weak self] occasion in
change 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() {
change 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" just isn't 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.important)
.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.gadgets
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: "Strive trying to find repo.")
return 0
}
tableView.backgroundView = nil
return cellViewModels.depend
}
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.important).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.top.width.equalTo(containerView.snp.top)
}
textContainerView.snp.makeConstraints { make in
make.left.equalTo(authorAvatarImageView.snp.proper).offset(10)
make.prime.proper.backside.equalToSuperview()
}
repositoryNameLabel.snp.makeConstraints { make in
make.prime.proper.equalToSuperview().inset(10)
make.left.equalTo(authorAvatarImageView.snp.proper).offset(10)
}
authorNameLabel.snp.makeConstraints { make in
make.proper.equalToSuperview().inset(10)
make.prime.equalTo(repositoryNameLabel.snp.backside).offset(2)
make.left.equalTo(authorAvatarImageView.snp.proper).offset(10)
}
numberOfWatchersLabel.snp.makeConstraints { make in
make.prime.equalTo(authorNameLabel.snp.backside).offset(5)
make.left.equalTo(authorAvatarImageView.snp.proper).offset(10)
}
numberOfForksLabel.snp.makeConstraints { make in
make.prime.equalTo(numberOfWatchersLabel.snp.prime)
make.left.equalTo(numberOfWatchersLabel.snp.proper).offset(10)
}
numberOfIssuesLabel.snp.makeConstraints { make in
make.prime.equalTo(numberOfWatchersLabel.snp.backside).offset(2)
make.left.equalTo(authorAvatarImageView.snp.proper).offset(10)
}
numberOfStarsLabel.snp.makeConstraints { make in
make.prime.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 liable 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!