From 508d73662e64c2a4c1a1782f3a6c54b4ec117529 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Mon, 4 Nov 2024 10:51:45 -0900 Subject: [PATCH 01/12] wip building the form --- Sentry.xcodeproj/project.pbxproj | 4 + .../SentryUserFeedbackConfiguration.swift | 4 + .../SentryUserFeedbackFormConfiguration.swift | 2 +- .../UserFeedback/SentryUserFeedbackForm.swift | 143 ++++++++++++++++++ .../SentryUserFeedbackWidget.swift | 49 ++++-- .../SentryUserFeedbackWidgetButtonView.swift | 35 ++--- 6 files changed, 202 insertions(+), 35 deletions(-) create mode 100644 Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index ae4646f023..12d8ff5f45 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -690,6 +690,7 @@ 84AEB46A2C2F97FC007E46E1 /* ArrayAccesses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEB4682C2F9673007E46E1 /* ArrayAccesses.swift */; }; 84AF45A629A7FFA500FBB177 /* SentryProfiledTracerConcurrency.h in Headers */ = {isa = PBXBuildFile; fileRef = 84AF45A429A7FFA500FBB177 /* SentryProfiledTracerConcurrency.h */; }; 84AF45A729A7FFA500FBB177 /* SentryProfiledTracerConcurrency.mm in Sources */ = {isa = PBXBuildFile; fileRef = 84AF45A529A7FFA500FBB177 /* SentryProfiledTracerConcurrency.mm */; }; + 84B0DFF42CD2CF64007FB332 /* SentryUserFeedbackForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B0DFF32CD2CF64007FB332 /* SentryUserFeedbackForm.swift */; }; 84B7FA3529B285FC00AD93B1 /* Sentry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63AA759B1EB8AEF500D153DE /* Sentry.framework */; }; 84B7FA3C29B2876F00AD93B1 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF3DD6243DD4A1008A5414 /* TestConstants.swift */; }; 84B7FA3D29B2879C00AD93B1 /* libSentryTestUtils.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8431F00A29B284F200D8DC56 /* libSentryTestUtils.a */; }; @@ -1767,6 +1768,7 @@ 84AEB4682C2F9673007E46E1 /* ArrayAccesses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayAccesses.swift; sourceTree = ""; }; 84AF45A429A7FFA500FBB177 /* SentryProfiledTracerConcurrency.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryProfiledTracerConcurrency.h; path = ../include/SentryProfiledTracerConcurrency.h; sourceTree = ""; }; 84AF45A529A7FFA500FBB177 /* SentryProfiledTracerConcurrency.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryProfiledTracerConcurrency.mm; sourceTree = ""; }; + 84B0DFF32CD2CF64007FB332 /* SentryUserFeedbackForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserFeedbackForm.swift; sourceTree = ""; }; 84B7FA3B29B2866200AD93B1 /* SentryTestUtils-ObjC-BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryTestUtils-ObjC-BridgingHeader.h"; sourceTree = ""; }; 84BA62262CAE2EEF0049F636 /* SentryUserFeedbackWidgetButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserFeedbackWidgetButtonView.swift; sourceTree = ""; }; 84C47B2B2A09239100DAEB8A /* .codecov.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .codecov.yml; sourceTree = ""; }; @@ -3522,6 +3524,7 @@ 84CFA4CB2C9E0CA3008DA5F4 /* SentryUserFeedbackIntegration.h */, 84CFA4CC2C9E0CA3008DA5F4 /* SentryUserFeedbackIntegration.m */, 84CFA4C92C9DF884008DA5F4 /* SentryUserFeedbackWidget.swift */, + 84B0DFF32CD2CF64007FB332 /* SentryUserFeedbackForm.swift */, 84BA62262CAE2EEF0049F636 /* SentryUserFeedbackWidgetButtonView.swift */, 84E13B832CBF1D91003B52EC /* SentryUserFeedbackWidgetButtonMegaphoneIconView.swift */, ); @@ -4702,6 +4705,7 @@ 629428802CB3BF69002C454C /* SwizzleClassNameExclude.swift in Sources */, 638DC9A11EBC6B6400A66E41 /* SentryRequestOperation.m in Sources */, 63AA767A1EB8D20500D153DE /* SentryLogC.m in Sources */, + 84B0DFF42CD2CF64007FB332 /* SentryUserFeedbackForm.swift in Sources */, 6344DDBA1EC3115C00D9160D /* SentryCrashReportConverter.m in Sources */, D8739CF32BECF70F007D2F66 /* SentryLevel.swift in Sources */, 63FE70FD20DA4C1000CDBAE8 /* SentryCrashCachedData.c in Sources */, diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift index d19d6797e5..5f4bbcc699 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift @@ -127,6 +127,10 @@ public class SentryUserFeedbackConfiguration: NSObject { scaleFactor > 1 ? 1 : scaleFactor }() + // MARK: Layout + + let padding: CGFloat = 16 + let spacing: CGFloat = 8 } #endif // os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift index 609aa58a71..4e868ed1d5 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift @@ -39,7 +39,7 @@ public class SentryUserFeedbackFormConfiguration: NSObject { * The label shown next to an input field that is required. * - note: Default: `"(required)"` */ - public var isRequiredLabel: String = "(required)" + public var isRequiredLabel: String = "(Required)" /** * The message displayed after a successful feedback submission. diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift new file mode 100644 index 0000000000..6bf83e9e70 --- /dev/null +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -0,0 +1,143 @@ +import Foundation +#if os(iOS) && !SENTRY_NO_UIKIT +@_implementationOnly import _SentryPrivate +import UIKit + +@available(iOS 13.0, *) +protocol SentryUserFeedbackFormDelegate: NSObjectProtocol { + func cancelled() + func confirmed() +} + +@available(iOS 13.0, *) +@objcMembers +class SentryUserFeedbackForm: UIViewController { + let config: SentryUserFeedbackConfiguration + weak var delegate: (any SentryUserFeedbackFormDelegate)? + + init(config: SentryUserFeedbackConfiguration, delegate: any SentryUserFeedbackFormDelegate) { + self.config = config + self.delegate = delegate + super.init(nibName: nil, bundle: nil) + layoutUI(config) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Actions + + func addScreenshotButtonTapped() { + + } + + func submitFeedbackButtonTapped() { + // ???: marshal data how? + delegate?.confirmed() + } + + func cancelButtonTapped() { + delegate?.cancelled() + } + + // MARK: UI + + lazy var fullNameLabel = { + let label = UILabel(frame: .zero) + label.text = config.formConfig.nameLabel + return label + }() + + lazy var fullNameTextField = { + let field = UITextField(frame: .zero) + field.placeholder = config.formConfig.namePlaceholder + field.borderStyle = .roundedRect + return field + }() + + lazy var emailLabel = { + let label = UILabel(frame: .zero) + label.text = config.formConfig.emailLabel + return label + }() + + lazy var emailTextField = { + let field = UITextField(frame: .zero) + field.placeholder = config.formConfig.emailPlaceholder + field.borderStyle = .roundedRect + return field + }() + + lazy var messageLabel = { + let label = UILabel(frame: .zero) + label.text = config.formConfig.messageLabel + return label + }() + + lazy var messageTextView = { + let textView = UITextView(frame: .zero) + textView.text = config.formConfig.messagePlaceholder // TODO: color the text as placeholder if this is the content of the textview, otherwise change to regular foreground color + textView.isScrollEnabled = true + textView.isEditable = true + textView.layer.borderWidth = 0.3 + textView.layer.borderColor = UIColor(white: 204 / 255, alpha: 1).cgColor // this is the observed color of a textfield outline when using borderStyle = .roundedRect + textView.layer.cornerRadius = 5 + return textView + }() + + lazy var screenshotButton = { + let button = UIButton(frame: .zero) + button.setTitle(config.formConfig.addScreenshotButtonLabel, for: .normal) + button.backgroundColor = .systemBlue + button.addTarget(self, action: #selector(addScreenshotButtonTapped), for: .touchUpInside) + return button + }() + + lazy var submitButton = { + let button = UIButton(frame: .zero) + button.setTitle(config.formConfig.confirmButtonLabel, for: .normal) + button.backgroundColor = .systemGreen + button.addTarget(self, action: #selector(submitFeedbackButtonTapped), for: .touchUpInside) + return button + }() + + lazy var cancelButton = { + let button = UIButton(frame: .zero) + button.setTitle(config.formConfig.cancelButtonLabel, for: .normal) + button.backgroundColor = .systemRed + button.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside) + return button + }() + + func layoutUI(_ config: SentryUserFeedbackConfiguration) { + view.backgroundColor = .systemBackground + + let stackView = UIStackView(arrangedSubviews: [fullNameLabel, fullNameTextField, emailLabel, emailTextField, messageLabel, messageTextView, screenshotButton, submitButton, cancelButton]) + stackView.axis = .vertical + stackView.spacing = 8 + + let scrollView = UIScrollView(frame: view.bounds) + view.addSubview(scrollView) + scrollView.addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + scrollView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: config.spacing), + scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: config.spacing), + scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -config.spacing), + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -config.spacing), + + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor), + stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + messageTextView.heightAnchor.constraint(equalToConstant: config.theme.font.lineHeight * 5) + ]) + } +} + +#endif // os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift index f91be22092..0ebead40a1 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift @@ -3,10 +3,12 @@ import Foundation @_implementationOnly import _SentryPrivate import UIKit +var displayingForm = false + @available(iOS 13.0, *) struct SentryUserFeedbackWidget { class Window: UIWindow { - class RootViewController: UIViewController { + class RootViewController: UIViewController, SentryUserFeedbackFormDelegate { let defaultWidgetSpacing: CGFloat = 8 lazy var button = SentryUserFeedbackWidgetButtonView(config: config, action: { sender in @@ -18,20 +20,9 @@ struct SentryUserFeedbackWidget { sender.isHidden = true } - let formDialog = UIViewController(nibName: nil, bundle: nil) - formDialog.view.backgroundColor = .white - let label = UILabel(frame: .zero) - label.text = "Hi, I'm a user feedback form!" - formDialog.view.addSubview(label) - label.translatesAutoresizingMaskIntoConstraints = false - label.textAlignment = .center - NSLayoutConstraint.activate([ - label.leadingAnchor.constraint(equalTo: formDialog.view.leadingAnchor), - label.trailingAnchor.constraint(equalTo: formDialog.view.trailingAnchor), - label.centerYAnchor.constraint(equalTo: formDialog.view.centerYAnchor) - ]) - - self.present(formDialog, animated: self.config.widgetConfig.animations) + displayingForm = true + let form = SentryUserFeedbackForm(config: self.config, delegate: self) + self.present(form, animated: self.config.widgetConfig.animations) }) let config: SentryUserFeedbackConfiguration @@ -60,6 +51,30 @@ struct SentryUserFeedbackWidget { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + func closeForm() { + if config.widgetConfig.animations { + UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) { + self.button.alpha = 1 + } + } else { + button.isHidden = false + } + + displayingForm = false + dismiss(animated: config.widgetConfig.animations) + } + + // MARK: SentryUserFeedbackFormDelegate + + func cancelled() { + closeForm() + } + + func confirmed() { + // TODO: submit + closeForm() + } } init(config: SentryUserFeedbackConfiguration) { @@ -73,6 +88,10 @@ struct SentryUserFeedbackWidget { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard !displayingForm else { + return super.hitTest(point, with: event) + } + guard let result = super.hitTest(point, with: event) else { return nil } diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonView.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonView.swift index 478df2c2d2..8c19c0bf01 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonView.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonView.swift @@ -6,12 +6,9 @@ import UIKit @available(iOS 13.0, *) class SentryUserFeedbackWidgetButtonView: UIView { // MARK: Measurements - - let padding: CGFloat = 16 - let spacing: CGFloat = 8 let svgSize: CGFloat = 16 - lazy var sizeWithoutLabel = CGSize(width: svgSize * config.scaleFactor + 2 * padding, height: svgSize * config.scaleFactor + 2 * padding) + lazy var sizeWithoutLabel = CGSize(width: svgSize * config.scaleFactor + 2 * config.padding, height: svgSize * config.scaleFactor + 2 * config.padding) // MARK: Properties @@ -46,7 +43,7 @@ class SentryUserFeedbackWidgetButtonView: UIView { constraints.append(contentsOf: [ megaphone.heightAnchor.constraint(equalToConstant: svgSize), megaphone.widthAnchor.constraint(equalTo: megaphone.heightAnchor), - megaphone.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding) + megaphone.leadingAnchor.constraint(equalTo: leadingAnchor, constant: config.padding) ]) } @@ -59,16 +56,16 @@ class SentryUserFeedbackWidgetButtonView: UIView { addSubview(megaphone) let megaphoneCenteringConstraint = config.theme.font.familyName == "Damascus" ? megaphone.centerYAnchor.constraint(equalTo: label.centerYAnchor, constant: -(config.theme.font.capHeight - config.theme.font.ascender)) : megaphone.centerYAnchor.constraint(equalTo: label.firstBaselineAnchor, constant: -config.textEffectiveHeightCenter) constraints.append(contentsOf: [ - label.leadingAnchor.constraint(equalTo: megaphone.trailingAnchor, constant: spacing * config.scaleFactor), + label.leadingAnchor.constraint(equalTo: megaphone.trailingAnchor, constant: config.spacing * config.scaleFactor), megaphoneCenteringConstraint ]) } else { constraints.append(contentsOf: [ - label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding) + label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: config.padding) ]) } constraints.append(contentsOf: [ - label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding), + label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -config.padding), label.topAnchor.constraint(equalTo: topAnchor), label.bottomAnchor.constraint(equalTo: bottomAnchor) ]) @@ -77,9 +74,9 @@ class SentryUserFeedbackWidgetButtonView: UIView { layer.addSublayer(lozenge) addSubview(megaphone) constraints.append(contentsOf: [ - megaphone.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding), - megaphone.topAnchor.constraint(equalTo: topAnchor, constant: padding), - megaphone.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -padding) + megaphone.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -config.padding), + megaphone.topAnchor.constraint(equalTo: topAnchor, constant: config.padding), + megaphone.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -config.padding) ]) } @@ -135,15 +132,15 @@ class SentryUserFeedbackWidgetButtonView: UIView { var finalSize = size let hasText = config.widgetConfig.labelText != nil - let scaledLeftPadding = (padding * config.scaleFactor) / 2 + let scaledLeftPadding = (config.padding * config.scaleFactor) / 2 let scaledIconSize = svgSize * config.scaleFactor - let scaledSpacing = spacing + let scaledSpacing = config.spacing if hasText { - let iconWidthAdditions = config.widgetConfig.showIcon ? scaledLeftPadding + scaledIconSize + scaledSpacing : padding - finalSize.width += iconWidthAdditions + padding + let iconWidthAdditions = config.widgetConfig.showIcon ? scaledLeftPadding + scaledIconSize + scaledSpacing : config.padding + finalSize.width += iconWidthAdditions + config.padding let lozengeHeight = config.theme.font.familyName == "Damascus" ? config.theme.font.lineHeight : (2 * (config.theme.font.ascender - config.textEffectiveHeightCenter)) - finalSize.height = lozengeHeight + 2 * padding * config.paddingScaleFactor + finalSize.height = lozengeHeight + 2 * config.padding * config.paddingScaleFactor } let radius: CGFloat = finalSize.height / 2 @@ -169,10 +166,10 @@ class SentryUserFeedbackWidgetButtonView: UIView { let iconSizeDifference = (scaledIconSize - svgSize) / 2 if hasText { - let paddingDifference = (scaledLeftPadding - padding) / 2 - let spacingDifference = scaledSpacing - spacing + let paddingDifference = (scaledLeftPadding - config.padding) / 2 + let spacingDifference = scaledSpacing - config.spacing let increasedIconLeftPadAmountDueToScaling: CGFloat = config.widgetConfig.showIcon ? SentryLocale.isRightToLeftLanguage() ? paddingDifference : paddingDifference + iconSizeDifference + spacingDifference : 0 - let yTranslation = config.theme.font.familyName == "Damascus" ? -(padding * config.paddingScaleFactor + (config.theme.font.capHeight - config.theme.font.ascender) * config.paddingScaleFactor) : (-padding * config.paddingScaleFactor) + let yTranslation = config.theme.font.familyName == "Damascus" ? -(config.padding * config.paddingScaleFactor + (config.theme.font.capHeight - config.theme.font.ascender) * config.paddingScaleFactor) : (-config.padding * config.paddingScaleFactor) lozengeLayer.transform = CATransform3DTranslate(lozengeLayer.transform, -increasedIconLeftPadAmountDueToScaling, yTranslation, 0) } else { lozengeLayer.transform = CATransform3DTranslate(lozengeLayer.transform, -iconSizeDifference, -iconSizeDifference, 0) From 2144d8f3a7de1861f17979bab367c35b0c77a260 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 5 Nov 2024 10:11:18 -0900 Subject: [PATCH 02/12] iterating on form; extract iconography paths; add more accessibility config --- Sentry.xcodeproj/project.pbxproj | 4 + Sources/Swift/Helper/SentryIconography.swift | 189 ++++++++++++++++++ .../SentryUserFeedbackFormConfiguration.swift | 26 ++- ...entryUserFeedbackWidgetConfiguration.swift | 6 +- .../UserFeedback/SentryUserFeedbackForm.swift | 134 ++++++++++--- ...eedbackWidgetButtonMegaphoneIconView.swift | 25 +-- 6 files changed, 316 insertions(+), 68 deletions(-) create mode 100644 Sources/Swift/Helper/SentryIconography.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 12d8ff5f45..83086b77c6 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -691,6 +691,7 @@ 84AF45A629A7FFA500FBB177 /* SentryProfiledTracerConcurrency.h in Headers */ = {isa = PBXBuildFile; fileRef = 84AF45A429A7FFA500FBB177 /* SentryProfiledTracerConcurrency.h */; }; 84AF45A729A7FFA500FBB177 /* SentryProfiledTracerConcurrency.mm in Sources */ = {isa = PBXBuildFile; fileRef = 84AF45A529A7FFA500FBB177 /* SentryProfiledTracerConcurrency.mm */; }; 84B0DFF42CD2CF64007FB332 /* SentryUserFeedbackForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B0DFF32CD2CF64007FB332 /* SentryUserFeedbackForm.swift */; }; + 84B0E0072CD963FD007FB332 /* SentryIconography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B0E0062CD963F9007FB332 /* SentryIconography.swift */; }; 84B7FA3529B285FC00AD93B1 /* Sentry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63AA759B1EB8AEF500D153DE /* Sentry.framework */; }; 84B7FA3C29B2876F00AD93B1 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF3DD6243DD4A1008A5414 /* TestConstants.swift */; }; 84B7FA3D29B2879C00AD93B1 /* libSentryTestUtils.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8431F00A29B284F200D8DC56 /* libSentryTestUtils.a */; }; @@ -1769,6 +1770,7 @@ 84AF45A429A7FFA500FBB177 /* SentryProfiledTracerConcurrency.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryProfiledTracerConcurrency.h; path = ../include/SentryProfiledTracerConcurrency.h; sourceTree = ""; }; 84AF45A529A7FFA500FBB177 /* SentryProfiledTracerConcurrency.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryProfiledTracerConcurrency.mm; sourceTree = ""; }; 84B0DFF32CD2CF64007FB332 /* SentryUserFeedbackForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserFeedbackForm.swift; sourceTree = ""; }; + 84B0E0062CD963F9007FB332 /* SentryIconography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryIconography.swift; sourceTree = ""; }; 84B7FA3B29B2866200AD93B1 /* SentryTestUtils-ObjC-BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryTestUtils-ObjC-BridgingHeader.h"; sourceTree = ""; }; 84BA62262CAE2EEF0049F636 /* SentryUserFeedbackWidgetButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserFeedbackWidgetButtonView.swift; sourceTree = ""; }; 84C47B2B2A09239100DAEB8A /* .codecov.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .codecov.yml; sourceTree = ""; }; @@ -2109,6 +2111,7 @@ 621D9F2D2B9B030E003D94DE /* Helper */ = { isa = PBXGroup; children = ( + 84B0E0062CD963F9007FB332 /* SentryIconography.swift */, D8739CF62BECFF86007D2F66 /* Log */, 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */, 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */, @@ -4690,6 +4693,7 @@ 7BECF42826145CD900D9826E /* SentryMechanismMeta.m in Sources */, 8E7C982F2693D56000E6336C /* SentryTraceHeader.m in Sources */, 63FE715F20DA4C1100CDBAE8 /* SentryCrashID.c in Sources */, + 84B0E0072CD963FD007FB332 /* SentryIconography.swift in Sources */, 7DB3A687238EA75E00A2D442 /* SentryHttpTransport.m in Sources */, 63FE70D520DA4C1000CDBAE8 /* SentryCrashMonitor_NSException.m in Sources */, D80CD8D12B751442002F710B /* HTTPHeaderSanitizer.swift in Sources */, diff --git a/Sources/Swift/Helper/SentryIconography.swift b/Sources/Swift/Helper/SentryIconography.swift new file mode 100644 index 0000000000..1dc5293796 --- /dev/null +++ b/Sources/Swift/Helper/SentryIconography.swift @@ -0,0 +1,189 @@ +import CoreGraphics + +struct SentryIconography { + static let logo = { + let svg = """ + +""" + let path = CGMutablePath() + + // M 29,2.26 + // Pick up the pen and Move it to { x: 29, y: 2.26 } + path.move(to: CGPoint(x: 29, y: 2.26)) + + // a 4.67,4.67 0 0 0 -8,0 + // Put down the pen and Draw an Arc curve from the current point to a new point { x: previous point - 8, y: previous point + 0 } + // Its radii are { x: 4.67, y: 4.67 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles + path.addArc(center: CGPoint(x: 21, y: 2.26), radius: 4.67, startAngle: CGFloat.pi, endAngle: 0, clockwise: false) + + // L 14.42,13.53 + // Draw a line to { x: 14.42, y: 13.53 } + path.addLine(to: CGPoint(x: 14.42, y: 13.53)) + + // A 32.21,32.21 0 0 1 32.17,40.19 + // Draw an Arc curve from the current point to a new point { x: 32.17, y: 40.19 } + // Its radii are { x: 32.21, y: 32.21 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at positive angles + path.addArc(center: CGPoint(x: 32.17, y: 26.53), radius: 32.21, startAngle: CGFloat.pi, endAngle: 0, clockwise: true) + + // H 27.55 + // Move horizontally to 27.55 + path.addLine(to: CGPoint(x: 27.55, y: 26.53)) + + // A 27.68,27.68 0 0 0 12.09,17.47 + // Draw an Arc curve from the current point to a new point { x: 12.09, y: 17.47 } + // Its radii are { x: 27.68, y: 27.68 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles + path.addArc(center: CGPoint(x: 19.62, y: 17.47), radius: 27.68, startAngle: CGFloat.pi, endAngle: 0, clockwise: false) + + // L 6,28 + // Draw a line to { x: 6, y: 28 } + path.addLine(to: CGPoint(x: 6, y: 28)) + + // a 15.92,15.92 0 0 1 9.23,12.17 + // Draw an Arc curve from the current point to a new point { x: previous point + 9.23, y: previous point + 12.17 } + // Its radii are { x: 15.92, y: 15.92 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at positive angles + path.addArc(center: CGPoint(x: 15.23, y: 40.17), radius: 15.92, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true) + + // H 4.62 + // Move horizontally to 4.62 + path.addLine(to: CGPoint(x: 4.62, y: 40.17)) + + // A 0.76,0.76 0 0 1 4,39.06 + // Draw an Arc curve from the current point to a new point { x: 4, y: 39.06 } + // Its radii are { x: 0.76, y: 0.76 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at positive angles + path.addArc(center: CGPoint(x: 4, y: 39.06), radius: 0.76, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true) + + // l 2.94,-5 + // Move right 2.94 and top 5 from the current position + path.addLine(to: CGPoint(x: 7.56, y: 34.06)) + + // a 10.74,10.74 0 0 0 -3.36,-1.9 + // Draw an Arc curve from the current point to a new point { x: previous point - 3.36, y: previous point - 1.9 } + // Its radii are { x: 10.74, y: 10.74 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles + path.addArc(center: CGPoint(x: 4.2, y: 32.16), radius: 10.74, startAngle: CGFloat.pi, endAngle: 0, clockwise: false) + + // l -2.91,5 + // Move left 2.91 and bottom 5 from the current position + path.addLine(to: CGPoint(x: 4.62, y: 39.06)) + + // a 4.54,4.54 0 0 0 1.69,6.24 + // Draw an Arc curve from the current point to a new point { x: previous point + 1.69, y: previous point + 6.24 } + // Its radii are { x: 4.54, y: 4.54 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles + path.addArc(center: CGPoint(x: 4.62, y: 44), radius: 4.54, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false) + + // A 4.66,4.66 0 0 0 4.62,44 + // Draw an Arc curve from the current point to a new point { x: 4.62, y: 44 } + // Its radii are { x: 4.66, y: 4.66 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles + path.addArc(center: CGPoint(x: 4.62, y: 44), radius: 4.66, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false) + + // H 19.15 + // Move horizontally to 19.15 + path.addLine(to: CGPoint(x: 19.15, y: 44)) + + // a 19.4,19.4 0 0 0 -8,-17.31 + // Draw an Arc curve from the current point to a new point { x: previous point - 8, y: previous point - 17.31 } + // Its radii are { x: 19.4, y: 19.4 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles + path.addArc(center: CGPoint(x: 11.15, y: 26.69), radius: 19.4, startAngle: CGFloat.pi, endAngle: 0, clockwise: false) + + // l 2.31,-4 + // Move right 2.31 and top 4 from the current position + path.addLine(to: CGPoint(x: 21.46, y: 40.69)) + + // A 23.87,23.87 0 0 1 23.76,44 + // Draw an Arc curve from the current point to a new point { x: 23.76, y: 44 } + // Its radii are { x: 23.87, y: 23.87 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at positive angles + path.addArc(center: CGPoint(x: 23.76, y: 44), radius: 23.87, startAngle: CGFloat.pi, endAngle: 0, clockwise: true) + + // H 36.07 + // Move horizontally to 36.07 + path.addLine(to: CGPoint(x: 36.07, y: 44)) + + // a 35.88,35.88 0 0 0 -16.41,-31.8 + // Draw an Arc curve from the current point to a new point { x: previous point - 16.41, y: previous point - 31.8 } + // Its radii are { x: 35.88, y: 35.88 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles + path.addArc(center: CGPoint(x: 19.66, y: 12.2), radius: 35.88, startAngle: CGFloat.pi, endAngle: 0, clockwise: false) + + // l 4.67,-8 + // Move right 4.67 and top 8 from the current position + path.addLine(to: CGPoint(x: 40.74, y: 36.07)) + + // a 0.77,0.77 0 0 1 1.05,-0.27 + // Draw an Arc curve from the current point to a new point { x: previous point + 1.05, y: previous point - 0.27 } + // Its radii are { x: 0.77, y: 0.77 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at positive angles + path.addArc(center: CGPoint(x: 41.81, y: 35.8), radius: 0.77, startAngle: CGFloat.pi, endAngle: 0, clockwise: true) + + // c 0.53,0.29 20.29,34.77 20.66,35.17 + // Draw a Bézier curve from the current point to a new point { x: previous point + 20.66, y: previous point + 35.17 } + // The start control point is { x: previous point + 0.53, y: previous point + 0.29 } and the end control point is { x: previous point + 20.29, y: previous point + 34.77 } + path.addCurve(to: CGPoint(x: 20.66, y: 35.17), control1: CGPoint(x: 21.34, y: 36.09), control2: CGPoint(x: 20.29, y: 34.77)) + + // a 0.76,0.76 0 0 1 -0.68,1.13 + // Draw an Arc curve from the current point to a new point { x: previous point - 0.68, y: previous point + 1.13 } + // Its radii are { x: 0.76, y: 0.76 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at positive angles + path.addArc(center: CGPoint(x: 40.6, y: 40.6), radius: 0.76, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true) + + // H 40.6 + // Move horizontally to 40.6 + path.addLine(to: CGPoint(x: 40.6, y: 44.41)) + + // q 0.09,1.91 0,3.81 + // Draw a quadratic Bézier curve from the current point to a new point { x: previous point + 0, y: previous point + 3.81 } + // The control point is { x: previous point + 0.09, y: previous point + 1.91 } + path.addQuadCurve(to: CGPoint(x: 40.6, y: 48.22), control: CGPoint(x: 40.69, y: 46.32)) + + // h 4.78 + // Move right 4.78 from the current position + path.addLine(to: CGPoint(x: 45.38, y: 48.22)) + + // A 4.59,4.59 0 0 0 50,39.43 + // Draw an Arc curve from the current point to a new point { x: 50, y: 39.43 } + // Its radii are { x: 4.59, y: 4.59 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles + path.addArc(center: CGPoint(x: 50, y: 39.43), radius: 4.59, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false) + + // a 4.49,4.49 0 0 0 -0.62,-2.28 + // Draw an Arc curve from the current point to a new point { x: previous point - 0.62, y: previous point - 2.28 } + // Its radii are { x: 4.49, y: 4.49 }, and with no rotation + // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles + path.addArc(center: CGPoint(x: 49.38, y: 37.15), radius: 4.49, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false) + + // Z + // Draw a line straight back to the start + path.closeSubpath() + + return path + }() + + static let megaphone = { + let path = CGMutablePath() + + path.move(to: CGPoint(x: 1, y: 3)) + path.addLine(to: CGPoint(x: 7, y: 3)) + path.addLine(to: CGPoint(x: 10, y: 1)) + path.addLine(to: CGPoint(x: 12, y: 1)) + path.addLine(to: CGPoint(x: 12, y: 11)) + path.addLine(to: CGPoint(x: 10, y: 11)) + path.addLine(to: CGPoint(x: 7, y: 9)) + path.addLine(to: CGPoint(x: 1, y: 9)) + path.closeSubpath() + + path.addRect(CGRect(x: 2, y: 9, width: 3.5, height: 6)) + + path.move(to: CGPoint(x: 12, y: 6)) + path.addRelativeArc(center: CGPoint(x: 12, y: 6), radius: 3, startAngle: -(.pi / 2), delta: .pi) + + return path + }() +} diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift index 4e868ed1d5..56e614ef54 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift @@ -35,6 +35,8 @@ public class SentryUserFeedbackFormConfiguration: NSObject { */ public var messagePlaceholder: String = "What's the bug? What did you expect?" + public lazy var messageTextViewAccessibilityLabel: String = messagePlaceholder + /** * The label shown next to an input field that is required. * - note: Default: `"(required)"` @@ -62,6 +64,8 @@ public class SentryUserFeedbackFormConfiguration: NSObject { */ public var addScreenshotButtonLabel: String = "Add a screenshot" + public lazy var addScreenshotButtonAccessibilityLabel = addScreenshotButtonLabel + /** * The label of the button to remove the screenshot from the form. * - note: Default: `"Remove screenshot"` @@ -69,6 +73,8 @@ public class SentryUserFeedbackFormConfiguration: NSObject { */ public var removeScreenshotButtonLabel: String = "Remove screenshot" + public lazy var removeScreenshotButtonAccessibilityLabel = removeScreenshotButtonLabel + // MARK: Name /** @@ -98,6 +104,8 @@ public class SentryUserFeedbackFormConfiguration: NSObject { */ public var namePlaceholder: String = "Your Name" + public lazy var nameTextFieldAccessibilityLabel = namePlaceholder + // MARK: Email /** @@ -125,6 +133,8 @@ public class SentryUserFeedbackFormConfiguration: NSObject { */ public var emailPlaceholder: String = "your.email@example.org" + public lazy var emailTextFieldAccessibilityLabel = emailPlaceholder + // MARK: Buttons /** @@ -137,7 +147,7 @@ public class SentryUserFeedbackFormConfiguration: NSObject { * The accessibility label of the form's "Submit" button. * - note: Default: `submitButtonLabel` value */ - public var submitButtonAccessibilityLabel: String? + public lazy var submitButtonAccessibilityLabel: String = submitButtonLabel /** * The label of cancel buttons used in the feedback form. @@ -149,19 +159,7 @@ public class SentryUserFeedbackFormConfiguration: NSObject { * The accessibility label of the form's "Cancel" button. * - note: Default: `cancelButtonLabel` value */ - public var cancelButtonAccessibilityLabel: String? - - /** - * The label of confirm buttons used in the feedback form. - * - note: Default: `"Confirm"` - */ - public var confirmButtonLabel: String = "Confirm" - - /** - * The accessibility label of the form's "Confirm" button. - * - note: Default: `confirmButtonLabel` value - */ - public var confirmButtonAccessibilityLabel: String? + public lazy var cancelButtonAccessibilityLabel: String = cancelButtonLabel } #endif // os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackWidgetConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackWidgetConfiguration.swift index bb0835a2a9..8cdee46e2c 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackWidgetConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackWidgetConfiguration.swift @@ -23,11 +23,13 @@ public class SentryUserFeedbackWidgetConfiguration: NSObject { */ public var animations: Bool = true + let defaultLabelText = "Report a Bug" + /** * The label of the injected button that opens up the feedback form when clicked. If `nil`, no text is displayed and only the icon image is shown. * - note: Default: `"Report a Bug"` */ - public var labelText: String? = "Report a Bug" + public lazy var labelText: String? = defaultLabelText /** * Whether or not to show our icon along with the text in the button. @@ -39,7 +41,7 @@ public class SentryUserFeedbackWidgetConfiguration: NSObject { * The accessibility label of the injected button that opens up the feedback form when clicked. * - note: Default: `labelText` value */ - public var widgetAccessibilityLabel: String? + public lazy var widgetAccessibilityLabel: String? = labelText ?? defaultLabelText /** * The window level of the widget. diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index 6bf83e9e70..d95bad4eb1 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -19,7 +19,23 @@ class SentryUserFeedbackForm: UIViewController { self.config = config self.delegate = delegate super.init(nibName: nil, bundle: nil) - layoutUI(config) + + view.backgroundColor = .systemBackground + + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: config.spacing), + scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: config.spacing), + scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -config.spacing), + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -config.spacing), + + stack.topAnchor.constraint(equalTo: scrollView.topAnchor), + stack.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + stack.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + stack.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + messageTextView.heightAnchor.constraint(equalToConstant: config.theme.font.lineHeight * 5) + ]) } required init?(coder: NSCoder) { @@ -32,8 +48,12 @@ class SentryUserFeedbackForm: UIViewController { } + func removeScreenshotButtonTapped() { + + } + func submitFeedbackButtonTapped() { - // ???: marshal data how? + // TODO: validate and package entries delegate?.confirmed() } @@ -43,9 +63,26 @@ class SentryUserFeedbackForm: UIViewController { // MARK: UI + lazy var formTitleLabel = { + let label = UILabel(frame: .zero) + label.text = config.formConfig.formTitle + return label + }() + + lazy var sentryLogoView = { + let shapeLayer = CAShapeLayer() + shapeLayer.path = SentryIconography.logo + shapeLayer.fillColor = self.config.theme.foreground.cgColor + + let view = UIView(frame: .zero) + view.layer.addSublayer(shapeLayer) + view.accessibilityLabel = "provided by Sentry" // ???: what do we want to say here? + return view + }() + lazy var fullNameLabel = { let label = UILabel(frame: .zero) - label.text = config.formConfig.nameLabel + label.text = fullLabelText(labelText: config.formConfig.nameLabel, required: config.formConfig.isNameRequired) return label }() @@ -53,12 +90,13 @@ class SentryUserFeedbackForm: UIViewController { let field = UITextField(frame: .zero) field.placeholder = config.formConfig.namePlaceholder field.borderStyle = .roundedRect + field.accessibilityLabel = config.formConfig.nameTextFieldAccessibilityLabel return field }() lazy var emailLabel = { let label = UILabel(frame: .zero) - label.text = config.formConfig.emailLabel + label.text = fullLabelText(labelText: config.formConfig.emailLabel, required: config.formConfig.isEmailRequired) return label }() @@ -66,6 +104,7 @@ class SentryUserFeedbackForm: UIViewController { let field = UITextField(frame: .zero) field.placeholder = config.formConfig.emailPlaceholder field.borderStyle = .roundedRect + field.accessibilityLabel = config.formConfig.emailTextFieldAccessibilityLabel return field }() @@ -83,20 +122,32 @@ class SentryUserFeedbackForm: UIViewController { textView.layer.borderWidth = 0.3 textView.layer.borderColor = UIColor(white: 204 / 255, alpha: 1).cgColor // this is the observed color of a textfield outline when using borderStyle = .roundedRect textView.layer.cornerRadius = 5 + textView.accessibilityLabel = config.formConfig.messageTextViewAccessibilityLabel return textView }() - lazy var screenshotButton = { + lazy var addScreenshotButton = { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.addScreenshotButtonLabel, for: .normal) + button.accessibilityLabel = config.formConfig.addScreenshotButtonAccessibilityLabel button.backgroundColor = .systemBlue button.addTarget(self, action: #selector(addScreenshotButtonTapped), for: .touchUpInside) return button }() + lazy var removeScreenshotButton = { + let button = UIButton(frame: .zero) + button.setTitle(config.formConfig.removeScreenshotButtonLabel, for: .normal) + button.accessibilityLabel = config.formConfig.removeScreenshotButtonAccessibilityLabel + button.backgroundColor = .systemBlue + button.addTarget(self, action: #selector(removeScreenshotButtonTapped), for: .touchUpInside) + return button + }() + lazy var submitButton = { let button = UIButton(frame: .zero) - button.setTitle(config.formConfig.confirmButtonLabel, for: .normal) + button.setTitle(config.formConfig.submitButtonLabel, for: .normal) + button.accessibilityLabel = config.formConfig.submitButtonAccessibilityLabel button.backgroundColor = .systemGreen button.addTarget(self, action: #selector(submitFeedbackButtonTapped), for: .touchUpInside) return button @@ -105,38 +156,65 @@ class SentryUserFeedbackForm: UIViewController { lazy var cancelButton = { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.cancelButtonLabel, for: .normal) + button.accessibilityLabel = config.formConfig.cancelButtonAccessibilityLabel button.backgroundColor = .systemRed button.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside) return button }() - func layoutUI(_ config: SentryUserFeedbackConfiguration) { - view.backgroundColor = .systemBackground + lazy var stack = { + let headerStack = UIStackView(arrangedSubviews: [self.formTitleLabel]) + if self.config.formConfig.showBranding { + headerStack.addArrangedSubview(self.sentryLogoView) + } + + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = 8 + + stack.addArrangedSubview(headerStack) + + if self.config.formConfig.showName { + stack.addArrangedSubview(self.fullNameLabel) + stack.addArrangedSubview(self.fullNameTextField) + } + + if self.config.formConfig.showEmail { + stack.addArrangedSubview(self.emailLabel) + stack.addArrangedSubview(self.emailTextField) + } - let stackView = UIStackView(arrangedSubviews: [fullNameLabel, fullNameTextField, emailLabel, emailTextField, messageLabel, messageTextView, screenshotButton, submitButton, cancelButton]) - stackView.axis = .vertical - stackView.spacing = 8 + stack.addArrangedSubview(self.messageLabel) + stack.addArrangedSubview(self.messageTextView) + if self.config.formConfig.enableScreenshot { + stack.addArrangedSubview(self.addScreenshotButton) + } + + stack.addArrangedSubview(self.submitButton) + stack.addArrangedSubview(self.cancelButton) + + stack.translatesAutoresizingMaskIntoConstraints = false + + return stack + }() + + lazy var scrollView = { let scrollView = UIScrollView(frame: view.bounds) view.addSubview(scrollView) - scrollView.addSubview(stackView) - stackView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(stack) scrollView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: config.spacing), - scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: config.spacing), - scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -config.spacing), - scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -config.spacing), - - stackView.topAnchor.constraint(equalTo: scrollView.topAnchor), - stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), - stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), - stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), - - messageTextView.heightAnchor.constraint(equalToConstant: config.theme.font.lineHeight * 5) - ]) + return scrollView + }() + + // MARK: Helpers + + func fullLabelText(labelText: String, required: Bool) -> String { + if required { + return labelText + " " + config.formConfig.isRequiredLabel + } else { + return labelText + } } } diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonMegaphoneIconView.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonMegaphoneIconView.swift index 064a3919d2..749347a107 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonMegaphoneIconView.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonMegaphoneIconView.swift @@ -9,7 +9,7 @@ class SentryUserFeedbackWidgetButtonMegaphoneIconView: UIView { super.init(frame: .zero) let svgLayer = CAShapeLayer() - svgLayer.path = megaphoneShape + svgLayer.path = SentryIconography.megaphone svgLayer.fillColor = UIColor.clear.cgColor if UIScreen.main.traitCollection.userInterfaceStyle == .dark { @@ -36,29 +36,6 @@ class SentryUserFeedbackWidgetButtonMegaphoneIconView: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - //swiftlint:disable function_body_length - lazy var megaphoneShape: CGPath = { - let path = CGMutablePath() - - path.move(to: CGPoint(x: 1, y: 3)) - path.addLine(to: CGPoint(x: 7, y: 3)) - path.addLine(to: CGPoint(x: 10, y: 1)) - path.addLine(to: CGPoint(x: 12, y: 1)) - path.addLine(to: CGPoint(x: 12, y: 11)) - path.addLine(to: CGPoint(x: 10, y: 11)) - path.addLine(to: CGPoint(x: 7, y: 9)) - path.addLine(to: CGPoint(x: 1, y: 9)) - path.closeSubpath() - - path.addRect(CGRect(x: 2, y: 9, width: 3.5, height: 6)) - - path.move(to: CGPoint(x: 12, y: 6)) - path.addRelativeArc(center: CGPoint(x: 12, y: 6), radius: 3, startAngle: -(.pi / 2), delta: .pi) - - return path - }() - //swiftlint:enable function_body_length } #endif // os(iOS) && !SENTRY_NO_UIKIT From 114a2f5195a4364d4d3737d843905a1a28124267 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 5 Nov 2024 11:00:43 -0900 Subject: [PATCH 03/12] show widget after dismissing --- .../SentryUserFeedbackWidget.swift | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift index 0ebead40a1..0fbcdedc3c 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift @@ -8,20 +8,13 @@ var displayingForm = false @available(iOS 13.0, *) struct SentryUserFeedbackWidget { class Window: UIWindow { - class RootViewController: UIViewController, SentryUserFeedbackFormDelegate { + class RootViewController: UIViewController, SentryUserFeedbackFormDelegate, UIAdaptivePresentationControllerDelegate { let defaultWidgetSpacing: CGFloat = 8 lazy var button = SentryUserFeedbackWidgetButtonView(config: config, action: { sender in - if self.config.widgetConfig.animations { - UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) { - sender.alpha = 0 - } - } else { - sender.isHidden = true - } - - displayingForm = true + self.setWidget(visible: false) let form = SentryUserFeedbackForm(config: self.config, delegate: self) + form.presentationController?.delegate = self self.present(form, animated: self.config.widgetConfig.animations) }) @@ -52,16 +45,22 @@ struct SentryUserFeedbackWidget { fatalError("init(coder:) has not been implemented") } - func closeForm() { + // MARK: Helpers + + func setWidget(visible: Bool) { if config.widgetConfig.animations { UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) { - self.button.alpha = 1 + self.button.alpha = visible ? 1 : 0 } } else { - button.isHidden = false + button.isHidden = !visible } - displayingForm = false + displayingForm = !visible + } + + func closeForm() { + setWidget(visible: true) dismiss(animated: config.widgetConfig.animations) } @@ -75,6 +74,12 @@ struct SentryUserFeedbackWidget { // TODO: submit closeForm() } + + // MARK: UIAdaptivePresentationControllerDelegate + + func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { + setWidget(visible: true) + } } init(config: SentryUserFeedbackConfiguration) { From 214fa4169cdefd6e70f71711b26b9dc623db8496 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 12 Nov 2024 12:50:23 -0900 Subject: [PATCH 04/12] logo svg --- Sources/Swift/Helper/SentryIconography.swift | 336 ++++++++++-------- .../UserFeedback/SentryUserFeedbackForm.swift | 5 +- 2 files changed, 185 insertions(+), 156 deletions(-) diff --git a/Sources/Swift/Helper/SentryIconography.swift b/Sources/Swift/Helper/SentryIconography.swift index 1dc5293796..88cd4e4755 100644 --- a/Sources/Swift/Helper/SentryIconography.swift +++ b/Sources/Swift/Helper/SentryIconography.swift @@ -1,166 +1,192 @@ import CoreGraphics +extension CGMutablePath { + func addSVGArc(startPoint: CGPoint, xyRadii: CGFloat, clockwise: Bool, endPoint: CGPoint) { + let midX: CGFloat = (startPoint.x + endPoint.x) / 2 + let midY: CGFloat = (startPoint.y + endPoint.y) / 2 + let d: CGFloat = sqrt(pow(endPoint.x - startPoint.x, 2) + pow(endPoint.y - startPoint.y, 2)) + let h: CGFloat = sqrt(pow(xyRadii, 2) - pow(d / 2, 2)) + let orientation: CGFloat = clockwise ? 1 : -1 + + let centerX = midX + orientation * h * (startPoint.y - endPoint.y) / d + let centerY = midY + orientation * h * (endPoint.x - startPoint.x) / d + let centerPoint = CGPoint(x: centerX, y: centerY) + + let startAngleAtan2X = startPoint.x - centerX + let startAngleAtan2Y = startPoint.y - centerY + let startAngle: CGFloat = atan2(startAngleAtan2Y, startAngleAtan2X) + + let endAngleAtan2X = endPoint.x - centerX + let endAngleAtan2Y = endPoint.y - centerY + let endAngle: CGFloat = atan2(endAngleAtan2Y, endAngleAtan2X) + + addArc(center: centerPoint, radius: xyRadii, startAngle: startAngle, endAngle: endAngle, clockwise: !clockwise) + } +} + +extension CGPoint { + func translated(x: CGFloat = 0, y: CGFloat = 0) -> CGPoint { + .init(x: self.x + x, y: self.y + y) + } +} + struct SentryIconography { static let logo = { - let svg = """ - -""" let path = CGMutablePath() - // M 29,2.26 - // Pick up the pen and Move it to { x: 29, y: 2.26 } - path.move(to: CGPoint(x: 29, y: 2.26)) - - // a 4.67,4.67 0 0 0 -8,0 - // Put down the pen and Draw an Arc curve from the current point to a new point { x: previous point - 8, y: previous point + 0 } - // Its radii are { x: 4.67, y: 4.67 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles - path.addArc(center: CGPoint(x: 21, y: 2.26), radius: 4.67, startAngle: CGFloat.pi, endAngle: 0, clockwise: false) - - // L 14.42,13.53 - // Draw a line to { x: 14.42, y: 13.53 } - path.addLine(to: CGPoint(x: 14.42, y: 13.53)) - - // A 32.21,32.21 0 0 1 32.17,40.19 - // Draw an Arc curve from the current point to a new point { x: 32.17, y: 40.19 } - // Its radii are { x: 32.21, y: 32.21 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at positive angles - path.addArc(center: CGPoint(x: 32.17, y: 26.53), radius: 32.21, startAngle: CGFloat.pi, endAngle: 0, clockwise: true) - - // H 27.55 - // Move horizontally to 27.55 - path.addLine(to: CGPoint(x: 27.55, y: 26.53)) - - // A 27.68,27.68 0 0 0 12.09,17.47 - // Draw an Arc curve from the current point to a new point { x: 12.09, y: 17.47 } - // Its radii are { x: 27.68, y: 27.68 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles - path.addArc(center: CGPoint(x: 19.62, y: 17.47), radius: 27.68, startAngle: CGFloat.pi, endAngle: 0, clockwise: false) - - // L 6,28 - // Draw a line to { x: 6, y: 28 } - path.addLine(to: CGPoint(x: 6, y: 28)) - - // a 15.92,15.92 0 0 1 9.23,12.17 - // Draw an Arc curve from the current point to a new point { x: previous point + 9.23, y: previous point + 12.17 } - // Its radii are { x: 15.92, y: 15.92 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at positive angles - path.addArc(center: CGPoint(x: 15.23, y: 40.17), radius: 15.92, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true) - - // H 4.62 - // Move horizontally to 4.62 - path.addLine(to: CGPoint(x: 4.62, y: 40.17)) - - // A 0.76,0.76 0 0 1 4,39.06 - // Draw an Arc curve from the current point to a new point { x: 4, y: 39.06 } - // Its radii are { x: 0.76, y: 0.76 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at positive angles - path.addArc(center: CGPoint(x: 4, y: 39.06), radius: 0.76, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true) - - // l 2.94,-5 - // Move right 2.94 and top 5 from the current position - path.addLine(to: CGPoint(x: 7.56, y: 34.06)) - - // a 10.74,10.74 0 0 0 -3.36,-1.9 - // Draw an Arc curve from the current point to a new point { x: previous point - 3.36, y: previous point - 1.9 } - // Its radii are { x: 10.74, y: 10.74 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles - path.addArc(center: CGPoint(x: 4.2, y: 32.16), radius: 10.74, startAngle: CGFloat.pi, endAngle: 0, clockwise: false) - - // l -2.91,5 - // Move left 2.91 and bottom 5 from the current position - path.addLine(to: CGPoint(x: 4.62, y: 39.06)) - - // a 4.54,4.54 0 0 0 1.69,6.24 - // Draw an Arc curve from the current point to a new point { x: previous point + 1.69, y: previous point + 6.24 } - // Its radii are { x: 4.54, y: 4.54 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles - path.addArc(center: CGPoint(x: 4.62, y: 44), radius: 4.54, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false) - - // A 4.66,4.66 0 0 0 4.62,44 - // Draw an Arc curve from the current point to a new point { x: 4.62, y: 44 } - // Its radii are { x: 4.66, y: 4.66 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles - path.addArc(center: CGPoint(x: 4.62, y: 44), radius: 4.66, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false) - - // H 19.15 - // Move horizontally to 19.15 - path.addLine(to: CGPoint(x: 19.15, y: 44)) - - // a 19.4,19.4 0 0 0 -8,-17.31 - // Draw an Arc curve from the current point to a new point { x: previous point - 8, y: previous point - 17.31 } - // Its radii are { x: 19.4, y: 19.4 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles - path.addArc(center: CGPoint(x: 11.15, y: 26.69), radius: 19.4, startAngle: CGFloat.pi, endAngle: 0, clockwise: false) - - // l 2.31,-4 - // Move right 2.31 and top 4 from the current position - path.addLine(to: CGPoint(x: 21.46, y: 40.69)) - - // A 23.87,23.87 0 0 1 23.76,44 - // Draw an Arc curve from the current point to a new point { x: 23.76, y: 44 } - // Its radii are { x: 23.87, y: 23.87 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at positive angles - path.addArc(center: CGPoint(x: 23.76, y: 44), radius: 23.87, startAngle: CGFloat.pi, endAngle: 0, clockwise: true) - - // H 36.07 - // Move horizontally to 36.07 - path.addLine(to: CGPoint(x: 36.07, y: 44)) - - // a 35.88,35.88 0 0 0 -16.41,-31.8 - // Draw an Arc curve from the current point to a new point { x: previous point - 16.41, y: previous point - 31.8 } - // Its radii are { x: 35.88, y: 35.88 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles - path.addArc(center: CGPoint(x: 19.66, y: 12.2), radius: 35.88, startAngle: CGFloat.pi, endAngle: 0, clockwise: false) - - // l 4.67,-8 - // Move right 4.67 and top 8 from the current position - path.addLine(to: CGPoint(x: 40.74, y: 36.07)) - - // a 0.77,0.77 0 0 1 1.05,-0.27 - // Draw an Arc curve from the current point to a new point { x: previous point + 1.05, y: previous point - 0.27 } - // Its radii are { x: 0.77, y: 0.77 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at positive angles - path.addArc(center: CGPoint(x: 41.81, y: 35.8), radius: 0.77, startAngle: CGFloat.pi, endAngle: 0, clockwise: true) - - // c 0.53,0.29 20.29,34.77 20.66,35.17 - // Draw a Bézier curve from the current point to a new point { x: previous point + 20.66, y: previous point + 35.17 } - // The start control point is { x: previous point + 0.53, y: previous point + 0.29 } and the end control point is { x: previous point + 20.29, y: previous point + 34.77 } - path.addCurve(to: CGPoint(x: 20.66, y: 35.17), control1: CGPoint(x: 21.34, y: 36.09), control2: CGPoint(x: 20.29, y: 34.77)) - - // a 0.76,0.76 0 0 1 -0.68,1.13 - // Draw an Arc curve from the current point to a new point { x: previous point - 0.68, y: previous point + 1.13 } - // Its radii are { x: 0.76, y: 0.76 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at positive angles - path.addArc(center: CGPoint(x: 40.6, y: 40.6), radius: 0.76, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true) - - // H 40.6 - // Move horizontally to 40.6 - path.addLine(to: CGPoint(x: 40.6, y: 44.41)) - - // q 0.09,1.91 0,3.81 - // Draw a quadratic Bézier curve from the current point to a new point { x: previous point + 0, y: previous point + 3.81 } - // The control point is { x: previous point + 0.09, y: previous point + 1.91 } - path.addQuadCurve(to: CGPoint(x: 40.6, y: 48.22), control: CGPoint(x: 40.69, y: 46.32)) - - // h 4.78 - // Move right 4.78 from the current position - path.addLine(to: CGPoint(x: 45.38, y: 48.22)) - - // A 4.59,4.59 0 0 0 50,39.43 - // Draw an Arc curve from the current point to a new point { x: 50, y: 39.43 } - // Its radii are { x: 4.59, y: 4.59 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles - path.addArc(center: CGPoint(x: 50, y: 39.43), radius: 4.59, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false) - - // a 4.49,4.49 0 0 0 -0.62,-2.28 - // Draw an Arc curve from the current point to a new point { x: previous point - 0.62, y: previous point - 2.28 } - // Its radii are { x: 4.49, y: 4.49 }, and with no rotation - // Out of the 4 possible arcs described by the above parameters, this arc is the one lesser than 180 degrees and moving at negative angles - path.addArc(center: CGPoint(x: 49.38, y: 37.15), radius: 4.49, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false) + // M29,2.26 + var point: CGPoint = .init(x: 29, y: 2.26) + path.move(to: point) + + // a4.67,4.67,0,0,0-8,0 + var endpoint: CGPoint = point.translated(x: -8) + path.addSVGArc(startPoint: point, xyRadii: 4.67, clockwise: false, endPoint: endpoint) + point = endpoint + + // L14.42,13.53 + endpoint = .init(x: 14.42, y: 13.53) + path.addLine(to: endpoint) + point = endpoint + + // A32.21,32.21,0,0,1,32.17,40.19 + endpoint = .init(x: 32.17, y: 40.19) + path.addSVGArc(startPoint: point, xyRadii: 32.21, clockwise: true, endPoint: endpoint) + point = endpoint + + // H27.55 + endpoint = .init(x: 27.55, y: point.y) + path.addLine(to: endpoint) + point = endpoint + + // A27.68,27.68,0,0,0,12.09,17.47 + endpoint = .init(x: 12.09, y: 17.47) + path.addSVGArc(startPoint: point, xyRadii: 27.68, clockwise: false, endPoint: endpoint) + point = endpoint + + // L6,28 + endpoint = CGPoint(x: 6, y: 28) + path.addLine(to: endpoint) + point = endpoint + + // a15.92,15.92,0,0,1,9.23,12.17 + endpoint = point.translated(x: 9.23, y: 12.17) + path.addSVGArc(startPoint: point, xyRadii: 15.92, clockwise: true, endPoint: endpoint) + point = endpoint + + // H4.62 + endpoint = .init(x: 4.62, y: point.y) + path.addLine(to: endpoint) + point = endpoint + + // A.76.76,0,0,1,4,39.06 + endpoint = .init(x: 4, y: 39.06) + path.addSVGArc(startPoint: point, xyRadii: 0.76, clockwise: true, endPoint: endpoint) + point = endpoint + + // l2.94-5 + endpoint = point.translated(x: 2.94, y: -5) + path.addLine(to: endpoint) + point = endpoint + + // a10.74,10.74,0,0,0-3.36-1.9 + endpoint = point.translated(x: -3.36, y: -1.9) + path.addSVGArc(startPoint: point, xyRadii: 10.74, clockwise: false, endPoint: endpoint) + point = endpoint + + // l-2.91,5 + endpoint = point.translated(x: -2.91, y: 5) + path.addLine(to: endpoint) + point = endpoint + + // a4.54,4.54,0,0,0,1.69,6.24 + endpoint = point.translated(x: 1.69, y: 6.24) + path.addSVGArc(startPoint: point, xyRadii: 4.54, clockwise: false, endPoint: endpoint) + point = endpoint + + // A4.66,4.66,0,0,0,4.62,44 + endpoint = .init(x: 4.62, y: 44) + path.addSVGArc(startPoint: point, xyRadii: 4.66, clockwise: false, endPoint: endpoint) + point = endpoint + + // H19.15 + endpoint = CGPoint(x: 19.15, y: point.y) + path.addLine(to: endpoint) + point = endpoint + + // a19.4,19.4,0,0,0-8-17.31 + endpoint = point.translated(x: -8, y: -17.31) + path.addSVGArc(startPoint: point, xyRadii: 19.4, clockwise: false, endPoint: endpoint) + point = endpoint + + // l2.31-4 + endpoint = point.translated(x: 2.31, y: -4) + path.addLine(to: endpoint) + point = endpoint + + // A23.87,23.87,0,0,1,23.76,44 + endpoint = .init(x: 23.76, y: 44) + path.addSVGArc(startPoint: point, xyRadii: 23.87, clockwise: true, endPoint: endpoint) + point = endpoint + + // H36.07 + endpoint = CGPoint(x: 36.07, y: point.y) + path.addLine(to: endpoint) + point = endpoint + + // a35.88,35.88,0,0,0-16.41-31.8 + endpoint = point.translated(x: -16.41, y: -31.8) + path.addSVGArc(startPoint: point, xyRadii: 35.88, clockwise: false, endPoint: endpoint) + point = endpoint + + // l4.67-8 + endpoint = point.translated(x: 4.67, y: -8) + path.addLine(to: endpoint) + point = endpoint + + // a.77.77,0,0,1,1.05-.27 + endpoint = point.translated(x: 1.05, y: -0.27) + path.addSVGArc(startPoint: point, xyRadii: 0.77, clockwise: true, endPoint: endpoint) + point = endpoint + + // c.53.29,20.29,34.77,20.66,35.17 + var c1 = point.translated(x: 0.53, y: 0.29) + var c2 = point.translated(x: 20.29, y: 34.77) + endpoint = point.translated(x: 20.66, y: 35.17) + path.addCurve(to: endpoint, control1: c1, control2: c2) + point = endpoint + + // a.76.76,0,0,1-.68,1.13 + endpoint = point.translated(x: -0.68, y: 1.13) + path.addSVGArc(startPoint: point, xyRadii: 0.76, clockwise: true, endPoint: endpoint) + point = endpoint + + // H40.6 + endpoint = CGPoint(x: 40.6, y: point.y) + path.addLine(to: endpoint) + point = endpoint + + // q.09,1.91,0,3.81 + c1 = .init(x: point.x + 0.09, y: point.y + 1.91) + endpoint = .init(x: point.x, y: point.y + 3.81) + path.addQuadCurve(to: endpoint, control: c1) + point = endpoint + + // h4.78 + endpoint = .init(x: point.x + 4.78, y: point.y) + path.addLine(to: endpoint) + point = endpoint + + // A4.59,4.59,0,0,0,50,39.43 + endpoint = .init(x: 50, y: 39.43) + path.addSVGArc(startPoint: point, xyRadii: 4.59, clockwise: false, endPoint: endpoint) + point = endpoint + + // a4.49,4.49,0,0,0-.62-2.28 + endpoint = point.translated(x: -0.62, y: -2.28) + path.addSVGArc(startPoint: point, xyRadii: 4.49, clockwise: false, endPoint: endpoint) + point = endpoint // Z - // Draw a line straight back to the start path.closeSubpath() return path diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index d95bad4eb1..5734ae6570 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -34,7 +34,10 @@ class SentryUserFeedbackForm: UIViewController { stack.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), stack.widthAnchor.constraint(equalTo: scrollView.widthAnchor), - messageTextView.heightAnchor.constraint(equalToConstant: config.theme.font.lineHeight * 5) + messageTextView.heightAnchor.constraint(equalToConstant: config.theme.font.lineHeight * 5), + + sentryLogoView.widthAnchor.constraint(equalToConstant: 72), + sentryLogoView.heightAnchor.constraint(equalToConstant: 66) ]) } From b2b1eba4c4b78e8ec0499fa50d863afa76d9ea2f Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 12 Nov 2024 12:52:17 -0900 Subject: [PATCH 05/12] outline colors and other theming --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 2 +- ...SentryUserFeedbackThemeConfiguration.swift | 68 +++++++++++-------- .../UserFeedback/SentryUserFeedbackForm.swift | 46 ++++++++++--- .../SentryUserFeedbackWidgetButtonView.swift | 4 +- 4 files changed, 79 insertions(+), 41 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index a0df2dd3b7..923d96c17f 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -219,7 +219,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { fontFamily = "ChalkboardSE-Regular" } theme.font = UIFont(name: fontFamily, size: fontSize) ?? UIFont.systemFont(ofSize: fontSize) - theme.outlineColor = .purple + theme.outlineStyle = .init(outlineColor: .purple) theme.foreground = .purple theme.background = .purple.withAlphaComponent(0.1) } diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift index a0e461d8eb..5bb2f0d454 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift @@ -31,55 +31,65 @@ public class SentryUserFeedbackThemeConfiguration: NSObject { * Foreground color for the form submit button. * - note: Default: `rgb(255, 255, 255)` for both dark and light modes */ - public var accentForeground: UIColor = UIColor.white + public var submitForeground: UIColor = UIColor.white /** * Background color for the form submit button in light and dark modes. * - note: Default: `rgb(88, 74, 192)` for both light and dark modes */ - public var accentBackground: UIColor = UIColor(red: 88 / 255, green: 74 / 255, blue: 192 / 255, alpha: 1) + public var submitBackground: UIColor = UIColor(red: 88 / 255, green: 74 / 255, blue: 192 / 255, alpha: 1) /** - * Color used for success-related components (such as text color when feedback is submitted successfully). - * - note: Default light mode: `rgb(38, 141, 117)`; dark mode: `rgb(45, 169, 140)` + * Foreground color for the cancel and screenshot buttons. + * - note: Default: Same as `foreground` for both dark and light modes */ - public var successColor = UIScreen.main.traitCollection.userInterfaceStyle == .dark ? UIColor(red: 45 / 255, green: 169 / 255, blue: 140 / 255, alpha: 1) : UIColor(red: 38 / 255, green: 141 / 255, blue: 117 / 255, alpha: 1) + public lazy var buttonForeground: UIColor = foreground /** - * Color used for error-related components (such as text color when there's an error submitting feedback). - * - note: Default light mode: `rgb(223, 51, 56)`; dark mode: `rgb(245, 84, 89)` + * Background color for the form cancel and screenshot buttons in light and dark modes. + * - note: Default: Transparent for both light and dark modes */ - public var errorColor = UIScreen.main.traitCollection.userInterfaceStyle == .dark ? UIColor(red: 245 / 255, green: 84 / 255, blue: 89 / 255, alpha: 1) : UIColor(red: 223 / 255, green: 51 / 255, blue: 56 / 255, alpha: 1) + public var buttonBackground: UIColor = UIColor.clear /** - * Normal outline color for form inputs. - * - note: Default: `nil (system default)` + * Color used for success-related components (such as text color when feedback is submitted successfully). + * - note: Default light mode: `rgb(38, 141, 117)`; dark mode: `rgb(45, 169, 140)` */ - public var outlineColor = UIColor.systemGray3 + public var successColor = UIScreen.main.traitCollection.userInterfaceStyle == .dark ? UIColor(red: 45 / 255, green: 169 / 255, blue: 140 / 255, alpha: 1) : UIColor(red: 38 / 255, green: 141 / 255, blue: 117 / 255, alpha: 1) /** - * Outline color for form inputs when focused. - * - note: Default: `nil (system default)` + * Color used for error-related components (such as text color when there's an error submitting feedback). + * - note: Default light mode: `rgb(223, 51, 56)`; dark mode: `rgb(245, 84, 89)` */ - public var outlineColorFocussed: UIColor? + public var errorColor = UIScreen.main.traitCollection.userInterfaceStyle == .dark ? UIColor(red: 245 / 255, green: 84 / 255, blue: 89 / 255, alpha: 1) : UIColor(red: 223 / 255, green: 51 / 255, blue: 56 / 255, alpha: 1) - /** - * Normal outline thickness for form inputs. - * - note: Default: `nil (system default)` - */ - public var outlineThickness: NSNumber? + public struct OutlineStyle: Equatable { + /** + * Outline color for form inputs. + * - note: Default: The system default of a UITextField outline with borderStyle of .roundedRect. + */ + public var outlineColor = UIColor(white: 204 / 255, alpha: 1) + + /** + * Outline corner radius for form input elements. + * - note: Default: `5` + */ + public var cornerRadius: CGFloat = 5 + + public var outlineWidth: CGFloat = 0.5 + + public init(outlineColor: UIColor = UIColor(white: 204 / 255, alpha: 1), cornerRadius: CGFloat = 5, outlineWidth: CGFloat = 0.5) { + self.outlineColor = outlineColor + self.cornerRadius = cornerRadius + self.outlineWidth = outlineWidth + } + } - /** - * Outline thickness for form inputs when focused. - * - note: Default: `nil (system default)` - */ - public var outlineThicknessFocussed: NSNumber? + let defaultOutlineStyle = OutlineStyle() + public lazy var outlineStyle: OutlineStyle = defaultOutlineStyle - /** - * Outline corner radius for form input elements. - * - note: Default: `nil (system default)` - */ - public var cornerRadius: NSNumber? + public var inputBackground: UIColor = UIColor.secondarySystemBackground + public var inputBorder: CGFloat? = nil } #endif // os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index 5734ae6570..90acd05f12 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -92,7 +92,13 @@ class SentryUserFeedbackForm: UIViewController { lazy var fullNameTextField = { let field = UITextField(frame: .zero) field.placeholder = config.formConfig.namePlaceholder - field.borderStyle = .roundedRect + if config.theme.outlineStyle == config.theme.defaultOutlineStyle { + field.borderStyle = .roundedRect + } else { + field.layer.cornerRadius = config.theme.outlineStyle.cornerRadius + field.layer.borderWidth = config.theme.outlineStyle.outlineWidth + field.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor + } field.accessibilityLabel = config.formConfig.nameTextFieldAccessibilityLabel return field }() @@ -106,7 +112,13 @@ class SentryUserFeedbackForm: UIViewController { lazy var emailTextField = { let field = UITextField(frame: .zero) field.placeholder = config.formConfig.emailPlaceholder - field.borderStyle = .roundedRect + if config.theme.outlineStyle == config.theme.defaultOutlineStyle { + field.borderStyle = .roundedRect + } else { + field.layer.cornerRadius = config.theme.outlineStyle.cornerRadius + field.layer.borderWidth = config.theme.outlineStyle.outlineWidth + field.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor + } field.accessibilityLabel = config.formConfig.emailTextFieldAccessibilityLabel return field }() @@ -122,9 +134,9 @@ class SentryUserFeedbackForm: UIViewController { textView.text = config.formConfig.messagePlaceholder // TODO: color the text as placeholder if this is the content of the textview, otherwise change to regular foreground color textView.isScrollEnabled = true textView.isEditable = true - textView.layer.borderWidth = 0.3 - textView.layer.borderColor = UIColor(white: 204 / 255, alpha: 1).cgColor // this is the observed color of a textfield outline when using borderStyle = .roundedRect - textView.layer.cornerRadius = 5 + textView.layer.cornerRadius = config.theme.outlineStyle.cornerRadius + textView.layer.borderWidth = config.theme.outlineStyle.outlineWidth + textView.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor textView.accessibilityLabel = config.formConfig.messageTextViewAccessibilityLabel return textView }() @@ -133,8 +145,12 @@ class SentryUserFeedbackForm: UIViewController { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.addScreenshotButtonLabel, for: .normal) button.accessibilityLabel = config.formConfig.addScreenshotButtonAccessibilityLabel - button.backgroundColor = .systemBlue + button.backgroundColor = config.theme.buttonBackground + button.setTitleColor(config.theme.buttonForeground, for: .normal) button.addTarget(self, action: #selector(addScreenshotButtonTapped), for: .touchUpInside) + button.layer.cornerRadius = config.theme.outlineStyle.cornerRadius + button.layer.borderWidth = config.theme.outlineStyle.outlineWidth + button.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor return button }() @@ -142,8 +158,12 @@ class SentryUserFeedbackForm: UIViewController { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.removeScreenshotButtonLabel, for: .normal) button.accessibilityLabel = config.formConfig.removeScreenshotButtonAccessibilityLabel - button.backgroundColor = .systemBlue + button.backgroundColor = config.theme.buttonBackground + button.setTitleColor(config.theme.buttonForeground, for: .normal) button.addTarget(self, action: #selector(removeScreenshotButtonTapped), for: .touchUpInside) + button.layer.cornerRadius = config.theme.outlineStyle.cornerRadius + button.layer.borderWidth = config.theme.outlineStyle.outlineWidth + button.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor return button }() @@ -151,8 +171,12 @@ class SentryUserFeedbackForm: UIViewController { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.submitButtonLabel, for: .normal) button.accessibilityLabel = config.formConfig.submitButtonAccessibilityLabel - button.backgroundColor = .systemGreen + button.backgroundColor = config.theme.submitBackground + button.setTitleColor(config.theme.submitForeground, for: .normal) button.addTarget(self, action: #selector(submitFeedbackButtonTapped), for: .touchUpInside) + button.layer.cornerRadius = config.theme.outlineStyle.cornerRadius + button.layer.borderWidth = config.theme.outlineStyle.outlineWidth + button.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor return button }() @@ -160,8 +184,12 @@ class SentryUserFeedbackForm: UIViewController { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.cancelButtonLabel, for: .normal) button.accessibilityLabel = config.formConfig.cancelButtonAccessibilityLabel - button.backgroundColor = .systemRed + button.backgroundColor = config.theme.buttonBackground + button.setTitleColor(config.theme.buttonForeground, for: .normal) button.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside) + button.layer.cornerRadius = config.theme.outlineStyle.cornerRadius + button.layer.borderWidth = config.theme.outlineStyle.outlineWidth + button.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor return button }() diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonView.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonView.swift index 8c19c0bf01..42d292a748 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonView.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonView.swift @@ -158,10 +158,10 @@ class SentryUserFeedbackWidgetButtonView: UIView { if UIScreen.main.traitCollection.userInterfaceStyle == .dark { lozengeLayer.fillColor = config.darkTheme.background.cgColor - lozengeLayer.strokeColor = config.darkTheme.outlineColor.cgColor + lozengeLayer.strokeColor = config.darkTheme.outlineStyle.outlineColor.cgColor } else { lozengeLayer.fillColor = config.theme.background.cgColor - lozengeLayer.strokeColor = config.theme.outlineColor.cgColor + lozengeLayer.strokeColor = config.theme.outlineStyle.outlineColor.cgColor } let iconSizeDifference = (scaledIconSize - svgSize) / 2 From 78689e198010574ddeb80791e7cbf6dcdbd804ff Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 12 Nov 2024 12:52:59 -0900 Subject: [PATCH 06/12] spacing, layout --- .../UserFeedback/SentryUserFeedbackForm.swift | 32 +++++++++++++------ .../SentryUserFeedbackWidget.swift | 2 +- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index 90acd05f12..b88b0807a9 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -201,29 +201,41 @@ class SentryUserFeedbackForm: UIViewController { let stack = UIStackView() stack.axis = .vertical - stack.spacing = 8 + stack.spacing = 50 stack.addArrangedSubview(headerStack) + let inputStack = UIStackView() + if self.config.formConfig.showName { - stack.addArrangedSubview(self.fullNameLabel) - stack.addArrangedSubview(self.fullNameTextField) + inputStack.addArrangedSubview(self.fullNameLabel) + inputStack.addArrangedSubview(self.fullNameTextField) } if self.config.formConfig.showEmail { - stack.addArrangedSubview(self.emailLabel) - stack.addArrangedSubview(self.emailTextField) + inputStack.addArrangedSubview(self.emailLabel) + inputStack.addArrangedSubview(self.emailTextField) } - stack.addArrangedSubview(self.messageLabel) - stack.addArrangedSubview(self.messageTextView) + inputStack.addArrangedSubview(self.messageLabel) + inputStack.addArrangedSubview(self.messageTextView) if self.config.formConfig.enableScreenshot { - stack.addArrangedSubview(self.addScreenshotButton) + inputStack.addArrangedSubview(self.addScreenshotButton) } - stack.addArrangedSubview(self.submitButton) - stack.addArrangedSubview(self.cancelButton) + stack.addArrangedSubview(inputStack) + + let controlsStack = UIStackView() + + controlsStack.addArrangedSubview(self.submitButton) + controlsStack.addArrangedSubview(self.cancelButton) + stack.addArrangedSubview(controlsStack) + + [inputStack, controlsStack].forEach { + $0.axis = .vertical + $0.spacing = 8 + } stack.translatesAutoresizingMaskIntoConstraints = false diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift index 0fbcdedc3c..e4184a735c 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift @@ -11,7 +11,7 @@ struct SentryUserFeedbackWidget { class RootViewController: UIViewController, SentryUserFeedbackFormDelegate, UIAdaptivePresentationControllerDelegate { let defaultWidgetSpacing: CGFloat = 8 - lazy var button = SentryUserFeedbackWidgetButtonView(config: config, action: { sender in + lazy var button = SentryUserFeedbackWidgetButtonView(config: config, action: { _ in self.setWidget(visible: false) let form = SentryUserFeedbackForm(config: self.config, delegate: self) form.presentationController?.delegate = self From b88e5f081156449878c3ab6a128aecc9e1ed4a43 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 12 Nov 2024 17:13:41 -0900 Subject: [PATCH 07/12] spacing and message placeholder --- .../SentryUserFeedbackConfiguration.swift | 1 + .../SentryUserFeedbackFormConfiguration.swift | 10 +++ ...SentryUserFeedbackThemeConfiguration.swift | 8 +- .../UserFeedback/SentryUserFeedbackForm.swift | 88 +++++++++++++------ 4 files changed, 78 insertions(+), 29 deletions(-) diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift index 5f4bbcc699..510597017e 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift @@ -131,6 +131,7 @@ public class SentryUserFeedbackConfiguration: NSObject { let padding: CGFloat = 16 let spacing: CGFloat = 8 + let margin: CGFloat = 32 } #endif // os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift index 56e614ef54..fa1b2a81a6 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift @@ -29,6 +29,8 @@ public class SentryUserFeedbackFormConfiguration: NSObject { */ public var messageLabel: String = "Description" + lazy var messageLabelContents = fullLabelText(labelText: messageLabel, required: true) + /** * The placeholder for the feedback description input field. * - note: Default: `"What's the bug? What did you expect?"` @@ -97,6 +99,8 @@ public class SentryUserFeedbackFormConfiguration: NSObject { */ public var nameLabel: String = "Name" + lazy var nameLabelContents = fullLabelText(labelText: nameLabel, required: isNameRequired) + /** * The placeholder for the name input field. * - note: Default: `"Your Name"` @@ -127,6 +131,8 @@ public class SentryUserFeedbackFormConfiguration: NSObject { */ public var emailLabel: String = "Email" + lazy var emailLabelContents = fullLabelText(labelText: emailLabel, required: isEmailRequired) + /** * The placeholder for the email input field. * - note: Default: `"your.email@example.org"` @@ -160,6 +166,10 @@ public class SentryUserFeedbackFormConfiguration: NSObject { * - note: Default: `cancelButtonLabel` value */ public lazy var cancelButtonAccessibilityLabel: String = cancelButtonLabel + + func fullLabelText(labelText: String, required: Bool) -> String { + required ? labelText + " " + isRequiredLabel : labelText + } } #endif // os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift index 5bb2f0d454..78171a67cb 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift @@ -15,6 +15,10 @@ public class SentryUserFeedbackThemeConfiguration: NSObject { */ public var font: UIFont = UIFont.preferredFont(forTextStyle: .callout) + public var titleFont = UIFont.preferredFont(forTextStyle: .title1) + + public var headingFont = UIFont.preferredFont(forTextStyle: .headline) + /** * Foreground text color of the widget and form. * - note: Default light mode: `rgb(43, 34, 51)`; dark mode: `rgb(235, 230, 239)` @@ -68,7 +72,7 @@ public class SentryUserFeedbackThemeConfiguration: NSObject { * Outline color for form inputs. * - note: Default: The system default of a UITextField outline with borderStyle of .roundedRect. */ - public var outlineColor = UIColor(white: 204 / 255, alpha: 1) + public var outlineColor = UIColor(white: 204 / 255, alpha: 1) /** * Outline corner radius for form input elements. @@ -89,7 +93,7 @@ public class SentryUserFeedbackThemeConfiguration: NSObject { public lazy var outlineStyle: OutlineStyle = defaultOutlineStyle public var inputBackground: UIColor = UIColor.secondarySystemBackground - public var inputBorder: CGFloat? = nil + public var inputBorder: CGFloat? } #endif // os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index b88b0807a9..359ad3ce30 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -22,11 +22,12 @@ class SentryUserFeedbackForm: UIViewController { view.backgroundColor = .systemBackground + let formElementHeight: CGFloat = 50 NSLayoutConstraint.activate([ - scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: config.spacing), - scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: config.spacing), - scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -config.spacing), - scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -config.spacing), + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: config.margin), + scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: config.margin), + scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -config.margin), + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -config.margin), stack.topAnchor.constraint(equalTo: scrollView.topAnchor), stack.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), @@ -36,8 +37,18 @@ class SentryUserFeedbackForm: UIViewController { messageTextView.heightAnchor.constraint(equalToConstant: config.theme.font.lineHeight * 5), - sentryLogoView.widthAnchor.constraint(equalToConstant: 72), - sentryLogoView.heightAnchor.constraint(equalToConstant: 66) + sentryLogoView.widthAnchor.constraint(equalToConstant: 50), + sentryLogoView.heightAnchor.constraint(equalTo: sentryLogoView.widthAnchor, multiplier: 66 / 72), + + fullNameTextField.heightAnchor.constraint(equalToConstant: formElementHeight), + emailTextField.heightAnchor.constraint(equalToConstant: formElementHeight), + addScreenshotButton.heightAnchor.constraint(equalToConstant: formElementHeight), + removeScreenshotButton.heightAnchor.constraint(equalToConstant: formElementHeight), + submitButton.heightAnchor.constraint(equalToConstant: formElementHeight), + cancelButton.heightAnchor.constraint(equalToConstant: formElementHeight), + + messageTextViewPlaceholder.leadingAnchor.constraint(equalTo: messageTextView.leadingAnchor, constant: messageTextView.textContainerInset.left + 5), + messageTextViewPlaceholder.topAnchor.constraint(equalTo: messageTextView.topAnchor, constant: messageTextView.textContainerInset.top) ]) } @@ -69,6 +80,8 @@ class SentryUserFeedbackForm: UIViewController { lazy var formTitleLabel = { let label = UILabel(frame: .zero) label.text = config.formConfig.formTitle + label.font = config.theme.titleFont + label.setContentCompressionResistancePriority(.required, for: .horizontal) return label }() @@ -85,13 +98,15 @@ class SentryUserFeedbackForm: UIViewController { lazy var fullNameLabel = { let label = UILabel(frame: .zero) - label.text = fullLabelText(labelText: config.formConfig.nameLabel, required: config.formConfig.isNameRequired) + label.text = config.formConfig.nameLabelContents + label.font = config.theme.headingFont return label }() lazy var fullNameTextField = { let field = UITextField(frame: .zero) field.placeholder = config.formConfig.namePlaceholder + field.font = config.theme.font if config.theme.outlineStyle == config.theme.defaultOutlineStyle { field.borderStyle = .roundedRect } else { @@ -105,13 +120,15 @@ class SentryUserFeedbackForm: UIViewController { lazy var emailLabel = { let label = UILabel(frame: .zero) - label.text = fullLabelText(labelText: config.formConfig.emailLabel, required: config.formConfig.isEmailRequired) + label.text = config.formConfig.emailLabelContents + label.font = config.theme.headingFont return label }() lazy var emailTextField = { let field = UITextField(frame: .zero) field.placeholder = config.formConfig.emailPlaceholder + field.font = config.theme.font if config.theme.outlineStyle == config.theme.defaultOutlineStyle { field.borderStyle = .roundedRect } else { @@ -125,25 +142,38 @@ class SentryUserFeedbackForm: UIViewController { lazy var messageLabel = { let label = UILabel(frame: .zero) - label.text = config.formConfig.messageLabel + label.text = config.formConfig.messageLabelContents + label.font = config.theme.headingFont + return label + }() + + lazy var messageTextViewPlaceholder = { + let label = UILabel(frame: .zero) + label.text = config.formConfig.messagePlaceholder + label.font = config.theme.font + label.textColor = .placeholderText + label.translatesAutoresizingMaskIntoConstraints = false return label }() lazy var messageTextView = { let textView = UITextView(frame: .zero) - textView.text = config.formConfig.messagePlaceholder // TODO: color the text as placeholder if this is the content of the textview, otherwise change to regular foreground color + textView.font = config.theme.font textView.isScrollEnabled = true textView.isEditable = true textView.layer.cornerRadius = config.theme.outlineStyle.cornerRadius textView.layer.borderWidth = config.theme.outlineStyle.outlineWidth textView.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor textView.accessibilityLabel = config.formConfig.messageTextViewAccessibilityLabel + textView.textContainerInset = .init(top: 13, left: 2, bottom: 13, right: 2) + textView.delegate = self return textView }() lazy var addScreenshotButton = { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.addScreenshotButtonLabel, for: .normal) + button.titleLabel?.font = config.theme.headingFont button.accessibilityLabel = config.formConfig.addScreenshotButtonAccessibilityLabel button.backgroundColor = config.theme.buttonBackground button.setTitleColor(config.theme.buttonForeground, for: .normal) @@ -157,6 +187,7 @@ class SentryUserFeedbackForm: UIViewController { lazy var removeScreenshotButton = { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.removeScreenshotButtonLabel, for: .normal) + button.titleLabel?.font = config.theme.headingFont button.accessibilityLabel = config.formConfig.removeScreenshotButtonAccessibilityLabel button.backgroundColor = config.theme.buttonBackground button.setTitleColor(config.theme.buttonForeground, for: .normal) @@ -170,6 +201,7 @@ class SentryUserFeedbackForm: UIViewController { lazy var submitButton = { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.submitButtonLabel, for: .normal) + button.titleLabel?.font = config.theme.headingFont button.accessibilityLabel = config.formConfig.submitButtonAccessibilityLabel button.backgroundColor = config.theme.submitBackground button.setTitleColor(config.theme.submitForeground, for: .normal) @@ -183,6 +215,7 @@ class SentryUserFeedbackForm: UIViewController { lazy var cancelButton = { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.cancelButtonLabel, for: .normal) + button.titleLabel?.font = config.theme.headingFont button.accessibilityLabel = config.formConfig.cancelButtonAccessibilityLabel button.backgroundColor = config.theme.buttonBackground button.setTitleColor(config.theme.buttonForeground, for: .normal) @@ -206,6 +239,8 @@ class SentryUserFeedbackForm: UIViewController { stack.addArrangedSubview(headerStack) let inputStack = UIStackView() + inputStack.axis = .vertical + inputStack.spacing = config.theme.font.xHeight if self.config.formConfig.showName { inputStack.addArrangedSubview(self.fullNameLabel) @@ -218,25 +253,26 @@ class SentryUserFeedbackForm: UIViewController { } inputStack.addArrangedSubview(self.messageLabel) - inputStack.addArrangedSubview(self.messageTextView) + + let messageAndScreenshotStack = UIStackView(arrangedSubviews: [self.messageTextView]) + messageAndScreenshotStack.axis = .vertical if self.config.formConfig.enableScreenshot { - inputStack.addArrangedSubview(self.addScreenshotButton) + messageAndScreenshotStack.addArrangedSubview(self.addScreenshotButton) } + messageAndScreenshotStack.spacing = config.theme.font.lineHeight - config.theme.font.xHeight + + inputStack.addArrangedSubview(messageAndScreenshotStack) stack.addArrangedSubview(inputStack) let controlsStack = UIStackView() - + controlsStack.axis = .vertical + controlsStack.spacing = config.theme.font.lineHeight - config.theme.font.xHeight controlsStack.addArrangedSubview(self.submitButton) controlsStack.addArrangedSubview(self.cancelButton) stack.addArrangedSubview(controlsStack) - [inputStack, controlsStack].forEach { - $0.axis = .vertical - $0.spacing = 8 - } - stack.translatesAutoresizingMaskIntoConstraints = false return stack @@ -247,17 +283,15 @@ class SentryUserFeedbackForm: UIViewController { view.addSubview(scrollView) scrollView.addSubview(stack) scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(messageTextViewPlaceholder) return scrollView }() - - // MARK: Helpers - - func fullLabelText(labelText: String, required: Bool) -> String { - if required { - return labelText + " " + config.formConfig.isRequiredLabel - } else { - return labelText - } +} + +@available(iOS 13.0, *) +extension SentryUserFeedbackForm: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + messageTextViewPlaceholder.isHidden = textView.text != "" } } From 4d611fbb9f90941d2823e1f0802edf4c85553b81 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 12 Nov 2024 17:30:29 -0900 Subject: [PATCH 08/12] refactor --- .../UserFeedback/SentryUserFeedbackForm.swift | 76 ++++++++----------- 1 file changed, 30 insertions(+), 46 deletions(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index 359ad3ce30..541c9bf6f3 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -50,6 +50,36 @@ class SentryUserFeedbackForm: UIViewController { messageTextViewPlaceholder.leadingAnchor.constraint(equalTo: messageTextView.leadingAnchor, constant: messageTextView.textContainerInset.left + 5), messageTextViewPlaceholder.topAnchor.constraint(equalTo: messageTextView.topAnchor, constant: messageTextView.textContainerInset.top) ]) + + [fullNameTextField, emailTextField].forEach { + $0.font = config.theme.font + if config.theme.outlineStyle == config.theme.defaultOutlineStyle { + $0.borderStyle = .roundedRect + } else { + $0.layer.cornerRadius = config.theme.outlineStyle.cornerRadius + $0.layer.borderWidth = config.theme.outlineStyle.outlineWidth + $0.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor + } + } + + [fullNameLabel, emailLabel, messageLabel].forEach { + $0.font = config.theme.headingFont + } + + [submitButton, addScreenshotButton, removeScreenshotButton, cancelButton].forEach { + $0.titleLabel?.font = config.theme.headingFont + } + + [submitButton, addScreenshotButton, removeScreenshotButton, cancelButton, messageTextView].forEach { + $0.layer.cornerRadius = config.theme.outlineStyle.cornerRadius + $0.layer.borderWidth = config.theme.outlineStyle.outlineWidth + $0.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor + } + + [addScreenshotButton, removeScreenshotButton, cancelButton].forEach { + $0.backgroundColor = config.theme.buttonBackground + $0.setTitleColor(config.theme.buttonForeground, for: .normal) + } } required init?(coder: NSCoder) { @@ -99,21 +129,12 @@ class SentryUserFeedbackForm: UIViewController { lazy var fullNameLabel = { let label = UILabel(frame: .zero) label.text = config.formConfig.nameLabelContents - label.font = config.theme.headingFont return label }() lazy var fullNameTextField = { let field = UITextField(frame: .zero) field.placeholder = config.formConfig.namePlaceholder - field.font = config.theme.font - if config.theme.outlineStyle == config.theme.defaultOutlineStyle { - field.borderStyle = .roundedRect - } else { - field.layer.cornerRadius = config.theme.outlineStyle.cornerRadius - field.layer.borderWidth = config.theme.outlineStyle.outlineWidth - field.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor - } field.accessibilityLabel = config.formConfig.nameTextFieldAccessibilityLabel return field }() @@ -121,21 +142,12 @@ class SentryUserFeedbackForm: UIViewController { lazy var emailLabel = { let label = UILabel(frame: .zero) label.text = config.formConfig.emailLabelContents - label.font = config.theme.headingFont return label }() lazy var emailTextField = { let field = UITextField(frame: .zero) field.placeholder = config.formConfig.emailPlaceholder - field.font = config.theme.font - if config.theme.outlineStyle == config.theme.defaultOutlineStyle { - field.borderStyle = .roundedRect - } else { - field.layer.cornerRadius = config.theme.outlineStyle.cornerRadius - field.layer.borderWidth = config.theme.outlineStyle.outlineWidth - field.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor - } field.accessibilityLabel = config.formConfig.emailTextFieldAccessibilityLabel return field }() @@ -143,7 +155,6 @@ class SentryUserFeedbackForm: UIViewController { lazy var messageLabel = { let label = UILabel(frame: .zero) label.text = config.formConfig.messageLabelContents - label.font = config.theme.headingFont return label }() @@ -159,11 +170,6 @@ class SentryUserFeedbackForm: UIViewController { lazy var messageTextView = { let textView = UITextView(frame: .zero) textView.font = config.theme.font - textView.isScrollEnabled = true - textView.isEditable = true - textView.layer.cornerRadius = config.theme.outlineStyle.cornerRadius - textView.layer.borderWidth = config.theme.outlineStyle.outlineWidth - textView.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor textView.accessibilityLabel = config.formConfig.messageTextViewAccessibilityLabel textView.textContainerInset = .init(top: 13, left: 2, bottom: 13, right: 2) textView.delegate = self @@ -173,56 +179,34 @@ class SentryUserFeedbackForm: UIViewController { lazy var addScreenshotButton = { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.addScreenshotButtonLabel, for: .normal) - button.titleLabel?.font = config.theme.headingFont button.accessibilityLabel = config.formConfig.addScreenshotButtonAccessibilityLabel - button.backgroundColor = config.theme.buttonBackground - button.setTitleColor(config.theme.buttonForeground, for: .normal) button.addTarget(self, action: #selector(addScreenshotButtonTapped), for: .touchUpInside) - button.layer.cornerRadius = config.theme.outlineStyle.cornerRadius - button.layer.borderWidth = config.theme.outlineStyle.outlineWidth - button.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor return button }() lazy var removeScreenshotButton = { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.removeScreenshotButtonLabel, for: .normal) - button.titleLabel?.font = config.theme.headingFont button.accessibilityLabel = config.formConfig.removeScreenshotButtonAccessibilityLabel - button.backgroundColor = config.theme.buttonBackground - button.setTitleColor(config.theme.buttonForeground, for: .normal) button.addTarget(self, action: #selector(removeScreenshotButtonTapped), for: .touchUpInside) - button.layer.cornerRadius = config.theme.outlineStyle.cornerRadius - button.layer.borderWidth = config.theme.outlineStyle.outlineWidth - button.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor return button }() lazy var submitButton = { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.submitButtonLabel, for: .normal) - button.titleLabel?.font = config.theme.headingFont button.accessibilityLabel = config.formConfig.submitButtonAccessibilityLabel button.backgroundColor = config.theme.submitBackground button.setTitleColor(config.theme.submitForeground, for: .normal) button.addTarget(self, action: #selector(submitFeedbackButtonTapped), for: .touchUpInside) - button.layer.cornerRadius = config.theme.outlineStyle.cornerRadius - button.layer.borderWidth = config.theme.outlineStyle.outlineWidth - button.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor return button }() lazy var cancelButton = { let button = UIButton(frame: .zero) button.setTitle(config.formConfig.cancelButtonLabel, for: .normal) - button.titleLabel?.font = config.theme.headingFont button.accessibilityLabel = config.formConfig.cancelButtonAccessibilityLabel - button.backgroundColor = config.theme.buttonBackground - button.setTitleColor(config.theme.buttonForeground, for: .normal) button.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside) - button.layer.cornerRadius = config.theme.outlineStyle.cornerRadius - button.layer.borderWidth = config.theme.outlineStyle.outlineWidth - button.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor return button }() From 06dbae05668b254e79782bf4e75bd83d84819162 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 14 Nov 2024 12:53:00 -0900 Subject: [PATCH 09/12] respond to changes in dynamic type size --- .../SentryUserFeedbackConfiguration.swift | 17 +++- ...SentryUserFeedbackThemeConfiguration.swift | 26 ++++++- .../UserFeedback/SentryUserFeedbackForm.swift | 77 +++++++++++++++---- 3 files changed, 98 insertions(+), 22 deletions(-) diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift index 510597017e..102f682df7 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift @@ -113,19 +113,28 @@ public class SentryUserFeedbackConfiguration: NSObject { }() /// The ratio of the configured font size to the system default font size, to know how large to scale things like the icon and lozenge shape. - lazy var scaleFactor: CGFloat = { + lazy var scaleFactor = calculateScaleFactor() + + func calculateScaleFactor() -> CGFloat { let fontSize = theme.font.pointSize guard fontSize > 0 else { return 1 } return fontSize / UIFont.systemFontSize - }() + } /// Too much padding as the font size grows larger makes the button look weird with lots of negative space. Keeping the padding constant looks weird if the text is too small. So, scale it down below system default font sizes, but keep it fixed with larger font sizes. - lazy var paddingScaleFactor: CGFloat = { + lazy var paddingScaleFactor = calculatePaddingScaleFactor() + + func calculatePaddingScaleFactor() -> CGFloat { scaleFactor > 1 ? 1 : scaleFactor - }() + } + + func recalculateScaleFactors() { + scaleFactor = calculateScaleFactor() + paddingScaleFactor = calculatePaddingScaleFactor() + } // MARK: Layout diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift index 78171a67cb..5ecead6182 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift @@ -9,15 +9,30 @@ import UIKit @available(iOS 13.0, *) @objcMembers public class SentryUserFeedbackThemeConfiguration: NSObject { + lazy var defaultFont = UIFont.preferredFont(forTextStyle: .callout) + + lazy var defaultTitleFont = UIFont.preferredFont(forTextStyle: .title1) + + lazy var defaultHeadingFont = UIFont.preferredFont(forTextStyle: .headline) + /** * The default font to use. * - note: Defaults to the current system default. */ - public var font: UIFont = UIFont.preferredFont(forTextStyle: .callout) + public lazy var font = defaultFont - public var titleFont = UIFont.preferredFont(forTextStyle: .title1) + public lazy var titleFont = defaultTitleFont - public var headingFont = UIFont.preferredFont(forTextStyle: .headline) + public lazy var headingFont = defaultHeadingFont + + func updateDefaultFonts() { + defaultFont = UIFont.preferredFont(forTextStyle: .callout) + defaultTitleFont = UIFont.preferredFont(forTextStyle: .title1) + defaultHeadingFont = UIFont.preferredFont(forTextStyle: .headline) + font = defaultFont + titleFont = defaultTitleFont + headingFont = defaultHeadingFont + } /** * Foreground text color of the widget and form. @@ -89,11 +104,14 @@ public class SentryUserFeedbackThemeConfiguration: NSObject { } } + // We need to keep a reference to a default instance of this for comparison purposes later. We don't use the default to give UITextFields a default style, instead, we use `UITextField.BorderStyle.roundedRect` if `SentryUserFeedbackThemeConfiguration.outlineStyle == defaultOutlineStyle`. let defaultOutlineStyle = OutlineStyle() + + // Options for styling the outline of input elements and buttons in the feedback form. public lazy var outlineStyle: OutlineStyle = defaultOutlineStyle + // The background color to use for text inputs in the feedback form. public var inputBackground: UIColor = UIColor.secondarySystemBackground - public var inputBorder: CGFloat? } #endif // os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index 541c9bf6f3..5569dbdbea 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -15,14 +15,35 @@ class SentryUserFeedbackForm: UIViewController { let config: SentryUserFeedbackConfiguration weak var delegate: (any SentryUserFeedbackFormDelegate)? + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + if config.theme.font == config.theme.defaultFont { + config.theme.updateDefaultFonts() + config.recalculateScaleFactors() + } + + updateLayout() + } + + let formElementHeight: CGFloat = 40 + let logoWidth: CGFloat = 47 + lazy var messageTextViewHeightConstraint = messageTextView.heightAnchor.constraint(equalToConstant: config.theme.font.lineHeight * 5) + lazy var logoViewWidthConstraint = sentryLogoView.widthAnchor.constraint(equalToConstant: logoWidth * config.scaleFactor) + lazy var messagePlaceholderLeadingConstraint = messageTextViewPlaceholder.leadingAnchor.constraint(equalTo: messageTextView.leadingAnchor, constant: messageTextView.textContainerInset.left + 5) + lazy var messagePlaceholderTopConstraint = messageTextViewPlaceholder.topAnchor.constraint(equalTo: messageTextView.topAnchor, constant: messageTextView.textContainerInset.top) + lazy var fullNameTextFieldHeightConstraint = fullNameTextField.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) + lazy var emailTextFieldHeightConstraint = emailTextField.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) + lazy var addScreenshotButtonHeightConstraint = addScreenshotButton.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) + lazy var removeScreenshotButtonHeightConstraint = removeScreenshotButton.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) + lazy var submitButtonHeightConstraint = submitButton.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) + lazy var cancelButtonHeightConstraint = cancelButton.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) + init(config: SentryUserFeedbackConfiguration, delegate: any SentryUserFeedbackFormDelegate) { self.config = config self.delegate = delegate super.init(nibName: nil, bundle: nil) - view.backgroundColor = .systemBackground - let formElementHeight: CGFloat = 50 + NSLayoutConstraint.activate([ scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: config.margin), scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: config.margin), @@ -35,24 +56,26 @@ class SentryUserFeedbackForm: UIViewController { stack.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), stack.widthAnchor.constraint(equalTo: scrollView.widthAnchor), - messageTextView.heightAnchor.constraint(equalToConstant: config.theme.font.lineHeight * 5), + messageTextViewHeightConstraint, - sentryLogoView.widthAnchor.constraint(equalToConstant: 50), - sentryLogoView.heightAnchor.constraint(equalTo: sentryLogoView.widthAnchor, multiplier: 66 / 72), + logoViewWidthConstraint, + sentryLogoView.heightAnchor.constraint(equalTo: sentryLogoView.widthAnchor, multiplier: 41 / 47), - fullNameTextField.heightAnchor.constraint(equalToConstant: formElementHeight), - emailTextField.heightAnchor.constraint(equalToConstant: formElementHeight), - addScreenshotButton.heightAnchor.constraint(equalToConstant: formElementHeight), - removeScreenshotButton.heightAnchor.constraint(equalToConstant: formElementHeight), - submitButton.heightAnchor.constraint(equalToConstant: formElementHeight), - cancelButton.heightAnchor.constraint(equalToConstant: formElementHeight), + fullNameTextFieldHeightConstraint, + emailTextFieldHeightConstraint, + addScreenshotButtonHeightConstraint, + removeScreenshotButtonHeightConstraint, + submitButtonHeightConstraint, + cancelButtonHeightConstraint, - messageTextViewPlaceholder.leadingAnchor.constraint(equalTo: messageTextView.leadingAnchor, constant: messageTextView.textContainerInset.left + 5), - messageTextViewPlaceholder.topAnchor.constraint(equalTo: messageTextView.topAnchor, constant: messageTextView.textContainerInset.top) + // the extra 5 pixels was observed experimentally and is invariant under changes in dynamic type sizes + messagePlaceholderLeadingConstraint, + messagePlaceholderTopConstraint ]) [fullNameTextField, emailTextField].forEach { $0.font = config.theme.font + $0.adjustsFontForContentSizeCategory = true if config.theme.outlineStyle == config.theme.defaultOutlineStyle { $0.borderStyle = .roundedRect } else { @@ -62,12 +85,18 @@ class SentryUserFeedbackForm: UIViewController { } } + [fullNameTextField, emailTextField, messageTextView].forEach { + $0.backgroundColor = config.theme.inputBackground + } + [fullNameLabel, emailLabel, messageLabel].forEach { $0.font = config.theme.headingFont + $0.adjustsFontForContentSizeCategory = true } [submitButton, addScreenshotButton, removeScreenshotButton, cancelButton].forEach { $0.titleLabel?.font = config.theme.headingFont + $0.titleLabel?.adjustsFontForContentSizeCategory = true } [submitButton, addScreenshotButton, removeScreenshotButton, cancelButton, messageTextView].forEach { @@ -107,11 +136,30 @@ class SentryUserFeedbackForm: UIViewController { // MARK: UI + func updateLayout() { + let verticalPadding: CGFloat = 8 + messageTextView.textContainerInset = .init(top: verticalPadding * config.scaleFactor, left: 2 * config.scaleFactor, bottom: verticalPadding * config.scaleFactor, right: 2 * config.scaleFactor) + + messageTextViewHeightConstraint.constant = config.theme.font.lineHeight * 5 + logoViewWidthConstraint.constant = logoWidth * config.scaleFactor + messagePlaceholderLeadingConstraint.constant = messageTextView.textContainerInset.left + 5 + messagePlaceholderTopConstraint.constant = messageTextView.textContainerInset.top + fullNameTextFieldHeightConstraint.constant = formElementHeight * config.scaleFactor + emailTextFieldHeightConstraint.constant = formElementHeight * config.scaleFactor + addScreenshotButtonHeightConstraint.constant = formElementHeight * config.scaleFactor + removeScreenshotButtonHeightConstraint.constant = formElementHeight * config.scaleFactor + submitButtonHeightConstraint.constant = formElementHeight * config.scaleFactor + cancelButtonHeightConstraint.constant = formElementHeight * config.scaleFactor + + + } + lazy var formTitleLabel = { let label = UILabel(frame: .zero) label.text = config.formConfig.formTitle label.font = config.theme.titleFont label.setContentCompressionResistancePriority(.required, for: .horizontal) + label.adjustsFontForContentSizeCategory = true return label }() @@ -164,14 +212,15 @@ class SentryUserFeedbackForm: UIViewController { label.font = config.theme.font label.textColor = .placeholderText label.translatesAutoresizingMaskIntoConstraints = false + label.adjustsFontForContentSizeCategory = true return label }() lazy var messageTextView = { let textView = UITextView(frame: .zero) textView.font = config.theme.font + textView.adjustsFontForContentSizeCategory = true textView.accessibilityLabel = config.formConfig.messageTextViewAccessibilityLabel - textView.textContainerInset = .init(top: 13, left: 2, bottom: 13, right: 2) textView.delegate = self return textView }() From 82d63dfdf4bc3a15f620d6d2c0745a4daa678d1d Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 14 Nov 2024 12:53:29 -0900 Subject: [PATCH 10/12] fix unresponsiveness with partial swipe to dismiss --- .../Integrations/UserFeedback/SentryUserFeedbackWidget.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift index e4184a735c..986158422d 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift @@ -77,7 +77,7 @@ struct SentryUserFeedbackWidget { // MARK: UIAdaptivePresentationControllerDelegate - func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { setWidget(visible: true) } } From ed478f5d3dcb564b9cf7e89fc1995dffcba4fb4d Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 14 Nov 2024 12:53:42 -0900 Subject: [PATCH 11/12] docs --- .../SentryUserFeedbackThemeConfiguration.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift index 5ecead6182..e06181a46b 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift @@ -91,10 +91,14 @@ public class SentryUserFeedbackThemeConfiguration: NSObject { /** * Outline corner radius for form input elements. - * - note: Default: `5` + * - note: Default: `5`. */ public var cornerRadius: CGFloat = 5 + /** + * The thickness of the outline. + * - note: Default: `0.5`. + */ public var outlineWidth: CGFloat = 0.5 public init(outlineColor: UIColor = UIColor(white: 204 / 255, alpha: 1), cornerRadius: CGFloat = 5, outlineWidth: CGFloat = 0.5) { From f24bd668433481c37f0990a47aa3f73e230ed066 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 14 Nov 2024 14:08:19 -0900 Subject: [PATCH 12/12] more docs, theming --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 12 ++-- ...SentryUserFeedbackThemeConfiguration.swift | 60 +++++++++++++------ .../UserFeedback/SentryUserFeedbackForm.swift | 49 +++++++-------- 3 files changed, 72 insertions(+), 49 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 923d96c17f..df2b19014e 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -204,8 +204,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { uiForm.messagePlaceholder = "Describe the nature of the jank. Its essence, if you will." } config.configureTheme = { theme in - let fontSize: CGFloat = 25 - let fontFamily: String if Locale.current.languageCode == "ar" { // arabic; ar_EG fontFamily = "Damascus" @@ -218,15 +216,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } else { fontFamily = "ChalkboardSE-Regular" } - theme.font = UIFont(name: fontFamily, size: fontSize) ?? UIFont.systemFont(ofSize: fontSize) + theme.fontFamily = fontFamily theme.outlineStyle = .init(outlineColor: .purple) theme.foreground = .purple - theme.background = .purple.withAlphaComponent(0.1) + theme.background = .init(red: 0.95, green: 0.9, blue: 0.95, alpha: 1) + theme.submitBackground = .orange + theme.submitForeground = .purple + theme.buttonBackground = .purple + theme.buttonForeground = .white } config.onSubmitSuccess = { info in let name = info["name"] ?? "$shakespearean_insult_name" let alert = UIAlertController(title: "Thanks?", message: "We have enough jank of our own, we really didn't need yours too, \(name).", preferredStyle: .alert) - alert.addAction(.init(title: "Derp", style: .default)) + alert.addAction(.init(title: "Deal with it 🕶️", style: .default)) self.window?.rootViewController?.present(alert, animated: true) } config.onSubmitError = { error in diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift index e06181a46b..5607ed0118 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift @@ -9,29 +9,47 @@ import UIKit @available(iOS 13.0, *) @objcMembers public class SentryUserFeedbackThemeConfiguration: NSObject { - lazy var defaultFont = UIFont.preferredFont(forTextStyle: .callout) - - lazy var defaultTitleFont = UIFont.preferredFont(forTextStyle: .title1) + /** + * The font family to use for form text elements. + * - note: Defaults to the system default, if this property is `nil`. + */ + public lazy var fontFamily: String? = nil - lazy var defaultHeadingFont = UIFont.preferredFont(forTextStyle: .headline) + /** + * Font for form input elements. + * - note: Defaults to `UIFont.TextStyle.callout`. + */ + lazy var font = scaledFont(style: .callout) /** - * The default font to use. - * - note: Defaults to the current system default. + * Font for main header title of the feedback form. + * - note: Defaults to `UIFont.TextStyle.title1`. */ - public lazy var font = defaultFont + lazy var headerFont = scaledFont(style: .title1) - public lazy var titleFont = defaultTitleFont + /** + * Font for titles of text fields and buttons in the form. + * - note: Defaults to `UIFont.TextStyle.headline`. + */ + lazy var titleFont = scaledFont(style: .headline) - public lazy var headingFont = defaultHeadingFont + /** + * Return a scaled font for the given style, using the configured font family. + */ + func scaledFont(style: UIFont.TextStyle) -> UIFont { + guard let fontFamily = fontFamily, let font = UIFont(name: fontFamily, size: UIFont.systemFontSize) else { + return UIFont.preferredFont(forTextStyle: style) + } + return UIFontMetrics(forTextStyle: style).scaledFont(for: font) + } + /** + * Helps respond to dynamic font size changes when the app is in the background, and then comes back to the foreground. + */ func updateDefaultFonts() { - defaultFont = UIFont.preferredFont(forTextStyle: .callout) - defaultTitleFont = UIFont.preferredFont(forTextStyle: .title1) - defaultHeadingFont = UIFont.preferredFont(forTextStyle: .headline) - font = defaultFont - titleFont = defaultTitleFont - headingFont = defaultHeadingFont + font = scaledFont(style: .callout) + headerFont = scaledFont(style: .title1) + titleFont = scaledFont(style: .headline) } /** @@ -108,13 +126,19 @@ public class SentryUserFeedbackThemeConfiguration: NSObject { } } - // We need to keep a reference to a default instance of this for comparison purposes later. We don't use the default to give UITextFields a default style, instead, we use `UITextField.BorderStyle.roundedRect` if `SentryUserFeedbackThemeConfiguration.outlineStyle == defaultOutlineStyle`. + /** + * - note: We need to keep a reference to a default instance of this for comparison purposes later. We don't use the default to give UITextFields a default style, instead, we use `UITextField.BorderStyle.roundedRect` if `SentryUserFeedbackThemeConfiguration.outlineStyle == defaultOutlineStyle`. + */ let defaultOutlineStyle = OutlineStyle() - // Options for styling the outline of input elements and buttons in the feedback form. + /** + * Options for styling the outline of input elements and buttons in the feedback form. + */ public lazy var outlineStyle: OutlineStyle = defaultOutlineStyle - // The background color to use for text inputs in the feedback form. + /** + * Background color to use for text inputs in the feedback form. + */ public var inputBackground: UIColor = UIColor.secondarySystemBackground } diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index 5569dbdbea..a2c1f10c5f 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -16,33 +16,16 @@ class SentryUserFeedbackForm: UIViewController { weak var delegate: (any SentryUserFeedbackFormDelegate)? override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - if config.theme.font == config.theme.defaultFont { - config.theme.updateDefaultFonts() - config.recalculateScaleFactors() - } - + config.theme.updateDefaultFonts() + config.recalculateScaleFactors() updateLayout() } - let formElementHeight: CGFloat = 40 - let logoWidth: CGFloat = 47 - lazy var messageTextViewHeightConstraint = messageTextView.heightAnchor.constraint(equalToConstant: config.theme.font.lineHeight * 5) - lazy var logoViewWidthConstraint = sentryLogoView.widthAnchor.constraint(equalToConstant: logoWidth * config.scaleFactor) - lazy var messagePlaceholderLeadingConstraint = messageTextViewPlaceholder.leadingAnchor.constraint(equalTo: messageTextView.leadingAnchor, constant: messageTextView.textContainerInset.left + 5) - lazy var messagePlaceholderTopConstraint = messageTextViewPlaceholder.topAnchor.constraint(equalTo: messageTextView.topAnchor, constant: messageTextView.textContainerInset.top) - lazy var fullNameTextFieldHeightConstraint = fullNameTextField.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) - lazy var emailTextFieldHeightConstraint = emailTextField.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) - lazy var addScreenshotButtonHeightConstraint = addScreenshotButton.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) - lazy var removeScreenshotButtonHeightConstraint = removeScreenshotButton.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) - lazy var submitButtonHeightConstraint = submitButton.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) - lazy var cancelButtonHeightConstraint = cancelButton.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) - init(config: SentryUserFeedbackConfiguration, delegate: any SentryUserFeedbackFormDelegate) { self.config = config self.delegate = delegate super.init(nibName: nil, bundle: nil) - view.backgroundColor = .systemBackground - + view.backgroundColor = config.theme.background NSLayoutConstraint.activate([ scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: config.margin), @@ -90,12 +73,12 @@ class SentryUserFeedbackForm: UIViewController { } [fullNameLabel, emailLabel, messageLabel].forEach { - $0.font = config.theme.headingFont + $0.font = config.theme.titleFont $0.adjustsFontForContentSizeCategory = true } [submitButton, addScreenshotButton, removeScreenshotButton, cancelButton].forEach { - $0.titleLabel?.font = config.theme.headingFont + $0.titleLabel?.font = config.theme.titleFont $0.titleLabel?.adjustsFontForContentSizeCategory = true } @@ -134,7 +117,20 @@ class SentryUserFeedbackForm: UIViewController { delegate?.cancelled() } - // MARK: UI + // MARK: Layout + + let formElementHeight: CGFloat = 40 + let logoWidth: CGFloat = 47 + lazy var messageTextViewHeightConstraint = messageTextView.heightAnchor.constraint(equalToConstant: config.theme.font.lineHeight * 5) + lazy var logoViewWidthConstraint = sentryLogoView.widthAnchor.constraint(equalToConstant: logoWidth * config.scaleFactor) + lazy var messagePlaceholderLeadingConstraint = messageTextViewPlaceholder.leadingAnchor.constraint(equalTo: messageTextView.leadingAnchor, constant: messageTextView.textContainerInset.left + 5) + lazy var messagePlaceholderTopConstraint = messageTextViewPlaceholder.topAnchor.constraint(equalTo: messageTextView.topAnchor, constant: messageTextView.textContainerInset.top) + lazy var fullNameTextFieldHeightConstraint = fullNameTextField.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) + lazy var emailTextFieldHeightConstraint = emailTextField.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) + lazy var addScreenshotButtonHeightConstraint = addScreenshotButton.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) + lazy var removeScreenshotButtonHeightConstraint = removeScreenshotButton.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) + lazy var submitButtonHeightConstraint = submitButton.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) + lazy var cancelButtonHeightConstraint = cancelButton.heightAnchor.constraint(equalToConstant: formElementHeight * config.scaleFactor) func updateLayout() { let verticalPadding: CGFloat = 8 @@ -150,14 +146,14 @@ class SentryUserFeedbackForm: UIViewController { removeScreenshotButtonHeightConstraint.constant = formElementHeight * config.scaleFactor submitButtonHeightConstraint.constant = formElementHeight * config.scaleFactor cancelButtonHeightConstraint.constant = formElementHeight * config.scaleFactor - - } + // MARK: UI Elements + lazy var formTitleLabel = { let label = UILabel(frame: .zero) label.text = config.formConfig.formTitle - label.font = config.theme.titleFont + label.font = config.theme.headerFont label.setContentCompressionResistancePriority(.required, for: .horizontal) label.adjustsFontForContentSizeCategory = true return label @@ -321,6 +317,7 @@ class SentryUserFeedbackForm: UIViewController { }() } +// MARK: UITextViewDelegate @available(iOS 13.0, *) extension SentryUserFeedbackForm: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) {