Move composer send media selection to it's own coordinator and us e ioS 15 api for sheet

This commit is contained in:
David Langley
2022-10-08 10:46:05 +01:00
parent 02929684c7
commit 9645cf939e
13 changed files with 395 additions and 235 deletions
@@ -31,7 +31,7 @@ enum FormatType {
case underline
}
@objc enum ComposerModule: Int {
@objc enum ComposerCreateAction: Int {
case photoLibrary
case stickers
case attachments
@@ -105,11 +105,11 @@ extension FormatType {
}
}
extension ComposerModule: CaseIterable, Identifiable {
extension ComposerCreateAction: CaseIterable, Identifiable {
var id: Self { self }
}
extension ComposerModule {
extension ComposerCreateAction {
var title: String {
switch self {
case .photoLibrary:
@@ -144,3 +144,12 @@ extension ComposerModule {
}
}
}
struct ComposerCreateActionListViewState: BindableState {
let actions: [ComposerCreateAction]
}
enum ComposerCreateActionListViewModelResult {
case done(ComposerCreateAction)
}
@@ -0,0 +1,89 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
@objc protocol ComposerCreateActionListBridgePresenterDelegate {
func composerCreateActionListBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: ComposerCreateActionListBridgePresenter, action: ComposerCreateAction)
func composerCreateActionListBridgePresenterDidDismissInteractively(_ coordinatorBridgePresenter: ComposerCreateActionListBridgePresenter)
}
/// ComposerCreateActionListBridgePresenter enables to start ComposerCreateActionList from a view controller.
/// This bridge is used while waiting for global usage of coordinator pattern.
/// **WARNING**: This class breaks the Coordinator abstraction and it has been introduced for **Objective-C compatibility only**
/// (mainly for integration in legacy view controllers). Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator.
@objcMembers
final class ComposerCreateActionListBridgePresenter: NSObject {
// MARK: - Constants
// MARK: - Properties
// MARK: Private
private let actions: [ComposerCreateAction]
private var coordinator: ComposerCreateActionListCoordinator?
// MARK: Public
weak var delegate: ComposerCreateActionListBridgePresenterDelegate?
// MARK: - Setup
init(actions: [Int]) {
self.actions = actions.compactMap({ ComposerCreateAction(rawValue: $0) })
super.init()
}
// MARK: - Public
// NOTE: Default value feature is not compatible with Objective-C.
// func present(from viewController: UIViewController, animated: Bool) {
// self.present(from: viewController, animated: animated)
// }
func present(from viewController: UIViewController, animated: Bool) {
let composerCreateActionListCoordinator = ComposerCreateActionListCoordinator(actions: actions)
composerCreateActionListCoordinator.callback = { [weak self] action in
guard let self = self else { return }
switch action {
case .done(let composeAction):
self.delegate?.composerCreateActionListBridgePresenterDelegateDidComplete(self, action: composeAction)
case .cancel:
self.delegate?.composerCreateActionListBridgePresenterDidDismissInteractively(self)
}
}
let presentable = composerCreateActionListCoordinator.toPresentable()
viewController.present(presentable, animated: animated, completion: nil)
composerCreateActionListCoordinator.start()
self.coordinator = composerCreateActionListCoordinator
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
guard let coordinator = self.coordinator else {
return
}
// Dismiss modal
coordinator.toPresentable().dismiss(animated: animated) {
self.coordinator = nil
if let completion = completion {
completion()
}
}
}
}
@@ -0,0 +1,71 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
/// Actions returned by the coordinator callback
enum ComposerCreateActionListCoordinatorAction {
case done(ComposerCreateAction)
case cancel
}
final class ComposerCreateActionListCoordinator: NSObject, Coordinator, Presentable, UISheetPresentationControllerDelegate {
// MARK: - Properties
// MARK: Private
private let hostingController: UIViewController
private var view: ComposerCreateActionList
private var viewModel: ComposerCreateActionListViewModel
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var callback: ((ComposerCreateActionListCoordinatorAction) -> Void)?
// MARK: - Setup
init(actions: [ComposerCreateAction]) {
viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: actions))
self.view = ComposerCreateActionList(viewModel: viewModel.context)
let hostingVC = VectorHostingController(rootView: view)
hostingVC.bottomSheetPreferences = VectorHostingBottomSheetPreferences(detents: [.medium])
hostingController = hostingVC
super.init()
hostingVC.presentationController?.delegate = self
}
// MARK: - Public
func start() {
MXLog.debug("[ComposerCreateActionListCoordinator] did start.")
viewModel.callback = { result in
switch result {
case .done(let action):
self.callback?(.done(action))
}
}
}
func toPresentable() -> UIViewController {
return self.hostingController
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
self.callback?(.cancel)
}
}
@@ -0,0 +1,59 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct ComposerCreateActionList: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
@ObservedObject var viewModel: ComposerCreateActionListViewModel.Context
var body: some View {
VStack{
VStack(alignment: .leading) {
ForEach(viewModel.viewState.actions) { action in
HStack(spacing: 16) {
Image(action.icon)
.renderingMode(.template)
.foregroundColor(theme.colors.accent)
Text(action.title)
.foregroundColor(theme.colors.primaryContent)
.font(theme.fonts.body)
Spacer()
}
.contentShape(Rectangle())
.onTapGesture {
viewModel.send(viewAction: .selectAction(action))
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}
.padding(.top, 16)
Spacer()
}.background(theme.colors.background.ignoresSafeArea())
}
}
struct ComposerCreateActionList_Previews: PreviewProvider {
static let viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: ComposerCreateAction.allCases))
static var previews: some View {
ComposerCreateActionList(viewModel: viewModel.context)
}
}
@@ -0,0 +1,22 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
protocol ComposerCreateActionListViewModelProtocol {
var callback: ((ComposerCreateActionListViewModelResult) -> Void)? { get set }
var context: ComposerCreateActionListViewModelType.Context { get }
}
@@ -0,0 +1,44 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
typealias ComposerCreateActionListViewModelType = StateStoreViewModel<ComposerCreateActionListViewState,
Never,
ComposerCreateActionListViewAction>
class ComposerCreateActionListViewModel: ComposerCreateActionListViewModelType, ComposerCreateActionListViewModelProtocol {
// MARK: - Properties
// MARK: Private
// MARK: Public
var callback: ((ComposerCreateActionListViewModelResult) -> Void)?
// MARK: - Setup
// MARK: - Public
override func process(viewAction: ComposerCreateActionListViewAction) {
switch viewAction {
case .selectAction(let action):
callback?(.done(action))
}
}
}
@@ -18,7 +18,7 @@ import Foundation
import WysiwygComposer
import SwiftUI
@available(iOS 16.0, *)
@available(iOS 15.0, *)
enum MockComposerScreenState: MockScreenState, CaseIterable {
case composer
@@ -39,7 +39,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable {
[viewModel],
AnyView(VStack{
Spacer()
Composer(viewModel: viewModel, sendMessageAction: { _ in }, startModuleAction: { _ in })
Composer(viewModel: viewModel, sendMessageAction: { _ in }, showSendMediaActions: { })
}.frame(
minWidth: 0,
maxWidth: .infinity,
@@ -18,16 +18,16 @@ import SwiftUI
import WysiwygComposer
import DSBottomSheet
@available(iOS 16.0, *)
@available(iOS 15.0, *)
struct Composer: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
@ObservedObject var viewModel: WysiwygComposerViewModel
let sendMessageAction: (WysiwygComposerContent) -> Void
let startModuleAction: (ComposerModule) -> Void
@State private var isBottomSheetExpanded = false
let showSendMediaActions: () -> Void
var textColor = Color(.label)
@State private var showSendButton = false
private let borderHeight: CGFloat = 44
@@ -81,7 +81,7 @@ struct Composer: View {
.padding(.bottom, 4)
HStack{
Button {
isBottomSheetExpanded = true
showSendMediaActions()
} label: {
Image(Asset.Images.startComposeModule.name)
.foregroundColor(theme.colors.tertiaryContent)
@@ -117,44 +117,19 @@ struct Composer: View {
.padding(.horizontal, 16)
.padding(.bottom, 4)
}
.sheet(isPresented: $isBottomSheetExpanded) {
moduleSelectionList
.presentationDetents([.medium])
}
}
var moduleSelectionList: some View {
VStack {
VStack(alignment: .leading) {
ForEach(ComposerModule.allCases) { module in
HStack(spacing: 16) {
Image(module.icon)
.renderingMode(.template)
.foregroundColor(theme.colors.accent)
Text(module.title)
.foregroundColor(theme.colors.primaryContent)
.font(theme.fonts.body)
Spacer()
}
.onTapGesture {
isBottomSheetExpanded = false
self.startModuleAction(module)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}
.padding(.top, 16)
.background(theme.colors.background.ignoresSafeArea())
Spacer()
}
}
}
@available(iOS 16.0, *)
@available(iOS 15.0, *)
struct Composer_Previews: PreviewProvider {
static let stateRenderer = MockComposerScreenState.stateRenderer
static var previews: some View {
stateRenderer.screenGroup()
}
}
enum ComposerCreateActionListViewAction {
case selectAction(ComposerCreateAction)
}