mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-29 12:46:58 +02:00
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:
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user