Remove invisible view from share extension

This commit is contained in:
Max Radermacher
2025-10-07 12:08:52 -05:00
committed by GitHub
parent 08ee88e24e
commit 9863fed518
10 changed files with 238 additions and 295 deletions

View File

@@ -454,7 +454,6 @@
4520D8D51D417D8E00123472 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4520D8D41D417D8E00123472 /* Photos.framework */; };
452EC6DF205E9E30000E787C /* MediaGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EC6DE205E9E30000E787C /* MediaGallery.swift */; };
4535186B1FC635DD00210559 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4535186A1FC635DD00210559 /* ShareViewController.swift */; };
4535186E1FC635DD00210559 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4535186C1FC635DD00210559 /* MainInterface.storyboard */; };
453518721FC635DD00210559 /* SignalShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 453518681FC635DD00210559 /* SignalShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
45360B911F952AA900FA666C /* MarqueeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */; };
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4539B5851F79348F007141FF /* PushRegistrationManager.swift */; };
@@ -4352,7 +4351,6 @@
452EC6DE205E9E30000E787C /* MediaGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGallery.swift; sourceTree = "<group>"; };
453518681FC635DD00210559 /* SignalShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SignalShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
4535186A1FC635DD00210559 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
4535186D1FC635DD00210559 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
4535186F1FC635DD00210559 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
4539B5851F79348F007141FF /* PushRegistrationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushRegistrationManager.swift; sourceTree = "<group>"; };
4542DF53208D40AC007B4E76 /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
@@ -8842,7 +8840,6 @@
isa = PBXGroup;
children = (
4535186F1FC635DD00210559 /* Info.plist */,
4535186C1FC635DD00210559 /* MainInterface.storyboard */,
5010526A2BDB23F50097DDC5 /* PrivacyInfo.xcprivacy */,
347850561FD86544007B8332 /* SAEFailedViewController.swift */,
3461284A1FD0B93F00532771 /* SAELoadViewController.swift */,
@@ -15085,7 +15082,6 @@
buildActionMask = 2147483647;
files = (
3478504C1FD7496D007B8332 /* Images.xcassets in Resources */,
4535186E1FC635DD00210559 /* MainInterface.storyboard in Resources */,
5010526B2BDB23F50097DDC5 /* PrivacyInfo.xcprivacy in Resources */,
7665BC9E2A3A72910060279B /* Symbols.xcassets in Resources */,
);
@@ -19142,14 +19138,6 @@
name = PluralAware.stringsdict;
sourceTree = "<group>";
};
4535186C1FC635DD00210559 /* MainInterface.storyboard */ = {
isa = PBXVariantGroup;
children = (
4535186D1FC635DD00210559 /* Base */,
);
name = MainInterface.storyboard;
sourceTree = "<group>";
};
A5E7C673248C5442007C949A /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13527"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Share View Controller-->
<scene sceneID="ceB-am-kn3">
<objects>
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModule="SignalShareExtension" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@@ -80,10 +80,10 @@
)
</string>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
</dict>
<key>OWSBundleIDPrefix</key>
<string>$(SIGNAL_BUNDLEID_PREFIX)</string>

View File

