RealityKit – ARView raycasting returns 0 results - ios

I have a subclass of RealityKit's ARView that has the following function for making a Raycast:
func makeRaycastQuery(alignmentType: ARRaycastQuery.TargetAlignment) -> simd_float4x4? {
let results = self.raycast(from: self.center,
// Better for pinning to planes
allowing: .estimatedPlane,
alignment: alignmentType)
// We don't care about changing scale on raycast so keep it the same
guard var result = results.first?.worldTransform else { return nil }
result.scale = SCNVector3(1, 1, 1)
return result
}
However, my results array is always empty. Is there some sort of other configuration I need to do when setting up an ARView to enable raycasting?

Try this code (I've written it for iPad's Playgrounds):
import RealityKit
import SwiftUI
import ARKit
import PlaygroundSupport
struct ContentView : View {
#State private var arView = ARView(frame: .zero)
var body: some View {
return ARContainer(arView: $arView)
.gesture(
TapGesture()
.onEnded { _ in
raycasting(arView: arView)
}
)
}
func raycasting(arView: ARView) {
guard let query = arView.makeRaycastQuery(from: arView.center,
allowing: .estimatedPlane,
alignment: .any)
else { fatalError() }
guard let result = arView.session.raycast(query).first
else { fatalError() }
let entity = ModelEntity(mesh: .generateSphere(radius: 0.1))
let anchor = AnchorEntity(raycastResult: result)
anchor.addChild(entity)
arView.scene.anchors.append(anchor)
}
}
struct ARContainer : UIViewRepresentable {
#Binding var arView: ARView
func makeUIView(context: Context) -> ARView {
arView.cameraMode = .ar
return arView
}
func updateUIView(_ view: ARView, context: Context) { }
}
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.setLiveView(ContentView())
P. S.
This version works in UIKit app.

Related

SwiftUI - How to display camera feed in PiP mode

