Skip to content
This repository was archived by the owner on Feb 15, 2022. It is now read-only.

Commit 0eaa38e

Browse files
authored
Merge pull request #278 from Lickability/limited-photo-library-access
Limited Photo Library Access Draft
2 parents cfbb391 + 4659ef6 commit 0eaa38e

File tree

11 files changed

+625
-405
lines changed

11 files changed

+625
-405
lines changed

Example/PinpointKitExample/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
<string>UIInterfaceOrientationLandscapeLeft</string>
3737
<string>UIInterfaceOrientationLandscapeRight</string>
3838
</array>
39+
<key>NSPhotoLibraryUsageDescription</key>
40+
<string>The example application accesses your photo library.</string>
3941
<key>UISupportedInterfaceOrientations~ipad</key>
4042
<array>
4143
<string>UIInterfaceOrientationPortrait</string>

Example/Pods/Pods.xcodeproj/project.pbxproj

Lines changed: 381 additions & 377 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

PinpointKit/PinpointKit.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
DA0DA60D1C53049B0012ADBE /* PinpointKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA0DA6021C53049B0012ADBE /* PinpointKit.framework */; };
7171
F24381121D54CBAB004CC87F /* SystemLogCollectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24381111D54CBAB004CC87F /* SystemLogCollectorTests.swift */; };
7272
F24381151D54ECD2004CC87F /* XCTestCaseExpectationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24381141D54ECD2004CC87F /* XCTestCaseExpectationExtensions.swift */; };
73+
F2619BA32512C6100083286C /* RequestScreenshotCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2619BA22512C6100083286C /* RequestScreenshotCell.swift */; };
7374
/* End PBXBuildFile section */
7475

7576
/* Begin PBXContainerItemProxy section */
@@ -147,6 +148,7 @@
147148
DA0DA6131C53049B0012ADBE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
148149
F24381111D54CBAB004CC87F /* SystemLogCollectorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemLogCollectorTests.swift; sourceTree = "<group>"; };
149150
F24381141D54ECD2004CC87F /* XCTestCaseExpectationExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestCaseExpectationExtensions.swift; sourceTree = "<group>"; };
151+
F2619BA22512C6100083286C /* RequestScreenshotCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RequestScreenshotCell.swift; path = Core/RequestScreenshotCell.swift; sourceTree = "<group>"; };
150152
/* End PBXFileReference section */
151153

152154
/* Begin PBXFrameworksBuildPhase section */
@@ -206,6 +208,7 @@
206208
4C4037F11D9EB01800305A6E /* FeedbackViewController.swift */,
207209
4C4037EF1D9EB01800305A6E /* FeedbackNavigationController.swift */,
208210
4C4037FC1D9EB04400305A6E /* Screenshotter.swift */,
211+
F2619BA22512C6100083286C /* RequestScreenshotCell.swift */,
209212
4C4037F91D9EB02300305A6E /* ScreenshotCell.swift */,
210213
4C4037EE1D9EB01800305A6E /* FeedbackConfiguration.swift */,
211214
4C4037F01D9EB01800305A6E /* FeedbackTableViewDataSource.swift */,
@@ -490,6 +493,7 @@
490493
4C4038091D9EB0CB00305A6E /* UIView+PinpointKit.swift in Sources */,
491494
4CFB58801E29464B00B64D0D /* EditImageViewControllerBarButtonItemProviding.swift in Sources */,
492495
4C40382C1D9EB7A800305A6E /* ScreenshotDetector.swift in Sources */,
496+
F2619BA32512C6100083286C /* RequestScreenshotCell.swift in Sources */,
493497
4C4038011D9EB07000305A6E /* LogViewer.swift in Sources */,
494498
4C4037FF1D9EB06900305A6E /* BasicLogViewController.swift in Sources */,
495499
4C4037D51D9EAF0D00305A6E /* Editor.swift in Sources */,

