Frame Photos with SwiftUI
TODO: collapasable, copyable, download, etc
I want to write an iOS app that lets you frame your photos. I am trying to use @Observable but I don't really understand what I'm doing. Here is my code
FrameMakerApp - SwiftUI App which has a ContentView and a Frame ContentView - Has a PhotosPicker, a @State var selected: PhotosPickerItem? and a @State private var picture: UIImage? . When a photo is chosen, we load it into picture. If the picture is present, we use a RealityView to render the picture onto a Plane.
I was trying to extract the ReailtyView into a new Frame class and to have the photo be changable. I got stuck on @
It sounds like you're working on an iOS app in SwiftUI that uses ARKit (since you mentioned RealityView
) for framing photos. Extracting your RealityView
into a separate Frame
class and managing a changeable photo involves a few steps in SwiftUI. Here’s a general approach you might take to refactor your code and use the @ObservableObject
and @Published
property wrappers to manage state across your components.
1. Define an ObservableObject
You can create an ObservableObject
that holds the state for the selected photo. This object will update any views that depend on it when the photo changes.
import SwiftUI
import Combine
class PhotoModel: ObservableObject {
@Published var picture: UIImage?
// Add any other photo-related properties and methods here
}
2. Update ContentView
In ContentView
, you can use an instance of PhotoModel
. When a photo is picked, update the picture
property of this model.
struct ContentView: View {
@StateObject private var photoModel = PhotoModel()
@State private var isPickerPresented = false
var body: some View {
VStack {
if let picture = photoModel.picture {
Frame(photoModel: photoModel)
} else {
Button("Pick Photo") {
isPickerPresented = true
}
}
}
.sheet(isPresented: $isPickerPresented) {
PhotosPicker(
selection: $photoModel.picture,
matching: .images,
photoLibrary: .shared()) {
// Handle completion
}
}
}
}
3. Define the Frame View
Create a new SwiftUI view called Frame
that takes PhotoModel
as a parameter and observes it. This view will re-render whenever the picture
in PhotoModel
changes.
struct Frame: View {
@ObservedObject var photoModel: PhotoModel
var body: some View {
if let picture = photoModel.picture {
RealityView(picture: picture)
.frame(width: 300, height: 300)
} else {
Text("No picture selected")
}
}
}
4. Define RealityView
Make sure RealityView
can take a UIImage
and display it appropriately. Assuming you're using RealityKit, you might need to update a TextureResource
or similar.
struct RealityView: View {
var picture: UIImage
var body: some View {
// Example RealityKit view
ARViewContainer(image: picture).edgesIgnoringSafeArea(.all)
}
}
struct ARViewContainer: UIViewRepresentable {
var image: UIImage
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
// Setup ARView with the image
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
// Update the ARView if needed
}
}
Final Notes
- Make sure you have the necessary permissions to access the photo library and the camera if you're integrating AR features.
- Test the flow from picking a photo to displaying it in the AR view to ensure all data passes correctly between your views.
This setup should help you structure your app for better state management and separation of concerns. If you have specific errors or parts of the code you're stuck on, feel free to ask!I never see my RealityView code run more than once. I would expect it to run each time I choose a different photo: ```// // ContentView.swift // FrameMaker // // Created by Austin King on 3/9/24. //
import SwiftUI import RealityKit import RealityKitContent import PhotosUI
struct ContentView: View { // Observable object that can be changed @State private var userPhotoModel = UserPhoto()
// Temporary machinery for PhotosPicker
@State var selected: PhotosPickerItem?
// @State private var picture: UIImage?
@Environment(\.openWindow) private var openWindow
var body: some View {
VStack {
// if let picture {
if let aPicture = userPhotoModel.selectedPhoto {
Image(uiImage: aPicture)
.resizable()
.scaledToFit()
Spacer()
}
if let aPicture = userPhotoModel.selectedPhoto {
RealityView { content in
let width: Float = Float(aPicture.size.width) / 10000.0
let height: Float = Float(aPicture.size.height) / 10000.0
print("generating \(width) x \(height)")
let planeMesh = MeshResource.generatePlane(width: width, depth: height)
// let planeMaterial = // UnlitMaterial.Color(patternImage: picture)
// AOK start copy/pate
let texture = try! TextureResource.generate(from: aPicture.cgImage!, options: .init(semantic: .color))
print("Running after image changed \(aPicture)")
var material = UnlitMaterial()
// note: color will be different if anything other than .white is specified
material.color = .init(tint: .white, texture: .init(texture))
// note: Using 1.0 was too dark, so 2.0 is used
material.blending = .transparent(opacity: PhysicallyBasedMaterial.Opacity(floatLiteral: 1.0))
// AOK end copy / paste
let plane = ModelEntity(mesh: planeMesh)
plane.model?.materials = [material]
// plane.transform.rotation = simd_quatf(angle: Float.pi, axis: SIMD3(x: 1, y: 0, z: 0))
// plane.transform.rotation = simd_quatf(angle: Float.pi, axis: SIMD3(x: 0, y: 0, z: 1))
// plane.transform.rotation = simd_quatf(angle: Float.pi, axis: SIMD3(x: 0, y: 1, z: 0))
// https://stackoverflow.com/questions/59294602/how-do-i-rotate-an-object-around-only-one-axis-in-realitykit
plane.orientation = simd_quatf(angle: .pi/2, /* 45 Degrees */
axis: [1,0,0]) /* About Z axis */
plane.transform.translation.x -= 0.1
plane.transform.translation.y -= 0.2
plane.transform.translation.z += 0.2
// plane.position = World
// TODO: fuck around with anchros
// let anchor = AnchorEntity(world: .zero)
content.entities.removeAll()
content.add(plane)
}
}
/* Model3D(named: "Scene", bundle: realityKitContentBundle)
.padding(.bottom, 50)*/
Text("Make a Frame")
/* Button(action: pickImage, label: {
Text("Choose a Photo")
})*/
Button(action: pickImage, label: {
Text("Show Frame")
})
PhotosPicker(
selection: $selected,
matching: .images
) {
Text("Select a Photo")
}
}
.padding()
.onChange(of: selected) {
Task(priority: .background) {
if let data = try? await selected?.loadTransferable(type: Data.self) {
print("AOK photo picker onChange fired")
userPhotoModel.selectedPhoto = UIImage(data: data)
}
}
}
}
func pickImage() {
// https://developer.apple.com/videos/play/wwdc2023/10107/
openWindow(id: "frame")
}
}
/* #Preview(windowStyle: .automatic) {
ContentView(currentUserPhoto)
}
*/
```It looks like the issue where your RealityView
code is not re-running might be tied to how SwiftUI views react to data changes. If RealityView
is a custom component that internally processes significant changes (like changing the photo to be rendered), it needs to be made reactive to changes in its input data — in this case, the aPicture
passed to it.
Here's how you can adjust your setup to ensure the view updates reactively:
1. Ensure RealityView
is a Proper SwiftUI View
First, ensure that RealityView
is a proper SwiftUI view that reacts to changes in its properties. If RealityView
is a custom SwiftUI view that wraps a ARView
, it should take the image as a @Binding
or at least as a property that SwiftUI can track.
If you haven't already, modify RealityView
to accept the image directly as a property:
struct RealityView: View {
var picture: UIImage
var body: some View {
// Your AR rendering logic here, which should use 'picture'
ARViewContainer(image: picture).edgesIgnoringSafeArea(.all)
}
}
2. Use @ObservedObject
or @StateObject
If RealityView
relies heavily on userPhotoModel
, ensure this model is provided as an @ObservedObject
(if passed from a parent view) or @StateObject
(if owned by the view) so that changes in userPhotoModel.selectedPhoto
trigger view updates:
struct ContentView: View {
@StateObject private var userPhotoModel = UserPhoto() // This should probably be @StateObject if it's initialized here
var body: some View {
VStack {
if let aPicture = userPhotoModel.selectedPhoto {
RealityView(picture: aPicture)
}
Button(action: pickImage) {
Text("Show Frame")
}
PhotosPicker(selection: $userPhotoModel.selected) {
Text("Select a Photo")
}
}
.onChange(of: userPhotoModel.selected) {
// Load the new image
// This might not be necessary if UserPhoto handles it
}
}
}
3. Debugging
To ensure that changes are captured, add more debugging print statements within theRealityView is part of the iOS frameworkAh, if you're referring to a RealityKit view or something similar, that does change things a bit. Since RealityKit isn't inherently designed to be reactive like SwiftUI components, you need to explicitly handle updates to make sure the AR content reflects the new image each time it changes.
You can manage updates in SwiftUI by ensuring that whenever the image changes, the whole AR scene or the relevant parts get properly refreshed. Here’s how you might approach it:
Ensuring Updates in RealityKit
Explicitly Remove and Re-add Content: Since AR content doesn't automatically update when a state changes, you need to make sure you're properly refreshing your content. This might involve removing all existing entities and re-adding them when the image changes.
Use
@State
or@ObservableObject
for Image Updates: You are currently using an@ObservableObject
which is good. However, make sure every part of your SwiftUI body that depends on this object correctly reacts to changes. It seems like you're doing this, but you may need to explicitly reset the AR scene when the image changes.
Here’s a refined example, assuming RealityView
is a placeholder for where you use RealityKit views:
struct ContentView: View {
@StateObject private var userPhotoModel = UserPhoto()
var body: some View {
VStack {
if let aPicture = userPhotoModel.selectedPhoto {
ARViewContainer(image: aPicture)
}
Button(action: pickImage) {
Text("Show Frame")
}
PhotosPicker(selection: $userPhotoModel.selected) {
Text("Select a Photo")
}
}
.onChange(of: userPhotoModel.selected) { _ in
loadNewImage()
}
}
func loadNewImage() {
Task(priority: .background) {
if let data = try? await userPhotoModel.selected?.loadTransferable(type: Data.self) {
userPhotoModel.selectedPhoto = UIImage(data: data)
}
}
}
func pickImage() {
// Implementation depends on how you want to trigger photo selection
}
}
struct ARViewContainer: View {
let image: UIImage
var body: some View {
CustomARView(image: image)
.edgesIgnoringSafeArea(.all)
}
}
struct CustomARView: UIViewRepresentable {
let image: UIImage
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
setupARContent(arView, with: image)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
// This gets called if SwiftUI sees a change in the `image`
setupARContent(uiView, with: image)
}
private func setupARContent(_ arView: ARView, with image: UIImage) {
arView.scene.anchors.removeAll()
let texture = try! TextureResource.generate(from: image.cgImage!, options: .init(semantic: .color))
var material = UnlitMaterial()
material.color = .init(tint: .white, texture: .init(texture))
let width: Float = Float(image.size.width) / 10000.0
let height: Float = Float(image.size.height) / 10000.0
let planeMesh = MeshResource.generatePlane(width: width, depth: height)
let plane = ModelEntity(mesh: planeMesh, materials: [material])
let anchor = AnchorEntity(world: .zero)
anchor.addChild(plane)
arView.scene.addAnchor(anchor)
}
}
Key Points:
- React to Changes: Use
.onChange(of:)
to react to changes in the photo picker selection and then update the AR content. - Update AR Content Dynamically: When updating AR content, make sure to clear the previous content and set up new content with the updated image. This ensures your AR scene reflects the new image each time it changes.
This should help make sure your AR content updates whenever you choose a new photo, making it more interactive and responsive.
Frame Photos with SwiftUI created at Tue Jan 20 1970 20:22:04 GMT+0000 (Coordinated Universal Time)Related Chats
Frame Photos with SwiftUI 0.999
Pass Data to SwiftUI 0.563
Pass Data to SwiftUI Window. 0.560
SwiftUI App, Centered Image 0.559
Shared ViewModel Across Views 0.548
Reusable SwiftUI custom view. 0.532
StateObject Error Resolution 0.526
Add Function to SwiftUI. 0.515
Fix Warning: Use StateObject 0.513
SwiftUI View with Model 0.507