Add OnboardingCelebrationScreen and EffectsSceneView.

This commit is contained in:
Doug 2022-03-21 17:08:54 +00:00
parent d10c387460
commit 78435972e3
18 changed files with 549 additions and 7 deletions

View file

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "onboarding_celebration_icon.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 70 70" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M35,70C54.33,70 70,54.33 70,35C70,15.67 54.33,0 35,0C15.67,0 0,15.67 0,35C0,54.33 15.67,70 35,70ZM34.863,42.213L37.589,50.392C37.725,50.801 38.107,51.076 38.538,51.076C38.968,51.076 39.35,50.801 39.487,50.392L42.213,42.213C42.213,42.213 50.392,39.487 50.392,39.487C50.8,39.351 51.076,38.969 51.076,38.538C51.076,38.108 50.8,37.726 50.392,37.589L42.213,34.863C42.213,34.863 39.487,26.684 39.487,26.684C39.35,26.276 38.968,26 38.538,26C38.107,26 37.725,26.276 37.589,26.684L34.863,34.863C34.863,34.863 26.684,37.589 26.684,37.589C26.275,37.726 26,38.108 26,38.538C26,38.969 26.275,39.351 26.684,39.487L34.863,42.213ZM19.882,23L18.106,26.553C17.913,26.938 17.989,27.403 18.293,27.707C18.597,28.011 19.062,28.087 19.447,27.894L23,26.118C23,26.118 26.553,27.894 26.553,27.894C26.938,28.087 27.403,28.011 27.707,27.707C28.011,27.403 28.087,26.938 27.894,26.553L26.118,23C26.118,23 27.894,19.447 27.894,19.447C28.087,19.062 28.011,18.597 27.707,18.293C27.403,17.989 26.938,17.913 26.553,18.106L23,19.882C23,19.882 19.447,18.106 19.447,18.106C19.062,17.913 18.597,17.989 18.293,18.293C17.989,18.597 17.913,19.062 18.106,19.447L19.882,23Z" style="fill:rgb(13,189,139);"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -35,4 +35,8 @@
"onboarding_avatar_message" = "You can change this anytime.";
"onboarding_avatar_accessibility_label" = "Profile picture";
"onboarding_celebration_title" = "Youre all set!";
"onboarding_celebration_message" = "Your preferences have been saved.";
"onboarding_celebration_button" = "Let's go";
"image_picker_action_files" = "Choose from files";

View file

@ -126,6 +126,7 @@ internal class Asset: NSObject {
internal static let onboardingSplashScreenPage4Dark = ImageAsset(name: "OnboardingSplashScreenPage4Dark")
internal static let onboardingAvatarCamera = ImageAsset(name: "onboarding_avatar_camera")
internal static let onboardingAvatarEdit = ImageAsset(name: "onboarding_avatar_edit")
internal static let onboardingCelebrationIcon = ImageAsset(name: "onboarding_celebration_icon")
internal static let onboardingCongratulationsIcon = ImageAsset(name: "onboarding_congratulations_icon")
internal static let onboardingUseCaseCommunity = ImageAsset(name: "onboarding_use_case_community")
internal static let onboardingUseCaseCommunityDark = ImageAsset(name: "onboarding_use_case_community_dark")

View file

@ -26,6 +26,18 @@ public extension VectorL10n {
static var onboardingAvatarTitle: String {
return VectorL10n.tr("Untranslated", "onboarding_avatar_title")
}
/// Let's go
static var onboardingCelebrationButton: String {
return VectorL10n.tr("Untranslated", "onboarding_celebration_button")
}
/// Your preferences have been saved.
static var onboardingCelebrationMessage: String {
return VectorL10n.tr("Untranslated", "onboarding_celebration_message")
}
/// Youre all set!
static var onboardingCelebrationTitle: String {
return VectorL10n.tr("Untranslated", "onboarding_celebration_title")
}
/// Take me home
static var onboardingCongratulationsHomeButton: String {
return VectorL10n.tr("Untranslated", "onboarding_congratulations_home_button")

View file

@ -391,14 +391,9 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
private func displayNameCoordinator(_ coordinator: OnboardingDisplayNameCoordinator, didCompleteWith userSession: UserSession) {
if shouldShowAvatarScreen {
showAvatarScreen(for: userSession)
return
} else if Analytics.shared.shouldShowAnalyticsPrompt {
showAnalyticsPrompt(for: userSession.matrixSession)
return
} else {
showCelebrationScreen(for: userSession)
}
onboardingFinished = true
completeIfReady()
}
/// Show the avatar personalization screen for new users using the supplied user session.
@ -431,6 +426,31 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
/// Displays the next view in the flow after the avatar screen.
@available(iOS 14.0, *)
private func avatarCoordinator(_ coordinator: OnboardingAvatarCoordinator, didCompleteWith userSession: UserSession) {
showCelebrationScreen(for: userSession)
}
@available(iOS 14.0, *)
private func showCelebrationScreen(for userSession: UserSession) {
MXLog.debug("[OnboardingCoordinator] showCelebrationScreen")
let parameters = OnboardingCelebrationCoordinatorParameters(userSession: userSession)
let coordinator = OnboardingCelebrationCoordinator(parameters: parameters)
coordinator.completion = { [weak self, weak coordinator] userSession in
guard let self = self, let coordinator = coordinator else { return }
self.celebrationCoordinator(coordinator, didCompleteWith: userSession)
}
add(childCoordinator: coordinator)
coordinator.start()
navigationRouter.setRootModule(coordinator, hideNavigationBar: true, animated: true) { [weak self] in
self?.remove(childCoordinator: coordinator)
}
}
@available(iOS 14.0, *)
private func celebrationCoordinator(_ coordinator: OnboardingCelebrationCoordinator, didCompleteWith userSession: UserSession) {
if Analytics.shared.shouldShowAnalyticsPrompt {
showAnalyticsPrompt(for: userSession.matrixSession)
return

View file

@ -20,6 +20,7 @@ import Foundation
@available(iOS 14.0, *)
enum MockAppScreens {
static let appScreens: [MockScreenState.Type] = [
MockOnboardingCelebrationScreenState.self,
MockOnboardingAvatarScreenState.self,
MockOnboardingDisplayNameScreenState.self,
MockOnboardingCongratulationsScreenState.self,

View file

@ -0,0 +1,65 @@
//
// 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 OnboardingCelebrationCoordinatorParameters {
let userSession: UserSession
}
@available(iOS 14.0, *)
final class OnboardingCelebrationCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let parameters: OnboardingCelebrationCoordinatorParameters
private let onboardingCelebrationHostingController: VectorHostingController
private var onboardingCelebrationViewModel: OnboardingCelebrationViewModelProtocol
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: ((UserSession) -> Void)?
// MARK: - Setup
init(parameters: OnboardingCelebrationCoordinatorParameters) {
self.parameters = parameters
let viewModel = OnboardingCelebrationViewModel()
let view = OnboardingCelebrationScreen(viewModel: viewModel.context)
onboardingCelebrationViewModel = viewModel
onboardingCelebrationHostingController = VectorHostingController(rootView: view)
onboardingCelebrationHostingController.enableNavigationBarScrollEdgeAppearance = true
}
// MARK: - Public
func start() {
MXLog.debug("[OnboardingCelebrationCoordinator] did start.")
onboardingCelebrationViewModel.completion = { [weak self] result in
guard let self = self else { return }
MXLog.debug("[OnboardingCelebrationCoordinator] OnboardingCelebrationViewModel did complete with result: \(result).")
self.completion?(self.parameters.userSession)
}
}
func toPresentable() -> UIViewController {
return self.onboardingCelebrationHostingController
}
}

View file

@ -0,0 +1,78 @@
//
// 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 SceneKit
import SwiftUI
@available(iOS 14.0, *)
class EffectsScene: SCNScene {
// MARK: - Constants
private enum Constants {
static let confettiSceneName = "ConfettiScene.scn"
static let particlesNodeName = "particles"
}
// MARK: - Public
static func confetti(with theme: ThemeSwiftUI) -> EffectsScene? {
guard let scene = EffectsScene(named: Constants.confettiSceneName) else { return nil }
let colors: [[Float]] = theme.colors.namesAndAvatars.compactMap { $0.floatComponents }
if let particles = scene.rootNode.childNode(withName: Constants.particlesNodeName, recursively: false)?.particleSystems?.first {
// The particles need a non-zero color variation for the handler to affect the color
particles.particleColorVariation = SCNVector4(x: 0, y: 0, z: 0, w: 0.1)
// Add a handler to customize the color of the particles.
particles.handle(.birth, forProperties: [.color]) { data, dataStride, indices, count in
for index in 0..<count {
// Pick a random color to apply to the particle.
guard let color = colors.randomElement() else { continue }
// Get the particle's color pointer.
let colorPointer = data[0] + dataStride[0] * index
let rgbaPointer = colorPointer.bindMemory(to: Float.self, capacity: dataStride[0])
// Update the color for the particle.
rgbaPointer[0] = color[0]
rgbaPointer[1] = color[1]
rgbaPointer[2] = color[2]
rgbaPointer[3] = 1
}
}
}
return scene
}
}
@available(iOS 14.0, *)
fileprivate extension Color {
/// The color's components as an array of floats in the extended linear sRGB colorspace.
///
/// SceneKit works in a colorspace with a linear gamma, which is why this conversion is necessary.
var floatComponents: [Float]? {
guard
let colorSpace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB),
let linearColor = cgColor?.converted(to: colorSpace, intent: .defaultIntent, options: nil),
let components = linearColor.components
else { return nil }
return components.map { Float($0) }
}
}

View file

@ -0,0 +1,34 @@
//
// 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
import SceneKit
@available(iOS 14.0, *)
/// A SwiftUI wrapper around `SCNView`, that unlike `SceneView` allows the
/// scene to have a transparent background and be rendered on top of other views.
struct EffectsSceneView: UIViewRepresentable {
let scene: SCNScene?
func makeUIView(context: Context) -> SCNView {
SCNView(frame: .zero)
}
func updateUIView(_ sceneView: SCNView, context: Context) {
sceneView.scene = scene
sceneView.backgroundColor = .clear
}
}

View file

@ -0,0 +1,46 @@
//
// 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 SwiftUI
/// 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 MockOnboardingCelebrationScreenState: MockScreenState, CaseIterable {
// A case for each state you want to represent
// with specific, minimal associated data that will allow you
// mock that screen.
case confetti
/// The associated screen
var screenType: Any.Type {
OnboardingCelebrationScreen.self
}
/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
let viewModel = OnboardingCelebrationViewModel()
// can simulate service and viewModel actions here if needs be.
return (
[self, viewModel],
AnyView(OnboardingCelebrationScreen(viewModel: viewModel.context)
.addDependency(MockAvatarService.example))
)
}
}

View file

@ -0,0 +1,31 @@
//
// 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
// MARK: View model
enum OnboardingCelebrationViewModelResult {
case complete
}
// MARK: View
struct OnboardingCelebrationViewState: BindableState { }
enum OnboardingCelebrationViewAction {
case complete
}

View file

@ -0,0 +1,48 @@
//
// 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, *)
typealias OnboardingCelebrationViewModelType = StateStoreViewModel<OnboardingCelebrationViewState,
Never,
OnboardingCelebrationViewAction>
@available(iOS 14, *)
class OnboardingCelebrationViewModel: OnboardingCelebrationViewModelType, OnboardingCelebrationViewModelProtocol {
// MARK: - Properties
// MARK: Private
// MARK: Public
var completion: ((OnboardingCelebrationViewModelResult) -> Void)?
// MARK: - Setup
init() {
super.init(initialViewState: OnboardingCelebrationViewState())
}
// MARK: - Public
override func process(viewAction: OnboardingCelebrationViewAction) {
switch viewAction {
case .complete:
completion?(.complete)
}
}
}

View file

@ -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 OnboardingCelebrationViewModelProtocol {
var completion: ((OnboardingCelebrationViewModelResult) -> Void)? { get set }
@available(iOS 14, *)
var context: OnboardingCelebrationViewModelType.Context { get }
}

View file

@ -0,0 +1,23 @@
//
// 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 OnboardingCelebrationUITests: MockScreenTest {
// Nothing to test as the view is completely static
}

View file

@ -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 XCTest
@testable import RiotSwiftUI
@available(iOS 14.0, *)
class OnboardingCelebrationViewModelTests: XCTestCase {
// Nothing to test as there is no mutable state
}

View file

@ -0,0 +1,111 @@
//
// 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 SceneKit
@available(iOS 14.0, *)
struct OnboardingCelebrationScreen: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
private var horizontalPadding: CGFloat {
horizontalSizeClass == .regular ? 50 : 16
}
// MARK: Public
@ObservedObject var viewModel: OnboardingCelebrationViewModel.Context
// MARK: Views
var body: some View {
GeometryReader { geometry in
VStack {
ScrollView(showsIndicators: false) {
mainContent
.padding(.top, 106)
.padding(.horizontal, horizontalPadding)
.frame(maxWidth: OnboardingConstants.maxContentWidth)
}
.frame(maxWidth: .infinity)
buttons
.frame(maxWidth: OnboardingConstants.maxContentWidth)
.padding(.horizontal, horizontalPadding)
.padding(.bottom, 24)
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
}
.frame(maxWidth: .infinity)
}
.overlay(effects.ignoresSafeArea())
.background(theme.colors.background.ignoresSafeArea())
.accentColor(theme.colors.accent)
}
/// The main content of the view to be shown in a scroll view.
var mainContent: some View {
VStack(spacing: 8) {
Image(Asset.Images.onboardingCelebrationIcon.name)
.resizable()
.scaledToFit()
.frame(width: 90)
.foregroundColor(theme.colors.accent)
.background(Circle().foregroundColor(.white).padding(2))
.padding(.bottom, 42)
Text(VectorL10n.onboardingCelebrationTitle)
.font(theme.fonts.title2B)
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.primaryContent)
Text(VectorL10n.onboardingCelebrationMessage)
.font(theme.fonts.subheadline)
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.secondaryContent)
}
}
/// The action buttons shown at the bottom of the view.
var buttons: some View {
VStack {
Button { viewModel.send(viewAction: .complete) } label: {
Text(VectorL10n.onboardingCelebrationButton)
.font(theme.fonts.body)
}
.buttonStyle(PrimaryActionButtonStyle())
}
}
var effects: some View {
EffectsSceneView(scene: EffectsScene.confetti(with: theme))
}
}
// MARK: - Previews
@available(iOS 15.0, *)
struct OnboardingCelebration_Previews: PreviewProvider {
static let stateRenderer = MockOnboardingCelebrationScreenState.stateRenderer
static var previews: some View {
stateRenderer.screenGroup()
}
}