Add error reporter to Client.Error toast on long-press

This commit is contained in:
Shadowfacts 2022-03-29 22:21:33 -04:00
parent 727615a818
commit 2444783edf
13 changed files with 286 additions and 163 deletions

View File

@ -68,29 +68,32 @@ public class Client {
@discardableResult @discardableResult
public func run<Result>(_ request: Request<Result>, completion: @escaping Callback<Result>) -> URLSessionTask? { public func run<Result>(_ request: Request<Result>, completion: @escaping Callback<Result>) -> URLSessionTask? {
guard let request = createURLRequest(request: request) else { guard let urlRequest = createURLRequest(request: request) else {
completion(.failure(Error.invalidRequest)) completion(.failure(Error(request: request, type: .invalidRequest)))
return nil return nil
} }
let task = session.dataTask(with: request) { data, response, error in let task = session.dataTask(with: urlRequest) { data, response, error in
if let error = error { if let error = error {
completion(.failure(.networkError(error))) completion(.failure(Error(request: request, type: .networkError(error))))
return return
} }
guard let data = data, guard let data = data,
let response = response as? HTTPURLResponse else { let response = response as? HTTPURLResponse else {
completion(.failure(.invalidResponse)) completion(.failure(Error(request: request, type: .invalidResponse)))
return return
} }
guard response.statusCode == 200 else { guard response.statusCode == 200 else {
let mastodonError = try? Client.decoder.decode(MastodonError.self, from: data) let mastodonError = try? Client.decoder.decode(MastodonError.self, from: data)
let error: Error = mastodonError.flatMap { .mastodonError($0.description) } ?? .unexpectedStatus(response.statusCode) let type: ErrorType = mastodonError.flatMap { .mastodonError($0.description) } ?? .unexpectedStatus(response.statusCode)
completion(.failure(error)) completion(.failure(Error(request: request, type: type)))
return return
} }
guard let result = try? Client.decoder.decode(Result.self, from: data) else { let result: Result
completion(.failure(.invalidModel)) do {
result = try Client.decoder.decode(Result.self, from: data)
} catch {
completion(.failure(Error(request: request, type: .invalidModel(error))))
return return
} }
let pagination = response.allHeaderFields["Link"].flatMap { $0 as? String }.flatMap(Pagination.init) let pagination = response.allHeaderFields["Link"].flatMap { $0 as? String }.flatMap(Pagination.init)
@ -392,16 +395,19 @@ public class Client {
} }
extension Client { extension Client {
public enum Error: LocalizedError { public struct Error: LocalizedError {
case networkError(Swift.Error) public let requestMethod: Method
case unexpectedStatus(Int) public let requestPath: String
case invalidRequest public let type: ErrorType
case invalidResponse
case invalidModel init<ResultType: Decodable>(request: Request<ResultType>, type: ErrorType) {
case mastodonError(String) self.requestMethod = request.method
self.requestPath = request.path
self.type = type
}
public var localizedDescription: String { public var localizedDescription: String {
switch self { switch type {
case .networkError(let error): case .networkError(let error):
return "Network Error: \(error.localizedDescription)" return "Network Error: \(error.localizedDescription)"
// todo: support more status codes // todo: support more status codes
@ -413,11 +419,19 @@ extension Client {
return "Invalid Request" return "Invalid Request"
case .invalidResponse: case .invalidResponse:
return "Invalid Response" return "Invalid Response"
case .invalidModel: case .invalidModel(_):
return "Invalid Model" return "Invalid Model"
case .mastodonError(let error): case .mastodonError(let error):
return "Server Error: \(error)" return "Server Error: \(error)"
} }
} }
} }
public enum ErrorType: LocalizedError {
case networkError(Swift.Error)
case unexpectedStatus(Int)
case invalidRequest
case invalidResponse
case invalidModel(Swift.Error)
case mastodonError(String)
}
} }

View File

@ -8,12 +8,12 @@
import Foundation import Foundation
enum Method { public enum Method {
case get, post, put, patch, delete case get, post, put, patch, delete
} }
extension Method { extension Method {
var name: String { public var name: String {
switch self { switch self {
case .get: case .get:
return "GET" return "GET"

View File

@ -12,7 +12,7 @@ public class InstanceSelector {
private static let decoder = JSONDecoder() private static let decoder = JSONDecoder()
public static func getInstances(category: String?, completion: @escaping Client.Callback<[Instance]>) { public static func getInstances(category: String?, completion: @escaping (Result<[Instance], Client.ErrorType>) -> Void) {
let url: URL let url: URL
if let category = category { if let category = category {
url = URL(string: "https://api.joinmastodon.org/servers?category=\(category)")! url = URL(string: "https://api.joinmastodon.org/servers?category=\(category)")!
@ -34,11 +34,14 @@ public class InstanceSelector {
completion(.failure(.unexpectedStatus(response.statusCode))) completion(.failure(.unexpectedStatus(response.statusCode)))
return return
} }
guard let result = try? decoder.decode([Instance].self, from: data) else { let result: [Instance]
completion(.failure(Client.Error.invalidModel)) do {
result = try decoder.decode([Instance].self, from: data)
} catch {
completion(.failure(.invalidModel(error)))
return return
} }
completion(.success(result, nil)) completion(.success(result))
} }
task.resume() task.resume()
} }

View File

@ -69,6 +69,7 @@
D6109A0D214599E100432DC2 /* RequestRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A0C214599E100432DC2 /* RequestRange.swift */; }; D6109A0D214599E100432DC2 /* RequestRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A0C214599E100432DC2 /* RequestRange.swift */; };
D6109A0F21459B6900432DC2 /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A0E21459B6900432DC2 /* Pagination.swift */; }; D6109A0F21459B6900432DC2 /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A0E21459B6900432DC2 /* Pagination.swift */; };
D6109A11214607D500432DC2 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A10214607D500432DC2 /* Timeline.swift */; }; D6109A11214607D500432DC2 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A10214607D500432DC2 /* Timeline.swift */; };
D6114E0927F3EA3D0080E273 /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E0827F3EA3D0080E273 /* CrashReporterViewController.swift */; };
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */; }; D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */; };
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; }; D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; };
D61AC1D3232E928600C54D2D /* InstanceSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D2232E928600C54D2D /* InstanceSelector.swift */; }; D61AC1D3232E928600C54D2D /* InstanceSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D2232E928600C54D2D /* InstanceSelector.swift */; };
@ -340,8 +341,8 @@
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; }; D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; };
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; }; D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
D6F1F9DF27B0613300CB7D88 /* WebURL in Frameworks */ = {isa = PBXBuildFile; productRef = D6F1F9DE27B0613300CB7D88 /* WebURL */; settings = {ATTRIBUTES = (Required, ); }; }; D6F1F9DF27B0613300CB7D88 /* WebURL in Frameworks */ = {isa = PBXBuildFile; productRef = D6F1F9DE27B0613300CB7D88 /* WebURL */; settings = {ATTRIBUTES = (Required, ); }; };
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */; }; D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */; };
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; }; D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */; };
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; }; D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */; }; D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -480,6 +481,7 @@
D6109A0C214599E100432DC2 /* RequestRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestRange.swift; sourceTree = "<group>"; }; D6109A0C214599E100432DC2 /* RequestRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestRange.swift; sourceTree = "<group>"; };
D6109A0E21459B6900432DC2 /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = "<group>"; }; D6109A0E21459B6900432DC2 /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = "<group>"; };
D6109A10214607D500432DC2 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = "<group>"; }; D6109A10214607D500432DC2 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = "<group>"; };
D6114E0827F3EA3D0080E273 /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = "<group>"; };
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; }; D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; };
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; }; D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
D61AC1D2232E928600C54D2D /* InstanceSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelector.swift; sourceTree = "<group>"; }; D61AC1D2232E928600C54D2D /* InstanceSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelector.swift; sourceTree = "<group>"; };
@ -757,8 +759,8 @@
D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCollapseButton.swift; sourceTree = "<group>"; }; D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCollapseButton.swift; sourceTree = "<group>"; };
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = "<group>"; }; D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = "<group>"; };
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = "<group>"; }; D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = "<group>"; };
D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = "<group>"; }; D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReporterViewController.swift; sourceTree = "<group>"; };
D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CrashReporterViewController.xib; sourceTree = "<group>"; }; D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IssueReporterViewController.xib; sourceTree = "<group>"; };
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; }; D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = "<group>"; }; D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -1681,8 +1683,9 @@
D6F2E960249E772F005846BB /* Crash Reporter */ = { D6F2E960249E772F005846BB /* Crash Reporter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */, D6114E0827F3EA3D0080E273 /* CrashReporterViewController.swift */,
D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */, D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */,
D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */,
); );
path = "Crash Reporter"; path = "Crash Reporter";
sourceTree = "<group>"; sourceTree = "<group>";
@ -1950,7 +1953,7 @@
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */, D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */,
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */, D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */, D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */, D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */,
D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */, D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */,
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */, D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
D6DEA0DF268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib in Resources */, D6DEA0DF268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib in Resources */,
@ -2144,7 +2147,7 @@
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */, D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
D620483623D38075008A63EF /* ContentTextView.swift in Sources */, D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */, D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */,
D6A57408255C53EC00674551 /* ComposeTextViewCaretScrolling.swift in Sources */, D6A57408255C53EC00674551 /* ComposeTextViewCaretScrolling.swift in Sources */,
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */, D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */,
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */, D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
@ -2318,6 +2321,7 @@
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */, D677284E24ECC01D00C732D3 /* Draft.swift in Sources */,
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */, D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */, D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */,
D6114E0927F3EA3D0080E273 /* CrashReporterViewController.swift in Sources */,
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */, D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */,
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */, D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */, D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,

View File

@ -127,7 +127,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
return return
} }
window!.rootViewController = CrashReporterViewController.create(report: report) window!.rootViewController = CrashReporterViewController.create(report: report, delegate: self)
#endif #endif
} }
@ -208,10 +208,8 @@ extension MainSceneDelegate: OnboardingViewControllerDelegate {
} }
} }
extension MainSceneDelegate: MFMailComposeViewControllerDelegate { extension MainSceneDelegate: IssueReporterViewControllerDelegate {
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { func didDismissReporter() {
controller.dismiss(animated: true) { showAppOrOnboardingUI()
self.showAppOrOnboardingUI()
}
} }
} }

View File

@ -157,7 +157,7 @@ class ConversationTableViewController: EnhancedTableViewController {
DispatchQueue.main.async { DispatchQueue.main.async {
self.loadingState = .unloaded self.loadingState = .unloaded
let config = ToastConfiguration(from: error, with: "Error Loading Status") { [weak self] (toast) in let config = ToastConfiguration(from: error, with: "Error Loading Status", in: self) { [weak self] (toast) in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
self?.loadMainStatus() self?.loadMainStatus()
} }
@ -210,7 +210,7 @@ class ConversationTableViewController: EnhancedTableViewController {
DispatchQueue.main.async { DispatchQueue.main.async {
self.loadingState = .loadedMain self.loadingState = .loadedMain
let config = ToastConfiguration(from: error, with: "Error Loading Content") { [weak self] (toast) in let config = ToastConfiguration(from: error, with: "Error Loading Content", in: self) { [weak self] (toast) in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
self?.loadContext(for: mainStatus) self?.loadContext(for: mainStatus)
} }

View File

@ -2,132 +2,46 @@
// CrashReporterViewController.swift // CrashReporterViewController.swift
// Tusker // Tusker
// //
// Created by Shadowfacts on 6/20/20. // Created by Shadowfacts on 3/29/22.
// Copyright © 2020 Shadowfacts. All rights reserved. // Copyright © 2022 Shadowfacts. All rights reserved.
// //
import UIKit import UIKit
import CrashReporter import CrashReporter
import MessageUI
class CrashReporterViewController: UIViewController {
class CrashReporterViewController: IssueReporterViewController {
private let report: PLCrashReport private let report: PLCrashReport
private var reportText: String!
private var reportFilename: String { override var preamble: String {
let timestamp = ISO8601DateFormatter().string(from: report.systemInfo.timestamp) "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."
return "Tusker-crash-\(timestamp).crash"
} }
@IBOutlet weak var crashReportTextView: UITextView! override var subject: String {
@IBOutlet weak var sendReportButton: UIButton! "Tusker Crash Report"
static func create(report: PLCrashReport) -> UINavigationController {
let nav = UINavigationController(rootViewController: CrashReporterViewController(report: report))
nav.navigationBar.prefersLargeTitles = true
return nav
} }
private init(report: PLCrashReport){ static func create(report: PLCrashReport, delegate: IssueReporterViewControllerDelegate) -> UINavigationController {
return create(CrashReporterViewController(report: report, delegate: delegate))
}
private init(report: PLCrashReport, delegate: IssueReporterViewControllerDelegate) {
self.report = report self.report = report
let reportText = PLCrashReportTextFormatter.stringValue(for: report, with: PLCrashReportTextFormatiOS)!
let timestamp = ISO8601DateFormatter().string(from: report.systemInfo.timestamp)
let reportFilename = "Tusker-crash-\(timestamp).crash"
super.init(nibName: "CrashReporterViewController", bundle: .main) super.init(reportText: reportText, reportFilename: reportFilename, delegate: delegate)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
navigationItem.title = NSLocalizedString("Crash Detected", comment: "crash reporter title") 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! MainSceneDelegate).showAppOrOnboardingUI()
}
}
extension CrashReporterViewController: MFMailComposeViewControllerDelegate {
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true) {
(self.view.window!.windowScene!.delegate as! MainSceneDelegate).showAppOrOnboardingUI()
}
}
} }

View File

@ -0,0 +1,145 @@
//
// IssueReporterViewController.swift
// Tusker
//
// Created by Shadowfacts on 6/20/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import CrashReporter
import MessageUI
protocol IssueReporterViewControllerDelegate: AnyObject {
func didDismissReporter()
}
class IssueReporterViewController: UIViewController {
static func create(_ self: IssueReporterViewController) -> UINavigationController {
let nav = UINavigationController(rootViewController: self)
nav.navigationBar.prefersLargeTitles = true
return nav
}
static func create(reportText: String, reportFilename: String? = nil, delegate: IssueReporterViewControllerDelegate) -> UINavigationController {
let filename = reportFilename ?? "Tusker-error-\(ISO8601DateFormatter().string(from: Date())).txt"
return create(IssueReporterViewController(reportText: reportText, reportFilename: filename, delegate: delegate))
}
let reportText: String
let reportFilename: String
private weak var delegate: IssueReporterViewControllerDelegate?
var preamble: String {
"Tusker has encountered an error. You can email a report to the developer. 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 that may be pertinent."
}
var subject: String {
"Tusker Error Report"
}
@IBOutlet weak var crashReportTextView: UITextView!
@IBOutlet weak var sendReportButton: UIButton!
init(reportText: String, reportFilename: String, delegate: IssueReporterViewControllerDelegate?) {
self.reportText = reportText
self.reportFilename = reportFilename
self.delegate = delegate
super.init(nibName: "IssueReporterViewController", bundle: .main)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Report an Error"
navigationItem.largeTitleDisplayMode = .always
crashReportTextView.font = .monospacedSystemFont(ofSize: 14, weight: .regular)
let attributed = NSMutableAttributedString()
attributed.append(NSAttributedString(string: preamble, attributes: [
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17),
NSAttributedString.Key.foregroundColor: UIColor.label
]))
attributed.append(NSAttributedString(string: "\n\n"))
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(subject)
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) {
delegate?.didDismissReporter()
}
}
extension IssueReporterViewController: MFMailComposeViewControllerDelegate {
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true) {
self.delegate?.didDismissReporter()
}
}
}

View File

@ -1,13 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="CrashReporterViewController" customModule="Tusker" customModuleProvider="target"> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="IssueReporterViewController" customModule="Tusker" customModuleProvider="target">
<connections> <connections>
<outlet property="crashReportTextView" destination="hxN-7J-Usc" id="TGd-yq-Ds5"/> <outlet property="crashReportTextView" destination="hxN-7J-Usc" id="TGd-yq-Ds5"/>
<outlet property="sendReportButton" destination="Ofm-5l-nAp" id="6xM-hz-uvw"/> <outlet property="sendReportButton" destination="Ofm-5l-nAp" id="6xM-hz-uvw"/>
@ -27,9 +29,9 @@
<subviews> <subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalHuggingPriority="249" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="hxN-7J-Usc"> <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"/> <rect key="frame" x="0.0" y="0.0" width="414" height="166.5"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor"/>
<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> <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"/> <color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/> <fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/> <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView> </textView>
@ -43,13 +45,13 @@
<viewLayoutGuide key="contentLayoutGuide" id="LRh-7Z-mV1"/> <viewLayoutGuide key="contentLayoutGuide" id="LRh-7Z-mV1"/>
<viewLayoutGuide key="frameLayoutGuide" id="Rgd-t7-8QN"/> <viewLayoutGuide key="frameLayoutGuide" id="Rgd-t7-8QN"/>
</scrollView> </scrollView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ofm-5l-nAp"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ofm-5l-nAp">
<rect key="frame" x="52" y="730" width="310.5" height="50"/> <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"/> <color key="backgroundColor" systemColor="systemBlueColor"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="50" id="jHf-W0-qQn"/> <constraint firstAttribute="height" constant="50" id="jHf-W0-qQn"/>
</constraints> </constraints>
<state key="normal" title="Send Crash Report"> <state key="normal" title="Send Report">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state> </state>
<connections> <connections>
@ -59,8 +61,8 @@
<action selector="sendReportTouchUpInside:" destination="-1" eventType="touchUpInside" id="ggd-fm-Orq"/> <action selector="sendReportTouchUpInside:" destination="-1" eventType="touchUpInside" id="ggd-fm-Orq"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JiJ-Ng-jOz"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JiJ-Ng-jOz">
<rect key="frame" x="169" y="788" width="76" height="30"/> <rect key="frame" x="168.5" y="788" width="77" height="30"/>
<state key="normal" title="Don't Send"/> <state key="normal" title="Don't Send"/>
<connections> <connections>
<action selector="cancelPressed:" destination="-1" eventType="touchUpInside" id="o4R-0Q-STS"/> <action selector="cancelPressed:" destination="-1" eventType="touchUpInside" id="o4R-0Q-STS"/>
@ -69,7 +71,8 @@
</subviews> </subviews>
</stackView> </stackView>
</subviews> </subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> <viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints> <constraints>
<constraint firstItem="uQy-Yw-Dba" firstAttribute="width" secondItem="i5M-Pr-FkT" secondAttribute="width" id="AX2-9e-cO0"/> <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="fnl-2z-Ty3" firstAttribute="bottom" secondItem="a8U-KI-8PM" secondAttribute="bottom" id="Ec3-Px-dSW"/>
@ -79,8 +82,18 @@
<constraint firstItem="a8U-KI-8PM" firstAttribute="trailing" secondItem="fnl-2z-Ty3" secondAttribute="trailing" id="f59-qB-5T7"/> <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"/> <constraint firstItem="Ofm-5l-nAp" firstAttribute="width" secondItem="i5M-Pr-FkT" secondAttribute="width" multiplier="0.75" id="ueo-xb-Tfm"/>
</constraints> </constraints>
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
<point key="canvasLocation" x="133" y="154"/> <point key="canvasLocation" x="133" y="154"/>
</view> </view>
</objects> </objects>
<resources>
<systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemBlueColor">
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document> </document>

View File

@ -167,7 +167,7 @@ class InstanceSelectorTableViewController: UITableViewController {
switch response { switch response {
case let .failure(error): case let .failure(error):
self.showRecommendationsError(error) self.showRecommendationsError(error)
case let .success(instances, _): case let .success(instances):
self.recommendedInstances = instances self.recommendedInstances = instances
self.filterRecommendedResults() self.filterRecommendedResults()
} }
@ -197,7 +197,7 @@ class InstanceSelectorTableViewController: UITableViewController {
tableView.tableHeaderView = header tableView.tableHeaderView = header
} }
private func showRecommendationsError(_ error: Client.Error) { private func showRecommendationsError(_ error: Client.ErrorType) {
let footer = UITableViewHeaderFooterView() let footer = UITableViewHeaderFooterView()
footer.translatesAutoresizingMaskIntoConstraints = false footer.translatesAutoresizingMaskIntoConstraints = false

View File

@ -116,7 +116,7 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
case let .failure(.client(error)): case let .failure(.client(error)):
self.state = .unloaded self.state = .unloaded
let config = ToastConfiguration(from: error, with: "Error Loading") { [weak self] (toast) in let config = ToastConfiguration(from: error, with: "Error Loading", in: self) { [weak self] (toast) in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
self?.loadInitial() self?.loadInitial()
} }
@ -148,7 +148,7 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
self.dataSource.apply(snapshot, animatingDifferences: false) self.dataSource.apply(snapshot, animatingDifferences: false)
case let .failure(.client(error)): case let .failure(.client(error)):
let config = ToastConfiguration(from: error, with: "Error Loading Older") { [weak self] (toast) in let config = ToastConfiguration(from: error, with: "Error Loading Older", in: self) { [weak self] (toast) in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
self?.loadOlder() self?.loadOlder()
} }
@ -236,7 +236,7 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
} }
case let .failure(.client(error)): case let .failure(.client(error)):
let config = ToastConfiguration(from: error, with: "Error Loading Newer") { [weak self] (toast) in let config = ToastConfiguration(from: error, with: "Error Loading Newer", in: self) { [weak self] (toast) in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
self?.refresh() self?.refresh()
} }

View File

@ -16,6 +16,7 @@ struct ToastConfiguration {
var subtitle: String? var subtitle: String?
var actionTitle: String? var actionTitle: String?
var action: ((ToastView) -> Void)? var action: ((ToastView) -> Void)?
var longPressAction: ((ToastView) -> Void)?
var edgeSpacing: CGFloat = 8 var edgeSpacing: CGFloat = 8
var edge: Edge = .automatic var edge: Edge = .automatic
var dismissOnScroll = true var dismissOnScroll = true
@ -34,18 +35,29 @@ struct ToastConfiguration {
} }
extension ToastConfiguration { extension ToastConfiguration {
init(from error: Client.Error, with title: String, retryAction: @escaping (ToastView) -> Void) { init(from error: Client.Error, with title: String, in viewController: UIViewController, retryAction: @escaping (ToastView) -> Void) {
self.init(title: title) self.init(title: title)
self.subtitle = error.localizedDescription self.subtitle = error.localizedDescription
self.systemImageName = error.systemImageName self.systemImageName = error.systemImageName
self.actionTitle = "Retry" self.actionTitle = "Retry"
self.action = retryAction self.action = retryAction
self.longPressAction = { [unowned viewController] toast in
toast.dismissToast(animated: true)
let text = """
\(title):
\(error.requestMethod.name) \(error.requestPath)
\(error.type)
"""
let reporter = IssueReporterViewController.create(reportText: text, delegate: viewController)
viewController.present(reporter, animated: true)
}
} }
} }
fileprivate extension Client.Error { fileprivate extension Client.Error {
var systemImageName: String { var systemImageName: String {
switch self { switch type {
case .networkError(_): case .networkError(_):
return "wifi.exclamationmark" return "wifi.exclamationmark"
default: default:
@ -53,3 +65,10 @@ fileprivate extension Client.Error {
} }
} }
} }
// todo: i don't like that this protocol conformance is accessible outside of this file
extension UIViewController: IssueReporterViewControllerDelegate {
func didDismissReporter() {
self.dismiss(animated: true)
}
}

View File

@ -14,6 +14,7 @@ class ToastView: UIView {
private var shrinkAnimator: UIViewPropertyAnimator? private var shrinkAnimator: UIViewPropertyAnimator?
private var recognizedGesture = false private var recognizedGesture = false
private var handledLongPress = false
private var shouldDismissOnScroll = false private var shouldDismissOnScroll = false
private(set) var shouldDismissAutomatically = true private(set) var shouldDismissAutomatically = true
@ -102,6 +103,8 @@ class ToastView: UIView {
let pan = UIPanGestureRecognizer(target: self, action: #selector(panRecognized)) let pan = UIPanGestureRecognizer(target: self, action: #selector(panRecognized))
addGestureRecognizer(pan) addGestureRecognizer(pan)
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPressRecognized))
addGestureRecognizer(longPress)
} }
override func layoutSubviews() { override func layoutSubviews() {
@ -154,6 +157,8 @@ class ToastView: UIView {
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event) super.touchesEnded(touches, with: event)
handledLongPress = false
if !recognizedGesture { if !recognizedGesture {
guard let shrinkAnimator = shrinkAnimator else { guard let shrinkAnimator = shrinkAnimator else {
return return
@ -252,4 +257,12 @@ class ToastView: UIView {
} }
} }
@objc private func longPressRecognized() {
guard !handledLongPress else {
return
}
configuration.longPressAction?(self)
handledLongPress = true
}
} }