MESSENGER-3671 Merge FOSS 1.9.8

Conflicts:
 - CommonConfiguration.swift
 - BuildSettings.swift
 - Generated/images.Swift
 - RoomMemberDetailsViewController.m
 - LiveLocationSharingViewModell
-  PinCodeEnterViewController.m
This commit is contained in:
Frank Rotermund
2022-10-05 15:47:27 +02:00
744 changed files with 6999 additions and 3819 deletions
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,7 +18,6 @@ import SwiftUI
/// A visual cue to user that something is in progress.
struct ActivityIndicator: View {
private enum Constants {
static let backgroundColor = Color(UIColor(white: 0.8, alpha: 0.9))
}
@@ -29,7 +28,6 @@ struct ActivityIndicator: View {
.padding()
.background(Constants.backgroundColor)
.cornerRadius(5)
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -37,6 +37,6 @@ struct ActivityIndicatorModifier: ViewModifier {
extension View {
func activityIndicator(show: Bool) -> some View {
self.modifier(ActivityIndicatorModifier(show: show))
modifier(ActivityIndicatorModifier(show: show))
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,7 +18,7 @@ import Foundation
/// A protocol that any class or struct can conform to
/// so that it can easily produce avatar data.
///
///
/// E.g. MXRoom, MxUser can conform to this making it
/// easy to grab the avatar data for display.
protocol Avatarable: AvatarInputProtocol { }
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,10 +14,10 @@
// limitations under the License.
//
import Foundation
import MatrixSDK
import Combine
import DesignKit
import Foundation
import MatrixSDK
enum AvatarServiceError: Error {
case pathNotfound
@@ -25,7 +25,6 @@ enum AvatarServiceError: Error {
}
class AvatarService: AvatarServiceProtocol {
private enum Constants {
static let mimeType = "image/jpeg"
static let thumbnailMethod = MXThumbnailingMethodCrop
@@ -34,7 +33,7 @@ class AvatarService: AvatarServiceProtocol {
private let mediaManager: MXMediaManager
static func instantiate(mediaManager: MXMediaManager) -> AvatarServiceProtocol {
return AvatarService(mediaManager: mediaManager)
AvatarService(mediaManager: mediaManager)
}
init(mediaManager: MXMediaManager) {
@@ -49,13 +48,13 @@ class AvatarService: AvatarServiceProtocol {
/// - avatarSize: The size of avatar to retrieve as defined in the DesignKit spec.
/// - Returns: A Future of UIImage that returns an error if it fails to fetch the image.
func avatarImage(mxContentUri: String, avatarSize: AvatarSize) -> Future<UIImage, Error> {
let cachePath = MXMediaManager.thumbnailCachePath(
forMatrixContentURI: mxContentUri,
andType: Constants.mimeType,
inFolder: nil,
inFolder: nil,
toFitViewSize: avatarSize.size,
with: Constants.thumbnailMethod)
with: Constants.thumbnailMethod
)
return Future<UIImage, Error> { promise in
if let image = MXMediaManager.loadThroughCache(withFilePath: cachePath),
@@ -69,7 +68,8 @@ class AvatarService: AvatarServiceProtocol {
withType: Constants.mimeType,
inFolder: nil,
toFitViewSize: avatarSize.size,
with: Constants.thumbnailMethod) { path in
with: Constants.thumbnailMethod
) { path in
guard let path = path else {
promise(.failure(AvatarServiceError.pathNotfound))
return
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,9 +14,9 @@
// limitations under the License.
//
import Foundation
import Combine
import DesignKit
import Foundation
import UIKit
class MockAvatarService: AvatarServiceProtocol {
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,11 +14,10 @@
// limitations under the License.
//
import SwiftUI
import DesignKit
import SwiftUI
struct AvatarImage: View {
@Environment(\.theme) var theme: ThemeSwiftUI
@Environment(\.dependencies) var dependencies: DependencyContainer
@StateObject var viewModel = AvatarViewModel()
@@ -75,7 +74,7 @@ extension AvatarImage {
/// Use display name color as border color by default
func border() -> some View {
let borderColor = theme.userColor(for: matrixItemId)
return self.border(color: borderColor)
return border(color: borderColor)
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,7 +22,6 @@ import SwiftUI
/// This view has a forced 1:1 aspect ratio but will appear very large until a `.frame`
/// modifier is applied.
struct PlaceholderAvatarImage: View {
// MARK: - Private
@Environment(\.theme) private var theme
@@ -14,11 +14,10 @@
// limitations under the License.
//
import SwiftUI
import DesignKit
import SwiftUI
struct SpaceAvatarImage: View {
@Environment(\.theme) var theme: ThemeSwiftUI
@Environment(\.dependencies) var dependencies: DependencyContainer
@StateObject var viewModel = AvatarViewModel()
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,12 +14,11 @@
// limitations under the License.
//
import Foundation
import DesignKit
import Combine
import DesignKit
import Foundation
import UIKit
/// Provides a simple api to retrieve and cache avatar images
protocol AvatarServiceProtocol {
func avatarImage(mxContentUri: String, avatarSize: AvatarSize) -> Future<UIImage, Error>
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,13 +14,12 @@
// limitations under the License.
//
import Foundation
import Combine
import DesignKit
import Foundation
/// Simple ViewModel that supports loading an avatar image
class AvatarViewModel: InjectableObject, ObservableObject {
@Inject var avatarService: AvatarServiceProtocol
@Published private(set) var viewState = AvatarViewState.empty
@@ -34,24 +33,22 @@ class AvatarViewModel: InjectableObject, ObservableObject {
/// - displayName: Display name of the avatar.
/// - colorCount: The count of total avatar colors used to generate the stable color index.
/// - avatarSize: The size of the avatar to fetch (as defined within DesignKit).
func loadAvatar(
mxContentUri: String?,
matrixItemId: String,
displayName: String?,
colorCount: Int,
avatarSize: AvatarSize) {
func loadAvatar(mxContentUri: String?,
matrixItemId: String,
displayName: String?,
colorCount: Int,
avatarSize: AvatarSize) {
let placeholderViewModel = PlaceholderAvatarViewModel(displayName: displayName,
matrixItemId: matrixItemId,
colorCount: colorCount)
self.viewState = .placeholder(placeholderViewModel.firstCharacterCapitalized, placeholderViewModel.stableColorIndex)
viewState = .placeholder(placeholderViewModel.firstCharacterCapitalized, placeholderViewModel.stableColorIndex)
guard let mxContentUri = mxContentUri, mxContentUri.count > 0 else {
return
}
avatarService.avatarImage(mxContentUri: mxContentUri, avatarSize: avatarSize)
avatarService.avatarImage(mxContentUri: mxContentUri, avatarSize: avatarSize)
.sink { completion in
guard case let .failure(error) = completion else { return }
UILog.error("[AvatarService] Failed to retrieve avatar", context: error)
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
//
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -27,7 +27,7 @@ struct PlaceholderAvatarViewModel {
/// Get the first character of the display name capitalized or else a space character.
var firstCharacterCapitalized: Character {
return displayName?.capitalized.first ?? " "
displayName?.capitalized.first ?? " "
}
/// Provides the same color each time for a specified matrixId
@@ -39,7 +39,7 @@ struct PlaceholderAvatarViewModel {
var stableColorIndex: Int {
// Sum all characters
let sum = matrixItemId.utf8
.map({ UInt($0) })
.map { UInt($0) }
.reduce(0, +)
// modulo the color count
return Int(sum) % colorCount
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,7 +20,6 @@ import SwiftUI
///
/// Provides any app level configuration the SwiftUI hierarchy might need (E.g. to monitor theme changes).
struct VectorContentModifier: ViewModifier {
@ObservedObject private var themePublisher = ThemePublisher.shared
@Environment(\.layoutDirection) private var defaultLayoutDirection
@@ -39,6 +38,6 @@ struct VectorContentModifier: ViewModifier {
extension View {
func vectorContent() -> some View {
self.modifier(VectorContentModifier())
modifier(VectorContentModifier())
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,7 +18,6 @@ import Foundation
/// Used for storing and resolving dependencies at runtime.
struct DependencyContainer {
// Stores the dependencies with type information removed.
private var dependencyStore: [String: Any] = [:]
@@ -26,7 +25,7 @@ struct DependencyContainer {
///
/// Given a particular `Type` (Inferred from return type),
/// generate a key and retrieve from storage.
///
///
/// - Returns: The resolved dependency.
func resolve<T>() -> T {
let key = String(describing: T.self)
@@ -33,7 +33,6 @@ extension EnvironmentValues {
}
extension View {
/// A modifier for adding a dependency to the SwiftUI view hierarchy's dependency container.
///
/// Important: When adding a dependency to cast it to the type in which it will be injected.
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,12 +22,9 @@ import Foundation
/// @Inject var someClass: SomeClass
/// ```
@propertyWrapper struct Inject<Value> {
static subscript<T: Injectable>(
_enclosingInstance instance: T,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
) -> Value {
static subscript<T: Injectable>(_enclosingInstance instance: T,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<T, Self>) -> Value {
get {
// Resolve dependencies from enclosing instance's `dependencies` property
let v: Value = instance.dependencies.resolve()
@@ -38,9 +35,9 @@ import Foundation
}
}
@available(*, unavailable, message: "This property wrapper can only be applied to classes")
@available(*, unavailable, message: "This property wrapper can only be applied to classes")
var wrappedValue: Value {
get { fatalError("wrappedValue get not used") }
set { fatalError("wrappedValue set not used. \(newValue)" ) }
set { fatalError("wrappedValue set not used. \(newValue)") }
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,9 +21,7 @@ protocol Injectable: AnyObject {
var dependencies: DependencyContainer! { get set }
}
extension Injectable {
/// Used to inject the dependency container into an Injectable.
/// - Parameter dependencies: The `DependencyContainer` to inject.
func inject(dependencies: DependencyContainer) {
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
//
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,7 +18,6 @@ import SceneKit
import SwiftUI
class EffectsScene: SCNScene {
// MARK: - Constants
private enum Constants {
@@ -31,14 +30,14 @@ class EffectsScene: SCNScene {
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 }
let colors: [[Float]] = theme.colors.namesAndAvatars.compactMap(\.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
particles.handle(.birth, forProperties: [.color]) { data, dataStride, _, count in
for index in 0..<count {
// Pick a random color to apply to the particle.
guard let color = colors.randomElement() else { continue }
@@ -60,7 +59,7 @@ class EffectsScene: SCNScene {
}
}
fileprivate extension Color {
private 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.
@@ -1,4 +1,4 @@
//
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,13 +14,12 @@
// limitations under the License.
//
import SwiftUI
import SceneKit
import SwiftUI
/// 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 EffectsView: UIViewRepresentable {
// MARK: - Private
@Environment(\.theme) private var theme
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -29,11 +29,11 @@ struct AlertInfo<T: Hashable>: Identifiable {
/// The alert's title.
let title: String
/// The alert's message (optional).
var message: String? = nil
var message: String?
/// The alert's primary button title and action. Defaults to an Ok button with no action.
var primaryButton: (title: String, action: (() -> Void)?) = (VectorL10n.ok, nil)
/// The alert's secondary button title and action.
var secondaryButton: (title: String, action: (() -> Void)?)? = nil
var secondaryButton: (title: String, action: (() -> Void)?)?
}
extension AlertInfo {
@@ -47,7 +47,7 @@ extension AlertInfo {
/// - id: An ID that identifies the error.
/// - error: The Error that occurred.
init?(id: T, error: NSError? = nil) {
guard error?.domain != NSURLErrorDomain && error?.code != NSURLErrorCancelled else { return nil }
guard error?.domain != NSURLErrorDomain, error?.code != NSURLErrorCancelled else { return nil }
self.id = id
title = error?.userInfo[NSLocalizedFailureReasonErrorKey] as? String ?? VectorL10n.error
@@ -1,4 +1,4 @@
//
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,18 +14,16 @@
// limitations under the License.
//
import Foundation
import CoreLocation
import Foundation
extension CLLocationCoordinate2D {
/// Compare two coordinates
/// - parameter coordinate: another coordinate to compare
/// - parameter precision:it represente how close you want the two coordinates
/// - return: bool value
func isEqual(to coordinate: CLLocationCoordinate2D, precision: Double) -> Bool {
if fabs(self.latitude - coordinate.latitude) <= precision && fabs(self.longitude - coordinate.longitude) <= precision {
if fabs(latitude - coordinate.latitude) <= precision, fabs(longitude - coordinate.longitude) <= precision {
return true
}
return false
@@ -0,0 +1,24 @@
//
// 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
extension Collection {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -27,5 +27,4 @@ extension MXRoom: Avatarable {
var displayName: String? {
summary.displayname
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -27,5 +27,4 @@ extension MXUser: Avatarable {
var displayName: String? {
displayname
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,17 +14,15 @@
// limitations under the License.
//
import Foundation
import Combine
import Foundation
extension Publisher where Failure == Never {
/// Same as `assign(to:on:)` but maintains a weak reference to object
///
/// Useful in cases where you want to pass self and not cause a retain cycle.
func weakAssign<T: AnyObject>(
to keyPath: ReferenceWritableKeyPath<T, Output>,
on object: T
) -> AnyCancellable {
func weakAssign<T: AnyObject>(to keyPath: ReferenceWritableKeyPath<T, Output>,
on object: T) -> AnyCancellable {
sink { [weak object] value in
object?[keyPath: keyPath] = value
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,6 +18,6 @@ import Foundation
class LocaleProvider: LocaleProviderType {
static var locale: Locale? {
return nil
nil
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,15 +23,19 @@ class PrintLogger: LoggerProtocol {
static func verbose(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
print(message())
}
static func debug(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
print(message())
}
static func info(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
print(message())
}
static func warning(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
print(message())
}
static func error(_ message: @autoclosure () -> StaticString, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
print(message())
}
+26 -32
View File
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,54 +20,48 @@ import Foundation
///
/// It can be configured at runtime with a suitable logger.
class UILog: LoggerProtocol {
static var _logger: LoggerProtocol.Type?
static func configure(logger: LoggerProtocol.Type) {
_logger = logger
}
static func verbose(
_ message: @autoclosure () -> Any,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
static func verbose(_ message: @autoclosure () -> Any,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
_logger?.verbose(message(), file, function, line: line, context: context)
}
static func debug(
_ message: @autoclosure () -> Any,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
static func debug(_ message: @autoclosure () -> Any,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
_logger?.debug(message(), file, function, line: line, context: context)
}
static func info(
_ message: @autoclosure () -> Any,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
static func info(_ message: @autoclosure () -> Any,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
_logger?.info(message(), file, function, line: line, context: context)
}
static func warning(
_ message: @autoclosure () -> Any,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
static func warning(_ message: @autoclosure () -> Any,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
_logger?.warning(message(), file, function, line: line, context: context)
}
static func error(
_ message: @autoclosure () -> StaticString,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
static func error(_ message: @autoclosure () -> StaticString,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
_logger?.error(message(), file, function, line: line, context: context)
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,6 +20,8 @@ import Foundation
enum MockAppScreens {
static let appScreens: [MockScreenState.Type] = [
MockUserSessionsOverviewScreenState.self,
MockUserSessionDetailsScreenState.self,
MockUserSessionOverviewScreenState.self,
MockLiveLocationLabPromotionScreenState.self,
MockLiveLocationSharingViewerScreenState.self,
MockAuthenticationLoginScreenState.self,
@@ -61,4 +63,3 @@ enum MockAppScreens {
MockSpaceSelectorScreenState.self
]
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,12 +24,11 @@ protocol MockScreenState {
}
extension MockScreenState {
/// Get a list of the screens for every screen state.
static var stateRenderer: StateRenderer {
let depsAndViews = screenStates.map(\.screenView)
let deps = depsAndViews.map({ $0.0 })
let views = depsAndViews.map({ $0.1 })
let depsAndViews = screenStates.map(\.screenView)
let deps = depsAndViews.map(\.0)
let views = depsAndViews.map(\.1)
let titles = screenStates.map(\.title)
var states = [ScreenStateInfo]()
@@ -45,7 +44,7 @@ extension MockScreenState {
/// All available screen state keys
static var screenNames: [String] {
screenStates.map { $0.title }
screenStates.map(\.title)
}
/// A title to represent the screen and it's screen state
@@ -54,12 +53,12 @@ extension MockScreenState {
}
private func simpleTypeName(_ type: Any) -> String {
String(describing: type).components(separatedBy: .punctuationCharacters).filter { $0.count > 0}.last!
String(describing: type).components(separatedBy: .punctuationCharacters).filter { $0.count > 0 }.last!
}
}
extension MockScreenState where Self: CaseIterable {
static var screenStates: [MockScreenState] {
return Array(self.allCases)
Array(allCases)
}
}
@@ -13,11 +13,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import SwiftUI
struct ScreenList: View {
private let allStates: [ScreenStateInfo]
@State private var searchQuery = ""
@@ -25,8 +23,9 @@ struct ScreenList: View {
init(screens: [MockScreenState.Type]) {
let states = screens
// swiftformat:disable:next preferKeyPath
.map { $0.stateRenderer }
.flatMap { $0.states }
.flatMap(\.states)
allStates = states
filteredStates = states
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -35,7 +35,7 @@ class StateRenderer {
addNavigation: Bool = false
) -> some View {
Group {
ForEach(0..<states.count) { i in
ForEach(0..<states.count, id: \.self) { i in
let state = self.states[i]
Self.wrapWithNavigation(addNavigation, view: state.view)
.previewDisplayName(state.screenTitle)
@@ -46,7 +46,7 @@ class StateRenderer {
@ViewBuilder
static func wrapWithNavigation<V: View>(_ wrap: Bool, view: V) -> some View {
if wrap {
NavigationView{
NavigationView {
view
.navigationBarTitleDisplayMode(.inline)
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,8 +14,8 @@
// limitations under the License.
//
import XCTest
import RiotSwiftUI
import XCTest
/// XCTestCase subclass to ease testing of `MockScreenState`.
/// Launches the app with an environment variable used to disable animations.
@@ -26,7 +26,7 @@ import RiotSwiftUI
class MockScreenTestCase: XCTestCase {
let app = XCUIApplication()
open override func setUpWithError() throws {
override open func setUpWithError() throws {
app.launchEnvironment = ["IS_RUNNING_UI_TESTS": "1"]
app.launch()
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,11 +23,11 @@ extension XCUIApplication {
textFields["searchQueryTextField"].tap()
typeText(identifier)
let button = self.buttons[identifier]
let button = buttons[identifier]
let footer = staticTexts["footerText"]
while !button.isHittable && !footer.isHittable {
self.tables.firstMatch.swipeUp()
while !button.isHittable, !footer.isHittable {
tables.firstMatch.swipeUp()
}
button.tap()
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,8 +14,8 @@
// limitations under the License.
//
import XCTest
import Combine
import XCTest
extension XCTestCase {
/// XCTest utility to wait for results from publishers, so that the output can be used for assertions.
@@ -29,11 +29,9 @@ extension XCTestCase {
/// - timeout: A timeout after which we give up.
/// - Throws: If it can't get the unwrapped result.
/// - Returns: The unwrapped result.
func xcAwait<T: Publisher>(
_ publisher: T,
timeout: TimeInterval = 10
) throws -> T.Output {
return try xcAwaitDeferred(publisher, timeout: timeout)()
func xcAwait<T: Publisher>(_ publisher: T,
timeout: TimeInterval = 10) throws -> T.Output {
try xcAwaitDeferred(publisher, timeout: timeout)()
}
/// XCTest utility that allows for a deferred wait of results from publishers, so that the output can be used for assertions.
@@ -48,12 +46,10 @@ extension XCTestCase {
/// - publisher: The publisher to wait on.
/// - timeout: A timeout after which we give up.
/// - Returns: A closure that starts the waiting of results when called. The closure will return the unwrapped result.
func xcAwaitDeferred<T: Publisher>(
_ publisher: T,
timeout: TimeInterval = 10
) -> (() throws -> (T.Output)) {
func xcAwaitDeferred<T: Publisher>(_ publisher: T,
timeout: TimeInterval = 10) -> (() throws -> (T.Output)) {
var result: Result<T.Output, Error>?
let expectation = self.expectation(description: "Awaiting publisher")
let expectation = expectation(description: "Awaiting publisher")
let cancellable = publisher.sink(
receiveCompletion: { completion in
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,8 +14,8 @@
// limitations under the License.
//
import Foundation
import DesignKit
import Foundation
extension ThemeIdentifier {
fileprivate static let defaultTheme = DefaultThemeSwiftUI()
+12 -12
View File
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,35 +14,35 @@
// limitations under the License.
//
import DesignKit
import Foundation
import SwiftUI
import DesignKit
private struct ThemeKey: EnvironmentKey {
static let defaultValue = ThemePublisher.shared.theme
}
extension EnvironmentValues {
var theme: ThemeSwiftUI {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
var theme: ThemeSwiftUI {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
extension View {
/// A theme modifier for setting the theme for this view and all its descendants in the hierarchy.
/// - Parameter theme: A theme to be set as the environment value.
/// - Returns: The target view with the theme applied.
func theme(_ theme: ThemeSwiftUI) -> some View {
environment(\.theme, theme)
}
func theme(_ theme: ThemeSwiftUI) -> some View {
environment(\.theme, theme)
}
}
extension View {
/// A theme modifier for setting the theme by id for this view and all its descendants in the hierarchy.
/// - Parameter themeId: ThemeIdentifier of a theme to be set as the environment value.
/// - Returns: The target view with the theme applied.
func theme(_ themeId: ThemeIdentifier) -> some View {
return environment(\.theme, themeId.themeSwiftUI)
}
func theme(_ themeId: ThemeIdentifier) -> some View {
environment(\.theme, themeId.themeSwiftUI)
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,21 +14,20 @@
// limitations under the License.
//
import Foundation
import Combine
import Foundation
/// Provides the theme and theme updates to SwiftUI.
///
/// Replaces the old ThemeObserver. Riot app can push updates to this class
/// removing the dependency of this class on the `ThemeService`.
class ThemePublisher: ObservableObject {
private static var _shared: ThemePublisher? = nil
private static var _shared: ThemePublisher?
static var shared: ThemePublisher {
if _shared == nil {
configure(themeId: .light)
}
return _shared!
if _shared == nil {
configure(themeId: .light)
}
return _shared!
}
@Published private(set) var theme: ThemeSwiftUI
@@ -38,7 +37,7 @@ class ThemePublisher: ObservableObject {
}
init(themeId: ThemeIdentifier) {
_theme = Published.init(initialValue: themeId.themeSwiftUI)
_theme = Published(initialValue: themeId.themeSwiftUI)
}
func republish(themeIdPublisher: AnyPublisher<ThemeIdentifier, Never>) {
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,8 +14,8 @@
// limitations under the License.
//
import Foundation
import DesignKit
import Foundation
protocol ThemeSwiftUI: ThemeSwiftUIType {
var identifier: ThemeIdentifier { get }
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,7 +18,6 @@ import Foundation
import SwiftUI
extension ThemeSwiftUI {
/// Get the stable display user color based on userId.
/// - Parameter userId: The user id used to hash.
/// - Returns: The SwiftUI color for the associated userId.
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,12 +14,12 @@
// limitations under the License.
//
import Foundation
import DesignKit
import Foundation
struct DarkThemeSwiftUI: ThemeSwiftUI {
var identifier: ThemeIdentifier = .dark
let isDark: Bool = true
let isDark = true
var colors: ColorSwiftUI = DarkColors.swiftUI
var fonts: FontSwiftUI = FontSwiftUI(values: ElementFonts())
var fonts = FontSwiftUI(values: ElementFonts())
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,12 +14,12 @@
// limitations under the License.
//
import Foundation
import DesignKit
import Foundation
struct DefaultThemeSwiftUI: ThemeSwiftUI {
var identifier: ThemeIdentifier = .light
let isDark: Bool = false
let isDark = false
var colors: ColorSwiftUI = LightColors.swiftUI
var fonts: FontSwiftUI = FontSwiftUI(values: ElementFonts())
var fonts = FontSwiftUI(values: ElementFonts())
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,7 +17,6 @@
import SwiftUI
struct BorderModifier<Shape: InsettableShape>: ViewModifier {
var color: Color
var borderWidth: CGFloat
var shape: Shape
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,20 +15,19 @@
//
import Foundation
import SwiftUI
import Introspect
import SwiftUI
/// A bordered style of text input
///
/// As defined in:
/// https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=2039%3A26415
struct BorderedInputFieldStyle: TextFieldStyle {
@Environment(\.theme) private var theme: ThemeSwiftUI
@Environment(\.isEnabled) private var isEnabled: Bool
var isEditing: Bool = false
var isError: Bool = false
var isEditing = false
var isError = false
private var borderColor: Color {
if isError {
@@ -47,7 +46,7 @@ struct BorderedInputFieldStyle: TextFieldStyle {
}
private var textColor: Color {
if (theme.identifier == ThemeIdentifier.dark) {
if theme.identifier == ThemeIdentifier.dark {
return (isEnabled ? theme.colors.primaryContent : theme.colors.tertiaryContent)
} else {
return (isEnabled ? theme.colors.primaryContent : theme.colors.quarterlyContent)
@@ -55,18 +54,18 @@ struct BorderedInputFieldStyle: TextFieldStyle {
}
private var backgroundColor: Color {
if !isEnabled && (theme.identifier == ThemeIdentifier.dark) {
if !isEnabled, theme.identifier == ThemeIdentifier.dark {
return theme.colors.quinaryContent
}
return theme.colors.background
}
private var placeholderColor: Color {
return theme.colors.tertiaryContent
theme.colors.tertiaryContent
}
private var borderWidth: CGFloat {
return isEditing || isError ? 2.0 : 1.5
isEditing || isError ? 2.0 : 1.5
}
func _body(configuration: TextField<_Label>) -> some View {
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,7 +18,7 @@ import SwiftUI
extension ThemableTextEditor {
func showClearButton(text: Binding<String>, alignment: VerticalAlignment = .top) -> some View {
return modifier(ClearViewModifier(alignment: alignment, text: text))
modifier(ClearViewModifier(alignment: alignment, text: text))
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,7 +19,6 @@ import SwiftUI
@available(iOS, introduced: 14.0, deprecated: 15.0, message: "Use Text with an AttributedString instead that includes a link and handle the tap by adding an OpenURLAction to the environment.")
/// A `Button`, that fakes having a tappable string inside of a regular string.
struct InlineTextButton: View {
private struct StringComponent {
let string: Substring
let isTinted: Bool
@@ -33,7 +32,6 @@ struct InlineTextButton: View {
private let components: [StringComponent]
private let action: () -> Void
// MARK: - Setup
/// Creates a new `InlineTextButton`.
@@ -43,7 +41,7 @@ struct InlineTextButton: View {
/// - action: The action to perform when tapping the button.
internal init(_ mainText: String, tappableText: String, action: @escaping () -> Void) {
guard let range = mainText.range(of: "%@") else {
self.components = [StringComponent(string: Substring(mainText), isTinted: false)]
components = [StringComponent(string: Substring(mainText), isTinted: false)]
self.action = action
return
}
@@ -52,7 +50,7 @@ struct InlineTextButton: View {
let middleComponent = StringComponent(string: Substring(tappableText), isTinted: true)
let lastComponent = StringComponent(string: mainText[range.upperBound...], isTinted: false)
self.components = [firstComponent, middleComponent, lastComponent]
components = [firstComponent, middleComponent, lastComponent]
self.action = action
}
@@ -63,7 +61,7 @@ struct InlineTextButton: View {
EmptyView()
}
.buttonStyle(Style(components: components))
.accessibilityLabel(components.map { $0.string }.joined())
.accessibilityLabel(components.map(\.string).joined())
}
private struct Style: ButtonStyle {
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,14 +17,13 @@
import SwiftUI
struct MultilineTextField: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
@Binding private var text: String
@State private var dynamicHeight: CGFloat = 100
@State private var isEditing = false
private var placeholder: String = ""
private var placeholder = ""
private var showingPlaceholder: Bool {
text.isEmpty
@@ -32,11 +31,11 @@ struct MultilineTextField: View {
init(_ placeholder: String, text: Binding<String>) {
self.placeholder = placeholder
self._text = text
_text = text
}
private var textColor: Color {
if (theme.identifier == ThemeIdentifier.dark) {
if theme.identifier == ThemeIdentifier.dark {
return theme.colors.primaryContent
} else {
return theme.colors.primaryContent
@@ -44,11 +43,11 @@ struct MultilineTextField: View {
}
private var backgroundColor: Color {
return theme.colors.background
theme.colors.background
}
private var placeholderColor: Color {
return theme.colors.tertiaryContent
theme.colors.tertiaryContent
}
private var borderColor: Color {
@@ -60,7 +59,7 @@ struct MultilineTextField: View {
}
private var borderWidth: CGFloat {
return isEditing ? 2.0 : 1.5
isEditing ? 2.0 : 1.5
}
var body: some View {
@@ -91,7 +90,7 @@ struct MultilineTextField: View {
}
}
fileprivate struct UITextViewWrapper: UIViewRepresentable {
private struct UITextViewWrapper: UIViewRepresentable {
typealias UIViewType = UITextView
@Binding var text: String
@@ -115,8 +114,8 @@ fileprivate struct UITextViewWrapper: UIViewRepresentable {
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
if uiView.text != self.text {
uiView.text = self.text
if uiView.text != text {
uiView.text = text
}
UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
@@ -132,7 +131,7 @@ fileprivate struct UITextViewWrapper: UIViewRepresentable {
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, height: $calculatedHeight, isEditing: $isEditing)
Coordinator(text: $text, height: $calculatedHeight, isEditing: $isEditing)
}
final class Coordinator: NSObject, UITextViewDelegate {
@@ -142,7 +141,7 @@ fileprivate struct UITextViewWrapper: UIViewRepresentable {
init(text: Binding<String>, height: Binding<CGFloat>, isEditing: Binding<Bool>) {
self.text = text
self.calculatedHeight = height
calculatedHeight = height
self.isEditing = isEditing
}
@@ -171,9 +170,8 @@ fileprivate struct UITextViewWrapper: UIViewRepresentable {
}
struct MultilineTextField_Previews: PreviewProvider {
static var previews: some View {
return Group {
Group {
VStack {
PreviewWrapper()
PlaceholderPreviewWrapper()
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,11 +17,9 @@
import SwiftUI
struct OptionButton: View {
// MARK: - Style
private struct Style: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.97 : 1)
@@ -61,8 +59,7 @@ struct OptionButton: View {
.background(theme.colors.quinaryContent)
.foregroundColor(theme.colors.secondaryContent)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
)
})
.buttonStyle(Style())
}
}
@@ -73,14 +70,14 @@ struct OptionButton_Previews: PreviewProvider {
static var previews: some View {
Group {
VStack {
OptionButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", detailMessage: "Some details for this option", action: {}).theme(.light)
OptionButton(icon: nil, title: "A title", detailMessage: "Some details for this option", action: {}).theme(.light)
OptionButton(icon: nil, title: "A title", detailMessage: nil, action: {}).theme(.light)
OptionButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", detailMessage: "Some details for this option", action: { }).theme(.light)
OptionButton(icon: nil, title: "A title", detailMessage: "Some details for this option", action: { }).theme(.light)
OptionButton(icon: nil, title: "A title", detailMessage: nil, action: { }).theme(.light)
}
VStack {
OptionButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", detailMessage: "Some details for this option", action: {}).theme(.dark)
OptionButton(icon: nil, title: "A title", detailMessage: "Some details for this option", action: {}).theme(.dark)
OptionButton(icon: nil, title: "A title", detailMessage: nil, action: {}).theme(.dark)
OptionButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", detailMessage: "Some details for this option", action: { }).theme(.dark)
OptionButton(icon: nil, title: "A title", detailMessage: "Some details for this option", action: { }).theme(.dark)
OptionButton(icon: nil, title: "A title", detailMessage: nil, action: { }).theme(.dark)
}.preferredColorScheme(.dark)
}
.padding()
@@ -19,7 +19,6 @@ import SwiftUI
/// Adds a reveal password button (e.g. an eye button) on the
/// right side of the view. For use with `ThemableTextField`.
struct PasswordButtonModifier: ViewModifier {
// MARK: - Properties
let text: String
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,7 +20,7 @@ struct PrimaryActionButtonStyle: ButtonStyle {
@Environment(\.theme) private var theme
@Environment(\.isEnabled) private var isEnabled
var customColor: Color? = nil
var customColor: Color?
private var fontColor: Color {
// Always white unless disabled with a dark theme.
@@ -69,7 +69,7 @@ struct PrimaryActionButtonStyle_Previews: PreviewProvider {
.buttonStyle(PrimaryActionButtonStyle(customColor: .clear))
Button("Red BG") { }
.buttonStyle(PrimaryActionButtonStyle(customColor: .red))
.buttonStyle(PrimaryActionButtonStyle(customColor: .red))
}
.padding()
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,7 +17,6 @@
import SwiftUI
struct RadioButton: View {
// MARK: - Properties
var title: String
@@ -61,8 +60,8 @@ struct RadioButton_Previews: PreviewProvider {
static var buttonGroup: some View {
VStack {
RadioButton(title: "A title", selected: false, action: {})
RadioButton(title: "A title", selected: true, action: {})
RadioButton(title: "A title", selected: false, action: { })
RadioButton(title: "A title", selected: true, action: { })
}
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,7 +18,7 @@ import SwiftUI
/// Positions this view within an invisible frame that fills the width of its parent view,
/// whilst limiting the width of the content to a readable size (which is customizable).
fileprivate struct ReadableFrameModifier: ViewModifier {
private struct ReadableFrameModifier: ViewModifier {
var maxWidth: CGFloat
func body(content: Content) -> some View {
@@ -18,8 +18,7 @@ import UIKit
/// `ResponderManager` is used to chain `SwiftUI` text editing views that embed `UIKit` text editing views using `UIViewRepresentable`
class ResponderManager {
private static var tagIndex: Int = 1000
private static var tagIndex = 1000
private static var registeredResponders = NSMapTable<NSNumber, UIView>(keyOptions: .strongMemory, valueOptions: .weakMemory)
private static var nextIndex: Int {
@@ -63,7 +62,7 @@ class ResponderManager {
/// Tries to get the focused registered responder and give the focus to it's next responder
/// - Returns: `True` if the next responder has been found and is successfully focused. `False` otherwise.
static func makeActiveNextResponder() -> Bool {
guard let firstResponder = self.firstResponder else {
guard let firstResponder = firstResponder else {
return false
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,17 +17,16 @@
import SwiftUI
struct RoundedBorderTextEditor: View {
// MARK: - Properties
var title: String? = nil
var title: String?
let placeHolder: String
@Binding var text: String
var textMaxHeight: CGFloat? = nil
var error: String? = nil
var textMaxHeight: CGFloat?
var error: String?
var onTextChanged: ((String) -> Void)? = nil
var onEditingChanged: ((Bool) -> Void)? = nil
var onTextChanged: ((String) -> Void)?
var onEditingChanged: ((Bool) -> Void)?
@State private var editing = false
@@ -62,7 +61,7 @@ struct RoundedBorderTextEditor: View {
})
.showClearButton(text: $text)
// Found no good solution here. Hidding next button for the moment
// .modifier(NextViewModifier(alignment: .bottomTrailing, isEditing: $editing))
// .modifier(NextViewModifier(alignment: .bottomTrailing, isEditing: $editing))
.padding(EdgeInsets(top: 2, leading: 6, bottom: 0, trailing: 0))
.onChange(of: text, perform: { newText in
onTextChanged?(newText)
@@ -82,7 +81,7 @@ struct RoundedBorderTextEditor: View {
}
.background(RoundedRectangle(cornerRadius: 8).fill(theme.colors.background))
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(editing ? theme.colors.accent : (error == nil ? theme.colors.quinaryContent : theme.colors.alert), lineWidth: editing || error != nil ? 2 : 1))
.stroke(editing ? theme.colors.accent : (error == nil ? theme.colors.quinaryContent : theme.colors.alert), lineWidth: editing || error != nil ? 2 : 1))
.frame(height: textMaxHeight)
if let error = self.error {
Text(error)
@@ -101,7 +100,6 @@ struct RoundedBorderTextEditor: View {
struct ThemableTextEditor_Previews: PreviewProvider {
static var previews: some View {
Group {
sampleView.theme(.light).preferredColorScheme(.light)
sampleView.theme(.dark).preferredColorScheme(.dark)
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,22 +17,21 @@
import SwiftUI
struct RoundedBorderTextField: View {
// MARK: - Properties
var title: String? = nil
var title: String?
let placeHolder: String
@Binding var text: String
var footerText: String? = nil
var isError: Bool = false
var footerText: String?
var isError = false
var isFirstResponder = false
var configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration()
var configuration = UIKitTextInputConfiguration()
@State var isSecureTextVisible = false
var onTextChanged: ((String) -> Void)? = nil
var onEditingChanged: ((Bool) -> Void)? = nil
var onCommit: (() -> Void)? = nil
var onTextChanged: ((String) -> Void)?
var onEditingChanged: ((Bool) -> Void)?
var onCommit: (() -> Void)?
// MARK: Private
@@ -101,7 +100,7 @@ struct RoundedBorderTextField: View {
private var borderColor: Color {
if isEditing {
return theme.colors.accent
} else if footerText != nil && isError {
} else if footerText != nil, isError {
return theme.colors.alert
} else {
return theme.colors.quinaryContent
@@ -118,7 +117,6 @@ struct RoundedBorderTextField: View {
struct TextFieldWithError_Previews: PreviewProvider {
static var previews: some View {
Group {
sampleView.theme(.light).preferredColorScheme(.light)
sampleView.theme(.dark).preferredColorScheme(.dark)
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,7 +18,6 @@ import Foundation
import SwiftUI
struct RoundedCornerShape: Shape {
let radius: CGFloat
let corners: UIRectCorner
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,7 +22,6 @@ import SwiftUI
Replace with Swift 5.5 bindings enumerator later.
*/
struct SafeBindingCollectionEnumerator<T: RandomAccessCollection & MutableCollection, C: View>: View {
typealias BoundElement = Binding<T.Element>
private let binding: BoundElement
private let content: (BoundElement) -> C
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,6 +33,6 @@ struct ScreenTrackerViewModifier: ViewModifier {
extension View {
func track(screen: AnalyticsScreen) -> some View {
return self.modifier(ScreenTrackerViewModifier(screen: screen))
modifier(ScreenTrackerViewModifier(screen: screen))
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,7 +17,6 @@
import SwiftUI
struct SearchBar: View {
// MARK: - Properties
var placeholder: String
@@ -49,7 +48,7 @@ struct SearchBar: View {
.foregroundColor(theme.colors.quarterlyContent)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
if isEditing && !text.isEmpty {
if isEditing, !text.isEmpty {
Button(action: {
self.text = ""
}) {
@@ -20,7 +20,7 @@ struct SecondaryActionButtonStyle: ButtonStyle {
@Environment(\.theme) private var theme
@Environment(\.isEnabled) private var isEnabled
var customColor: Color? = nil
var customColor: Color?
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
@@ -29,8 +29,8 @@ struct SecondaryActionButtonStyle: ButtonStyle {
.foregroundColor(customColor ?? theme.colors.accent)
.font(theme.fonts.body)
.background(RoundedRectangle(cornerRadius: 8)
.strokeBorder()
.foregroundColor(customColor ?? theme.colors.accent))
.strokeBorder()
.foregroundColor(customColor ?? theme.colors.accent))
.opacity(opacity(when: configuration.isPressed))
}
@@ -62,7 +62,7 @@ struct SecondaryActionButtonStyle_Previews: PreviewProvider {
.disabled(true)
Button("Red BG") { }
.buttonStyle(SecondaryActionButtonStyle(customColor: .red))
.buttonStyle(SecondaryActionButtonStyle(customColor: .red))
Button { } label: {
Text("Custom")
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,14 +14,13 @@
// limitations under the License.
//
import SwiftUI
import DesignKit
import SwiftUI
@available(iOS, introduced: 14.0, deprecated: 15.0, message: "Use Text with an AttributedString instead.")
/// A `Text` view that renders attributed strings with their `.font` and `.foregroundColor` attributes.
/// This view is a workaround for iOS 13/14 not supporting `AttributedString`.
struct StyledText: View {
// MARK: - Properties
// MARK: Private
@@ -31,8 +30,8 @@ struct StyledText: View {
/// A string with a bold property.
private struct StringComponent {
let string: String
var font: Font? = nil
var color: Color? = nil
var font: Font?
var color: Color?
}
/// Internal representation of the string as composable parts.
@@ -47,7 +46,7 @@ struct StyledText: View {
let range = NSRange(location: 0, length: attributedString.length)
let string = attributedString.string as NSString
attributedString.enumerateAttributes(in: range, options: []) { attributes, range, stop in
attributedString.enumerateAttributes(in: range, options: []) { attributes, range, _ in
let font = attributes[.font] as? UIFont
let color = attributes[.foregroundColor] as? UIColor
@@ -66,7 +65,7 @@ struct StyledText: View {
/// Creates a `StyledText` using a plain string.
/// - Parameter string: The plain string to display
init(_ string: String) {
self.components = [StringComponent(string: string, font: nil)]
components = [StringComponent(string: string, font: nil)]
}
// MARK: - Views
@@ -80,7 +79,6 @@ struct StyledText: View {
}
}
struct StyledText_Previews: PreviewProvider {
static func prettyText() -> NSAttributedString {
let string = NSMutableAttributedString(string: "T", attributes: [
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,11 +17,9 @@
import SwiftUI
struct ThemableButton: View {
// MARK: - Style
private struct Style: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.97 : 1)
@@ -67,12 +65,12 @@ struct ThemableButton_Previews: PreviewProvider {
static var previews: some View {
Group {
VStack(alignment: .center, spacing: 20) {
ThemableButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", action: {}).theme(.light).preferredColorScheme(.light)
ThemableButton(icon: nil, title: "A title", action: {}).theme(.light).preferredColorScheme(.light)
ThemableButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", action: { }).theme(.light).preferredColorScheme(.light)
ThemableButton(icon: nil, title: "A title", action: { }).theme(.light).preferredColorScheme(.light)
}
VStack(alignment: .center, spacing: 20) {
ThemableButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", action: {}).theme(.dark).preferredColorScheme(.dark)
ThemableButton(icon: nil, title: "A title", action: {}).theme(.dark).preferredColorScheme(.dark)
ThemableButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", action: { }).theme(.dark).preferredColorScheme(.dark)
ThemableButton(icon: nil, title: "A title", action: { }).theme(.dark).preferredColorScheme(.dark)
}
}
.padding()
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,7 +17,6 @@
import SwiftUI
struct ThemableNavigationBar: View {
// MARK: - Style
// MARK: - Properties
@@ -36,8 +35,7 @@ struct ThemableNavigationBar: View {
@ViewBuilder
var body: some View {
HStack {
Button(action: {backAction()})
{
Button(action: { backAction() }) {
Image(uiImage: Asset.Images.spacesModalBack.image)
.renderingMode(.template)
.foregroundColor(theme.colors.secondaryContent)
@@ -49,8 +47,7 @@ struct ThemableNavigationBar: View {
.foregroundColor(theme.colors.primaryContent)
}
Spacer()
Button(action: {closeAction()})
{
Button(action: { closeAction() }) {
Image(uiImage: Asset.Images.spacesModalClose.image)
.renderingMode(.template)
.foregroundColor(theme.colors.secondaryContent)
@@ -68,16 +65,16 @@ struct NavigationBar_Previews: PreviewProvider {
static var previews: some View {
Group {
VStack {
ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {})
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {})
ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {})
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {})
ThemableNavigationBar(title: nil, showBackButton: true, backAction: { }, closeAction: { })
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: { }, closeAction: { })
ThemableNavigationBar(title: nil, showBackButton: false, backAction: { }, closeAction: { })
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: { }, closeAction: { })
}
VStack {
ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: nil, showBackButton: true, backAction: { }, closeAction: { }).theme(.dark)
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: { }, closeAction: { }).theme(.dark)
ThemableNavigationBar(title: nil, showBackButton: false, backAction: { }, closeAction: { }).theme(.dark)
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: { }, closeAction: { }).theme(.dark)
}.preferredColorScheme(.dark)
}
.padding()
@@ -16,20 +16,18 @@
import SwiftUI
struct ThemableTextEditor: UIViewRepresentable {
// MARK: Properties
@Binding var text: String
@State var configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration()
@State var configuration = UIKitTextInputConfiguration()
var onEditingChanged: ((_ edit: Bool) -> Void)?
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
private let textView: UITextView = UITextView()
private let textView = UITextView()
private let internalParams = InternalParams()
// MARK: Setup
@@ -37,8 +35,8 @@ struct ThemableTextEditor: UIViewRepresentable {
init(text: Binding<String>,
configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration(),
onEditingChanged: ((_ edit: Bool) -> Void)? = nil) {
self._text = text
self._configuration = State(initialValue: configuration)
_text = text
_configuration = State(initialValue: configuration)
self.onEditingChanged = onEditingChanged
ResponderManager.register(view: textView)
@@ -63,8 +61,8 @@ struct ThemableTextEditor: UIViewRepresentable {
uiView.textColor = UIColor(theme.colors.primaryContent)
uiView.tintColor = UIColor(theme.colors.accent)
if uiView.text != self.text {
uiView.text = self.text
if uiView.text != text {
uiView.text = text
}
uiView.keyboardType = configuration.keyboardType
@@ -81,7 +79,7 @@ struct ThemableTextEditor: UIViewRepresentable {
// MARK: - Private
private func replaceText(with newText: String) {
self.text = newText
text = newText
}
private class InternalParams {
@@ -91,7 +89,7 @@ struct ThemableTextEditor: UIViewRepresentable {
// MARK: - Coordinator
func makeCoordinator() -> Coordinator {
return Coordinator(self)
Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
@@ -19,18 +19,17 @@ import SwiftUI
struct UIKitTextInputConfiguration {
var keyboardType: UIKeyboardType = .default
var returnKeyType: UIReturnKeyType = .default
var isSecureTextEntry: Bool = false
var isSecureTextEntry = false
var autocapitalizationType: UITextAutocapitalizationType = .sentences
var autocorrectionType: UITextAutocorrectionType = .default
}
struct ThemableTextField: UIViewRepresentable {
// MARK: Properties
@State var placeholder: String?
@Binding var text: String
@State var configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration()
@State var configuration = UIKitTextInputConfiguration()
@Binding var isSecureTextVisible: Bool
var onEditingChanged: ((_ edit: Bool) -> Void)?
var onCommit: (() -> Void)?
@@ -39,7 +38,7 @@ struct ThemableTextField: UIViewRepresentable {
@Environment(\.theme) private var theme: ThemeSwiftUI
private let textField: UITextField = UITextField()
private let textField = UITextField()
private let internalParams = InternalParams()
// MARK: Setup
@@ -50,10 +49,10 @@ struct ThemableTextField: UIViewRepresentable {
isSecureTextVisible: Binding<Bool> = .constant(false),
onEditingChanged: ((_ edit: Bool) -> Void)? = nil,
onCommit: (() -> Void)? = nil) {
self._text = text
self._placeholder = State(initialValue: placeholder)
self._configuration = State(initialValue: configuration)
self._isSecureTextVisible = isSecureTextVisible
_text = text
_placeholder = State(initialValue: placeholder)
_configuration = State(initialValue: configuration)
_isSecureTextVisible = isSecureTextVisible
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
@@ -84,8 +83,8 @@ struct ThemableTextField: UIViewRepresentable {
uiView.textColor = UIColor(theme.colors.primaryContent)
uiView.tintColor = UIColor(theme.colors.accent)
if uiView.text != self.text {
uiView.text = self.text
if uiView.text != text {
uiView.text = text
}
uiView.placeholder = placeholder
@@ -103,17 +102,16 @@ struct ThemableTextField: UIViewRepresentable {
// MARK: - Private
private func replaceText(with newText: String) {
self.text = newText
text = newText
}
// MARK: - Coordinator
func makeCoordinator() -> Coordinator {
return Coordinator(self)
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: ThemableTextField
init(_ parent: ThemableTextField) {
@@ -146,14 +144,13 @@ struct ThemableTextField: UIViewRepresentable {
private class InternalParams {
var isFirstResponder = false
}
}
// MARK: - modifiers
extension ThemableTextField {
func makeFirstResponder() -> ThemableTextField {
return makeFirstResponder(true)
makeFirstResponder(true)
}
func makeFirstResponder(_ isFirstResponder: Bool) -> ThemableTextField {
@@ -167,7 +164,7 @@ extension ThemableTextField {
/// - alignment: The vertical alignment of the button in the text field. Default to `center`
@ViewBuilder
func addButton(_ show: Bool, alignment: VerticalAlignment = .center) -> some View {
if show && configuration.isSecureTextEntry {
if show, configuration.isSecureTextEntry {
modifier(PasswordButtonModifier(text: text,
isSecureTextVisible: $isSecureTextVisible,
alignment: alignment))
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,7 +20,7 @@ import SwiftUI
extension View {
@ViewBuilder func isHidden(_ isHidden: Bool) -> some View {
if isHidden {
self.hidden()
hidden()
} else {
self
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,7 +18,6 @@ import SwiftUI
/// A modifier for showing the wait overlay view over a view.
struct WaitOverlayModifier: ViewModifier {
var allowUserInteraction: Bool
var show: Bool
var message: String?
@@ -27,15 +26,16 @@ struct WaitOverlayModifier: ViewModifier {
func body(content: Content) -> some View {
content
.modifier(WaitOverlay(
allowUserInteraction: allowUserInteraction,
message: message,
isLoading: show))
allowUserInteraction: allowUserInteraction,
message: message,
isLoading: show
))
}
}
extension View {
func waitOverlay(show: Bool, message: String? = nil, allowUserInteraction: Bool = true) -> some View {
self.modifier(WaitOverlayModifier(allowUserInteraction: allowUserInteraction, show: show, message: message))
modifier(WaitOverlayModifier(allowUserInteraction: allowUserInteraction, show: show, message: message))
}
}
@@ -44,7 +44,7 @@ struct WaitOverlay: ViewModifier {
// MARK: - Properties
var alignment: Alignment = .center
var allowUserInteraction: Bool = true
var allowUserInteraction = true
var message: String?
var isLoading: Bool
@@ -66,8 +66,7 @@ struct WaitOverlay: ViewModifier {
// MARK: - Public
public func body(content: Content) -> some View
{
public func body(content: Content) -> some View {
ZStack {
content
if isLoading {
@@ -89,7 +88,7 @@ struct WaitOverlay: ViewModifier {
}
.padding(12)
.background(RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(theme.colors.navigation.opacity(0.9)))
.fill(theme.colors.navigation.opacity(0.9)))
}
.edgesIgnoringSafeArea(.all)
.transition(.opacity)
@@ -103,24 +102,24 @@ struct WaitOverlay_Previews: PreviewProvider {
static var previews: some View {
Group {
VStack {
ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {})
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {})
ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {})
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {})
ThemableNavigationBar(title: nil, showBackButton: true, backAction: { }, closeAction: { })
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: { }, closeAction: { })
ThemableNavigationBar(title: nil, showBackButton: false, backAction: { }, closeAction: { })
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: { }, closeAction: { })
}
.modifier(WaitOverlay(isLoading: true))
VStack {
ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {})
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {})
ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {})
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {})
ThemableNavigationBar(title: nil, showBackButton: true, backAction: { }, closeAction: { })
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: { }, closeAction: { })
ThemableNavigationBar(title: nil, showBackButton: false, backAction: { }, closeAction: { })
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: { }, closeAction: { })
}
.modifier(WaitOverlay(alignment:.topLeading, isLoading: true))
.modifier(WaitOverlay(alignment: .topLeading, isLoading: true))
VStack {
ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: nil, showBackButton: true, backAction: { }, closeAction: { }).theme(.dark)
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: { }, closeAction: { }).theme(.dark)
ThemableNavigationBar(title: nil, showBackButton: false, backAction: { }, closeAction: { }).theme(.dark)
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: { }, closeAction: { }).theme(.dark)
}
.modifier(WaitOverlay(isLoading: true)).theme(.dark)
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,12 +14,11 @@
// limitations under the License.
//
import Foundation
import Combine
import Foundation
import Combine
import Combine
import Foundation
/// A constrained and concise interface for interacting with the ViewModel.
///
@@ -34,7 +33,7 @@ import Combine
/// It provides a nice layer of consistency and also safety. As we are not passing the `ViewModel` to the view directly, shortcuts/hacks
/// can't be made into the `ViewModel`.
@dynamicMemberLookup
class ViewModelContext<ViewState:BindableState, ViewAction>: ObservableObject {
class ViewModelContext<ViewState: BindableState, ViewAction>: ObservableObject {
// MARK: - Properties
// MARK: Private
@@ -74,8 +73,7 @@ class ViewModelContext<ViewState:BindableState, ViewAction>: ObservableObject {
/// a specific portion of state that can be safely bound to.
/// If we decide to add more features to our state management (like doing state processing off the main thread)
/// we can do it in this centralised place.
class StateStoreViewModel<State: BindableState, StateAction, ViewAction> {
class StateStoreViewModel<State: BindableState, ViewAction> {
typealias Context = ViewModelContext<State, ViewAction>
// MARK: - Properties
@@ -98,45 +96,17 @@ class StateStoreViewModel<State: BindableState, StateAction, ViewAction> {
// MARK: Setup
init(initialViewState: State) {
self.context = Context(initialViewState: initialViewState)
self.context.viewActions.sink { [weak self] action in
context = Context(initialViewState: initialViewState)
context.viewActions.sink { [weak self] action in
guard let self = self else { return }
self.process(viewAction: action)
}
.store(in: &cancellables)
}
/// Send state actions to modify the state within the reducer.
/// - Parameter action: The state action to send to the reducer.
@available(*, deprecated, message: "Mutate state directly instead")
func dispatch(action: StateAction) {
Self.reducer(state: &context.viewState, action: action)
}
/// Send state actions from a publisher to modify the state within the reducer.
/// - Parameter actionPublisher: The publisher that produces actions to be sent to the reducer
@available(*, deprecated, message: "Mutate state directly instead")
func dispatch(actionPublisher: AnyPublisher<StateAction, Never>) {
actionPublisher.sink { [weak self] action in
guard let self = self else { return }
Self.reducer(state: &self.context.viewState, action: action)
}
.store(in: &cancellables)
}
/// Override to handle mutations to the `State`
///
/// A redux style reducer, all modifications to state happen here.
/// - Parameters:
/// - state: The `inout` state to be modified,
/// - action: The action that defines which state modification should take place.
class func reducer(state: inout State, action: StateAction) {
//Default implementation, -no-op
}
/// Override to handles incoming `ViewAction`s from the `ViewModel`.
/// - Parameter viewAction: The `ViewAction` to be processed in `ViewModel` implementation.
func process(viewAction: ViewAction) {
//Default implementation, -no-op
// Default implementation, -no-op
}
}