App Layout: Release Experience

- First iteration before final design
This commit is contained in:
Gil Eluard
2022-08-23 11:20:46 +02:00
parent 30cb697304
commit 2312f46e1a
24 changed files with 615 additions and 1 deletions
@@ -0,0 +1,43 @@
//
// 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
// MARK: - Coordinator
// MARK: View model
enum AllChatsOnboardingViewModelResult {
case cancel
}
// MARK: View
struct AllChatsOnboardingPage: Identifiable {
let id = UUID().uuidString
let image: UIImage
let title: String
let message: String
}
struct AllChatsOnboardingViewState: BindableState {
let pages: [AllChatsOnboardingPage]
}
enum AllChatsOnboardingViewAction {
case cancel
}
@@ -0,0 +1,69 @@
//
// 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
typealias AllChatsOnboardingViewModelType = StateStoreViewModel<AllChatsOnboardingViewState,
Never,
AllChatsOnboardingViewAction>
class AllChatsOnboardingViewModel: AllChatsOnboardingViewModelType, AllChatsOnboardingViewModelProtocol {
// MARK: - Properties
// MARK: Private
// MARK: Public
var completion: ((AllChatsOnboardingViewModelResult) -> Void)?
// MARK: - Setup
static func makeAllChatsOnboardingViewModel() -> AllChatsOnboardingViewModelProtocol {
return AllChatsOnboardingViewModel()
}
private init() {
super.init(initialViewState: Self.defaultState())
}
private static func defaultState() -> AllChatsOnboardingViewState {
return AllChatsOnboardingViewState(pages: [
AllChatsOnboardingPage(image: Asset.Images.allChatsOnboardingDark1.image,
title: VectorL10n.allChatsOnboardingPageTitle1,
message: VectorL10n.allChatsOnboardingPageMessage1),
AllChatsOnboardingPage(image: Asset.Images.allChatsOnboardingDark2.image,
title: VectorL10n.allChatsOnboardingPageTitle2,
message: VectorL10n.allChatsOnboardingPageMessage2),
AllChatsOnboardingPage(image: Asset.Images.allChatsOnboardingDark3.image,
title: VectorL10n.allChatsOnboardingPageTitle3,
message: VectorL10n.allChatsOnboardingPageMessage3),
AllChatsOnboardingPage(image: Asset.Images.allChatsOnboardingDark4.image,
title: VectorL10n.allChatsOnboardingPageTitle4,
message: VectorL10n.allChatsOnboardingPageMessage4),
])
}
// MARK: - Public
override func process(viewAction: AllChatsOnboardingViewAction) {
switch viewAction {
case .cancel:
completion?(.cancel)
}
}
}
@@ -0,0 +1,24 @@
//
// 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 AllChatsOnboardingViewModelProtocol {
var completion: ((AllChatsOnboardingViewModelResult) -> Void)? { get set }
static func makeAllChatsOnboardingViewModel() -> AllChatsOnboardingViewModelProtocol
var context: AllChatsOnboardingViewModelType.Context { get }
}
@@ -0,0 +1,95 @@
//
// 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 CommonKit
/// All Chats onboarding screen
final class AllChatsOnboardingCoordinator: NSObject, Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let hostingController: UIViewController
private var viewModel: AllChatsOnboardingViewModelProtocol
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
private var loadingIndicator: UserIndicator?
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: (() -> Void)?
// MARK: - Setup
override init() {
let viewModel = AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel()
let view = AllChatsOnboarding(viewModel: viewModel.context)
self.viewModel = viewModel
self.hostingController = VectorHostingController(rootView: view)
self.indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingController)
super.init()
hostingController.presentationController?.delegate = self
}
// MARK: - Public
func start() {
MXLog.debug("[AllChatsOnboardingCoordinator] did start.")
viewModel.completion = { [weak self] result in
guard let self = self else { return }
MXLog.debug("[AllChatsOnboardingCoordinator] AllChatsOnboardingViewModel did complete with result: \(result).")
switch result {
case .cancel:
self.completion?()
}
}
}
func toPresentable() -> UIViewController {
return self.hostingController
}
// MARK: - Private
/// Show an activity indicator whilst loading.
/// - Parameters:
/// - label: The label to show on the indicator.
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) {
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
}
/// Hide the currently displayed activity indicator.
private func stopLoading() {
loadingIndicator = nil
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension AllChatsOnboardingCoordinator: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
completion?()
}
}
@@ -0,0 +1,65 @@
//
// 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 AllChatsOnboardingCoordinatorBridgePresenterDelegate {
func allChatsOnboardingCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: AllChatsOnboardingCoordinatorBridgePresenter)
}
/// `AllChatsOnboardingCoordinatorBridgePresenter` enables to start `AllChatsOnboardingCoordinator` from a view controller.
/// This bridge is used while waiting for global usage of coordinator pattern.
/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (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 AllChatsOnboardingCoordinatorBridgePresenter: NSObject {
// MARK: - Properties
// MARK: Private
private var coordinator: AllChatsOnboardingCoordinator?
// MARK: Public
var completion: (() -> Void)?
// MARK: - Public
func present(from viewController: UIViewController, animated: Bool) {
let coordinator = AllChatsOnboardingCoordinator()
coordinator.completion = { [weak self] in
guard let self = self else { return }
self.completion?()
}
let presentable = coordinator.toPresentable()
viewController.present(presentable, animated: animated, completion: nil)
coordinator.start()
self.coordinator = coordinator
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
guard let coordinator = self.coordinator else {
return
}
coordinator.toPresentable().dismiss(animated: animated) {
self.coordinator = nil
completion?()
}
}
}
@@ -0,0 +1,92 @@
//
// 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
struct AllChatsOnboarding: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
@ObservedObject var viewModel: AllChatsOnboardingViewModel.Context
var body: some View {
VStack {
Text(VectorL10n.allChatsOnboardingTitle)
.font(theme.fonts.title3SB)
.foregroundColor(theme.colors.primaryContent)
.padding()
TabView {
ForEach(viewModel.viewState.pages) { page in
pageView(image: page.image,
title: page.title,
message: page.message)
}
}
.tabViewStyle(PageTabViewStyle())
Button { viewModel.send(viewAction: .cancel) } label: {
Text(VectorL10n.allChatsOnboardingTryIt)
}
.buttonStyle(PrimaryActionButtonStyle())
.padding()
}
.background(theme.colors.background.ignoresSafeArea())
.frame(maxHeight: .infinity)
.onAppear {
self.setupAppearance()
}
}
@ViewBuilder
private func pageView(image: UIImage, title: String, message: String) -> some View {
VStack {
Spacer()
Image(uiImage: image)
Spacer()
Text(title)
.font(theme.fonts.title2B)
.foregroundColor(theme.colors.primaryContent)
.padding(.bottom, 16)
Text(message)
.multilineTextAlignment(.center)
.font(theme.fonts.callout)
.foregroundColor(theme.colors.primaryContent)
Spacer()
}
.padding(.horizontal)
}
private func setupAppearance() {
let tintColor: UIColor = theme.isDark ? .white : .black
UIPageControl.appearance().currentPageIndicatorTintColor = tintColor
UIPageControl.appearance().pageIndicatorTintColor = tintColor.withAlphaComponent(0.2)
}
}
// MARK: - Previews
struct AllChatsOnboarding_Previews: PreviewProvider {
static var previews: some View {
AllChatsOnboarding(viewModel: AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel().context).theme(.light).preferredColorScheme(.light)
AllChatsOnboarding(viewModel: AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel().context).theme(.dark).preferredColorScheme(.dark)
}
}