From 82bc7a3981871acbe282ac2621b91c6845d34616 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 19 Dec 2022 12:07:29 +0100 Subject: [PATCH 01/10] Added the README file to the Xcode project. --- BeReal.xcodeproj/project.pbxproj | 2 ++ README.md | 0 2 files changed, 2 insertions(+) create mode 100644 README.md diff --git a/BeReal.xcodeproj/project.pbxproj b/BeReal.xcodeproj/project.pbxproj index f0ad43e..e08e578 100644 --- a/BeReal.xcodeproj/project.pbxproj +++ b/BeReal.xcodeproj/project.pbxproj @@ -51,6 +51,7 @@ 02AE650529363DC1005A4AF3 /* BeRealUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BeRealUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 02AE650929363DC1005A4AF3 /* BeRealUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeRealUITests.swift; sourceTree = ""; }; 02AE650B29363DC1005A4AF3 /* BeRealUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeRealUITestsLaunchTests.swift; sourceTree = ""; }; + 02E4701B29506B5100269158 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 466D180A29465340003828DC /* Cores */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Cores; sourceTree = ""; }; /* End PBXFileReference section */ @@ -110,6 +111,7 @@ 02AE64E229363DBF005A4AF3 = { isa = PBXGroup; children = ( + 02E4701B29506B5100269158 /* README.md */, 466D180A29465340003828DC /* Cores */, 026D9823293B6365009FE888 /* Libraries */, 02784F03293A8331005F839D /* Modules */, diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 From 8f2a5dbdb7ca25c1f174a1789afdf03c55d220a7 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 20 Dec 2022 01:48:27 +0100 Subject: [PATCH 02/10] Written the README file. --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/README.md b/README.md index e69de29..947adfd 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,53 @@ +# BeReal assignment + +The _"My Files"_ application was created as a result of the requirements defined by the given assignment by [BeReal](https://bereal.com/en). + +## App + +In a nutshell, the purpose of this application is to manage a remote file system accessible through the Internet via a provided REST API endpoint. + +In its current state, the application allows its users to: + +* *handle GET, POST and DELETE requests to the API endpoint;* +* *require the user to login to the app to access the file system;* +* *load the items located at the root folder;* +* *browse through the folders hierarchy;* +* *create new folder in the current folder location;* +* *support for pull-to-refresh on folders;* +* *open JPG and PNG images in the image viewer;* +* *upload documents from the phone or an **iCloud** location to the current folder location;* +* *download document from the current folder location to the phone or an **iCloud** location;* +* *delete existing folders in the current folder location;* +* *delete existing documents in the current folder location;* +* *show available user information in the profile;* +* *allow the user to logout from the app;* +* *show to the user relevant errors that could happen while using the app.* + +> It is necessary to clarify that, to use any **iCloud** location, the user is required to have an actual **iCloud** account and to be logged to that **iCloud** account in the phone. + +## Implementation + +This application was built as a `SwiftUI` application as the kind of app this assignment defined actually fits the use case to use this young framework, even though `UIKit` has been battle-tested for more than a decade now. The declarative nature of the framework, which is based in describing the behaviour of the UI interface, and no by implementing how it should work (like with the imperative approach we were used to), allows the developer for simpler, more maintainable codebases. Of course, the bridge support to native UI framework, like `UIKit` or `AppKit`, is also available for those cases when the developer needs to build tailored UI components and/or more fine-grained control over UI controls. Finally, another advantage worth mentioning, but given the scope of this assignment, is not really relevant is the possibility of porting the app to other platforms, like iPad, Mac, Apple TV, Apple Watch or even CarPlay easily, as the framework implements an encapsulation (or better said, an erasure...) through its APIs of several `UIKit`, `AppKit`, and `WatchKit` components that developer usually need to build their apps. + +With choosing the `SwiftUI` framework to build this app, it also comes the question of the type of architecture to use with it. Arguably, my preferred answer to this particular question is **"None"**. Given this framework is highly opinionated (much more opinionated than the view controllers from `UIKit` or `Appkit`), and that it is supported by the power and capabilities that the Swift language itself provides, this framework has lots of extra, helpful features that a SwiftUI `View` can use out of the box (like the `@State`, `@Binding`, `@AppStorage` and `@FetchRequest` property wrappers), I would rather prefer to fully understand how SwiftUI works and use some appropriate, useful design patterns instead. Given my experience, it is preferable to just go with the flow of the technology you use, because the less time spent swimming against the current (or fighting against your framework of choice), the more time developers have to actually solve the problems that actually matter to their apps and, of course, their users. + +Now that design patterns have been mentioned, in this exercise some well-known patterns are being used in some degree. For example, the *Singleton* pattern is used to initialise the container in the `DependencyInjection` core library. Both public and internal *Interfaces* that either describe an object or how it should behave are used throughout this codebase, as this pattern is essential to plug the libraries into the modules, and the modules into the actual app and, as a consequence, for testability purposes as developer can easily create mocks, stubs and spies out of them. Then the *Adapter* pattern is used to transform some object into another object like, for example, when transforming some data from a business model instance to a model used exclusively by a UI component or view. Last, but definitely not least, the *Use cases* are a pattern from Android that basically execute a function based on some given input, and provides an output after that particular function is finished. This pattern is particularly useful to encapsulate the logic that a view needs to execute in a simple way, without the need to create view model classes that inherit from the `ObservableObject` class. + +This application was built with scalability in terms of the codebase in mind, which tries to address how this codebase could grow in a controllable, organised manner. For this very reason, this application uses the Swift Package Manager (or simply `SPM`) to define the `Cores`, `Libraries` and `Modules` packages. Inside each package, there are targets that encapsulate a some certain functionality or feature with its respective assets inside. Of course, some of these targets also have related test targets that contain test cases with some relevant unit tests. The application takes a bottom-up approach, as the App could use `Cores`, `Libraries` and `Modules` targets but the Modules could use only `Cores` and `Libraries` targets; Libraries could use only `Cores` targets and 3rd party dependencies; and Cores could use only 3rd party dependencies if needed. This approach forces the developer to think about actual separation of concerns, as the different features and dependencies should be grouped as independent, reusable building blocks, and to move the code into the SPM packages out of the main app target, reducing compiling time and overal weight of the application. + +## TODO + +Of course, this application is not completely done yet and, as mentioned in the assignment text, some more time is definitely required to actually finish the app and polish the code as intended. + +Currently, the work that has been left outside of this assignment + +- [ ] Better error and connectivity handling; +- [ ] Move duplicated code from Modules to sensible, common Libraries dependencies; +- [ ] Add support for new APIs introduced on iOS 16 (especially for navigation); +- [ ] Improve communication between the public views on the Modules and the core application; +- [ ] Write further documentation, especially for the public interfaces; +- [ ] Write more unit tests with the `XCTest` framework for all the relevant logic code distributed in the Core, Library and Modules dependencies; +- [ ] Write UI tests for happy and error paths with the `XCUITest` framework; +- [ ] Set Xcode plugin that uses the `SwiftFormat` library to lint and format the source code on development delivery pipeline; +- [ ] Set Xcode plugin that uses `DocC` library to generate Xcode and web-ready documentation on release delivery pipeline; +- [ ] Set [Xcode Cloud](https://developer.apple.com/xcode-cloud/) as continuous integration and delivery service to **TestFlight** and to the **App Store**. From dc00fa66cbfd7f71d97640f77f3e4ffb9041ed57 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 20 Dec 2022 02:01:52 +0100 Subject: [PATCH 03/10] Defined the app icon for the App target. --- .../AppIcon.appiconset/Contents.json | 1 + .../AppIcon.appiconset/MyFiles.png | Bin 0 -> 34689 bytes 2 files changed, 1 insertion(+) create mode 100644 BeReal/Assets.xcassets/AppIcon.appiconset/MyFiles.png diff --git a/BeReal/Assets.xcassets/AppIcon.appiconset/Contents.json b/BeReal/Assets.xcassets/AppIcon.appiconset/Contents.json index 13613e3..d9a45d6 100644 --- a/BeReal/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/BeReal/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "MyFiles.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/BeReal/Assets.xcassets/AppIcon.appiconset/MyFiles.png b/BeReal/Assets.xcassets/AppIcon.appiconset/MyFiles.png new file mode 100644 index 0000000000000000000000000000000000000000..ee5105200f081b0c1db5691994a7c203111e1652 GIT binary patch literal 34689 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 G-1&c*J9!}h literal 0 HcmV?d00001 From b77f1b758c215ffc7acdd73a47ac282d0a0a9072 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 20 Dec 2022 02:05:22 +0100 Subject: [PATCH 04/10] Added support for content types PNG in the DocumentView view for the Browse module. --- Modules/Sources/Browse/UI/Views/DocumentView.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Modules/Sources/Browse/UI/Views/DocumentView.swift b/Modules/Sources/Browse/UI/Views/DocumentView.swift index 8a4e5e8..0fe9294 100644 --- a/Modules/Sources/Browse/UI/Views/DocumentView.swift +++ b/Modules/Sources/Browse/UI/Views/DocumentView.swift @@ -100,6 +100,10 @@ private extension DocumentView { // MARK: Computed + var supportedContentTypes: [String] { + [.ContentType.jpeg, .ContentType.png] + } + var imageFromData: UIImage { guard let loadedData, @@ -114,7 +118,7 @@ private extension DocumentView { // MARK: Functions func loadDataIfPossible() async { - guard document.contentType == .Constants.supportedContentType else { + guard supportedContentTypes.contains(document.contentType) else { status = .notSupported return } @@ -147,8 +151,9 @@ private extension DocumentView { // MARK: - String+Constants private extension String { - enum Constants { - static let supportedContentType = "image/jpeg" + enum ContentType { + static let jpeg = "image/jpeg" + static let png = "image/png" } } From 5aeec04e198dfac8b3e0de26a681e92e2c52f62d Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 20 Dec 2022 02:09:19 +0100 Subject: [PATCH 05/10] Filter newline characters from the "textField(_: shouldChangeCharactersIn: replacementString: )" function in the InputAlertView component for the Browse module. --- .../Sources/Browse/UI/Components/InputAlertView.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Modules/Sources/Browse/UI/Components/InputAlertView.swift b/Modules/Sources/Browse/UI/Components/InputAlertView.swift index a1c1b36..4b5157d 100644 --- a/Modules/Sources/Browse/UI/Components/InputAlertView.swift +++ b/Modules/Sources/Browse/UI/Components/InputAlertView.swift @@ -118,10 +118,16 @@ extension InputAlertView { // MARK: UITextFieldDelegate - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { component.textFieldString = { if let text = textField.text as NSString? { - return text.replacingCharacters(in: range, with: string) + return text + .replacingCharacters(in: range, with: string) + .trimmingCharacters(in: .newlines) } else { return .empty } From 9682a78f807eb1df4af3e22675ec4133ae58ed21 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 20 Dec 2022 02:49:26 +0100 Subject: [PATCH 06/10] Tweaked the README file a bit more. --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 947adfd..0b1d382 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The _"My Files"_ application was created as a result of the requirements defined ## App -In a nutshell, the purpose of this application is to manage a remote file system accessible through the Internet via a provided REST API endpoint. +In a nutshell, the purpose of this application is to manage a remote file system accessible throught the Internet via a provided REST API endpoint. In its current state, the application allows its users to: @@ -27,13 +27,13 @@ In its current state, the application allows its users to: ## Implementation -This application was built as a `SwiftUI` application as the kind of app this assignment defined actually fits the use case to use this young framework, even though `UIKit` has been battle-tested for more than a decade now. The declarative nature of the framework, which is based in describing the behaviour of the UI interface, and no by implementing how it should work (like with the imperative approach we were used to), allows the developer for simpler, more maintainable codebases. Of course, the bridge support to native UI framework, like `UIKit` or `AppKit`, is also available for those cases when the developer needs to build tailored UI components and/or more fine-grained control over UI controls. Finally, another advantage worth mentioning, but given the scope of this assignment, is not really relevant is the possibility of porting the app to other platforms, like iPad, Mac, Apple TV, Apple Watch or even CarPlay easily, as the framework implements an encapsulation (or better said, an erasure...) through its APIs of several `UIKit`, `AppKit`, and `WatchKit` components that developer usually need to build their apps. +This application was built as a `SwiftUI` application as the kind of app this assignment defined actually fits the use case to use this young framework, even though `UIKit` has been battle-tested for more than a decade now. The declarative nature of the framework, which is based in describing the behavior of the UI interface, and no by implementing how it should work (like with the imperative approach we were used to), allows the developer for simpler, more maintenable codebases. Of course, the bridge support to native UI framework, like `UIKit` or `AppKit`, is also available for those cases when the developer needs to build tailored UI components and/or more fine-grained control over UI controls. Finally, another advantage worth mentioning, but given the scope of this assignment, is not really relevant is the possibility of porting the app to other platforms, like iPad, Mac, Apple TV, Apple Watch or even Carplay easily, as the framework implementes an encapsulation (or better said, an erasure...) through its APIs of several `UIKit`, `AppKit`, and `WatchKit` components that developer usually need to build their apps. With choosing the `SwiftUI` framework to build this app, it also comes the question of the type of architecture to use with it. Arguably, my preferred answer to this particular question is **"None"**. Given this framework is highly opinionated (much more opinionated than the view controllers from `UIKit` or `Appkit`), and that it is supported by the power and capabilities that the Swift language itself provides, this framework has lots of extra, helpful features that a SwiftUI `View` can use out of the box (like the `@State`, `@Binding`, `@AppStorage` and `@FetchRequest` property wrappers), I would rather prefer to fully understand how SwiftUI works and use some appropriate, useful design patterns instead. Given my experience, it is preferable to just go with the flow of the technology you use, because the less time spent swimming against the current (or fighting against your framework of choice), the more time developers have to actually solve the problems that actually matter to their apps and, of course, their users. Now that design patterns have been mentioned, in this exercise some well-known patterns are being used in some degree. For example, the *Singleton* pattern is used to initialise the container in the `DependencyInjection` core library. Both public and internal *Interfaces* that either describe an object or how it should behave are used throughout this codebase, as this pattern is essential to plug the libraries into the modules, and the modules into the actual app and, as a consequence, for testability purposes as developer can easily create mocks, stubs and spies out of them. Then the *Adapter* pattern is used to transform some object into another object like, for example, when transforming some data from a business model instance to a model used exclusively by a UI component or view. Last, but definitely not least, the *Use cases* are a pattern from Android that basically execute a function based on some given input, and provides an output after that particular function is finished. This pattern is particularly useful to encapsulate the logic that a view needs to execute in a simple way, without the need to create view model classes that inherit from the `ObservableObject` class. -This application was built with scalability in terms of the codebase in mind, which tries to address how this codebase could grow in a controllable, organised manner. For this very reason, this application uses the Swift Package Manager (or simply `SPM`) to define the `Cores`, `Libraries` and `Modules` packages. Inside each package, there are targets that encapsulate a some certain functionality or feature with its respective assets inside. Of course, some of these targets also have related test targets that contain test cases with some relevant unit tests. The application takes a bottom-up approach, as the App could use `Cores`, `Libraries` and `Modules` targets but the Modules could use only `Cores` and `Libraries` targets; Libraries could use only `Cores` targets and 3rd party dependencies; and Cores could use only 3rd party dependencies if needed. This approach forces the developer to think about actual separation of concerns, as the different features and dependencies should be grouped as independent, reusable building blocks, and to move the code into the SPM packages out of the main app target, reducing compiling time and overal weight of the application. +This application was built with scalability in terms of the codebase in mind, which tries to address how this codebase could grow in a controllable, organised manner. For this very reason, this application uses the Swift Package Manager (or simply `SPM`) to define the `Cores`, `Libraries` and `Modules` packages. Inside each package, there are targets that encapsulate a some certain functionality or feature with its respective assets inside. Of course, some of these targets also have related test targets that contain test cases with some relevant unit tests. The application takes a bottom-up approach, as the App could use `Cores`, `Libraries` and `Modules` targets but the Modules could use only `Cores` and `Libraries` targets; Libraries could use only `Cores` targets and 3rd party dependencies; and Cores could use only 3rd party dependencies if needed. This approach forces the deveeloper to think about actual separation of concerns, as the different features and dependencies should be grouped as independent, reusable building blocks, and to move the code into the SPM packages out of the main app target, reducing compiling time and overal weight of the application. The only drawback I have encountered with this approach is that the Xcode previews for those views that implemented previews don't work in the current Xcode version. ## TODO @@ -45,9 +45,12 @@ Currently, the work that has been left outside of this assignment - [ ] Move duplicated code from Modules to sensible, common Libraries dependencies; - [ ] Add support for new APIs introduced on iOS 16 (especially for navigation); - [ ] Improve communication between the public views on the Modules and the core application; -- [ ] Write further documentation, especially for the public interfaces; +- [ ] Implement API response caching to optimise the use of the network connection; +- [ ] Implement an offline mode with a synchronisation mechanism to keep the device and remote file systems in sync; +- [ ] Write further inline documentation, especially for the public interfaces; +- [ ] Write further `DocC` documentation and articles for the libraries, modules and app; - [ ] Write more unit tests with the `XCTest` framework for all the relevant logic code distributed in the Core, Library and Modules dependencies; - [ ] Write UI tests for happy and error paths with the `XCUITest` framework; - [ ] Set Xcode plugin that uses the `SwiftFormat` library to lint and format the source code on development delivery pipeline; - [ ] Set Xcode plugin that uses `DocC` library to generate Xcode and web-ready documentation on release delivery pipeline; -- [ ] Set [Xcode Cloud](https://developer.apple.com/xcode-cloud/) as continuous integration and delivery service to **TestFlight** and to the **App Store**. +- [ ] Set [Xcode Cloud](https://developer.apple.com/xcode-cloud/) as continuous integration and delivery service to **TestFlight** and to the **App Store**. \ No newline at end of file From d0dae4a7eb7e30ef25d20c9e0a41c8d93c532808 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 20 Dec 2022 02:57:16 +0100 Subject: [PATCH 07/10] Removed the BeReal unit test target and arranged the folder structure of the BeReal app target. --- BeReal.xcodeproj/project.pbxproj | 159 +++--------------- BeReal/{ => App}/BeRealApp.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/MyFiles.png | Bin .../Assets.xcassets/Contents.json | 0 .../Preview Assets.xcassets/Contents.json | 0 BeRealTests/BeRealTests.swift | 37 ---- 8 files changed, 21 insertions(+), 175 deletions(-) rename BeReal/{ => App}/BeRealApp.swift (100%) rename BeReal/{ => Assets}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename BeReal/{ => Assets}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename BeReal/{ => Assets}/Assets.xcassets/AppIcon.appiconset/MyFiles.png (100%) rename BeReal/{ => Assets}/Assets.xcassets/Contents.json (100%) rename BeReal/{Preview Content => Assets}/Preview Assets.xcassets/Contents.json (100%) delete mode 100644 BeRealTests/BeRealTests.swift diff --git a/BeReal.xcodeproj/project.pbxproj b/BeReal.xcodeproj/project.pbxproj index e08e578..0233f15 100644 --- a/BeReal.xcodeproj/project.pbxproj +++ b/BeReal.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 02AE64F129363DBF005A4AF3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE64F029363DBF005A4AF3 /* ContentView.swift */; }; 02AE64F329363DC1005A4AF3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 02AE64F229363DC1005A4AF3 /* Assets.xcassets */; }; 02AE64F629363DC1005A4AF3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 02AE64F529363DC1005A4AF3 /* Preview Assets.xcassets */; }; - 02AE650029363DC1005A4AF3 /* BeRealTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE64FF29363DC1005A4AF3 /* BeRealTests.swift */; }; 02AE650A29363DC1005A4AF3 /* BeRealUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE650929363DC1005A4AF3 /* BeRealUITests.swift */; }; 02AE650C29363DC1005A4AF3 /* BeRealUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE650B29363DC1005A4AF3 /* BeRealUITestsLaunchTests.swift */; }; 4673A2AC294656A1000FE043 /* Cores in Frameworks */ = {isa = PBXBuildFile; productRef = 4673A2AB294656A1000FE043 /* Cores */; }; @@ -21,13 +20,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 02AE64FC29363DC1005A4AF3 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 02AE64E329363DBF005A4AF3 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 02AE64EA29363DBF005A4AF3; - remoteInfo = BeReal; - }; 02AE650629363DC1005A4AF3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 02AE64E329363DBF005A4AF3 /* Project object */; @@ -46,8 +38,6 @@ 02AE64F029363DBF005A4AF3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 02AE64F229363DC1005A4AF3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 02AE64F529363DC1005A4AF3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 02AE64FB29363DC1005A4AF3 /* BeRealTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BeRealTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 02AE64FF29363DC1005A4AF3 /* BeRealTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeRealTests.swift; sourceTree = ""; }; 02AE650529363DC1005A4AF3 /* BeRealUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BeRealUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 02AE650929363DC1005A4AF3 /* BeRealUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeRealUITests.swift; sourceTree = ""; }; 02AE650B29363DC1005A4AF3 /* BeRealUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeRealUITestsLaunchTests.swift; sourceTree = ""; }; @@ -66,13 +56,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 02AE64F829363DC1005A4AF3 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 02AE650229363DC1005A4AF3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -83,6 +66,23 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 024E087A29514D35002C4DF9 /* App */ = { + isa = PBXGroup; + children = ( + 02AE64EE29363DBF005A4AF3 /* BeRealApp.swift */, + ); + path = App; + sourceTree = ""; + }; + 024E087B29514D40002C4DF9 /* Assets */ = { + isa = PBXGroup; + children = ( + 02AE64F229363DC1005A4AF3 /* Assets.xcassets */, + 02AE64F529363DC1005A4AF3 /* Preview Assets.xcassets */, + ); + path = Assets; + sourceTree = ""; + }; 02659B152946AA2700C3AD63 /* UI */ = { isa = PBXGroup; children = ( @@ -116,7 +116,6 @@ 026D9823293B6365009FE888 /* Libraries */, 02784F03293A8331005F839D /* Modules */, 02AE64ED29363DBF005A4AF3 /* BeReal */, - 02AE64FE29363DC1005A4AF3 /* BeRealTests */, 02AE650829363DC1005A4AF3 /* BeRealUITests */, 02AE64EC29363DBF005A4AF3 /* Products */, 4694AA9E293A7C8800D54903 /* Frameworks */, @@ -127,7 +126,6 @@ isa = PBXGroup; children = ( 02AE64EB29363DBF005A4AF3 /* BeReal.app */, - 02AE64FB29363DC1005A4AF3 /* BeRealTests.xctest */, 02AE650529363DC1005A4AF3 /* BeRealUITests.xctest */, ); name = Products; @@ -136,30 +134,13 @@ 02AE64ED29363DBF005A4AF3 /* BeReal */ = { isa = PBXGroup; children = ( - 02AE64EE29363DBF005A4AF3 /* BeRealApp.swift */, - 02AE64F229363DC1005A4AF3 /* Assets.xcassets */, + 024E087A29514D35002C4DF9 /* App */, 02659B152946AA2700C3AD63 /* UI */, - 02AE64F429363DC1005A4AF3 /* Preview Content */, + 024E087B29514D40002C4DF9 /* Assets */, ); path = BeReal; sourceTree = ""; }; - 02AE64F429363DC1005A4AF3 /* Preview Content */ = { - isa = PBXGroup; - children = ( - 02AE64F529363DC1005A4AF3 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - 02AE64FE29363DC1005A4AF3 /* BeRealTests */ = { - isa = PBXGroup; - children = ( - 02AE64FF29363DC1005A4AF3 /* BeRealTests.swift */, - ); - path = BeRealTests; - sourceTree = ""; - }; 02AE650829363DC1005A4AF3 /* BeRealUITests */ = { isa = PBXGroup; children = ( @@ -201,24 +182,6 @@ productReference = 02AE64EB29363DBF005A4AF3 /* BeReal.app */; productType = "com.apple.product-type.application"; }; - 02AE64FA29363DC1005A4AF3 /* BeRealTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 02AE651229363DC1005A4AF3 /* Build configuration list for PBXNativeTarget "BeRealTests" */; - buildPhases = ( - 02AE64F729363DC1005A4AF3 /* Sources */, - 02AE64F829363DC1005A4AF3 /* Frameworks */, - 02AE64F929363DC1005A4AF3 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 02AE64FD29363DC1005A4AF3 /* PBXTargetDependency */, - ); - name = BeRealTests; - productName = BeRealTests; - productReference = 02AE64FB29363DC1005A4AF3 /* BeRealTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; 02AE650429363DC1005A4AF3 /* BeRealUITests */ = { isa = PBXNativeTarget; buildConfigurationList = 02AE651529363DC1005A4AF3 /* Build configuration list for PBXNativeTarget "BeRealUITests" */; @@ -251,10 +214,6 @@ 02AE64EA29363DBF005A4AF3 = { CreatedOnToolsVersion = 14.1; }; - 02AE64FA29363DC1005A4AF3 = { - CreatedOnToolsVersion = 14.1; - TestTargetID = 02AE64EA29363DBF005A4AF3; - }; 02AE650429363DC1005A4AF3 = { CreatedOnToolsVersion = 14.1; TestTargetID = 02AE64EA29363DBF005A4AF3; @@ -275,7 +234,6 @@ projectRoot = ""; targets = ( 02AE64EA29363DBF005A4AF3 /* BeReal */, - 02AE64FA29363DC1005A4AF3 /* BeRealTests */, 02AE650429363DC1005A4AF3 /* BeRealUITests */, ); }; @@ -291,13 +249,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 02AE64F929363DC1005A4AF3 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 02AE650329363DC1005A4AF3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -318,14 +269,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 02AE64F729363DC1005A4AF3 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 02AE650029363DC1005A4AF3 /* BeRealTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 02AE650129363DC1005A4AF3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -338,11 +281,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 02AE64FD29363DC1005A4AF3 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 02AE64EA29363DBF005A4AF3 /* BeReal */; - targetProxy = 02AE64FC29363DC1005A4AF3 /* PBXContainerItemProxy */; - }; 02AE650729363DC1005A4AF3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 02AE64EA29363DBF005A4AF3 /* BeReal */; @@ -472,7 +410,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"BeReal/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = "\"BeReal/Assets\""; DEVELOPMENT_TEAM = 7FMNM89WKG; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -506,7 +444,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"BeReal/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = "\"BeReal/Assets\""; DEVELOPMENT_TEAM = 7FMNM89WKG; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -533,52 +471,6 @@ }; name = Release; }; - 02AE651329363DC1005A4AF3 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 7FMNM89WKG; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.rockncode.app.assignment.be-real.tests.unit"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BeReal.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/BeReal"; - }; - name = Debug; - }; - 02AE651429363DC1005A4AF3 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 7FMNM89WKG; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.rockncode.app.assignment.be-real.tests.unit"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BeReal.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/BeReal"; - }; - name = Release; - }; 02AE651629363DC1005A4AF3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -644,15 +536,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 02AE651229363DC1005A4AF3 /* Build configuration list for PBXNativeTarget "BeRealTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 02AE651329363DC1005A4AF3 /* Debug */, - 02AE651429363DC1005A4AF3 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 02AE651529363DC1005A4AF3 /* Build configuration list for PBXNativeTarget "BeRealUITests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/BeReal/BeRealApp.swift b/BeReal/App/BeRealApp.swift similarity index 100% rename from BeReal/BeRealApp.swift rename to BeReal/App/BeRealApp.swift diff --git a/BeReal/Assets.xcassets/AccentColor.colorset/Contents.json b/BeReal/Assets/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from BeReal/Assets.xcassets/AccentColor.colorset/Contents.json rename to BeReal/Assets/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/BeReal/Assets.xcassets/AppIcon.appiconset/Contents.json b/BeReal/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from BeReal/Assets.xcassets/AppIcon.appiconset/Contents.json rename to BeReal/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/BeReal/Assets.xcassets/AppIcon.appiconset/MyFiles.png b/BeReal/Assets/Assets.xcassets/AppIcon.appiconset/MyFiles.png similarity index 100% rename from BeReal/Assets.xcassets/AppIcon.appiconset/MyFiles.png rename to BeReal/Assets/Assets.xcassets/AppIcon.appiconset/MyFiles.png diff --git a/BeReal/Assets.xcassets/Contents.json b/BeReal/Assets/Assets.xcassets/Contents.json similarity index 100% rename from BeReal/Assets.xcassets/Contents.json rename to BeReal/Assets/Assets.xcassets/Contents.json diff --git a/BeReal/Preview Content/Preview Assets.xcassets/Contents.json b/BeReal/Assets/Preview Assets.xcassets/Contents.json similarity index 100% rename from BeReal/Preview Content/Preview Assets.xcassets/Contents.json rename to BeReal/Assets/Preview Assets.xcassets/Contents.json diff --git a/BeRealTests/BeRealTests.swift b/BeRealTests/BeRealTests.swift deleted file mode 100644 index 5dc634c..0000000 --- a/BeRealTests/BeRealTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// BeRealTests.swift -// BeRealTests -// -// Created by Javier Cicchelli on 29/11/2022. -// Copyright © 2022 Röck+Cöde. All rights reserved. -// - -import XCTest -@testable import BeReal - -final class BeRealTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} From d8efab9c315c924fc4127aaf8bcfefee996a0665 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 20 Dec 2022 02:58:40 +0100 Subject: [PATCH 08/10] Tweaked the README file a bit more. --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0b1d382..946ea40 100644 --- a/README.md +++ b/README.md @@ -8,20 +8,20 @@ In a nutshell, the purpose of this application is to manage a remote file system In its current state, the application allows its users to: -* *handle GET, POST and DELETE requests to the API endpoint;* -* *require the user to login to the app to access the file system;* -* *load the items located at the root folder;* -* *browse through the folders hierarchy;* -* *create new folder in the current folder location;* -* *support for pull-to-refresh on folders;* -* *open JPG and PNG images in the image viewer;* -* *upload documents from the phone or an **iCloud** location to the current folder location;* -* *download document from the current folder location to the phone or an **iCloud** location;* -* *delete existing folders in the current folder location;* -* *delete existing documents in the current folder location;* -* *show available user information in the profile;* -* *allow the user to logout from the app;* -* *show to the user relevant errors that could happen while using the app.* +- [x] handle GET, POST and DELETE requests to the API endpoint;* +- [x] require the user to login to the app to access the file system;* +- [x] load the items located at the root folder;* +- [x] browse through the folders hierarchy;* +- [x] create new folder in the current folder location;* +- [x] support for pull-to-refresh on folders;* +- [x] open JPG and PNG images in the image viewer;* +- [x] upload documents from the phone or an **iCloud** location to the current folder location;* +- [x] download document from the current folder location to the phone or an **iCloud** location;* +- [x] delete existing folders in the current folder location;* +- [x] delete existing documents in the current folder location;* +- [x] show available user information in the profile;* +- [x] allow the user to logout from the app;* +- [x] show to the user relevant errors that could happen while using the app.* > It is necessary to clarify that, to use any **iCloud** location, the user is required to have an actual **iCloud** account and to be logged to that **iCloud** account in the phone. @@ -53,4 +53,4 @@ Currently, the work that has been left outside of this assignment - [ ] Write UI tests for happy and error paths with the `XCUITest` framework; - [ ] Set Xcode plugin that uses the `SwiftFormat` library to lint and format the source code on development delivery pipeline; - [ ] Set Xcode plugin that uses `DocC` library to generate Xcode and web-ready documentation on release delivery pipeline; -- [ ] Set [Xcode Cloud](https://developer.apple.com/xcode-cloud/) as continuous integration and delivery service to **TestFlight** and to the **App Store**. \ No newline at end of file +- [ ] Set [Xcode Cloud](https://developer.apple.com/xcode-cloud/) as continuous integration and delivery service to **TestFlight** and to the **App Store**. From e9a7e8b33b9b5a774ba67af9dd0a0f452677125f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 20 Dec 2022 03:33:03 +0100 Subject: [PATCH 09/10] Fixed the "logUserOut()" function in the ProfileView view for the Profile module to actually remove the account from the keychain storage. --- .../Sources/Profile/UI/Views/ProfileView.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Modules/Sources/Profile/UI/Views/ProfileView.swift b/Modules/Sources/Profile/UI/Views/ProfileView.swift index 9e3b7fd..d72fe28 100644 --- a/Modules/Sources/Profile/UI/Views/ProfileView.swift +++ b/Modules/Sources/Profile/UI/Views/ProfileView.swift @@ -7,6 +7,7 @@ // import DataModels +import KeychainStorage import SwiftUI public struct ProfileView: View { @@ -15,6 +16,10 @@ public struct ProfileView: View { @Environment(\.dismiss) private var dismiss + // MARK: Storages + + @KeychainStorage(key: .KeychainStorage.account) var account: Account? + // MARK: Properties private let user: User? @@ -84,7 +89,7 @@ public struct ProfileView: View { Section { Button { - logout() + Task { await logUserOut() } } label: { Text( "profile.button.log_out.text", @@ -107,6 +112,16 @@ public struct ProfileView: View { } +// MARK: - Helpers + +private extension ProfileView { + func logUserOut() async { + account = nil + + logout() + } +} + // MARK: - Images+Constants private extension Image { From 3ecf5c7468f815795d4cb9cc95a36c9eeacca3d8 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 20 Dec 2022 03:34:24 +0100 Subject: [PATCH 10/10] Fixed the initialisation of the keychain storage in the GetUserUseCase use case --- BeReal/UI/Views/ContentView.swift | 2 +- .../UseCases/Users/GetUserUseCase.swift | 22 +++++++------------ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/BeReal/UI/Views/ContentView.swift b/BeReal/UI/Views/ContentView.swift index a9316c6..bd0359c 100644 --- a/BeReal/UI/Views/ContentView.swift +++ b/BeReal/UI/Views/ContentView.swift @@ -95,7 +95,7 @@ private extension ContentView { do { user = try await getUser() } catch { - // TODO: Handle this error appropriately. + showSheet = .login } } } diff --git a/Libraries/Sources/UseCases/Users/GetUserUseCase.swift b/Libraries/Sources/UseCases/Users/GetUserUseCase.swift index fe2eaed..0109d6d 100644 --- a/Libraries/Sources/UseCases/Users/GetUserUseCase.swift +++ b/Libraries/Sources/UseCases/Users/GetUserUseCase.swift @@ -14,26 +14,24 @@ import KeychainStorage public actor GetUserUseCase { + // MARK: Storages + + @KeychainStorage(key: .KeychainStorage.account) var account: Account? + // MARK: Properties private let apiService: APIService - private var account: Account? - // MARK: Initialisers - public init( - apiService: APIService, - account: Account? - ) { + public init(apiService: APIService) { self.apiService = apiService - self.account = account } // MARK: Functions public func callAsFunction() async throws -> User { - guard let account else { throw GetUserError .accountNotFound } + guard let account else { throw GetUserError.accountNotFound } return try await getUser( username: account.username, @@ -65,12 +63,8 @@ public actor GetUserUseCase { public extension GetUserUseCase { init() { @Dependency(\.apiService) var apiService - @KeychainStorage(key: .KeychainStorage.account) var account: Account? - - self.init( - apiService: apiService, - account: account - ) + + self.init(apiService: apiService) } }