Add crash report helper

This commit is contained in:
Shadowfacts 2020-06-20 23:11:35 -04:00
parent 393a134648
commit 6adcad63b3
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
6 changed files with 321 additions and 15 deletions

View File

@ -185,6 +185,7 @@
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */; };
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9F240C8384002843CE /* EmojiLabel.swift */; };
D6969EA4240DD28D002843CE /* UnknownNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */; };
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = D69CCBBE249E6EFD000AF167 /* CrashReporter */; };
D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */; };
D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */; };
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */; };
@ -248,6 +249,8 @@
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; };
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */; };
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; };
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */; };
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
/* End PBXBuildFile section */
@ -554,6 +557,8 @@
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = "<group>"; };
D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CrashReporterViewController.xib; sourceTree = "<group>"; };
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
D6F98BD523AE951F008A4DAC /* Swifter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Swifter.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -582,6 +587,7 @@
D61099C02144B0CC00432DC2 /* Pachyderm.framework in Frameworks */,
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */,
D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */,
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */,
0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */,
);
@ -851,6 +857,7 @@
isa = PBXGroup;
children = (
D6C693FA2162FE5D007D6A6D /* Utilities */,
D6F2E960249E772F005846BB /* Crash Reporter */,
D641C782213DD7F0004B4513 /* Main */,
D641C783213DD7FE004B4513 /* Onboarding */,
D641C781213DD7DD004B4513 /* Timeline */,
@ -1333,6 +1340,15 @@
path = Caching;
sourceTree = "<group>";
};
D6F2E960249E772F005846BB /* Crash Reporter */ = {
isa = PBXGroup;
children = (
D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */,
D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */,
);
path = "Crash Reporter";
sourceTree = "<group>";
};
D6F953F121251A2F00CF0F2B /* Controllers */ = {
isa = PBXGroup;
children = (
@ -1410,6 +1426,7 @@
name = Tusker;
packageProductDependencies = (
D6B0539E23BD2BA300A066FA /* SheetController */,
D69CCBBE249E6EFD000AF167 /* CrashReporter */,
);
productName = Tusker;
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
@ -1505,6 +1522,7 @@
mainGroup = D6D4DDC3212518A000E1C4BB;
packageReferences = (
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */,
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */,
);
productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */;
projectDirPath = "";
@ -1563,6 +1581,7 @@
D63F9C66241C4CC3004C03CF /* AddAttachmentTableViewCell.xib in Resources */,
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */,
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1705,6 +1724,7 @@
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */,
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
@ -2298,6 +2318,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/microsoft/plcrashreporter";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 1.7.0;
};
};
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://git.shadowfacts.net/shadowfacts/SheetController.git";
@ -2309,6 +2337,11 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
D69CCBBE249E6EFD000AF167 /* CrashReporter */ = {
isa = XCSwiftPackageProductDependency;
package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */;
productName = CrashReporter;
};
D6B0539E23BD2BA300A066FA /* SheetController */ = {
isa = XCSwiftPackageProductDependency;
package = D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */;

View File

@ -2,12 +2,12 @@
"object": {
"pins": [
{
"package": "SheetController",
"repositoryURL": "https://git.shadowfacts.net/shadowfacts/SheetController.git",
"package": "PLCrashReporter",
"repositoryURL": "https://github.com/microsoft/plcrashreporter",
"state": {
"branch": "master",
"revision": "6926446c4e15eb7f4513c4c00df9279553b330be",
"version": null
"branch": null,
"revision": "4637a7854de2cc5c354d46fb931d74bdbc2c043e",
"version": "1.7.0"
}
}
]

View File

@ -7,13 +7,35 @@
//
import UIKit
import CrashReporter
import MessageUI
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
static private(set) var crashReporter: PLCrashReporter!
static var pendingCrashReport: PLCrashReport?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
#if !DEBUG
setupCrashReporter()
#endif
AppShortcutItem.createItems(for: application)
return true
}
private func setupCrashReporter() {
let config = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: .all)
AppDelegate.crashReporter = PLCrashReporter(configuration: config)
if AppDelegate.crashReporter.hasPendingCrashReport() {
let data = try! AppDelegate.crashReporter.loadPendingCrashReportDataAndReturnError()
AppDelegate.crashReporter.purgePendingCrashReport()
let report = try! PLCrashReport(data: data)
AppDelegate.pendingCrashReport = report
}
AppDelegate.crashReporter.enable()
}
}

View File

