mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-29 12:46:58 +02:00
Add Simple Template Example
- Add a simple Template example that shows a user profile with avatar, displayName and presence. - ScreenCoordinator: closure based with less protocols and delegates. - Reducer: Reducer function that manages all state modifications. - SwiftUI View: Decomposes UI into appropriate sub components. - Uses Theme and Dependency Management Infrastructure
This commit is contained in:
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
final class TemplateUserProfileCoordinator: Coordinator {
|
||||
|
||||
typealias Completion = () -> Void
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let session: MXSession
|
||||
private let templateUserProfileViewController: UIViewController
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var completion: Completion?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
init(session: MXSession) {
|
||||
self.session = session
|
||||
let hostViewController = VectorHostingController()
|
||||
templateUserProfileViewController = UINavigationController(rootViewController: hostViewController)
|
||||
let rootView = TemplateUserProfile.instantiate(session: session, completion: self.userProfileCompletion(result:))
|
||||
hostViewController.setRoot(view: rootView)
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
func userProfileCompletion(result: TemplateUserProfile.Result) {
|
||||
switch result {
|
||||
case .cancel, .done:
|
||||
completion?()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
|
||||
func start() {
|
||||
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.templateUserProfileViewController
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension TemplateUserProfile {
|
||||
static func instantiate(session: MXSession, completion: @escaping TemplateUserProfile.Completion) -> some View {
|
||||
let templateUserProfileViewModel = TemplateUserProfileViewModel(userService: MXTemplateUserService(session: session))
|
||||
let templateUserProfile = TemplateUserProfile(viewModel: templateUserProfileViewModel, completion: completion)
|
||||
return templateUserProfile.addDependency(MXAvatarService.instantiate(mediaManager: session.mediaManager))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TemplatePresence {
|
||||
case online
|
||||
case idle
|
||||
case offline
|
||||
}
|
||||
|
||||
extension TemplatePresence {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .online:
|
||||
return VectorL10n.roomParticipantsOnline
|
||||
case .idle:
|
||||
return VectorL10n.roomParticipantsIdle
|
||||
case .offline:
|
||||
return VectorL10n.roomParticipantsOffline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TemplatePresence: CaseIterable { }
|
||||
|
||||
extension TemplatePresence: Identifiable {
|
||||
var id: Self { self }
|
||||
}
|
||||
+2
-9
@@ -16,13 +16,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TemplateMockUserService: TemplateUserServiceType {
|
||||
|
||||
static let example = TemplateMockUserService(userId: "123", displayName: "Alice", avatarUrl: "mx123@matrix.com", currentlyActive: true, lastActive: 123456)
|
||||
|
||||
let userId: String
|
||||
let displayName: String?
|
||||
let avatarUrl: String?
|
||||
let currentlyActive: Bool
|
||||
let lastActive: UInt
|
||||
enum TemplateProfileStateAction {
|
||||
case updatePresence(TemplatePresence)
|
||||
}
|
||||
+1
@@ -19,4 +19,5 @@ import Foundation
|
||||
struct TemplateUserProfileViewState {
|
||||
let avatar: AvatarInputType?
|
||||
let displayName: String?
|
||||
var presence: TemplatePresence = .offline
|
||||
}
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class MXTemplateUserService: TemplateUserServiceType {
|
||||
|
||||
let session: MXSession
|
||||
var listenerReference: Any!
|
||||
@Published var presence: TemplatePresence = .offline
|
||||
|
||||
init(session: MXSession) {
|
||||
self.session = session
|
||||
|
||||
let listenerReference = session.myUser.listen { [weak self] event in
|
||||
guard let self = self,
|
||||
let event = event,
|
||||
case .presence = MXEventType(identifier: event.eventId)
|
||||
else { return }
|
||||
self.presence = TemplatePresence(mxPresence: self.session.myUser.presence)
|
||||
}
|
||||
self.listenerReference = listenerReference
|
||||
}
|
||||
|
||||
var userId: String {
|
||||
return session.myUser.userId
|
||||
}
|
||||
|
||||
var displayName: String? {
|
||||
session.myUser.displayname
|
||||
}
|
||||
|
||||
var avatarUrl: String? {
|
||||
session.myUser.avatarUrl
|
||||
}
|
||||
|
||||
var presencePublisher: AnyPublisher<TemplatePresence, Never> {
|
||||
$presence.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
deinit {
|
||||
session.myUser.removeListener(listenerReference)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension TemplatePresence {
|
||||
|
||||
init(mxPresence: MXPresence) {
|
||||
|
||||
switch mxPresence {
|
||||
case MXPresenceOnline:
|
||||
self = .online
|
||||
case MXPresenceUnavailable:
|
||||
self = .idle
|
||||
case MXPresenceOffline, MXPresenceUnknown:
|
||||
self = .offline
|
||||
default:
|
||||
self = .offline
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class MockTemplateUserService: TemplateUserServiceType, ObservableObject {
|
||||
|
||||
static let example = MockTemplateUserService()
|
||||
@Published var presence: TemplatePresence = .online
|
||||
var presencePublisher: AnyPublisher<TemplatePresence, Never> {
|
||||
$presence.eraseToAnyPublisher()
|
||||
}
|
||||
let userId: String = "123"
|
||||
let displayName: String? = "Alice"
|
||||
let avatarUrl: String? = "mx123@matrix.com"
|
||||
let currentlyActive: Bool = true
|
||||
let lastActive: UInt = 1630596918513
|
||||
}
|
||||
+4
-2
@@ -15,15 +15,17 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
protocol TemplateUserServiceType: Avatarable {
|
||||
var userId: String { get }
|
||||
var displayName: String? { get }
|
||||
var avatarUrl: String? { get }
|
||||
var currentlyActive: Bool { get }
|
||||
var lastActive: UInt { get }
|
||||
var presencePublisher: AnyPublisher<TemplatePresence, Never> { get }
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension TemplateUserServiceType {
|
||||
var mxContentUri: String? {
|
||||
avatarUrl
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplatePresenceView: View {
|
||||
|
||||
let presense: TemplatePresence
|
||||
|
||||
var foregroundColor: Color {
|
||||
switch presense {
|
||||
case .online:
|
||||
return .green
|
||||
case .idle:
|
||||
return .orange
|
||||
case .offline:
|
||||
return .gray
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(systemName: "circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 8, height: 8)
|
||||
.foregroundColor(foregroundColor)
|
||||
Text(presense.title)
|
||||
.font(.subheadline)
|
||||
}
|
||||
.foregroundColor(foregroundColor)
|
||||
.padding(0)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplatePresenceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack(alignment:.leading){
|
||||
Text("Presence")
|
||||
ForEach(TemplatePresence.allCases) { presence in
|
||||
TemplatePresenceView(presense: presence)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,32 +19,61 @@ import SwiftUI
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateUserProfile: View {
|
||||
|
||||
enum Result {
|
||||
case cancel
|
||||
case done
|
||||
}
|
||||
|
||||
typealias Completion = (Result) -> Void
|
||||
|
||||
@Environment(\.theme) var theme: ThemeSwiftUI
|
||||
@ObservedObject var viewModel: TemplateUserProfileViewModel
|
||||
|
||||
var header: some View {
|
||||
var completion: Completion
|
||||
|
||||
var leftButton: some View {
|
||||
Button(VectorL10n.cancel) {
|
||||
completion(.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
var rightButton: some View {
|
||||
Button(VectorL10n.done) {
|
||||
completion(.done)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if let avatar = viewModel.viewState.avatar {
|
||||
HStack{
|
||||
TemplateUserProfileHeader(
|
||||
avatar: viewModel.viewState.avatar,
|
||||
displayName: viewModel.viewState.displayName,
|
||||
presence: viewModel.viewState.presence
|
||||
)
|
||||
Divider()
|
||||
VStack{
|
||||
HStack(alignment: .center){
|
||||
Spacer()
|
||||
AvatarImage(avatarData: avatar, size: .xxLarge)
|
||||
Text("More great user content!")
|
||||
.font(theme.fonts.title2)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
Text(viewModel.viewState.displayName ?? "")
|
||||
}
|
||||
|
||||
}
|
||||
var body: some View {
|
||||
VectorForm {
|
||||
header
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.navigationTitle(viewModel.viewState.displayName ?? "")
|
||||
.navigationBarItems(leading: leftButton, trailing: rightButton)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateUserProfile_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TemplateUserProfile(viewModel: TemplateUserProfileViewModel(userService: TemplateMockUserService.example))
|
||||
.addDependency(MockAvatarService.example)
|
||||
TemplateUserProfile(viewModel: TemplateUserProfileViewModel(userService: MockTemplateUserService.example)) { _ in
|
||||
|
||||
}
|
||||
.addDependency(MockAvatarService.example)
|
||||
}
|
||||
}
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateUserProfileHeader: View {
|
||||
|
||||
@Environment(\.theme) var theme: ThemeSwiftUI
|
||||
let avatar: AvatarInputType?
|
||||
let displayName: String?
|
||||
let presence: TemplatePresence
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if let avatar = avatar {
|
||||
HStack{
|
||||
Spacer()
|
||||
AvatarImage(avatarData: avatar, size: .xxLarge)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical)
|
||||
}
|
||||
VStack(spacing: 8){
|
||||
Text(displayName ?? "")
|
||||
.font(theme.fonts.title3)
|
||||
TemplatePresenceView(presense: presence)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateUserProfileHeader_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TemplateUserProfileHeader(avatar: MockAvatarInput.example, displayName: "Alice", presence: .online)
|
||||
.addDependency(MockAvatarService.example)
|
||||
}
|
||||
}
|
||||
+28
-1
@@ -15,13 +15,15 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateUserProfileViewModel: ObservableObject {
|
||||
|
||||
private let userService: TemplateUserServiceType
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
@Published var viewState: TemplateUserProfileViewState
|
||||
@Published private(set) var viewState: TemplateUserProfileViewState
|
||||
|
||||
private static func defaultState(userService: TemplateUserServiceType) -> TemplateUserProfileViewState {
|
||||
return TemplateUserProfileViewState(avatar: userService.avatarData, displayName: userService.displayName)
|
||||
@@ -30,5 +32,30 @@ class TemplateUserProfileViewModel: ObservableObject {
|
||||
init(userService: TemplateUserServiceType, initialState: TemplateUserProfileViewState? = nil) {
|
||||
self.userService = userService
|
||||
self.viewState = initialState ?? Self.defaultState(userService: userService)
|
||||
|
||||
userService.presencePublisher
|
||||
.map(TemplateProfileStateAction.updatePresence)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: self.dispatch(action:))
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
/**
|
||||
Send state actions to mutate the state.
|
||||
*/
|
||||
private func dispatch(action: TemplateProfileStateAction) {
|
||||
var newState = self.viewState
|
||||
reducer(state: &newState, action: action)
|
||||
self.viewState = newState
|
||||
}
|
||||
|
||||
/**
|
||||
A redux style reducer, all modifications to state happen here. Recieves a state and a state action and produces a new state.
|
||||
*/
|
||||
private func reducer(state: inout TemplateUserProfileViewState, action: TemplateProfileStateAction) {
|
||||
switch action {
|
||||
case .updatePresence(let presence):
|
||||
state.presence = presence
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user