I am using PhotosPicker to let users select a photo. How do I retrieve the url of the selected photo?
I've tried printing out the imageSelection.itemIdentifier and got Optional("03966B05-1F51-4A20-801C-B617A2BC14DB/L0/001"), I don't know if this is related to a url path.
Here is my class using PhotosPicker, using sample code from WWDC2022.
class CreateViewModel: ObservableObject {
#Published var post: Post = Post() // Record being added to CloudKit
enum ImageState {
case empty, loading(Progress), success(Image), failure(Error)
}
#Published private(set) var imageState: ImageState = .empty
#Published var imageSelection: PhotosPickerItem? {
didSet {
if let imageSelection {
let progress = loadTransferable(from: imageSelection)
imageState = .loading(progress)
} else {
imageState = .empty
}
}
}
// Load asset data using transferable
private func loadTransferable(from imageSelection: PhotosPickerItem) -> Progress {
return imageSelection.loadTransferable(type: Image.self) { result in
DispatchQueue.main.async {
guard imageSelection == self.imageSelection else { return }
switch result {
case .success(let image?):
// Handle the success case with the image.
print("Image ID: \(imageSelection.itemIdentifier)") // Optional("03966B05-1F51-4A20-801C-B617A2BC14DB/L0/001")
self.imageState = .success(image)
case.success(nil):
// Handle the success case with an empty value.
self.imageState = .empty
case .failure(let error):
// Handle the failure case with the provided error.
print("Error image: \(error)")
self.imageState = .failure(error)
}
}
}
}
func createPost() async {
// 1. Create record object
let record = CKRecord(recordType: "Post")
// 2. Set record's fields
record.setValuesForKeys([
"title": post.title,
"caption": post.caption,
"likes": post.likes,
"size": post.keyboard.size.rawValue,
"keycaps": post.keyboard.keycaps,
"switches": post.keyboard.switches,
"case": post.keyboard.case,
"plate": post.keyboard.plate,
"foam": post.keyboard.foam
])
// Create CKAsset to store image onto CloudKit
let url = ...
record["image"] = CKAsset(fileURL: url) // Stuck! Don't know how to access url using PhotoPicker
// 3. Save to icloud (public database)
await savePost(record: record)
}
}
Related
I have a simple form with textfields and image picker, user can upload it to Firebase.
How to let #DocumentID = ... in Storage section?
Purpose: I need the image to be saved as a link in one document. At the moment the image is saved only in Storage. How to do it correctly?
All attempts led me to the fact that pic is created as a separate document in the database, and not inside same document.
Button: Done -[AddItemView]
#ObservedObject var viewModel = NewItemView()
func handleDoneTapped() {
self.viewModel.handleDoneTapped()
self.uploadImage()
self.dismiss()
}
Storage section -[AddItemView]
#ObservedObject var viewModel = NewItemView()
func uploadImage() {
let storage = Storage.storage().reference()
let picData: Data = pickedImages[0].jpegData(compressionQuality: 0.5)!
// let userId = Auth.auth().currentUser?.uid
let path = "itemImages/\(UUID().uuidString).jpg"
let ref = storage.child(path)
let metadata = StorageMetadata()
metadata.contentType = "image/jpg"
let uploadTask = ref.putData(picData, metadata: metadata, completion: { (storageMetaData, error) in
if error != nil {
print(error?.localizedDescription as Any)
return
}
})
}
All vars -[SingleItem]
struct SingleItem: Identifiable, Codable {
#DocumentID var id: String? // How I can use it in Storage section?
var title : String
var author : String
var description : String
#ServerTimestamp var createdTime: Timestamp?
var userId : String?
var pic : String
}
enum CodingKeys: String, CodingKey {
case id
case title
case author
case description = ""
case pic
}
Firestore -[NewItemView]
class NewItemView: ObservableObject {
// MARK: - Public properties
#Published var singleitem: SingleItem
#Published var modified = false
// MARK: - Internal properties
private var cancellables = Set<AnyCancellable>()
// MARK: - Constructors
init(singleitem: SingleItem = SingleItem(title: "", author: "", description: "", pic: "")) {
self.singleitem = singleitem
self.$singleitem
.dropFirst()
.sink { [weak self] singleitem in
self?.modified = true
}
.store(in: &self.cancellables)
}
// MARK: - Firestore
private var db = Firestore.firestore()
private func addItem(_ singleitem: SingleItem) {
do {
var addedItem = singleitem
addedItem.userId = Auth.auth().currentUser?.uid
_ = try db.collection("items").addDocument(from: addedItem)
}
catch {
print(error)
}
}
private func updateItem(_ singleitem: SingleItem) {
if let documentID = singleitem.id {
do {
try db.collection("items").document(documentID).setData(from: singleitem)
}
catch {
print(error)
}
}
}
private func updateOrAddItem() {
if singleitem.id != nil {
self.updateItem(self.singleitem)
}
else {
addItem(singleitem)
}
}
func handleDoneTapped() { // And this func I call in Done button
self.updateOrAddItem()
}
}
You created your path in uploadImage. You just need to update your object with that path that you statically created.
// You make the path to the image here
let path = "itemImages/\(UUID().uuidString).jpg"
You can do this multiple ways, but you need something like
func uploadImage() {
let storage = Storage.storage().reference()
let picData: Data = pickedImages[0].jpegData(compressionQuality: 0.5)!
let path = "itemImages/\(UUID().uuidString).jpg"
let ref = storage.child(path)
let metadata = StorageMetadata()
metadata.contentType = "image/jpg"
let uploadTask = ref.putData(picData, metadata: metadata, completion: { (storageMetaData, error) in
if error != nil {
print(error?.localizedDescription as Any)
return
} else {
// Upload succeeded with the path you added
// So set the pic variable to the path
self.viewModel.singleItem.pic = path
// Then update the item
self.viewModel.updateOrAddItem()
}
})
}
I have an app Im trying to build with sleep data for sleep apnea. I successfully setup CoreData with some help and its saving the first user input from the textfields and displaying it in the log from CoreData (changing the strings to Doubles was a challenge but success). However, whenever I put another 2nd input in, it saves and displays the first input again. It won't change. What am I doing wrong? A side note, it also is not creating a unique ID for each entry for some reason. Thank you guys....you'd been awesome so far helping as Im a newbie.
SleepModel.swift
struct SleepModel: Identifiable, Hashable {
var id = UUID().uuidString // ID variable
var hoursSlept: Double // Hours slept variable
var ahiReading: Double // AHI variable
}
Save function in AddEntryView.swift which is called when the button is pushed
private func saveSleepModel() {
guard let hoursSleptDouble = Double(hoursSleptString) else {
print("hours slept is invalid")
return
}
guard let ahiReadingDouble = Double(ahiReadingString) else {
print("ahi reading is invalid")
return
}
// Creates Sleep Model for user input & sends parameters
var sleepModel = SleepModel(hoursSlept: hoursSleptDouble, ahiReading: ahiReadingDouble)
coreDataViewModel.saveRecord(sleepModel: sleepModel) {
print("Success")
}
}
CoreDataViewModel
import SwiftUI
import CoreData
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedRecords: [SleepEntity] = []
init() {
container = NSPersistentContainer(name: "SleepContainer")
container.loadPersistentStores { description, error in
if let error = error {
print("Error loading contaienr - \(error.localizedDescription)")
} else {
print("Core data loaded successfully")
}
}
fetchRecords()
}
func saveRecord(sleepModel: SleepModel, onSuccess: #escaping() -> Void) {
let newRecord = SleepEntity(context: container.viewContext)
newRecord.hoursSlept = sleepModel.hoursSlept
newRecord.ahiReading = sleepModel.ahiReading
saveData(onSuccess: onSuccess)
}
func saveData(onSuccess: #escaping() -> Void) {
do {
try container.viewContext.save()
fetchRecords()
onSuccess()
} catch let error {
print("Error saving items: \(error)")
}
}
func fetchRecords() {
let request = NSFetchRequest<SleepEntity>(entityName: "SleepEntity")
request.sortDescriptors = [
NSSortDescriptor(keyPath: \SleepEntity.id, ascending: false)
]
do {
savedRecords = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching requests - \(error.localizedDescription)")
}
}
}
I want to get data from server and update my DB after that I'll show received data to the user. For this goal I have a method(getData()) in my view model that returns a Single I call and subscribe this method in the view controller(myVC.getData.subscribe({single in ...})) in this method at first I call and subscribe(#1)(getUnread()->Single) the method run but I can not get the single event, I can not understand why I can't get the event(#3) in call back(#4)
after that I want to save data with calling(#2)(save([Moddel])->single)
//I removed some part of this code it was to big
//This method is View Model
func getData() -> Single<[Model]> {
return Single<[Model]>.create {[weak self] single in
//#1
self!.restRepo.getUnread().subscribe({ [weak self] event in
//#4
switch event {
case .success(let response):
let models = response
//#2
self!.dbRepo.save(issues!).subscribe({ event in
switch event {
case .success(let response):
let models = response
single(.success(models))
case .error(let error):
single(.error(error))
}
}).disposed(by: self!.disposeBag)
case .error(let error):
single(.error(error))
}
}).disposed(by: self!.disposeBag)
return Disposables.create()
}
}
.
.
//I removed some part of this code it was to big
//This method is in RestRepo class
func getUnread() -> Single<[Model]> {
return Single<[Model]>.create { single in
let urlComponent = ApiHelper.instance.dolphinURLComponents(for: ApiHelper.ISSUES_PATH)
var urlRequest = URLRequest(url: urlComponent.url!)
ApiHelper.instance.alamofire.request(urlRequest).intercept().responseJSON { response in
debugPrint(response)
let statusCode = response.response?.statusCode
switch statusCode {
case 200:
do {
let models = try JSONDecoder().decode([Model].self, from: response.data!)
//#3
single(.success(models))
}catch{
print(error)
}
case 304:
debugPrint(response)
default:
single(.error(IssueResponseStatusCodeError(code: statusCode ?? 0)))
}
}
return Disposables.create()
}
First you need to change your thinking. You don't do anything in the app. At best, you lay out the Observable chains (which don't do anything anymore than water pipes "do" something.) Then you start the app and let the "water" flow.
So with that in mind, let's examine your question:
I want to get data from server...
It's not that "you" want to get the data. The request is made as a result of some action (probably a button tap) by the user or by some other side effect. What action is that? That needs to be expressed in the code. For the following I will assume it's a button tap. That means you should have:
class Example: UIViewController {
var button: UIButton!
var restRepo: RestRepo!
override func viewDidLoad() {
super.viewDidLoad()
let serverResponse = button.rx.tap
.flatMapLatest { [restRepo] in
restRepo!.getUnread()
.map { Result<[Model], Error>.success($0) }
.catchError { .just(Result<[Model], Error>.failure($0)) }
}
.share(replay: 1)
}
}
protocol RestRepo {
func getUnread() -> Observable<[Model]>
}
struct ProductionRestRepo: RestRepo {
func getUnread() -> Observable<[Model]> {
let urlComponent = ApiHelper.instance.dolphinURLComponents(for: ApiHelper.ISSUES_PATH)
let urlRequest = URLRequest(url: urlComponent.url!)
return URLSession.shared.rx.data(request: urlRequest)
.map { try JSONDecoder().decode([Model].self, from: $0) }
}
}
class ApiHelper {
static let ISSUES_PATH = ""
static let instance = ApiHelper()
func dolphinURLComponents(for: String) -> URLComponents { fatalError() }
}
struct Model: Decodable { }
The thing to notice here is that getUnread() is an effect that is caused by button.rx.tap. The above establishes a cause-effect chain.
Your question goes on to say "you" want to:
... update my DB...
Here, the cause is the network request and the effect is the DB save so we simply need to add this to the viewDidLoad (note that the code below uses RxEnumKit.):
let dbResponse = serverResponse
.capture(case: Result.success)
.flatMapLatest { [dbRepo] models in
dbRepo!.save(models)
.map { Result<Void, Error>.success(()) }
.catchError { .just(Result<Void, Error>.failure($0)) }
}
Your question also says that "you" want to:
... show received data to the user.
Note here that showing the received data to the user has nothing to do with the DB save. They are two independent operations that can be done in parallel.
Showing the received data to the user has the serverResponse as the cause, and the showing as the effect.
serverResponse
.capture(case: Result.success)
.subscribe(onNext: { models in
print("display the data to the user.", models)
})
.disposed(by: disposeBag)
Lastly, you don't mention it, but you also have to handle the errors:
So add this to the viewDidLoad as well:
Observable.merge(serverResponse.capture(case: Result.failure), dbResponse.capture(case: Result.failure))
.subscribe(onNext: { error in
print("an error occured:", error)
})
.disposed(by: disposeBag)
The code below is all of the above as a single block. This compiles fine...
import UIKit
import RxSwift
import RxCocoa
import EnumKit
import RxEnumKit
extension Result: CaseAccessible { }
class Example: UIViewController {
var button: UIButton!
var restRepo: RestRepo!
var dbRepo: DBRepo!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let serverResponse = button.rx.tap
.flatMapLatest { [restRepo] in
restRepo!.getUnread()
.map { Result<[Model], Error>.success($0) }
.catchError { .just(Result<[Model], Error>.failure($0)) }
}
.share(replay: 1)
let dbResponse = serverResponse
.capture(case: Result.success)
.flatMapLatest { [dbRepo] models in
dbRepo!.save(models)
.map { Result<Void, Error>.success(()) }
.catchError { .just(Result<Void, Error>.failure($0)) }
}
serverResponse
.capture(case: Result.success)
.subscribe(onNext: { models in
print("display the data to the user.", models)
})
.disposed(by: disposeBag)
Observable.merge(serverResponse.capture(case: Result.failure), dbResponse.capture(case: Result.failure))
.subscribe(onNext: { error in
print("an error occured:", error)
})
.disposed(by: disposeBag)
}
}
protocol RestRepo {
func getUnread() -> Observable<[Model]>
}
protocol DBRepo {
func save(_ models: [Model]) -> Observable<Void>
}
struct ProductionRestRepo: RestRepo {
func getUnread() -> Observable<[Model]> {
let urlComponent = ApiHelper.instance.dolphinURLComponents(for: ApiHelper.ISSUES_PATH)
let urlRequest = URLRequest(url: urlComponent.url!)
return URLSession.shared.rx.data(request: urlRequest)
.map { try JSONDecoder().decode([Model].self, from: $0) }
}
}
class ApiHelper {
static let ISSUES_PATH = ""
static let instance = ApiHelper()
func dolphinURLComponents(for: String) -> URLComponents { fatalError() }
}
struct Model: Decodable { }
I hope all this helps you, or at least generates more questions.
hello developer right now I learn mvvm. I want to populate the data in my viewModel and I create my service as a singleton. but somehow it didn't work, in my uilabel. instead my uilabel text is disappear, I didn't know if this my setup viewModel is wrong or not. should I separate the model, since I have two model object inside my view model. here I show you my code.
class ProfileViewModel {
private var infos: InfoResult?
private var cities = [City]()
private var religions = [Religion]()
private let services: BasicInfoServices
var profileID: String {
return infos?.id ?? ""
}
var imageURL: String {
let imageUrl = infos?.docAwsUrl ?? ""
return imageUrl
}
var fullName: String {
return infos?.fullName ?? ""
}
var phoneNumber: String {
return infos?.phoneNumber ?? ""
}
var email: String {
return infos?.email ?? ""
}
var cityOfBirth: String {
var cityName = ""
cities.forEach { city in
if infos?.pobId == city.id {
cityName = city.name ?? ""
}
}
return cityName
}
var dateOfBirth: String {
return infos?.dob ?? ""
}
var religion: String {
var religionName = ""
religions.forEach { religion in
if infos?.religionId == religion.id {
religionName = religion.name
}
}
return religionName
}
init(services: BasicInfoServices) {
self.services = services
populateProfile()
}
}
extension ProfileViewModel {
func populateProfile() {
// Basic Info
self.services.getBasicInfo { [weak self] result in
switch result {
case .success(let profile):
self?.infos = profile
case .failure(let error):
print(error)
}
}
// City
self.services.getCity { [weak self] result in
switch result {
case .success(let cities):
self?.cities = cities
case .failure(let error):
print(error)
}
}
// Religion
self.services.getReligion { [weak self] result in
switch result {
case .success(let religions):
self?.religions = religions
case .failure(let error):
print(error)
}
}
}
}
// I initialise it in viewController
var viewModel: ProfileViewModel!
var services = BasicInfoServices()
// than I test it in viewDidLoad
viewModel = ProfileViewModel(services: services)
profileLbl.text = viewModel.fullName // when set this my profileLbl place holder disappear.
profileImage.getUserImage(urlString: viewModel.imageURL)
1) At this line, you make API requests so this is an async process
viewModel = ProfileViewModel(services: services)
2) Without waiting for the success response, you try to use response data in next line
profileLbl.text = viewModel.fullName
Tips for you
1) You should use closures to detect API responses.
class ProfileViewModel {
var info: InfoResult?
private let services: BasicInfoServices
init(services: BasicInfoServices) {
self.services = services
}
func loadData(success: (()->()), failure: ((String)->())){
self.services.getBasicInfo { [weak self] result in
switch result {
case .success(let infoResult):
self?.info = infoResult
success()
case .failure(let error):
print(error)
failure(error.localizedDescription)
}
}
}
}
2) After receiving data, you can show this on the view.
class ViewController: UIViewController {
let viewModel = ProfileViewModel(services: BasicInfoServices())
#IBOutlet weak var lblProfileID:UILabel!
#IBOutlet weak var lblFullName:UILabel!
#IBOutlet weak var lblPhoneNumber:UILabel!
#IBOutlet weak var lblEmail:UILabel!
#IBOutlet weak var lblDataOfBirth:UILabel!
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.loadData(success: {
self.populateUserData()
}, failure: { errorString in
print(errorString)
})
}
func populateUserData(){
self.lblProfileID.text = self.viewModel.info?.id
self.lblFullName.text = self.viewModel.info?.fullName
self.lblPhoneNumber.text = self.viewModel.info?.phoneNumber
self.lblEmail.text = self.viewModel.info?.email
self.lblDataOfBirth.text = self.viewModel.info?.dob
}
}
I have an application where I want to make an API call once the screen is awaken in the ViewController. Basically, I am using Universal Link to activate the ViewCOntroller and when it displays the UIViewController, I want to make an API call based on the Data got. I am currently using the MVVM Architecture and I have added my code below
My ViewModel
class EmailVerificationViewModel: ViewModel, ViewModelType {
struct Input {
let editEmailTrigger: Driver<Void>
}
struct Output {
}
let routeManager: BehaviorRelay<RouteMatchResult?>
let currentEmail: BehaviorRelay<String?>
init(routeManager: RouteMatchResult?, provider: Api, currentEmail: String?) {
self.routeManager = BehaviorRelay(value: routeManager)
self.currentEmail = BehaviorRelay(value: currentEmail)
super.init(provider: provider)
}
func transform(input: Input) -> Output {
// THE CALL I WANT TO MAKE
routeManager.errorOnNil().asObservable()
.flatMapLatest { (code) -> Observable<RxSwift.Event<User>> in
log("=========++++++++++++==========")
// guard let code = code else {return}
let params = code.values
let challengeId = Int(params["xxx"] as? String ?? "0")
let login = LoginResponseModel(identifier: params["xxxx"] as? String, key: params["xxxxxx"] as? String, oth: params["xxxxx"] as? String, id: 0, challengeId: challengeId)
return self.provider.postVerifyApp(challengeId: login.challengeId!, oth: login.oth!, identifier: login.identifier!)
.trackActivity(self.loading)
.trackError(self.error)
.materialize()
}.subscribe(onNext: { [weak self] (event) in
switch event {
case .next(let token):
log(token)
AuthManager.setToken(token: token)
// self?.tokenSaved.onNext(())
case .error(let error):
log(error.localizedDescription)
default: break
}
}).disposed(by: rx.disposeBag)
return Output()
}
}
My Viewcontroller
override func bindViewModel() {
super.bindViewModel()
guard let viewModel = viewModel as? EmailVerificationViewModel else { return }
let input = EmailVerificationViewModel.Input(editEmailTrigger: editEmailBtn.rx.tap.asDriver())
let output = viewModel.transform(input: input)
viewModel.loading.asObservable().bind(to: isLoading).disposed(by: rx.disposeBag)
viewModel.parsedError.asObservable().bind(to: error).disposed(by: rx.disposeBag)
isLoading.asDriver().drive(onNext: { [weak self] (isLoading) in
isLoading ? self?.startAnimating() : self?.stopAnimating()
}).disposed(by: rx.disposeBag)
error.subscribe(onNext: { [weak self] (error) in
var title = ""
var description = ""
let image = R.image.icon_toast_warning()
switch error {
case .serverError(let response):
title = response.message ?? ""
}
self?.view.makeToast(description, title: title, image: image)
}).disposed(by: rx.disposeBag)
}
so how can I make the call on the commented like THE CALL I WANT TO MAKE once the application catches the universal link and loads up. Basically making an API call on viewDidLoad
The code in your sample was way more than is needed to answer the question. Here is how you make a network call on viewDidLoad:
class ViewController: UIViewController {
var viewModel: ViewModel!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let input = ViewModel.Input()
let output = viewModel.transform(input: input)
output.viewData
.bind(onNext: { viewData in
// setup the view with viewData
})
.disposed(by: disposeBag)
}
}
class ViewModel {
struct Input { }
struct Output {
let viewData: Observable<ViewData>
}
init(api: API) {
self.api = api
}
func transform(input: Input) -> Output {
let viewData = api.networkCall()
.map { ViewData(from: $0) }
return Output(viewData: viewData)
}
let api: API
}