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

Limited Photo Library Access Draft #278

Merged
merged 13 commits into from
Sep 18, 2020
2 changes: 2 additions & 0 deletions Example/PinpointKitExample/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSPhotoLibraryUsageDescription</key>
<string>The example application accesses your photo library.</string>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
Expand Down
758 changes: 381 additions & 377 deletions Example/Pods/Pods.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions PinpointKit/PinpointKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
DA0DA60D1C53049B0012ADBE /* PinpointKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA0DA6021C53049B0012ADBE /* PinpointKit.framework */; };
F24381121D54CBAB004CC87F /* SystemLogCollectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24381111D54CBAB004CC87F /* SystemLogCollectorTests.swift */; };
F24381151D54ECD2004CC87F /* XCTestCaseExpectationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24381141D54ECD2004CC87F /* XCTestCaseExpectationExtensions.swift */; };
F2619BA32512C6100083286C /* RequestScreenshotCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2619BA22512C6100083286C /* RequestScreenshotCell.swift */; };
/* End PBXBuildFile section */

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

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -206,6 +208,7 @@
4C4037F11D9EB01800305A6E /* FeedbackViewController.swift */,
4C4037EF1D9EB01800305A6E /* FeedbackNavigationController.swift */,
4C4037FC1D9EB04400305A6E /* Screenshotter.swift */,
F2619BA22512C6100083286C /* RequestScreenshotCell.swift */,
4C4037F91D9EB02300305A6E /* ScreenshotCell.swift */,
4C4037EE1D9EB01800305A6E /* FeedbackConfiguration.swift */,
4C4037F01D9EB01800305A6E /* FeedbackTableViewDataSource.swift */,
Expand Down Expand Up @@ -490,6 +493,7 @@
4C4038091D9EB0CB00305A6E /* UIView+PinpointKit.swift in Sources */,
4CFB58801E29464B00B64D0D /* EditImageViewControllerBarButtonItemProviding.swift in Sources */,
4C40382C1D9EB7A800305A6E /* ScreenshotDetector.swift in Sources */,
F2619BA32512C6100083286C /* RequestScreenshotCell.swift in Sources */,
4C4038011D9EB07000305A6E /* LogViewer.swift in Sources */,
4C4037FF1D9EB06900305A6E /* BasicLogViewController.swift in Sources */,
4C4037D51D9EAF0D00305A6E /* Editor.swift in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions PinpointKit/PinpointKit/Sources/Core/FeedbackCollector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ public protocol FeedbackCollector: class, LogSupporting, InterfaceCustomizable {
/**
Begins feedback collection about a screenshot from a view controller.

- parameter screenshot: The screenshot the user will be providing feedback on.
- 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.
- parameter viewController: The view controller from which to present.
*/
func collectFeedback(with screenshot: UIImage, from viewController: UIViewController)
func collectFeedback(with screenshot: UIImage?, from viewController: UIViewController)
}

extension FeedbackCollector where Self: UIViewController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ public final class FeedbackNavigationController: UINavigationController, Feedbac

// MARK: - FeedbackCollector

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ final class FeedbackTableViewDataSource: NSObject, UITableViewDataSource {
Initializes the data source with a configuration and a boolean value indicating whether the user has enabled log collection.

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

private enum Row {
case selectScreenshot(text: String, font: UIFont)
case screenshot(screensot: UIImage, hintText: String?, hintFont: UIFont)
case collectLogs(enabled: Bool, title: String, font: UIFont, canView: Bool)
}

// MARK: - FeedbackTableViewDataSource

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

let screenshotRow = Row.screenshot(screensot: screenshot, hintText: interfaceCustomization.interfaceText.feedbackEditHint, hintFont: interfaceCustomization.appearance.feedbackEditHintFont)
let screenshotSection = Section.feedback(rows: [screenshotRow])

sections.append(screenshotSection)
if let screenshot = screenshot {
let screenshotRow = Row.screenshot(screensot: screenshot, hintText: interfaceCustomization.interfaceText.feedbackEditHint, hintFont: interfaceCustomization.appearance.feedbackEditHintFont)
let screenshotSection = Section.feedback(rows: [screenshotRow])

sections.append(screenshotSection)
} else {
let requestScreenshotRow = Row.selectScreenshot(text: interfaceCustomization.interfaceText.selectScreenshotButtonTitle, font: interfaceCustomization.appearance.selectScreenshotButtonFont)

let screenshotSection = Section.feedback(rows: [requestScreenshotRow])

sections.append(screenshotSection)
}

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

private func requestScreenshotCell(for row: Row) -> UITableViewCell {
let cell = RequestScreenshotCell()

guard case let .selectScreenshot(text, font) = row else {
assertionFailure("Found unexpected row type when creating screenshot cell.")
return cell
}

cell.viewModel = RequestScreenshotCell.ViewModel(buttonText: text, buttonFont: font)
cell.screenshotButtonTapHandler = { [weak self] _ in
guard let self = self else { return }

if #available(iOS 14, *) {
self.delegate?.feedbackTableViewDataSourceDidRequestScreenshot(feedbackTableViewDataSource: self)
}
}

return cell
}

