-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from udacity/Exercise-SwiftUI-Tasks
Exercise: SwiftUI Tasks
- Loading branch information
Showing
48 changed files
with
3,195 additions
and
0 deletions.
There are no files selected for viewing
Binary file modified
BIN
+2.01 KB
(100%)
...odeproj/project.xcworkspace/xcuserdata/guerrix.xcuserdatad/UserInterfaceState.xcuserstate
Binary file not shown.
28 changes: 28 additions & 0 deletions
28
lesson-2-concurrency-in-iOS-apps/exercises/swiftUI-tasks/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
### Exercise: SwiftUI Tasks | ||
|
||
## Overview | ||
|
||
This exercise is a continuation of the TripJournal app. You will refactor the app to use SwiftUI's `Task` modifier for managing asynchronous operations, ensuring that tasks are properly managed and canceled when no longer needed. | ||
|
||
#### Objectives | ||
- Use the `Task` modifier to manage asynchronous operations. | ||
- Cancel tasks when they are no longer needed. | ||
- Ensure state changes and UI updates are properly handled. | ||
|
||
#### Instructions | ||
|
||
1. **Starter:** | ||
- Navigate to the starter folder and open `TripJournal.xcodeproj`. | ||
- Your task is to fill in the missing code as indicated by comments in the Swift files (`AuthView.swift`, `TripList.swift`, `TripForm.swift`). | ||
|
||
2. **Solution:** | ||
- After you complete the exercise, or if you need to check your work, you can find the complete code in the solution folder. | ||
- Compare your solution with the completed code to understand different approaches or to debug any issues you encountered. | ||
|
||
#### Setup | ||
- Ensure you have the latest version of Xcode installed on your Mac. | ||
- Open the Xcode project from the `starter` folder to begin the exercise. | ||
|
||
Feel free to reach out if you encounter any difficulties or have questions regarding the exercises. | ||
|
||
Happy coding! |
447 changes: 447 additions & 0 deletions
447
...rrency-in-iOS-apps/exercises/swiftUI-tasks/solution/TripJournal.xcodeproj/project.pbxproj
Large diffs are not rendered by default.
Oops, something went wrong.
7 changes: 7 additions & 0 deletions
7
...swiftUI-tasks/solution/TripJournal.xcodeproj/project.xcworkspace/contents.xcworkspacedata
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
8 changes: 8 additions & 0 deletions
8
.../solution/TripJournal.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>IDEDidComputeMac32BitWarning</key> | ||
<true/> | ||
</dict> | ||
</plist> |
10 changes: 10 additions & 0 deletions
10
lesson-2-concurrency-in-iOS-apps/exercises/swiftUI-tasks/solution/TripJournal/App.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import SwiftUI | ||
|
||
@main | ||
struct TripJournalApp: App { | ||
var body: some Scene { | ||
WindowGroup { | ||
RootView(service: JournalServiceLive()) | ||
} | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
...ses/swiftUI-tasks/solution/TripJournal/Assets.xcassets/AccentColor.colorset/Contents.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"colors" : [ | ||
{ | ||
"color" : { | ||
"platform" : "universal", | ||
"reference" : "systemPurpleColor" | ||
}, | ||
"idiom" : "universal" | ||
} | ||
], | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
...cises/swiftUI-tasks/solution/TripJournal/Assets.xcassets/AppIcon.appiconset/Contents.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"images" : [ | ||
{ | ||
"filename" : "icon.png", | ||
"idiom" : "universal", | ||
"platform" : "ios", | ||
"size" : "1024x1024" | ||
} | ||
], | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} |
Binary file added
BIN
+35.3 KB
.../swiftUI-tasks/solution/TripJournal/Assets.xcassets/AppIcon.appiconset/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions
15
...ises/swiftUI-tasks/solution/TripJournal/Assets.xcassets/AuthHeader.imageset/Contents.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"images" : [ | ||
{ | ||
"filename" : "icon.png", | ||
"idiom" : "universal" | ||
} | ||
], | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
}, | ||
"properties" : { | ||
"template-rendering-intent" : "original" | ||
} | ||
} |
Binary file added
BIN
+35.3 KB
...swiftUI-tasks/solution/TripJournal/Assets.xcassets/AuthHeader.imageset/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions
6
...cy-in-iOS-apps/exercises/swiftUI-tasks/solution/TripJournal/Assets.xcassets/Contents.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} |
138 changes: 138 additions & 0 deletions
138
...-concurrency-in-iOS-apps/exercises/swiftUI-tasks/solution/TripJournal/Auth/AuthView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import SwiftUI | ||
|
||
struct AuthView: View { | ||
/// Describes validation errors that might occur locally in the form. | ||
struct ValidationError: LocalizedError { | ||
var errorDescription: String? | ||
|
||
static let emptyUsername = Self(errorDescription: "Username is required.") | ||
static let emptyPassword = Self(errorDescription: "Password is required.") | ||
} | ||
|
||
@State private var username: String = "" | ||
@State private var password: String = "" | ||
@State private var isLoading = false | ||
@State private var error: Error? | ||
@Environment(\.journalService) private var journalService | ||
|
||
@State private var task: Task<Void, Never>? = nil | ||
|
||
// MARK: - Body | ||
|
||
var body: some View { | ||
Form { | ||
Section( | ||
content: inputs, | ||
header: header, | ||
footer: buttons | ||
) | ||
} | ||
.loadingOverlay(isLoading) | ||
.alert(error: $error) | ||
.task { | ||
await checkTokenExpiration() | ||
} | ||
.onDisappear { | ||
task?.cancel() | ||
} | ||
} | ||
|
||
// MARK: - Views | ||
|
||
private func header() -> some View { | ||
Image(.authHeader) | ||
.resizable() | ||
.frame(width: 100, height: 100) | ||
.clipShape(RoundedRectangle(cornerRadius: 25)) | ||
.frame(maxWidth: .infinity, alignment: .center) | ||
.padding(.top, 30) | ||
.padding(.bottom, 70) | ||
} | ||
|
||
@ViewBuilder | ||
private func inputs() -> some View { | ||
TextField("Username", text: $username) | ||
.autocorrectionDisabled() | ||
.textInputAutocapitalization(.never) | ||
.textContentType(.username) | ||
SecureField("Password", text: $password) | ||
.autocorrectionDisabled() | ||
.textInputAutocapitalization(.never) | ||
.textContentType(.password) | ||
} | ||
|
||
private func buttons() -> some View { | ||
VStack(alignment: .center, spacing: 10) { | ||
Button( | ||
action: { | ||
task = Task { | ||
await logIn() | ||
} | ||
}, | ||
label: { | ||
Text("Log In") | ||
.frame(maxWidth: .infinity, alignment: .center) | ||
} | ||
) | ||
.buttonBorderShape(.capsule) | ||
.buttonStyle(.borderedProminent) | ||
|
||
Button( | ||
action: { | ||
task = Task { | ||
await register() | ||
} | ||
}, | ||
label: { | ||
Text("Create Account") | ||
.frame(maxWidth: .infinity, alignment: .center) | ||
} | ||
) | ||
.buttonBorderShape(.capsule) | ||
.buttonStyle(.bordered) | ||
} | ||
.padding() | ||
} | ||
|
||
// MARK: - Networking | ||
|
||
private func validateForm() throws { | ||
if username.nonEmpty == nil { | ||
throw ValidationError.emptyUsername | ||
} | ||
if password.nonEmpty == nil { | ||
throw ValidationError.emptyPassword | ||
} | ||
} | ||
|
||
private func logIn() async { | ||
isLoading = true | ||
do { | ||
try validateForm() | ||
try await journalService.logIn(username: username, password: password) | ||
} catch { | ||
self.error = error | ||
} | ||
isLoading = false | ||
} | ||
|
||
private func register() async { | ||
isLoading = true | ||
do { | ||
try validateForm() | ||
try await journalService.register(username: username, password: password) | ||
} catch { | ||
self.error = error | ||
} | ||
isLoading = false | ||
} | ||
|
||
private func checkTokenExpiration() async { | ||
DispatchQueue.main.async { | ||
guard journalService.tokenExpired else { | ||
return | ||
} | ||
error = SessionError.expired | ||
} | ||
} | ||
} |
78 changes: 78 additions & 0 deletions
78
...ncy-in-iOS-apps/exercises/swiftUI-tasks/solution/TripJournal/Helpers/KeychainHelper.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
// | ||
// KeychainHelper.swift | ||
// TripJournal | ||
// | ||
// Created by Jesus Guerra on 5/17/24. | ||
// | ||
|
||
import Foundation | ||
import Security | ||
|
||
class KeychainHelper { | ||
static let shared = KeychainHelper() | ||
private let serviceName = "com.TripJournal.service" | ||
private let accountName = "authToken" | ||
|
||
private init() {} | ||
|
||
func saveToken(_ token: Token) throws { | ||
let tokenData = try JSONEncoder().encode(token) | ||
let query: [String: Any] = [ | ||
kSecClass as String: kSecClassGenericPassword, | ||
kSecAttrService as String: serviceName, | ||
kSecAttrAccount as String: accountName, | ||
kSecValueData as String: tokenData, | ||
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked | ||
] | ||
|
||
SecItemDelete(query as CFDictionary) // Delete any existing item | ||
let status = SecItemAdd(query as CFDictionary, nil) | ||
|
||
guard status == errSecSuccess else { | ||
throw KeychainError.unableToSaveToken | ||
} | ||
} | ||
|
||
func getToken() throws -> Token? { | ||
let query: [String: Any] = [ | ||
kSecClass as String: kSecClassGenericPassword, | ||
kSecAttrService as String: serviceName, | ||
kSecAttrAccount as String: accountName, | ||
kSecReturnData as String: kCFBooleanTrue!, | ||
kSecMatchLimit as String: kSecMatchLimitOne | ||
] | ||
|
||
var dataTypeRef: AnyObject? = nil | ||
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) | ||
|
||
guard status == errSecSuccess else { | ||
return nil | ||
} | ||
|
||
guard let data = dataTypeRef as? Data else { | ||
return nil | ||
} | ||
|
||
return try JSONDecoder().decode(Token.self, from: data) | ||
} | ||
|
||
func deleteToken() throws { | ||
let query: [String: Any] = [ | ||
kSecClass as String: kSecClassGenericPassword, | ||
kSecAttrService as String: serviceName, | ||
kSecAttrAccount as String: accountName | ||
] | ||
|
||
let status = SecItemDelete(query as CFDictionary) | ||
|
||
guard status == errSecSuccess else { | ||
throw KeychainError.unableToDeleteToken | ||
} | ||
} | ||
|
||
enum KeychainError: Error { | ||
case unableToSaveToken | ||
case unableToDeleteToken | ||
} | ||
} | ||
|
13 changes: 13 additions & 0 deletions
13
...rcises/swiftUI-tasks/solution/TripJournal/JournalService/JournalService+Environment.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import SwiftUI | ||
|
||
private struct JournalServiceKey: EnvironmentKey { | ||
static var defaultValue: JournalService = JournalServiceLive() | ||
} | ||
|
||
extension EnvironmentValues { | ||
/// A service that can used to interact with the trip journal API. | ||
var journalService: JournalService { | ||
get { self[JournalServiceKey.self] } | ||
set { self[JournalServiceKey.self] = newValue } | ||
} | ||
} |
Oops, something went wrong.