WKWebView-fallback fΓΆr
avancerade webbvyer under beta-perioden.
// QuiXzoom iOS β Projektstruktur QuiXzoom/ βββ App/ β βββ QuiXzoomApp.swift // @main β βββ AppDelegate.swift // APNs + deep links βββ Features/ β βββ Auth/ β β βββ BankIDView.swift β β βββ AuthViewModel.swift β βββ Missions/ β β βββ MissionFeedView.swift β β βββ MissionMapView.swift // MapKit β β βββ ActiveMissionView.swift β βββ Camera/ β β βββ CameraUploadView.swift β βββ Wallet/ β β βββ WalletView.swift β βββ Profile/ β βββ ProfileView.swift βββ Services/ β βββ APIClient.swift // REST + JWT β βββ LocationService.swift β βββ PushService.swift βββ DesignSystem/ βββ Tokens.swift // iOS 18 native tokens
import Foundation import Security @MainActor class AuthViewModel: ObservableObject { @Published var isAuthenticated = false @Published var isLoading = false private let keychainKey = "qz.jwt" private let baseURL = "https://amos.wavult.com" // 1. Starta BankID-flΓΆde func initiateBankID(personnummer: String) async throws { isLoading = true defer { isLoading = false } let response = try await APIClient.post( "/api/auth/bankid/init", body: ["personnummer": personnummer, "role": "zoomer"] ) // Γppna BankID-appen if let url = response["autoStartToken"] as? String { UIApplication.shared.open( URL(string: "bankid:///?autostarttoken=\(url)")! ) } } // 2. Polls tills BankID bekrΓ€ftar (max 30s) func collectBankID(orderRef: String) async throws -> String { for _ in 0..30 { try await Task.sleep(nanoseconds: 1_000_000_000) let res = try await APIClient.post( "/api/auth/bankid/collect", body: ["orderRef": orderRef] ) if res["status"] as? String == "complete" { return res["jwt"] as! String } } throw AuthError.timeout } // 3. Spara JWT i Keychain func saveToken(_ jwt: String) { let data = jwt.data(using: .utf8)! let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrAccount: keychainKey, kSecValueData: data, kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly ] SecItemDelete(query as CFDictionary) SecItemAdd(query as CFDictionary, nil) isAuthenticated = true } }
https://amos.wavult.com/api/quixzoom/.
JWT skickas som Authorization: Bearer <token>.
struct APIClient { static let base = "https://amos.wavult.com/api/quixzoom" static func get<T: Decodable>( _ path: String, type: T.Type ) async throws -> T { var req = URLRequest(url: URL(string: base + path)!) req.setValue("Bearer \(Keychain.get("qz.jwt"))", forHTTPHeaderField: "Authorization") let (data, _) = try await URLSession.shared.data(for: req) return try JSONDecoder().decode(T.self, from: data) } // Upload mission with photos static func submitMission( id: Int, images: [UIImage] ) async throws -> MissionResult { var req = URLRequest(url: URL(string: "\(base)/missions/\(id)/submit")!) req.httpMethod = "POST" let boundary = UUID().uuidString req.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") req.setValue("Bearer \(Keychain.get("qz.jwt"))", forHTTPHeaderField: "Authorization") var body = Data() images.forEach { img in body.append("--\(boundary)\r\n".data(using: .utf8)!) body.append("Content-Disposition: form-data; name=\"images\"; filename=\"photo.jpg\"\r\n".data(using: .utf8)!) body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!) body.append(img.jpegData(compressionQuality: 0.85)!) body.append("\r\n".data(using: .utf8)!) } body.append("--\(boundary)--\r\n".data(using: .utf8)!) req.httpBody = body let (data, _) = try await URLSession.shared.data(for: req) return try JSONDecoder().decode(MissionResult.self, from: data) } }
func application( _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data ) { let token = deviceToken.map { String(format: "%02x", $0) }.joined() Task { try? await APIClient.post("/me/device-token", body: ["token": token, "platform": "apns"]) } } // Payload-format frΓ₯n servern: // { // "aps": { "alert": { "title": "Nytt uppdrag!", "body": "350 kr Β· Storgatan 12" }, // "badge": 1, "sound": "default" }, // "type": "new_mission", // "missionId": 42 // } func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler: @escaping () -> Void ) { let info = response.notification.request.content.userInfo if let missionId = info["missionId"] as? Int { // Navigate to mission detail NavigationRouter.shared.navigate(to: .mission(id: missionId)) } withCompletionHandler() }
NSCameraUsageDescription
och NSLocationWhenInUseUsageDescription i Info.plist med
motiveringar pΓ₯ svenska (krΓ€vs fΓΆr App Store Review).
import SwiftUI import PhotosUI struct CameraUploadView: View { let missionId: Int @State private var selectedItems: [PhotosPickerItem] = [] @State private var capturedImages: [UIImage] = [] @State private var isUploading = false var body: some View { VStack { PhotosPicker(selection: $selectedItems, maxSelectionCount: 10, matching: .images) { Label("Ta / vΓ€lj foton", systemImage: "camera.fill") .frame(maxWidth: .infinity) .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(12) } if !capturedImages.isEmpty { Button("Skicka in \(capturedImages.count) bilder") { Task { try await upload() } } .disabled(isUploading) } } .onChange(of: selectedItems) { _, items in Task { await loadImages(items) } } } private func upload() async throws { isUploading = true let _ = try await APIClient.submitMission( id: missionId, images: capturedImages ) isUploading = false } }
location
i Background Modes capability fΓΆr spΓ₯rning under aktivt uppdrag. Motivera i Privacy Manifest.
import CoreLocation import MapKit @MainActor class LocationService: NSObject, ObservableObject, CLLocationManagerDelegate { @Published var userLocation: CLLocation? @Published var distanceToMission: Double = 0 private let manager = CLLocationManager() func startTracking(mission: Mission) { manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyBest manager.distanceFilter = 10 // update every 10m manager.allowsBackgroundLocationUpdates = true manager.startUpdatingLocation() updateDistance(to: mission) } func stopTracking() { manager.stopUpdatingLocation() manager.allowsBackgroundLocationUpdates = false } nonisolated func locationManager( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { Task { @MainActor in self.userLocation = locations.last } } }
quixzoom://
anvΓ€nds fΓΆr push-navigation och delning av uppdrag.
/.well-known/apple-app-site-association
mΓ₯ste publiceras pΓ₯ amos.wavult.com med Team ID + Bundle ID fΓΆr Universal Links.