mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2024-12-23 07:26:34 +01:00
feat: update wizard for new iPad design
This commit is contained in:
parent
30b2a35b84
commit
865718351d
@ -315,12 +315,12 @@
|
||||
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F726391D660018D199 /* MediaPreviewingViewController.swift */; };
|
||||
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */; };
|
||||
DB63BE7F268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63BE7E268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift */; };
|
||||
DB647C5726F1E97300F7F82C /* MainTabBarController+Wizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB647C5626F1E97300F7F82C /* MainTabBarController+Wizard.swift */; };
|
||||
DB647C5926F1EA2700F7F82C /* WizardPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB647C5826F1EA2700F7F82C /* WizardPreference.swift */; };
|
||||
DB66728C25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */; };
|
||||
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; };
|
||||
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; };
|
||||
DB67D08427312970006A36CF /* APIService+Following.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67D08327312970006A36CF /* APIService+Following.swift */; };
|
||||
DB67D08627312E67006A36CF /* WizardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67D08527312E67006A36CF /* WizardViewController.swift */; };
|
||||
DB68045B2636DC6A00430867 /* MastodonNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68045A2636DC6A00430867 /* MastodonNotification.swift */; };
|
||||
DB6804662636DC9000430867 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; };
|
||||
DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68045A2636DC6A00430867 /* MastodonNotification.swift */; };
|
||||
@ -1135,12 +1135,12 @@
|
||||
DB6180F726391D660018D199 /* MediaPreviewingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewingViewController.swift; sourceTree = "<group>"; };
|
||||
DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewViewModel.swift; sourceTree = "<group>"; };
|
||||
DB63BE7E268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewController+StatusProvider.swift"; sourceTree = "<group>"; };
|
||||
DB647C5626F1E97300F7F82C /* MainTabBarController+Wizard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainTabBarController+Wizard.swift"; sourceTree = "<group>"; };
|
||||
DB647C5826F1EA2700F7F82C /* WizardPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardPreference.swift; sourceTree = "<group>"; };
|
||||
DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+DataSource.swift"; sourceTree = "<group>"; };
|
||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusSection.swift; sourceTree = "<group>"; };
|
||||
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusItem.swift; sourceTree = "<group>"; };
|
||||
DB67D08327312970006A36CF /* APIService+Following.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Following.swift"; sourceTree = "<group>"; };
|
||||
DB67D08527312E67006A36CF /* WizardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardViewController.swift; sourceTree = "<group>"; };
|
||||
DB68045A2636DC6A00430867 /* MastodonNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonNotification.swift; sourceTree = "<group>"; };
|
||||
DB68047F2637CD4C00430867 /* AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DB6804812637CD4C00430867 /* AppShared.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppShared.h; sourceTree = "<group>"; };
|
||||
@ -2529,6 +2529,14 @@
|
||||
path = Image;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB67D08727312E6A006A36CF /* Wizard */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB67D08527312E67006A36CF /* WizardViewController.swift */,
|
||||
);
|
||||
path = Wizard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB6804802637CD4C00430867 /* AppShared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -2770,7 +2778,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */,
|
||||
DB647C5626F1E97300F7F82C /* MainTabBarController+Wizard.swift */,
|
||||
);
|
||||
path = MainTab;
|
||||
sourceTree = "<group>";
|
||||
@ -2782,6 +2789,7 @@
|
||||
DB6180E426391A500018D199 /* Transition */,
|
||||
DB852D1D26FB021900FC9D81 /* Root */,
|
||||
DB01409B25C40BB600F9F3CF /* Onboarding */,
|
||||
DB67D08727312E6A006A36CF /* Wizard */,
|
||||
DB9F58ED26EF435800E7BBE9 /* Account */,
|
||||
2D38F1D325CD463600561493 /* HomeTimeline */,
|
||||
2D76316325C14BAC00929FB9 /* PublicTimeline */,
|
||||
@ -4358,6 +4366,7 @@
|
||||
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
|
||||
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
|
||||
DBBC24C026A5443100398BB9 /* MastodonTheme.swift in Sources */,
|
||||
DB67D08627312E67006A36CF /* WizardViewController.swift in Sources */,
|
||||
DBBC24B526A540AE00398BB9 /* AvatarConfigurableView.swift in Sources */,
|
||||
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
|
||||
DBFEF07726A691FB006D7ED1 /* MastodonAuthenticationBox.swift in Sources */,
|
||||
@ -4371,7 +4380,6 @@
|
||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
||||
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */,
|
||||
DB6D9F6F2635807F008423CD /* Setting.swift in Sources */,
|
||||
DB647C5726F1E97300F7F82C /* MainTabBarController+Wizard.swift in Sources */,
|
||||
DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */,
|
||||
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
|
||||
DB6B74F8272FBFB100C70B6E /* FollowerListViewController+Provider.swift in Sources */,
|
||||
|
@ -23,6 +23,7 @@ final public class SceneCoordinator {
|
||||
|
||||
private(set) weak var tabBarController: MainTabBarController!
|
||||
private(set) weak var splitViewController: RootSplitViewController?
|
||||
private(set) var wizardViewController: WizardViewController?
|
||||
|
||||
private(set) var secondaryStackHashValues = Set<Int>()
|
||||
|
||||
@ -221,17 +222,34 @@ extension SceneCoordinator {
|
||||
extension SceneCoordinator {
|
||||
|
||||
func setup() {
|
||||
let rootViewController: UIViewController
|
||||
switch UIDevice.current.userInterfaceIdiom {
|
||||
case .phone:
|
||||
let viewController = MainTabBarController(context: appContext, coordinator: self)
|
||||
sceneDelegate.window?.rootViewController = viewController
|
||||
tabBarController = viewController
|
||||
self.splitViewController = nil
|
||||
self.tabBarController = viewController
|
||||
rootViewController = viewController
|
||||
default:
|
||||
let splitViewController = RootSplitViewController(context: appContext, coordinator: self)
|
||||
self.splitViewController = splitViewController
|
||||
self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
|
||||
sceneDelegate.window?.rootViewController = splitViewController
|
||||
rootViewController = splitViewController
|
||||
}
|
||||
|
||||
let wizardViewController = WizardViewController()
|
||||
if !wizardViewController.items.isEmpty,
|
||||
let delegate = rootViewController as? WizardViewControllerDelegate
|
||||
{
|
||||
// do not add as child view controller.
|
||||
// otherwise, the tab bar controller will add as a new tab
|
||||
wizardViewController.delegate = delegate
|
||||
wizardViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
wizardViewController.view.frame = rootViewController.view.bounds
|
||||
rootViewController.view.addSubview(wizardViewController.view)
|
||||
self.wizardViewController = wizardViewController
|
||||
}
|
||||
|
||||
sceneDelegate.window?.rootViewController = rootViewController
|
||||
}
|
||||
|
||||
func setupOnboardingIfNeeds(animated: Bool) {
|
||||
|
@ -96,6 +96,13 @@ extension WizardCardView {
|
||||
path.addArc(withCenter: CGPoint(x: rect.minX, y: rect.minY), radius: radius, startAngle: -.pi / 2, endAngle: -.pi, clockwise: false)
|
||||
path.addLine(to: CGPoint(x: rect.minX - radius, y: rect.maxY + radius + WizardCardView.bubbleArrowHeight))
|
||||
path.close()
|
||||
case .allCorners: // no arrow
|
||||
path.move(to: CGPoint(x: rect.maxX, y: rect.maxY + radius))
|
||||
path.addArc(withCenter: CGPoint(x: rect.minX, y: rect.maxY), radius: radius, startAngle: .pi / 2, endAngle: .pi, clockwise: true)
|
||||
path.addArc(withCenter: CGPoint(x: rect.minX, y: rect.minY), radius: radius, startAngle: .pi, endAngle: .pi / 2 * 3, clockwise: true)
|
||||
path.addArc(withCenter: CGPoint(x: rect.maxX, y: rect.minY), radius: radius, startAngle: .pi / 2 * 3, endAngle: .pi * 2, clockwise: true)
|
||||
path.addArc(withCenter: CGPoint(x: rect.maxX, y: rect.maxY), radius: radius, startAngle: .pi * 2, endAngle: .pi / 2 * 5, clockwise: true)
|
||||
path.close()
|
||||
default:
|
||||
assertionFailure("FIXME")
|
||||
}
|
||||
|
@ -1,137 +0,0 @@
|
||||
//
|
||||
// MainTabBarController+Wizard.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-9-15.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
|
||||
protocol WizardDelegate: AnyObject {
|
||||
func spotlight(item: MainTabBarController.Wizard.Item) -> UIBezierPath
|
||||
func layoutWizardCard(_ wizard: MainTabBarController.Wizard, item: MainTabBarController.Wizard.Item)
|
||||
}
|
||||
|
||||
extension MainTabBarController {
|
||||
class Wizard {
|
||||
|
||||
let logger = Logger(subsystem: "Wizard", category: "UI")
|
||||
|
||||
weak var delegate: WizardDelegate?
|
||||
|
||||
private(set) var items: [Item]
|
||||
|
||||
let backgroundView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.black.withAlphaComponent(0.7)
|
||||
return view
|
||||
}()
|
||||
|
||||
init() {
|
||||
var items: [Item] = []
|
||||
if !UserDefaults.shared.didShowMultipleAccountSwitchWizard {
|
||||
items.append(.multipleAccountSwitch)
|
||||
}
|
||||
self.items = items
|
||||
|
||||
let backgroundTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
backgroundTapGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.Wizard.backgroundTapGestureRecognizerHandler(_:)))
|
||||
backgroundView.addGestureRecognizer(backgroundTapGestureRecognizer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MainTabBarController.Wizard {
|
||||
enum Item {
|
||||
case multipleAccountSwitch
|
||||
|
||||
var title: String {
|
||||
return L10n.Scene.Wizard.newInMastodon
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .multipleAccountSwitch:
|
||||
return L10n.Scene.Wizard.multipleAccountSwitchIntroDescription
|
||||
}
|
||||
}
|
||||
|
||||
func markAsRead() {
|
||||
switch self {
|
||||
case .multipleAccountSwitch:
|
||||
UserDefaults.shared.didShowMultipleAccountSwitchWizard = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MainTabBarController.Wizard {
|
||||
|
||||
func setup(in view: UIView) {
|
||||
assert(delegate != nil, "need set delegate before use")
|
||||
|
||||
guard !items.isEmpty else { return }
|
||||
|
||||
backgroundView.frame = view.bounds
|
||||
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(backgroundView)
|
||||
NSLayoutConstraint.activate([
|
||||
backgroundView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
func consume() {
|
||||
guard !items.isEmpty else {
|
||||
backgroundView.removeFromSuperview()
|
||||
return
|
||||
}
|
||||
let item = items.removeFirst()
|
||||
perform(item: item)
|
||||
}
|
||||
|
||||
private func perform(item: Item) {
|
||||
guard let delegate = delegate else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
// prepare for reuse
|
||||
prepareForReuse()
|
||||
|
||||
// set wizard item read
|
||||
item.markAsRead()
|
||||
|
||||
// add spotlight
|
||||
let spotlight = delegate.spotlight(item: item)
|
||||
let maskLayer = CAShapeLayer()
|
||||
let path = UIBezierPath(rect: backgroundView.bounds)
|
||||
path.append(spotlight)
|
||||
maskLayer.fillRule = .evenOdd
|
||||
maskLayer.path = path.cgPath
|
||||
backgroundView.layer.mask = maskLayer
|
||||
|
||||
// layout wizard card
|
||||
delegate.layoutWizardCard(self, item: item)
|
||||
}
|
||||
|
||||
private func prepareForReuse() {
|
||||
backgroundView.subviews.forEach { subview in
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
backgroundView.mask = nil
|
||||
backgroundView.layer.mask = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MainTabBarController.Wizard {
|
||||
@objc private func backgroundTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
|
||||
consume()
|
||||
}
|
||||
}
|
@ -21,8 +21,6 @@ class MainTabBarController: UITabBarController {
|
||||
|
||||
static let avatarButtonSize = CGSize(width: 25, height: 25)
|
||||
let avatarButton = CircleAvatarButton()
|
||||
|
||||
let wizard = Wizard()
|
||||
|
||||
var currentTab = CurrentValueSubject<Tab, Never>(.home)
|
||||
|
||||
@ -108,6 +106,8 @@ class MainTabBarController: UITabBarController {
|
||||
|
||||
var _viewControllers: [UIViewController] = []
|
||||
|
||||
private(set) var isReadyForWizardAvatarButton = false
|
||||
|
||||
init(context: AppContext, coordinator: SceneCoordinator) {
|
||||
self.context = context
|
||||
self.coordinator = coordinator
|
||||
@ -247,9 +247,6 @@ extension MainTabBarController {
|
||||
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
wizard.delegate = self
|
||||
wizard.setup(in: view)
|
||||
|
||||
let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer()
|
||||
tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:)))
|
||||
@ -265,7 +262,7 @@ extension MainTabBarController {
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
wizard.consume()
|
||||
isReadyForWizardAvatarButton = true
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
@ -377,51 +374,57 @@ extension MainTabBarController: UITabBarControllerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - WizardDataSource
|
||||
extension MainTabBarController: WizardDelegate {
|
||||
func spotlight(item: Wizard.Item) -> UIBezierPath {
|
||||
// MARK: - WizardViewControllerDelegate
|
||||
extension MainTabBarController: WizardViewControllerDelegate {
|
||||
func readyToLayoutItem(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> Bool {
|
||||
switch item {
|
||||
case .multipleAccountSwitch:
|
||||
guard let avatarButtonFrameInView = avatarButtonFrameInView() else {
|
||||
return UIBezierPath()
|
||||
}
|
||||
return UIBezierPath(ovalIn: avatarButtonFrameInView)
|
||||
|
||||
return isReadyForWizardAvatarButton
|
||||
}
|
||||
}
|
||||
|
||||
func layoutWizardCard(_ wizard: MainTabBarController.Wizard, item: Wizard.Item) {
|
||||
func layoutSpotlight(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> UIBezierPath {
|
||||
switch item {
|
||||
case .multipleAccountSwitch:
|
||||
guard let avatarButtonFrameInView = avatarButtonFrameInView() else {
|
||||
guard let avatarButtonFrameInView = avatarButtonFrameInWizardView(wizardView: wizardViewController.view) else {
|
||||
return UIBezierPath()
|
||||
}
|
||||
return UIBezierPath(ovalIn: avatarButtonFrameInView)
|
||||
}
|
||||
}
|
||||
|
||||
func layoutWizardCard(_ wizardViewController: WizardViewController, item: WizardViewController.Item) {
|
||||
switch item {
|
||||
case .multipleAccountSwitch:
|
||||
guard let avatarButtonFrameInView = avatarButtonFrameInWizardView(wizardView: wizardViewController.view) else {
|
||||
return
|
||||
}
|
||||
let anchorView = UIView()
|
||||
anchorView.frame = avatarButtonFrameInView
|
||||
wizard.backgroundView.addSubview(anchorView)
|
||||
wizardViewController.backgroundView.addSubview(anchorView)
|
||||
|
||||
let wizardCardView = WizardCardView()
|
||||
wizardCardView.arrowRectCorner = view.traitCollection.layoutDirection == .leftToRight ? .bottomRight : .bottomLeft
|
||||
wizardCardView.titleLabel.text = item.title
|
||||
wizardCardView.descriptionLabel.text = item.description
|
||||
|
||||
|
||||
wizardCardView.translatesAutoresizingMaskIntoConstraints = false
|
||||
wizard.backgroundView.addSubview(wizardCardView)
|
||||
wizardViewController.backgroundView.addSubview(wizardCardView)
|
||||
NSLayoutConstraint.activate([
|
||||
anchorView.topAnchor.constraint(equalTo: wizardCardView.bottomAnchor, constant: 13), // 13pt spacing
|
||||
wizardCardView.trailingAnchor.constraint(equalTo: anchorView.centerXAnchor),
|
||||
wizardCardView.widthAnchor.constraint(equalTo: wizard.backgroundView.widthAnchor, multiplier: 2.0/3.0).priority(.required - 1),
|
||||
wizardCardView.widthAnchor.constraint(equalTo: wizardViewController.view.widthAnchor, multiplier: 2.0/3.0).priority(.required - 1),
|
||||
])
|
||||
wizardCardView.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
}
|
||||
}
|
||||
|
||||
private func avatarButtonFrameInView() -> CGRect? {
|
||||
private func avatarButtonFrameInWizardView(wizardView: UIView) -> CGRect? {
|
||||
guard let superview = avatarButton.superview else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
return superview.convert(avatarButton.frame, to: view)
|
||||
return superview.convert(avatarButton.frame, to: wizardView)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,3 +241,90 @@ extension RootSplitViewController: UISplitViewControllerDelegate {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - WizardViewControllerDelegate
|
||||
extension RootSplitViewController: WizardViewControllerDelegate {
|
||||
|
||||
func readyToLayoutItem(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> Bool {
|
||||
guard traitCollection.horizontalSizeClass != .compact else {
|
||||
return compactMainTabBarViewController.readyToLayoutItem(wizardViewController, item: item)
|
||||
}
|
||||
|
||||
switch item {
|
||||
case .multipleAccountSwitch:
|
||||
return contentSplitViewController.sidebarViewController.viewModel.isReadyForWizardAvatarButton
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func layoutSpotlight(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> UIBezierPath {
|
||||
guard traitCollection.horizontalSizeClass != .compact else {
|
||||
return compactMainTabBarViewController.layoutSpotlight(wizardViewController, item: item)
|
||||
}
|
||||
|
||||
switch item {
|
||||
case .multipleAccountSwitch:
|
||||
guard let frame = avatarButtonFrameInWizardView(wizardView: wizardViewController.view)
|
||||
else {
|
||||
assertionFailure()
|
||||
return UIBezierPath()
|
||||
}
|
||||
return UIBezierPath(ovalIn: frame)
|
||||
}
|
||||
}
|
||||
|
||||
func layoutWizardCard(_ wizardViewController: WizardViewController, item: WizardViewController.Item) {
|
||||
guard traitCollection.horizontalSizeClass != .compact else {
|
||||
return compactMainTabBarViewController.layoutWizardCard(wizardViewController, item: item)
|
||||
}
|
||||
|
||||
guard let frame = avatarButtonFrameInWizardView(wizardView: wizardViewController.view) else {
|
||||
return
|
||||
}
|
||||
|
||||
let anchorView = UIView()
|
||||
anchorView.frame = frame
|
||||
wizardViewController.backgroundView.addSubview(anchorView)
|
||||
|
||||
let wizardCardView = WizardCardView()
|
||||
wizardCardView.arrowRectCorner = .allCorners // no arrow
|
||||
wizardCardView.titleLabel.text = item.title
|
||||
wizardCardView.descriptionLabel.text = item.description
|
||||
|
||||
wizardCardView.translatesAutoresizingMaskIntoConstraints = false
|
||||
wizardViewController.backgroundView.addSubview(wizardCardView)
|
||||
NSLayoutConstraint.activate([
|
||||
wizardCardView.centerYAnchor.constraint(equalTo: anchorView.centerYAnchor),
|
||||
wizardCardView.leadingAnchor.constraint(equalTo: anchorView.trailingAnchor, constant: 20), // 20pt spacing
|
||||
wizardCardView.widthAnchor.constraint(equalToConstant: 320),
|
||||
])
|
||||
wizardCardView.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
}
|
||||
|
||||
private func avatarButtonFrameInWizardView(wizardView: UIView) -> CGRect? {
|
||||
guard let diffableDataSource = contentSplitViewController.sidebarViewController.viewModel.diffableDataSource,
|
||||
let indexPath = diffableDataSource.indexPath(for: .tab(.me)),
|
||||
let cell = contentSplitViewController.sidebarViewController.collectionView.cellForItem(at: indexPath) as? SidebarListCollectionViewCell,
|
||||
let contentView = cell._contentView,
|
||||
let frame = sourceViewFrameInTargetView(
|
||||
sourceView: contentView.avatarButton,
|
||||
targetView: wizardView
|
||||
)
|
||||
else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
private func sourceViewFrameInTargetView(
|
||||
sourceView: UIView,
|
||||
targetView: UIView
|
||||
) -> CGRect? {
|
||||
guard let superview = sourceView.superview else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
return superview.convert(sourceView.frame, to: targetView)
|
||||
}
|
||||
}
|
||||
|
@ -125,6 +125,7 @@ extension SidebarViewController {
|
||||
|
||||
secondaryCollectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] secondaryCollectionView, _ in
|
||||
guard let self = self else { return }
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): secondaryCollectionView contentSize: \(secondaryCollectionView.contentSize.debugDescription)")
|
||||
let height = secondaryCollectionView.contentSize.height
|
||||
self.secondaryCollectionViewHeightLayoutConstraint.constant = height
|
||||
self.collectionView.contentInset.bottom = height
|
||||
|
@ -23,6 +23,7 @@ final class SidebarViewModel {
|
||||
// output
|
||||
var diffableDataSource: UICollectionViewDiffableDataSource<Section, Item>?
|
||||
var secondaryDiffableDataSource: UICollectionViewDiffableDataSource<Section, Item>?
|
||||
private(set) var isReadyForWizardAvatarButton = false
|
||||
|
||||
let activeMastodonAuthenticationObjectID = CurrentValueSubject<NSManagedObjectID?, Never>(nil)
|
||||
|
||||
@ -170,8 +171,12 @@ extension SidebarViewModel {
|
||||
.setting,
|
||||
]
|
||||
sectionSnapshot.append(items, to: nil)
|
||||
_diffableDataSource.apply(sectionSnapshot, to: .main)
|
||||
|
||||
// animatingDifferences must to be `true`
|
||||
// otherwise the UI layout will infinity loop
|
||||
_diffableDataSource.apply(sectionSnapshot, to: .main, animatingDifferences: true) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.isReadyForWizardAvatarButton = true
|
||||
}
|
||||
|
||||
// secondary
|
||||
let _secondaryDiffableDataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: secondaryCollectionView) { collectionView, indexPath, item in
|
||||
|
211
Mastodon/Scene/Wizard/WizardViewController.swift
Normal file
211
Mastodon/Scene/Wizard/WizardViewController.swift
Normal file
@ -0,0 +1,211 @@
|
||||
//
|
||||
// WizardViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-11-2.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
protocol WizardViewControllerDelegate: AnyObject {
|
||||
func readyToLayoutItem(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> Bool
|
||||
func layoutSpotlight(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> UIBezierPath
|
||||
func layoutWizardCard(_ wizardViewController: WizardViewController, item: WizardViewController.Item)
|
||||
}
|
||||
|
||||
class WizardViewController: UIViewController {
|
||||
|
||||
let logger = Logger(subsystem: "Wizard", category: "UI")
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
weak var delegate: WizardViewControllerDelegate?
|
||||
|
||||
private(set) var items: [Item] = {
|
||||
var items: [Item] = []
|
||||
if !UserDefaults.shared.didShowMultipleAccountSwitchWizard {
|
||||
items.append(.multipleAccountSwitch)
|
||||
}
|
||||
return items
|
||||
}()
|
||||
|
||||
let pendingItem = CurrentValueSubject<Item?, Never>(nil)
|
||||
let currentItem = CurrentValueSubject<Item?, Never>(nil)
|
||||
|
||||
let backgroundView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.black.withAlphaComponent(0.7)
|
||||
return view
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension WizardViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setup()
|
||||
|
||||
let backgroundTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
backgroundTapGestureRecognizer.addTarget(self, action: #selector(WizardViewController.backgroundTapGestureRecognizerHandler(_:)))
|
||||
backgroundView.addGestureRecognizer(backgroundTapGestureRecognizer)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
// Create a timer to consume pending item
|
||||
Timer.publish(every: 0.5, on: .main, in: .default)
|
||||
.autoconnect()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
guard self.pendingItem.value != nil else { return }
|
||||
self.consume()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
consume()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
invalidLayoutForCurrentItem()
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
coordinator.animate { context in
|
||||
|
||||
} completion: { [weak self] context in
|
||||
guard let self = self else { return }
|
||||
self.invalidLayoutForCurrentItem()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension WizardViewController {
|
||||
enum Item {
|
||||
case multipleAccountSwitch
|
||||
|
||||
var title: String {
|
||||
return L10n.Scene.Wizard.newInMastodon
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .multipleAccountSwitch:
|
||||
return L10n.Scene.Wizard.multipleAccountSwitchIntroDescription
|
||||
}
|
||||
}
|
||||
|
||||
func markAsRead() {
|
||||
switch self {
|
||||
case .multipleAccountSwitch:
|
||||
UserDefaults.shared.didShowMultipleAccountSwitchWizard = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WizardViewController {
|
||||
|
||||
func setup() {
|
||||
assert(delegate != nil, "need set delegate before use")
|
||||
|
||||
guard !items.isEmpty else { return }
|
||||
|
||||
backgroundView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
backgroundView.frame = view.bounds
|
||||
view.addSubview(backgroundView)
|
||||
}
|
||||
|
||||
func destroy() {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
|
||||
func consume() {
|
||||
guard !items.isEmpty else {
|
||||
destroy()
|
||||
return
|
||||
}
|
||||
|
||||
guard let first = items.first else { return }
|
||||
guard delegate?.readyToLayoutItem(self, item: first) == true else {
|
||||
pendingItem.value = first
|
||||
return
|
||||
}
|
||||
pendingItem.value = nil
|
||||
currentItem.value = nil
|
||||
|
||||
let item = items.removeFirst()
|
||||
perform(item: item)
|
||||
}
|
||||
|
||||
private func perform(item: Item) {
|
||||
guard let delegate = delegate else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
// prepare for reuse
|
||||
prepareForReuse()
|
||||
|
||||
// set wizard item read
|
||||
item.markAsRead()
|
||||
|
||||
// add spotlight
|
||||
let spotlight = delegate.layoutSpotlight(self, item: item)
|
||||
let maskLayer = CAShapeLayer()
|
||||
// expand rect to make sure view always fill the screen when device rotate
|
||||
let expandRect: CGRect = {
|
||||
var rect = backgroundView.bounds
|
||||
rect.size.width *= 2
|
||||
rect.size.height *= 2
|
||||
return rect
|
||||
}()
|
||||
let path = UIBezierPath(rect: expandRect)
|
||||
path.append(spotlight)
|
||||
maskLayer.fillRule = .evenOdd
|
||||
maskLayer.path = path.cgPath
|
||||
backgroundView.layer.mask = maskLayer
|
||||
|
||||
// layout wizard card
|
||||
delegate.layoutWizardCard(self, item: item)
|
||||
|
||||
currentItem.value = item
|
||||
}
|
||||
|
||||
private func prepareForReuse() {
|
||||
backgroundView.subviews.forEach { subview in
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
backgroundView.mask = nil
|
||||
backgroundView.layer.mask = nil
|
||||
}
|
||||
|
||||
private func invalidLayoutForCurrentItem() {
|
||||
if let item = currentItem.value {
|
||||
perform(item: item)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension WizardViewController {
|
||||
@objc private func backgroundTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
|
||||
consume()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user