Merge pull request #1 from knezzy/master
Added user data store as an example of ObservableObject.
This commit is contained in:
commit
8a56f7bd2c
4 changed files with 94 additions and 51 deletions
|
@ -22,6 +22,7 @@
|
|||
28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC9528233C433400BFB70A /* TransactionRepository.swift */; };
|
||||
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC952B233C434800BFB70A /* UserRepository.swift */; };
|
||||
28AC952E233C43A300BFB70A /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC952D233C43A300BFB70A /* User.swift */; };
|
||||
543ECE42233E82A40018A9D9 /* UserDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543ECE41233E82A40018A9D9 /* UserDataStore.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -65,6 +66,7 @@
|
|||
28AC9528233C433400BFB70A /* TransactionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionRepository.swift; sourceTree = "<group>"; };
|
||||
28AC952B233C434800BFB70A /* UserRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepository.swift; sourceTree = "<group>"; };
|
||||
28AC952D233C43A300BFB70A /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
|
||||
543ECE41233E82A40018A9D9 /* UserDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataStore.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -187,6 +189,7 @@
|
|||
28AC952B233C434800BFB70A /* UserRepository.swift */,
|
||||
28AC952D233C43A300BFB70A /* User.swift */,
|
||||
2857EAEE233DA73B0026BC83 /* UserData.swift */,
|
||||
543ECE41233E82A40018A9D9 /* UserDataStore.swift */,
|
||||
);
|
||||
path = User;
|
||||
sourceTree = "<group>";
|
||||
|
@ -332,6 +335,7 @@
|
|||
28AC9525233C42D100BFB70A /* BudgetApiService.swift in Sources */,
|
||||
2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */,
|
||||
28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */,
|
||||
543ECE42233E82A40018A9D9 /* UserDataStore.swift in Sources */,
|
||||
28AC952E233C43A300BFB70A /* User.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -16,7 +16,7 @@ class BudgetApiService {
|
|||
self.requestHelper = requestHelper
|
||||
}
|
||||
|
||||
func login(username: String, password: String) -> Future<User, NetworkError> {
|
||||
func login(username: String, password: String) -> AnyPublisher<User, NetworkError> {
|
||||
requestHelper.credentials = (username, password)
|
||||
return requestHelper.post(
|
||||
endPoint: "/users/login",
|
||||
|
@ -24,11 +24,11 @@ class BudgetApiService {
|
|||
)
|
||||
}
|
||||
|
||||
func getUser(id: Int) -> Future<User, NetworkError> {
|
||||
func getUser(id: Int) -> AnyPublisher<User, NetworkError> {
|
||||
return requestHelper.get(endPoint: "/users/\(id)")
|
||||
}
|
||||
|
||||
func searchUsers(query: String) -> Future<[User], NetworkError> {
|
||||
func searchUsers(query: String) -> AnyPublisher<[User], NetworkError> {
|
||||
return requestHelper.get(
|
||||
endPoint: "/users/search",
|
||||
queries: ["query": [query]]
|
||||
|
@ -40,6 +40,8 @@ class RequestHelper {
|
|||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
let baseUrl: String
|
||||
// Note:
|
||||
// There shouldn't be a reason to sink when building a typical request.
|
||||
private var subscriptions = Set<AnyCancellable>()
|
||||
var credentials: (String, String)?
|
||||
|
||||
|
@ -50,7 +52,7 @@ class RequestHelper {
|
|||
func get<ResultType: Codable>(
|
||||
endPoint: String,
|
||||
queries: [String: Array<String>]? = nil
|
||||
) -> Future<ResultType, NetworkError> {
|
||||
) -> AnyPublisher<ResultType, NetworkError> {
|
||||
var combinedEndPoint = endPoint
|
||||
if (queries != nil) {
|
||||
for (key, values) in queries! {
|
||||
|
@ -67,7 +69,7 @@ class RequestHelper {
|
|||
func post<ResultType: Codable>(
|
||||
endPoint: String,
|
||||
data: Codable
|
||||
) -> Future<ResultType, NetworkError> {
|
||||
) -> AnyPublisher<ResultType, NetworkError> {
|
||||
return buildRequest(
|
||||
endPoint: endPoint,
|
||||
method: "POST",
|
||||
|
@ -78,7 +80,7 @@ class RequestHelper {
|
|||
func put<ResultType: Codable>(
|
||||
endPoint: String,
|
||||
data: ResultType
|
||||
) -> Future<ResultType, NetworkError> {
|
||||
) -> AnyPublisher<ResultType, NetworkError> {
|
||||
return buildRequest(
|
||||
endPoint: endPoint,
|
||||
method: "PUT",
|
||||
|
@ -86,7 +88,7 @@ class RequestHelper {
|
|||
)
|
||||
}
|
||||
|
||||
func delete<ResultType: Codable>(endPoint: String) -> Future<ResultType, NetworkError> {
|
||||
func delete<ResultType: Codable>(endPoint: String) -> AnyPublisher<ResultType, NetworkError> {
|
||||
return buildRequest(endPoint: endPoint, method: "DELETE")
|
||||
}
|
||||
|
||||
|
@ -94,48 +96,42 @@ class RequestHelper {
|
|||
endPoint: String,
|
||||
method: String,
|
||||
data: Encodable? = nil
|
||||
) -> Future<ResultType, NetworkError> {
|
||||
return Future<ResultType, NetworkError> {[unowned self] promise in
|
||||
guard let url = URL(string: self.baseUrl + endPoint) else {
|
||||
promise(.failure(NetworkError.invalidUrl))
|
||||
return
|
||||
) -> AnyPublisher<ResultType, NetworkError> {
|
||||
|
||||
guard let url = URL(string: self.baseUrl + endPoint) else {
|
||||
return Future<ResultType, NetworkError> { promise in
|
||||
promise(.failure(.invalidUrl))
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
if (self.credentials != nil) {
|
||||
if let encodedCredentials = "\(self.credentials!.0):\(self.credentials!.1)"
|
||||
.data(using: String.Encoding.utf8)?.base64EncodedString() {
|
||||
request.addValue("Basic \(encodedCredentials)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
}
|
||||
request.httpBody = data?.toJSONData()
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpMethod = method
|
||||
|
||||
let task = URLSession.shared.dataTaskPublisher(for: request)
|
||||
.tryMap { (data, res) -> Data in
|
||||
guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else {
|
||||
switch (res as? HTTPURLResponse)?.statusCode {
|
||||
case 400: throw NetworkError.badRequest
|
||||
case 401, 403: throw NetworkError.unauthorized
|
||||
case 404: throw NetworkError.notFound
|
||||
default: throw NetworkError.unknown
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
if (self.credentials != nil) {
|
||||
if let encodedCredentials = "\(self.credentials!.0):\(self.credentials!.1)"
|
||||
.data(using: String.Encoding.utf8)?.base64EncodedString() {
|
||||
request.addValue("Basic \(encodedCredentials)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
}
|
||||
request.httpBody = data?.toJSONData()
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpMethod = method
|
||||
// TODO: Return this as well?
|
||||
URLSession.shared.dataTaskPublisher(for: request)
|
||||
.tryMap { (data, res) -> Data in
|
||||
guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else {
|
||||
switch (res as? HTTPURLResponse)?.statusCode {
|
||||
case 400: throw NetworkError.badRequest
|
||||
case 401, 403: throw NetworkError.unauthorized
|
||||
case 404: throw NetworkError.notFound
|
||||
default: throw NetworkError.unknown
|
||||
}
|
||||
}
|
||||
if let jsonResult = try? JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary {
|
||||
print(jsonResult)
|
||||
}
|
||||
return data
|
||||
}
|
||||
.decode(type: ResultType.self, decoder: JSONDecoder())
|
||||
.print()
|
||||
.receive(on: RunLoop.main)
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { data in
|
||||
promise(.success(data))
|
||||
})
|
||||
.store(in: &self.subscriptions)
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
.decode(type: ResultType.self, decoder: JSONDecoder())
|
||||
.mapError {
|
||||
return NetworkError.jsonParsingFailed($0)
|
||||
}
|
||||
return task.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,6 +141,7 @@ enum NetworkError: Error {
|
|||
case unauthorized
|
||||
case badRequest
|
||||
case invalidUrl
|
||||
case jsonParsingFailed(Error)
|
||||
}
|
||||
|
||||
extension Encodable {
|
||||
|
|
42
Budget/User/UserDataStore.swift
Normal file
42
Budget/User/UserDataStore.swift
Normal file
|
@ -0,0 +1,42 @@
|
|||
import Foundation
|
||||
import Combine
|
||||
|
||||
class UserDataStore: ObservableObject {
|
||||
|
||||
// Note: You can combine these into one Result type
|
||||
// Result<User, Status>
|
||||
var currentUser: User? = nil
|
||||
var status: UserStatus = .unauthenticated
|
||||
|
||||
func login(username: String, password: String) {
|
||||
|
||||
// Changes the status and notifies any observers of the change
|
||||
self.status = .authenticating
|
||||
self.objectWillChange.send()
|
||||
|
||||
// Perform the login
|
||||
_ = self.userRepository.login(username: username, password: password)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { (status) in
|
||||
switch status {
|
||||
case .finished:
|
||||
return
|
||||
// Do nothing it means the network request just ended
|
||||
case .failure(let networkError):
|
||||
// Poulate your status with failed authenticating
|
||||
self.status = .failedAuthentication
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}) { (user) in
|
||||
self.currentUser = user
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
init(_ userRepository: UserRepository) {
|
||||
self.userRepository = userRepository
|
||||
}
|
||||
|
||||
private let userRepository: UserRepository
|
||||
}
|
||||
|
|
@ -16,15 +16,15 @@ class UserRepository {
|
|||
self.apiService = apiService
|
||||
}
|
||||
|
||||
func getUser(id: Int) -> Future<User, NetworkError> {
|
||||
func getUser(id: Int) -> AnyPublisher<User, NetworkError> {
|
||||
return apiService.getUser(id: id)
|
||||
}
|
||||
|
||||
func searchUsers(withUsername: String) -> Future<[User], NetworkError> {
|
||||
func searchUsers(withUsername: String) -> AnyPublisher<[User], NetworkError> {
|
||||
return apiService.searchUsers(query: withUsername)
|
||||
}
|
||||
|
||||
func login(username: String, password: String) -> Future<User, NetworkError> {
|
||||
func login(username: String, password: String) -> AnyPublisher<User, NetworkError> {
|
||||
return apiService.login(username: username, password: password)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue