Sorry for beginner question, I am trying to transition from UIKit to SwiftUI. #State variable's didSet does not get triggered like it does in UIKit.
I have KFImage that loads an image from a user's photoUrl and I want it when tapped, launches image picker, then update the userUIImage but since KFImage needs a url to be updated, I am not sure how I can call my updateUserImage() to update the photoUrl to update KFImage.
Here' my code below
struct ProfileView: View {
//MARK: Properties
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
#State private var isImageLoaded: Bool = false
#State private var showImagePicker = false
#State private var photoUrl: URL? = URL(string: (Customer.current?.photoUrl)!)
#State private var userImage: Image? = Image(uiImage: UIImage())
#State private var userUIImage: UIImage? = UIImage() {
didSet {
if isImageLoaded {
updateUserImage()
}
}
}
var body: some View {
KFImage(photoUrl)
.resizable()
.onSuccess { result in
self.userUIImage = result.image
self.isImageLoaded = true
}
.aspectRatio(contentMode: .fill)
.frame(width: screenWidth / 2.5)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
.onTapGesture { self.showImagePicker = true }
.sheet(isPresented: $showImagePicker) {
CustomImagePickerView(sourceType: .photoLibrary, image: $userImage, uiImage: $userUIImage, isPresented: $showImagePicker)
}
}
//MARK: Methods
func updateUserImage() {
CustomerService.updateUserImage(image: userUIImage!) { (photoUrl, error) in
if let error = error {
handleError(errorBody: error)
return
}
guard var user = Customer.current else { return }
self.photoUrl = photoUrl
user.photoUrl = photoUrl?.absoluteString
CustomerService.updateUserDatabase(user: user) { (error) in
if let error = error {
handleError(errorBody: error)
return
}
Customer.setCurrent(user, writeToUserDefaults: true)
}
}
}
}
Try using onChange / onReceive instead:
KFImage(photoUrl)
.resizable()
// ...
// remove .onSuccess implementation as self.userUIImage = result.image will result in an infinite loop
.onChange(of: userUIImage) { newImage in
updateUserImage()
}
or
import Combine
...
KFImage(photoUrl)
.resizable()
// ...
.onReceive(Just(userUIImage)) { newImage in
updateUserImage()
}
Related
I'm trying to play with Apple's RealityKit and some usdz files I got from their website.
I made a small app where I can load each 3D model at a time.
It works but with one issue. If I add a single 3D object is fine but when I load the new models they all get stuck upon each other. I can't seem to figure out how to remove the old 3D object before adding a different one.
Here is my code
My Model object
import Foundation
import UIKit
import RealityKit
import Combine
class Model {
var modelName: String
var image: UIImage
var modelEntity: ModelEntity?
private var cancellable: AnyCancellable? = nil
var message: String = ""
init(modelName: String){
self.modelName = modelName
self.image = UIImage(named: modelName)!
let filename = modelName + ".usdz"
self.cancellable = ModelEntity.loadModelAsync(named: filename)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Model.swift -> DEBUG: Succesfully loaded \(self.modelName)")
break
case .failure(let error):
print("Model.swift -> DEBUG: Unable to load : \(self.modelName)")
self.message = error.localizedDescription
}
}, receiveValue: { modelEntity in
//get model entity
self.modelEntity = modelEntity
})
}
}
RealityKit and SwiftUI View
import SwiftUI
import RealityKit
import ARKit
struct ContentView : View {
#State private var isPlacementEnabled = false
#State private var selectedModel: Model?
#State private var modelConfirmedForPlacement: Model?
private var models: [Model] {
//Dynamicaly get file names
let filemanager = FileManager.default
guard let path = Bundle.main.resourcePath,
let files = try? filemanager.contentsOfDirectory(atPath: path)
else {
return []
}
var availableModels: [Model] = []
for filename in files where filename.hasSuffix("usdz") {
let modelName = filename.replacingOccurrences(of: ".usdz", with: "")
let model = Model(modelName: modelName)
availableModels.append(model)
}
return availableModels
}
var body: some View {
ZStack(alignment: .bottom){
ARViewContainer(modelConfirmedForPlacement: self.$modelConfirmedForPlacement)
if self.isPlacementEnabled {
PlacementButtonsView(isPlacementEnabled: self.$isPlacementEnabled,
selectedModel: self.$selectedModel,
modelConfirmedForPlacement: self.$modelConfirmedForPlacement
)
}
else {
ModelPickerView(models: self.models,
isPlacementEnabled: self.$isPlacementEnabled,
selectedModel: self.$selectedModel)
}
}
}
}
struct ARViewContainer: UIViewRepresentable {
#Binding var modelConfirmedForPlacement: Model?
var anchorEntity = AnchorEntity(world: .init(x: 0, y: 0, z: 0))
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let config = ARWorldTrackingConfiguration()
config.planeDetection = [.horizontal, .vertical ]
config.environmentTexturing = .automatic
if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh){
config.sceneReconstruction = .mesh
}
arView.session.run(config)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
//Here is where I try to remove the old 3D Model before loading the new one
if self.modelConfirmedForPlacement?.modelEntity != nil {
uiView.scene.anchors.first?.removeChild((self.modelConfirmedForPlacement?.modelEntity)!)
anchorEntity.scene?.removeAnchor(anchorEntity)
self.anchorEntity.removeChild((self.modelConfirmedForPlacement?.modelEntity)!)
}
if let model = self.modelConfirmedForPlacement{
if model.modelEntity != nil {
print("ARContainer -> DEBUG: adding model to scene - \(model.modelName)" )
}
else{
print("ARContainer -> DEBUG: Unable to load model entity for - \(model.modelName)" )
}
DispatchQueue.main.async {
let modelEntity = try? Entity.load( named: model.modelName + ".usdz")
anchorEntity.addChild(modelEntity!)
uiView.scene.addAnchor(anchorEntity)
}
}
}
}
struct ModelPickerView: View {
var models: [Model]
#Binding var isPlacementEnabled: Bool
#Binding var selectedModel: Model?
var body: some View {
ScrollView(.horizontal, showsIndicators: false){
HStack(spacing: 30){
ForEach(0 ..< self.models.count , id: \.self){ index in
Button {
// print("DEBUG : selected model with name: \(self.models[index].modelName)")
self.isPlacementEnabled = true
self.selectedModel = self.models[index]
} label: {
Image(uiImage: self.models[index].image)
.resizable()
.frame(height: 80 )
.aspectRatio(1/1, contentMode: .fit)
.background(Color.white)
.cornerRadius(12)
}
.buttonStyle(PlainButtonStyle())
}
}
}
.padding(20)
.background(Color.black.opacity(0.5))
}
}
struct PlacementButtonsView: View {
#Binding var isPlacementEnabled: Bool
#Binding var selectedModel: Model?
#Binding var modelConfirmedForPlacement: Model?
var body: some View {
HStack{
//Cancel button
Button {
// print("DEBUG: model ploacement cancelled")
self.resetPlacementParameters()
} label: {
Image(systemName: "xmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.75))
.cornerRadius(30)
.padding(20)
}
//Confirm button
Button {
// print("DEBUG: model ploacement confirmed")
self.modelConfirmedForPlacement = self.selectedModel
self.resetPlacementParameters()
} label: {
Image(systemName: "checkmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.75))
.cornerRadius(30)
.padding(20)
}
}
}
func resetPlacementParameters(){
self.isPlacementEnabled = false
self.selectedModel = nil
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
My code converts the output .WEBP image from an URL on user's screen, and saves it to iOS photo library when the user clicks the download button.
The download button opens up the share sheet, and user clicks "save image" and it fails while saving after user gives its consent to access the photo library.
It does not fail on the simulator, but when I click the save image, it does not show any image in the photo library, because the image after .Webp conversion returns an empty file.
import SwiftUI
import SDWebImageSwiftUI
import UIKit
extension UIImageView{
func imageFrom(url:URL) -> UIImage {
DispatchQueue.global().async { [weak self] in
if let data = try? Data(contentsOf: url){
if let image = UIImage(data:data){
DispatchQueue.main.async{
self?.image = image
}
}
}
}
return image!
}
}
extension String {
func load() -> UIImage {
do {
guard let url = URL(string: self)
else {
return UIImage()
}
let data: Data = try Data(contentsOf:
} catch {URL)
return UIImage(data:
}
return UIImage()
}
}
struct ResultView: View {
#State var chocolate = chocolate.shared
#ObservedObject var imagesModel = ImagesModel.shared
#ObservedObject var appState = AppState.shared
#State var imageSaver = ImageSaver.shared
#State var sharingImageItems: [Any] = []
#State var sharingImage: UIImage = UIImage()
#State var isShareSheetShowing:Bool = false
#Binding var presentedResultView: Bool
func shareButton() {
isShareSheetShowing.toggle()
}
var body: some View {
VStack {
HStack {
Text("Result")
.font(.system(size: 20, weight: .bold))
.foregroundColor(.black)
Spacer()
Button {
appState.presentResult = false
} label: {
Image("Glyph")
}
}
.padding(20)
Image(uiImage: imagesModel.imageUrl.load())
.resizable()
.scaledToFit()
.cornerRadius(6)
Image(uiImage: sharingImage)
Text("\(ContentView().promptField)")
.font(.system(size: 16))
.foregroundColor(.textColor)
Spacer()
VStack {
HStack{
Button("Download"){
sharingImageItems.removeAll()
sharingImageItems.append(sharingImage)
isShareSheetShowing.toggle()
}
.frame(width: 150, height: 45)
.font(Font.system(size:18, weight: .bold))
.foregroundColor(Color.white)
.background(Color.black)
.cornerRadius(6)
}
}
.onAppear() {
sharingImage = imageSaver.saveImage(imageUrl: imagesModel.imageUrl) }
}
.sheet(isPresented: $isShareSheetShowing, content: {
ShareSheet(items: sharingImageItems)
})
.frame(maxWidth: .infinity, maxHeight: .infinity )
.padding(20)
.background(Color.white)
}
}
struct ShareSheet : UIViewControllerRepresentable {
var items: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: items, applicationActivities: nil)
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
}
}
data) ?? UIImage()
I want to separate the views, and I created a struct view to get only the image to make the code more cleaner, please I want to ask how can I pass the selected image to the next view, to make the code more cleaner and Can use this struct view everywhere in the project.
I put in second view PickAPhoto() but I'm not sure how to get the data from it.
Many Thanks,
struct PickAPhoto: View {
#State var imgSelected: UIImage = UIImage(named: "addCameraImg")!
#State var showAddPhotoSheet = false
// phppicker begin
#State private var showPhotoSheet = false
// phppicker end
#State private var sourceType: UIImagePickerController.SourceType = .camera
#State var image: Image? = nil
var body: some View {
Image(uiImage: imgSelected)
.resizable()
.cornerRadius(4)
// .frame(width: 200 , height: 200)
.padding(.top)
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 200, maxHeight: 200)
.transition(.slide)
HStack {
Spacer()
Button(action: {showAddPhotoSheet.toggle()}) {
Label("Take a photo", systemImage: "camera")
.foregroundColor(Color("BrandPrimary"))
}
.sheet(isPresented: $showAddPhotoSheet){
ImagePicker(imageSelected: $imgSelected, sourceType: $sourceType)
// ImageViewPicker()
}
Button(action: { showPhotoSheet = true }) {
Label("Choose photo", systemImage: "photo.fill")
.foregroundColor(Color("BrandPrimary"))
}
.fullScreenCover(isPresented: $showPhotoSheet) {
PhotoPicker(filter: .images, limit: 1) { results in
PhotoPicker.convertToUIImageArray(fromResults: results) { (imagesOrNil, errorOrNil) in
if let error = errorOrNil {
print(error)
}
if let images = imagesOrNil {
if let first = images.first {
print(first)
// image = first
imgSelected = first
}
}
}
}
.edgesIgnoringSafeArea(.all)
}
Spacer()
}
}
}
struct SecondVIew: View {
var body: some View {
PickAPhoto()
}
func getImage(){
// I want to get the image here
}
}
Move the imgSelected source of truth state up in the View hierarchy and pass a write-access binding down.
struct SecondVIew: View {
#State var imgSelected: UIImage = UIImage(named: "addCameraImg")!
var body: some View {
PickAPhoto(imgSelected: $imgSelected)
}
}
struct PickAPhoto: View {
#Binding var imgSelected: UIImage
So I am working on users uploading images to a "profile picture" of sorts. So my image picker I believe is working fine. The issue is it uploads, but the default image still shows initially.
I know this because if I go to upload another image, for a brief moment the default picture disappears and the new uploaded image can be seen. My button triggers the showingImagePicker bool which shows the new image and should hide the default image, I don't understand why the default image would still be showing.
struct RecipeEditorImage: View {
#State private var showingImagePicker = false
#State private var inputImage: UIImage?
#State private var image: Image?
var body: some View {
ZStack (alignment: .trailing){
if showingImagePicker{
image?
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width:200, height: 100)
}
else{
Image("ExampleRecipePicture")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width:200, height: 100)
}
Image(systemName:("plus.circle.fill")).renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.offset(x: 20)
.foregroundColor(Color("completeGreen"))
.frame(width:50, height:10)
.onTapGesture {
showingImagePicker = true
}
.sheet(isPresented: $showingImagePicker){
EditorImagePicker(image: $inputImage)
}
.onChange(of: inputImage){ _ in loadImage() }
}
}
func loadImage(){
guard let inputImage = inputImage else { return }
image = Image(uiImage: inputImage)
}
}
import PhotosUI
import SwiftUI
struct EditorImagePicker: UIViewControllerRepresentable{
#Binding var image: UIImage?
class Coordinator: NSObject, PHPickerViewControllerDelegate{
var parent: EditorImagePicker
init(_ parent: EditorImagePicker){
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let provider = results.first?.itemProvider else { return }
if provider.canLoadObject(ofClass: UIImage.self){
provider.loadObject(ofClass: UIImage.self){image, _ in
self.parent.image = image as? UIImage
}
}
}
}
func makeUIViewController(context: Context) -> PHPickerViewController {
//configures ios to just be able to select images
var config = PHPickerConfiguration()
config.filter = .images
//the view of picker
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
//leave empty for now
}
}
There are a few issues:
Your first if clause guarantees the chosen image will only be shown if the sheet is shown as well.
You probably want to avoid the extra step of storing a reference to the Image View -- just use it dynamically. That gets rid of the onChange as well.
Your loadObject closure needs to call back to the main thread.
import SwiftUI
import PhotosUI
struct RecipeEditorImage: View {
#State private var showingImagePicker = false
#State private var inputImage: UIImage?
var body: some View {
ZStack (alignment: .trailing){
if let inputImage = inputImage {
Image(uiImage: inputImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width:200, height: 100)
} else{
Image(systemName: "pencil")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width:200, height: 100)
}
Image(systemName:("plus.circle.fill")).renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.offset(x: 20)
.foregroundColor(Color("completeGreen"))
.frame(width:50, height:10)
.contentShape(Rectangle())
.onTapGesture {
showingImagePicker = true
}
.sheet(isPresented: $showingImagePicker){
EditorImagePicker(image: $inputImage)
}
}
}
}
struct EditorImagePicker: UIViewControllerRepresentable{
#Binding var image: UIImage?
class Coordinator: NSObject, PHPickerViewControllerDelegate{
var parent: EditorImagePicker
init(_ parent: EditorImagePicker){
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let provider = results.first?.itemProvider else { return }
if provider.canLoadObject(ofClass: UIImage.self){
provider.loadObject(ofClass: UIImage.self){image, _ in
DispatchQueue.main.async {
self.parent.image = image as? UIImage
}
}
}
}
}
func makeUIViewController(context: Context) -> PHPickerViewController {
//configures ios to just be able to select images
var config = PHPickerConfiguration()
config.filter = .images
//the view of picker
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
//leave empty for now
}
}
Coming to a problem where when I upload an image in my app to firebase storage it just overrides the current image I have on my storage. Like when I upload a new photo I want to show the new photo in my storage along with my old photo that is already in there. Can we do something with my code below? thanks for the help.
here is my code:
struct UploadPhoto: View {
#State var showActionSheet = false
#State var showImagePicker = false
#State var sourceType:UIImagePickerController.SourceType = .camera
#State var upload_image:UIImage?
#State var download_image:UIImage?
#State private var showsAlert = false
var body: some View {
VStack {
Text("Please upload your photos here!")
HStack{
if upload_image != nil {
Image(uiImage: upload_image!)
.resizable()
.scaledToFit()
.frame(width:120, height:120, alignment: .center)
} else {
Image(systemName: "photo")
.resizable()
.scaledToFit()
.frame(width:200, height:200, alignment: .center)
}
}.padding()
Button(action: {
self.showActionSheet = true
}) {
Text("Choose...")
}.actionSheet(isPresented: $showActionSheet){
ActionSheet(title: Text("Choose from camera or photo library..."), message: nil, buttons: [
//Button1
.default(Text("Camera"), action: {
self.showImagePicker = true
self.sourceType = .camera
}),
.default(Text("Photo Library"), action: {
self.showImagePicker = true
self.sourceType = .photoLibrary
}),
.cancel()
])
}.sheet(isPresented: $showImagePicker){
imagePicker(image: self.$upload_image, showImagePicker: self.$showImagePicker, sourceType: self.sourceType)
}
Button(action: {
if let thisImage = self.upload_image {
uploadImage(image: thisImage)
} else {
print("")
}
}) {
Text("Submit")
}
}
}
}
func uploadImage(image:UIImage){
if let imageData = image.jpegData(compressionQuality: 1){
let storage = Storage.storage()
storage.reference().child("photo").putData(imageData, metadata: nil){
(_, err) in
if let err = err {
print("an error has occurred - \(err.localizedDescription)")
}
} else {
print("Your photos have been successfully
}
}
} else {
print("coldn't unwrap/case image to data")
}
}
struct UploadPhoto_Previews: PreviewProvider {
static var previews: some View {
UploadPhoto()
}
}
When you upload an image to Firebase, you have to tell it the path where you want to save the image. In your code, you are putting the data to storage.reference().child("photo"). If you want to store multiple images, you need to change the "photo" path, otherwise it will overwrite the current image at the path. I would also recommend using sub-folders.
I would call something like:
let imageName = "image1"
let folderName = "folder1"
let path = "\(folderName)/\(imageName)"
storage.reference(withPath: path).putData...