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