// MARK: - UITableViewDataSource

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
Expand All @@ -115,6 +144,8 @@ final class FeedbackTableViewDataSource: NSObject, UITableViewDataSource {
case let .feedback(rows):
let row = rows[indexPath.row]
switch row {
case .selectScreenshot:
return requestScreenshotCell(for: row)
case .screenshot:
return screenshotCell(for: row)
case .collectLogs:
Expand All @@ -134,4 +165,12 @@ protocol FeedbackTableViewDataSourceDelegate: class {
- parameter screenshot: The screenshot that was tapped.
*/
func feedbackTableViewDataSource(feedbackTableViewDataSource: FeedbackTableViewDataSource, didTapScreenshot screenshot: UIImage)

/**
Notifies the delegate that a screenshot is being requested.

- parameter feedbackTableViewDataSource: The feedback table view data source that sent the message.
*/
@available(iOS 14, *)
func feedbackTableViewDataSourceDidRequestScreenshot(feedbackTableViewDataSource: FeedbackTableViewDataSource)
}
38 changes: 36 additions & 2 deletions PinpointKit/PinpointKit/Sources/Core/FeedbackViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import UIKit
import PhotosUI

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

private func updateDataSource() {
guard let interfaceCustomization = interfaceCustomization else { assertionFailure(); return }
guard let screenshot = screenshot else { assertionFailure(); return }
let screenshotToDisplay = annotatedScreenshot ?? screenshot

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

extension FeedbackViewController: FeedbackCollector {
public func collectFeedback(with screenshot: UIImage, from viewController: UIViewController) {
public func collectFeedback(with screenshot: UIImage?, from viewController: UIViewController) {
self.screenshot = screenshot
annotatedScreenshot = nil
viewController.showDetailViewController(self, sender: viewController)
Expand Down Expand Up @@ -249,4 +249,38 @@ extension FeedbackViewController: FeedbackTableViewDataSourceDelegate {
editImageViewController.modalPresentationStyle = feedbackConfiguration?.presentationStyle ?? .fullScreen
present(editImageViewController, animated: true, completion: nil)
}

@available(iOS 14, *)
func feedbackTableViewDataSourceDidRequestScreenshot(feedbackTableViewDataSource: FeedbackTableViewDataSource) {
var configuration = PHPickerConfiguration(photoLibrary: .shared())
configuration.filter = .images

let pickerController = PHPickerViewController(configuration: configuration)
pickerController.delegate = self
viewController.present(pickerController, animated: true, completion: nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any way to limit this to just screenshots via the configuration (none that I’ve seen so far, but maybe there’s an API for a predicate (like we use in screenshot detector) that I’ve missed)? At the very least, we should apply the PHPickerFilter for images, as you can currently select videos and live photos.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I limited it using the filter, but through this API, I'm not sure there's a better way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, not really.

}
}

@available(iOS 14, *)
extension FeedbackViewController: PHPickerViewControllerDelegate {

public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
guard let result = results.first else {
picker.presentingViewController?.dismiss(animated: true)
return
}

result.itemProvider.loadObject(ofClass: UIImage.self, completionHandler: { image, _ in
OperationQueue.main.addOperation {
defer {
picker.presentingViewController?.dismiss(animated: true)
}

guard let image = image as? UIImage else { return }
self.screenshot = image

self.tableView.reloadData()
}
})
}
}
11 changes: 11 additions & 0 deletions PinpointKit/PinpointKit/Sources/Core/InterfaceCustomization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public struct InterfaceCustomization {
/// The font used for navigation titles.
let navigationTitleFont: UIFont

/// The font used for the select screenshot button used for `.limited` photo library access.
let selectScreenshotButtonFont: UIFont

/// The font used for the button that sends feedback.
let feedbackSendButtonFont: UIFont

Expand Down Expand Up @@ -101,6 +104,7 @@ public struct InterfaceCustomization {
annotationTextAttributes: [NSAttributedString.Key: AnyObject]? = nil,
navigationTitleColor: UIColor = Self.defaultNavigationTitleColor,
navigationTitleFont: UIFont = .sourceSansProFont(ofSize: 19, weight: .semibold),
selectScreenshotButtonFont: UIFont = .sourceSansProFont(ofSize: 19, weight: .semibold),
feedbackSendButtonFont: UIFont = .sourceSansProFont(ofSize: 19, weight: .semibold),
feedbackCancelButtonFont: UIFont = .sourceSansProFont(ofSize: 19),
feedbackEditHintFont: UIFont = .sourceSansProFont(ofSize: 14),
Expand Down Expand Up @@ -129,6 +133,7 @@ public struct InterfaceCustomization {

self.logFont = logFont
self.navigationTitleFont = navigationTitleFont
self.selectScreenshotButtonFont = selectScreenshotButtonFont
self.feedbackSendButtonFont = feedbackSendButtonFont
self.feedbackCancelButtonFont = feedbackCancelButtonFont
self.feedbackEditHintFont = feedbackEditHintFont
Expand Down Expand Up @@ -169,6 +174,9 @@ public struct InterfaceCustomization {
/// A hint to the user on how to edit the screenshot from the feedback screen.
let feedbackEditHint: String?

/// A title to use for the select screenshot button.
let selectScreenshotButtonTitle: String

/// The title of the log collection screen.
let logCollectorTitle: String?

Expand All @@ -189,6 +197,7 @@ public struct InterfaceCustomization {
- parameter feedbackCancelButtonTitle: The title of the cancel button.
- parameter feedbackBackButtonTitle: The title of the back button.
- parameter feedbackEditHint: The hint to show during editing.
- parameter selectScreenshotButtonTitle: The title of the select screenshot button.
- parameter logCollectorTitle: The title of the log collector.
- parameter logCollectionPermissionTitle: The title of the permission button.
- parameter textEditingDismissButtonTitle: The title of the text editing dismiss button.
Expand All @@ -199,6 +208,7 @@ public struct InterfaceCustomization {
feedbackCancelButtonTitle: String? = nil,
feedbackBackButtonTitle: String? = NSLocalizedString("Report", comment: "Back button title of a view that reports a bug"),
feedbackEditHint: String? = NSLocalizedString("Tap the screenshot to annotate.", comment: "A hint on how to edit the screenshot"),
selectScreenshotButtonTitle: String = NSLocalizedString("Select Screenshot…", comment: "A button that allows screenshot selection from the photo library."),
logCollectorTitle: String? = NSLocalizedString("Console Log", comment: "Title of a view that collects logs"),
logCollectionPermissionTitle: String = NSLocalizedString("Include Console Log", comment: "Title of a button asking the user to include system logs"),
textEditingDismissButtonTitle: String = NSLocalizedString("Dismiss", comment: "Title of a button that dismisses text editing"),
Expand All @@ -208,6 +218,7 @@ public struct InterfaceCustomization {
self.feedbackCancelButtonTitle = feedbackCancelButtonTitle
self.feedbackBackButtonTitle = feedbackBackButtonTitle
self.feedbackEditHint = feedbackEditHint
self.selectScreenshotButtonTitle = selectScreenshotButtonTitle
self.logCollectorTitle = logCollectorTitle
self.logCollectionPermissionTitle = logCollectionPermissionTitle
self.textEditingDismissButtonTitle = textEditingDismissButtonTitle
Expand Down
2 changes: 1 addition & 1 deletion PinpointKit/PinpointKit/Sources/Core/PinpointKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ open class PinpointKit {
- parameter viewController: The view controller from which to present.
- 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.
*/
open func show(from viewController: UIViewController, screenshot: UIImage = Screenshotter.takeScreenshot()) {
open func show(from viewController: UIViewController, screenshot: UIImage? = Screenshotter.takeScreenshot()) {
displayingViewController = viewController
configuration.editor.clearAllAnnotations()
configuration.feedbackCollector.collectFeedback(with: screenshot, from: viewController)
Expand Down
Loading