@ -8,6 +8,8 @@
import UIKit
import Pachyderm
import CrashReporter
import MessageUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@ -21,17 +23,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
window = UIWindow(windowScene: windowScene)
if LocalData.shared.onboardingComplete {
if session.mastodonController == nil {
let account = LocalData.shared.getMostRecentAccount()!
session.mastodonController = MastodonController.getForAccount(account)
}
showAppUI()
if let report = AppDelegate.pendingCrashReport {
AppDelegate.pendingCrashReport = nil
handlePendingCrashReport(report, session: session)
} else {
showOnboardingUI()
showAppOrOnboardingUI(session: session)
}
window!.makeKeyAndVisible()
NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil)
@ -113,6 +111,32 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
try! scene.session.mastodonController?.persistentContainer.viewContext.save()
}
private func handlePendingCrashReport(_ report: PLCrashReport, session: UISceneSession) {
#if !DEBUG
guard MFMailComposeViewController.canSendMail() else {
print("Cannot send email")
showAppOrOnboardingUI(session: session)
return
}
window!.rootViewController = CrashReporterViewController.create(report: report)
#endif
}
func showAppOrOnboardingUI(session: UISceneSession? = nil) {
let session = session ?? window!.windowScene!.session
if LocalData.shared.onboardingComplete {
if session.mastodonController == nil {
let account = LocalData.shared.getMostRecentAccount()!
session.mastodonController = MastodonController.getForAccount(account)
}
showAppUI()
} else {
showOnboardingUI()
}
}
func activateAccount(_ account: LocalData.UserAccountInfo) {
LocalData.shared.setMostRecentAccount(account)
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
@ -154,3 +178,11 @@ extension SceneDelegate: OnboardingViewControllerDelegate {
activateAccount(account)
}
}
extension SceneDelegate: MFMailComposeViewControllerDelegate {
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true) {
self.showAppOrOnboardingUI()
}
}
}

View File

