From ed9d69e1cacbc6b0084ae495a6802913eefe22d6 Mon Sep 17 00:00:00 2001 From: hgarcia-grio <hgarcia@grio.com> Date: Fri, 6 Sep 2019 13:39:30 -0500 Subject: [PATCH 1/5] Update for user feedback flow. --- BugSnap/BugSnap.xcodeproj/project.pbxproj | 12 +- .../BugSnap/Utilities/JIRAIssueCreator.swift | 34 +++- .../UIApplication+ScreenRecording.swift | 33 +++- .../Utilities/UIApplication+Snap.swift | 12 +- .../UIViewController+JIRAIssueFlow.swift | 39 +++- .../FeedbackCaptureViewController.swift | 170 ++++++++++++++---- .../FeedbackCardViewController.swift | 0 7 files changed, 249 insertions(+), 51 deletions(-) rename BugSnap/BugSnap/ViewControllers/{ => UserFeedbackFlow}/FeedbackCaptureViewController.swift (64%) rename BugSnap/BugSnap/ViewControllers/{ => UserFeedbackFlow}/FeedbackCardViewController.swift (100%) diff --git a/BugSnap/BugSnap.xcodeproj/project.pbxproj b/BugSnap/BugSnap.xcodeproj/project.pbxproj index 0d51b4b..0eb08a2 100644 --- a/BugSnap/BugSnap.xcodeproj/project.pbxproj +++ b/BugSnap/BugSnap.xcodeproj/project.pbxproj @@ -280,6 +280,7 @@ 7F0BCF9F22B04F9B006B4F26 /* ViewControllers */ = { isa = PBXGroup; children = ( + 7FBE149A2322C4E4006661BB /* UserFeedbackFlow */, 7F66ABEF22C5183400806661 /* JIRA Forms */, 7FB62E2E22B980360007A02E /* JIRA Data Selectors */, 7FB62E1B22B7D4500007A02E /* Annotation Editor Menues */, @@ -288,8 +289,6 @@ 7F66ABF722C5336D00806661 /* ScrolledViewController.swift */, 7F4C8E0122F89445001EAD96 /* CaptureOptionSheetViewController.swift */, 7FEB52FC22FC5ECC000498BC /* OptionSelectorPopupViewController.swift */, - 7F2DBE47230C9852002E5011 /* FeedbackCardViewController.swift */, - 7FC50C66230EED190066A8C3 /* FeedbackCaptureViewController.swift */, ); path = ViewControllers; sourceTree = "<group>"; @@ -426,6 +425,15 @@ path = JIRA; sourceTree = "<group>"; }; + 7FBE149A2322C4E4006661BB /* UserFeedbackFlow */ = { + isa = PBXGroup; + children = ( + 7F2DBE47230C9852002E5011 /* FeedbackCardViewController.swift */, + 7FC50C66230EED190066A8C3 /* FeedbackCaptureViewController.swift */, + ); + path = UserFeedbackFlow; + sourceTree = "<group>"; + }; 7FEBAC4D22B17FF70022158D /* CustomViews */ = { isa = PBXGroup; children = ( diff --git a/BugSnap/BugSnap/Utilities/JIRAIssueCreator.swift b/BugSnap/BugSnap/Utilities/JIRAIssueCreator.swift index 20a256d..1f12df7 100644 --- a/BugSnap/BugSnap/Utilities/JIRAIssueCreator.swift +++ b/BugSnap/BugSnap/Utilities/JIRAIssueCreator.swift @@ -31,15 +31,23 @@ public class JIRAIssueCreator: NSObject { /// The text to set in the issue private var issueText : String! + /// The description to set in the issue + private var descriptionText : String! + /// The snapshot to set in the issue - private var snapshot : UIImage! + private var snapshot : UIImage? = nil + + /// The video url to set as an attachment + private var videoURL : URL? = nil /// MARK: - Facing API - @objc public func createIssue( text : String, snapshot : UIImage ) { + @objc public func createIssue( text : String, description : String, snapshot : UIImage? = nil , videoURL : URL? = nil ) { issueText = text + descriptionText = description self.snapshot = snapshot + self.videoURL = videoURL selectProject() } @@ -58,6 +66,19 @@ public class JIRAIssueCreator: NSObject { } } + /** + Uploads the video result of the annotated screen recording. + - Parameter issue: The issue created previously with the summary and description provided by the UI. + */ + private func uploadScreenRecording( issue : JIRA.Object ) { + loadingViewController?.message = "Uploading video..." + JIRARestAPI.sharedInstance.attach(fileURL: videoURL!, mimeType: .mp4Video, issue: issue) { [weak self] (_, errors) in + + guard let strongSelf = self else { return } + strongSelf.handleAttachmentResponse(issue: issue, errors: errors, caller: strongSelf.uploadScreenRecording(issue:)) + } + } + private func uploadLogs( issue : JIRA.Object ) { guard let data = UIApplication.lastLogs() else { @@ -123,7 +144,7 @@ public class JIRAIssueCreator: NSObject { $0.value = value } else if ($0.key ?? "") == "description" { let value = JIRA.IssueField.Value() - value.stringValue = issueText + value.stringValue = descriptionText $0.value = value } else if ($0.key ?? "") == "environment" { let value = JIRA.IssueField.Value() @@ -159,7 +180,12 @@ public class JIRAIssueCreator: NSObject { return } - uploadImage(issue: issueObject) + if snapshot != nil { + uploadImage(issue: issueObject) + } else if videoURL != nil { + uploadScreenRecording(issue: issueObject) + } + } private func handleAttachmentResponse( issue : JIRA.Object , errors : [String]? , caller : @escaping (JIRA.Object)->Void ) { diff --git a/BugSnap/BugSnap/Utilities/UIApplication+ScreenRecording.swift b/BugSnap/BugSnap/Utilities/UIApplication+ScreenRecording.swift index 284f541..10d204a 100644 --- a/BugSnap/BugSnap/Utilities/UIApplication+ScreenRecording.swift +++ b/BugSnap/BugSnap/Utilities/UIApplication+ScreenRecording.swift @@ -20,6 +20,9 @@ fileprivate var _recordingIndicatorWindow = "_recordingIndicatorWindow" /// A key for storing the file for saving the video fileprivate var _avassetwriterfile = "_avassetwriterfile" +/// A key for storing whether the screen recording has been stopped +fileprivate var _recordingstopped = "_recordingStoppedKey" + /// Extension to support screen recording extension UIApplication { @@ -37,6 +40,20 @@ extension UIApplication { } } + /// Flag to know whether screen recording has been stopped. This should be only modified from the main thread + var wasScreenRecordingStopped: Bool { + get { + guard let number = objc_getAssociatedObject(self, &_recordingstopped) as? NSNumber else { return false } + return number.boolValue + } + set(newVal) { + let number = NSNumber(booleanLiteral: newVal) + willChangeValue(forKey: "wasScreenRecordingStopped") + objc_setAssociatedObject(self, &_recordingstopped, number, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + didChangeValue(forKey: "wasScreenRecordingStopped") + } + } + /// The video writer var videoWriter : VideoFileWriter? { get { @@ -83,17 +100,22 @@ extension UIApplication { let videoWriter = VideoFileWriter() self.videoWriter = videoWriter + wasScreenRecordingStopped = false RPScreenRecorder.shared().delegate = self RPScreenRecorder.shared().isMicrophoneEnabled = false RPScreenRecorder.shared().startCapture(handler: { [weak self] (buffer, bufferType, error) in guard error == nil else { return } - self?.isScreenRecording = true - if self?.screenRecordingIndicator == nil { - DispatchQueue.main.async { - let window = UIView.addRecordingIndicator() - self?.screenRecordingIndicator = window + DispatchQueue.main.async { + + if !(self?.wasScreenRecordingStopped ?? true) { + self?.isScreenRecording = true + if self?.screenRecordingIndicator == nil { + + let window = UIView.addRecordingIndicator() + self?.screenRecordingIndicator = window + } } } @@ -114,6 +136,7 @@ extension UIApplication { let loading = UIViewController.topMostViewController?.presentLoading(message: "Stopping capture...") RPScreenRecorder.shared().stopCapture { [weak self] (error) in DispatchQueue.main.async { + self?.wasScreenRecordingStopped = true self?.showEndCapture(loading: loading!, error: error) RPScreenRecorder.shared().delegate = nil } diff --git a/BugSnap/BugSnap/Utilities/UIApplication+Snap.swift b/BugSnap/BugSnap/Utilities/UIApplication+Snap.swift index 204e278..c825393 100644 --- a/BugSnap/BugSnap/Utilities/UIApplication+Snap.swift +++ b/BugSnap/BugSnap/Utilities/UIApplication+Snap.swift @@ -100,7 +100,7 @@ public extension UIApplication { guard Thread.current.isMainThread, applicationState != .background else { - NSLog("Tried to enable shake gesture on either a secondary thread or in a non active state") + NSLog("Tried to call shake gesture on either a secondary thread or in a non active state") return } @@ -108,7 +108,7 @@ public extension UIApplication { if isScreenRecording { doEndCapture() // Check whether the screen recorder is available - } else if RPScreenRecorder.shared().isAvailable && !userFeedbackFlow { + } else if RPScreenRecorder.shared().isAvailable { askSnapAction() // Otherwise defaults to the snapshot action } else { @@ -158,7 +158,7 @@ public extension UIApplication { [weak self] (image) in if self?.userFeedbackFlow ?? false { - self?.startUserFeedbackFlow(image: image!) + UIViewController.topMostViewController?.startJIRACapture(snapshot: image) } else { self?.startQAFlow(image: image) } @@ -168,15 +168,17 @@ public extension UIApplication { /** Starts the flow with the capture card in order to have some sort of automated user feedback - Parameter image: The image captured + - Parameter url: The URL for the video recording. */ - private func startUserFeedbackFlow( image : UIImage? ) { + private func startUserFeedbackFlow( image : UIImage?, url : URL? = nil ) { guard let topMost = UIViewController.topMostViewController else { return } let loading = topMost.presentLoading(message: "Loading image...") - let controller = FeedbackCardViewController() + let controller = FeedbackCaptureViewController() controller.snapshot = image + controller.videoURL = url controller.modalPresentationStyle = .overCurrentContext loading.dismiss(animated: true, completion: { diff --git a/BugSnap/BugSnap/Utilities/UIViewController+JIRAIssueFlow.swift b/BugSnap/BugSnap/Utilities/UIViewController+JIRAIssueFlow.swift index c985ff4..d9e1e29 100644 --- a/BugSnap/BugSnap/Utilities/UIViewController+JIRAIssueFlow.swift +++ b/BugSnap/BugSnap/Utilities/UIViewController+JIRAIssueFlow.swift @@ -17,11 +17,48 @@ public extension UIViewController { /** Starts the flow for capturing the issue with a snapshot or a video URL (that can be captured with the framework too). - This method presents the JIRALoginViewController to validate the credentials and then JIRAIssueFormViewController to finalize the capture of the information related to the issue. After the information is validated it can be uploaded to JIRA. + - Parameter snapshot: The snapshot to be uploaded to JIRA as attachement - Parameter videoURL: The videoURL (in the local file system, ideally in the caches directory) to be uploaded in JIRA as an attachement. */ @objc func startJIRACapture( snapshot : UIImage? = nil, videoURL : URL? = nil) { + if UIApplication.shared.userFeedbackFlow { + startUserFeedbackFlow(snapshot: snapshot, videoURL: videoURL) + } else { + startJIRAUserFeedbackFlow(snapshot: snapshot, videoURL: videoURL) + } + } + + /** + Starts the flow for user feedback with a card being presented. + - Parameter snapshot: The snapshot to be uploaded to JIRA as attachement + - Parameter videoURL: The videoURL (in the local file system, ideally in the caches directory) to be uploaded in JIRA as an attachement. + */ + private func startUserFeedbackFlow( snapshot : UIImage? = nil, videoURL : URL? = nil ) { + + guard let topMost = UIViewController.topMostViewController else { + return + } + let loading = topMost.presentLoading(message: "Loading image...") + + let controller = FeedbackCaptureViewController() + controller.snapshot = snapshot + controller.videoURL = videoURL + controller.modalPresentationStyle = .overCurrentContext + + loading.dismiss(animated: true, completion: { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: { + topMost.present(controller, animated: true, completion: nil) + }) + }) + } + + /** + This method presents the JIRALoginViewController to validate the credentials and then JIRAIssueFormViewController to finalize the capture of the information related to the issue. After the information is validated it can be uploaded to JIRA. + - Parameter snapshot: The image captured from the screen + - Parameter videoURL: The video URL for the video screen recording. + */ + private func startJIRAUserFeedbackFlow( snapshot : UIImage? = nil, videoURL : URL? = nil) { let jiraLoginViewController = JIRALoginViewController() jiraLoginViewController.modalPresentationStyle = .overCurrentContext jiraLoginViewController.modalTransitionStyle = .coverVertical diff --git a/BugSnap/BugSnap/ViewControllers/FeedbackCaptureViewController.swift b/BugSnap/BugSnap/ViewControllers/UserFeedbackFlow/FeedbackCaptureViewController.swift similarity index 64% rename from BugSnap/BugSnap/ViewControllers/FeedbackCaptureViewController.swift rename to BugSnap/BugSnap/ViewControllers/UserFeedbackFlow/FeedbackCaptureViewController.swift index dbc22b1..b6b39df 100644 --- a/BugSnap/BugSnap/ViewControllers/FeedbackCaptureViewController.swift +++ b/BugSnap/BugSnap/ViewControllers/UserFeedbackFlow/FeedbackCaptureViewController.swift @@ -7,18 +7,22 @@ // import UIKit +import AVFoundation /** View controller to show a briefing about the issue to be reported. Notice this view controller sends the data to the system for feedback and does the attachment of the screenshot. */ -public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate { +public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate, UITextFieldDelegate { // MARK: - UI Elements /// The attachment that was captured during the preview @objc public var snapshot : UIImage? = nil + /// The URL for the video attachment that was captrued during the preview + @objc public var videoURL : URL? = nil + // MARK: - Constants /// The spacing in the horizontal axis @@ -32,12 +36,18 @@ public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate /// The title label for the card private var titleLabel = UILabel() - /// The capture field - private var textView = UITextView() + /// The summary field + private var summaryField = UITextField() + + /// The description field + private var descriptionField = UITextView() /// The placeholder for the textview private var placeholder = UILabel() + /// The attachment disclosure + private var attachmentDisclosure = UILabel() + /// The attachment title private var snapshotButton = UIImageView() @@ -49,6 +59,15 @@ public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate /// The issue creator private var issueCreator : JIRAIssueCreator? = nil + + /// The video player layer for playing back the video + private var videoPlayerLayer : AVPlayerLayer? = nil + + // MARK: - Deinitialization + + deinit { + NotificationCenter.default.removeObserver(self) + } // MARK: - View LifeCycle @@ -62,6 +81,12 @@ public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate animateCardEntry() } + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + // Update the bounds for the video player layer + videoPlayerLayer?.bounds = snapshotButton.bounds + } + // MARK: - Animate card private func animateCardEntry() { @@ -74,7 +99,10 @@ public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate UIView.animate(withDuration: 0.8, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.6, options: [.beginFromCurrentState,.curveEaseInOut], animations: { self.card.transform = CGAffineTransform.identity self.view.backgroundColor = UIColor(white: 0.0, alpha: 0.3) - }, completion: nil) + }, completion: { (_) in + // Update the bounds for the video player layer + self.videoPlayerLayer?.bounds = self.snapshotButton.bounds + }) } private func animateCardDismiss() { @@ -94,8 +122,9 @@ public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate setupTitle() setupButtons() setupSeparator() - setupTextView() + setupSummaryField() setupAttachment() + setupDescriptionField() card.isHidden = true } @@ -164,31 +193,51 @@ public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate sectionSeparator.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5.0).isActive = true } - private func setupTextView() { - textView.textAlignment = .left - textView.font = UIFont(name: "HelveticaNeue", size: 16.0) - textView.textColor = UIColor.black - textView.translatesAutoresizingMaskIntoConstraints = false - textView.delegate = self + private func setupSummaryField() { + summaryField.textAlignment = .left + summaryField.font = UIFont(name: "HelveticaNeue", size: 16.0) + summaryField.textColor = UIColor.black + summaryField.translatesAutoresizingMaskIntoConstraints = false + summaryField.delegate = self + summaryField.placeholder = "Add an explanation" + summaryField.returnKeyType = .continue + + card.addSubview(summaryField) + summaryField.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: horizontalMargin).isActive = true + summaryField.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -horizontalMargin).isActive = true + summaryField.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10.0).isActive = true + summaryField.heightAnchor.constraint(equalToConstant: 40.0).isActive = true + } + + private func setupDescriptionField() { + descriptionField.textAlignment = .left + descriptionField.font = UIFont(name: "HelveticaNeue", size: 12.0) + descriptionField.textColor = UIColor.black + descriptionField.translatesAutoresizingMaskIntoConstraints = false + descriptionField.delegate = self + descriptionField.cornerRadius = 8.0 + descriptionField.borderWidth = 1.0 + descriptionField.borderColor = UIColor.lightGray - placeholder.text = "Add an explanation" - placeholder.font = UIFont(name: "HelveticaNeue", size: 16.0) + placeholder.text = "In a few words, describe the situation in more detail." + placeholder.font = UIFont(name: "HelveticaNeue", size: 12.0) placeholder.textColor = UIColor.gray placeholder.textAlignment = .left + placeholder.numberOfLines = 0 placeholder.translatesAutoresizingMaskIntoConstraints = false - textView.addSubview(placeholder) + placeholder.lineBreakMode = .byWordWrapping + descriptionField.addSubview(placeholder) - placeholder.leadingAnchor.constraint(equalTo: textView.leadingAnchor, constant: 5.0).isActive = true - placeholder.trailingAnchor.constraint(equalTo: textView.trailingAnchor).isActive = true - placeholder.topAnchor.constraint(equalTo: textView.topAnchor, constant: 5.0).isActive = true - placeholder.heightAnchor.constraint(equalToConstant: 30.0).isActive = true + placeholder.leadingAnchor.constraint(equalTo: descriptionField.leadingAnchor, constant: 5.0).isActive = true + placeholder.topAnchor.constraint(equalTo: descriptionField.topAnchor, constant: 8.0).isActive = true + placeholder.widthAnchor.constraint(equalTo: descriptionField.widthAnchor, multiplier: 0.7, constant: 0.0).isActive = true - card.addSubview(textView) - textView.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: horizontalMargin).isActive = true - textView.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -horizontalMargin).isActive = true - textView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10.0).isActive = true - textView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true + card.addSubview(descriptionField) + descriptionField.leadingAnchor.constraint(equalTo: snapshotButton.trailingAnchor, constant: 40.0).isActive = true + descriptionField.trailingAnchor.constraint(equalTo: attachmentDisclosure.trailingAnchor).isActive = true + descriptionField.topAnchor.constraint(equalTo: snapshotButton.topAnchor).isActive = true + descriptionField.bottomAnchor.constraint(equalTo: snapshotButton.bottomAnchor).isActive = true } private func setupAttachment() { @@ -203,11 +252,9 @@ public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate card.addSubview(attachmentTitle) attachmentTitle.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: horizontalMargin).isActive = true attachmentTitle.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -horizontalMargin).isActive = true - attachmentTitle.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 10.0).isActive = true + attachmentTitle.topAnchor.constraint(equalTo: summaryField.bottomAnchor, constant: 10.0).isActive = true attachmentTitle.heightAnchor.constraint(equalToConstant: 20.0).isActive = true - - snapshotButton.image = snapshot snapshotButton.cornerRadius = 5.0 snapshotButton.borderColor = UIColor.lightGray snapshotButton.borderWidth = 1.0 @@ -215,6 +262,7 @@ public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate snapshotButton.clipsToBounds = true snapshotButton.translatesAutoresizingMaskIntoConstraints = false snapshotButton.isUserInteractionEnabled = true + setupCaptureThumbnail() card.addSubview(snapshotButton) let aspect = UIScreen.main.bounds.height / UIScreen.main.bounds.width @@ -223,11 +271,7 @@ public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate snapshotButton.topAnchor.constraint(equalTo: attachmentTitle.bottomAnchor, constant: 5.0).isActive = true snapshotButton.heightAnchor.constraint(equalTo: snapshotButton.widthAnchor, multiplier: aspect).isActive = true - let tap = UITapGestureRecognizer(target: self, action: #selector(onSnapshot)) - snapshotButton.addGestureRecognizer(tap) - - let attachmentDisclosure = UILabel() - attachmentDisclosure.text = "The information about your device and this app will be included automatically in this report." + attachmentDisclosure.text = "Information about your device and this app will be included automatically in this report." attachmentDisclosure.numberOfLines = 0 attachmentDisclosure.font = UIFont(name: "HelveticaNeue", size: 12.0) attachmentDisclosure.textColor = UIColor.lightGray @@ -241,6 +285,34 @@ public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate attachmentDisclosure.bottomAnchor.constraint(lessThanOrEqualTo: card.bottomAnchor, constant: -10.0).isActive = true } + private func setupCaptureThumbnail() { + + guard let url = videoURL else { + setupSnapshot(image: snapshot! ) + return + } + setupVideo(url: url) + } + + private func setupSnapshot( image : UIImage ) { + snapshotButton.image = image + let tap = UITapGestureRecognizer(target: self, action: #selector(onSnapshot)) + snapshotButton.addGestureRecognizer(tap) + } + + private func setupVideo( url : URL ) { + let videoPlayer = AVPlayer(url: url) + videoPlayerLayer = AVPlayerLayer(player: videoPlayer) + snapshotButton.layer.addSublayer(videoPlayerLayer!) + videoPlayerLayer?.bounds = snapshotButton.bounds + videoPlayerLayer?.videoGravity = .resizeAspectFill + videoPlayerLayer?.anchorPoint = CGPoint(x: 0.0, y: 0.0) + + let tap = UITapGestureRecognizer(target: self, action: #selector(onToggleVideoPlayBack)) + snapshotButton.addGestureRecognizer(tap) + NotificationCenter.default.addObserver(self, selector: #selector(onVideoStopped(notification:)), name: .AVPlayerItemDidPlayToEndTime, object: nil) + } + // MARK: - UICallback @objc func onSnapshot() { @@ -263,15 +335,38 @@ public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate } + @objc func onToggleVideoPlayBack() { + + if videoPlayerLayer?.player?.rate ?? 0 > 0.0 { + videoPlayerLayer?.player?.pause() + } else { + videoPlayerLayer?.player?.play() + } + } + @objc func onBack() { animateCardDismiss() } @objc func onSend() { - textView.resignFirstResponder() + view.endEditing(true) issueCreator = JIRAIssueCreator() issueCreator?.viewController = self - issueCreator?.createIssue(text: textView.text, snapshot: snapshot!) + issueCreator?.createIssue(text: summaryField.text ?? "", description: descriptionField.text ?? "", snapshot: snapshot!) + } + + // MARK: - UITextViewDelegate + + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let mutableString = NSMutableString(string: textField.text ?? "") + mutableString.replaceCharacters(in: range, with: string) + sendButton.isEnabled = placeholder.isHidden && (summaryField.text?.count ?? 0) > 0 + return true + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + descriptionField.becomeFirstResponder() + return true } // MARK: - UITextViewDelegate @@ -280,7 +375,14 @@ public class FeedbackCaptureViewController: UIViewController, UITextViewDelegate let mutableString = NSMutableString(string: textView.text) mutableString.replaceCharacters(in: range, with: text) placeholder.isHidden = mutableString.length > 0 - sendButton.isEnabled = placeholder.isHidden + sendButton.isEnabled = placeholder.isHidden && (summaryField.text?.count ?? 0) > 0 return true } + + // MARK: - Notifications + + @objc func onVideoStopped( notification : Notification ) { + let scale = CMTimeScale(100.0) + videoPlayerLayer?.player?.seek(to: CMTime(seconds: 0.0, preferredTimescale: scale), completionHandler: { (_) in }) + } } diff --git a/BugSnap/BugSnap/ViewControllers/FeedbackCardViewController.swift b/BugSnap/BugSnap/ViewControllers/UserFeedbackFlow/FeedbackCardViewController.swift similarity index 100% rename from BugSnap/BugSnap/ViewControllers/FeedbackCardViewController.swift rename to BugSnap/BugSnap/ViewControllers/UserFeedbackFlow/FeedbackCardViewController.swift From 2f7a703f06e53435ff400f8f7ece58dea4d7cc60 Mon Sep 17 00:00:00 2001 From: hgarcia-grio <hgarcia@grio.com> Date: Fri, 6 Sep 2019 13:42:39 -0500 Subject: [PATCH 2/5] Increased the number of projects (need to improve this). --- BugSnap/BugSnap/Networking/JIRA-API.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BugSnap/BugSnap/Networking/JIRA-API.swift b/BugSnap/BugSnap/Networking/JIRA-API.swift index 9ed73ef..ebbac2a 100644 --- a/BugSnap/BugSnap/Networking/JIRA-API.swift +++ b/BugSnap/BugSnap/Networking/JIRA-API.swift @@ -217,7 +217,7 @@ public class JIRARestAPI : NSObject { */ func allProjects( completion : @escaping ([JIRA.Project]?)->Void) { - var request = URLRequest(url: URL(string: "rest/api/3/project/search?startAt=0&maxResults=10&orderBy=name", relativeTo: serverURL)!) + var request = URLRequest(url: URL(string: "rest/api/3/project/search?startAt=0&maxResults=50&orderBy=name", relativeTo: serverURL)!) request.httpMethod = "GET" let urlSession = URLSession(configuration: sessionConfiguration) From 03c2e29ea8c16f180c32d9b9d220a90ef77a9d8a Mon Sep 17 00:00:00 2001 From: hgarcia-grio <hgarcia@grio.com> Date: Fri, 6 Sep 2019 13:53:09 -0500 Subject: [PATCH 3/5] Exposed the askSnapAction for compatibility with React Native in Debug Mode --- BugSnap/BugSnap/Utilities/UIApplication+Snap.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/BugSnap/BugSnap/Utilities/UIApplication+Snap.swift b/BugSnap/BugSnap/Utilities/UIApplication+Snap.swift index c825393..00018d1 100644 --- a/BugSnap/BugSnap/Utilities/UIApplication+Snap.swift +++ b/BugSnap/BugSnap/Utilities/UIApplication+Snap.swift @@ -118,12 +118,18 @@ public extension UIApplication { // MARK: - Support - private func askSnapAction() { + @objc func askSnapAction() { guard let topMost = UIViewController.topMostViewController else { NSLog("Couldn't find either the key window or the top most view controller") return } + // Stop screen recording if it's recording already + guard !isScreenRecording else { + doEndCapture() + return + } + let optionSheetController = CaptureOptionSheetViewController() optionSheetController.optionSelectionHandler = { [weak self] (option) in From 2fcab2cf5691f712477ffcabc4ca5e4add156df6 Mon Sep 17 00:00:00 2001 From: hgarcia-grio <hgarcia@grio.com> Date: Fri, 6 Sep 2019 13:55:39 -0500 Subject: [PATCH 4/5] Incremented the build version number --- BugSnap Demo App/BugSnap Demo App/Info.plist | 2 +- BugSnap.podspec | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BugSnap Demo App/BugSnap Demo App/Info.plist b/BugSnap Demo App/BugSnap Demo App/Info.plist index 8521be5..c78c98a 100644 --- a/BugSnap Demo App/BugSnap Demo App/Info.plist +++ b/BugSnap Demo App/BugSnap Demo App/Info.plist @@ -19,7 +19,7 @@ <key>CFBundleShortVersionString</key> <string>1.0.4</string> <key>CFBundleVersion</key> - <string>9</string> + <string>10</string> <key>ITSAppUsesNonExemptEncryption</key> <false/> <key>LSRequiresIPhoneOS</key> diff --git a/BugSnap.podspec b/BugSnap.podspec index 84e30ad..fedf9db 100644 --- a/BugSnap.podspec +++ b/BugSnap.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "BugSnap" - spec.version = "1.0.5" + spec.version = "1.0.6" spec.summary = "POD that allows to snapshot or record the screen after a shake gesture, annotate the image and upload it to JIRA" # This description is used to generate tags and improve search results. @@ -72,7 +72,7 @@ Pod::Spec.new do |spec| # Supports git, hg, bzr, svn and HTTP. # - spec.source = { :git => "https://github.com/GrioSF/bugsnap-iOS.git", :tag => "v1.0.5" } + spec.source = { :git => "https://github.com/GrioSF/bugsnap-iOS.git", :tag => "v1.0.6" } # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # From 41eb00159a546f43fac12fab3869bf15d410c2c0 Mon Sep 17 00:00:00 2001 From: hgarcia-grio <hgarcia@grio.com> Date: Fri, 6 Sep 2019 13:56:13 -0500 Subject: [PATCH 5/5] xcode support for the podspec --- BugSnap/BugSnap.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BugSnap/BugSnap.xcodeproj/project.pbxproj b/BugSnap/BugSnap.xcodeproj/project.pbxproj index 0eb08a2..ea2f248 100644 --- a/BugSnap/BugSnap.xcodeproj/project.pbxproj +++ b/BugSnap/BugSnap.xcodeproj/project.pbxproj @@ -178,7 +178,7 @@ 7FC50C68230F080C0066A8C3 /* JIRAIssueCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JIRAIssueCreator.swift; sourceTree = "<group>"; }; 7FD2312722E8B0F100292997 /* UIApplication+Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Logging.swift"; sourceTree = "<group>"; }; 7FD2312922E8FA8D00292997 /* FileManager+Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Util.swift"; sourceTree = "<group>"; }; - 7FE43E1622F1050D000F8ABC /* BugSnap.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; name = BugSnap.podspec; path = ../../BugSnap.podspec; sourceTree = "<group>"; }; + 7FE43E1622F1050D000F8ABC /* BugSnap.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; name = BugSnap.podspec; path = ../../BugSnap.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 7FEB197A22BC050300E9C8B5 /* UIColor+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Helpers.swift"; sourceTree = "<group>"; }; 7FEB197C22BC264800E9C8B5 /* PaddedTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddedTextField.swift; sourceTree = "<group>"; }; 7FEB197E22BD91A400E9C8B5 /* AutocompleteTextFieldViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteTextFieldViewController.swift; sourceTree = "<group>"; };