From 2ad43928dbdaf1bb1b4c0638ed48243a0c6f44e8 Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Mon, 12 Jul 2021 01:28:52 +0300 Subject: [PATCH] Logging in and displaying alerts --- AutoCat2.xcodeproj/project.pbxproj | 18 ++++++++++ AutoCat2Tests/ApiTests.swift | 3 +- AutoCat2Tests/Mocks/MockURLProtocol.swift | 44 +++++++++++++++++++++++ Shared/ContentView.swift | 2 +- Shared/Extensions/Alert.swift | 28 +++++++++++++++ Shared/Utils/Api.swift | 4 +-- Shared/ViewModels/AuthVM.swift | 2 +- Shared/Views/AuthView.swift | 24 +++++++++---- 8 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 AutoCat2Tests/Mocks/MockURLProtocol.swift create mode 100644 Shared/Extensions/Alert.swift diff --git a/AutoCat2.xcodeproj/project.pbxproj b/AutoCat2.xcodeproj/project.pbxproj index 5fb9815..39d9fe5 100644 --- a/AutoCat2.xcodeproj/project.pbxproj +++ b/AutoCat2.xcodeproj/project.pbxproj @@ -32,6 +32,9 @@ 7A40D5FF2693A91F009B0BC4 /* CocoaError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A40D5FD2693A91F009B0BC4 /* CocoaError.swift */; }; 7A40D6022694FF5D009B0BC4 /* Api.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A40D6012694FF5D009B0BC4 /* Api.swift */; }; 7A40D6032694FF5D009B0BC4 /* Api.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A40D6012694FF5D009B0BC4 /* Api.swift */; }; + 7A40D60826998DCF009B0BC4 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A40D60726998DCF009B0BC4 /* Alert.swift */; }; + 7A40D60926998DCF009B0BC4 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A40D60726998DCF009B0BC4 /* Alert.swift */; }; + 7A40D60C2699A070009B0BC4 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A40D60B2699A070009B0BC4 /* MockURLProtocol.swift */; }; 7A683999269612EA00B2188A /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A683998269612EA00B2188A /* Response.swift */; }; 7A68399A269612EA00B2188A /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A683998269612EA00B2188A /* Response.swift */; }; 7ACD05D72695C08A00557667 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACD05D62695C08A00557667 /* Constants.swift */; }; @@ -108,6 +111,8 @@ 7A40D5F52693A63A009B0BC4 /* SettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTests.swift; sourceTree = ""; }; 7A40D5FD2693A91F009B0BC4 /* CocoaError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CocoaError.swift; sourceTree = ""; }; 7A40D6012694FF5D009B0BC4 /* Api.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Api.swift; sourceTree = ""; }; + 7A40D60726998DCF009B0BC4 /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = ""; }; + 7A40D60B2699A070009B0BC4 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; }; 7A683998269612EA00B2188A /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; 7ACD05D62695C08A00557667 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 7AEFAEEC26985A3400ED2C85 /* ACProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACProgressView.swift; sourceTree = ""; }; @@ -255,6 +260,7 @@ 7A40D5F42693A63A009B0BC4 /* AutoCat2Tests */ = { isa = PBXGroup; children = ( + 7A40D60A2699A04F009B0BC4 /* Mocks */, 7A40D5F52693A63A009B0BC4 /* SettingsTests.swift */, 7AF552D82696E5C100578083 /* ApiTests.swift */, ); @@ -265,6 +271,7 @@ isa = PBXGroup; children = ( 7A40D5FD2693A91F009B0BC4 /* CocoaError.swift */, + 7A40D60726998DCF009B0BC4 /* Alert.swift */, ); path = Extensions; sourceTree = ""; @@ -278,6 +285,14 @@ path = Utils; sourceTree = ""; }; + 7A40D60A2699A04F009B0BC4 /* Mocks */ = { + isa = PBXGroup; + children = ( + 7A40D60B2699A070009B0BC4 /* MockURLProtocol.swift */, + ); + path = Mocks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -474,6 +489,7 @@ 7AEFAEED26985A3400ED2C85 /* ACProgressView.swift in Sources */, 7A40D5A02691C6D8009B0BC4 /* AutoCat2App.swift in Sources */, 7A683999269612EA00B2188A /* Response.swift in Sources */, + 7A40D60826998DCF009B0BC4 /* Alert.swift in Sources */, 7A40D5A42691C6D8009B0BC4 /* Persistence.swift in Sources */, 7A40D5ED2693A1EA009B0BC4 /* AuthVM.swift in Sources */, 7A40D59E2691C6D8009B0BC4 /* AutoCat2.xcdatamodeld in Sources */, @@ -494,6 +510,7 @@ 7AEFAEEE26985A3400ED2C85 /* ACProgressView.swift in Sources */, 7A40D5A52691C6D8009B0BC4 /* Persistence.swift in Sources */, 7A68399A269612EA00B2188A /* Response.swift in Sources */, + 7A40D60926998DCF009B0BC4 /* Alert.swift in Sources */, 7A40D5E526924B0C009B0BC4 /* User.swift in Sources */, 7A40D5EE2693A1EA009B0BC4 /* AuthVM.swift in Sources */, 7A40D59F2691C6D8009B0BC4 /* AutoCat2.xcdatamodeld in Sources */, @@ -524,6 +541,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7A40D60C2699A070009B0BC4 /* MockURLProtocol.swift in Sources */, 7AF552D92696E5C100578083 /* ApiTests.swift in Sources */, 7A40D5F62693A63A009B0BC4 /* SettingsTests.swift in Sources */, ); diff --git a/AutoCat2Tests/ApiTests.swift b/AutoCat2Tests/ApiTests.swift index 75cd001..762e4b0 100644 --- a/AutoCat2Tests/ApiTests.swift +++ b/AutoCat2Tests/ApiTests.swift @@ -7,6 +7,7 @@ class ApiTests: XCTestCase { override func setUpWithError() throws { let sessionConfig = URLSessionConfiguration.default + sessionConfig.protocolClasses = [MockURLProtocol.self] let session = URLSession(configuration: sessionConfig) self.api = Api(session: session) } @@ -16,7 +17,7 @@ class ApiTests: XCTestCase { } func testLogin() async throws { - let user = try await self.api.login() + let user = try await self.api.login(email: "", password: "") } // func testPerformanceExample() throws { diff --git a/AutoCat2Tests/Mocks/MockURLProtocol.swift b/AutoCat2Tests/Mocks/MockURLProtocol.swift new file mode 100644 index 0000000..264640f --- /dev/null +++ b/AutoCat2Tests/Mocks/MockURLProtocol.swift @@ -0,0 +1,44 @@ +import Foundation + +class MockURLProtocol: URLProtocol { + + override class func canInit(with request: URLRequest) -> Bool { + // To check if this protocol can handle the given request. + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + // Here you return the canonical version of the request but most of the time you pass the orignal one. + return request + } + + override func startLoading() { + guard let handler = MockURLProtocol.requestHandler else { + fatalError("Handler is unavailable.") + } + + do { + // 2. Call handler with received request and capture the tuple of response and data. + let (response, data) = try handler(request) + + // 3. Send received response to the client. + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + + if let data = data { + // 4. Send received data to the client. + client?.urlProtocol(self, didLoad: data) + } + + // 5. Notify request has been finished. + client?.urlProtocolDidFinishLoading(self) + } catch { + // 6. Notify received error. + client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() { + // This is called if the request gets canceled or completed. + print("") + } +} diff --git a/Shared/ContentView.swift b/Shared/ContentView.swift index 7024e5f..92fe106 100644 --- a/Shared/ContentView.swift +++ b/Shared/ContentView.swift @@ -4,7 +4,7 @@ import CoreData struct ContentView: View { var body: some View { if Settings.shared.user.token.isEmpty { - AuthView(login: "", password: "") + AuthView() } else { EmptyView() } diff --git a/Shared/Extensions/Alert.swift b/Shared/Extensions/Alert.swift new file mode 100644 index 0000000..b6d95d2 --- /dev/null +++ b/Shared/Extensions/Alert.swift @@ -0,0 +1,28 @@ +import Foundation +import SwiftUI + +enum AlertMessage: Identifiable { + case info(title: String, body: String) + case error(error: Error) + + var id: Int { + switch self { + case .info(let title, let body): + return title.hashValue + body.hashValue + case .error(let error): + return error.localizedDescription.hashValue + } + } +} + +extension Alert { + init(_ message: AlertMessage) { + switch message { + case .info(let title, let body): + self.init(title: Text(title), message: Text(body)) + case .error(let error): + let msg = (error as NSError).displayMessage + self.init(title: Text(msg.title), message: Text(msg.body)) + } + } +} diff --git a/Shared/Utils/Api.swift b/Shared/Utils/Api.swift index 6661e6e..ae78cd0 100644 --- a/Shared/Utils/Api.swift +++ b/Shared/Utils/Api.swift @@ -95,11 +95,11 @@ public class Api { // MARK: - AutoCat public API - public static func login(email: String, password: String) async throws -> User { + public func login(email: String, password: String) async throws -> User { let body = [ "email": email, "password": password ] - return try await self.shared.makeBodyRequest(api: "user/login", body: body) + return try await self.makeBodyRequest(api: "user/login", body: body) } } diff --git a/Shared/ViewModels/AuthVM.swift b/Shared/ViewModels/AuthVM.swift index d1fe531..a1cd530 100644 --- a/Shared/ViewModels/AuthVM.swift +++ b/Shared/ViewModels/AuthVM.swift @@ -2,6 +2,6 @@ import Foundation public class AuthVM: ObservableObject { public func login(user: String, password: String) async throws { - + Settings.shared.user = try await Api.shared.login(email: user, password: password) } } diff --git a/Shared/Views/AuthView.swift b/Shared/Views/AuthView.swift index 0594e3c..942adbd 100644 --- a/Shared/Views/AuthView.swift +++ b/Shared/Views/AuthView.swift @@ -3,9 +3,10 @@ import SwiftUI struct AuthView: View { @ObservedObject var viewModel = AuthVM() - @State var login: String - @State var password: String - @State var showProgress: Bool = false + @State private var login: String = "" + @State private var password: String = "" + @State private var showProgress: Bool = false + @State private var alert: AlertMessage? = nil var body: some View { ZStack { @@ -14,11 +15,20 @@ struct AuthView: View { TextField("Login", text: $login) SecureField("Password", text: $password) Button("Login") { - self.showProgress = true async { - try await self.viewModel.login(user: self.login, password: self.password) + do { + self.showProgress = true + try await self.viewModel.login(user: self.login, password: self.password) + self.showProgress = false + } catch { + self.showProgress = false + self.alert = .error(error: error) + } } } + .alert(item: $alert) { id in + Alert(id) + } Spacer() } .buttonStyle(.bordered) @@ -35,9 +45,9 @@ struct AuthView: View { struct AuthView_Previews: PreviewProvider { static var previews: some View { Group { - AuthView(login: "", password: "") + AuthView() .previewDevice("iPhone 8") - AuthView(login: "", password: "") + AuthView() .preferredColorScheme(.dark) .previewDevice("iPhone 8") }