feat: add toolbar for compose scene

This commit is contained in:
CMK 2021-03-12 15:23:28 +08:00
parent d9e2453464
commit 1746c1fc77
5 changed files with 269 additions and 8 deletions

View File

@ -203,6 +203,7 @@
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; };
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; };
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; };
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; };
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
@ -466,6 +467,7 @@
DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = "<group>"; };
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
@ -984,6 +986,14 @@
path = Preference;
sourceTree = "<group>";
};
DB55D32225FB4D320002F825 /* View */ = {
isa = PBXGroup;
children = (
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
);
path = View;
sourceTree = "<group>";
};
DB68A03825E900CC00CFDF14 /* Share */ = {
isa = PBXGroup;
children = (
@ -1022,6 +1032,7 @@
DB789A1025F9F29B0071ACA0 /* Compose */ = {
isa = PBXGroup;
children = (
DB55D32225FB4D320002F825 /* View */,
DB789A2125F9F76D0071ACA0 /* TableViewCell */,
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
@ -1731,6 +1742,7 @@
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,

View File

@ -9,6 +9,7 @@ import os.log
import UIKit
import Combine
import TwitterTextEditor
import KeyboardGuide
final class ComposeViewController: UIViewController, NeedsDependency {
@ -41,6 +42,16 @@ final class ComposeViewController: UIViewController, NeedsDependency {
return tableView
}()
let composeToolbarView: ComposeToolbarView = {
let composeToolbarView = ComposeToolbarView()
return composeToolbarView
}()
var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
let composeToolbarBackgroundView: UIView = {
let backgroundView = UIView()
return backgroundView
}()
}
extension ComposeViewController {
@ -69,6 +80,60 @@ extension ComposeViewController {
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
composeToolbarView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(composeToolbarView)
composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor)
NSLayoutConstraint.activate([
composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
composeToolbarViewBottomLayoutConstraint,
composeToolbarView.heightAnchor.constraint(equalToConstant: 44),
])
composeToolbarView.preservesSuperviewLayoutMargins = true
composeToolbarView.delegate = self
// respond scrollView overlap change
view.layoutIfNeeded()
Publishers.CombineLatest3(
KeyboardResponderService.shared.isShow.eraseToAnyPublisher(),
KeyboardResponderService.shared.state.eraseToAnyPublisher(),
KeyboardResponderService.shared.endFrame.eraseToAnyPublisher()
)
.sink(receiveValue: { [weak self] isShow, state, endFrame in
guard let self = self else { return }
guard isShow, state == .dock else {
self.tableView.contentInset.bottom = 0.0
self.tableView.verticalScrollIndicatorInsets.bottom = 0.0
UIView.animate(withDuration: 0.3) {
self.composeToolbarViewBottomLayoutConstraint.constant = 0.0
self.view.layoutIfNeeded()
}
return
}
// isShow AND dock state
let contentFrame = self.view.convert(self.tableView.frame, to: nil)
let padding = contentFrame.maxY - endFrame.minY
guard padding > 0 else {
self.tableView.contentInset.bottom = 0.0
self.tableView.verticalScrollIndicatorInsets.bottom = 0.0
UIView.animate(withDuration: 0.3) {
self.composeToolbarViewBottomLayoutConstraint.constant = 0.0
self.view.layoutIfNeeded()
}
return
}
self.tableView.contentInset.bottom = padding
self.tableView.verticalScrollIndicatorInsets.bottom = padding
UIView.animate(withDuration: 0.3) {
self.composeToolbarViewBottomLayoutConstraint.constant = padding
self.view.layoutIfNeeded()
}
})
.store(in: &disposeBag)
tableView.delegate = self
viewModel.setupDiffableDataSource(for: tableView, dependency: self)
}
@ -123,6 +188,31 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
}
// MARK: - ComposeToolbarViewDelegate
extension ComposeViewController: ComposeToolbarViewDelegate {
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, locationButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
// MARK: - UITableViewDelegate
extension ComposeViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
@ -132,6 +222,14 @@ extension ComposeViewController: UITableViewDelegate {
// MARK: - ComposeViewController
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
// func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
// switch traitCollection.userInterfaceIdiom {
// case .phone:
// return .fullScreen
// default:
// return .pageSheet
// }
// }
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return viewModel.shouldDismiss.value

View File

@ -0,0 +1,154 @@
//
// ComposeToolbarView.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-12.
//
import UIKit
protocol ComposeToolbarViewDelegate: class {
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, locationButtonDidPressed sender: UIButton)
}
final class ComposeToolbarView: UIView {
weak var delegate: ComposeToolbarViewDelegate?
let mediaButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
return button
}()
let pollButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
button.setImage(UIImage(systemName: "list.bullet", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)), for: .normal)
return button
}()
let emojiButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
button.setImage(UIImage(systemName: "face.smiling", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
return button
}()
let contentWarningButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
button.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
return button
}()
let visibilityButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
button.setImage(UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension ComposeToolbarView {
private func _init() {
backgroundColor = .secondarySystemBackground
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 0
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
layoutMarginsGuide.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 8), // tweak button margin offset
])
let buttons = [
mediaButton,
pollButton,
emojiButton,
contentWarningButton,
visibilityButton,
]
buttons.forEach { button in
button.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(button)
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: 44),
button.heightAnchor.constraint(equalToConstant: 44),
])
}
mediaButton.addTarget(self, action: #selector(ComposeToolbarView.cameraButtonDidPressed(_:)), for: .touchUpInside)
pollButton.addTarget(self, action: #selector(ComposeToolbarView.gifButtonDidPressed(_:)), for: .touchUpInside)
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.atButtonDidPressed(_:)), for: .touchUpInside)
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.topicButtonDidPressed(_:)), for: .touchUpInside)
visibilityButton.addTarget(self, action: #selector(ComposeToolbarView.locationButtonDidPressed(_:)), for: .touchUpInside)
}
}
extension ComposeToolbarView {
@objc private func cameraButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, cameraButtonDidPressed: sender)
}
@objc private func gifButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, gifButtonDidPressed: sender)
}
@objc private func atButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, atButtonDidPressed: sender)
}
@objc private func topicButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, topicButtonDidPressed: sender)
}
@objc private func locationButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, locationButtonDidPressed: sender)
}
}
#if canImport(SwiftUI) && DEBUG
import SwiftUI
struct ComposeToolbarView_Previews: PreviewProvider {
static var previews: some View {
UIViewPreview(width: 375) {
let tootbarView = ComposeToolbarView()
tootbarView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tootbarView.widthAnchor.constraint(equalToConstant: 375).priority(.defaultHigh),
tootbarView.heightAnchor.constraint(equalToConstant: 64).priority(.defaultHigh),
])
return tootbarView
}
.previewLayout(.fixed(width: 375, height: 100))
}
}
#endif