PinpointKit/PinpointKit/Sources/Core/FeedbackCollector.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ public protocol FeedbackCollector: class, LogSupporting, InterfaceCustomizable {
2424
/**
2525
Begins feedback collection about a screenshot from a view controller.
2626

27-
- parameter screenshot: The screenshot the user will be providing feedback on.
27+
- parameter screenshot: The screenshot the user will be providing feedback on. If the screenshot is nil, the user will be presented with a button to select a screenshot from their photo library.
2828
- parameter viewController: The view controller from which to present.
2929
*/
30-
func collectFeedback(with screenshot: UIImage, from viewController: UIViewController)
30+
func collectFeedback(with screenshot: UIImage?, from viewController: UIViewController)
3131
}
3232

3333
extension FeedbackCollector where Self: UIViewController {

PinpointKit/PinpointKit/Sources/Core/FeedbackNavigationController.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ public final class FeedbackNavigationController: UINavigationController, Feedbac
114114

115115
// MARK: - FeedbackCollector
116116

117-
public func collectFeedback(with screenshot: UIImage, from viewController: UIViewController) {
117+
public func collectFeedback(with screenshot: UIImage?, from viewController: UIViewController) {
118118
guard presentingViewController == nil else {
119-
NSLog("Unable to present FeedbackNavigationController because it is already being presetned")
119+
NSLog("Unable to present FeedbackNavigationController because it is already being presented")
120120
return
121121
}
122122

PinpointKit/PinpointKit/Sources/Core/FeedbackTableViewDataSource.swift

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ final class FeedbackTableViewDataSource: NSObject, UITableViewDataSource {
1818
Initializes the data source with a configuration and a boolean value indicating whether the user has enabled log collection.
1919

2020
- parameter interfaceCustomization: The interface customization used to set up the data source.
21-
- parameter screenshot: The screenshot to display for annotating.
21+
- parameter screenshot: The screenshot to display for annotating. If nil, a button will appear allowing the user to select a screenshot.
2222
- parameter logSupporting: The object the controls the support of logging.
2323
- parameter userEnabledLogCollection: A boolean value indicating whether the user has enabled log collection.
2424
- parameter delegate: The object informed when a screenshot is tapped.
2525
*/
26-
init(interfaceCustomization: InterfaceCustomization, screenshot: UIImage, logSupporting: LogSupporting, userEnabledLogCollection: Bool, delegate: FeedbackTableViewDataSourceDelegate? = nil) {
26+
init(interfaceCustomization: InterfaceCustomization, screenshot: UIImage?, logSupporting: LogSupporting, userEnabledLogCollection: Bool, delegate: FeedbackTableViewDataSourceDelegate? = nil) {
2727
sections = type(of: self).sectionsFromConfiguration(interfaceCustomization, screenshot: screenshot, logSupporting: logSupporting, userEnabledLogCollection: userEnabledLogCollection)
2828
self.delegate = delegate
2929
}
@@ -40,19 +40,28 @@ final class FeedbackTableViewDataSource: NSObject, UITableViewDataSource {
4040
}
4141

4242
private enum Row {
43+
case selectScreenshot(text: String, font: UIFont)
4344
case screenshot(screensot: UIImage, hintText: String?, hintFont: UIFont)
4445
case collectLogs(enabled: Bool, title: String, font: UIFont, canView: Bool)
4546
}
4647

4748
// MARK: - FeedbackTableViewDataSource
4849

49-
private static func sectionsFromConfiguration(_ interfaceCustomization: InterfaceCustomization, screenshot: UIImage, logSupporting: LogSupporting, userEnabledLogCollection: Bool) -> [Section] {
50+
private static func sectionsFromConfiguration(_ interfaceCustomization: InterfaceCustomization, screenshot: UIImage?, logSupporting: LogSupporting, userEnabledLogCollection: Bool) -> [Section] {
5051
var sections: [Section] = []
5152

52-
let screenshotRow = Row.screenshot(screensot: screenshot, hintText: interfaceCustomization.interfaceText.feedbackEditHint, hintFont: interfaceCustomization.appearance.feedbackEditHintFont)
53-
let screenshotSection = Section.feedback(rows: [screenshotRow])
54-
55-
sections.append(screenshotSection)
53+
if let screenshot = screenshot {
54+
let screenshotRow = Row.screenshot(screensot: screenshot, hintText: interfaceCustomization.interfaceText.feedbackEditHint, hintFont: interfaceCustomization.appearance.feedbackEditHintFont)
55+
let screenshotSection = Section.feedback(rows: [screenshotRow])
56+
57+
sections.append(screenshotSection)
58+
} else {
59+
let requestScreenshotRow = Row.selectScreenshot(text: interfaceCustomization.interfaceText.selectScreenshotButtonTitle, font: interfaceCustomization.appearance.selectScreenshotButtonFont)
60+
61+
let screenshotSection = Section.feedback(rows: [requestScreenshotRow])
62+
63+
sections.append(screenshotSection)
64+
}
5665

5766
if logSupporting.logCollector != nil {
5867
let collectLogsRow = Row.collectLogs(enabled: userEnabledLogCollection, title: interfaceCustomization.interfaceText.logCollectionPermissionTitle, font: interfaceCustomization.appearance.logCollectionPermissionFont, canView: logSupporting.logViewer != nil)
@@ -98,6 +107,26 @@ final class FeedbackTableViewDataSource: NSObject, UITableViewDataSource {
98107
return cell
99108
}
100109

110+
private func requestScreenshotCell(for row: Row) -> UITableViewCell {
111+
let cell = RequestScreenshotCell()
112+
113+
guard case let .selectScreenshot(text, font) = row else {
114+
assertionFailure("Found unexpected row type when creating screenshot cell.")
115+
return cell
116+
}
117+
118+
cell.viewModel = RequestScreenshotCell.ViewModel(buttonText: text, buttonFont: font)
119+
cell.screenshotButtonTapHandler = { [weak self] _ in
120+
guard let self = self else { return }
121+
122+
if #available(iOS 14, *) {
123+
self.delegate?.feedbackTableViewDataSourceDidRequestScreenshot(feedbackTableViewDataSource: self)
124+
}
125+
}
126+
127+
return cell
128+
}
129+
101130
// MARK: - UITableViewDataSource
102131

103132
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -115,6 +144,8 @@ final class FeedbackTableViewDataSource: NSObject, UITableViewDataSource {
115144
case let .feedback(rows):
116145
let row = rows[indexPath.row]
117146
switch row {
147+
case .selectScreenshot:
148+
return requestScreenshotCell(for: row)
118149
case .screenshot:
119150
return screenshotCell(for: row)
120151
case .collectLogs:
@@ -134,4 +165,12 @@ protocol FeedbackTableViewDataSourceDelegate: class {
134165
- parameter screenshot: The screenshot that was tapped.
135166
*/
136167
func feedbackTableViewDataSource(feedbackTableViewDataSource: FeedbackTableViewDataSource, didTapScreenshot screenshot: UIImage)
168+
169+
/**
170+
Notifies the delegate that a screenshot is being requested.
171+
172+
- parameter feedbackTableViewDataSource: The feedback table view data source that sent the message.
173+
*/
174+
@available(iOS 14, *)
175+
func feedbackTableViewDataSourceDidRequestScreenshot(feedbackTableViewDataSource: FeedbackTableViewDataSource)
137176
}

PinpointKit/PinpointKit/Sources/Core/FeedbackViewController.swift

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
import UIKit
10+
import PhotosUI
1011

1112
/// A `UITableViewController` that conforms to `FeedbackCollector` in order to display an interface that allows the user to see, change, and send feedback.
1213
public final class FeedbackViewController: UITableViewController {
@@ -108,7 +109,6 @@ public final class FeedbackViewController: UITableViewController {
108109

109110
private func updateDataSource() {
110111
guard let interfaceCustomization = interfaceCustomization else { assertionFailure(); return }
111-
guard let screenshot = screenshot else { assertionFailure(); return }
112112
let screenshotToDisplay = annotatedScreenshot ?? screenshot
113113

114114
dataSource = FeedbackTableViewDataSource(interfaceCustomization: interfaceCustomization, screenshot: screenshotToDisplay, logSupporting: self, userEnabledLogCollection: userEnabledLogCollection, delegate: self)
@@ -190,7 +190,7 @@ public final class FeedbackViewController: UITableViewController {
190190
// MARK: - FeedbackCollector
191191

192192
extension FeedbackViewController: FeedbackCollector {
193-
public func collectFeedback(with screenshot: UIImage, from viewController: UIViewController) {
193+
public func collectFeedback(with screenshot: UIImage?, from viewController: UIViewController) {
194194
self.screenshot = screenshot
195195
annotatedScreenshot = nil
196196
viewController.showDetailViewController(self, sender: viewController)
@@ -249,4 +249,38 @@ extension FeedbackViewController: FeedbackTableViewDataSourceDelegate {
249249
editImageViewController.modalPresentationStyle = feedbackConfiguration?.presentationStyle ?? .fullScreen
250250
present(editImageViewController, animated: true, completion: nil)
251251
}
252+
253+
@available(iOS 14, *)
254+
func feedbackTableViewDataSourceDidRequestScreenshot(feedbackTableViewDataSource: FeedbackTableViewDataSource) {
255+
var configuration = PHPickerConfiguration(photoLibrary: .shared())
256+
configuration.filter = .images
257+
258+
let pickerController = PHPickerViewController(configuration: configuration)
259+
pickerController.delegate = self
260+
viewController.present(pickerController, animated: true, completion: nil)
261+
}
262+
}
263+
264+
@available(iOS 14, *)
265+
extension FeedbackViewController: PHPickerViewControllerDelegate {
266+
267+
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
268+
guard let result = results.first else {
269+
picker.presentingViewController?.dismiss(animated: true)
270+
return
271+
}
272+
273+
result.itemProvider.loadObject(ofClass: UIImage.self, completionHandler: { image, _ in
274+
OperationQueue.main.addOperation {
275+
defer {
276+
picker.presentingViewController?.dismiss(animated: true)
277+
}
278+
279+
guard let image = image as? UIImage else { return }
280+
self.screenshot = image
281+
282+
self.tableView.reloadData()
283+
}
284+
})
285+
}
252286
}

PinpointKit/PinpointKit/Sources/Core/InterfaceCustomization.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ public struct InterfaceCustomization {
4747
/// The font used for navigation titles.
4848
let navigationTitleFont: UIFont
4949

50+
/// The font used for the select screenshot button used for `.limited` photo library access.
51+
let selectScreenshotButtonFont: UIFont
52+
5053
/// The font used for the button that sends feedback.
5154
let feedbackSendButtonFont: UIFont
5255

@@ -84,6 +87,7 @@ public struct InterfaceCustomization {
8487
- parameter annotationTextAttributes: The text attributes for annotations.
8588
- parameter navigationTitleColor: The color used for navigation titles.
8689
- parameter navigationTitleFont: The font used for navigation titles.
90+
- parameter selectScreenshotButtonFont: The font used for the select screenshot button.
8791
- parameter feedbackSendButtonFont: The font used for the button that sends feedback.
8892
- parameter feedbackCancelButtonFont: The font used for the button that cancels feedback collection.
8993
- parameter feedbackEditHintFont: The font used for the hint to the user on how to edit the screenshot from the feedback screen.
@@ -101,6 +105,7 @@ public struct InterfaceCustomization {
101105
annotationTextAttributes: [NSAttributedString.Key: AnyObject]? = nil,
102106
navigationTitleColor: UIColor = Self.defaultNavigationTitleColor,
103107
navigationTitleFont: UIFont = .sourceSansProFont(ofSize: 19, weight: .semibold),
108+
selectScreenshotButtonFont: UIFont = .sourceSansProFont(ofSize: 19, weight: .semibold),
104109
feedbackSendButtonFont: UIFont = .sourceSansProFont(ofSize: 19, weight: .semibold),
105110
feedbackCancelButtonFont: UIFont = .sourceSansProFont(ofSize: 19),
106111
feedbackEditHintFont: UIFont = .sourceSansProFont(ofSize: 14),
@@ -129,6 +134,7 @@ public struct InterfaceCustomization {
129134

130135
self.logFont = logFont
131136
self.navigationTitleFont = navigationTitleFont
137+
self.selectScreenshotButtonFont = selectScreenshotButtonFont
132138
self.feedbackSendButtonFont = feedbackSendButtonFont
133139
self.feedbackCancelButtonFont = feedbackCancelButtonFont
134140
self.feedbackEditHintFont = feedbackEditHintFont
@@ -169,6 +175,9 @@ public struct InterfaceCustomization {
169175
/// A hint to the user on how to edit the screenshot from the feedback screen.
170176
let feedbackEditHint: String?
171177

178+
/// A title to use for the select screenshot button.
179+
let selectScreenshotButtonTitle: String
180+
172181
/// The title of the log collection screen.
173182
let logCollectorTitle: String?
174183

@@ -189,6 +198,7 @@ public struct InterfaceCustomization {
189198
- parameter feedbackCancelButtonTitle: The title of the cancel button.
190199
- parameter feedbackBackButtonTitle: The title of the back button.
191200
- parameter feedbackEditHint: The hint to show during editing.
201+
- parameter selectScreenshotButtonTitle: The title of the select screenshot button.
192202
- parameter logCollectorTitle: The title of the log collector.
193203
- parameter logCollectionPermissionTitle: The title of the permission button.
194204
- parameter textEditingDismissButtonTitle: The title of the text editing dismiss button.
@@ -199,6 +209,7 @@ public struct InterfaceCustomization {
199209
feedbackCancelButtonTitle: String? = nil,
200210
feedbackBackButtonTitle: String? = NSLocalizedString("Report", comment: "Back button title of a view that reports a bug"),
201211
feedbackEditHint: String? = NSLocalizedString("Tap the screenshot to annotate.", comment: "A hint on how to edit the screenshot"),
212+
selectScreenshotButtonTitle: String = NSLocalizedString("Select Screenshot…", comment: "A button that allows screenshot selection from the photo library."),
202213
logCollectorTitle: String? = NSLocalizedString("Console Log", comment: "Title of a view that collects logs"),
203214
logCollectionPermissionTitle: String = NSLocalizedString("Include Console Log", comment: "Title of a button asking the user to include system logs"),
204215
textEditingDismissButtonTitle: String = NSLocalizedString("Dismiss", comment: "Title of a button that dismisses text editing"),
@@ -208,6 +219,7 @@ public struct InterfaceCustomization {
208219
self.feedbackCancelButtonTitle = feedbackCancelButtonTitle
209220
self.feedbackBackButtonTitle = feedbackBackButtonTitle
210221
self.feedbackEditHint = feedbackEditHint
222+
self.selectScreenshotButtonTitle = selectScreenshotButtonTitle
211223
self.logCollectorTitle = logCollectorTitle
212224
self.logCollectionPermissionTitle = logCollectionPermissionTitle
213225
self.textEditingDismissButtonTitle = textEditingDismissButtonTitle

PinpointKit/PinpointKit/Sources/Core/PinpointKit.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ open class PinpointKit {
5454
- parameter viewController: The view controller from which to present.
5555
- parameter screenshot: The screenshot to be annotated. The default value is a screenshot taken at the time this method is called. This image is intended to match the device’s screen size in points.
5656
*/
57-
open func show(from viewController: UIViewController, screenshot: UIImage = Screenshotter.takeScreenshot()) {
57+
open func show(from viewController: UIViewController, screenshot: UIImage? = Screenshotter.takeScreenshot()) {
5858
displayingViewController = viewController
5959
configuration.editor.clearAllAnnotations()
6060
configuration.feedbackCollector.collectFeedback(with: screenshot, from: viewController)

0 commit comments

Comments
 (0)