Merge pull request #11 from rock-n-code/integration/profile

Integration: Profile
This commit is contained in:
Javier Cicchelli 2022-12-12 03:22:02 +01:00 committed by GitHub
commit f324b59601
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 477 additions and 167 deletions

View File

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
02659B192946AA6900C3AD63 /* SheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02659B182946AA6900C3AD63 /* SheetView.swift */; };
026D9825293B6374009FE888 /* Libraries in Frameworks */ = {isa = PBXBuildFile; productRef = 026D9824293B6374009FE888 /* Libraries */; };
02AE64EF29363DBF005A4AF3 /* BeRealApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE64EE29363DBF005A4AF3 /* BeRealApp.swift */; };
02AE64F129363DBF005A4AF3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE64F029363DBF005A4AF3 /* ContentView.swift */; };
@ -37,6 +38,7 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
02659B182946AA6900C3AD63 /* SheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetView.swift; sourceTree = "<group>"; };
026D9823293B6365009FE888 /* Libraries */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Libraries; sourceTree = "<group>"; };
02784F03293A8331005F839D /* Modules */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Modules; sourceTree = "<group>"; };
02AE64EB29363DBF005A4AF3 /* BeReal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BeReal.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -80,6 +82,31 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
02659B152946AA2700C3AD63 /* UI */ = {
isa = PBXGroup;
children = (
02659B162946AA2E00C3AD63 /* Enumerations */,
02659B172946AA4400C3AD63 /* Views */,
);
path = UI;
sourceTree = "<group>";
};
02659B162946AA2E00C3AD63 /* Enumerations */ = {
isa = PBXGroup;
children = (
02659B182946AA6900C3AD63 /* SheetView.swift */,
);
path = Enumerations;
sourceTree = "<group>";
};
02659B172946AA4400C3AD63 /* Views */ = {
isa = PBXGroup;
children = (
02AE64F029363DBF005A4AF3 /* ContentView.swift */,
);
path = Views;
sourceTree = "<group>";
};
02AE64E229363DBF005A4AF3 = {
isa = PBXGroup;
children = (
@ -108,8 +135,8 @@
isa = PBXGroup;
children = (
02AE64EE29363DBF005A4AF3 /* BeRealApp.swift */,
02AE64F029363DBF005A4AF3 /* ContentView.swift */,
02AE64F229363DC1005A4AF3 /* Assets.xcassets */,
02659B152946AA2700C3AD63 /* UI */,
02AE64F429363DC1005A4AF3 /* Preview Content */,
);
path = BeReal;
@ -284,6 +311,7 @@
buildActionMask = 2147483647;
files = (
02AE64F129363DBF005A4AF3 /* ContentView.swift in Sources */,
02659B192946AA6900C3AD63 /* SheetView.swift in Sources */,
02AE64EF29363DBF005A4AF3 /* BeRealApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -1,49 +0,0 @@
//
// ContentView.swift
// BeReal
//
// Created by Javier Cicchelli on 29/11/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import Browse
import DataModels
import Login
import KeychainStorage
import Profile
import SwiftUI
struct ContentView: View {
// MARK: Storages
@KeychainStorage(key: .KeychainStorage.account) private var account: Account?
// MARK: Body
var body: some View {
NavigationView {
BrowseView()
}
.sheet(isPresented: showLogin) {
LoginView()
}
}
}
// MARK: - Helpers
private extension ContentView {
var showLogin: Binding<Bool> {
.init { account == nil } set: { _ in }
}
}
// MARK: - Previews
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

View File

@ -0,0 +1,20 @@
//
// SheetView.swift
// BeReal
//
// Created by Javier Cicchelli on 12/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
// MARK: - Enumerations
enum SheetView: Int {
case login
case profile
}
// MARK: - Identifiable
extension SheetView: Identifiable {
var id: Int { rawValue }
}

View File

@ -0,0 +1,79 @@
//
// ContentView.swift
// BeReal
//
// Created by Javier Cicchelli on 29/11/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import Browse
import DataModels
import Login
import KeychainStorage
import Profile
import SwiftUI
struct ContentView: View {
// MARK: Storages
@KeychainStorage(key: .KeychainStorage.account) private var account: Account?
// MARK: States
@State private var user: User?
@State private var showSheet: SheetView?
// MARK: Body
var body: some View {
NavigationView {
BrowseView {
// ...
} uploadFile: {
// ...
} showProfile: {
showSheet = .profile
}
}
.onAppear {
shouldShowLogin()
}
.onChange(of: account) { _ in
shouldShowLogin()
}
.sheet(item: $showSheet) { sheet in
switch sheet {
case .login:
LoginView {
user = $1
account = $0
}
case .profile:
ProfileView(user: user) {
user = nil
account = nil
}
}
}
}
}
// MARK: - Helpers
private extension ContentView {
func shouldShowLogin() {
showSheet = account == nil
? .login
: nil
}
}
// MARK: - Previews
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

View File

@ -0,0 +1,9 @@
//
// Typealiases.swift
// DataModels
//
// Created by Javier Cicchelli on 12/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
public typealias ActionClosure = () -> Void

View File

@ -6,7 +6,7 @@
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
public struct Account: Codable {
public struct Account: Codable, Equatable {
// MARK: Properties

View File

@ -31,10 +31,18 @@ let package = Package(
),
.target(
name: "Browse",
dependencies: [
"Cores",
"Libraries"
],
resources: [.process("Resources")]
),
.target(
name: "Profile",
dependencies: [
"Cores",
"Libraries"
],
resources: [.process("Resources")]
)
]

View File

@ -1,19 +1,29 @@
//
// BrowseToolbar.swift
// BeReal
// Browse
//
// Created by Javier Cicchelli on 03/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import DataModels
import SwiftUI
struct BrowseToolbar: ToolbarContent {
// MARK: Properties
let createFolder: ActionClosure
let uploadFile: ActionClosure
let showProfile: ActionClosure
// MARK: Body
var body: some ToolbarContent {
ToolbarItem(placement: .primaryAction) {
Menu {
Button {
// TODO: Implement the creation of a new folder.
createFolder()
} label: {
Label {
Text(
@ -27,7 +37,7 @@ struct BrowseToolbar: ToolbarContent {
}
Button {
// TODO: Implement the upload of a file from the device to the API.
uploadFile()
} label: {
Label {
Text(
@ -55,7 +65,7 @@ struct BrowseToolbar: ToolbarContent {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
// TODO: Implement the show of the user profile.
showProfile()
} label: {
Label {
Text(
@ -70,6 +80,7 @@ struct BrowseToolbar: ToolbarContent {
}
}
}
}
// MARK: - Image+Constants

View File

@ -6,13 +6,28 @@
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import DataModels
import SwiftUI
public struct BrowseView: View {
// MARK: Properties
private let createFolder: ActionClosure
private let uploadFile: ActionClosure
private let showProfile: ActionClosure
// MARK: Initialisers
public init() {}
public init(
createFolder: @escaping ActionClosure,
uploadFile: @escaping ActionClosure,
showProfile: @escaping ActionClosure
) {
self.createFolder = createFolder
self.uploadFile = uploadFile
self.showProfile = showProfile
}
// MARK: Body
@ -106,7 +121,11 @@ public struct BrowseView: View {
.background(Color.red)
.navigationTitle("Folder name")
.toolbar {
BrowseToolbar()
BrowseToolbar(
createFolder: createFolder,
uploadFile: uploadFile,
showProfile: showProfile
)
}
}
}
@ -123,7 +142,13 @@ private extension Image {
struct BrowseView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
BrowseView()
BrowseView {
// ...
} uploadFile: {
// ...
} showProfile: {
// ...
}
}
}
}

View File

@ -0,0 +1,11 @@
//
// Typealiases.swift
// Login
//
// Created by Javier Cicchelli on 12/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import DataModels
public typealias AuthenticatedClosure = (Account, User) -> Void

View File

@ -0,0 +1,55 @@
//
// GetUserUseCase.swift
// Login
//
// Created by Javier Cicchelli on 12/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import APIService
import DependencyInjection
import Dependencies
struct GetUserUseCase {
// MARK: Dependencies
@Dependency(\.apiService) private var apiService
// MARK: Properties
let authenticated: AuthenticatedClosure
// MARK: Functions
func callAsFunction(
username: String,
password: String
) async throws {
let me = try await apiService.getUser(
credentials: .init(
username: username,
password: password
)
)
authenticated(
.init(
username: username,
password: password
),
.init(
profile: .init(
firstName: me.firstName,
lastName: me.lastName
),
rootFolder: .init(
id: me.rootItem.id,
name: me.rootItem.name,
lastModifiedAt: me.rootItem.lastModifiedAt
)
)
)
}
}

View File

@ -7,10 +7,6 @@
//
import APIService
import DataModels
import DependencyInjection
import Dependencies
import KeychainStorage
import SwiftUI
public struct LoginView: View {
@ -19,9 +15,15 @@ public struct LoginView: View {
@State private var containerTopPadding: CGFloat = 0
// MARK: Properties
private let authenticated: AuthenticatedClosure
// MARK: Initialisers
public init() {}
public init(authenticated: @escaping AuthenticatedClosure) {
self.authenticated = authenticated
}
// MARK: Body
@ -30,7 +32,7 @@ public struct LoginView: View {
.vertical,
showsIndicators: false
) {
LoginContainer()
LoginContainer(authenticated: authenticated)
.padding(.horizontal, 24)
.padding(.top, containerTopPadding)
}
@ -39,6 +41,7 @@ public struct LoginView: View {
.onPreferenceChange(ViewHeightPreferenceKey.self) { height in
containerTopPadding = height * 0.1
}
.interactiveDismissDisabled()
}
}
@ -47,21 +50,23 @@ public struct LoginView: View {
fileprivate extension LoginView {
struct LoginContainer: View {
// MARK: Dependencies
@Dependency(\.apiService) private var apiService
// MARK: Storages
@KeychainStorage(key: .KeychainStorage.account) private var account: Account?
// MARK: States
@State private var isAuthenticating: Bool = false
@State private var username: String = ""
@State private var password: String = ""
@State private var errorMessage: String?
// MARK: Properties
private let getUser: GetUserUseCase
// MARK: Initialisers
init(authenticated: @escaping AuthenticatedClosure) {
self.getUser = .init(authenticated: authenticated)
}
// MARK: Body
@ -133,12 +138,7 @@ private extension LoginView.LoginContainer {
guard isAuthenticating else { return }
do {
_ = try await apiService.getUser(credentials: .init(
username: username,
password: password
))
account = .init(
try await getUser(
username: username,
password: password
)
@ -157,6 +157,8 @@ private extension LoginView.LoginContainer {
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
LoginView { _, _ in
// closure for authenticated action.
}
}
}

View File

@ -0,0 +1,41 @@
//
// DateAdapter.swift
// Profile
//
// Created by Javier Cicchelli on 12/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import Foundation
struct DateAdapter {
// MARK: Properties
private let dateFormatter: DateFormatter = .dateTimeFormatter
// MARK: Functions
func callAsFunction(value: Date?) -> String {
if let value {
return dateFormatter.string(from: value)
} else {
return .Constants.noValue
}
}
}
// MARK: - DateFormatter+Formats
private extension DateFormatter {
static let dateTimeFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .short
formatter.locale = .current
return formatter
}()
}

View File

@ -0,0 +1,13 @@
//
// StringAdapter.swift
// Profile
//
// Created by Javier Cicchelli on 12/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
struct StringAdapter {
func callAsFunction(value: String?) -> String {
value ?? .Constants.noValue
}
}

View File

@ -0,0 +1,13 @@
//
// String+Constants.swift
// Profile
//
// Created by Javier Cicchelli on 12/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
extension String {
enum Constants {
static let noValue = "-"
}
}

View File

@ -0,0 +1,75 @@
//
// ProfileSection.swift
// Profile
//
// Created by Javier Cicchelli on 12/12/2022.
//
import SwiftUI
struct ProfileSection: View {
// MARK: Properties
let header: LocalizedStringKey
let items: [Item]
// MARK: Body
var body: some View {
Section {
ForEach(items) { item in
Label {
Text(item.value)
} icon: {
Text(
item.key,
bundle: .module
)
}
.labelStyle(.nameAndValue)
}
} header: {
Text(
header,
bundle: .module
)
}
}
}
// MARK: - Structs
extension ProfileSection {
struct Item {
let key: LocalizedStringKey
let value: String
}
}
// MARK: - Identifiable
extension ProfileSection.Item: Identifiable {
var id: String { UUID().uuidString }
}
// MARK: - Previews
struct ProfileSection_Previews: PreviewProvider {
static var previews: some View {
ProfileSection(
header: "some-localised-header-key",
items: [
.init(
key: "some-localized-key",
value: "some value"
),
.init(
key: "some-other-localised-key",
value: "some other value"
)
]
)
}
}

View File

@ -6,18 +6,27 @@
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import DataModels
import SwiftUI
public struct ProfileView: View {
// MARK: Properties
private let logOut: () -> Void
private let user: User?
private let logout: ActionClosure
private let stringAdapter = StringAdapter()
private let dateAdapter = DateAdapter()
// MARK: Initialisers
public init(logOut: @escaping () -> Void) {
self.logOut = logOut
public init(
user: User?,
logout: @escaping ActionClosure
) {
self.user = user
self.logout = logout
}
// MARK: Body
@ -33,96 +42,45 @@ public struct ProfileView: View {
}
.listRowBackground(Color.clear)
Section {
Label {
Text("Javier")
} icon: {
Text(
"profile.sections.names.label.first_name.text",
bundle: .module,
comment: "First name label text."
ProfileSection(
header: "profile.sections.names.header.text",
items: [
.init(
key: "profile.sections.names.label.first_name.text",
value: stringAdapter(value: user?.profile.firstName)
),
.init(
key: "profile.sections.names.label.last_name.text",
value: stringAdapter(value: user?.profile.lastName)
)
}
.labelStyle(.nameAndValue)
Label {
Text("Cicchelli")
} icon: {
Text(
"profile.sections.names.label.last_name.text",
bundle: .module,
comment: "Last name label text."
]
)
ProfileSection(
header: "profile.sections.root_info.header.text",
items: [
.init(
key: "profile.sections.root_info.label.identifier.text",
value: stringAdapter(value: user?.rootFolder.id)
),
.init(
key: "profile.sections.root_info.label.name.text",
value: stringAdapter(value: user?.rootFolder.name)
),
.init(
key: "profile.sections.root_info.label.last_modified.text",
value: dateAdapter(value: user?.rootFolder.lastModifiedAt)
)
}
.labelStyle(.nameAndValue)
} header: {
Text(
"profile.sections.names.header.text",
bundle: .module,
comment: "Names section header text."
)
}
]
)
Section {
Label {
Text("71207ee4c0573fde80b03643caafe62731406404")
} icon: {
Text(
"profile.sections.root_info.label.identifier.text",
bundle: .module,
comment: "Identifier label text."
)
}
.labelStyle(.nameAndValue)
Label {
Text("Yes")
} icon: {
Text(
"profile.sections.root_info.label.is_directory.text",
bundle: .module,
comment: "Is directory label text."
)
}
.labelStyle(.nameAndValue)
Label {
Text("3 days ago")
} icon: {
Text(
"profile.sections.root_info.label.last_modified.text",
bundle: .module,
comment: "Last modified label text."
)
}
.labelStyle(.nameAndValue)
Label {
Text("My files")
} icon: {
Text(
"profile.sections.root_info.label.name.text",
bundle: .module,
comment: "Root name label text."
)
}
.labelStyle(.nameAndValue)
} header: {
Text(
"profile.sections.root_info.header.text",
bundle: .module,
comment: "Root item information header text."
)
}
Section {
Button {
logOut()
logout()
} label: {
Text(
"profile.button.log_out.text",
bundle: .module,
comment: "Log out button text."
bundle: .module
)
.fontWeight(.semibold)
.foregroundColor(.primary)
@ -137,6 +95,7 @@ public struct ProfileView: View {
}
.background(Color.red)
}
}
// MARK: - Images+Constants
@ -149,8 +108,18 @@ private extension Image {
struct ProfileView_Previews: PreviewProvider {
static var previews: some View {
ProfileView {
// closure for log out action.
ProfileView(user: .init(
profile: .init(
firstName: "Some first name...",
lastName: "Some last name..."
),
rootFolder: .init(
id: "1234567890",
name: "Some folder name...",
lastModifiedAt: .now
)
)) {
// closure for logout action.
}
}
}