Skip to content

Commit 0ba6dd9

Browse files
committed
- Fix GlossaryViewController
- Use new ArticleViewController (WKWebView + reload + share support) instead of SFSafariViewController for the News tab - Correct some table view selection
1 parent 90524ed commit 0ba6dd9

File tree

8 files changed

+169
-11
lines changed

8 files changed

+169
-11
lines changed

Unwrap.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,7 @@
597597
B93AEC4D2273DE70005A50DF /* ChallengeTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93AEC4C2273DE70005A50DF /* ChallengeTableViewCell.swift */; };
598598
B93AEC4F2273DF03005A50DF /* PreviousChallengeTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93AEC4E2273DF03005A50DF /* PreviousChallengeTableViewCell.swift */; };
599599
B96D1C3F227333BE0096492B /* GlossaryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96D1C3E227333BE0096492B /* GlossaryTableViewCell.swift */; };
600+
B9AC3C9C227B1262007AC17C /* ArticleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9AC3C9B227B1262007AC17C /* ArticleViewController.swift */; };
600601
FBCAB466480A05C9E3D694CF /* Pods_Unwrap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1042D2A80E9E96DA0B7C6F35 /* Pods_Unwrap.framework */; };
601602
FE81585F2274F8C2007E5E11 /* User-Progress.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE81585E2274F8C2007E5E11 /* User-Progress.swift */; };
602603
/* End PBXBuildFile section */
@@ -1241,6 +1242,7 @@
12411242
B93AEC4C2273DE70005A50DF /* ChallengeTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChallengeTableViewCell.swift; sourceTree = "<group>"; };
12421243
B93AEC4E2273DF03005A50DF /* PreviousChallengeTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviousChallengeTableViewCell.swift; sourceTree = "<group>"; };
12431244
B96D1C3E227333BE0096492B /* GlossaryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlossaryTableViewCell.swift; sourceTree = "<group>"; };
1245+
B9AC3C9B227B1262007AC17C /* ArticleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleViewController.swift; sourceTree = "<group>"; };
12441246
BB24D72F99ED46CE1F202A82 /* Pods-Unwrap.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Unwrap.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Unwrap/Pods-Unwrap.debug.xcconfig"; sourceTree = "<group>"; };
12451247
BE61552411B754C7764869FE /* Pods-UnwrapUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnwrapUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-UnwrapUITests/Pods-UnwrapUITests.debug.xcconfig"; sourceTree = "<group>"; };
12461248
BE64715D944F01A45D8EE2EB /* Pods-UnwrapUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnwrapUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-UnwrapUITests/Pods-UnwrapUITests.release.xcconfig"; sourceTree = "<group>"; };
@@ -1314,6 +1316,7 @@
13141316
511FD6AA2105E6EB0023E92C /* NewsDataSource.swift */,
13151317
51FA55D8211B55DE00C38188 /* NewsEmptyDataSource.swift */,
13161318
4C8950C32253633F002F9FA6 /* NewsTableViewCell.swift */,
1319+
B9AC3C9B227B1262007AC17C /* ArticleViewController.swift */,
13171320
);
13181321
path = News;
13191322
sourceTree = "<group>";
@@ -3025,6 +3028,7 @@
30253028
51CC13A12065B20500F37A67 /* Storyboarded.swift in Sources */,
30263029
5192241C210E2767007D7D74 /* DarkTheme.swift in Sources */,
30273030
51A447692108A0C3005D8665 /* BadgeDataSource.swift in Sources */,
3031+
B9AC3C9C227B1262007AC17C /* ArticleViewController.swift in Sources */,
30283032
5153F4A020EF9409009E829D /* TypeCheckerDataSource.swift in Sources */,
30293033
51053A0421135C6000B28328 /* TourItem.swift in Sources */,
30303034
51B3A7CA20CCCAF2000ACA6D /* HomeDataSource.swift in Sources */,

