This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended. To summarise this work: - [x] created a new **Xcode** project; - [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project; - [x] created the `Locations` app and also, its `Libraries` package; - [x] created the `Shared` package to share dependencies between the apps; - [x] added a `Makefile` file and implemented some **environment** and **help** commands. Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Reviewed-on: rock-n-code/deep-linking-assignment#1
225 lines
9.9 KiB
Swift
225 lines
9.9 KiB
Swift
import WMF
|
|
|
|
public struct LibraryUsed {
|
|
let title:String
|
|
let licenseName:String
|
|
let licenseText:String
|
|
}
|
|
|
|
class LibrariesUsedViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
|
|
var libraries:[LibraryUsed] = []
|
|
@IBOutlet weak var tableView: UITableView!
|
|
|
|
@objc public static let storyboardName = "LibrariesUsed"
|
|
|
|
private static let cellReuseIdentifier = "org.wikimedia.libraries.used.cell"
|
|
private static let dataFileName = "LibrariesUsed.plist"
|
|
|
|
private static let plistLibrariesUsedKey = "LibrariesUsed"
|
|
private static let plistTitleKey = "Title"
|
|
private static let plistLicenseNameKey = "LicenseName"
|
|
private static let plistLicenseTextKey = "LicenseText"
|
|
|
|
fileprivate var theme = Theme.standard
|
|
|
|
@objc func closeButtonPushed(_ : UIBarButtonItem) {
|
|
dismiss(animated: true, completion: nil)
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(named:"close"), style: .plain, target:self, action:#selector(closeButtonPushed(_:)))
|
|
navigationItem.leftBarButtonItem?.accessibilityLabel = CommonStrings.closeButtonAccessibilityLabel
|
|
}
|
|
|
|
lazy private var tableHeaderView: UIView = {
|
|
let headerFrame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: 56)
|
|
let headerView = UIView.init(frame: headerFrame)
|
|
let labelFrame = headerView.frame.insetBy(dx: 10, dy: 10)
|
|
let label = UILabel.init(frame: labelFrame)
|
|
label.adjustsFontForContentSizeCategory = true
|
|
label.font = UIFont.preferredFont(forTextStyle: .footnote)
|
|
label.textColor = self.theme.colors.primaryText
|
|
label.textAlignment = .center
|
|
label.numberOfLines = 0
|
|
label.lineBreakMode = .byWordWrapping
|
|
label.text = String.localizedStringWithFormat(WMFLocalizedString("about-libraries-licenses-title", value:"We love open source software %1$@", comment:"Title for list of library licenses. %1$@ will be replaced with an emoji expressing our love for open source software"), "💖")
|
|
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
headerView.addSubview(label)
|
|
return headerView
|
|
}()
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
self.apply(theme: self.theme)
|
|
view.backgroundColor = .gray400
|
|
tableView.register(UITableViewCell.self, forCellReuseIdentifier: LibrariesUsedViewController.cellReuseIdentifier)
|
|
tableView.estimatedRowHeight = 41
|
|
tableView.rowHeight = UITableView.automaticDimension
|
|
tableView.tableHeaderView = tableHeaderView
|
|
tableView.semanticContentAttribute = .forceLeftToRight
|
|
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target:nil, action:nil)
|
|
|
|
title = WMFLocalizedString("about-libraries", value:"Libraries used", comment:"Header text for libraries section (as in a collection of subprograms used to develop software) of the about page. Is not capitalised for aesthetic reasons, but could be capitalised in translations.")
|
|
|
|
let fileName = LibrariesUsedViewController.dataFileName
|
|
guard
|
|
let plistPath = Bundle.main.path(forResource: fileName.wmf_substring(before: "."), ofType: fileName.wmf_substring(after: "."))
|
|
else {
|
|
assertionFailure("Could find '\(fileName)' resource.")
|
|
return
|
|
}
|
|
libraries = librariesUsed(from: plistPath)
|
|
}
|
|
|
|
private func librariesUsed(from plistPath: String) -> [LibraryUsed] {
|
|
guard
|
|
let dict = NSDictionary(contentsOfFile: plistPath) as? [String: Any],
|
|
let librariesUsedDataArray = dict[LibrariesUsedViewController.plistLibrariesUsedKey] as? [[String: Any]]
|
|
else {
|
|
assertionFailure("\n\nUnexpected items found in '\(plistPath)' or its '\(LibrariesUsedViewController.plistLibrariesUsedKey)' array.\n\n")
|
|
return []
|
|
}
|
|
return librariesUsedDataArray
|
|
.compactMap {library -> LibraryUsed? in
|
|
guard
|
|
let title = library[LibrariesUsedViewController.plistTitleKey] as? String,
|
|
let licenseName = library[LibrariesUsedViewController.plistLicenseNameKey] as? String,
|
|
let licenseText = library[LibrariesUsedViewController.plistLicenseTextKey] as? String
|
|
else {
|
|
assertionFailure("\n\nOne of the following required keys not found in '\(LibrariesUsedViewController.plistLibrariesUsedKey)' array in '\(LibrariesUsedViewController.dataFileName)': '\(LibrariesUsedViewController.plistTitleKey)', '\(LibrariesUsedViewController.plistLicenseNameKey)', '\(LibrariesUsedViewController.plistLicenseTextKey)'\n\n")
|
|
return nil
|
|
}
|
|
return LibraryUsed.init(title: title.wmf_stringByCapitalizingFirstCharacter(usingWikipediaLanguageCode: "en"), licenseName: licenseName, licenseText: licenseText)
|
|
}
|
|
.sorted(by: {
|
|
$0.title < $1.title
|
|
})
|
|
}
|
|
|
|
func numberOfSections(in tableView: UITableView) -> Int {
|
|
return 1
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
return libraries.count
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: LibrariesUsedViewController.cellReuseIdentifier, for: indexPath)
|
|
cell.contentView.semanticContentAttribute = .forceLeftToRight
|
|
cell.textLabel?.semanticContentAttribute = .forceLeftToRight
|
|
cell.textLabel?.textAlignment = .left
|
|
|
|
cell.backgroundColor = theme.colors.paperBackground
|
|
cell.textLabel?.textColor = theme.colors.primaryText
|
|
|
|
cell.selectionStyle = .default
|
|
cell.selectedBackgroundView = UIView()
|
|
cell.selectedBackgroundView?.backgroundColor = theme.colors.midBackground
|
|
|
|
let library:LibraryUsed = self.libraries[indexPath.row]
|
|
cell.textLabel?.text = library.title
|
|
return cell
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
tableView.deselectRow(at: indexPath, animated: true)
|
|
let libraryVC = LibraryUsedViewController.wmf_viewControllerFromStoryboardNamed(LibrariesUsedViewController.storyboardName)
|
|
libraryVC.apply(theme: self.theme)
|
|
let library = self.libraries[indexPath.row]
|
|
libraryVC.library = library
|
|
libraryVC.title = library.title
|
|
navigationController?.pushViewController(libraryVC, animated: true)
|
|
}
|
|
}
|
|
|
|
class LibraryUsedViewController: UIViewController {
|
|
@IBOutlet weak var textView: UITextView!
|
|
public var library: LibraryUsed?
|
|
|
|
fileprivate var theme = Theme.standard
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
self.apply(theme: self.theme)
|
|
|
|
textView.adjustsFontForContentSizeCategory = true
|
|
textView.textContainerInset = UIEdgeInsets.init(top: 10, left: 10, bottom: 10, right: 10)
|
|
guard let licenseText = library?.licenseText else { return }
|
|
textView.text = normalizeWhitespaceForBetterReadability(from: licenseText)
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
textView.setContentOffset(.zero, animated: false)
|
|
}
|
|
|
|
private var newlineOptionalWhitespaceNewlineRegex: NSRegularExpression? = {
|
|
do {
|
|
return try NSRegularExpression(pattern: "\\R\\s*\\R", options: [])
|
|
} catch {
|
|
assertionFailure("regex failed to compile")
|
|
}
|
|
return nil
|
|
}()
|
|
|
|
private var oneOrMoreWhitespaceCharactersRegex: NSRegularExpression? = {
|
|
do {
|
|
return try NSRegularExpression(pattern: "\\s+", options: [])
|
|
} catch {
|
|
assertionFailure("regex failed to compile")
|
|
}
|
|
return nil
|
|
}()
|
|
|
|
// Minimal cleanups on license text.
|
|
// - consecutive line breaks reduce to 2 line breaks
|
|
// - non-consecutive line breaks converted to spaces (similar to HTML)
|
|
// Imperfect but *vast* improvement in readability especially with line wrapping.
|
|
private func normalizeWhitespaceForBetterReadability(from licenseString: String) -> String {
|
|
guard
|
|
let multiNewlineRegex = newlineOptionalWhitespaceNewlineRegex,
|
|
let whitespaceRegex = oneOrMoreWhitespaceCharactersRegex
|
|
else {
|
|
assertionFailure("regex(s) failed to compile")
|
|
return licenseString
|
|
}
|
|
var string = licenseString
|
|
let placeholder = "#temporary_placeholder#"
|
|
string = multiNewlineRegex.stringByReplacingMatches(in: string, options: [], range: string.fullRange, withTemplate: placeholder)
|
|
string = string.components(separatedBy: .newlines).joined(separator: " ")
|
|
string = whitespaceRegex.stringByReplacingMatches(in: string, options: [], range: string.fullRange, withTemplate: " ")
|
|
string = string.replacingOccurrences(of: placeholder, with: "\n\n")
|
|
return string
|
|
}
|
|
}
|
|
|
|
extension LibrariesUsedViewController: Themeable {
|
|
public func apply(theme: Theme) {
|
|
self.theme = theme
|
|
|
|
guard viewIfLoaded != nil else {
|
|
return
|
|
}
|
|
tableView.backgroundColor = theme.colors.baseBackground
|
|
tableView.separatorColor = theme.colors.chromeBackground
|
|
tableView.reloadData()
|
|
}
|
|
}
|
|
|
|
extension LibraryUsedViewController: Themeable {
|
|
public func apply(theme: Theme) {
|
|
self.theme = theme
|
|
|
|
guard viewIfLoaded != nil else {
|
|
return
|
|
}
|
|
self.view.backgroundColor = theme.colors.baseBackground
|
|
self.textView.backgroundColor = theme.colors.baseBackground
|
|
self.textView.textColor = theme.colors.primaryText
|
|
}
|
|
}
|