I'm building an app where I have my camera preview playing in a component, with the following code :
import SwiftUI
import AVFoundation
import AVKit
struct CameraPreview: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let controller = CameraPreviewViewController()
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
}
class CameraPreviewViewController: UIViewController {
var session: AVCaptureSession?
var previewLayer: AVCaptureVideoPreviewLayer?
var player: AVPlayer?
var playerLayer: AVPlayerLayer?
var pipController: AVPictureInPictureController?
override func viewDidLoad() {
super.viewDidLoad()
setupSession()
setupPreview()
startPIP()
}
func setupSession() {
session = AVCaptureSession()
let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .front)
guard let device = deviceDiscoverySession.devices.first else { return }
guard let input = try? AVCaptureDeviceInput(device: device) else { return }
session?.addInput(input)
}
func setupPreview() {
previewLayer = AVCaptureVideoPreviewLayer(session: session!)
previewLayer?.videoGravity = AVLayerVideoGravity.resize
previewLayer?.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
view.layer.insertSublayer(previewLayer!, at: 0)
player = AVPlayer()
playerLayer = AVPlayerLayer(player: player)
playerLayer?.frame = previewLayer!.frame
view.layer.addSublayer(playerLayer!)
session?.startRunning()
player?.play()
}
func startPIP() {
guard AVPictureInPictureController.isPictureInPictureSupported() else { return }
pipController = AVPictureInPictureController(playerLayer: playerLayer!)
pipController?.startPictureInPicture()
}
}
And I've been trying to launch this view in Picture in Picture mode with the Pipify package.
import SwiftUI
import Pipify
struct ContentView: View {
#State var isPresentedThree = false
var body: some View {
VStack {
HStack{
Image(systemName: "camera")
.imageScale(.large)
.foregroundColor(.accentColor)
Image(systemName: "person")
.imageScale(.large)
.foregroundColor(.accentColor)
}
Text("FLOATING CAMERA")
.padding(6.0)
Button("Launch PiP mode") { isPresentedThree.toggle() }
.pipify(isPresented: $isPresentedThree) {
CameraPreview()
.foregroundColor(.red)
.padding()
.onPipSkip { _ in }
.onPipPlayPause { _ in }
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm new to SwiftUI so I'm quite confused, that code doesn't work for my CameraPreview but does work for text and such.
How could I fix this or do it another way ?
Thanks !
I expect the camera preview to display in Picture in Picture as soon as the user clicks the button "Launch PiP mode".

Broadcast View (MTHKView) not Extending full screen

Consider the following example
import AVFoundation
import HaishinKit
import VideoToolbox
import SwiftUI
struct TestStreamView: View {
#State var rtmpStream: RTMPStream?
var rtmpConnection = RTMPConnection()
var body: some View {
ZStack(alignment: .topLeading) {
if let stream = rtmpStream {
BroadcastView(stream: stream)
.cornerRadius(12)
}
}
.edgesIgnoringSafeArea(.top)
.onAppear {
rtmpStream = RTMPStream(connection: rtmpConnection)
guard let stream = rtmpStream else { return }
stream.orientation = .portrait
stream.captureSettings = [
.sessionPreset: AVCaptureSession.Preset.hd1920x1080,
.continuousAutofocus: true,
.continuousExposure: true,
.fps: 30
]
stream.videoSettings = [
.scalingMode: ScalingMode.cropSourceToCleanAperture,
.width: 1080,
.height: 1920,
.bitrate: 5000000,
.profileLevel: kVTProfileLevel_H264_Main_AutoLevel,
.maxKeyFrameIntervalDuration: 2
]
stream.audioSettings = [
.bitrate: 128000 // Always use 128kbps
]
stream.attachAudio(AVCaptureDevice.default(for: .audio))
stream.attachCamera(DeviceUtil.device(withPosition: .front))
}
}
}
Where I am using a library called HaishinKit to set up my live stream feature.
I created a UIViewRepresentable for the MTHKView found in HaishinKit as follows
public struct BroadcastView: UIViewRepresentable {
let stream: RTMPStream
#State private var broadcastView: MTHKView?
public class Coordinator: NSObject, RTMPStreamDelegate {
var parent: BroadcastView
init(_ parent: BroadcastView) {
self.parent = parent
}
// MARK: - RTMPStreamDelegate callbacks
public func rtmpStreamDidClear(_ stream: RTMPStream) {}
public func rtmpStream(_ stream: RTMPStream, didStatics connection: RTMPConnection) {}
public func rtmpStream(_ stream: RTMPStream, didPublishInsufficientBW connection: RTMPConnection) {}
public func rtmpStream(_ stream: RTMPStream, didPublishSufficientBW connection: RTMPConnection) {}
#objc func focusGesture(sender: UITapGestureRecognizer) {
guard let broadcastView = parent.broadcastView else { return }
if sender.state == UIGestureRecognizer.State.ended {
let point = sender.location(in: broadcastView)
let pointOfInterest = CGPoint(x: point.x / broadcastView.bounds.size.width, y: point.y / broadcastView.bounds.size.height)
parent.stream.setPointOfInterest(pointOfInterest, exposure: pointOfInterest)
}
}
}
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
public func makeUIView(context: Context) -> MTHKView {
let view = MTHKView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.attachStream(stream)
stream.delegate = context.coordinator
DispatchQueue.main.async {
self.broadcastView = view
}
let focusGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.focusGesture(sender:)))
view.addGestureRecognizer(focusGesture)
return view
}
public func updateUIView(_ uiView: MTHKView, context: Context) {
uiView.attachStream(stream)
}
}
I'm facing two major issues with this setup here.
The MTHKView video capture doesn't extend to fullscreen (observe screenshot below with the black spacing)
I'm required to set the aspectRatio on the videoSettings property which doesn't seem the most ideal if using a different device.
Any input or ideas would be greatly appreciated! Thank you!
Adding view.videoGravity = .resizeAspectFill fixes the problem

SwiftUI | Using onDrag and onDrop to reorder Items within one single LazyGrid?

I was wondering if it is possible to use the View.onDrag and View.onDrop to add drag and drop reordering within one LazyGrid manually?
Though I was able to make every Item draggable using onDrag, I have no idea how to implement the dropping part.
Here is the code I was experimenting with:
import SwiftUI
//MARK: - Data
struct Data: Identifiable {
let id: Int
}
//MARK: - Model
class Model: ObservableObject {
#Published var data: [Data]
let columns = [
GridItem(.fixed(160)),
GridItem(.fixed(160))
]
init() {
data = Array<Data>(repeating: Data(id: 0), count: 100)
for i in 0..<data.count {
data[i] = Data(id: i)
}
}
}
//MARK: - Grid
struct ContentView: View {
#StateObject private var model = Model()
var body: some View {
ScrollView {
LazyVGrid(columns: model.columns, spacing: 32) {
ForEach(model.data) { d in
ItemView(d: d)
.id(d.id)
.frame(width: 160, height: 240)
.background(Color.green)
.onDrag { return NSItemProvider(object: String(d.id) as NSString) }
}
}
}
}
}
//MARK: - GridItem
struct ItemView: View {
var d: Data
var body: some View {
VStack {
Text(String(d.id))
.font(.headline)
.foregroundColor(.white)
}
}
}
Thank you!
SwiftUI 2.0
Here is completed simple demo of possible approach (did not tune it much, `cause code growing fast as for demo).
Important points are: a) reordering does not suppose waiting for drop, so should be tracked on the fly; b) to avoid dances with coordinates it is more simple to handle drop by grid item views; c) find what to where move and do this in data model, so SwiftUI animate views by itself.
Tested with Xcode 12b3 / iOS 14
import SwiftUI
import UniformTypeIdentifiers
struct GridData: Identifiable, Equatable {
let id: Int
}
//MARK: - Model
class Model: ObservableObject {
#Published var data: [GridData]
let columns = [
GridItem(.fixed(160)),
GridItem(.fixed(160))
]
init() {
data = Array(repeating: GridData(id: 0), count: 100)
for i in 0..<data.count {
data[i] = GridData(id: i)
}
}
}
//MARK: - Grid
struct DemoDragRelocateView: View {
#StateObject private var model = Model()
#State private var dragging: GridData?
var body: some View {
ScrollView {
LazyVGrid(columns: model.columns, spacing: 32) {
ForEach(model.data) { d in
GridItemView(d: d)
.overlay(dragging?.id == d.id ? Color.white.opacity(0.8) : Color.clear)
.onDrag {
self.dragging = d
return NSItemProvider(object: String(d.id) as NSString)
}
.onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging))
}
}.animation(.default, value: model.data)
}
}
}
struct DragRelocateDelegate: DropDelegate {
let item: GridData
#Binding var listData: [GridData]
#Binding var current: GridData?
func dropEntered(info: DropInfo) {
if item != current {
let from = listData.firstIndex(of: current!)!
let to = listData.firstIndex(of: item)!
if listData[to].id != current!.id {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
self.current = nil
return true
}
}
//MARK: - GridItem
struct GridItemView: View {
var d: GridData
var body: some View {
VStack {
Text(String(d.id))
.font(.headline)
.foregroundColor(.white)
}
.frame(width: 160, height: 240)
.background(Color.green)
}
}
Edit
Here is how to fix the never disappearing drag item when dropped outside of any grid item:
struct DropOutsideDelegate: DropDelegate {
#Binding var current: GridData?
func performDrop(info: DropInfo) -> Bool {
current = nil
return true
}
}
struct DemoDragRelocateView: View {
...
var body: some View {
ScrollView {
...
}
.onDrop(of: [UTType.text], delegate: DropOutsideDelegate(current: $dragging))
}
}
Here's my solution (based on Asperi's answer) for those who seek for a generic approach for ForEach where I abstracted the view away:
struct ReorderableForEach<Content: View, Item: Identifiable & Equatable>: View {
let items: [Item]
let content: (Item) -> Content
let moveAction: (IndexSet, Int) -> Void
// A little hack that is needed in order to make view back opaque
// if the drag and drop hasn't ever changed the position
// Without this hack the item remains semi-transparent
#State private var hasChangedLocation: Bool = false
init(
items: [Item],
#ViewBuilder content: #escaping (Item) -> Content,
moveAction: #escaping (IndexSet, Int) -> Void
) {
self.items = items
self.content = content
self.moveAction = moveAction
}
#State private var draggingItem: Item?
var body: some View {
ForEach(items) { item in
content(item)
.overlay(draggingItem == item && hasChangedLocation ? Color.white.opacity(0.8) : Color.clear)
.onDrag {
draggingItem = item
return NSItemProvider(object: "\(item.id)" as NSString)
}
.onDrop(
of: [UTType.text],
delegate: DragRelocateDelegate(
item: item,
listData: items,
current: $draggingItem,
hasChangedLocation: $hasChangedLocation
) { from, to in
withAnimation {
moveAction(from, to)
}
}
)
}
}
}
The DragRelocateDelegate basically stayed the same, although I made it a bit more generic and safer:
struct DragRelocateDelegate<Item: Equatable>: DropDelegate {
let item: Item
var listData: [Item]
#Binding var current: Item?
#Binding var hasChangedLocation: Bool
var moveAction: (IndexSet, Int) -> Void
func dropEntered(info: DropInfo) {
guard item != current, let current = current else { return }
guard let from = listData.firstIndex(of: current), let to = listData.firstIndex(of: item) else { return }
hasChangedLocation = true
if listData[to] != current {
moveAction(IndexSet(integer: from), to > from ? to + 1 : to)
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
hasChangedLocation = false
current = nil
return true
}
}
And finally here is the actual usage:
ReorderableForEach(items: itemsArr) { item in
SomeFancyView(for: item)
} moveAction: { from, to in
itemsArr.move(fromOffsets: from, toOffset: to)
}
There was a few additional issues raised to the excellent solutions above, so here's what I could come up with on Jan 1st with a hangover (i.e. apologies for being less than eloquent):
If you pick a griditem and release it (to cancel), then the view is not reset
I added a bool that checks if the view had been dragged yet, and if it hasn't then it doesn't hide the view in the first place. It's a bit of a hack, because it doesn't really reset, it just postpones hiding the view until it knows that you want to drag it. I.e. if you drag really fast, you can see the view briefly before it's hidden.
If you drop a griditem outside the view, then the view is not reset
This one was partially addressed already, by adding the dropOutside delegate, but SwiftUI doesn't trigger it unless you have a background view (like a color), which I think caused some confusion. I therefore added a background in grey to illustrate how to properly trigger it.
Hope this helps anyone:
import SwiftUI
import UniformTypeIdentifiers
struct GridData: Identifiable, Equatable {
let id: String
}
//MARK: - Model
class Model: ObservableObject {
#Published var data: [GridData]
let columns = [
GridItem(.flexible(minimum: 60, maximum: 60))
]
init() {
data = Array(repeating: GridData(id: "0"), count: 50)
for i in 0..<data.count {
data[i] = GridData(id: String("\(i)"))
}
}
}
//MARK: - Grid
struct DemoDragRelocateView: View {
#StateObject private var model = Model()
#State private var dragging: GridData? // I can't reset this when user drops view ins ame location as drag started
#State private var changedView: Bool = false
var body: some View {
VStack {
ScrollView(.vertical) {
LazyVGrid(columns: model.columns, spacing: 5) {
ForEach(model.data) { d in
GridItemView(d: d)
.opacity(dragging?.id == d.id && changedView ? 0 : 1)
.onDrag {
self.dragging = d
changedView = false
return NSItemProvider(object: String(d.id) as NSString)
}
.onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging, changedView: $changedView))
}
}.animation(.default, value: model.data)
}
}
.frame(maxWidth:.infinity, maxHeight: .infinity)
.background(Color.gray.edgesIgnoringSafeArea(.all))
.onDrop(of: [UTType.text], delegate: DropOutsideDelegate(current: $dragging, changedView: $changedView))
}
}
struct DragRelocateDelegate: DropDelegate {
let item: GridData
#Binding var listData: [GridData]
#Binding var current: GridData?
#Binding var changedView: Bool
func dropEntered(info: DropInfo) {
if current == nil { current = item }
changedView = true
if item != current {
let from = listData.firstIndex(of: current!)!
let to = listData.firstIndex(of: item)!
if listData[to].id != current!.id {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
changedView = false
self.current = nil
return true
}
}
struct DropOutsideDelegate: DropDelegate {
#Binding var current: GridData?
#Binding var changedView: Bool
func dropEntered(info: DropInfo) {
changedView = true
}
func performDrop(info: DropInfo) -> Bool {
changedView = false
current = nil
return true
}
}
//MARK: - GridItem
struct GridItemView: View {
var d: GridData
var body: some View {
VStack {
Text(String(d.id))
.font(.headline)
.foregroundColor(.white)
}
.frame(width: 60, height: 60)
.background(Circle().fill(Color.green))
}
}
Goal: Reordering Items in HStack
I was trying to figure out how to leverage this solution in SwiftUI for macOS when dragging icons to re-order a horizontal set of items. Thanks to #ramzesenok and #Asperi for the overall solution. I added a CGPoint property along with their solution to achieve the desired behavior. See the animation below.
Define the point
#State private var drugItemLocation: CGPoint?
I used in dropEntered, dropExited, and performDrop DropDelegate functions.
func dropEntered(info: DropInfo) {
if current == nil {
current = item
drugItemLocation = info.location
}
guard item != current,
let current = current,
let from = icons.firstIndex(of: current),
let toIndex = icons.firstIndex(of: item) else { return }
hasChangedLocation = true
drugItemLocation = info.location
if icons[toIndex] != current {
icons.move(fromOffsets: IndexSet(integer: from), toOffset: toIndex > from ? toIndex + 1 : toIndex)
}
}
func dropExited(info: DropInfo) {
drugItemLocation = nil
}
func performDrop(info: DropInfo) -> Bool {
hasChangedLocation = false
drugItemLocation = nil
current = nil
return true
}
For a full demo, I created a gist using Playgrounds
Here is how you implement the on drop part. But remember the ondrop can allow content to be dropped in from outside the app if the data conforms to the UTType. More on UTTypes.
Add the onDrop instance to your lazyVGrid.
LazyVGrid(columns: model.columns, spacing: 32) {
ForEach(model.data) { d in
ItemView(d: d)
.id(d.id)
.frame(width: 160, height: 240)
.background(Color.green)
.onDrag { return NSItemProvider(object: String(d.id) as NSString) }
}
}.onDrop(of: ["public.plain-text"], delegate: CardsDropDelegate(listData: $model.data))
Create a DropDelegate to handling dropped content and the drop location with the given view.
struct CardsDropDelegate: DropDelegate {
#Binding var listData: [MyData]
func performDrop(info: DropInfo) -> Bool {
// check if data conforms to UTType
guard info.hasItemsConforming(to: ["public.plain-text"]) else {
return false
}
let items = info.itemProviders(for: ["public.plain-text"])
for item in items {
_ = item.loadObject(ofClass: String.self) { data, _ in
// idea is to reindex data with dropped view
let index = Int(data!)
DispatchQueue.main.async {
// id of dropped view
print("View Id dropped \(index)")
}
}
}
return true
}
}
Also the only real useful parameter of performDrop is info.location a CGPoint of the drop location, Mapping a CGPoint to the view you want to replace seems unreasonable. I would think the OnMove would be a better option and would make moving your data/Views a breeze. I was unsuccessful to get OnMove working within a LazyVGrid.
As LazyVGrid are still in beta and are bound to change. I would abstain from use on more complex tasks.
I came with a bit different approach that works fine on macOS. Instead of using .onDrag and .onDrop Im using .gesture(DragGesture) with a helper class and modifiers.
Here are helper objects (just copy this to the new file):
// Helper class for dragging objects inside LazyVGrid.
// Grid items must be of the same size
final class DraggingManager<Entry: Identifiable>: ObservableObject {
let coordinateSpaceID = UUID()
private var gridDimensions: CGRect = .zero
private var numberOfColumns = 0
private var numberOfRows = 0
private var framesOfEntries = [Int: CGRect]() // Positions of entries views in coordinate space
func setFrameOfEntry(at entryIndex: Int, frame: CGRect) {
guard draggedEntry == nil else { return }
framesOfEntries[entryIndex] = frame
}
var initialEntries: [Entry] = [] {
didSet {
entries = initialEntries
calculateGridDimensions()
}
}
#Published // Currently displayed (while dragging)
var entries: [Entry]?
var draggedEntry: Entry? { // Detected when dragging starts
didSet { draggedEntryInitialIndex = initialEntries.firstIndex(where: { $0.id == draggedEntry?.id }) }
}
var draggedEntryInitialIndex: Int?
var draggedToIndex: Int? { // Last index where device was dragged to
didSet {
guard let draggedToIndex, let draggedEntryInitialIndex, let draggedEntry else { return }
var newArray = initialEntries
newArray.remove(at: draggedEntryInitialIndex)
newArray.insert(draggedEntry, at: draggedToIndex)
withAnimation {
entries = newArray
}
}
}
func indexForPoint(_ point: CGPoint) -> Int {
let x = max(0, min(Int((point.x - gridDimensions.origin.x) / gridDimensions.size.width), numberOfColumns - 1))
let y = max(0, min(Int((point.y - gridDimensions.origin.y) / gridDimensions.size.height), numberOfRows - 1))
return max(0, min(y * numberOfColumns + x, initialEntries.count - 1))
}
private func calculateGridDimensions() {
let allFrames = framesOfEntries.values
let rows = Dictionary(grouping: allFrames) { frame in
frame.origin.y
}
numberOfRows = rows.count
numberOfColumns = rows.values.map(\.count).max() ?? 0
let minX = allFrames.map(\.minX).min() ?? 0
let maxX = allFrames.map(\.maxX).max() ?? 0
let minY = allFrames.map(\.minY).min() ?? 0
let maxY = allFrames.map(\.maxY).max() ?? 0
let width = (maxX - minX) / CGFloat(numberOfColumns)
let height = (maxY - minY) / CGFloat(numberOfRows)
let origin = CGPoint(x: minX, y: minY)
let size = CGSize(width: width, height: height)
gridDimensions = CGRect(origin: origin, size: size)
}
}
struct Draggable<Entry: Identifiable>: ViewModifier {
#Binding
var originalEntries: [Entry]
let draggingManager: DraggingManager<Entry>
let entry: Entry
#ViewBuilder
func body(content: Content) -> some View {
if let entryIndex = originalEntries.firstIndex(where: { $0.id == entry.id }) {
let isBeingDragged = entryIndex == draggingManager.draggedEntryInitialIndex
let scale: CGFloat = isBeingDragged ? 1.1 : 1.0
content.background(
GeometryReader { geometry -> Color in
draggingManager.setFrameOfEntry(at: entryIndex, frame: geometry.frame(in: .named(draggingManager.coordinateSpaceID)))
return .clear
}
)
.scaleEffect(x: scale, y: scale)
.gesture(
dragGesture(
draggingManager: draggingManager,
entry: entry,
originalEntries: $originalEntries
)
)
}
else {
content
}
}
func dragGesture<Entry: Identifiable>(draggingManager: DraggingManager<Entry>, entry: Entry, originalEntries: Binding<[Entry]>) -> some Gesture {
DragGesture(coordinateSpace: .named(draggingManager.coordinateSpaceID))
.onChanged { value in
// Detect start of dragging
if draggingManager.draggedEntry?.id != entry.id {
withAnimation {
draggingManager.initialEntries = originalEntries.wrappedValue
draggingManager.draggedEntry = entry
}
}
let point = draggingManager.indexForPoint(value.location)
if point != draggingManager.draggedToIndex {
draggingManager.draggedToIndex = point
}
}
.onEnded { value in
withAnimation {
originalEntries.wrappedValue = draggingManager.entries!
draggingManager.entries = nil
draggingManager.draggedEntry = nil
draggingManager.draggedToIndex = nil
}
}
}
}
extension View {
// Allows item in LazyVGrid to be dragged between other items.
func draggable<Entry: Identifiable>(draggingManager: DraggingManager<Entry>, entry: Entry, originalEntries: Binding<[Entry]>) -> some View {
self.modifier(Draggable(originalEntries: originalEntries, draggingManager: draggingManager, entry: entry))
}
}
Now to use it in view you have to do few things:
Create a draggingManager that is a StateObject
Create a var that exposes either real array you are using or temporary array used by draggingManager during dragging.
Apply coordinateSpace from draggingManager to the container (LazyVGrid)
That way draggingManager only modifies its copy of the array during the process, and you can update the original after dragging is done.
struct VirtualMachineSettingsDevicesView: View {
#ObservedObject
var vmEntity: VMEntity
#StateObject
private var devicesDraggingManager = DraggingManager<VMDeviceInfo>()
// Currently displaying devices - different during dragging.
private var displayedDevices: [VMDeviceInfo] { devicesDraggingManager.entries ?? vmEntity.config.devices }
var body: some View {
Section("Devices") {
LazyVGrid(columns: [.init(.adaptive(minimum: 64, maximum: 64))], alignment: .leading, spacing: 20) {
Group {
ForEach(displayedDevices) { device in
Button(action: { configureDevice = device }) {
device.label
.draggable(
draggingManager: devicesDraggingManager,
entry: device,
originalEntries: $vmEntity.config.devices
)
}
}
Button(action: { configureNewDevice = true }, label: { Label("Add device", systemImage: "plus") })
}
.labelStyle(IconLabelStyle())
}
.coordinateSpace(name: devicesDraggingManager.coordinateSpaceID)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.buttonStyle(.plain)
}
}

SwiftUI List not updating inside pull to refresh view

I have a List inside a Custom Pull to refresh view. When the array inside SeeAllViewModel is updated, this list in the view is not being updated. Not only that but the counter is not being updated also. When I put the list outside this CustomScrollView it updates just fine. So I'm guessing there is something wrong with my CustomScrollView. Any idea why this is happening? Also I will provide the code for my ViewModel, just in case.
struct SeeAllView: View {
#ObservedObject var seeAllViewModel: SeeAllViewModel
var body: some View {
GeometryReader { geometry in
VStack {
Text("\(self.seeAllViewModel.category.items.count)") // updated on refresh
CustomScrollView(width: geometry.size.width, height: geometry.size.height, viewModel: self.seeAllViewModel) {
VStack {
Text("\(self.seeAllViewModel.category.items.count)") // not being updated
List {
ForEach(self.seeAllViewModel.category.items) { (item: Item) in
ItemRowView(itemViewModel: ItemViewModel(item: item))
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle(Text(self.seeAllViewModel.category.title.firstCapitalized))
}
}
Button(action: {
self.seeAllViewModel.refresh()
}) { Text("refresh")
}
}
}
}
}
CustomScrollView
struct CustomScrollView<Content: View, VM: LoadProtocol> : UIViewRepresentable {
var width : CGFloat
var height : CGFloat
let viewModel: VM
let content: () -> Content
func makeCoordinator() -> Coordinator {
Coordinator(self, viewModel: viewModel)
}
func makeUIView(context: Context) -> UIScrollView {
let control = UIScrollView()
control.refreshControl = UIRefreshControl()
control.refreshControl?.addTarget(context.coordinator, action: #selector(Coordinator.handleRefreshControl), for: .valueChanged)
let childView = UIHostingController(rootView: content())
childView.view.frame = CGRect(x: 0, y: 0, width: width, height: height)
control.addSubview(childView.view)
return control
}
func updateUIView(_ uiView: UIScrollView, context: Context) { }
class Coordinator: NSObject {
var control: CustomScrollView<Content, VM>
var viewModel: VM
init(_ control: CustomScrollView, viewModel: VM) {
self.control = control
self.viewModel = viewModel
}
#objc func handleRefreshControl(sender: UIRefreshControl) {
sender.endRefreshing()
viewModel.refresh()
}
}
}
SeeAllViewModel
class SeeAllViewModel: ObservableObject, LoadProtocol {
#Published var category: Category
init(category: Category) {
self.category = category
}
func refresh() {
//everytime you need more data fetched and on database updates to your snapshot this will be triggered
let query = self.category.query.start(afterDocument: self.category.lastDocumentSnapshot!).limit(to: 1)
query.addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error retrieving cities: \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
self.category.lastDocumentSnapshot = lastSnapshot
// Construct a new query starting after this document,
// Use the query for pagination.
self.category.items += snapshot.documents.map { document -> Item in
return Item(document: document)
}
}
}
}
It appears that dynamic property update cannot pass boundary of different hosting controller, so the solution is pass it (in this case observable object) inside explicitly.
Tested with Xcode 11.4 / iOS 13.4 on replicated code
So, custom view is constructed as
CustomScrollView(width: geometry.size.width, height: geometry.size.height, viewModel: self.seeAllViewModel) {
// Separate internals to subview and pass view modal there
RefreshInternalView(seeAllViewModel: self.seeAllViewModel)
}
and here is separated view, nothing special - just extracted everything from above
struct RefreshInternalView: View {
#ObservedObject var seeAllViewModel: SeeAllViewModel
var body: some View {
VStack {
Text("\(self.seeAllViewModel.category.items.count)") // not being updated
List {
ForEach(self.seeAllViewModel.category.items) { (item: Item) in
ItemRowView(itemViewModel: ItemViewModel(item: item))
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle(Text(self.seeAllViewModel.category.title.firstCapitalized))
}
}
}

Firebase image is empty after switching views in SwiftUI

I'm trying to display images from a list of objects stored in Firebase. Initially the image loads fine, but if I switch to a different view and return to the list view the image never loads again.
Gif of the described bug
The image data seems to be saved as expected on both load attempts:
here
Below is my code for the image loader, which uses a url to fetch the images from Firebase Storage, and the list row that contains the image.
ImageLoader.swift
import Foundation
import SwiftUI
import Firebase
import FirebaseFirestore
class ImageLoader: ObservableObject {
#Published var dataIsValid = false
var data:Data?
func loadImage(url: String) {
let imageRef = Storage.storage().reference(forURL: url)
imageRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
guard let data = data else { return }
DispatchQueue.main.async {
print(self.dataIsValid)
self.dataIsValid = true
self.data = data
}
}
}
func imageFromData() -> UIImage {
UIImage(data: self.data!)!
}
}
ListRow.swift
import SwiftUI
import Combine
struct EventRow: View {
#ObservedObject var imageLoader = ImageLoader()
var imageUrl: String
var body: some View {
HStack {
Image(uiImage: self.imageLoader.dataIsValid ? self.imageLoader.imageFromData() : UIImage())
.resizable()
.frame(width: 100.0, height: 140.0)
.background(Color.gray)
.clipShape(RoundedRectangle(cornerRadius: 5.0))
}
.onAppear {
self.imageLoader.loadImage(url: self.imageUrl)
}
}
}
The way I fixed this was by creating a custom ImageView and handling the image loading within this view. I figured this out by following this tutorial and realized that was the step I was missed. If anyone can explain why using the built-in SwiftUI Image() causes this issue I would really appreciate it.
ListRow.swift
import SwiftUI
struct ListRow: View {
var imageUrl: String
var body: some View {
HStack {
FBURLImage(url: imageUrl)
}
}
}
FBURLImage.swift
import SwiftUI
struct FBURLImage: View {
#ObservedObject var imageLoader: ImageLoader
init(url: String) {
imageLoader = ImageLoader()
imageLoader.loadImage(url: url)
}
var body: some View {
Image(uiImage:
imageLoader.data != nil ? UIImage(data: imageLoader.data!)! : UIImage())
.resizable()
.frame(width: 100.0, height: 140.0)
.background(Color.gray)
.clipShape(RoundedRectangle(cornerRadius: 5.0))
}
}
ImageLoader.swift
import Foundation
import SwiftUI
import Firebase
import FirebaseFirestore
class ImageLoader: ObservableObject {
#Published var data: Data?
func loadImage(url: String) {
let imageRef = Storage.storage().reference(forURL: url)
imageRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
}
}
}
}

Resources