Unwrap/Activities/Learn/Glossary/GlossaryViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class GlossaryViewController: UITableViewController {
1515
super.viewDidLoad()
1616

1717
title = "Glossary"
18+
extendedLayoutIncludesOpaqueBars = true
1819
navigationItem.largeTitleDisplayMode = .never
1920

2021
tableView.dataSource = dataSource
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//
2+
// ArticleViewController.swift
3+
// Unwrap
4+
//
5+
// Created by Julian Schiavo on 2/5/2019.
6+
// Copyright © 2019 Hacking with Swift. All rights reserved.
7+
//
8+
9+
import UIKit
10+
import WebKit
11+
12+
class ArticleViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
13+
14+
var article: NewsArticle
15+
16+
let webView = WKWebView()
17+
let progressView = UIProgressView(progressViewStyle: .bar)
18+
var refreshButton: UIBarButtonItem?
19+
var shareButton: UIBarButtonItem?
20+
21+
private var estimatedProgressObserver: NSKeyValueObservation?
22+
23+
init(article: NewsArticle) {
24+
self.article = article
25+
super.init(nibName: nil, bundle: nil)
26+
27+
extendedLayoutIncludesOpaqueBars = true
28+
29+
webView.uiDelegate = self
30+
webView.navigationDelegate = self
31+
webView.allowsBackForwardNavigationGestures = true
32+
load(article: article)
33+
34+
setupEstimatedProgressObserver()
35+
}
36+
37+
required init?(coder aDecoder: NSCoder) {
38+
fatalError("init(coder:) has not been implemented")
39+
}
40+
41+
override func loadView() {
42+
view = UIView()
43+
44+
webView.translatesAutoresizingMaskIntoConstraints = false
45+
view.addSubview(webView)
46+
NSLayoutConstraint.activate([
47+
webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
48+
webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
49+
webView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
50+
webView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
51+
])
52+
53+
progressView.isHidden = true
54+
progressView.translatesAutoresizingMaskIntoConstraints = false
55+
view.addSubview(progressView)
56+
57+
NSLayoutConstraint.activate([
58+
progressView.heightAnchor.constraint(equalToConstant: 2.0),
59+
progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
60+
progressView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
61+
progressView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
62+
])
63+
}
64+
65+
override func viewDidLoad() {
66+
super.viewDidLoad()
67+
68+
refreshButton = UIBarButtonItem(barButtonSystemItem: .refresh, target: webView, action: #selector(WKWebView.reload))
69+
shareButton = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(shareArticle))
70+
navigationItem.setRightBarButtonItems([shareButton!, refreshButton!], animated: true)
71+
}
72+
73+
private func setupEstimatedProgressObserver() {
74+
estimatedProgressObserver = webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in
75+
guard let self = self else { return }
76+
self.progressView.setProgress(Float(webView.estimatedProgress), animated: true)
77+
}
78+
}
79+
80+
@objc private func shareArticle() {
81+
let alert = UIActivityViewController(activityItems: [article.title, article.url], applicationActivities: nil)
82+
alert.popoverPresentationController?.barButtonItem = shareButton
83+
present(alert, animated: true)
84+
}
85+
86+
/// Loads a new article
87+
func load(article: NewsArticle) {
88+
self.article = article
89+
90+
let request = URLRequest(url: article.url)
91+
webView.load(request)
92+
}
93+
94+
// MARK: WKNavigationDelegate
95+
96+
/// Allow all navigation but open third party urls
97+
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
98+
guard let url = navigationAction.request.url else {
99+
decisionHandler(.allow)
100+
return
101+
}
102+
103+
UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { (successful) in
104+
if successful {
105+
decisionHandler(.cancel)
106+
} else {
107+
decisionHandler(.allow)
108+
}
109+
}
110+
}
111+
112+
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
113+
title = article.title
114+
115+
progressView.progress = 0
116+
progressView.isHidden = false
117+
118+
refreshButton?.isEnabled = false
119+
}
120+
121+
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
122+
title = "Failed to load"
123+
refreshButton?.isEnabled = true
124+
}
125+
126+
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
127+
title = webView.title
128+
progressView.isHidden = true
129+
refreshButton?.isEnabled = true
130+
}
131+
132+
// MARK: WKUIDelegate
133+
134+
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
135+
webView.load(navigationAction.request)
136+
return nil
137+
}
138+
}

Unwrap/Activities/News/NewsCoordinator.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,28 +39,30 @@ class NewsCoordinator: Coordinator {
3939
splitViewController.delegate = SplitViewControllerDelegate.shared
4040
}
4141

42-
/// Creates and configures – but does not show! – a Safari view controller for a specific article. This might be called when the user tapped a story, or when they 3D touch one.
43-
func readViewController(for article: NewsArticle) -> UIViewController {
44-
let viewController = SFSafariViewController(url: article.url)
42+
/// Creates and configures – but does not show! – an ArticleViewController for a specific article.
43+
/// This might be called when the user tapped a story, or when they 3D touch one.
44+
func articleViewController(for article: NewsArticle) -> UIViewController {
45+
let viewController = ArticleViewController(article: article)
4546
return viewController
4647
}
4748

4849
/// Triggered when we already have a Safari view controller configured and ready to go, so we just show it.
4950
func startReading(using viewController: UIViewController, withURL url: URL) {
50-
splitViewController.showDetailViewController(viewController, sender: self)
51+
let detailNav = CoordinatedNavigationController(rootViewController: viewController)
52+
splitViewController.showDetailViewController(detailNav, sender: self)
5153
User.current.readNewsStory(forURL: url)
5254
}
5355

54-
/// Creates, configures, and presents a Safari view controller for a specific article.
56+
/// Creates, configures, and presents an ArticleViewController for a specific article.
5557
func read(_ article: NewsArticle) {
56-
let viewController = readViewController(for: article)
58+
let viewController = articleViewController(for: article)
5759
startReading(using: viewController, withURL: article.url)
5860
}
5961

6062
/// Loads the Hacking with Swift store.
6163
@objc func buyBooks() {
6264
let storeURL = URL(staticString: "https://www.hackingwithswift.com/store")
6365
let viewController = SFSafariViewController(url: storeURL)
64-
splitViewController.showDetailViewController(viewController, sender: self)
66+
splitViewController.present(viewController, animated: true)
6567
}
6668
}

