Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions IOSAccessAssessment.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
55659C122BB786580094DF01 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55659C112BB786580094DF01 /* ContentView.swift */; };
55659C142BB786700094DF01 /* AnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55659C132BB786700094DF01 /* AnnotationView.swift */; };
55C2F2842C00078D00B633D7 /* ObjectLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C2F2832C00078D00B633D7 /* ObjectLocation.swift */; };
CAA947762CDE6FBD000C6918 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA947752CDE6FBB000C6918 /* LoginView.swift */; };
CAA947792CDE700A000C6918 /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA947782CDE7007000C6918 /* AuthService.swift */; };
CAA9477B2CDE70D9000C6918 /* KeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9477A2CDE70D5000C6918 /* KeychainService.swift */; };
DA6332E72BAE3998009C80F9 /* espnetv2_pascal_256.mlmodel in Resources */ = {isa = PBXBuildFile; fileRef = 3222F94B2B62FF2E0019A079 /* espnetv2_pascal_256.mlmodel */; };
DAA2A9942CD766C400D41D81 /* deeplabv3plus_mobilenet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = DAA2A9932CD766C400D41D81 /* deeplabv3plus_mobilenet.mlmodel */; };
DAA7F8B52CA38C11003666D8 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA7F8B42CA38C11003666D8 /* Constants.swift */; };
Expand Down Expand Up @@ -68,6 +71,9 @@
55659C112BB786580094DF01 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
55659C132BB786700094DF01 /* AnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationView.swift; sourceTree = "<group>"; };
55C2F2832C00078D00B633D7 /* ObjectLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectLocation.swift; sourceTree = "<group>"; };
CAA947752CDE6FBB000C6918 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
CAA947782CDE7007000C6918 /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = "<group>"; };
CAA9477A2CDE70D5000C6918 /* KeychainService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainService.swift; sourceTree = "<group>"; };
DAA2A9932CD766C400D41D81 /* deeplabv3plus_mobilenet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; path = deeplabv3plus_mobilenet.mlmodel; sourceTree = "<group>"; };
DAA7F8B42CA38C11003666D8 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
DAA7F8B62CA3E4E7003666D8 /* SpinnerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -128,6 +134,7 @@
3222F9182B622DFD0019A079 /* IOSAccessAssessment */ = {
isa = PBXGroup;
children = (
CAA947772CDE7001000C6918 /* Services */,
DAA7F8C62CA76514003666D8 /* ImageProcessing */,
DAA7F8C52CA6858C003666D8 /* Location */,
DAA7F8BE2CA683DC003666D8 /* Segmentation */,
Expand Down Expand Up @@ -187,6 +194,7 @@
55659C0E2BB786240094DF01 /* Views */ = {
isa = PBXGroup;
children = (
CAA947752CDE6FBB000C6918 /* LoginView.swift */,
55659C0F2BB7863F0094DF01 /* SetupView.swift */,
55659C112BB786580094DF01 /* ContentView.swift */,
55659C132BB786700094DF01 /* AnnotationView.swift */,
Expand All @@ -196,6 +204,15 @@
path = Views;
sourceTree = "<group>";
};
CAA947772CDE7001000C6918 /* Services */ = {
isa = PBXGroup;
children = (
CAA9477A2CDE70D5000C6918 /* KeychainService.swift */,
CAA947782CDE7007000C6918 /* AuthService.swift */,
);
path = Services;
sourceTree = "<group>";
};
DAA7F8BB2CA67A3C003666D8 /* Camera */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -371,12 +388,15 @@
55659C082BB785CB0094DF01 /* CameraController.swift in Sources */,
DAA7F8C82CA76527003666D8 /* CIImageUtils.swift in Sources */,
55659C142BB786700094DF01 /* AnnotationView.swift in Sources */,
CAA947792CDE700A000C6918 /* AuthService.swift in Sources */,
55659C122BB786580094DF01 /* ContentView.swift in Sources */,
552D9A3D2BE0378E00E17E70 /* kernel.metal in Sources */,
55659C0D2BB786100094DF01 /* CameraManager.swift in Sources */,
55659C102BB7863F0094DF01 /* SetupView.swift in Sources */,
DAA7F8CA2CA76550003666D8 /* CVPixelBufferUtils.swift in Sources */,
DAA7F8B72CA3E4E7003666D8 /* SpinnerView.swift in Sources */,
CAA9477B2CDE70D9000C6918 /* KeychainService.swift in Sources */,
CAA947762CDE6FBD000C6918 /* LoginView.swift in Sources */,
3222F91A2B622DFD0019A079 /* IOSAccessAssessmentApp.swift in Sources */,
DAA2A9942CD766C400D41D81 /* deeplabv3plus_mobilenet.mlmodel in Sources */,
DAA7F8C42CA68513003666D8 /* AnnotationCameraViewController.swift in Sources */,
Expand Down
14 changes: 13 additions & 1 deletion IOSAccessAssessment/IOSAccessAssessmentApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,21 @@ import SwiftUI

@main
struct IOSAccessAssessmentApp: App {
private let keychainService = KeychainService()
@State private var isAuthenticated: Bool

init() {
let isTokenValid = keychainService.isTokenValid()
_isAuthenticated = State(initialValue: isTokenValid)
Copy link
Collaborator

@himanshunaidu himanshunaidu Nov 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So mostly, I do not see a problem with this kind of initialization for this context, despite its misgivings given in:
https://stackoverflow.com/questions/56691630/swiftui-state-var-initialization-issue
More specifically: https://stackoverflow.com/a/71247040

I wonder if there would come a situation where this initialization could cause problems. Most likely it shouldn't.
I'll just keep it in mind.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just as a note, we don't have a problem with this way to setting the State because the struct IOSAccessAssessment does not have any dependencies.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this article does mention that this is bad practice, since we are accommodating what may lead to potentially inconsistent behavior:
https://www.swiftcraft.io/articles/how-to-initialize-state-inside-the-views-init-

}

var body: some Scene {
WindowGroup {
SetupView()
if isAuthenticated {
SetupView()
} else {
LoginView(isAuthenticated: $isAuthenticated)
}
}
}
}
155 changes: 155 additions & 0 deletions IOSAccessAssessment/Services/AuthService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//
// AuthService.swift
// IOSAccessAssessment
//
// Created by Mariana Piz on 08.11.2024.
//

import Foundation

enum AuthError: Error, LocalizedError {
case invalidURL
case noData
case invalidResponse
case serverError(message: String)
case decodingError
case unknownError

var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid URL."
case .noData:
return "No data received from the server."
case .invalidResponse:
return "Invalid response from the server."
case .serverError(let message):
return message
case .decodingError:
return "Failed to decode the response from the server."
case .unknownError:
return "An unknown error occurred."
}
}
}

struct AuthResponse: Decodable {
let accessToken: String
let refreshToken: String
let expiresIn: Int
let refreshExpiresIn: Int

private enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case refreshToken = "refresh_token"
case expiresIn = "expires_in"
case refreshExpiresIn = "refresh_expires_in"
}
}

