forked from shadowfacts/Tusker
parent
3e5a3c81b5
commit
1e950b5ccb
|
@ -29,11 +29,11 @@ public protocol DuckableViewControllerDelegate: AnyObject {
|
|||
|
||||
extension UIViewController {
|
||||
@available(iOS 16.0, *)
|
||||
public func presentDuckable(_ viewController: DuckableViewController) -> Bool {
|
||||
public func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool = false) -> Bool {
|
||||
var cur: UIViewController? = self
|
||||
while let vc = cur {
|
||||
if let container = vc as? DuckableContainerViewController {
|
||||
container.presentDuckable(viewController, animated: true, completion: nil)
|
||||
container.presentDuckable(viewController, animated: animated, isDucked: isDucked, completion: nil)
|
||||
return true
|
||||
} else {
|
||||
cur = vc.parent
|
||||
|
|
|
@ -17,6 +17,14 @@ public class DuckableContainerViewController: UIViewController, DuckableViewCont
|
|||
private var bottomConstraint: NSLayoutConstraint!
|
||||
private(set) var state = State.idle
|
||||
|
||||
public var duckedViewController: DuckableViewController? {
|
||||
if case .ducked(let vc, placeholder: _) = state {
|
||||
return vc
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public init(child: UIViewController) {
|
||||
self.child = child
|
||||
|
||||
|
@ -50,7 +58,7 @@ public class DuckableContainerViewController: UIViewController, DuckableViewCont
|
|||
])
|
||||
}
|
||||
|
||||
func presentDuckable(_ viewController: DuckableViewController, animated: Bool, completion: (() -> Void)?) {
|
||||
func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
|
||||
guard case .idle = state else {
|
||||
if animated,
|
||||
case .ducked(_, placeholder: let placeholder) = state {
|
||||
|
@ -69,9 +77,14 @@ public class DuckableContainerViewController: UIViewController, DuckableViewCont
|
|||
}
|
||||
return
|
||||
}
|
||||
if isDucked {
|
||||
state = .ducked(viewController, placeholder: createPlaceholderForDuckedViewController(viewController))
|
||||
configureChildForDuckedPlaceholder()
|
||||
} else {
|
||||
state = .presentingDucked(viewController, isFirstPresentation: true)
|
||||
doPresentDuckable(viewController, animated: animated, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
private func doPresentDuckable(_ viewController: DuckableViewController, animated: Bool, completion: (() -> Void)?) {
|
||||
viewController.duckableDelegate = self
|
||||
|
@ -79,9 +92,7 @@ public class DuckableContainerViewController: UIViewController, DuckableViewCont
|
|||
nav.modalPresentationStyle = .custom
|
||||
nav.transitioningDelegate = self
|
||||
present(nav, animated: animated) {
|
||||
self.bottomConstraint.isActive = false
|
||||
self.bottomConstraint = self.child.view.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -detentHeight - 4)
|
||||
self.bottomConstraint.isActive = true
|
||||
self.configureChildForDuckedPlaceholder()
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
@ -127,10 +138,18 @@ public class DuckableContainerViewController: UIViewController, DuckableViewCont
|
|||
}
|
||||
let placeholder = createPlaceholderForDuckedViewController(viewController)
|
||||
state = .ducked(viewController, placeholder: placeholder)
|
||||
configureChildForDuckedPlaceholder()
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
private func configureChildForDuckedPlaceholder() {
|
||||
bottomConstraint.isActive = false
|
||||
bottomConstraint = child.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -detentHeight - 4)
|
||||
bottomConstraint.isActive = true
|
||||
|
||||
child.view.layer.cornerRadius = duckedCornerRadius
|
||||
child.view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
child.view.layer.masksToBounds = true
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
@objc func unduckViewController() {
|
||||
|
@ -191,7 +210,10 @@ extension DuckableContainerViewController: UIViewControllerTransitioningDelegate
|
|||
@available(iOS 16.0, *)
|
||||
extension DuckableContainerViewController: UISheetPresentationControllerDelegate {
|
||||
public func presentationController(_ presentationController: UIPresentationController, willPresentWithAdaptiveStyle style: UIModalPresentationStyle, transitionCoordinator: UIViewControllerTransitionCoordinator?) {
|
||||
let snapshot = child.view.snapshotView(afterScreenUpdates: false)!
|
||||
guard let snapshot = child.view.snapshotView(afterScreenUpdates: false) else {
|
||||
setOverrideTraitCollection(UITraitCollection(userInterfaceLevel: .elevated), forChild: child)
|
||||
return
|
||||
}
|
||||
snapshot.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.view.addSubview(snapshot)
|
||||
NSLayoutConstraint.activate([
|
||||
|
|
|
@ -43,11 +43,11 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Wra
|
|||
super.init(rootView: wrapper)
|
||||
|
||||
self.uiState.delegate = self
|
||||
|
||||
pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self)
|
||||
|
||||
userActivity = UserActivityManager.newPostActivity(accountID: mastodonController.accountInfo!.id)
|
||||
|
||||
updateNavigationTitle(draft: uiState.draft)
|
||||
|
||||
self.uiState.$draft
|
||||
.flatMap(\.objectWillChange)
|
||||
.debounce(for: .milliseconds(250), scheduler: DispatchQueue.global(qos: .utility))
|
||||
|
@ -55,12 +55,27 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Wra
|
|||
DraftsManager.save()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
self.uiState.$draft
|
||||
.sink { [unowned self] draft in
|
||||
self.updateNavigationTitle(draft: draft)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateNavigationTitle(draft: Draft) {
|
||||
if let id = draft.inReplyToID,
|
||||
let status = mastodonController.persistentContainer.status(for: id) {
|
||||
navigationItem.title = "Reply to @\(status.account.acct)"
|
||||
} else {
|
||||
navigationItem.title = "New Post"
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
|
|
|
@ -107,7 +107,6 @@ struct ComposeView: View {
|
|||
globalFrameOutsideList = frame
|
||||
}
|
||||
})
|
||||
.navigationTitle(navTitle)
|
||||
.sheet(isPresented: $uiState.isShowingDraftsList) {
|
||||
DraftsView(currentDraft: draft, mastodonController: mastodonController)
|
||||
}
|
||||
|
@ -211,15 +210,6 @@ struct ComposeView: View {
|
|||
}.frame(height: 50)
|
||||
}
|
||||
|
||||
private var navTitle: Text {
|
||||
if let id = draft.inReplyToID,
|
||||
let status = mastodonController.persistentContainer.status(for: id) {
|
||||
return Text("Reply to @\(status.account.acct)")
|
||||
} else {
|
||||
return Text("New Post")
|
||||
}
|
||||
}
|
||||
|
||||
private var cancelButton: some View {
|
||||
Button(action: self.cancel) {
|
||||
Text("Cancel")
|
||||
|
|
|
@ -12,10 +12,21 @@ import Duckable
|
|||
@available(iOS 16.0, *)
|
||||
extension DuckableContainerViewController: TuskerRootViewController {
|
||||
func stateRestorationActivity() -> NSUserActivity? {
|
||||
(child as? TuskerRootViewController)?.stateRestorationActivity()
|
||||
var activity = (child as? TuskerRootViewController)?.stateRestorationActivity()
|
||||
if let compose = duckedViewController as? ComposeHostingController,
|
||||
compose.draft.hasContent {
|
||||
activity = UserActivityManager.addDuckedDraft(to: activity, draft: compose.draft)
|
||||
}
|
||||
return activity
|
||||
}
|
||||
|
||||
func restoreActivity(_ activity: NSUserActivity) {
|
||||
if let draft = UserActivityManager.getDraft(from: activity),
|
||||
let account = UserActivityManager.getAccount(from: activity) {
|
||||
let mastodonController = MastodonController.getForAccount(account)
|
||||
let compose = ComposeHostingController(draft: draft, mastodonController: mastodonController)
|
||||
_ = presentDuckable(compose, animated: false, isDucked: true)
|
||||
}
|
||||
(child as? TuskerRootViewController)?.restoreActivity(activity)
|
||||
}
|
||||
|
||||
|
|
|
@ -13,11 +13,14 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
|||
weak var mastodonController: MastodonController!
|
||||
|
||||
private var composePlaceholder: UIViewController!
|
||||
private var fastAccountSwitcher: FastAccountSwitcherViewController!
|
||||
|
||||
private var fastAccountSwitcher: FastAccountSwitcherViewController!
|
||||
private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView!
|
||||
private var fastSwitcherConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
@available(iOS, obsoleted: 16.0)
|
||||
private var draftToPresentOnAppear: Draft?
|
||||
|
||||
var selectedTab: Tab {
|
||||
return Tab(rawValue: selectedIndex)!
|
||||
}
|
||||
|
@ -85,6 +88,11 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
|||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
stateRestorationLogger.info("MainTabBarViewController: viewDidAppear, selectedIndex=\(self.selectedIndex, privacy: .public)")
|
||||
|
||||
if let draftToPresentOnAppear {
|
||||
self.draftToPresentOnAppear = nil
|
||||
compose(editing: draftToPresentOnAppear, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
|
@ -235,15 +243,29 @@ extension MainTabBarViewController: TuskerNavigationDelegate {
|
|||
extension MainTabBarViewController: TuskerRootViewController {
|
||||
func stateRestorationActivity() -> NSUserActivity? {
|
||||
let nav = viewController(for: .timelines) as! UINavigationController
|
||||
guard let timelinePages = nav.viewControllers.first as? TimelinesPageViewController,
|
||||
let timelineVC = timelinePages.pageControllers[timelinePages.currentIndex] as? TimelineViewController else {
|
||||
stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration actiivty, couldn't find timeline/page VC")
|
||||
return nil
|
||||
var activity: NSUserActivity?
|
||||
if let timelinePages = nav.viewControllers.first as? TimelinesPageViewController,
|
||||
let timelineVC = timelinePages.pageControllers[timelinePages.currentIndex] as? TimelineViewController {
|
||||
activity = timelineVC.stateRestorationActivity()
|
||||
} else {
|
||||
stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find timeline/page VC")
|
||||
}
|
||||
return timelineVC.stateRestorationActivity()
|
||||
if let presentedNav = presentedViewController as? UINavigationController,
|
||||
let compose = presentedNav.viewControllers.first as? ComposeHostingController {
|
||||
activity = UserActivityManager.addEditedDraft(to: activity, draft: compose.draft)
|
||||
}
|
||||
return activity
|
||||
}
|
||||
|
||||
func restoreActivity(_ activity: NSUserActivity) {
|
||||
func restoreEditedDraft() {
|
||||
// on iOS 16+, this is handled by the duckable container
|
||||
if #unavailable(iOS 16.0),
|
||||
let draft = UserActivityManager.getDraft(from: activity) {
|
||||
draftToPresentOnAppear = draft
|
||||
}
|
||||
}
|
||||
|
||||
if activity.activityType == UserActivityType.showTimeline.rawValue {
|
||||
let nav = viewController(for: .timelines) as! UINavigationController
|
||||
guard let timelinePages = nav.viewControllers.first as? TimelinesPageViewController,
|
||||
|
@ -252,6 +274,10 @@ extension MainTabBarViewController: TuskerRootViewController {
|
|||
return
|
||||
}
|
||||
timelineVC.restoreActivity(activity)
|
||||
restoreEditedDraft()
|
||||
} else if activity.activityType == UserActivityType.newPost.rawValue {
|
||||
restoreEditedDraft()
|
||||
return
|
||||
} else {
|
||||
stateRestorationLogger.fault("MainTabBarViewController: Unable to restore activity of unexpected type \(activity.activityType, privacy: .public)")
|
||||
}
|
||||
|
|
|
@ -91,10 +91,37 @@ class UserActivityManager {
|
|||
return activity
|
||||
}
|
||||
|
||||
static func addDuckedDraft(to activity: NSUserActivity?, draft: Draft) -> NSUserActivity {
|
||||
if let activity {
|
||||
activity.addUserInfoEntries(from: [
|
||||
"duckedDraftID": draft.id.uuidString
|
||||
])
|
||||
return activity
|
||||
} else {
|
||||
return editDraftActivity(id: draft.id, accountID: draft.accountID)
|
||||
}
|
||||
}
|
||||
|
||||
static func addEditedDraft(to activity: NSUserActivity?, draft: Draft) -> NSUserActivity {
|
||||
if let activity {
|
||||
activity.addUserInfoEntries(from: [
|
||||
"editedDraftID": draft.id.uuidString
|
||||
])
|
||||
return activity
|
||||
} else {
|
||||
return editDraftActivity(id: draft.id, accountID: draft.accountID)
|
||||
}
|
||||
}
|
||||
|
||||
static func getDraft(from activity: NSUserActivity) -> Draft? {
|
||||
guard activity.activityType == UserActivityType.newPost.rawValue,
|
||||
let str = activity.userInfo?["draftID"] as? String,
|
||||
let uuid = UUID(uuidString: str) else {
|
||||
let idStr: String?
|
||||
if activity.activityType == UserActivityType.newPost.rawValue {
|
||||
idStr = activity.userInfo?["draftID"] as? String
|
||||
} else {
|
||||
idStr = activity.userInfo?["duckedDraftID"] as? String ?? activity.userInfo?["editedDraftID"] as? String
|
||||
}
|
||||
guard let idStr,
|
||||
let uuid = UUID(uuidString: idStr) else {
|
||||
return nil
|
||||
}
|
||||
return DraftsManager.shared.getBy(id: uuid)
|
||||
|
|
|
@ -88,7 +88,7 @@ extension TuskerNavigationDelegate {
|
|||
show(conversation(mainStatusID: statusID, state: state), sender: self)
|
||||
}
|
||||
|
||||
func compose(editing draft: Draft) {
|
||||
func compose(editing draft: Draft, animated: Bool = true) {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
let compose = UserActivityManager.editDraftActivity(id: draft.id, accountID: apiController.accountInfo!.id)
|
||||
let options = UIWindowScene.ActivationRequestOptions()
|
||||
|
@ -97,20 +97,20 @@ extension TuskerNavigationDelegate {
|
|||
} else {
|
||||
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
||||
if #available(iOS 16.0, *),
|
||||
presentDuckable(compose) {
|
||||
presentDuckable(compose, animated: animated) {
|
||||
return
|
||||
} else {
|
||||
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
||||
let nav = UINavigationController(rootViewController: compose)
|
||||
nav.presentationController?.delegate = compose
|
||||
present(nav, animated: true)
|
||||
present(nav, animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil) {
|
||||
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil, animated: Bool = true) {
|
||||
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
|
||||
compose(editing: draft)
|
||||
compose(editing: draft, animated: animated)
|
||||
}
|
||||
|
||||
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController {
|
||||
|
|
Loading…
Reference in New Issue