Unwrap/Activities/News/NewsViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class NewsViewController: UITableViewController, UIViewControllerPreviewingDeleg
8787
if let indexPath = tableView.indexPathForRow(at: location) {
8888
previewingContext.sourceRect = tableView.rectForRow(at: indexPath)
8989
currentSelectedArticle = dataSource.article(at: indexPath.row)
90-
return coordinator?.readViewController(for: currentSelectedArticle)
90+
return coordinator?.articleViewController(for: currentSelectedArticle)
9191
}
9292

9393
return nil

Unwrap/Activities/Practice/PracticeCoordinator.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ class PracticeCoordinator: Coordinator, Awarding, Skippable, AnswerHandling {
1313
var splitViewController = UISplitViewController()
1414
var primaryNavigationController = CoordinatedNavigationController()
1515

16+
var practiceViewController = PracticeViewController(style: .plain)
17+
1618
/// Stores whichever activity the user is currently taking, so that we can make new instances of it when working through a practice sequence.
1719
var currentActivity: PracticeActivity.Type?
1820

@@ -27,9 +29,8 @@ class PracticeCoordinator: Coordinator, Awarding, Skippable, AnswerHandling {
2729
primaryNavigationController.navigationBar.prefersLargeTitles = true
2830
primaryNavigationController.coordinator = self
2931

30-
let viewController = PracticeViewController(style: .plain)
31-
viewController.coordinator = self
32-
primaryNavigationController.viewControllers = [viewController]
32+
practiceViewController.coordinator = self
33+
primaryNavigationController.viewControllers = [practiceViewController]
3334

3435
// Set up the detail view controller
3536
splitViewController.viewControllers = [primaryNavigationController, PleaseSelectViewController.instantiate()]
@@ -46,7 +47,10 @@ class PracticeCoordinator: Coordinator, Awarding, Skippable, AnswerHandling {
4647
// They can't access this practice activity yet.
4748
let alert = UIAlertController(title: "Activity Locked", message: "You need to complete the chapter \"\(activity.lockedUntil)\" before you can practice this.", preferredStyle: .alert)
4849
alert.addAction(UIAlertAction(title: "OK", style: .default))
50+
4951
splitViewController.present(alert, animated: true)
52+
splitViewController.showDetailViewController(PleaseSelectViewController.instantiate(), sender: self)
53+
5054
return false
5155
} else {
5256
// They can access this activity, so clear our state and begin immediately.
@@ -97,6 +101,7 @@ class PracticeCoordinator: Coordinator, Awarding, Skippable, AnswerHandling {
97101

98102
/// Called when the user has requested to exit the current practice session, so we should terminate it without awarding points.
99103
func skipPracticing() {
104+
practiceViewController.resetTableView()
100105
returnToStart(pointsAwarded: false)
101106
}
102107

Unwrap/Activities/Practice/PracticeViewController.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ class PracticeViewController: UITableViewController, UserTracking {
3232
tableView.reloadData()
3333
}
3434

35+
/// Deselects the currently selected row in the table view when a practice activity is stopped
36+
func resetTableView() {
37+
if let indexPath = tableView.indexPathForSelectedRow {
38+
tableView.deselectRow(at: indexPath, animated: true)
39+
}
40+
}
41+
3542
/// When the user selects a practice activity, pull it out from our data source and ask the coordinator to kick it off.
3643
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
3744
let activity = dataSource.activity(at: indexPath.row)

Unwrap/Protocols/Awarding.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,6 @@ extension Awarding {
4141
/// Returns to the root of our navigation stack.
4242
func returnToStart(pointsAwarded: Bool) {
4343
splitViewController.popToRootViewController(animated: !pointsAwarded)
44+
splitViewController.showDetailViewController(PleaseSelectViewController.instantiate(), sender: self)
4445
}
4546
}

0 commit comments

Comments
 (0)