From 3724798277df7b325dd8e91339fdb35dc372142e Mon Sep 17 00:00:00 2001 From: Ryan Nystrom Date: Mon, 27 Nov 2017 13:08:44 -0500 Subject: [PATCH] Convert repo code browsing to base VC and IGLK (#1097) --- ...RepositoryCodeDirectoryCellExtension.swift | 32 ---- ...epositoryCodeDirectoryViewController.swift | 154 ++++++++++-------- Classes/Repository/RepositoryFile.swift | 19 ++- Classes/Repository/RepositoryFileCell.swift | 81 +++++++++ .../Repository/RepositoryViewController.swift | 2 +- .../UINavigationItem+TitleSubtitle.swift | 6 + Freetime.xcodeproj/project.pbxproj | 8 +- 7 files changed, 194 insertions(+), 108 deletions(-) delete mode 100644 Classes/Repository/RepositoryCodeDirectoryCellExtension.swift create mode 100644 Classes/Repository/RepositoryFileCell.swift diff --git a/Classes/Repository/RepositoryCodeDirectoryCellExtension.swift b/Classes/Repository/RepositoryCodeDirectoryCellExtension.swift deleted file mode 100644 index 546f2850..00000000 --- a/Classes/Repository/RepositoryCodeDirectoryCellExtension.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// RepositoryCodeDirectoryCellExtension.swift -// Freetime -// -// Created by Nicholas Meschke on 10/29/17. -// Copyright © 2017 Ryan Nystrom. All rights reserved. -// - -extension StyledTableCell { - - func setup(with file: RepositoryFile) { - textLabel?.text = file.name - - isAccessibilityElement = true - accessibilityTraits |= UIAccessibilityTraitButton - let fileType = file.isDirectory - ? NSLocalizedString("Directory", comment: "Used to specify the code cell is a directory.") - : NSLocalizedString("File", comment: "Used to specify the code cell is a file.") - accessibilityLabel = AccessibilityHelper - .generatedLabel(forCell: self) - .appending(".\n\(fileType)") - accessibilityHint = file.isDirectory - ? NSLocalizedString("Shows the contents of the directory", comment: "") - : NSLocalizedString("Shows the contents of the file", comment: "") - - let imageName = file.isDirectory ? "file-directory" : "file" - imageView?.image = UIImage(named: imageName)?.withRenderingMode(.alwaysTemplate) - imageView?.tintColor = Styles.Colors.blueGray.color - accessoryType = file.isDirectory ? .disclosureIndicator : .none - } - -} diff --git a/Classes/Repository/RepositoryCodeDirectoryViewController.swift b/Classes/Repository/RepositoryCodeDirectoryViewController.swift index 37553d68..9316ab18 100644 --- a/Classes/Repository/RepositoryCodeDirectoryViewController.swift +++ b/Classes/Repository/RepositoryCodeDirectoryViewController.swift @@ -7,29 +7,56 @@ // import UIKit +import IGListKit -final class RepositoryCodeDirectoryViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { +final class RepositoryCodeDirectoryViewController: BaseListViewController, +BaseListViewControllerDataSource, +ListSingleSectionControllerDelegate { - private let tableView = UITableView(frame: .zero, style: .plain) private let client: GithubClient private let branch: String + private let file: String private let path: String private let repo: RepositoryDetails - private let cellIdentifier = "cell" - private let feedRefresh = FeedRefresh() private var files = [RepositoryFile]() private let isRoot: Bool - init(client: GithubClient, repo: RepositoryDetails, branch: String, path: String, isRoot: Bool) { + init( + client: GithubClient, + repo: RepositoryDetails, + branch: String, + path: String, + file: String, + isRoot: Bool + ) { self.client = client self.repo = repo self.branch = branch - self.path = path self.isRoot = isRoot - super.init(nibName: nil, bundle: nil) - self.title = isRoot - ? NSLocalizedString("Code", comment: "") - : path + self.file = file + self.path = path + + super.init( + emptyErrorMessage: NSLocalizedString("Cannot load issues.", comment: ""), + dataSource: self + ) + + // set on init in case used by Tabman + self.title = NSLocalizedString("Code", comment: "") + } + + static func createRoot( + client: GithubClient, + repo: RepositoryDetails, + branch: String + ) -> RepositoryCodeDirectoryViewController { + return RepositoryCodeDirectoryViewController( + client: client, + repo: repo, + branch: branch, + path: "", + file: "", + isRoot: true) } required init?(coder aDecoder: NSCoder) { @@ -38,95 +65,82 @@ final class RepositoryCodeDirectoryViewController: UIViewController, UITableView override func viewDidLoad() { super.viewDidLoad() - - tableView.dataSource = self - tableView.delegate = self - view.addSubview(tableView) - - // set the frame in -viewDidLoad is required when working with TabMan - tableView.frame = view.bounds - if isRoot, #available(iOS 11.0, *) { - tableView.contentInsetAdjustmentBehavior = .never - } - - makeBackBarItemEmpty() - - feedRefresh.refreshControl.addTarget( - self, - action: #selector(RepositoryCodeDirectoryViewController.onRefresh), - for: .valueChanged - ) - tableView.refreshControl = feedRefresh.refreshControl - tableView.register(StyledTableCell.self, forCellReuseIdentifier: cellIdentifier) - tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 0.1)) - - feedRefresh.beginRefreshing() - fetch() - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - tableView.frame = view.bounds - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - rz_smoothlyDeselectRows(tableView: tableView) + self.navigationItem.configure(title: file, subtitle: path) } // MARK: Private API - func fetch() { - client.fetchFiles(owner: repo.owner, repo: repo.name, branch: branch, path: path) { [weak self] (result) in + var fullPath: String { + return path.isEmpty ? file : "\(path)/\(file)" + } + + // MARK: Overrides + + override func fetch(page: NSNumber?) { + client.fetchFiles(owner: repo.owner, repo: repo.name, branch: branch, path: fullPath) { [weak self] (result) in switch result { case .error: - ToastManager.showGenericError() + self?.error(animated: true) case .success(let files): self?.files = files - self?.tableView.reloadData() - self?.feedRefresh.endRefreshing() + self?.update(animated: true) } } } - @objc func onRefresh() { - fetch() + // MARK: BaseListViewControllerDataSource + + func headModels(listAdapter: ListAdapter) -> [ListDiffable] { + return [] } - // MARK: UITableViewDataSource - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return files.count + func models(listAdapter: ListAdapter) -> [ListDiffable] { + return files } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? StyledTableCell else { - fatalError("Could not dequeueCell with identifier: \(cellIdentifier)") - } - - let file = files[indexPath.row] - cell.setup(with: file) - - return cell + func sectionController(model: Any, listAdapter: ListAdapter) -> ListSectionController { + let controller = ListSingleSectionController(cellClass: RepositoryFileCell.self, configureBlock: { (file, cell: UICollectionViewCell) in + guard let cell = cell as? RepositoryFileCell, let file = file as? RepositoryFile else { return } + cell.configure(path: file.name, isDirectory: file.isDirectory) + }, sizeBlock: { (_, context: ListCollectionContext?) -> CGSize in + guard let width = context?.containerSize.width else { return .zero } + return CGSize(width: width, height: Styles.Sizes.tableCellHeight) + }) + controller.selectionDelegate = self + return controller } - // MARK: UITableViewDelegate + func emptySectionController(listAdapter: ListAdapter) -> ListSectionController { + return ListSingleSectionController(cellClass: LabelCell.self, configureBlock: { (_, cell: UICollectionViewCell) in + guard let cell = cell as? LabelCell else { return } + cell.label.text = NSLocalizedString("No files found.", comment: "") + }, sizeBlock: { [weak self] (_, context: ListCollectionContext?) -> CGSize in + guard let context = context, + let strongSelf = self + else { return .zero } + return CGSize( + width: context.containerSize.width, + height: context.containerSize.height - strongSelf.topLayoutGuide.length - strongSelf.bottomLayoutGuide.length + ) + }) + } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let file = files[indexPath.row] - let newPath = path.isEmpty ? file.name : "\(path)/\(file.name)" + // MARK: ListSingleSectionControllerDelegate + func didSelect(_ sectionController: ListSingleSectionController, with object: Any) { + guard let file = object as? RepositoryFile else { return } let controller: UIViewController if file.isDirectory { controller = RepositoryCodeDirectoryViewController( client: client, repo: repo, branch: branch, - path: newPath, + path: fullPath, + file: file.name, isRoot: false ) } else { - controller = RepositoryCodeBlobViewController(client: client, repo: repo, branch: branch, path: newPath) + controller = RepositoryCodeBlobViewController(client: client, repo: repo, branch: branch, path: fullPath) } navigationController?.pushViewController(controller, animated: true) } diff --git a/Classes/Repository/RepositoryFile.swift b/Classes/Repository/RepositoryFile.swift index 936218fc..9fd31faf 100644 --- a/Classes/Repository/RepositoryFile.swift +++ b/Classes/Repository/RepositoryFile.swift @@ -7,10 +7,27 @@ // import Foundation +import IGListKit -struct RepositoryFile { +final class RepositoryFile: ListDiffable { let name: String let isDirectory: Bool + init(name: String, isDirectory: Bool) { + self.name = name + self.isDirectory = isDirectory + } + + // MARK: ListDiffable + + func diffIdentifier() -> NSObjectProtocol { + return name as NSObjectProtocol + } + + func isEqual(toDiffableObject object: ListDiffable?) -> Bool { + // assume cannot change between blob and dir + return true + } + } diff --git a/Classes/Repository/RepositoryFileCell.swift b/Classes/Repository/RepositoryFileCell.swift new file mode 100644 index 00000000..d58009f6 --- /dev/null +++ b/Classes/Repository/RepositoryFileCell.swift @@ -0,0 +1,81 @@ +// +// RepositoryFileCell.swift +// Freetime +// +// Created by Ryan Nystrom on 11/26/17. +// Copyright © 2017 Ryan Nystrom. All rights reserved. +// + +import Foundation +import SnapKit + +final class RepositoryFileCell: SelectableCell { + + private let imageView = UIImageView() + private let label = UILabel() + private let disclosure = UIImageView(image: UIImage(named: "chevron-right")?.withRenderingMode(.alwaysTemplate)) + + override init(frame: CGRect) { + super.init(frame: frame) + + isAccessibilityElement = true + accessibilityTraits |= UIAccessibilityTraitButton + backgroundColor = .white + + disclosure.tintColor = Styles.Colors.Gray.light.color + disclosure.contentMode = .scaleAspectFit + contentView.addSubview(disclosure) + disclosure.snp.makeConstraints { make in + make.right.equalTo(-Styles.Sizes.gutter) + make.centerY.equalTo(contentView) + make.size.equalTo(Styles.Sizes.icon) + } + + imageView.tintColor = Styles.Colors.blueGray.color + imageView.contentMode = .scaleAspectFit + contentView.addSubview(imageView) + imageView.snp.makeConstraints { make in + make.left.equalTo(Styles.Sizes.gutter) + make.size.equalTo(Styles.Sizes.icon) + make.centerY.equalTo(contentView) + } + + label.font = Styles.Fonts.body + label.textColor = Styles.Colors.Gray.dark.color + label.lineBreakMode = .byTruncatingHead + contentView.addSubview(label) + label.snp.makeConstraints { make in + make.left.equalTo(imageView.snp.right).offset(Styles.Sizes.rowSpacing) + make.right.lessThanOrEqualTo(disclosure.snp.left).offset(-Styles.Sizes.rowSpacing) + make.centerY.equalTo(contentView) + } + + addBorder(.bottom, left: Styles.Sizes.gutter) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Public API + + func configure(path: String, isDirectory: Bool) { + label.text = path + + let fileType = isDirectory + ? NSLocalizedString("Directory", comment: "Used to specify the code cell is a directory.") + : NSLocalizedString("File", comment: "Used to specify the code cell is a file.") + accessibilityLabel = AccessibilityHelper + .generatedLabel(forCell: self) + .appending(".\n\(fileType)") + accessibilityHint = isDirectory + ? NSLocalizedString("Shows the contents of the directory", comment: "") + : NSLocalizedString("Shows the contents of the file", comment: "") + + let imageName = isDirectory ? "file-directory" : "file" + imageView.image = UIImage(named: imageName)?.withRenderingMode(.alwaysTemplate) + + disclosure.isHighlighted = !isDirectory + } + +} diff --git a/Classes/Repository/RepositoryViewController.swift b/Classes/Repository/RepositoryViewController.swift index 755cd35d..7f920bd1 100644 --- a/Classes/Repository/RepositoryViewController.swift +++ b/Classes/Repository/RepositoryViewController.swift @@ -46,7 +46,7 @@ NewIssueTableViewControllerDelegate { } controllers += [ RepositoryIssuesViewController(client: client, repo: repo, type: .pullRequests), - RepositoryCodeDirectoryViewController(client: client, repo: repo, branch: repo.defaultBranch, path: "", isRoot: true) + RepositoryCodeDirectoryViewController.createRoot(client: client, repo: repo, branch: repo.defaultBranch) ] self.controllers = controllers diff --git a/Classes/View Controllers/UINavigationItem+TitleSubtitle.swift b/Classes/View Controllers/UINavigationItem+TitleSubtitle.swift index 0f6e92b5..4963ebbf 100644 --- a/Classes/View Controllers/UINavigationItem+TitleSubtitle.swift +++ b/Classes/View Controllers/UINavigationItem+TitleSubtitle.swift @@ -11,6 +11,11 @@ import UIKit extension UINavigationItem { func configure(title: String, subtitle: String) { + guard !subtitle.isEmpty else { + self.title = title + return + } + let titleAttributes: [NSAttributedStringKey: Any] = [ .font: Styles.Fonts.bodyBold, .foregroundColor: Styles.Colors.Gray.dark.color @@ -29,6 +34,7 @@ extension UINavigationItem { label.numberOfLines = 0 label.textAlignment = .center label.attributedText = title + label.lineBreakMode = .byTruncatingHead label.sizeToFit() titleView = label diff --git a/Freetime.xcodeproj/project.pbxproj b/Freetime.xcodeproj/project.pbxproj index 204a764b..a7deb4a0 100644 --- a/Freetime.xcodeproj/project.pbxproj +++ b/Freetime.xcodeproj/project.pbxproj @@ -267,6 +267,7 @@ 29AF1E8E1F8ABC900008A0EF /* RepositoryFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29AF1E8D1F8ABC900008A0EF /* RepositoryFile.swift */; }; 29B0EF871F93DF6C00870291 /* RepositoryCodeBlobViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B0EF861F93DF6C00870291 /* RepositoryCodeBlobViewController.swift */; }; 29B94E671FCB2D4600715D7E /* CodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B94E661FCB2D4600715D7E /* CodeView.swift */; }; + 29B94E6F1FCB743900715D7E /* RepositoryFileCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B94E6E1FCB743900715D7E /* RepositoryFileCell.swift */; }; 29B94E691FCB36A000715D7E /* File+ListDiffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B94E681FCB36A000715D7E /* File+ListDiffable.swift */; }; 29B94E6D1FCB472400715D7E /* IssueFileChangesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B94E6C1FCB472400715D7E /* IssueFileChangesModel.swift */; }; 29C0E7071ECBC6C50051D756 /* GithubClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C0E7061ECBC6C50051D756 /* GithubClient.swift */; }; @@ -394,7 +395,6 @@ DCA5ED1B1FAEF78B0072F074 /* BookmarkSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA5ED1A1FAEF78B0072F074 /* BookmarkSectionController.swift */; }; DCA5ED1D1FAEF9EE0072F074 /* BookmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA5ED1C1FAEF9EE0072F074 /* BookmarkCell.swift */; }; DCD421C91FAFC049008CC2EC /* BookmarkHeaderSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD421C81FAFC049008CC2EC /* BookmarkHeaderSectionController.swift */; }; - E1DE7A3A1FA6A28300CA3845 /* RepositoryCodeDirectoryCellExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DE7A391FA6A28300CA3845 /* RepositoryCodeDirectoryCellExtension.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -672,6 +672,7 @@ 29AF1E8D1F8ABC900008A0EF /* RepositoryFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryFile.swift; sourceTree = ""; }; 29B0EF861F93DF6C00870291 /* RepositoryCodeBlobViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryCodeBlobViewController.swift; sourceTree = ""; }; 29B94E661FCB2D4600715D7E /* CodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeView.swift; sourceTree = ""; }; + 29B94E6E1FCB743900715D7E /* RepositoryFileCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryFileCell.swift; sourceTree = ""; }; 29B94E681FCB36A000715D7E /* File+ListDiffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "File+ListDiffable.swift"; sourceTree = ""; }; 29B94E6C1FCB472400715D7E /* IssueFileChangesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueFileChangesModel.swift; sourceTree = ""; }; 29C0E7061ECBC6C50051D756 /* GithubClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GithubClient.swift; sourceTree = ""; }; @@ -806,7 +807,6 @@ DCA5ED1A1FAEF78B0072F074 /* BookmarkSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSectionController.swift; sourceTree = ""; }; DCA5ED1C1FAEF9EE0072F074 /* BookmarkCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkCell.swift; sourceTree = ""; }; DCD421C81FAFC049008CC2EC /* BookmarkHeaderSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkHeaderSectionController.swift; sourceTree = ""; }; - E1DE7A391FA6A28300CA3845 /* RepositoryCodeDirectoryCellExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryCodeDirectoryCellExtension.swift; sourceTree = ""; }; EA08BE0B8263E4898B1DF86B /* Pods-Freetime.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Freetime.release.xcconfig"; path = "Pods/Target Support Files/Pods-Freetime/Pods-Freetime.release.xcconfig"; sourceTree = ""; }; F2F33C5360D6CF79F44FFF42 /* Pods_FreetimeTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FreetimeTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -1666,6 +1666,7 @@ 29AF1E8B1F8ABC5A0008A0EF /* RepositoryCodeDirectoryViewController.swift */, 295A77BD1F75C1CC007BC403 /* RepositoryDetails.swift */, 29AF1E8D1F8ABC900008A0EF /* RepositoryFile.swift */, + 29B94E6E1FCB743900715D7E /* RepositoryFileCell.swift */, 292ACE171F5C945B00C9A02C /* RepositoryIssueSummaryModel.swift */, 29A5AF401F92677D0065D529 /* RepositoryIssueSummaryModel+Filterable.swift */, 986B87331F2CAE9800AAB55C /* RepositoryIssueSummaryType.swift */, @@ -1676,7 +1677,6 @@ 986B87371F2CB29700AAB55C /* RepositorySummaryCell.swift */, 986B87351F2CB28C00AAB55C /* RepositorySummarySectionController.swift */, 2905AFAE1F7357FA0015AE32 /* RepositoryViewController.swift */, - E1DE7A391FA6A28300CA3845 /* RepositoryCodeDirectoryCellExtension.swift */, ); path = Repository; sourceTree = ""; @@ -2301,7 +2301,6 @@ 2919295F1F3FD1F40012067B /* IssuePatchContentViewController.swift in Sources */, 292FF8B91F303DB0009E63F7 /* IssuePreviewModel.swift in Sources */, 292FF8B51F303BD0009E63F7 /* IssuePreviewSectionController.swift in Sources */, - E1DE7A3A1FA6A28300CA3845 /* RepositoryCodeDirectoryCellExtension.swift in Sources */, 292FF8B71F303BD9009E63F7 /* IssuePreviewViewController.swift in Sources */, 292FCB211EDFCF870026635E /* IssueReactionCell.swift in Sources */, 29C53FC21F12EF4000A59ED5 /* IssueReferencedCell.swift in Sources */, @@ -2462,6 +2461,7 @@ DCA5ED121FAEE3AE0072F074 /* Store.swift in Sources */, 29973E561F68BFDE0004B693 /* Signature.swift in Sources */, 2971722D1F069E96005E43AC /* SpinnerCell.swift in Sources */, + 29B94E6F1FCB743900715D7E /* RepositoryFileCell.swift in Sources */, 295B51441FC26C5400C3993B /* PeopleViewController.swift in Sources */, 2930F2731F8A27750082BA26 /* WidthCache.swift in Sources */, 2971722B1F069E6B005E43AC /* SpinnerSectionController.swift in Sources */,