struct ErrorResponse: Decodable {
let timestamp: String
let status: String
let message: String
let errors: [String]?
}

class AuthService {

private enum Constants {
static let serverUrl = "https://tdei-gateway-stage.azurewebsites.net/api/v1/authenticate"
}

func authenticate(
username: String,
password: String,
completion: @escaping (Result<AuthResponse, AuthError>) -> Void
) {
guard let request = createRequest(username: username, password: password) else {
completion(.failure(.invalidURL))
return
}

URLSession.shared.dataTask(with: request) { data, response, error in
if let error {
completion(.failure(.serverError(message: error.localizedDescription)))
return
}

guard let data else {
completion(.failure(.noData))
return
}

guard let httpResponse = response as? HTTPURLResponse else {
completion(.failure(.invalidResponse))
return
}

self.handleResponse(data: data,
httpResponse: httpResponse,
completion: completion)
}.resume()
}

private func createRequest(username: String, password: String) -> URLRequest? {
guard let url = URL(string: Constants.serverUrl) else { return nil }

var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

let body = [
"username": username,
"password": password
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])

return request
}

private func handleResponse(
data: Data,
httpResponse: HTTPURLResponse,
completion: @escaping (Result<AuthResponse, AuthError>) -> Void
) {
if (200...299).contains(httpResponse.statusCode) {
decodeSuccessResponse(data: data,
completion: completion)
} else {
decodeErrorResponse(data: data,
statusCode: httpResponse.statusCode,
completion: completion)
}
}

