Skip to content

Commit 949345c

Browse files
authored
Merge pull request #199 from mattrubin/info-list
Add an app info screen
2 parents 44ca43c + 78710ed commit 949345c

14 files changed

+451
-80
lines changed

.travis.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ xcode_scheme: Authenticator
88
osx_image: xcode8.3
99

1010
env:
11-
- RUNTIME="iOS 8.2" DEVICE="iPhone 4s"
12-
- RUNTIME="iOS 8.4" DEVICE="iPhone 5s"
11+
- RUNTIME="iOS 9.0" DEVICE="iPhone 4s"
12+
- RUNTIME="iOS 9.1" DEVICE="iPhone 5"
13+
- RUNTIME="iOS 9.2" DEVICE="iPhone 5s"
1314
- RUNTIME="iOS 9.3" DEVICE="iPhone 6s"
15+
- RUNTIME="iOS 10.0" DEVICE="iPhone 6s Plus"
16+
- RUNTIME="iOS 10.1" DEVICE="iPhone SE"
17+
- RUNTIME="iOS 10.2" DEVICE="iPhone 7"
1418
- RUNTIME="iOS 10.3" DEVICE="iPhone 7 Plus"
1519

1620
install:

Authenticator.xcodeproj/project.pbxproj

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
C9D6C83F1906BD68004F0E08 /* SegmentedControlRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D6C83E1906BD68004F0E08 /* SegmentedControlRow.swift */; };
5353
C9D6C8461906CD54004F0E08 /* TextFieldRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D6C8451906CD54004F0E08 /* TextFieldRow.swift */; };
5454
C9D6C84C19075044004F0E08 /* OTPProgressRing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D6C84B19075044004F0E08 /* OTPProgressRing.swift */; };
55+
C9DE02E71ED2234D00D7E01C /* InfoList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DE02E61ED2234D00D7E01C /* InfoList.swift */; };
56+
C9DE02E91ED227D600D7E01C /* InfoListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DE02E81ED227D600D7E01C /* InfoListViewController.swift */; };
5557
C9E3FB9A1E281CBC00EFA8BB /* TokenScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E3FB991E281CBC00EFA8BB /* TokenScanner.swift */; };
5658
C9E3FB9C1E2860DD00EFA8BB /* TokenScannerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E3FB9B1E2860DD00EFA8BB /* TokenScannerTests.swift */; };
5759
C9EB448E1C52A74200ACFA87 /* Component.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9EB448D1C52A74200ACFA87 /* Component.swift */; };
@@ -173,6 +175,8 @@
173175
C9D6C84B19075044004F0E08 /* OTPProgressRing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTPProgressRing.swift; sourceTree = "<group>"; };
174176
C9D844341D4C576B00D5E343 /* CONTRIBUTING.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = "<group>"; };
175177
C9D844361D4C59D600D5E343 /* CONDUCT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONDUCT.md; sourceTree = "<group>"; };
178+
C9DE02E61ED2234D00D7E01C /* InfoList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoList.swift; sourceTree = "<group>"; };
179+
C9DE02E81ED227D600D7E01C /* InfoListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoListViewController.swift; sourceTree = "<group>"; };
176180
C9E3FB991E281CBC00EFA8BB /* TokenScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenScanner.swift; sourceTree = "<group>"; };
177181
C9E3FB9B1E2860DD00EFA8BB /* TokenScannerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenScannerTests.swift; sourceTree = "<group>"; };
178182
C9EB448D1C52A74200ACFA87 /* Component.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Component.swift; sourceTree = "<group>"; };
@@ -433,6 +437,8 @@
433437
C9A1C1A71E501D5F009E65D6 /* Info */ = {
434438
isa = PBXGroup;
435439
children = (
440+
C9DE02E61ED2234D00D7E01C /* InfoList.swift */,
441+
C9DE02E81ED227D600D7E01C /* InfoListViewController.swift */,
436442
C9A1C1A81E501D8B009E65D6 /* Info.swift */,
437443
C9A1C1A51E501CB2009E65D6 /* InfoViewController.swift */,
438444
);
@@ -645,6 +651,7 @@
645651
buildActionMask = 2147483647;
646652
files = (
647653
C93AD15219CD51BE007480E9 /* Colors.swift in Sources */,
654+
C9DE02E71ED2234D00D7E01C /* InfoList.swift in Sources */,
648655
C93BD6251C16841D00FFFB8F /* RootViewController.swift in Sources */,
649656
C9919CE01BA721A1006237C1 /* ButtonHeaderView.swift in Sources */,
650657
C9EB44901C52AE4500ACFA87 /* AppController.swift in Sources */,
@@ -674,6 +681,7 @@
674681
C9CC09551BA91D1C008C54FE /* TableViewModel.swift in Sources */,
675682
C92708AC19CFB0750033128B /* TokenListViewController.swift in Sources */,
676683
C9A262DA1E176A18004E6CEB /* Demo.swift in Sources */,
684+
C9DE02E91ED227D600D7E01C /* InfoListViewController.swift in Sources */,
677685
C9E3FB9A1E281CBC00EFA8BB /* TokenScanner.swift in Sources */,
678686
C93BD6291C168EBF00FFFB8F /* RootViewModel.swift in Sources */,
679687
C9F7A8611C4D90B50082E5AE /* TokenStore.swift in Sources */,
@@ -765,7 +773,7 @@
765773
"$(BUILT_PRODUCTS_DIR)",
766774
);
767775
GCC_DYNAMIC_NO_PIC = NO;
768-
IPHONEOS_DEPLOYMENT_TARGET = 8.2;
776+
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
769777
SWIFT_VERSION = 3.0;
770778
};
771779
name = Debug;
@@ -780,7 +788,7 @@
780788
"$(BUILT_PRODUCTS_DIR)",
781789
);
782790
GCC_DYNAMIC_NO_PIC = NO;
783-
IPHONEOS_DEPLOYMENT_TARGET = 8.2;
791+
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
784792
SWIFT_VERSION = 3.0;
785793
};
786794
name = Release;

