RealityKit removing old 3D objects from scene before adding new 3D objects - swift

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

Related

SwiftUI Image Saving Fails During .WEBP Image Conversion

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()

Blinking symbol with didSet in SwiftUI

This is synthesized from a much larger app. I'm trying to blink an SF symbol in SwiftUI by activating a timer in a property's didSet. A print statement inside timer prints the expected value but the view doesn't update.
I'm using structs throughout my model data and am guessing this will have something to do with value vs. reference types. I'm trying to avoid converting from structs to classes.
import SwiftUI
import Combine
#main
struct TestBlinkApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class Model: ObservableObject {
#Published var items: [Item] = []
static var loadData: Model {
let model = Model()
model.items = [Item("Item1"), Item("Item2"), Item("Item3"), Item("Item4")]
return model
}
}
struct Item {
static let ledBlinkTimer: TimeInterval = 0.5
private let ledTimer = Timer.publish(every: ledBlinkTimer, tolerance: ledBlinkTimer * 0.1, on: .main, in: .default).autoconnect()
private var timerSubscription: AnyCancellable? = nil
var name: String
var isLEDon = false
var isLedBlinking = false {
didSet {
var result = self
print("in didSet: isLedBlinking: \(result.isLedBlinking) isLEDon: \(result.isLEDon)")
guard result.isLedBlinking else {
result.isLEDon = true
result.ledTimer.upstream.connect().cancel()
print("Cancelling timer.")
return
}
result.timerSubscription = result.ledTimer
.sink { _ in
result.isLEDon.toggle()
print("\(result.name) in ledTimer isLEDon: \(result.isLEDon)")
}
}
}
init(_ name: String) {
self.name = name
}
}
struct ContentView: View {
#StateObject var model = Model.loadData
let color = Color(UIColor.label)
public var body: some View {
VStack {
Text(model.items[0].name)
Image(systemName: model.items[0].isLEDon ? "circle.fill" : "circle")
.foregroundColor(model.items[0].isLEDon ? .green : color)
Button("Toggle") {
model.items[0].isLedBlinking.toggle()
}
}
.foregroundColor(color)
}
}
Touching the "Toggle" button starts the timer that's suppose to blink the circle. The print statement shows the value changing but the view doesn't update. Why??
You can use animation to make it blink, instead of a timer.
The model of Item gets simplified, you just need a boolean variable, like this:
struct Item {
var name: String
// Just a toggle: blink/ no blink
var isLedBlinking = false
init(_ name: String) {
self.name = name
}
}
The "hard work" is done by the view: changing the variable triggers or stops the blinking. The animation does the magic:
struct ContentView: View {
#StateObject var model = Model.loadData
let color = Color(UIColor.label)
public var body: some View {
VStack {
Text(model.items[0].name)
.padding()
// Change based on isLedBlinking
Image(systemName: model.items[0].isLedBlinking ? "circle.fill" : "circle")
.font(.largeTitle)
.foregroundColor(model.items[0].isLedBlinking ? .green : color)
// Animates the view based on isLedBlinking: when is blinking, blinks forever, otherwise does nothing
.animation(model.items[0].isLedBlinking ? .easeInOut.repeatForever() : .default, value: model.items[0].isLedBlinking)
.padding()
Button("Toggle: \(model.items[0].isLedBlinking ? "Blinking" : "Still")") {
model.items[0].isLedBlinking.toggle()
}
.padding()
}
.foregroundColor(color)
}
}
A different approach with a timer:
struct ContentView: View {
#StateObject var model = Model.loadData
let timer = Timer.publish(every: 0.25, tolerance: 0.1, on: .main, in: .common).autoconnect()
let color = Color(UIColor.label)
public var body: some View {
VStack {
Text(model.items[0].name)
if model.items[0].isLedBlinking {
Image(systemName: model.items[0].isLEDon ? "circle.fill" : "circle")
.onReceive(timer) { _ in
model.items[0].isLEDon.toggle()
}
.foregroundColor(model.items[0].isLEDon ? .green : color)
} else {
Image(systemName: model.items[0].isLEDon ? "circle.fill" : "circle")
.foregroundColor(model.items[0].isLEDon ? .green : color)
}
Button("Toggle: \(model.items[0].isLedBlinking ? "Blinking" : "Still")") {
model.items[0].isLedBlinking.toggle()
}
}
.foregroundColor(color)
}
}

Using Data from Firestore Data Class in reusable picker SwiftUI