@ -0,0 +1,133 @@
//
// CrashReporterViewController.swift
// Tusker
//
// Created by Shadowfacts on 6/20/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import CrashReporter
import MessageUI
class CrashReporterViewController: UIViewController {
private let report: PLCrashReport
private var reportText: String!
private var reportFilename: String {
let timestamp = ISO8601DateFormatter().string(from: report.systemInfo.timestamp)
return "Tusker-crash-\(timestamp).crash"
}
@IBOutlet weak var crashReportTextView: UITextView!
@IBOutlet weak var sendReportButton: UIButton!
static func create(report: PLCrashReport) -> UINavigationController {
let nav = UINavigationController(rootViewController: CrashReporterViewController(report: report))
nav.navigationBar.prefersLargeTitles = true
return nav
}
private init(report: PLCrashReport){
self.report = report
super.init(nibName: "CrashReporterViewController", bundle: .main)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = NSLocalizedString("Crash Detected", comment: "crash reporter title")
navigationItem.largeTitleDisplayMode = .always
crashReportTextView.font = .monospacedSystemFont(ofSize: 14, weight: .regular)
reportText = PLCrashReportTextFormatter.stringValue(for: report, with: PLCrashReportTextFormatiOS)!
let info = "Tusker has detected that it crashed the last time it was running. You can email the report to the developer or skip sending and continue to the app. You may review the report below before sending.\n\nIf you choose to send the report, please include any additional details about what you were doing prior to the crash that may be pertinent.\n\n"
let attributed = NSMutableAttributedString()
attributed.append(NSAttributedString(string: info, attributes: [
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17),
NSAttributedString.Key.foregroundColor: UIColor.label
]))
attributed.append(NSAttributedString(string: reportText, attributes: [
NSAttributedString.Key.font: UIFont.monospacedSystemFont(ofSize: 14, weight: .regular),
NSAttributedString.Key.foregroundColor: UIColor.label
]))
crashReportTextView.attributedText = attributed
sendReportButton.layer.cornerRadius = 12.5
sendReportButton.layer.masksToBounds = true
sendReportButton.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(sendReportButtonLongPressed)))
}
private func updateSendReportButtonColor(lightened: Bool, animate: Bool) {
let color: UIColor
if lightened {
var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0
UIColor.systemBlue.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
color = UIColor(hue: hue, saturation: 0.85 * saturation, brightness: brightness, alpha: alpha)
} else {
color = .systemBlue
}
if animate {
UIView.animate(withDuration: 0.25) {
self.sendReportButton.backgroundColor = color
}
} else {
sendReportButton.backgroundColor = color
}
}
@IBAction func sendReportTouchDown(_ sender: Any) {
updateSendReportButtonColor(lightened: true, animate: false)
}
@IBAction func sendReportButtonTouchDragExit(_ sender: Any) {
updateSendReportButtonColor(lightened: false, animate: true)
}
@IBAction func sendReportButtonTouchDragEnter(_ sender: Any) {
updateSendReportButtonColor(lightened: true, animate: true)
}
@IBAction func sendReportTouchUpInside(_ sender: Any) {
updateSendReportButtonColor(lightened: false, animate: true)
let composeVC = MFMailComposeViewController()
composeVC.mailComposeDelegate = self
composeVC.setToRecipients(["me@shadowfacts.net"])
composeVC.setSubject("Tusker Crash Report")
let data = reportText.data(using: .utf8)!
composeVC.addAttachmentData(data, mimeType: "text/plain", fileName: reportFilename)
self.present(composeVC, animated: true)
}
@objc func sendReportButtonLongPressed() {
let dir = FileManager.default.temporaryDirectory
let url = dir.appendingPathComponent(reportFilename)
try! reportText.data(using: .utf8)!.write(to: url)
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
present(activityController, animated: true)
}
@IBAction func cancelPressed(_ sender: Any) {
(view.window!.windowScene!.delegate as! SceneDelegate).showAppOrOnboardingUI()
}
}
extension CrashReporterViewController: MFMailComposeViewControllerDelegate {
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true) {
(self.view.window!.windowScene!.delegate as! SceneDelegate).showAppOrOnboardingUI()
}
}
}

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="CrashReporterViewController" customModule="Tusker" customModuleProvider="target">
<connections>
<outlet property="crashReportTextView" destination="hxN-7J-Usc" id="TGd-yq-Ds5"/>
<outlet property="sendReportButton" destination="Ofm-5l-nAp" id="6xM-hz-uvw"/>
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="a8U-KI-8PM">
<rect key="frame" x="0.0" y="44" width="414" height="818"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="uQy-Yw-Dba">
<rect key="frame" x="0.0" y="0.0" width="414" height="722"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalHuggingPriority="249" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="hxN-7J-Usc">
<rect key="frame" x="0.0" y="0.0" width="414" height="166.5"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<constraints>
<constraint firstItem="LRh-7Z-mV1" firstAttribute="trailing" secondItem="hxN-7J-Usc" secondAttribute="trailing" id="1GJ-Nk-pIj"/>
<constraint firstItem="hxN-7J-Usc" firstAttribute="leading" secondItem="LRh-7Z-mV1" secondAttribute="leading" id="dtj-bw-No1"/>
<constraint firstItem="hxN-7J-Usc" firstAttribute="top" secondItem="LRh-7Z-mV1" secondAttribute="top" id="nvk-aw-YqH"/>
<constraint firstItem="LRh-7Z-mV1" firstAttribute="bottom" secondItem="hxN-7J-Usc" secondAttribute="bottom" id="xAU-OS-1Zy"/>
</constraints>
<viewLayoutGuide key="contentLayoutGuide" id="LRh-7Z-mV1"/>
<viewLayoutGuide key="frameLayoutGuide" id="Rgd-t7-8QN"/>
</scrollView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ofm-5l-nAp">
<rect key="frame" x="52" y="730" width="310.5" height="50"/>
<color key="backgroundColor" systemColor="systemBlueColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="jHf-W0-qQn"/>
</constraints>
<state key="normal" title="Send Crash Report">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="sendReportButtonTouchDragEnter:" destination="-1" eventType="touchDragEnter" id="jSF-RZ-NsG"/>
<action selector="sendReportButtonTouchDragExit:" destination="-1" eventType="touchDragExit" id="CrB-QL-bN1"/>
<action selector="sendReportTouchDown:" destination="-1" eventType="touchDown" id="P5K-4n-tO1"/>
<action selector="sendReportTouchUpInside:" destination="-1" eventType="touchUpInside" id="ggd-fm-Orq"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JiJ-Ng-jOz">
<rect key="frame" x="169" y="788" width="76" height="30"/>
<state key="normal" title="Don't Send"/>
<connections>
<action selector="cancelPressed:" destination="-1" eventType="touchUpInside" id="o4R-0Q-STS"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="uQy-Yw-Dba" firstAttribute="width" secondItem="i5M-Pr-FkT" secondAttribute="width" id="AX2-9e-cO0"/>
<constraint firstItem="fnl-2z-Ty3" firstAttribute="bottom" secondItem="a8U-KI-8PM" secondAttribute="bottom" id="Ec3-Px-dSW"/>
<constraint firstItem="hxN-7J-Usc" firstAttribute="width" secondItem="i5M-Pr-FkT" secondAttribute="width" id="Evf-Yz-Iui"/>
<constraint firstItem="a8U-KI-8PM" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="NDN-Jl-viT"/>
<constraint firstItem="a8U-KI-8PM" firstAttribute="top" secondItem="fnl-2z-Ty3" secondAttribute="top" id="O8Y-sl-55I"/>
<constraint firstItem="a8U-KI-8PM" firstAttribute="trailing" secondItem="fnl-2z-Ty3" secondAttribute="trailing" id="f59-qB-5T7"/>
<constraint firstItem="Ofm-5l-nAp" firstAttribute="width" secondItem="i5M-Pr-FkT" secondAttribute="width" multiplier="0.75" id="ueo-xb-Tfm"/>
</constraints>
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
<point key="canvasLocation" x="133" y="154"/>
</view>
</objects>
</document>