Authenticator/Source/InfoList.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//
2+
// InfoList.swift
3+
// Authenticator
4+
//
5+
// Copyright (c) 2017 Authenticator authors
6+
//
7+
// Permission is hereby granted, free of charge, to any person obtaining a copy
8+
// of this software and associated documentation files (the "Software"), to deal
9+
// in the Software without restriction, including without limitation the rights
10+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
// copies of the Software, and to permit persons to whom the Software is
12+
// furnished to do so, subject to the following conditions:
13+
//
14+
// The above copyright notice and this permission notice shall be included in all
15+
// copies or substantial portions of the Software.
16+
//
17+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
// SOFTWARE.
24+
//
25+
26+
import Foundation
27+
28+
struct InfoList {
29+
// MARK: View
30+
31+
struct ViewModel {
32+
let title: String
33+
let rowModels: [RowModel]
34+
}
35+
36+
struct RowModel {
37+
let title: String
38+
let description: String
39+
let callToAction: String
40+
let action: Effect
41+
}
42+
43+
var viewModel: ViewModel {
44+
let backupDescription = "For security reasons, tokens will be stored only on this device, and will not be included in iCloud or unencrypted backups."
45+
let licenseDescription = "Authenticator makes use of several third party libraries."
46+
47+
return ViewModel(title: "Info", rowModels: [
48+
RowModel(title: "Backups",
49+
description: backupDescription,
50+
callToAction: "Learn More →".replacingOccurrences(of: " ", with: "\u{00A0}"),
51+
action: .showBackupInfo),
52+
RowModel(title: "Open Source",
53+
description: licenseDescription,
54+
callToAction: "View Acknowledgements →".replacingOccurrences(of: " ", with: "\u{00A0}"),
55+
action: .showLicenseInfo),
56+
])
57+
}
58+
59+
// MARK: Update
60+
61+
enum Effect {
62+
case showBackupInfo
63+
case showLicenseInfo
64+
case done
65+
}
66+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
//
2+
// InfoListViewController.swift
3+
// Authenticator
4+
//
5+
// Copyright (c) 2017 Authenticator authors
6+
//
7+
// Permission is hereby granted, free of charge, to any person obtaining a copy
8+
// of this software and associated documentation files (the "Software"), to deal
9+
// in the Software without restriction, including without limitation the rights
10+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
// copies of the Software, and to permit persons to whom the Software is
12+
// furnished to do so, subject to the following conditions:
13+
//
14+
// The above copyright notice and this permission notice shall be included in all
15+
// copies or substantial portions of the Software.
16+
//
17+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
// SOFTWARE.
24+
//
25+
26+
import UIKit
27+
28+
final class InfoListViewController: UITableViewController {
29+
private var viewModel: InfoList.ViewModel
30+
private let dispatchAction: (InfoList.Effect) -> Void
31+
32+
// MARK: Initialization
33+
34+
init(viewModel: InfoList.ViewModel, dispatchAction: @escaping (InfoList.Effect) -> Void) {
35+
self.viewModel = viewModel
36+
self.dispatchAction = dispatchAction
37+
super.init(nibName: nil, bundle: nil)
38+
}
39+
40+
required init?(coder aDecoder: NSCoder) {
41+
fatalError("init(coder:) has not been implemented")
42+
}
43+
44+
func updateWithViewModel(_ viewModel: InfoList.ViewModel) {
45+
self.viewModel = viewModel
46+
applyViewModel()
47+
}
48+
49+
private func applyViewModel() {
50+
title = viewModel.title
51+
}
52+
53+
// MARK: View Lifecycle
54+
55+
override func viewDidLoad() {
56+
super.viewDidLoad()
57+
58+
tableView.backgroundColor = UIColor.otpBackgroundColor
59+
tableView.separatorStyle = .none
60+
tableView.indicatorStyle = .white
61+
self.tableView.rowHeight = UITableViewAutomaticDimension
62+
self.tableView.estimatedRowHeight = 44.0
63+
64+
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done,
65+
target: self,
66+
action: #selector(done))
67+
68+
applyViewModel()
69+
}
70+
71+
// MARK: Target Actions
72+
73+
func done() {
74+
dispatchAction(.done)
75+
}
76+
77+
// MARK: UITableViewDataSource
78+
79+
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
80+
return viewModel.rowModels.count
81+
}
82+
83+
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
84+
let cell = tableView.dequeueReusableCellWithClass(InfoListCell.self)
85+
updateCell(cell, forRowAtIndexPath: indexPath)
86+
return cell
87+
}
88+
89+
fileprivate func updateCell(_ cell: InfoListCell, forRowAtIndexPath indexPath: IndexPath) {
90+
let rowModel = viewModel.rowModels[indexPath.row]
91+
cell.updateWithRowModel(rowModel)
92+
}
93+
94+
// MARK: UITableViewDelegate
95+
96+
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
97+
let rowModel = viewModel.rowModels[indexPath.row]
98+
dispatchAction(rowModel.action)
99+
}
100+
}
101+
102+
class InfoListCell: UITableViewCell {
103+
private static let titleFont = UIFont.systemFont(ofSize: 18, weight: UIFontWeightMedium)
104+
private static let descriptionFont = UIFont.systemFont(ofSize: 15, weight: UIFontWeightLight)
105+
private static let callToActionFont = UIFont.systemFont(ofSize: 15, weight: UIFontWeightSemibold)
106+
private static let paragraphStyle: NSParagraphStyle = {
107+
let paragraphStyle = NSMutableParagraphStyle()
108+
paragraphStyle.lineHeightMultiple = 1.3
109+
return paragraphStyle
110+
}()
111+
112+
let titleLabel = UILabel()
113+
let descriptionLabel = UILabel()
114+
var customConstraints: [NSLayoutConstraint]?
115+
116+
// MARK: Initialization
117+
118+
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
119+
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
120+
configureCell()
121+
}
122+
123+
required init?(coder aDecoder: NSCoder) {
124+
super.init(coder: aDecoder)
125+
configureCell()
126+
}
127+
128+
private func configureCell() {
129+
backgroundColor = .otpBackgroundColor
130+
131+
titleLabel.textColor = .otpForegroundColor
132+
titleLabel.font = InfoListCell.titleFont
133+
titleLabel.translatesAutoresizingMaskIntoConstraints = false
134+
contentView.addSubview(titleLabel)
135+
136+
descriptionLabel.textColor = .otpForegroundColor
137+
descriptionLabel.numberOfLines = 0
138+
descriptionLabel.font = InfoListCell.descriptionFont
139+
descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
140+
contentView.addSubview(descriptionLabel)
141+
142+
selectedBackgroundView = UIView()
143+
selectedBackgroundView?.backgroundColor = UIColor(white: 0, alpha: 0.25)
144+
145+
setNeedsUpdateConstraints()
146+
}
147+
148+
override func updateConstraints() {
149+
if customConstraints == nil {
150+
let newConstraints = [
151+
titleLabel.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
152+
titleLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
153+
titleLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
154+
titleLabel.bottomAnchor.constraint(equalTo: descriptionLabel.topAnchor),
155+
descriptionLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
156+
descriptionLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
157+
descriptionLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor),
158+
]
159+
contentView.addConstraints(newConstraints)
160+
customConstraints = newConstraints
161+
}
162+
163+
// "Important: Call [super updateConstraints] as the final step in your implementation."
164+
super.updateConstraints()
165+
}
166+
167+
// MARK: Update
168+
169+
func updateWithRowModel(_ rowModel: InfoList.RowModel) {
170+
titleLabel.text = rowModel.title
171+
172+
let attributes = [NSParagraphStyleAttributeName: InfoListCell.paragraphStyle]
173+
let attributedDetails = NSMutableAttributedString(string: rowModel.description + " " + rowModel.callToAction,
174+
attributes: attributes)
175+
attributedDetails.addAttribute(NSFontAttributeName, value: InfoListCell.callToActionFont,
176+
range: (attributedDetails.string as NSString).range(of: rowModel.callToAction))
177+
descriptionLabel.attributedText = attributedDetails
178+
}
179+
}

Authenticator/Source/InfoViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import UIKit
2727
import WebKit
2828

29-
class InfoViewController: UIViewController, WKNavigationDelegate {
29+
final class InfoViewController: UIViewController, WKNavigationDelegate {
3030
private var viewModel: Info.ViewModel
3131
private let dispatchAction: (Info.Effect) -> Void
3232

0 commit comments

Comments
 (0)