diff --git a/BeReal.xcodeproj/project.pbxproj b/BeReal.xcodeproj/project.pbxproj index b46752b..f0ad43e 100644 --- a/BeReal.xcodeproj/project.pbxproj +++ b/BeReal.xcodeproj/project.pbxproj @@ -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 = ""; }; 026D9823293B6365009FE888 /* Libraries */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Libraries; sourceTree = ""; }; 02784F03293A8331005F839D /* Modules */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Modules; sourceTree = ""; }; 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 = ""; + }; + 02659B162946AA2E00C3AD63 /* Enumerations */ = { + isa = PBXGroup; + children = ( + 02659B182946AA6900C3AD63 /* SheetView.swift */, + ); + path = Enumerations; + sourceTree = ""; + }; + 02659B172946AA4400C3AD63 /* Views */ = { + isa = PBXGroup; + children = ( + 02AE64F029363DBF005A4AF3 /* ContentView.swift */, + ); + path = Views; + sourceTree = ""; + }; 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; diff --git a/BeReal/ContentView.swift b/BeReal/ContentView.swift deleted file mode 100644 index 2c544e1..0000000 --- a/BeReal/ContentView.swift +++ /dev/null @@ -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 { - .init { account == nil } set: { _ in } - } -} - -// MARK: - Previews - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/BeReal/UI/Enumerations/SheetView.swift b/BeReal/UI/Enumerations/SheetView.swift new file mode 100644 index 0000000..ea67624 --- /dev/null +++ b/BeReal/UI/Enumerations/SheetView.swift @@ -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 } +} diff --git a/BeReal/UI/Views/ContentView.swift b/BeReal/UI/Views/ContentView.swift new file mode 100644 index 0000000..aa429f0 --- /dev/null +++ b/BeReal/UI/Views/ContentView.swift @@ -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() + } +} diff --git a/Libraries/Sources/DataModels/Defines/Typealiases.swift b/Libraries/Sources/DataModels/Defines/Typealiases.swift new file mode 100644 index 0000000..11cf7ab --- /dev/null +++ b/Libraries/Sources/DataModels/Defines/Typealiases.swift @@ -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 diff --git a/Libraries/Sources/DataModels/Models/Account.swift b/Libraries/Sources/DataModels/Models/Account.swift index c0b1711..8ecf488 100644 --- a/Libraries/Sources/DataModels/Models/Account.swift +++ b/Libraries/Sources/DataModels/Models/Account.swift @@ -6,7 +6,7 @@ // Copyright © 2022 Röck+Cöde. All rights reserved. // -public struct Account: Codable { +public struct Account: Codable, Equatable { // MARK: Properties diff --git a/Modules/Package.swift b/Modules/Package.swift index 5e4826e..a32ed73 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -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")] ) ] diff --git a/Modules/Sources/Browse/UI/Toolbars/BrowseToolbar.swift b/Modules/Sources/Browse/UI/Toolbars/BrowseToolbar.swift index 746a3ba..a76540e 100644 --- a/Modules/Sources/Browse/UI/Toolbars/BrowseToolbar.swift +++ b/Modules/Sources/Browse/UI/Toolbars/BrowseToolbar.swift @@ -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 diff --git a/Modules/Sources/Browse/UI/Views/BrowseView.swift b/Modules/Sources/Browse/UI/Views/BrowseView.swift index 18fcf98..2ee78a4 100644 --- a/Modules/Sources/Browse/UI/Views/BrowseView.swift +++ b/Modules/Sources/Browse/UI/Views/BrowseView.swift @@ -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: { + // ... + } } } } diff --git a/Modules/Sources/Login/Logic/Defines/Typealiases.swift b/Modules/Sources/Login/Logic/Defines/Typealiases.swift new file mode 100644 index 0000000..b65b046 --- /dev/null +++ b/Modules/Sources/Login/Logic/Defines/Typealiases.swift @@ -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 diff --git a/Modules/Sources/Login/Logic/Use Cases/GetUserUseCase.swift b/Modules/Sources/Login/Logic/Use Cases/GetUserUseCase.swift new file mode 100644 index 0000000..f49fa92 --- /dev/null +++ b/Modules/Sources/Login/Logic/Use Cases/GetUserUseCase.swift @@ -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 + ) + ) + ) + } + +} diff --git a/Modules/Sources/Login/UI/Views/LoginView.swift b/Modules/Sources/Login/UI/Views/LoginView.swift index 754fe4c..6dbb018 100644 --- a/Modules/Sources/Login/UI/Views/LoginView.swift +++ b/Modules/Sources/Login/UI/Views/LoginView.swift @@ -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. + } } } diff --git a/Modules/Sources/Profile/Logic/Adapters/DateAdapter.swift b/Modules/Sources/Profile/Logic/Adapters/DateAdapter.swift new file mode 100644 index 0000000..441030a --- /dev/null +++ b/Modules/Sources/Profile/Logic/Adapters/DateAdapter.swift @@ -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 + }() +} diff --git a/Modules/Sources/Profile/Logic/Adapters/StringAdapter.swift b/Modules/Sources/Profile/Logic/Adapters/StringAdapter.swift new file mode 100644 index 0000000..4cee887 --- /dev/null +++ b/Modules/Sources/Profile/Logic/Adapters/StringAdapter.swift @@ -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 + } +} diff --git a/Modules/Sources/Profile/Logic/Extensions/String+Constants.swift b/Modules/Sources/Profile/Logic/Extensions/String+Constants.swift new file mode 100644 index 0000000..ba0dca4 --- /dev/null +++ b/Modules/Sources/Profile/Logic/Extensions/String+Constants.swift @@ -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 = "-" + } +} diff --git a/Modules/Sources/Profile/UI/Components/ProfileSection.swift b/Modules/Sources/Profile/UI/Components/ProfileSection.swift new file mode 100644 index 0000000..1246c44 --- /dev/null +++ b/Modules/Sources/Profile/UI/Components/ProfileSection.swift @@ -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" + ) + ] + ) + } +} diff --git a/Modules/Sources/Profile/UI/Views/ProfileView.swift b/Modules/Sources/Profile/UI/Views/ProfileView.swift index 3794647..0633db2 100644 --- a/Modules/Sources/Profile/UI/Views/ProfileView.swift +++ b/Modules/Sources/Profile/UI/Views/ProfileView.swift @@ -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. } } }