From 1746c1fc777ea6682cc408999b0ff10fcec3b8cf Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 12 Mar 2021 15:23:28 +0800 Subject: [PATCH] feat: add toolbar for compose scene --- Mastodon.xcodeproj/project.pbxproj | 12 ++ .../Scene/Compose/ComposeViewController.swift | 98 +++++++++++ .../Compose/View/ComposeToolbarView.swift | 154 ++++++++++++++++++ .../MastodonRegisterViewController.swift | 2 +- .../Service/KeyboardResponderService.swift | 11 +- 5 files changed, 269 insertions(+), 8 deletions(-) create mode 100644 Mastodon/Scene/Compose/View/ComposeToolbarView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 532ef6cbe..980271a24 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = ""; }; DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = ""; }; + DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = ""; }; DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = ""; }; DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = ""; }; DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = ""; }; @@ -984,6 +986,14 @@ path = Preference; sourceTree = ""; }; + DB55D32225FB4D320002F825 /* View */ = { + isa = PBXGroup; + children = ( + DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */, + ); + path = View; + sourceTree = ""; + }; 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 */, diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index f183bb255..492a69851 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -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 diff --git a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift new file mode 100644 index 000000000..7b501bf70 --- /dev/null +++ b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift @@ -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 + diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index f078e9b8d..d66f9717c 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -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 } diff --git a/Mastodon/Service/KeyboardResponderService.swift b/Mastodon/Service/KeyboardResponderService.swift index b21737963..d4bf9b58b 100644 --- a/Mastodon/Service/KeyboardResponderService.swift +++ b/Mastodon/Service/KeyboardResponderService.swift @@ -18,9 +18,8 @@ final class KeyboardResponderService { // output let isShow = CurrentValueSubject(false) let state = CurrentValueSubject(.none) - let didEndFrame = CurrentValueSubject(.zero) - let willEndFrame = CurrentValueSubject(.zero) - + let endFrame = CurrentValueSubject(.zero) + private init() { NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification, object: nil) .sink { notification in @@ -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