@@ -8,12 +8,13 @@ import PureLayout
import SignalServiceKit
import SignalUI
class SAELoadViewController: UIViewController {
class SAELoadViewController: UIViewController, OWSNavigationChildController {
weak var delegate: ShareViewDelegate?
private weak var delegate: ShareViewDelegate?
private let shouldMimicRecipientPicker: Bool
var activityIndicator: UIActivityIndicatorView!
var progressView: UIProgressView!
private var activityIndicator: UIActivityIndicatorView!
private var progressView: UIProgressView!
var progress: Progress? {
didSet {
@@ -26,7 +27,7 @@ class SAELoadViewController: UIViewController {
}
}
func updateProgressViewVisibility() {
private func updateProgressViewVisibility() {
guard progressView != nil, activityIndicator != nil else {
return
}
@@ -45,8 +46,9 @@ class SAELoadViewController: UIViewController {
// MARK: Initializers and Factory Methods
init(delegate: ShareViewDelegate) {
init(delegate: ShareViewDelegate, shouldMimicRecipientPicker: Bool = false) {
self.delegate = delegate
self.shouldMimicRecipientPicker = shouldMimicRecipientPicker
super.init(nibName: nil, bundle: nil)
}
@@ -58,7 +60,22 @@ class SAELoadViewController: UIViewController {
override func loadView() {
super.loadView()
self.view.backgroundColor = Theme.backgroundColor
// It's not (currently) safe to create a SharingThreadPickerViewController
// while the Share Extension is launching, so instead mimic the header of
// the picker on the loading view controller.
//
// TODO: Make it safe to do so and remove this hack.
if self.shouldMimicRecipientPicker {
self.title = ConversationPickerViewController.Strings.title
self.navigationItem.leftBarButtonItem = .cancelButton(action: {})
self.navigationItem.leftBarButtonItem?.isEnabled = false
}
self.view.backgroundColor = (
self.shouldMimicRecipientPicker
? Theme.tableView2PresentedBackgroundColor
: Theme.backgroundColor
)
let activityIndicator = UIActivityIndicatorView(style: .large)
activityIndicator.color = Theme.primaryIconColor
@@ -86,9 +103,15 @@ class SAELoadViewController: UIViewController {
label.autoPinEdge(.top, to: .bottom, of: activityIndicator, withOffset: 12)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
var preferredNavigationBarStyle: OWSNavigationBarStyle {
// The false case should be the default, but we can't access the
// extension's default implementation here.
return self.shouldMimicRecipientPicker ? .solid : .blur
}
self.navigationController?.isNavigationBarHidden = false
var navbarBackgroundColorOverride: UIColor? {
// The false case should be the default, but we can't access the
// extension's default implementation here.
return self.shouldMimicRecipientPicker ? Theme.tableView2PresentedBackgroundColor : nil
}
}

View File

@@ -5,12 +5,12 @@
import CoreServices
import Intents
public import PureLayout
import PureLayout
import SignalServiceKit
public import SignalUI
import UniformTypeIdentifiers
public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailedViewDelegate {
public class ShareViewController: OWSNavigationController, ShareViewDelegate, SAEFailedViewDelegate {
enum ShareViewControllerError: Error {
case obsoleteShare
@@ -23,12 +23,14 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
case noAttachments
}
public var shareViewNavigationController: OWSNavigationController?
public var shareViewNavigationController: OWSNavigationController { self }
private lazy var appReadiness = AppReadinessImpl()
private var connectionTokens = [OWSChatConnection.ConnectionToken]()
private var initialLoadViewController: SAELoadViewController?
override open func loadView() {
super.loadView()
@@ -43,6 +45,29 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
Logger.info("")
let initialLoadViewController = SAELoadViewController(
delegate: self,
shouldMimicRecipientPicker: self.extensionContext?.intent == nil,
)
self.setViewControllers([initialLoadViewController], animated: false)
self.initialLoadViewController = initialLoadViewController
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let initialLoadViewController = self.initialLoadViewController.take() {
// Wait one run loop to ensure the loading indicator is visible if setUp
// blocks the main thread.
DispatchQueue.main.async {
Task { try await self.setUp(initialLoadViewController: initialLoadViewController) }
}
}
}
private func setUp(initialLoadViewController: SAELoadViewController) async throws {
let appContext = CurrentAppContext()
let keychainStorage = KeychainStorageImpl(isUsingProductionService: TSConstants.isUsingProductionService)
let databaseStorage: SDSDatabaseStorage
do {
@@ -57,166 +82,146 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
}
databaseStorage.grdbStorage.setUpDatabasePathKVO()
let shareViewNavigationController = OWSNavigationController()
shareViewNavigationController.presentationController?.delegate = self
shareViewNavigationController.delegate = self
self.shareViewNavigationController = shareViewNavigationController
Task {
let initialLoadViewController = SAELoadViewController(delegate: self)
var didDisplaceInitialLoadViewController = false
async let _ = { @MainActor () async throws -> Void in
// Don't display load screen immediately because loading the database and
// preparing attachments (if the recipient is pre-selected) will usually be
// fast enough that we can avoid it altogether. If you haven't run GRDB
// migrations in a while or have selected a long video, though, you'll
// likely see the load screen after 800ms.
try await Task.sleep(nanoseconds: 0.8.clampedNanoseconds)
guard self.presentedViewController == nil else {
return
}
self.showPrimaryViewController(initialLoadViewController)
}()
let databaseContinuation = await AppSetup()
.start(
appContext: appContext,
databaseStorage: databaseStorage,
)
.migrateDatabaseSchema()
.initGlobals(
appReadiness: appReadiness,
backupArchiveErrorPresenterFactory: NoOpBackupArchiveErrorPresenterFactory(),
deviceBatteryLevelManager: nil,
deviceSleepManager: nil,
paymentsEvents: PaymentsEventsAppExtension(),
mobileCoinHelper: MobileCoinHelperMinimal(),
callMessageHandler: NoopCallMessageHandler(),
currentCallProvider: CurrentCallNoOpProvider(),
notificationPresenter: NoopNotificationPresenterImpl(),
incrementalMessageTSAttachmentMigratorFactory: NoOpIncrementalMessageTSAttachmentMigratorFactory(),
)
// Configure the rest of the globals before preparing the database.
SUIEnvironment.shared.setUp(
let databaseContinuation = await AppSetup()
.start(
appContext: appContext,
databaseStorage: databaseStorage,
)
.migrateDatabaseSchema()
.initGlobals(
appReadiness: appReadiness,
authCredentialManager: databaseContinuation.authCredentialManager
backupArchiveErrorPresenterFactory: NoOpBackupArchiveErrorPresenterFactory(),
deviceBatteryLevelManager: nil,
deviceSleepManager: nil,
paymentsEvents: PaymentsEventsAppExtension(),
mobileCoinHelper: MobileCoinHelperMinimal(),
callMessageHandler: NoopCallMessageHandler(),
currentCallProvider: CurrentCallNoOpProvider(),
notificationPresenter: NoopNotificationPresenterImpl(),
incrementalMessageTSAttachmentMigratorFactory: NoOpIncrementalMessageTSAttachmentMigratorFactory(),
)
let finalContinuation = await databaseContinuation.migrateDatabaseData()
finalContinuation.runLaunchTasksIfNeededAndReloadCaches()
switch finalContinuation.setUpLocalIdentifiers(
willResumeInProgressRegistration: false,
canInitiateRegistration: false
) {
case .corruptRegistrationState:
self.showNotRegisteredView()
return
case nil:
self.setAppIsReady()
}
// Configure the rest of the globals before preparing the database.
SUIEnvironment.shared.setUp(
appReadiness: appReadiness,
authCredentialManager: databaseContinuation.authCredentialManager
)
if ScreenLock.shared.isScreenLockEnabled() {
let didUnlock = await withCheckedContinuation { continuation in
let viewController = SAEScreenLockViewController { didUnlock in
continuation.resume(returning: didUnlock)
}
self.showPrimaryViewController(viewController)
let finalContinuation = await databaseContinuation.migrateDatabaseData()
finalContinuation.runLaunchTasksIfNeededAndReloadCaches()
switch finalContinuation.setUpLocalIdentifiers(
willResumeInProgressRegistration: false,
canInitiateRegistration: false
) {
case .corruptRegistrationState:
self.showNotRegisteredView()
return
case nil:
self.setAppIsReady()
}
var didDisplaceInitialLoadViewController = false
if ScreenLock.shared.isScreenLockEnabled() {
let didUnlock = await withCheckedContinuation { continuation in
let viewController = SAEScreenLockViewController { didUnlock in
continuation.resume(returning: didUnlock)
}
guard didUnlock else {
self.shareViewWasCancelled()
self.setViewControllers([viewController], animated: false)
}
guard didUnlock else {
self.shareViewWasCancelled()
return
}
// If we show the Screen Lock UI, that'll displace the loading view
// controller or prevent it from being shown.
didDisplaceInitialLoadViewController = true
}
// Prepare the attachments.
let typedItemProviders: [TypedItemProvider]
do {
typedItemProviders = try buildTypedItemProviders()
} catch {
self.presentAttachmentError(error)
return
}
// We need the unidentified connection for bulk identity key lookups.
let chatConnectionManager = DependenciesBridge.shared.chatConnectionManager
self.connectionTokens.append(chatConnectionManager.requestUnidentifiedConnection())
let conversationPicker: SharingThreadPickerViewController
conversationPicker = SharingThreadPickerViewController(
areAttachmentStoriesCompatPrecheck: typedItemProviders.allSatisfy { $0.isStoriesCompatible },
shareViewDelegate: self
)
let preSelectedThread = self.fetchPreSelectedThread()
let loadViewControllerToDisplay: SAELoadViewController?
let loadViewControllerForProgress: SAELoadViewController?
// If we have a pre-selected thread, we wait to show the approval view
// until the attachments have been built. Otherwise, we'll present it
// immediately and tell it what attachments we're sharing once we've
// finished building them.
if preSelectedThread == nil {
self.setViewControllers([conversationPicker], animated: false)
// We show a progress spinner on the recipient picker.
loadViewControllerToDisplay = nil
loadViewControllerForProgress = nil
} else if didDisplaceInitialLoadViewController {
// We hit this branch when isScreenLockEnabled() == true. In this case, we
// need a new instance because the initial one has already been
// shown/dismissed.
loadViewControllerToDisplay = SAELoadViewController(delegate: self)
loadViewControllerForProgress = loadViewControllerToDisplay
} else {
// We don't need to show anything (it'll be shown by the block at the
// beginning of this Task), but we do want to hook up progress reporting.
loadViewControllerToDisplay = nil
loadViewControllerForProgress = initialLoadViewController
}
let attachments: [SignalAttachment]
do {
// If buildAndValidateAttachments takes longer than 200ms, we want to show
// the new load view. If it takes less than 200ms, we'll exit out of this
// `do` block, that will cancel the `async let`, and then we'll leave the
// primary view controller alone as a result.
async let _ = { @MainActor () async throws -> Void in
guard let loadViewControllerToDisplay else {
return
}
// If we show the Screen Lock UI, that'll displace the loading view
// controller or prevent it from being shown.
didDisplaceInitialLoadViewController = true
}
// Prepare the attachments.
let typedItemProviders: [TypedItemProvider]
do {
typedItemProviders = try buildTypedItemProviders()
} catch {
self.presentAttachmentError(error)
return
}
// We need the unidentified connection for bulk identity key lookups.
let chatConnectionManager = DependenciesBridge.shared.chatConnectionManager
self.connectionTokens.append(chatConnectionManager.requestUnidentifiedConnection())
let conversationPicker: SharingThreadPickerViewController
conversationPicker = SharingThreadPickerViewController(
areAttachmentStoriesCompatPrecheck: typedItemProviders.allSatisfy { $0.isStoriesCompatible },
shareViewDelegate: self
try await Task.sleep(nanoseconds: 0.2.clampedNanoseconds)
// Check for cancellation on the main thread to ensure mutual exclusion
// with the the code outside of this do block.
try Task.checkCancellation()
self.setViewControllers([loadViewControllerToDisplay], animated: false)
}()
attachments = try await buildAndValidateAttachments(
for: typedItemProviders,
setProgress: { loadViewControllerForProgress?.progress = $0 }
)
} catch {
self.presentAttachmentError(error)
return
}
let preSelectedThread = self.fetchPreSelectedThread()
Logger.info("Setting picker attachments: \(attachments)")
conversationPicker.attachments = attachments
let loadViewControllerToDisplay: SAELoadViewController?
let loadViewControllerForProgress: SAELoadViewController?
if let preSelectedThread {
let approvalViewController = try conversationPicker.buildApprovalViewController(for: preSelectedThread)
self.setViewControllers([approvalViewController], animated: false)
// If we have a pre-selected thread, we wait to show the approval view
// until the attachments have been built. Otherwise, we'll present it
// immediately and tell it what attachments we're sharing once we've
// finished building them.
if preSelectedThread == nil {
self.showPrimaryViewController(conversationPicker)
// We show a progress spinner on the recipient picker.
loadViewControllerToDisplay = nil
loadViewControllerForProgress = nil
} else if didDisplaceInitialLoadViewController {
// We hit this branch when isScreenLockEnabled() == true. In this case, we
// need a new instance because the initial one has already been
// shown/dismissed.
loadViewControllerToDisplay = SAELoadViewController(delegate: self)
loadViewControllerForProgress = loadViewControllerToDisplay
} else {
// We don't need to show anything (it'll be shown by the block at the
// beginning of this Task), but we do want to hook up progress reporting.
loadViewControllerToDisplay = nil
loadViewControllerForProgress = initialLoadViewController
}
let attachments: [SignalAttachment]
do {
// If buildAndValidateAttachments takes longer than 200ms, we want to show
// the new load view. If it takes less than 200ms, we'll exit out of this
// `do` block, that will cancel the `async let`, and then we'll leave the
// primary view controller alone as a result.
async let _ = { @MainActor () async throws -> Void in
guard let loadViewControllerToDisplay else {
return
}
try await Task.sleep(nanoseconds: 0.2.clampedNanoseconds)
// Check for cancellation on the main thread to ensure mutual exclusion
// with the the code outside of this do block.
try Task.checkCancellation()
self.showPrimaryViewController(loadViewControllerToDisplay)
}()
attachments = try await buildAndValidateAttachments(
for: typedItemProviders,
setProgress: { loadViewControllerForProgress?.progress = $0 }
)
} catch {
self.presentAttachmentError(error)
return
}
Logger.info("Setting picker attachments: \(attachments)")
conversationPicker.attachments = attachments
if let preSelectedThread {
let approvalViewController = try conversationPicker.buildApprovalViewController(for: preSelectedThread)
self.showPrimaryViewController(approvalViewController)
// If you're sharing to a specific thread, the picker view controller isn't
// added to the view hierarchy, but it's the "brains" of the sending
// operation and must not be deallocated. Tie its lifetime to the lifetime
// of the view controller that's visible.
ObjectRetainer.retainObject(conversationPicker, forLifetimeOf: approvalViewController)
}
// If you're sharing to a specific thread, the picker view controller isn't
// added to the view hierarchy, but it's the "brains" of the sending
// operation and must not be deallocated. Tie its lifetime to the lifetime
// of the view controller that's visible.
ObjectRetainer.retainObject(conversationPicker, forLifetimeOf: approvalViewController)
}
NotificationCenter.default.addObserver(
@@ -241,7 +246,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
if ScreenLock.shared.isScreenLockEnabled() {
Logger.info("dismissing.")
dismissAndCompleteExtension(animated: false, error: ShareViewControllerError.screenLockEnabled)
dismissAndCompleteExtension(error: ShareViewControllerError.screenLockEnabled)
}
}
@@ -277,15 +282,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
let viewController = SAEFailedViewController(delegate: self, title: title, message: message)
let navigationController = UINavigationController()
navigationController.presentationController?.delegate = self
navigationController.setViewControllers([viewController], animated: false)
if self.presentedViewController == nil {
self.present(navigationController, animated: true)
} else {
owsFailDebug("modal already presented. swapping modal content for: \(type(of: viewController))")
assert(self.presentedViewController == navigationController)
}
self.setViewControllers([viewController], animated: false)
}
// MARK: ShareViewDelegate, SAEFailedViewDelegate
@@ -297,63 +294,39 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
public func shareViewWasCompleted() {
Logger.info("")
dismissAndCompleteExtension(animated: true, error: nil)
dismissAndCompleteExtension(error: nil)
}
public func shareViewWasCancelled() {
Logger.info("")
dismissAndCompleteExtension(animated: true, error: ShareViewControllerError.obsoleteShare)
dismissAndCompleteExtension(error: ShareViewControllerError.obsoleteShare)
}
public func shareViewFailed(error: Error) {
owsFailDebug("Error: \(error)")
dismissAndCompleteExtension(animated: true, error: error)
dismissAndCompleteExtension(error: error)
}
private func dismissAndCompleteExtension(animated: Bool, error: Error?) {
private func dismissAndCompleteExtension(error: Error?) {
AssertIsOnMainThread()
let extensionContext = self.extensionContext
dismiss(animated: animated) {
AssertIsOnMainThread()
if let error {
extensionContext?.cancelRequest(withError: error)
} else {
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
// Share extensions reside in a process that may be reused between usages.
// That isn't safe; the codebase is full of statics (e.g. singletons) which
// we can't easily clean up.
Logger.info("ExitShareExtension")
Logger.flush()
exit(0)
if let error {
extensionContext?.cancelRequest(withError: error)
} else {
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
// Share extensions reside in a process that may be reused between usages.
// That isn't safe; the codebase is full of statics (e.g. singletons) which
// we can't easily clean up.
Logger.info("ExitShareExtension")
Logger.flush()
exit(0)
}
// MARK: Helpers
// This view controller is not visible to the user. It exists to intercept touches, set up the
// extensions dependencies, and eventually present a visible view to the user.
// For speed of presentation, we only present a single modal, and if it's already been presented
// we swap out the contents.
// e.g. if loading is taking a while, the user will see the load screen presented with a modal
// animation. Next, when loading completes, the load view will be switched out for the contact
// picker view.
private func showPrimaryViewController(_ viewController: UIViewController) {
AssertIsOnMainThread()
guard let shareViewNavigationController = shareViewNavigationController else {
owsFailDebug("Missing shareViewNavigationController")
return
}
shareViewNavigationController.setViewControllers([viewController], animated: true)
if self.presentedViewController == nil {
self.present(shareViewNavigationController, animated: true)
} else {
assert(self.presentedViewController == shareViewNavigationController)
}
}
private func fetchPreSelectedThread() -> TSThread? {
let hasIntent = self.extensionContext?.intent != nil
Logger.info("hasIntent? \(hasIntent)")
@@ -488,34 +461,16 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
}
return result
}
}
extension ShareViewController: UIAdaptivePresentationControllerDelegate {
public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
public override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// If we're disappearing because we presented something else (e.g., image
// editing tools), don't cancel the share extension.
guard self.presentedViewController == nil else {
return
}
shareViewWasCancelled()
}
}
// MARK: -
extension ShareViewController: UINavigationControllerDelegate {
public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
updateNavigationBarVisibility(for: viewController, in: navigationController, animated: animated)
}
public func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
updateNavigationBarVisibility(for: viewController, in: navigationController, animated: animated)
}
private func updateNavigationBarVisibility(for viewController: UIViewController,
in navigationController: UINavigationController,
animated: Bool) {
switch viewController {
case is AttachmentApprovalViewController:
navigationController.setNavigationBarHidden(true, animated: animated)
default:
navigationController.setNavigationBarHidden(false, animated: animated)
}
}
}

View File

@@ -12,5 +12,5 @@ public protocol ShareViewDelegate: AnyObject {
func shareViewWasCompleted()
func shareViewWasCancelled()
func shareViewFailed(error: Error)
var shareViewNavigationController: OWSNavigationController? { get }
var shareViewNavigationController: OWSNavigationController { get }
}

View File

@@ -69,7 +69,7 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
if let navigationController = shareViewDelegate?.shareViewNavigationController {
navigationController.presentActionSheet(alert)
} else {
super.presentActionSheet(alert)
self.presentActionSheet(alert)
}
}

View File

@@ -228,6 +228,8 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
public override func viewDidLoad() {
super.viewDidLoad()
self.definesPresentationContext = true
view.backgroundColor = .black
// avoid an unpleasant "bounce" which doesn't make sense in the context of a single item.

View File

@@ -218,17 +218,20 @@ public class AttachmentPrepViewController: OWSViewController {
scrollView.transform = .scale(scale).translate(.init(x: 0, y: -offsetY))
}
private func presentFullScreen(viewController: UIViewController) {
private func _presentMediaTool(viewController: UIViewController) {
if let presentedViewController = presentedViewController {
owsAssertDebug(false, "Already has presented view controller. [\(presentedViewController)]")
presentedViewController.dismiss(animated: false) { [weak self] in
self?.presentFullScreen(viewController: viewController)
self?._presentMediaTool(viewController: viewController)
}
return
}
zoomOut(animated: true) { [weak self] in
self?.presentFullScreen(viewController, animated: false)
// Cover the current context. This ensures that we stay within our
// containing frame in the share extension.
viewController.modalPresentationStyle = .currentContext
self?.present(viewController, animated: false)
}
}
@@ -236,10 +239,10 @@ public class AttachmentPrepViewController: OWSViewController {
if let prepDelegate = prepDelegate {
isMediaToolViewControllerPresented = true
prepDelegate.attachmentPrepViewControllerDidRequestUpdateControlsVisibility(self) { _ in
self.presentFullScreen(viewController: viewController)
self._presentMediaTool(viewController: viewController)
}
} else {
self.presentFullScreen(viewController: viewController)
self._presentMediaTool(viewController: viewController)
}
}

View File

@@ -1281,12 +1281,12 @@ extension ConversationPickerViewController: ApprovalFooterDelegate {
// MARK: -
extension ConversationPickerViewController {
private struct Strings {
static let title = OWSLocalizedString("CONVERSATION_PICKER_TITLE", comment: "navbar header")
static let recentsSection = OWSLocalizedString("CONVERSATION_PICKER_SECTION_RECENTS", comment: "table section header for section containing recent conversations")
static let signalContactsSection = OWSLocalizedString("CONVERSATION_PICKER_SECTION_SIGNAL_CONTACTS", comment: "table section header for section containing contacts")
static let groupsSection = OWSLocalizedString("CONVERSATION_PICKER_SECTION_GROUPS", comment: "table section header for section containing groups")
static let storiesSection = OWSLocalizedString("CONVERSATION_PICKER_SECTION_STORIES", comment: "table section header for section containing stories")
public struct Strings {
public static let title = OWSLocalizedString("CONVERSATION_PICKER_TITLE", comment: "navbar header")
fileprivate static let recentsSection = OWSLocalizedString("CONVERSATION_PICKER_SECTION_RECENTS", comment: "table section header for section containing recent conversations")
fileprivate static let signalContactsSection = OWSLocalizedString("CONVERSATION_PICKER_SECTION_SIGNAL_CONTACTS", comment: "table section header for section containing contacts")
fileprivate static let groupsSection = OWSLocalizedString("CONVERSATION_PICKER_SECTION_GROUPS", comment: "table section header for section containing groups")
fileprivate static let storiesSection = OWSLocalizedString("CONVERSATION_PICKER_SECTION_STORIES", comment: "table section header for section containing stories")
}
}