I feel like I'm missing something really obvious and I can't seem to figure it out. I want to use a reusable picker in SwiftUI, the one I am referring to is Stewart Lynch's "Reusable-Custom-Picker" https://github.com/StewartLynch/Reusable-Custom-Picker-for-SwiftUI
I have tried multiple times to get the filter working with my Firestore data and I am able to get the picker to read the data but then I am unable to filter it.
and the reusable picker struct is
import Combine
import Firebase
import SwiftUI
struct CustomPickerView: View {
#ObservedObject var schoolData = SchoolDataStore()
var datas : SchoolDataStore
var items : [String]
#State private var filteredItems: [String] = []
#State private var filterString: String = ""
#State private var frameHeight: CGFloat = 400
#Binding var pickerField: String
#Binding var presentPicker: Bool
var body: some View {
let filterBinding = Binding<String> (
get: { filterString },
set: {
filterString = $0
if filterString != "" {
filteredItems = items.filter{$0.lowercased().contains(filterString.lowercased())}
} else {
filteredItems = items
}
setHeight()
}
)
return ZStack {
Color.black.opacity(0.4)
VStack {
VStack(alignment: .leading, spacing: 5) {
HStack {
Button(action: {
withAnimation {
presentPicker = false
}
}) {
Text("Cancel")
}
.padding(10)
Spacer()
}
.background(Color(UIColor.darkGray))
.foregroundColor(.white)
Text("Tap an entry to select it")
.font(.caption)
.padding(.leading,10)
TextField("Filter by entering text", text: filterBinding)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
List {
ForEach(schoolData.datas, id: \.id) { i in
Button(action: {
pickerField = i.name
withAnimation {
presentPicker = false
}
}) {
Text(i.name)
}
}
}
}
.background(Color(UIColor.secondarySystemBackground))
.cornerRadius(10)
.frame(maxWidth: 400)
.padding(.horizontal,10)
.frame(height: frameHeight)
.padding(.top, 40)
Spacer()
}
}
.edgesIgnoringSafeArea(.all)
.onAppear {
filteredItems = items
setHeight()
}
}
fileprivate func setHeight() {
withAnimation {
if filteredItems.count > 5 {
frameHeight = 400
} else if filteredItems.count == 0 {
frameHeight = 130
} else {
frameHeight = CGFloat(filteredItems.count * 45 + 130)
}
}
}
}
struct CustomPickerView_Previews: PreviewProvider {
static let sampleData = ["Milk", "Apples", "Sugar", "Eggs", "Oranges", "Potatoes", "Corn", "Bread"].sorted()
static var previews: some View {
CustomPickerView(datas: SchoolDataStore(), items: sampleData, pickerField: .constant(""), presentPicker: .constant(true))
}
}
class SchoolDataStore : ObservableObject{
#Published var datas = [schoolName]()
init() {
let db = Firestore.firestore()
db.collection("School Name").addSnapshotListener { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in snap!.documentChanges{
let id = i.document.documentID
let name = i.document.get("Name") as? String ?? ""
self.datas.append(schoolName(id: id, name: name))
}
}
}
}
struct schoolName : Identifiable, Codable {
var id : String
var name : String
}
I have managed to get the data from Firestore into my picker now, but I am currently unable to filter.
When I change the values of the filteredItems into schoolData.datas I get an error about converting to string or .filter is not a member etc.
Anybody able to point me in the right direction with this please?
Kindest Regards,

Kingfisher updating KFImage url is not updating the image in SwiftUI

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()
}

Passing an EnvronmentObject to NSHostingControllers

I'm using a custom SwiftUI View using NSSplitViewController that takes in ViewBuilders for the two subviews. My problem is any change in state of the environment doesn't propagate to the subviews inside SplitView, but propagates to another TextView in ContentView
import SwiftUI
class AppEnvironment : ObservableObject {
#Published var value: String = "default"
}
struct ContentView: View {
#EnvironmentObject var env : AppEnvironment
var body: some View {
HStack {
Button(action: {
self.env.value = "new value"
}, label: { Text("Change value") })
Text(self.env.value)
GeometryReader { geometry in
SplitView(master: {
Text("master")
.background(Color.yellow)
}, detail: {
HStack {
Text(self.env.value) }
.background(Color.orange)
}).frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
//
// Source: https://gist.github.com/HashNuke/f8895192fff1f275e66c30340f304d80
//
struct SplitView<Master: View, Detail: View>: View {
var master: Master
var detail: Detail
init(#ViewBuilder master: () -> Master, #ViewBuilder detail: () -> Detail) {
self.master = master()
self.detail = detail()
}
var body: some View {
let viewControllers = [NSHostingController(rootView: master), NSHostingController(rootView: detail)]
return SplitViewController(viewControllers: viewControllers)
}
}
struct SplitViewController: NSViewControllerRepresentable {
var viewControllers: [NSViewController]
private let splitViewResorationIdentifier = "com.company.restorationId:mainSplitViewController"
func makeNSViewController(context: Context) -> NSViewController {
let controller = NSSplitViewController()
controller.splitView.dividerStyle = .thin
controller.splitView.autosaveName = NSSplitView.AutosaveName(splitViewResorationIdentifier)
controller.splitView.identifier = NSUserInterfaceItemIdentifier(rawValue: splitViewResorationIdentifier)
let vcLeft = viewControllers[0]
let vcRight = viewControllers[1]
vcLeft.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
vcRight.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 70).isActive = true
let sidebarItem = NSSplitViewItem(contentListWithViewController: vcLeft)
sidebarItem.canCollapse = false
// I'm not sure if this has any impact
// controller.view.frame = CGRect(origin: .zero, size: CGSize(width: 800, height: 800))
controller.addSplitViewItem(sidebarItem)
let mainItem = NSSplitViewItem(viewController: vcRight)
controller.addSplitViewItem(mainItem)
return controller
}
func updateNSViewController(_ nsViewController: NSViewController, context: Context) {
print("should update splitView")
}
}
Yes, in such case EnvironmentObject is not injected automatically. The solution would be to separate content into designated views (for better design) and inject environment object manually.
Here it is
Text(self.env.value)
GeometryReader { geometry in
SplitView(master: {
MasterView().environmentObject(self.env)
}, detail: {
HStack {
DetailView().environmentObject(self.env)
}).frame(width: geometry.size.width, height: geometry.size.height)
}
and views
struct MasterView: View {
var body: some View {
Text("master")
.background(Color.yellow)
}
}
struct DetailView: View {
var body: some View {
HStack {
Text(self.env.value) }
.background(Color.orange)
}
}