Swift code examples and implementation patterns for our iOS development team
Our robust API client implementation that handles authentication, token refresh, and error handling.
import Foundation
import Combine
class RentManagerAPI: ObservableObject {
static let shared = RentManagerAPI()
private let baseURL = "https://rentmanager.io/api"
private let session = URLSession.shared
@Published var isAuthenticated = false
@Published var currentUser: User?
private init() {
// Check if user is already authenticated
if TokenManager.shared.getAccessToken() != nil {
isAuthenticated = true
loadCurrentUser()
}
}
// MARK: - Authentication
func authenticateWithApple(identityToken: String, user: [String: Any]?) async throws -> AuthResponse {
let endpoint = "/auth/apple"
let body: [String: Any] = [
"identityToken": identityToken,
"user": user ?? [:]
]
let response: AuthResponse = try await makeRequest(
endpoint: endpoint,
method: .POST,
body: body,
requiresAuth: false
)
if response.success {
TokenManager.shared.saveTokens(
accessToken: response.token,
refreshToken: response.refreshToken
)
DispatchQueue.main.async {
self.isAuthenticated = true
self.currentUser = response.user
}
}
return response
}
func logout() async {
do {
let _: LogoutResponse = try await makeRequest(
endpoint: "/auth/logout",
method: .GET
)
} catch {
print("Logout request failed: \(error)")
}
TokenManager.shared.clearTokens()
DispatchQueue.main.async {
self.isAuthenticated = false
self.currentUser = nil
}
}
// MARK: - Properties
func fetchProperties(page: Int = 1, limit: Int = 20) async throws -> PropertiesResponse {
let endpoint = "/properties?page=\(page)&limit=\(limit)"
return try await makeRequest(endpoint: endpoint, method: .GET)
}
func createProperty(_ property: PropertyRequest) async throws -> PropertyResponse {
return try await makeRequest(
endpoint: "/properties",
method: .POST,
body: property.dictionary
)
}
func updateProperty(id: Int, property: PropertyRequest) async throws -> PropertyResponse {
return try await makeRequest(
endpoint: "/properties/\(id)",
method: .PUT,
body: property.dictionary
)
}
func deleteProperty(id: Int) async throws {
let _: EmptyResponse = try await makeRequest(
endpoint: "/properties/\(id)",
method: .DELETE
)
}
// MARK: - Rentals
func fetchRentals() async throws -> RentalsResponse {
return try await makeRequest(endpoint: "/rentals", method: .GET)
}
func createAgreement(_ agreement: AgreementRequest) async throws -> AgreementResponse {
return try await makeRequest(
endpoint: "/agreements",
method: .POST,
body: agreement.dictionary
)
}
// MARK: - Finances
func fetchBalance() async throws -> BalanceResponse {
return try await makeRequest(endpoint: "/finances/balance", method: .GET)
}
func fetchTransactions(page: Int = 1) async throws -> TransactionsResponse {
let endpoint = "/finances/transactions?page=\(page)"
return try await makeRequest(endpoint: endpoint, method: .GET)
}
// MARK: - Private Methods
private func makeRequest(
endpoint: String,
method: HTTPMethod,
body: [String: Any]? = nil,
requiresAuth: Bool = true
) async throws -> T {
guard let url = URL(string: baseURL + endpoint) else {
throw APIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if requiresAuth, let token = TokenManager.shared.getAccessToken() {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
if let body = body {
request.httpBody = try JSONSerialization.data(withJSONObject: body)
}
do {
let (data, response) = try await session.data(for: request)
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200...299:
break
case 401:
if requiresAuth {
try await refreshTokenAndRetry()
return try await makeRequest(
endpoint: endpoint,
method: method,
body: body,
requiresAuth: requiresAuth
)
}
throw APIError.unauthorized
case 400...499:
throw APIError.clientError(httpResponse.statusCode)
case 500...599:
throw APIError.serverError(httpResponse.statusCode)
default:
throw APIError.unknown
}
}
return try JSONDecoder().decode(T.self, from: data)
} catch let error as APIError {
throw error
} catch {
throw APIError.networkError(error)
}
}
private func refreshTokenAndRetry() async throws {
guard let refreshToken = TokenManager.shared.getRefreshToken() else {
throw APIError.noRefreshToken
}
let refreshResponse: RefreshResponse = try await makeRequest(
endpoint: "/auth/refresh",
method: .POST,
body: ["refreshToken": refreshToken],
requiresAuth: false
)
TokenManager.shared.saveTokens(
accessToken: refreshResponse.token,
refreshToken: refreshToken
)
}
private func loadCurrentUser() {
Task {
do {
let user: User = try await makeRequest(endpoint: "/users/profile", method: .GET)
DispatchQueue.main.async {
self.currentUser = user
}
} catch {
print("Failed to load current user: \(error)")
}
}
}
}
// MARK: - HTTP Method Enum
enum HTTPMethod: String {
case GET = "GET"
case POST = "POST"
case PUT = "PUT"
case DELETE = "DELETE"
case PATCH = "PATCH"
}
// MARK: - API Error Enum
enum APIError: LocalizedError {
case invalidURL
case unauthorized
case noRefreshToken
case clientError(Int)
case serverError(Int)
case networkError(Error)
case unknown
var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid URL"
case .unauthorized:
return "Unauthorized access"
case .noRefreshToken:
return "No refresh token available"
case .clientError(let code):
return "Client error: \(code)"
case .serverError(let code):
return "Server error: \(code)"
case .networkError(let error):
return "Network error: \(error.localizedDescription)"
case .unknown:
return "Unknown error occurred"
}
}
}
Our property management implementation using MVVM pattern for the iOS application.
import Foundation
import Combine
@MainActor
class PropertyViewModel: ObservableObject {
@Published var properties: [Property] = []
@Published var isLoading = false
@Published var errorMessage: String?
@Published var showingCreateProperty = false
private let api = RentManagerAPI.shared
private var cancellables = Set()
init() {
loadProperties()
}
func loadProperties() {
isLoading = true
errorMessage = nil
Task {
do {
let response = try await api.fetchProperties()
self.properties = response.properties
} catch {
self.errorMessage = error.localizedDescription
}
self.isLoading = false
}
}
func createProperty(_ propertyRequest: PropertyRequest) {
Task {
do {
let response = try await api.createProperty(propertyRequest)
self.properties.insert(response.property, at: 0)
self.showingCreateProperty = false
} catch {
self.errorMessage = error.localizedDescription
}
}
}
func updateProperty(_ property: Property, with updates: PropertyRequest) {
Task {
do {
let response = try await api.updateProperty(id: property.id, property: updates)
if let index = properties.firstIndex(where: { $0.id == property.id }) {
properties[index] = response.property
}
} catch {
self.errorMessage = error.localizedDescription
}
}
}
func deleteProperty(_ property: Property) {
Task {
do {
try await api.deleteProperty(id: property.id)
self.properties.removeAll { $0.id == property.id }
} catch {
self.errorMessage = error.localizedDescription
}
}
}
func toggleAvailability(for property: Property) {
let updates = PropertyRequest(
type: property.type,
country: property.country,
address: property.address,
bedrooms: property.bedrooms,
bathrooms: property.bathrooms,
area: property.area,
rentPrice: property.rentPrice,
isAvailable: !property.isAvailable
)
updateProperty(property, with: updates)
}
}
import SwiftUI
struct PropertyListView: View {
@StateObject private var viewModel = PropertyViewModel()
@State private var showingCreateProperty = false
var body: some View {
NavigationView {
ZStack {
if viewModel.isLoading {
ProgressView("Loading properties...")
} else if viewModel.properties.isEmpty {
EmptyStateView(
icon: "house",
title: "No Properties",
message: "Add your first property to get started",
buttonTitle: "Add Property"
) {
showingCreateProperty = true
}
} else {
List {
ForEach(viewModel.properties) { property in
PropertyRowView(property: property) { action in
handlePropertyAction(action, for: property)
}
}
.onDelete(perform: deleteProperties)
}
.refreshable {
viewModel.loadProperties()
}
}
}
.navigationTitle("Properties")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add") {
showingCreateProperty = true
}
}
}
.sheet(isPresented: $showingCreateProperty) {
CreatePropertyView { propertyRequest in
viewModel.createProperty(propertyRequest)
}
}
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.errorMessage = nil
}
} message: {
Text(viewModel.errorMessage ?? "")
}
}
}
private func deleteProperties(offsets: IndexSet) {
for index in offsets {
let property = viewModel.properties[index]
viewModel.deleteProperty(property)
}
}
private func handlePropertyAction(_ action: PropertyAction, for property: Property) {
switch action {
case .toggleAvailability:
viewModel.toggleAvailability(for: property)
case .edit:
// Handle edit action
break
case .viewDetails:
// Handle view details action
break
}
}
}
enum PropertyAction {
case toggleAvailability
case edit
case viewDetails
}
import SwiftUI
struct PropertyRowView: View {
let property: Property
let onAction: (PropertyAction) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(property.address)
.font(.headline)
.lineLimit(2)
if let type = property.type {
Text(type.capitalized)
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
VStack(alignment: .trailing, spacing: 4) {
if let rentPrice = property.rentPrice {
Text("$\(rentPrice, specifier: "%.0f")")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.primary)
}
StatusBadge(isAvailable: property.isAvailable)
}
}
HStack {
if let bedrooms = property.bedrooms, bedrooms > 0 {
PropertyFeature(icon: "bed.double", text: "\(bedrooms) bed")
}
if let bathrooms = property.bathrooms, bathrooms > 0 {
PropertyFeature(icon: "bathtub", text: "\(bathrooms) bath")
}
if let area = property.area, area > 0 {
PropertyFeature(
icon: "ruler",
text: "\(Int(area)) \(property.areaUnit ?? "m²")"
)
}
Spacer()
Menu {
Button("Toggle Availability") {
onAction(.toggleAvailability)
}
Button("Edit") {
onAction(.edit)
}
Button("View Details") {
onAction(.viewDetails)
}
} label: {
Image(systemName: "ellipsis")
.foregroundColor(.secondary)
}
}
}
.padding(.vertical, 4)
}
}
struct PropertyFeature: View {
let icon: String
let text: String
var body: some View {
HStack(spacing: 4) {
Image(systemName: icon)
.font(.caption)
.foregroundColor(.secondary)
Text(text)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
struct StatusBadge: View {
let isAvailable: Bool
var body: some View {
Text(isAvailable ? "Available" : "Occupied")
.font(.caption)
.fontWeight(.medium)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(isAvailable ? Color.green.opacity(0.2) : Color.red.opacity(0.2))
.foregroundColor(isAvailable ? .green : .red)
.cornerRadius(12)
}
}
SwiftUI views implementation for our property creation and management features.
import SwiftUI
struct CreatePropertyView: View {
@Environment(\.presentationMode) var presentationMode
let onSave: (PropertyRequest) -> Void
@State private var type = ""
@State private var country = ""
@State private var address = ""
@State private var bedrooms = 1
@State private var bathrooms = 1
@State private var area: Double = 0
@State private var areaUnit = "m2"
@State private var rentPrice: Double = 0
@State private var rentCurrency = "USD"
@State private var isAvailable = true
@State private var furnished = "no"
private let propertyTypes = ["apartment", "house", "office", "studio", "villa"]
private let areaUnits = ["m2", "ft2"]
private let currencies = ["USD", "EUR", "GBP"]
private let furnishedOptions = ["yes", "no", "partial"]
var body: some View {
NavigationView {
Form {
Section(header: Text("Basic Information")) {
Picker("Property Type", selection: $type) {
Text("Select Type").tag("")
ForEach(propertyTypes, id: \.self) { type in
Text(type.capitalized).tag(type)
}
}
TextField("Country Code (e.g., US)", text: $country)
.textInputAutocapitalization(.characters)
.autocorrectionDisabled()
TextField("Full Address", text: $address, axis: .vertical)
.lineLimit(2...4)
}
Section(header: Text("Property Details")) {
Stepper("Bedrooms: \(bedrooms)", value: $bedrooms, in: 0...10)
Stepper("Bathrooms: \(bathrooms)", value: $bathrooms, in: 0...10)
HStack {
TextField("Area", value: $area, format: .number)
.keyboardType(.decimalPad)
Picker("Unit", selection: $areaUnit) {
ForEach(areaUnits, id: \.self) { unit in
Text(unit).tag(unit)
}
}
.pickerStyle(.segmented)
.frame(width: 100)
}
Picker("Furnished", selection: $furnished) {
ForEach(furnishedOptions, id: \.self) { option in
Text(option.capitalized).tag(option)
}
}
}
Section(header: Text("Rental Information")) {
HStack {
TextField("Rent Price", value: $rentPrice, format: .number)
.keyboardType(.decimalPad)
Picker("Currency", selection: $rentCurrency) {
ForEach(currencies, id: \.self) { currency in
Text(currency).tag(currency)
}
}
.pickerStyle(.menu)
.frame(width: 80)
}
Toggle("Available for Rent", isOn: $isAvailable)
}
}
.navigationTitle("New Property")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
presentationMode.wrappedValue.dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
saveProperty()
}
.disabled(!isValidProperty)
}
}
}
}
private var isValidProperty: Bool {
!country.isEmpty && !address.isEmpty
}
private func saveProperty() {
let propertyRequest = PropertyRequest(
type: type.isEmpty ? nil : type,
country: country,
address: address,
bedrooms: bedrooms,
bathrooms: bathrooms,
area: area > 0 ? area : nil,
areaUnit: areaUnit,
rentPrice: rentPrice > 0 ? rentPrice : nil,
rentCurrency: rentCurrency,
isAvailable: isAvailable,
furnished: furnished
)
onSave(propertyRequest)
presentationMode.wrappedValue.dismiss()
}
}
import SwiftUI
struct EmptyStateView: View {
let icon: String
let title: String
let message: String
let buttonTitle: String?
let action: (() -> Void)?
init(
icon: String,
title: String,
message: String,
buttonTitle: String? = nil,
action: (() -> Void)? = nil
) {
self.icon = icon
self.title = title
self.message = message
self.buttonTitle = buttonTitle
self.action = action
}
var body: some View {
VStack(spacing: 24) {
Image(systemName: icon)
.font(.system(size: 64))
.foregroundColor(.gray.opacity(0.5))
VStack(spacing: 8) {
Text(title)
.font(.title2)
.fontWeight(.semibold)
.foregroundColor(.primary)
Text(message)
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
if let buttonTitle = buttonTitle, let action = action {
Button(action: action) {
Text(buttonTitle)
.font(.headline)
.foregroundColor(.white)
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(Color.blue)
.cornerRadius(8)
}
}
}
.padding()
}
}
Data models for our API integration with proper Codable implementation.
import Foundation
// MARK: - Property Model
struct Property: Codable, Identifiable {
let id: Int
let type: String?
let country: String
let address: String
let bedrooms: Int?
let bathrooms: Int?
let area: Double?
let areaUnit: String?
let rentPrice: Double?
let rentCurrency: String?
let isAvailable: Bool
let furnished: String?
let createdAt: Date
let updatedAt: Date
enum CodingKeys: String, CodingKey {
case id, type, country, address, bedrooms, bathrooms, area
case areaUnit = "area_unit"
case rentPrice = "rent_price"
case rentCurrency = "rent_currency"
case isAvailable = "is_available"
case furnished
case createdAt = "created_at"
case updatedAt = "updated_at"
}
}
// MARK: - Property Request Model
struct PropertyRequest: Codable {
let type: String?
let country: String
let address: String
let bedrooms: Int?
let bathrooms: Int?
let area: Double?
let areaUnit: String?
let rentPrice: Double?
let rentCurrency: String?
let isAvailable: Bool
let furnished: String?
enum CodingKeys: String, CodingKey {
case type, country, address, bedrooms, bathrooms, area
case areaUnit = "area_unit"
case rentPrice = "rent_price"
case rentCurrency = "rent_currency"
case isAvailable = "is_available"
case furnished
}
var dictionary: [String: Any] {
var dict: [String: Any] = [
"country": country,
"address": address,
"is_available": isAvailable
]
if let type = type { dict["type"] = type }
if let bedrooms = bedrooms { dict["bedrooms"] = bedrooms }
if let bathrooms = bathrooms { dict["bathrooms"] = bathrooms }
if let area = area { dict["area"] = area }
if let areaUnit = areaUnit { dict["area_unit"] = areaUnit }
if let rentPrice = rentPrice { dict["rent_price"] = rentPrice }
if let rentCurrency = rentCurrency { dict["rent_currency"] = rentCurrency }
if let furnished = furnished { dict["furnished"] = furnished }
return dict
}
}
// MARK: - API Response Models
struct PropertiesResponse: Codable {
let properties: [Property]
let pagination: PaginationInfo
}
struct PropertyResponse: Codable {
let id: Int
let message: String
let property: Property
}
struct PaginationInfo: Codable {
let currentPage: Int
let totalPages: Int
let totalItems: Int
let itemsPerPage: Int
enum CodingKeys: String, CodingKey {
case currentPage = "current_page"
case totalPages = "total_pages"
case totalItems = "total_items"
case itemsPerPage = "items_per_page"
}
}
// MARK: - User Model
struct User: Codable, Identifiable {
let id: Int
let email: String
let fullName: String
let role: String
let accessLevel: String
let createdAt: Date
enum CodingKeys: String, CodingKey {
case id, email, role
case fullName = "full_name"
case accessLevel = "access_level"
case createdAt = "created_at"
}
}
// MARK: - Authentication Models
struct AuthResponse: Codable {
let success: Bool
let token: String
let refreshToken: String
let isNewUser: Bool
let user: User
enum CodingKeys: String, CodingKey {
case success, token, user
case refreshToken = "refreshToken"
case isNewUser = "isNewUser"
}
}
struct RefreshResponse: Codable {
let token: String
}
struct LogoutResponse: Codable {
let message: String
}
// MARK: - Financial Models
struct Balance: Codable {
let amount: Double
let currency: String
let lastUpdated: Date
enum CodingKeys: String, CodingKey {
case amount, currency
case lastUpdated = "last_updated"
}
}
struct BalanceResponse: Codable {
let balance: Balance
}
struct Transaction: Codable, Identifiable {
let id: Int
let type: String
let amount: Double
let currency: String
let description: String
let createdAt: Date
enum CodingKeys: String, CodingKey {
case id, type, amount, currency, description
case createdAt = "created_at"
}
}
struct TransactionsResponse: Codable {
let transactions: [Transaction]
let pagination: PaginationInfo
}
// MARK: - Rental Models
struct Rental: Codable, Identifiable {
let id: Int
let propertyId: Int
let tenantName: String
let startDate: Date
let endDate: Date?
let monthlyRent: Double
let status: String
enum CodingKeys: String, CodingKey {
case id, status
case propertyId = "property_id"
case tenantName = "tenant_name"
case startDate = "start_date"
case endDate = "end_date"
case monthlyRent = "monthly_rent"
}
}
struct RentalsResponse: Codable {
let rentals: [Rental]
}
// MARK: - Agreement Models
struct AgreementRequest: Codable {
let propertyId: Int
let tenantFullName: String
let tenantPassportNumber: String
let tenantCountry: String
let startDate: String
let endDate: String
let monthlyRent: Double
enum CodingKeys: String, CodingKey {
case propertyId = "property_id"
case tenantFullName = "tenant_full_name"
case tenantPassportNumber = "tenant_passport_number"
case tenantCountry = "tenant_country"
case startDate = "start_date"
case endDate = "end_date"
case monthlyRent = "monthly_rent"
}
var dictionary: [String: Any] {
return [
"property_id": propertyId,
"tenant_full_name": tenantFullName,
"tenant_passport_number": tenantPassportNumber,
"tenant_country": tenantCountry,
"start_date": startDate,
"end_date": endDate,
"monthly_rent": monthlyRent
]
}
}
struct Agreement: Codable, Identifiable {
let id: Int
let propertyId: Int
let tenantFullName: String
let startDate: Date
let endDate: Date
let monthlyRent: Double
let status: String
enum CodingKeys: String, CodingKey {
case id, status
case propertyId = "property_id"
case tenantFullName = "tenant_full_name"
case startDate = "start_date"
case endDate = "end_date"
case monthlyRent = "monthly_rent"
}
}
struct AgreementResponse: Codable {
let id: Int
let message: String
let agreement: Agreement
}
// MARK: - Empty Response
struct EmptyResponse: Codable {
// Used for endpoints that don't return data
}
Error handling implementation for our iOS application development.
import Foundation
import SwiftUI
class ErrorManager: ObservableObject {
@Published var currentError: ErrorInfo?
@Published var showingError = false
func handle(_ error: Error) {
DispatchQueue.main.async {
self.currentError = ErrorInfo(from: error)
self.showingError = true
}
}
func clearError() {
currentError = nil
showingError = false
}
}
struct ErrorInfo {
let title: String
let message: String
let isRetryable: Bool
init(from error: Error) {
switch error {
case let apiError as APIError:
self.init(from: apiError)
case let urlError as URLError:
self.init(from: urlError)
default:
title = "Unknown Error"
message = error.localizedDescription
isRetryable = true
}
}
private init(from apiError: APIError) {
switch apiError {
case .invalidURL:
title = "Invalid URL"
message = "The request URL is invalid. Please try again."
isRetryable = false
case .unauthorized:
title = "Authentication Required"
message = "Please sign in again to continue."
isRetryable = false
case .noRefreshToken:
title = "Session Expired"
message = "Your session has expired. Please sign in again."
isRetryable = false
case .clientError(let code):
title = "Request Error"
message = getClientErrorMessage(for: code)
isRetryable = code != 404
case .serverError(let code):
title = "Server Error"
message = "The server is experiencing issues (\(code)). Please try again later."
isRetryable = true
case .networkError(let underlyingError):
title = "Network Error"
message = "Please check your internet connection and try again."
isRetryable = true
case .unknown:
title = "Unknown Error"
message = "An unexpected error occurred. Please try again."
isRetryable = true
}
}
private init(from urlError: URLError) {
title = "Connection Error"
switch urlError.code {
case .notConnectedToInternet:
message = "No internet connection available."
case .timedOut:
message = "The request timed out. Please try again."
case .cannotConnectToHost:
message = "Cannot connect to the server. Please try again later."
default:
message = "A network error occurred. Please check your connection."
}
isRetryable = true
}
private func getClientErrorMessage(for code: Int) -> String {
switch code {
case 400:
return "The request was invalid. Please check your input."
case 401:
return "Authentication failed. Please sign in again."
case 403:
return "You don't have permission to perform this action."
case 404:
return "The requested resource was not found."
case 409:
return "This action conflicts with existing data."
case 422:
return "The provided data is invalid."
default:
return "A client error occurred (\(code))."
}
}
}
// MARK: - Error Alert View
struct ErrorAlert: ViewModifier {
@ObservedObject var errorManager: ErrorManager
let retryAction: (() -> Void)?
func body(content: Content) -> some View {
content
.alert("Error", isPresented: $errorManager.showingError) {
Button("OK") {
errorManager.clearError()
}
if let error = errorManager.currentError,
error.isRetryable,
let retryAction = retryAction {
Button("Retry") {
errorManager.clearError()
retryAction()
}
}
} message: {
if let error = errorManager.currentError {
Text(error.message)
}
}
}
}
extension View {
func errorAlert(
errorManager: ErrorManager,
retryAction: (() -> Void)? = nil
) -> some View {
modifier(ErrorAlert(errorManager: errorManager, retryAction: retryAction))
}
}
Swift concurrency implementation using async/await patterns in our codebase.
import Foundation
actor PropertyService {
private let api = RentManagerAPI.shared
private var cachedProperties: [Property] = []
private var lastFetchTime: Date?
func getProperties(forceRefresh: Bool = false) async throws -> [Property] {
if !forceRefresh,
let lastFetch = lastFetchTime,
Date().timeIntervalSince(lastFetch) < 300, // 5 minutes cache
!cachedProperties.isEmpty {
return cachedProperties
}
let response = try await api.fetchProperties()
cachedProperties = response.properties
lastFetchTime = Date()
return cachedProperties
}
func createProperty(_ request: PropertyRequest) async throws -> Property {
let response = try await api.createProperty(request)
cachedProperties.insert(response.property, at: 0)
return response.property
}
func updateProperty(id: Int, with request: PropertyRequest) async throws -> Property {
let response = try await api.updateProperty(id: id, property: request)
if let index = cachedProperties.firstIndex(where: { $0.id == id }) {
cachedProperties[index] = response.property
}
return response.property
}
func deleteProperty(id: Int) async throws {
try await api.deleteProperty(id: id)
cachedProperties.removeAll { $0.id == id }
}
}
// MARK: - Usage Example in ViewModel
@MainActor
class ModernPropertyViewModel: ObservableObject {
@Published var properties: [Property] = []
@Published var isLoading = false
@Published var error: Error?
private let propertyService = PropertyService()
func loadProperties(forceRefresh: Bool = false) async {
isLoading = true
error = nil
do {
properties = try await propertyService.getProperties(forceRefresh: forceRefresh)
} catch {
self.error = error
}
isLoading = false
}
func createProperty(_ request: PropertyRequest) async {
do {
let newProperty = try await propertyService.createProperty(request)
properties.insert(newProperty, at: 0)
} catch {
self.error = error
}
}
func updateProperty(id: Int, with request: PropertyRequest) async {
do {
let updatedProperty = try await propertyService.updateProperty(id: id, with: request)
if let index = properties.firstIndex(where: { $0.id == id }) {
properties[index] = updatedProperty
}
} catch {
self.error = error
}
}
func deleteProperty(id: Int) async {
do {
try await propertyService.deleteProperty(id: id)
properties.removeAll { $0.id == id }
} catch {
self.error = error
}
}
}
struct ModernPropertyListView: View {
@StateObject private var viewModel = ModernPropertyViewModel()
@StateObject private var errorManager = ErrorManager()
var body: some View {
NavigationView {
List {
ForEach(viewModel.properties) { property in
PropertyRowView(property: property) { action in
Task {
await handlePropertyAction(action, for: property)
}
}
}
.onDelete { indexSet in
Task {
await deleteProperties(at: indexSet)
}
}
}
.navigationTitle("Properties")
.refreshable {
await viewModel.loadProperties(forceRefresh: true)
}
.task {
await viewModel.loadProperties()
}
.onChange(of: viewModel.error) { error in
if let error = error {
errorManager.handle(error)
}
}
.errorAlert(errorManager: errorManager) {
Task {
await viewModel.loadProperties(forceRefresh: true)
}
}
}
}
private func handlePropertyAction(_ action: PropertyAction, for property: Property) async {
switch action {
case .toggleAvailability:
let request = PropertyRequest(
type: property.type,
country: property.country,
address: property.address,
bedrooms: property.bedrooms,
bathrooms: property.bathrooms,
area: property.area,
areaUnit: property.areaUnit,
rentPrice: property.rentPrice,
rentCurrency: property.rentCurrency,
isAvailable: !property.isAvailable,
furnished: property.furnished
)
await viewModel.updateProperty(id: property.id, with: request)
case .edit:
// Handle edit
break
case .viewDetails:
// Handle view details
break
}
}
private func deleteProperties(at indexSet: IndexSet) async {
for index in indexSet {
let property = viewModel.properties[index]
await viewModel.deleteProperty(id: property.id)
}
}
}