From cf5b97d9c8c40ae05d6edd650b79b330002106ed Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 23 Jun 2020 19:27:34 -0400 Subject: [PATCH 01/13] Fix crash showing custom instance on iOS 14 --- .../Onboarding/InstanceSelectorTableViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift index b77e8bc9d1..3dbb8993a6 100644 --- a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift +++ b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift @@ -118,7 +118,9 @@ class InstanceSelectorTableViewController: UITableViewController { let request = Client.getInstance() client.run(request) { (response) in var snapshot = self.dataSource.snapshot() - snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .selected)) + if snapshot.indexOfSection(.selected) != nil { + snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .selected)) + } if case let .success(instance, _) = response { if !snapshot.sectionIdentifiers.contains(.selected) { From 986fc5b833350b79eecb782138bf4babe6cd35c7 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 23 Jun 2020 22:21:50 -0400 Subject: [PATCH 02/13] Prevent crash when displaying accounts with no pinned statuses --- Tusker/Screens/Profile/ProfileTableViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index 33befb6ca5..b8d84b8f94 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -109,6 +109,7 @@ class ProfileTableViewController: EnhancedTableViewController { getStatuses(onlyPinned: true) { (response) in guard case let .success(statuses, _) = response else { fatalError() } + if statuses.isEmpty { return } self.mastodonController.persistentContainer.addAll(statuses: statuses) { self.pinnedStatuses = statuses.map { ($0.id, .unknown) } From 641ab765a7a625c9184142b4ad1c501372bec907 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 24 Jun 2020 15:42:56 -0400 Subject: [PATCH 03/13] Fix crash when displaying search results --- .../Search/SearchResultsViewController.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index d91522f5ae..4e51c68674 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -134,14 +134,18 @@ class SearchResultsViewController: EnhancedTableViewController { var snapshot = NSDiffableDataSourceSnapshot() self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in - oldSnapshot.itemIdentifiers(inSection: .accounts).forEach { (item) in - guard case let .account(id) = item else { return } - self.mastodonController.persistentContainer.account(for: id, in: context)?.decrementReferenceCount() + if oldSnapshot.indexOfSection(.accounts) != nil { + oldSnapshot.itemIdentifiers(inSection: .accounts).forEach { (item) in + guard case let .account(id) = item else { return } + self.mastodonController.persistentContainer.account(for: id, in: context)?.decrementReferenceCount() + } } - oldSnapshot.itemIdentifiers(inSection: .statuses).forEach { (item) in - guard case let .status(id, _) = item else { return } - self.mastodonController.persistentContainer.status(for: id, in: context)?.decrementReferenceCount() + if oldSnapshot.indexOfSection(.statuses) != nil { + oldSnapshot.itemIdentifiers(inSection: .statuses).forEach { (item) in + guard case let .status(id, _) = item else { return } + self.mastodonController.persistentContainer.status(for: id, in: context)?.decrementReferenceCount() + } } if self.onlySections.contains(.accounts) && !results.accounts.isEmpty { From e70a84274e8b9f628bdb0fee182664d1641ab47d Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 24 Jun 2020 16:41:01 -0400 Subject: [PATCH 04/13] Fix showing instance public timeline --- Tusker/Views/Status/BaseStatusTableViewCell.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index ca7d452092..c589a100b4 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -143,7 +143,11 @@ class BaseStatusTableViewCell: UITableViewCell { } let reblogDisabled: Bool - switch mastodonController.instance.instanceType { + switch mastodonController.instance?.instanceType { + case nil: + // todo: this handle a race condition in instance public timelines + // a somewhat better solution would be waiting to load the timeline until after the instance is loaded + reblogDisabled = true case .mastodon: reblogDisabled = status.visibility == .private || status.visibility == .direct case .pleroma: From fdcdbced38b0eb9a8dd9906fff7455db9c3a72d1 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 25 Jun 2020 10:42:46 -0400 Subject: [PATCH 05/13] Limit context menu previews in ContentTextView to link's text line rects --- Tusker/Views/ContentTextView.swift | 74 +++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index f313c1ce57..01079327be 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -27,6 +27,9 @@ class ContentTextView: LinkTextView { delegate = self + // Disable layer masking, otherwise the context menu opening animation + // may be clipped if it's at an edge of the text view + layer.masksToBounds = false addInteraction(UIContextMenuInteraction(delegate: self)) textDragInteraction?.isEnabled = false @@ -253,6 +256,12 @@ extension ContentTextView: MenuPreviewProvider { extension ContentTextView: UIContextMenuInteractionDelegate { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { if let (link, range) = getLinkAtPoint(location) { + // Determine the line rects that the link takes up in the coordinate space of this view + var rects = [CGRect]() + layoutManager.enumerateEnclosingRects(forGlyphRange: range, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0), in: textContainer) { (rect, stop) in + rects.append(rect) + } + let preview: UIContextMenuContentPreviewProvider = { self.getViewController(forLink: link, inRange: range) } @@ -268,11 +277,68 @@ extension ContentTextView: UIContextMenuInteractionDelegate { } return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions) } - return UIContextMenuConfiguration(identifier: nil, previewProvider: preview, actionProvider: actions) + + // Use a custom UIContentMenuConfiguration subclass to pass the text line rect information + // to the `contextMenuInteraction(_:previewForHighlightingMenuWithConfiguration:)` method. + let configuration = ContextMenuConfiguration(identifier: nil, previewProvider: preview, actionProvider: actions) + configuration.textLineRects = rects + return configuration } else { return nil } } + + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + // If there isn't custom text line rect data, use the default system-generated preview. + guard let config = configuration as? ContextMenuConfiguration, + let rects = config.textLineRects, + rects.count > 0 else { + return nil + } + + // Calculate the smallest rect enclosing all of the text line rects, in the coordinate space of this view. + var minX: CGFloat = .greatestFiniteMagnitude, maxX: CGFloat = .leastNonzeroMagnitude, minY: CGFloat = .greatestFiniteMagnitude, maxY: CGFloat = .leastNonzeroMagnitude + for rect in rects { + minX = min(rect.minX, minX) + maxX = max(rect.maxX, maxX) + minY = min(rect.minY, minY) + maxY = max(rect.maxY, maxY) + } + let rectEnclosingTextLineRects = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) + + // Try to create a snapshot view of this view that only shows the minimum + // rectangle necessary to fully display the link text (reduces the likelihood that + // other text will be displayed alongside it). + // If a snapshot view cannot be created, we bail and use the system-provided preview. + guard let snapshot = self.resizableSnapshotView(from: rectEnclosingTextLineRects, afterScreenUpdates: false, withCapInsets: .zero) else { + return nil + } + + // Convert the textLineRects from the context menu configuration to be in the + // coordinate space of the snapshot view. The snapshot view is created from + // rectEnclosingTextLineRects, which means that, while its size is the same as the + // enclosing rect, its coordinate space is relative to this text views by rectEnclosingTextLineRects.origin. + // Since the text line rects passed to UIPreviewParameters need to be in the coordinate space of + // the preview view, we subtract the origin position from each rect to convert to the snapshot view's + // coordinate space. + let rectsInCoordinateSpaceOfEnclosingRect = rects.map { + $0.offsetBy(dx: -rectEnclosingTextLineRects.minX, dy: -rectEnclosingTextLineRects.minY) + } + + // The preview parameters describe how the preview view is shown inside the prev. + let parameters = UIPreviewParameters(textLineRects: rectsInCoordinateSpaceOfEnclosingRect as [NSValue]) + // todo: parameters.visiblePath around text + + // The center point of the the minimum enclosing rect in our coordinate space is the point where the + // center of the preview should be, since that's also in this view's coordinate space. + let rectsCenter = CGPoint(x: rectEnclosingTextLineRects.midX, y: rectEnclosingTextLineRects.midY) + + // The preview target describes how the preview is positioned. + let target = UIPreviewTarget(container: self, center: rectsCenter) + + return UITargetedPreview(view: snapshot, parameters: parameters, target: target) + } + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { if let viewController = animator.previewViewController { animator.preferredCommitStyle = .pop @@ -281,4 +347,10 @@ extension ContentTextView: UIContextMenuInteractionDelegate { } } } + + /// Used to pass text line rect data between `contextMenuInteraction(_:configurationForMenuAtLocation:)` and `contextMenuInteraction(_:previewForHighlightingMenuWithConfiguration:)` + fileprivate class ContextMenuConfiguration: UIContextMenuConfiguration { + /// The line rects of the source of this context menu configuration in the coordinate space of the preview target view. + var textLineRects: [CGRect]? + } } From 47dc00ab8f5e4ab89dd0d44bda435f122ad9018c Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 30 Jun 2020 23:24:03 -0400 Subject: [PATCH 06/13] Fix sometimes broken masking of text view link preview animations --- Tusker.xcodeproj/project.pbxproj | 4 ++ Tusker/Extensions/UIBezierPath+Helpers.swift | 61 ++++++++++++++++++++ Tusker/Views/ContentTextView.swift | 15 ++++- 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 Tusker/Extensions/UIBezierPath+Helpers.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index be1f9d76e6..138d8715e0 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -175,6 +175,7 @@ D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; }; D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; }; D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; }; + D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */; }; D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; }; D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; }; D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; }; @@ -484,6 +485,7 @@ D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = ""; }; D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = ""; }; D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = ""; }; + D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Helpers.swift"; sourceTree = ""; }; D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = ""; }; D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = ""; }; D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = ""; }; @@ -1064,6 +1066,7 @@ D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */, D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */, D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */, + D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */, ); path = Extensions; sourceTree = ""; @@ -1849,6 +1852,7 @@ D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */, D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */, D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */, + D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */, D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */, D6163F2C21AA0AF1008DAC41 /* MyProfileTableViewController.swift in Sources */, ); diff --git a/Tusker/Extensions/UIBezierPath+Helpers.swift b/Tusker/Extensions/UIBezierPath+Helpers.swift new file mode 100644 index 0000000000..cf7023122d --- /dev/null +++ b/Tusker/Extensions/UIBezierPath+Helpers.swift @@ -0,0 +1,61 @@ +// +// UIBezierPath+Helpers.swift +// Tusker +// +// Created by Shadowfacts on 6/25/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +// TODO: write unit tests for this +extension UIBezierPath { + + /// Create a new UIBezierPath that wraps around the given array of rectangles. + /// This is not a convex hull aglorithm. What this does is it takes a set of rectangles + /// and draws a line around the outer borders of the combined shape. + convenience init(wrappingAround rects: [CGRect]) { + precondition(rects.count > 0) + let rects = rects.sorted { $0.minY < $1.minY } + + self.init() + + // start at the top left corner + self.move(to: CGPoint(x: rects.first!.minX, y: rects.first!.minY)) + + // walk down the left side + var prevLeft = rects.first!.minX + for rect in rects where !rect.minX.isEqual(to: prevLeft) { + self.addLine(to: CGPoint(x: prevLeft, y: rect.minY)) + self.addLine(to: CGPoint(x: rect.minX, y: rect.minY)) + prevLeft = rect.minX + } + + // ensure at the bottom left if not already + let bottomLeft = CGPoint(x: rects.last!.minX, y: rects.last!.maxY) + if !self.currentPoint.equalTo(bottomLeft) { + self.addLine(to: bottomLeft) + } + + // across the bottom of the last rect + self.addLine(to: CGPoint(x: rects.last!.maxX, y: rects.last!.maxY)) + + // walk up the right side + var prevRight = rects.last!.maxX + for rect in rects.reversed() where !rect.maxX.isEqual(to: prevRight) { + self.addLine(to: CGPoint(x: prevRight, y: rect.maxY)) + self.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + prevRight = rect.maxX + } + + // ensure at the top right if not already + let topRight = CGPoint(x: rects.first!.maxX, y: rects.first!.minY) + if !self.currentPoint.equalTo(topRight) { + self.addLine(to: topRight) + } + + // across the top of the first rect + self.addLine(to: CGPoint(x: rects.first!.minX, y: rects.first!.minY)) + } + +} diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index 01079327be..23bef986ac 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -327,7 +327,13 @@ extension ContentTextView: UIContextMenuInteractionDelegate { // The preview parameters describe how the preview view is shown inside the prev. let parameters = UIPreviewParameters(textLineRects: rectsInCoordinateSpaceOfEnclosingRect as [NSValue]) - // todo: parameters.visiblePath around text + + // Mask the snapshot layer to only show the text of the link, and nothing else. + // By default, the system-applied mask is too wide and other content may seep in. + let path = UIBezierPath(wrappingAround: rectsInCoordinateSpaceOfEnclosingRect) + let maskLayer = CAShapeLayer() + maskLayer.path = path.cgPath + snapshot.layer.mask = maskLayer // The center point of the the minimum enclosing rect in our coordinate space is the point where the // center of the preview should be, since that's also in this view's coordinate space. @@ -336,7 +342,12 @@ extension ContentTextView: UIContextMenuInteractionDelegate { // The preview target describes how the preview is positioned. let target = UIPreviewTarget(container: self, center: rectsCenter) - return UITargetedPreview(view: snapshot, parameters: parameters, target: target) + // Create a dummy containerview for the snapshot view, since using a view with a CALayer mask and UIPreviewParameters(textLineRects:) + // causes the mask to be ignored. See FB7832297 + let snapshotContainer = UIView(frame: snapshot.bounds) + snapshotContainer.addSubview(snapshot) + + return UITargetedPreview(view: snapshotContainer, parameters: parameters, target: target) } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { From c55ea2e0050cc94cb9b968b41cc218b4af28dbd9 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 1 Jul 2020 18:50:20 -0400 Subject: [PATCH 07/13] More link context menu preview tweaks --- Tusker/Extensions/UIBezierPath+Helpers.swift | 6 ++ Tusker/Views/ContentTextView.swift | 98 +++++++++----------- 2 files changed, 50 insertions(+), 54 deletions(-) diff --git a/Tusker/Extensions/UIBezierPath+Helpers.swift b/Tusker/Extensions/UIBezierPath+Helpers.swift index cf7023122d..a471fbf297 100644 --- a/Tusker/Extensions/UIBezierPath+Helpers.swift +++ b/Tusker/Extensions/UIBezierPath+Helpers.swift @@ -16,6 +16,12 @@ extension UIBezierPath { /// and draws a line around the outer borders of the combined shape. convenience init(wrappingAround rects: [CGRect]) { precondition(rects.count > 0) + + if rects.count == 1 { + self.init(rect: rects.first!) + return + } + let rects = rects.sorted { $0.minY < $1.minY } self.init() diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index 23bef986ac..81a405fde3 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -22,6 +22,11 @@ class ContentTextView: LinkTextView { var defaultFont: UIFont = .systemFont(ofSize: 17) var defaultColor: UIColor = .label + // The link range currently being previewed + private var currentPreviewedLinkRange: NSRange? + // The preview created in the previewForHighlighting method, so that we can use the same one in previewForDismissing. + private weak var currentTargetedPreview: UITargetedPreview? + override func awakeFromNib() { super.awakeFromNib() @@ -256,11 +261,8 @@ extension ContentTextView: MenuPreviewProvider { extension ContentTextView: UIContextMenuInteractionDelegate { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { if let (link, range) = getLinkAtPoint(location) { - // Determine the line rects that the link takes up in the coordinate space of this view - var rects = [CGRect]() - layoutManager.enumerateEnclosingRects(forGlyphRange: range, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0), in: textContainer) { (rect, stop) in - rects.append(rect) - } + // Store the previewed link range for use in the previewForHighlighting method + currentPreviewedLinkRange = range let preview: UIContextMenuContentPreviewProvider = { self.getViewController(forLink: link, inRange: range) @@ -278,66 +280,53 @@ extension ContentTextView: UIContextMenuInteractionDelegate { return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions) } - // Use a custom UIContentMenuConfiguration subclass to pass the text line rect information - // to the `contextMenuInteraction(_:previewForHighlightingMenuWithConfiguration:)` method. - let configuration = ContextMenuConfiguration(identifier: nil, previewProvider: preview, actionProvider: actions) - configuration.textLineRects = rects - return configuration + return UIContextMenuConfiguration(identifier: nil, previewProvider: preview, actionProvider: actions) } else { + currentPreviewedLinkRange = nil return nil } } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { - // If there isn't custom text line rect data, use the default system-generated preview. - guard let config = configuration as? ContextMenuConfiguration, - let rects = config.textLineRects, - rects.count > 0 else { + // If there isn't a link range, use the default system-generated preview. + guard let range = currentPreviewedLinkRange else { + return nil + } + currentPreviewedLinkRange = nil + + // Determine the line rects that the link takes up in the coordinate space of this view. + var rects = [CGRect]() + layoutManager.enumerateEnclosingRects(forGlyphRange: range, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0), in: textContainer) { (rect, stop) in + rects.append(rect) + } + + // Try to create a snapshot view of this view to disply as the preview. + // If a snapshot view cannot be created, we bail and use the system-provided preview. + guard let snapshot = self.snapshotView(afterScreenUpdates: false) else { return nil } + // Mask the snapshot layer to only show the text of the link, and nothing else. + // By default, the system-applied mask is too wide and other content may seep in. + let path = UIBezierPath(wrappingAround: rects) + let maskLayer = CAShapeLayer() + maskLayer.path = path.cgPath + snapshot.layer.mask = maskLayer + + // The preview parameters describe how the preview view is shown inside the preview. + let parameters = UIPreviewParameters(textLineRects: rects as [NSValue]) + // Calculate the smallest rect enclosing all of the text line rects, in the coordinate space of this view. - var minX: CGFloat = .greatestFiniteMagnitude, maxX: CGFloat = .leastNonzeroMagnitude, minY: CGFloat = .greatestFiniteMagnitude, maxY: CGFloat = .leastNonzeroMagnitude + var minX: CGFloat = .greatestFiniteMagnitude, maxX: CGFloat = -.greatestFiniteMagnitude, minY: CGFloat = .greatestFiniteMagnitude, maxY: CGFloat = -.greatestFiniteMagnitude for rect in rects { minX = min(rect.minX, minX) maxX = max(rect.maxX, maxX) minY = min(rect.minY, minY) maxY = max(rect.maxY, maxY) } - let rectEnclosingTextLineRects = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) - - // Try to create a snapshot view of this view that only shows the minimum - // rectangle necessary to fully display the link text (reduces the likelihood that - // other text will be displayed alongside it). - // If a snapshot view cannot be created, we bail and use the system-provided preview. - guard let snapshot = self.resizableSnapshotView(from: rectEnclosingTextLineRects, afterScreenUpdates: false, withCapInsets: .zero) else { - return nil - } - - // Convert the textLineRects from the context menu configuration to be in the - // coordinate space of the snapshot view. The snapshot view is created from - // rectEnclosingTextLineRects, which means that, while its size is the same as the - // enclosing rect, its coordinate space is relative to this text views by rectEnclosingTextLineRects.origin. - // Since the text line rects passed to UIPreviewParameters need to be in the coordinate space of - // the preview view, we subtract the origin position from each rect to convert to the snapshot view's - // coordinate space. - let rectsInCoordinateSpaceOfEnclosingRect = rects.map { - $0.offsetBy(dx: -rectEnclosingTextLineRects.minX, dy: -rectEnclosingTextLineRects.minY) - } - - // The preview parameters describe how the preview view is shown inside the prev. - let parameters = UIPreviewParameters(textLineRects: rectsInCoordinateSpaceOfEnclosingRect as [NSValue]) - - // Mask the snapshot layer to only show the text of the link, and nothing else. - // By default, the system-applied mask is too wide and other content may seep in. - let path = UIBezierPath(wrappingAround: rectsInCoordinateSpaceOfEnclosingRect) - let maskLayer = CAShapeLayer() - maskLayer.path = path.cgPath - snapshot.layer.mask = maskLayer - // The center point of the the minimum enclosing rect in our coordinate space is the point where the // center of the preview should be, since that's also in this view's coordinate space. - let rectsCenter = CGPoint(x: rectEnclosingTextLineRects.midX, y: rectEnclosingTextLineRects.midY) + let rectsCenter = CGPoint(x: (minX + maxX) / 2, y: (minY + maxY) / 2) // The preview target describes how the preview is positioned. let target = UIPreviewTarget(container: self, center: rectsCenter) @@ -347,7 +336,14 @@ extension ContentTextView: UIContextMenuInteractionDelegate { let snapshotContainer = UIView(frame: snapshot.bounds) snapshotContainer.addSubview(snapshot) - return UITargetedPreview(view: snapshotContainer, parameters: parameters, target: target) + let preview = UITargetedPreview(view: snapshotContainer, parameters: parameters, target: target) + currentTargetedPreview = preview + return preview + } + + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + // Use the same preview for dismissing as was used for highlighting, so that the link animates back to the original position. + return currentTargetedPreview } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { @@ -358,10 +354,4 @@ extension ContentTextView: UIContextMenuInteractionDelegate { } } } - - /// Used to pass text line rect data between `contextMenuInteraction(_:configurationForMenuAtLocation:)` and `contextMenuInteraction(_:previewForHighlightingMenuWithConfiguration:)` - fileprivate class ContextMenuConfiguration: UIContextMenuConfiguration { - /// The line rects of the source of this context menu configuration in the coordinate space of the preview target view. - var textLineRects: [CGRect]? - } } From 19fa12391d7dde01e31540330e7fc5c4080c24a6 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 3 Jul 2020 17:07:57 -0400 Subject: [PATCH 08/13] Fix Preferences button not appearing --- .../Screens/Profile/MyProfileTableViewController.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Tusker/Screens/Profile/MyProfileTableViewController.swift b/Tusker/Screens/Profile/MyProfileTableViewController.swift index 8e8cdf44b4..2fbc550387 100644 --- a/Tusker/Screens/Profile/MyProfileTableViewController.swift +++ b/Tusker/Screens/Profile/MyProfileTableViewController.swift @@ -7,7 +7,6 @@ // import UIKit -import SwiftUI class MyProfileTableViewController: ProfileTableViewController { @@ -33,8 +32,6 @@ class MyProfileTableViewController: ProfileTableViewController { } }) } - - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Preferences", style: .plain, target: self, action: #selector(preferencesPressed)) } required init?(coder aDecoder: NSCoder) { @@ -43,10 +40,8 @@ class MyProfileTableViewController: ProfileTableViewController { override func viewDidLoad() { super.viewDidLoad() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) + + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Preferences", style: .plain, target: self, action: #selector(preferencesPressed)) } @objc func preferencesPressed() { From b07efc150c5752c8e544159cc4572ff17dc73d90 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 3 Jul 2020 18:45:37 -0400 Subject: [PATCH 09/13] Use App Group for user defaults --- Tusker/LocalData.swift | 15 ++++++++++++++- Tusker/Tusker.entitlements | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 Tusker/Tusker.entitlements diff --git a/Tusker/LocalData.swift b/Tusker/LocalData.swift index 821b395587..5c9f51e0c5 100644 --- a/Tusker/LocalData.swift +++ b/Tusker/LocalData.swift @@ -30,7 +30,20 @@ class LocalData: ObservableObject { ] } } else { - defaults = UserDefaults() + defaults = UserDefaults(suiteName: "group.space.vaccor.Tusker")! + tryMigrateOldDefaults() + } + } + + // TODO: remove me before public beta + private func tryMigrateOldDefaults() { + let old = UserDefaults() + if let accounts = old.array(forKey: accountsKey) as? [[String: String]], + let mostRecentAccount = old.string(forKey: mostRecentAccountKey) { + defaults.setValue(accounts, forKey: accountsKey) + defaults.setValue(mostRecentAccount, forKey: mostRecentAccountKey) + old.removeObject(forKey: accountsKey) + old.removeObject(forKey: mostRecentAccountKey) } } diff --git a/Tusker/Tusker.entitlements b/Tusker/Tusker.entitlements new file mode 100644 index 0000000000..ad0d7a7480 --- /dev/null +++ b/Tusker/Tusker.entitlements @@ -0,0 +1,20 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.space.vaccor.Tusker + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + com.apple.security.network.client + + com.apple.security.personal-information.photos-library + + + From 4e4701ead544c81129a7277683707b6558878a38 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 23 Jun 2020 19:31:32 -0400 Subject: [PATCH 10/13] Use SwiftSoup from SPM instead of Git submodule --- .gitmodules | 3 --- SwiftSoup | 1 - Tusker.xcodeproj/project.pbxproj | 25 +++++++++++++------ Tusker.xcworkspace/contents.xcworkspacedata | 3 --- .../xcshareddata/swiftpm/Package.resolved | 18 +++++++++++++ 5 files changed, 35 insertions(+), 15 deletions(-) delete mode 160000 SwiftSoup diff --git a/.gitmodules b/.gitmodules index 2128c88aa9..c241e9841e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "SwiftSoup"] - path = SwiftSoup - url = git://github.com/scinfu/SwiftSoup.git [submodule "Cache"] path = Cache url = git@github.com:hyperoslo/Cache.git diff --git a/SwiftSoup b/SwiftSoup deleted file mode 160000 index f445c9067d..0000000000 --- a/SwiftSoup +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f445c9067d28346e828e615e2b43cb07b20bca35 diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 138d8715e0..a93af7abf2 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; }; 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; }; D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */; }; + D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; }; D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; }; D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; }; D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; }; @@ -137,7 +138,6 @@ D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; }; D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; }; D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; }; - D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; }; D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; }; D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */; }; D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */; }; @@ -227,7 +227,6 @@ D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */; }; D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; }; D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; }; - D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; }; D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; }; D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; }; @@ -304,7 +303,6 @@ dstSubfolderSpec = 10; files = ( D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */, - D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */, D6BC874621961F73006163F1 /* Gifu.framework in Embed Frameworks */, 0461A3912163CBAE00C0A807 /* Cache.framework in Embed Frameworks */, ); @@ -532,7 +530,6 @@ D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsMode.swift; sourceTree = ""; }; D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = ""; }; D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = ""; }; - D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = ""; }; D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = ""; }; D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = ""; }; @@ -567,7 +564,6 @@ D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CrashReporterViewController.xib; sourceTree = ""; }; D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = ""; }; D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = ""; }; - D6F98BD523AE951F008A4DAC /* Swifter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Swifter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -592,10 +588,10 @@ files = ( 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 */, + D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1033,7 +1029,6 @@ D65F613523AFD65900F3CFD3 /* Ambassador.framework */, D65F613023AE99E000F3CFD3 /* Ambassador.framework */, D65F612D23AE990C00F3CFD3 /* Embassy.framework */, - D6F98BD523AE951F008A4DAC /* Swifter.framework */, ); name = Frameworks; sourceTree = ""; @@ -1262,7 +1257,6 @@ children = ( D6BC874421961F73006163F1 /* Gifu.framework */, 0461A38F2163CBAE00C0A807 /* Cache.framework */, - D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */, D61099AC2144B0CC00432DC2 /* Pachyderm */, D61099B92144B0CC00432DC2 /* PachydermTests */, D6D4DDCE212518A000E1C4BB /* Tusker */, @@ -1436,6 +1430,7 @@ packageProductDependencies = ( D6B0539E23BD2BA300A066FA /* SheetController */, D69CCBBE249E6EFD000AF167 /* CrashReporter */, + D60CFFDA24A290BA00D00083 /* SwiftSoup */, ); productName = Tusker; productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */; @@ -1532,6 +1527,7 @@ packageReferences = ( D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */, D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */, + D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */, ); productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */; projectDirPath = ""; @@ -2330,6 +2326,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/scinfu/SwiftSoup.git"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 2.3.2; + }; + }; D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/microsoft/plcrashreporter"; @@ -2349,6 +2353,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + D60CFFDA24A290BA00D00083 /* SwiftSoup */ = { + isa = XCSwiftPackageProductDependency; + package = D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */; + productName = SwiftSoup; + }; D69CCBBE249E6EFD000AF167 /* CrashReporter */ = { isa = XCSwiftPackageProductDependency; package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */; diff --git a/Tusker.xcworkspace/contents.xcworkspacedata b/Tusker.xcworkspace/contents.xcworkspacedata index c5c106cd68..5223b08d8f 100644 --- a/Tusker.xcworkspace/contents.xcworkspacedata +++ b/Tusker.xcworkspace/contents.xcworkspacedata @@ -10,9 +10,6 @@ - - diff --git a/Tusker.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tusker.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7cd77ddd9b..c01ef22eb1 100644 --- a/Tusker.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Tusker.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,6 +9,24 @@ "revision": "4637a7854de2cc5c354d46fb931d74bdbc2c043e", "version": "1.7.0" } + }, + { + "package": "SheetController", + "repositoryURL": "https://git.shadowfacts.net/shadowfacts/SheetController.git", + "state": { + "branch": "master", + "revision": "6926446c4e15eb7f4513c4c00df9279553b330be", + "version": null + } + }, + { + "package": "SwiftSoup", + "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", + "state": { + "branch": null, + "revision": "774dc9c7213085db8aa59595e27c1cd22e428904", + "version": "2.3.2" + } } ] }, From d190636fbde8fcc5132fe647404888c53c6005f4 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 3 Jul 2020 19:36:08 -0400 Subject: [PATCH 11/13] Fix Preferences button not appearing (again) --- Tusker/Screens/Profile/ProfileTableViewController.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index b8d84b8f94..50e509a0bd 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -31,10 +31,6 @@ class ProfileTableViewController: EnhancedTableViewController { self.accountID = accountID super.init(style: .plain) - - self.refreshControl = UIRefreshControl() - refreshControl!.addTarget(self, action: #selector(refreshStatuses(_:)), for: .valueChanged) - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composePressed(_:))) } required init?(coder aDecoder: NSCoder) { @@ -51,6 +47,10 @@ class ProfileTableViewController: EnhancedTableViewController { override func viewDidLoad() { super.viewDidLoad() + + self.refreshControl = UIRefreshControl() + refreshControl!.addTarget(self, action: #selector(refreshStatuses(_:)), for: .valueChanged) + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composePressed(_:))) tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 140 From 2916d7a72d0e394aad0ce5f1769aaf7f574e4b53 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 3 Jul 2020 19:36:48 -0400 Subject: [PATCH 12/13] Add tapping the active tab bar item to scroll to top Closes #106 --- Tusker.xcodeproj/project.pbxproj | 4 ++++ Tusker/Screens/Main/MainTabBarViewController.swift | 7 +++++++ .../Utilities/EnhancedTableViewController.swift | 10 ++++++++++ .../Utilities/SegmentedPageViewController.swift | 8 ++++++++ .../Utilities/TabBarScrollableViewController.swift | 13 +++++++++++++ 5 files changed, 42 insertions(+) create mode 100644 Tusker/Screens/Utilities/TabBarScrollableViewController.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index a93af7abf2..e2fa5ffb65 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -118,6 +118,7 @@ D63F9C6C241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */; }; D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; }; D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; }; + D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */; }; D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; }; D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; }; D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6434EB2215B1856001A919A /* XCBRequest.swift */; }; @@ -422,6 +423,7 @@ D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeAttachmentTableViewCell.xib; sourceTree = ""; }; D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = ""; }; D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = ""; }; + D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScrollableViewController.swift; sourceTree = ""; }; D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = ""; }; D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = ""; }; D6434EB2215B1856001A919A /* XCBRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequest.swift; sourceTree = ""; }; @@ -1238,6 +1240,7 @@ D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */, D693DE5823FE24300061E07D /* InteractivePushTransition.swift */, D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */, + D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */, ); path = Utilities; sourceTree = ""; @@ -1701,6 +1704,7 @@ 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */, D60E2F292442372B005F8713 /* AccountMO.swift in Sources */, + D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */, D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */, D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */, 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */, diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 0636d7bcb3..7e21e989f9 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -57,6 +57,13 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { presentCompose() return false } + if viewController == viewControllers![selectedIndex], + let nav = viewController as? UINavigationController, + nav.viewControllers.count == 1, + let scrollableVC = nav.viewControllers.first as? TabBarScrollableViewController { + scrollableVC.tabBarScrollToTop() + return false + } return true } diff --git a/Tusker/Screens/Utilities/EnhancedTableViewController.swift b/Tusker/Screens/Utilities/EnhancedTableViewController.swift index 9aee1f5493..95cf0f1bdb 100644 --- a/Tusker/Screens/Utilities/EnhancedTableViewController.swift +++ b/Tusker/Screens/Utilities/EnhancedTableViewController.swift @@ -95,3 +95,13 @@ extension EnhancedTableViewController { } } + +extension EnhancedTableViewController: TabBarScrollableViewController { + func tabBarScrollToTop() { + if scrollViewShouldScrollToTop(tableView) { + let topOffset = CGPoint(x: 0, y: -tableView.adjustedContentInset.top) + tableView.setContentOffset(topOffset, animated: true) + scrollViewDidScrollToTop(tableView) + } + } +} diff --git a/Tusker/Screens/Utilities/SegmentedPageViewController.swift b/Tusker/Screens/Utilities/SegmentedPageViewController.swift index 203860152b..47ea6d51ff 100644 --- a/Tusker/Screens/Utilities/SegmentedPageViewController.swift +++ b/Tusker/Screens/Utilities/SegmentedPageViewController.swift @@ -63,3 +63,11 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel } } + +extension SegmentedPageViewController: TabBarScrollableViewController { + func tabBarScrollToTop() { + if let scrollableVC = pageControllers[currentIndex] as? TabBarScrollableViewController { + scrollableVC.tabBarScrollToTop() + } + } +} diff --git a/Tusker/Screens/Utilities/TabBarScrollableViewController.swift b/Tusker/Screens/Utilities/TabBarScrollableViewController.swift new file mode 100644 index 0000000000..8c7df9027e --- /dev/null +++ b/Tusker/Screens/Utilities/TabBarScrollableViewController.swift @@ -0,0 +1,13 @@ +// +// TabBarScrollableViewController.swift +// Tusker +// +// Created by Shadowfacts on 7/3/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +protocol TabBarScrollableViewController: UIViewController { + func tabBarScrollToTop() +} From caab5e357a67f1b939683e89ecda6862a1095c4b Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 3 Jul 2020 22:13:49 -0400 Subject: [PATCH 13/13] Fix crash loading audio attachment uploaded on Mastodon Closes #104 --- Pachyderm/Model/Attachment.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Pachyderm/Model/Attachment.swift b/Pachyderm/Model/Attachment.swift index f71a35fa24..cc376dff25 100644 --- a/Pachyderm/Model/Attachment.swift +++ b/Pachyderm/Model/Attachment.swift @@ -13,7 +13,7 @@ public class Attachment: Codable { public let kind: Kind public let url: URL public let remoteURL: URL? - public let previewURL: URL + public let previewURL: URL? public let textURL: URL? public let meta: Metadata? public let description: String? @@ -30,11 +30,11 @@ public class Attachment: Codable { self.id = try container.decode(String.self, forKey: .id) self.kind = try container.decode(Kind.self, forKey: .kind) self.url = try container.decode(URL.self, forKey: .url) - self.previewURL = try container.decode(URL.self, forKey: .previewURL) - self.remoteURL = try? container.decode(URL.self, forKey: .remoteURL) - self.textURL = try? container.decode(URL.self, forKey: .textURL) - self.meta = try? container.decode(Metadata.self, forKey: .meta) - self.description = try? container.decode(String.self, forKey: .description) + self.previewURL = try? container.decode(URL?.self, forKey: .previewURL) + self.remoteURL = try? container.decode(URL?.self, forKey: .remoteURL) + self.textURL = try? container.decode(URL?.self, forKey: .textURL) + self.meta = try? container.decode(Metadata?.self, forKey: .meta) + self.description = try? container.decode(String?.self, forKey: .description) } private enum CodingKeys: String, CodingKey {