Add PDF support (#1596)

* Add string extension for searching pdf files (#1552)

* Create RepositoryWebViewController (#1552)

* Initial presenting of the pdf files (#1552)

* refactor: Rename String+Resource -> String+BinaryFile (#1552)

* Build destination URL (#1552)

* Configure title (#1552)

* Encode file path

* Use EmptyView for errors

* Replace UIWebView with WKWebView

* Review fixes

* refactor: Make binarySuffix computed property

* Unify if-else path

* Group guard statements
This commit is contained in:
Ivan Magda
2018-03-11 22:59:51 +03:00
committed by Ryan Nystrom
parent 50a4c9bdc6
commit 6c3c451677
4 changed files with 272 additions and 6 deletions

View File

@@ -46,3 +46,35 @@ struct FilePath {
}
}
// MARK: - FilePath (BinaryFile) -
extension FilePath {
private static let supportedBinaries = [
"pdf": "application/pdf"
]
// MARK: Public API
/// A Boolean value indicating whether a string has binary file suffix.
///
/// Supported types: **pdf**.
var hasBinarySuffix: Bool {
return binarySuffix != nil
}
/// Returns mime type for the supported binary files.
var mimeType: String? {
guard let type = binarySuffix else { return nil }
return FilePath.supportedBinaries[type]
}
// MARK: Private API
private var binarySuffix: String? {
return FilePath.supportedBinaries.keys.first(where: { path.hasSuffix($0) })
}
}

View File

@@ -130,23 +130,50 @@ ListSingleSectionControllerDelegate {
func didSelect(_ sectionController: ListSingleSectionController, with object: Any) {
guard let file = object as? RepositoryFile else { return }
let next = path.appending(file.name)
let controller: UIViewController
let nextPath = path.appending(file.name)
if file.isDirectory {
controller = RepositoryCodeDirectoryViewController(
client: client,
showDirectory(at: nextPath)
} else {
showFile(at: nextPath)
}
}
}
// MARK: - RepositoryCodeDirectoryViewController (Navigation) -
extension RepositoryCodeDirectoryViewController {
private func showDirectory(at path: FilePath) {
let controller = RepositoryCodeDirectoryViewController(
client: client,
repo: repo,
branch: branch,
path: path
)
navigationController?.pushViewController(controller, animated: trueUnlessReduceMotionEnabled)
}
private func showFile(at path: FilePath) {
let controller: UIViewController
if path.hasBinarySuffix {
controller = RepositoryWebViewController(
repo: repo,
branch: branch,
path: next
path: path
)
} else {
controller = RepositoryCodeBlobViewController(
client: client,
repo: repo,
branch: branch,
path: next
path: path
)
}
navigationController?.pushViewController(controller, animated: trueUnlessReduceMotionEnabled)
}

View File

@@ -0,0 +1,203 @@
//
// RepositoryWebViewController.swift
// Freetime
//
// Created by Ivan Magda on 01/03/2018.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import UIKit
import WebKit
// MARK: RepositoryWebViewController: UIViewController
final class RepositoryWebViewController: UIViewController {
// MARK: Types
private enum State {
case idle
case fetching
case error(String?)
}
// MARK: Instance Variables
private let branch: String
private let path: FilePath
private let repo: RepositoryDetails
private var state = State.idle {
didSet {
switch state {
case .idle:
activityIndicator.stopAnimating()
emptyView.isHidden = true
webView.isHidden = false
case .fetching:
activityIndicator.startAnimating()
emptyView.isHidden = true
webView.isHidden = false
case .error(let message):
activityIndicator.stopAnimating()
webView.isHidden = true
emptyView.isHidden = false
emptyView.label.text = message
}
}
}
private var resourceURL: URL? {
guard let encodedPath = path.path.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else {
return nil
}
return URL(string: "https://github.com/\(repo.owner)/\(repo.name)/raw/\(branch)/\(encodedPath)")
}
private lazy var webView: WKWebView = {
let webView = WKWebView()
webView.translatesAutoresizingMaskIntoConstraints = false
webView.navigationDelegate = self
webView.backgroundColor = .white
return webView
}()
private let emptyView = EmptyView()
private lazy var activityIndicator: UIActivityIndicatorView = {
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
activityIndicator.hidesWhenStopped = true
return activityIndicator
}()
// MARK: Init
init(
repo: RepositoryDetails,
branch: String,
path: FilePath
) {
self.repo = repo
self.branch = branch
self.path = path
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: UIViewController Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setup()
fetch()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
emptyView.frame = view.bounds
activityIndicator.center = view.center
if let tabBarController = tabBarController {
webView.scrollView.contentInset.bottom = tabBarController.tabBar.bounds.height
}
}
}
// MARK: - RepositoryWebViewController (UIWebViewDelegate) -
extension RepositoryWebViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
state = .fetching
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
state = .idle
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
showError(cannotLoad: true)
}
}
// MARK: - RepositoryWebViewController (Fetch Data) -
extension RepositoryWebViewController {
private func fetch() {
guard let url = resourceURL else { return showError(cannotLoad: false) }
state = .fetching
URLSession.shared.dataTask(with: url) { [weak self] (data, _, error) in
DispatchQueue.main.async {
guard let strongSelf = self else { return }
guard error == nil,
let data = data else { return strongSelf.showError(cannotLoad: true) }
strongSelf.webView.load(
data,
mimeType: strongSelf.path.mimeType ?? "text/plain",
characterEncodingName: "UTF-8",
baseURL: url
)
}
}.resume()
}
}
// MARK: - RepositoryWebViewController (Actions) -
extension RepositoryWebViewController {
@objc private func onFileNavigationTitle(sender: UIView) {
showAlert(filePath: path, sender: sender)
}
}
// MARK: - RepositoryWebViewController (Private Helpers) -
extension RepositoryWebViewController {
private func setup() {
makeBackBarItemEmpty()
configureTitle(
filePath: path,
target: self,
action: #selector(onFileNavigationTitle(sender:))
)
state = .idle
emptyView.isHidden = true
view.backgroundColor = .white
view.addSubview(emptyView)
view.addSubview(webView)
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.topAnchor.constraint(equalTo: view.topAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private func showError(cannotLoad: Bool) {
state = .error(cannotLoad
? NSLocalizedString("Cannot display file", comment: "")
: NSLocalizedString("Error loading file", comment: "")
)
}
}

View File

@@ -359,6 +359,7 @@
5DB4DD471FC5C10000DF7ABF /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DB4DD461FC5C10000DF7ABF /* Accessibility.swift */; };
65A315292044369D0074E3B6 /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A315282044369D0074E3B6 /* TabBarController.swift */; };
65A3152B2044376D0074E3B6 /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A3152A2044376D0074E3B6 /* Route.swift */; };
7509865A2048959D00D1E37A /* RepositoryWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750986592048959D00D1E37A /* RepositoryWebViewController.swift */; };
754488B11F7ADF8D0032D08C /* UIAlertController+Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754488B01F7ADF8D0032D08C /* UIAlertController+Action.swift */; };
75468F7A1F7AFBC800F2BC19 /* AlertActionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75468F791F7AFBC800F2BC19 /* AlertActionBuilder.swift */; };
75A0ACF51F79A82D0062D99A /* AlertAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A0ACF41F79A82D0062D99A /* AlertAction.swift */; };
@@ -795,6 +796,7 @@
63A98647141C04CBCFCA7923 /* 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 = "<group>"; };
65A315282044369D0074E3B6 /* TabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = "<group>"; };
65A3152A2044376D0074E3B6 /* Route.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = "<group>"; };
750986592048959D00D1E37A /* RepositoryWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryWebViewController.swift; sourceTree = "<group>"; };
754488B01F7ADF8D0032D08C /* UIAlertController+Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Action.swift"; sourceTree = "<group>"; };
75468F791F7AFBC800F2BC19 /* AlertActionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertActionBuilder.swift; sourceTree = "<group>"; };
75A0ACF41F79A82D0062D99A /* AlertAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertAction.swift; sourceTree = "<group>"; };
@@ -1759,6 +1761,7 @@
986B873D1F2E1CE400AAB55C /* RepositoryClient.swift */,
29B0EF861F93DF6C00870291 /* RepositoryCodeBlobViewController.swift */,
29AF1E8B1F8ABC5A0008A0EF /* RepositoryCodeDirectoryViewController.swift */,
750986592048959D00D1E37A /* RepositoryWebViewController.swift */,
295A77BD1F75C1CC007BC403 /* RepositoryDetails.swift */,
29AF1E8D1F8ABC900008A0EF /* RepositoryFile.swift */,
29B94E6E1FCB743900715D7E /* RepositoryFileCell.swift */,
@@ -2333,6 +2336,7 @@
297403DD1F185A8700ABA95A /* IssueAssigneeUserCell.swift in Sources */,
2999972620310E9700995FFD /* IssueMergeModel.swift in Sources */,
2999972820310F3100995FFD /* IssueMergeContextModel.swift in Sources */,
7509865A2048959D00D1E37A /* RepositoryWebViewController.swift in Sources */,
29CC294B1FF83488006B6DE7 /* IssueCommentModel+Inset.swift in Sources */,
297403D31F1850DC00ABA95A /* IssueAssigneeViewModel.swift in Sources */,
292FCAFD1EDFCC510026635E /* IssueCollapsedBodies.swift in Sources */,