View File

@ -351,7 +351,7 @@ extension MastodonRegisterViewController {
Publishers.CombineLatest(
KeyboardResponderService.shared.state.eraseToAnyPublisher(),
KeyboardResponderService.shared.willEndFrame.eraseToAnyPublisher()
KeyboardResponderService.shared.endFrame.eraseToAnyPublisher()
)
.sink(receiveValue: { [weak self] state, endFrame in
guard let self = self else { return }

View File

@ -18,8 +18,7 @@ final class KeyboardResponderService {
// output
let isShow = CurrentValueSubject<Bool, Never>(false)
let state = CurrentValueSubject<KeyboardState, Never>(.none)
let didEndFrame = CurrentValueSubject<CGRect, Never>(.zero)
let willEndFrame = CurrentValueSubject<CGRect, Never>(.zero)
let endFrame = CurrentValueSubject<CGRect, Never>(.zero)
private init() {
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification, object: nil)
@ -38,15 +37,11 @@ final class KeyboardResponderService {
NotificationCenter.default.publisher(for: UIResponder.keyboardDidChangeFrameNotification, object: nil)
.sink { notification in
guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
self.didEndFrame.value = endFrame
self.updateInternalStatus(notification: notification)
}
.store(in: &disposeBag)
NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification, object: nil)
.sink { notification in
guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
self.willEndFrame.value = endFrame
self.updateInternalStatus(notification: notification)
}
.store(in: &disposeBag)
@ -62,6 +57,8 @@ extension KeyboardResponderService {
return
}
self.endFrame.value = endFrame
guard isLocal else {
self.state.value = .notLocal
return