feat: add toolbar for compose scene
This commit is contained in:
parent
d9e2453464
commit
1746c1fc77
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue