[iOS] Create public space #143

- Initial space creation flow
This commit is contained in:
Gil Eluard
2021-11-23 09:35:32 +01:00
parent ccfe3afb7d
commit fda0568d88
145 changed files with 7280 additions and 11 deletions
@@ -0,0 +1,73 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
/*
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 Foundation
import UIKit
import SwiftUI
final class SpaceCreationMenuCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let parameters: SpaceCreationMenuCoordinatorParameters
private let spaceCreationMenuHostingController: UIViewController
private var spaceCreationMenuViewModel: SpaceCreationMenuViewModelProtocol
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var callback: ((SpaceCreationMenuCoordinatorAction) -> Void)?
// MARK: - Setup
@available(iOS 14.0, *)
init(parameters: SpaceCreationMenuCoordinatorParameters) {
self.parameters = parameters
let viewModel = SpaceCreationMenuViewModel(navTitle: parameters.navTitle, creationParams: parameters.creationParams, title: parameters.title, detail: parameters.detail, options: parameters.options)
let view = SpaceCreationMenu(viewModel: viewModel.context)
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
spaceCreationMenuViewModel = viewModel
let hostingController = VectorHostingController(rootView: view)
hostingController.hidesBackTitleWhenPushed = true
spaceCreationMenuHostingController = hostingController
}
// MARK: - Public
func start() {
MXLog.debug("[SpaceCreationMenuCoordinator] did start.")
spaceCreationMenuViewModel.callback = { [weak self] result in
MXLog.debug("[SpaceCreationMenuCoordinator] SpaceCreationMenuViewModel did complete with result \(result).")
guard let self = self else { return }
switch result {
case .didSelectOption(let optionId):
self.callback?(.didSelectOption(optionId))
case .cancel:
self.callback?(.cancel)
break
}
}
}
func toPresentable() -> UIViewController {
return self.spaceCreationMenuHostingController
}
}
@@ -0,0 +1,28 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
//
// 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 Foundation
struct SpaceCreationMenuCoordinatorParameters {
let session: MXSession
let creationParams: SpaceCreationParameters
let navTitle: String?
let title: String
let detail: String
let options: [SpaceCreationMenuRoomOption]
}
@@ -0,0 +1,25 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
//
// 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 Foundation
/// Actions returned by the coordinator callback
enum SpaceCreationMenuCoordinatorAction {
case didSelectOption(_ optionId: SpaceCreationMenuRoomOptionId)
case cancel
}
@@ -0,0 +1,36 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
//
// 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 Foundation
import UIKit
enum SpaceCreationMenuRoomOptionId {
case publicSpace
case privateSpace
case ownedPrivateSpace
case sharedPrivateSpace
}
struct SpaceCreationMenuRoomOption {
let id: SpaceCreationMenuRoomOptionId
let icon: UIImage
let title: String
let detail: String
}
extension SpaceCreationMenuRoomOption: Identifiable, Equatable {}
@@ -0,0 +1,23 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
//
// 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 Foundation
/// Actions to be performed on the `ViewModel` State
enum SpaceCreationMenuStateAction {
}
@@ -0,0 +1,25 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
//
// 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 Foundation
/// Actions send from the `View` to the `ViewModel`.
enum SpaceCreationMenuViewAction {
case cancel
case didSelectOption(_ optionId: SpaceCreationMenuRoomOptionId)
}
@@ -0,0 +1,25 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
//
// 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 Foundation
/// Actions sent by the`ViewModel` to the `Coordinator`.
enum SpaceCreationMenuViewModelAction {
case didSelectOption(_ optionId: SpaceCreationMenuRoomOptionId)
case cancel
}
@@ -0,0 +1,27 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
//
// 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 Foundation
/// State managed by the `ViewModel` delivered to the `View`.
struct SpaceCreationMenuViewState: BindableState {
var navTitle: String
var title: String
var detail: String
var options: [SpaceCreationMenuRoomOption]
}
@@ -0,0 +1,56 @@
//
// 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 Foundation
import UIKit
class SpaceCreationParameters {
var name: String?
var topic: String?
var address: String?
var userDefinedAddress: String?
var isPublic: Bool = false
var showAddress: Bool {
isPublic
}
var userSelectedAvatar: UIImage?
var isShared: Bool = false
var newRooms: [SpaceCreationNewRoom] = [
SpaceCreationNewRoom(name: VectorL10n.spacesCreationNewRoomsGeneral, defaultName: VectorL10n.spacesCreationNewRoomsGeneral),
SpaceCreationNewRoom(name: VectorL10n.spacesCreationNewRoomsRandom, defaultName: VectorL10n.spacesCreationNewRoomsRandom),
SpaceCreationNewRoom(name: "", defaultName: VectorL10n.spacesCreationNewRoomsSupport)
]
var addedRoomIds: [String] = []
var emailInvites: [String] = ["", ""]
var userDefinedEmailInvites: [String] {
return emailInvites.filter { address in
return !address.isEmpty
}
}
var userIdInvites: [String] = []
}
struct SpaceCreationNewRoom: Equatable {
var name: String
var defaultName: String
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.defaultName == rhs.defaultName && lhs.name == rhs.name
}
}
@@ -0,0 +1,54 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
//
// 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 XCTest
import RiotSwiftUI
@available(iOS 14.0, *)
class SpaceCreationMenuUITests: MockScreenTest {
override class var screenType: MockScreenState.Type {
return MockSpaceCreationMenuScreenState.self
}
override class func createTest() -> MockScreenTest {
return SpaceCreationMenuUITests(selector: #selector(verifySpaceCreationMenuScreen))
}
func verifySpaceCreationMenuScreen() throws {
guard let screenState = screenState as? MockSpaceCreationMenuScreenState else { fatalError("no screen") }
switch screenState {
case .options:
verifySpaceCreationMenuOptions()
}
}
func verifySpaceCreationMenuOptions() {
let optionButtonCount = app.buttons.matching(identifier:"optionButton").count
XCTAssertEqual(optionButtonCount, 2)
let titleText = app.staticTexts["titleText"]
XCTAssert(titleText.exists)
XCTAssert(titleText.label == "Some title")
let detailText = app.staticTexts["detailText"]
XCTAssert(detailText.exists)
XCTAssert(detailText.label == "Some title")
}
}
@@ -0,0 +1,59 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
//
// 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 XCTest
import Combine
@testable import RiotSwiftUI
@available(iOS 14.0, *)
class SpaceCreationMenuViewModelTests: XCTestCase {
private enum Constants {
}
let navTitle = VectorL10n.spacesCreateSpaceTitle
var creationParams = SpaceCreationParameters()
let title = VectorL10n.spacesCreateSpaceTitle
let detail = VectorL10n.spacesCreationVisibilityMessage
let options = [
SpaceCreationMenuRoomOption(id: .publicSpace, icon: Asset.Images.spaceTypeIcon.image, title: VectorL10n.spacePublicJoinRule, detail: VectorL10n.spacePublicJoinRuleDetail),
SpaceCreationMenuRoomOption(id: .privateSpace, icon: Asset.Images.spacePrivateIcon.image, title: VectorL10n.spacePrivateJoinRule, detail: VectorL10n.spacePrivateJoinRuleDetail)
]
var viewModel: SpaceCreationMenuViewModel!
var context: SpaceCreationMenuViewModel.Context!
var cancellables = Set<AnyCancellable>()
override func setUpWithError() throws {
viewModel = SpaceCreationMenuViewModel(
navTitle: navTitle,
creationParams: creationParams,
title: title,
detail: detail,
options: options
)
context = viewModel.context
}
func testInitialState() {
XCTAssertEqual(context.viewState.navTitle, navTitle)
XCTAssertEqual(context.viewState.title, title)
XCTAssertEqual(context.viewState.detail, detail)
XCTAssertEqual(context.viewState.options, options)
}
}
@@ -0,0 +1,150 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
//
// 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
@available(iOS 14.0, *)
struct SpaceCreationMenu: View {
// MARK: - Properties
@ObservedObject var viewModel: SpaceCreationMenuViewModelType.Context
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
var body: some View {
mainScreen
.navigationTitle(viewModel.viewState.navTitle)
.configureNavigationBar{
$0.navigationBar.shadowImage = UIImage()
$0.navigationBar.barTintColor = UIColor(theme.colors.background)
$0.navigationBar.tintColor = UIColor(theme.colors.secondaryContent)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
viewModel.send(viewAction: .cancel)
}) {
Image(uiImage: Asset.Images.spacesModalClose.image).renderingMode(.template)
}
}
}
}
// MARK: - Private
@ViewBuilder
private var mainScreen: some View {
GeometryReader { reader in
ScrollView {
VStack {
headerView
Spacer()
optionsView
}
.frame(minHeight: reader.size.height - 2)
}
}
.padding(EdgeInsets(top: 24, leading: 16, bottom: 24, trailing: 16))
.background(theme.colors.background)
}
@ViewBuilder
private var headerView: some View {
VStack {
Text(viewModel.viewState.title)
.multilineTextAlignment(.center)
.font(theme.fonts.title3SB)
.foregroundColor(theme.colors.primaryContent)
.accessibility(identifier: "titleText")
.padding(.bottom, 20)
Text(viewModel.viewState.detail)
.multilineTextAlignment(.center)
.font(theme.fonts.body)
.foregroundColor(theme.colors.secondaryContent)
.accessibility(identifier: "detailText")
}
}
@ViewBuilder
private var optionsView: some View {
VStack(spacing: 16) {
ForEach(viewModel.viewState.options) { option in
OptionButton(icon: option.icon, title: option.title, detailMessage: option.detail) {
viewModel.send(viewAction: .didSelectOption(option.id))
}
.accessibility(identifier: "optionButton")
}
Text(VectorL10n.spacesCreationFooter)
.multilineTextAlignment(.center)
.font(theme.fonts.caption1)
.foregroundColor(theme.colors.secondaryContent)
}
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct SpaceCreationMenu_Previews: PreviewProvider {
static let stateRenderer = MockSpaceCreationMenuScreenState.stateRenderer
static var previews: some View {
Group {
stateRenderer.screenGroup(addNavigation: true)
.theme(.light).preferredColorScheme(.light)
stateRenderer.screenGroup(addNavigation: true)
.theme(.dark).preferredColorScheme(.dark)
}
}
}
/// Using an enum for the screen allows you define the different state cases with
/// the relevant associated data for each case.
@available(iOS 14.0, *)
enum MockSpaceCreationMenuScreenState: MockScreenState, CaseIterable {
// A case for each state you want to represent
// with specific, minimal associated data that will allow you
// mock that screen.
case options
/// The associated screen
var screenType: Any.Type {
SpaceCreationMenu.self
}
/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
let viewModel = SpaceCreationMenuViewModel(navTitle: VectorL10n.spacesCreateSpaceTitle, creationParams: SpaceCreationParameters(), title: "Some title", detail: "Some detail text", options: [
SpaceCreationMenuRoomOption(id: .publicSpace, icon: Asset.Images.spaceTypeIcon.image, title: "Title of option 1", detail: "Detail of option 1"),
SpaceCreationMenuRoomOption(id: .publicSpace, icon: Asset.Images.spaceTypeIcon.image, title: "Title of option 2", detail: "Detail of option 2")
])
// can simulate service and viewModel actions here if needs be.
return (
[viewModel],
AnyView(SpaceCreationMenu(viewModel: viewModel.context))
)
}
}
@@ -0,0 +1,89 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
//
// 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
import Combine
@available(iOS 14, *)
typealias SpaceCreationMenuViewModelType = StateStoreViewModel<SpaceCreationMenuViewState,
SpaceCreationMenuStateAction,
SpaceCreationMenuViewAction>
@available(iOS 14.0, *)
class SpaceCreationMenuViewModel: SpaceCreationMenuViewModelType, SpaceCreationMenuViewModelProtocol {
// MARK: - Properties
// MARK: Private
let creationParams: SpaceCreationParameters
// MARK: Public
var callback: ((SpaceCreationMenuViewModelAction) -> Void)?
// MARK: - Setup
init(navTitle: String?, creationParams: SpaceCreationParameters, title: String, detail: String, options: [SpaceCreationMenuRoomOption]) {
self.creationParams = creationParams
super.init(initialViewState: SpaceCreationMenuViewModel.defaultState(navTitle: navTitle, creationParams: creationParams, title: title, detail: detail, options: options))
}
private static func defaultState(navTitle: String?, creationParams: SpaceCreationParameters, title: String, detail: String, options: [SpaceCreationMenuRoomOption]) -> SpaceCreationMenuViewState {
var navigationTitle: String = ""
if let navTitle = navTitle {
navigationTitle = navTitle
} else {
navigationTitle = creationParams.isPublic ? VectorL10n.spacesCreationPublicSpaceTitle : VectorL10n.spacesCreationPrivateSpaceTitle
}
return SpaceCreationMenuViewState(navTitle: navigationTitle, title: title, detail: detail, options: options)
}
// MARK: - Public
override func process(viewAction: SpaceCreationMenuViewAction) {
switch viewAction {
case .didSelectOption(let optionId):
switch optionId {
case .publicSpace:
self.creationParams.isPublic = true
case .privateSpace:
self.creationParams.isPublic = false
case .ownedPrivateSpace:
self.creationParams.isShared = false
case .sharedPrivateSpace:
self.creationParams.isShared = true
}
didSelectOption(withId: optionId)
case .cancel:
done()
}
}
// MARK: - Private
private func done() {
callback?(.cancel)
}
private func didSelectOption(withId optionId: SpaceCreationMenuRoomOptionId) {
callback?(.didSelectOption(optionId))
}
}
@@ -0,0 +1,25 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
//
// 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 Foundation
protocol SpaceCreationMenuViewModelProtocol {
var callback: ((SpaceCreationMenuViewModelAction) -> Void)? { get set }
@available(iOS 14, *)
var context: SpaceCreationMenuViewModelType.Context { get }
}