amplify-swift/Amplify/DevMenu/View/IssueReporter.swift

201 lines
7.1 KiB
Swift

//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import SwiftUI
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
/// Issue report screen in developer menu
#if canImport(UIKit)
struct IssueReporter: View {
@State var issueDescription: String = ""
@State var includeLogs = true
@State var includeEnvInfo = true
@State var includeDeviceInfo = true
@State var showAlertIfInvalidURL = false
private static let minCharacterLimit = 140
private let issueDescriptionHint = "Please provide a short description of the issue.."
private let includeLogsText = "Include logs"
private let includeEnvInfoText = "Include environment information"
private let includeDeviceInfoText = "Include device information"
private let reportOnGithubButtonText = "Report on Github"
private let copyToClipboardButtonText = "Copy to Clipboard"
private let screenTitle = "Report Issue"
private let amplifyIosNewIssueUrl = "https://github.com/aws-amplify/amplify-ios/issues/new?&title=&body="
private let githubURLErrorTitle = "Error"
private let githubURLErrorMessage = "Unable to parse Github URL. Please use the Copy to Clipboard option."
private let moreCharactersRequired = "Characters required"
var body: some View {
ScrollView {
VStack {
MultilineTextField(text: $issueDescription, placeHolderText: issueDescriptionHint)
.border(Color.gray)
.frame(height: 350)
HStack {
Text("\(moreCharactersRequired): \(remainingCharactersRequired())")
.foregroundColor(Color.secondary)
.font(.system(size: 15))
Spacer()
}.padding(.bottom)
Toggle(isOn: $includeEnvInfo) {
Text(includeEnvInfoText).bold()
}.padding(.bottom)
Toggle(isOn: $includeDeviceInfo) {
Text(includeDeviceInfoText).bold()
}.padding(.bottom)
Spacer()
Button(reportOnGithubButtonText, action: reportToGithub)
.padding()
.font(.subheadline)
.frame(maxWidth: .infinity)
.border(Color.blue)
.padding(.bottom)
.alert(isPresented: $showAlertIfInvalidURL) {
Alert(title: Text(githubURLErrorTitle),
message: Text(githubURLErrorMessage),
dismissButton: .default(Text("OK")))
}
.disabled(shouldDisableReporting())
Button(copyToClipboardButtonText, action: copyToClipboard)
.padding()
.font(.subheadline)
.frame(maxWidth: .infinity)
.border(Color.blue)
.disabled(shouldDisableReporting())
}.padding()
.navigationBarTitle(Text(screenTitle))
}
}
/// Get number of extra characters required for a valid issue description length
/// Returns 0 if issue description length fulfills `minCharacterLimit` criteria
private func remainingCharactersRequired() -> Int {
return max(IssueReporter.minCharacterLimit - issueDescription.count, 0)
}
private func shouldDisableReporting() -> Bool {
return remainingCharactersRequired() > 0
}
/// Open Amplify iOS issue logging screen on Github
private func reportToGithub() {
let issueDescriptionMarkdown =
IssueInfoHelper.generateMarkdownForIssue(
issue: IssueInfo(issueDescription: issueDescription,
includeEnvInfo: includeEnvInfo,
includeDeviceInfo: includeDeviceInfo))
let urlString = amplifyIosNewIssueUrl + issueDescriptionMarkdown
guard let url = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
showAlertIfInvalidURL = true
return
}
guard let urlToOpen = URL(string: url) else {
showAlertIfInvalidURL = true
return
}
UIApplication.shared.open(urlToOpen)
}
/// Copy issue as a markdown string to clipboard
private func copyToClipboard() {
let issue = IssueInfo(issueDescription: issueDescription,
includeEnvInfo: includeEnvInfo,
includeDeviceInfo: includeDeviceInfo)
let value = IssueInfoHelper.generateMarkdownForIssue(issue: issue)
#if canImport(UIKit)
UIPasteboard.general.string = value
#elseif canImport(AppKit)
NSPasteboard.general.setString(value, forType: .string)
#endif
}
}
final class IssueReporter_Previews: PreviewProvider {
static var previews: some View {
IssueReporter()
}
}
/// Custom defined view for multi line text field
final class MultilineTextField: UIViewRepresentable {
@Binding var text: String
var placeHolderText: String = ""
init(text: Binding<String>, placeHolderText: String) {
self._text = text
self.placeHolderText = placeHolderText
}
func makeUIView(context: UIViewRepresentableContext<MultilineTextField>) -> UITextView {
let view = UITextView()
view.isScrollEnabled = true
view.isEditable = true
view.isUserInteractionEnabled = true
view.delegate = context.coordinator
view.font = .systemFont(ofSize: 15)
view.textColor = .secondaryLabel
view.text = placeHolderText
/// add a dismiss button in UIToolbar for keyboard
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 35))
let emptySpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let dismissButton = UIBarButtonItem(title: "Dismiss", style: .done,
target: self, action: #selector(dismissKeyboard))
toolbar.setItems([emptySpace, dismissButton], animated: false)
toolbar.sizeToFit()
view.inputAccessoryView = toolbar
return view
}
@objc func dismissKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
func makeCoordinator() -> Coordinator {
return MultilineTextField.Coordinator(parent: self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: MultilineTextField
init(parent: MultilineTextField) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
func textViewDidBeginEditing(_ textView: UITextView) {
if textView.textColor == .secondaryLabel {
textView.text = nil
textView.textColor = .label
}
}
}
}
#endif