private func decodeSuccessResponse(
data: Data,
completion: @escaping (Result<AuthResponse, AuthError>) -> Void
) {
do {
let authResponse = try JSONDecoder().decode(AuthResponse.self, from: data)
completion(.success(authResponse))
} catch {
completion(.failure(.decodingError))
}
}

private func decodeErrorResponse(
data: Data,
statusCode: Int,
completion: @escaping (Result<AuthResponse, AuthError>) -> Void
) {
do {
let errorResponse = try JSONDecoder().decode(ErrorResponse.self, from: data)
let errorMessage = errorResponse.message
.appending(": ")
.appending(errorResponse.errors?.joined(separator: "\n") ?? "")

completion(.failure(.serverError(message: errorMessage)))
} catch {
completion(.failure(.decodingError))
}
}

}
111 changes: 111 additions & 0 deletions IOSAccessAssessment/Services/KeychainService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// KeychainService.swift
// IOSAccessAssessment
//
// Created by Mariana Piz on 08.11.2024.
//

import Foundation
import Security

enum KeychainKey: String {
case accessToken
case expirationDate
}

final class KeychainService {

func setValue(_ value: String, for key: KeychainKey) {
guard let encodedValue = value.data(using: .utf8) else { return }

let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue
]

var status = SecItemCopyMatching(query as CFDictionary, nil)
if status == errSecSuccess {
let attributesToUpdate: [String: Any] = [
kSecValueData as String: encodedValue
]
status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
} else if status == errSecItemNotFound {
var newItem = query
newItem[kSecValueData as String] = encodedValue
status = SecItemAdd(newItem as CFDictionary, nil)
}

if status != errSecSuccess {
print("Keychain setValue error: \(status)")
}
}

func getValue(for key: KeychainKey) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue,
kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String: kSecMatchLimitOne
]

var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
SecItemCopyMatching(query as CFDictionary, $0)
}

guard status == errSecSuccess else {
return nil
}

if let data = queryResult as? Data, let value = String(data: data, encoding: .utf8) {
return value
}

return nil
}

func removeValue(for key: KeychainKey) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue
]

let status = SecItemDelete(query as CFDictionary)
if status != errSecSuccess && status != errSecItemNotFound {
print("Keychain removeValue error: \(status)")
}
}

func setDate(_ date: Date, for key: KeychainKey) {
let dateString = dateToString(date)
setValue(dateString, for: key)
}

func getDate(for key: KeychainKey) -> Date? {
guard let dateString = getValue(for: key) else { return nil }
return stringToDate(dateString)
}

func isTokenValid() -> Bool {
guard let _ = getValue(for: .accessToken) else {
return false
}

if let expirationDate = getDate(for: .expirationDate), expirationDate > Date() {
return true
}

return false
}

private func dateToString(_ date: Date) -> String {
let formatter = ISO8601DateFormatter()
return formatter.string(from: date)
}

private func stringToDate(_ dateString: String) -> Date? {
let formatter = ISO8601DateFormatter()
return formatter.date(from: dateString)
}

}
Loading