I use the "drag" and "paste" functions of multiple imagesViews to multiple imageViews in the same application. As the function "drag" allows to know the imageView of origin, does the function "drop" makes it possible to know the imageView "target" at the end of the "drag" (coordinates? Tag? ...). Thank you for any suggestions.
// Right here, itemsForBeginning session: UIDragSession) allows to retrieve the imageView of origin.
func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
let touchPoint = session.location(in: self.view)
print("\(touchPoint.x)")
print("\(touchPoint.y)")
//...
guard let image = viewTaped!.image else { return [] }
let item = UIDragItem(itemProvider: NSItemProvider(object: image))
return [item]
}
// But with the function "paste", Swift knows the target, but can I and how to recover the informations of imageView_yyy? (coordinates? Tag? ...).
override func paste(itemProviders: [NSItemProvider]) {
_ = itemProviders.first?.loadObject(ofClass: UIImage.self, completionHandler: { (image: NSItemProviderReading?, error: Error?) in
DispatchQueue.main.async {
self.imageView_yyy.image = image as? UIImage
}
})
}
You need to retain the loading callback. By assigning it to _ you are telling Swift that it's trash that can be discarded immediately
// This retains the progress
var pasteProgress: Progress?
override func paste(itemProviders: [NSItemProvider]) {
pasteProgress = itemProviders.first?.loadObject(ofClass: UIImage.self, completionHandler: { (image: NSItemProviderReading?, error: Error?) in
DispatchQueue.main.async {
self.imageView_yyy.image = image as? UIImage
}
})
}
Related
So I am trying to add a multipeer element to this Sticky Note app from Apple's own Sample Code. Link to Sample Code page There are several examples of multipeer ARKit apps but the problem here is, with the app I am working from, the Sticky Note is NOT a 3D element but
For the purposes of this sample app, the sticky note entity has no geometry and thus, no appearance. Its anchor provides a 3D location only, and itʼs the sticky noteʼs screen-space annotation that has an appearance. To display it, you define the sticky noteʼs annotation. Following RealityKitʼs entity-component model, design a component that houses the annotation, which in this case is a view. See ScreenSpaceComponent.
I have been trying to use the example of multipeer apps in ARthat use the ARKit element with 3D elements stored as either assets [the "Collaborative Session" example ] or using ModelEntity geometry [the Creating a Multiuser AR Experience example ] but I haven't been successful in translating this app which uses screen space only.
I am able to get the message on the screen that it's connected to a peer, but that is as far as it goes. It will not render the notes on the second phone. I am burned out from all the attempts of making it work:(
One alternative is to forget about the notes being tethered to the screen space, and recreating this as a regular 3D space and 2D geometry thing using SpriteKit.
The system will not render the apps sticky notes on the other phone. I know there is a way around this, but I have been trying for days and haven't been able to do it.
I have been testing this using 2 phones.
I have
Added the info on the p.list
Added the Multipeer Session file
Added the code on the ViewController file related to multipeer
Added code to the arGestureSetUp() extension file which has the rendering info for the sticky notes.
What works: I can see the notes on both phones, and I get the messages saying that a peer has joined. What I can't do is view the
other user's notes like I would in a regular 3D ARkit app. It will not
render.
This is what I have added to the insertNewSticky function
func insertNewSticky(_ sender: UITapGestureRecognizer)
from one of the other examples:
let anchor = ARAnchor(name: "Anchor for object placement", transform: raycastResult.worldTransform)
arView.session.add(anchor: anchor)
Below is the full code for the Gesture Recognizer Setup
import UIKit
import ARKit
extension ViewController {
// MARK: - Gesture recognizer setup
// - Tag: AddViewTapGesture
func arViewGestureSetup() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tappedOnARView))
arView.addGestureRecognizer(tapGesture)
let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipedDownOnARView))
swipeGesture.direction = .down
arView.addGestureRecognizer(swipeGesture)
}
func stickyNoteGestureSetup(_ note: StickyNoteEntity) {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panOnStickyView))
note.view?.addGestureRecognizer(panGesture)
let tapOnStickyView = UITapGestureRecognizer(target: self, action: #selector(tappedOnStickyView(_:)))
note.view?.addGestureRecognizer(tapOnStickyView)
}
// MARK: - Gesture recognizer callbacks
/// Tap gesture input handler.
/// - Tag: TapHandler
#objc
func tappedOnARView(_ sender: UITapGestureRecognizer) {
// Ignore the tap if the user is editing a sticky note.
for note in stickyNotes where note.isEditing { return }
// Create a new sticky note at the tap location.
insertNewSticky(sender)
}
/**
Hit test the feature point cloud and use any hit as the position of a new StickyNote. Otherwise, display a tip.
- Tag: ScreenSpaceViewInsertionTag
*/
func insertNewSticky(_ sender: UITapGestureRecognizer) {
// Get the user's tap screen location.
let touchLocation = sender.location(in: arView)
// Cast a ray to check for its intersection with any planes.
guard let raycastResult = arView.raycast(from: touchLocation, allowing: .estimatedPlane, alignment: .any).first
else {
messageLabel.displayMessage("No surface detected, try getting closer.", duration: 2.0)
return
}
// Create a new sticky note positioned at the hit test result's world position.
let frame = CGRect(origin: touchLocation, size: CGSize(width: 200, height: 200))
let note = StickyNoteEntity(frame: frame, worldTransform: raycastResult.worldTransform)
// Center the sticky note's view on the tap's screen location.
note.setPositionCenter(touchLocation)
// Add the sticky note to the scene's entity hierarchy.
arView.scene.addAnchor(note)
// Add the sticky note's view to the view hierarchy.
guard let stickyView = note.view else { return }
arView.insertSubview(stickyView, belowSubview: trashZone)
// Enable gestures on the sticky note.
stickyNoteGestureSetup(note)
// Save a reference to the sticky note.
stickyNotes.append(note)
// Volunteer to handle text view callbacks.
stickyView.textView.delegate = self
let anchor = ARAnchor(name: "Anchor for object placement", transform: raycastResult.worldTransform)
arView.session.add(anchor: anchor)
}
/// Dismisses the keyboard.
#objc
func swipedDownOnARView(_ sender: UISwipeGestureRecognizer) {
dismissKeyboard()
}
fileprivate func dismissKeyboard() {
for note in stickyNotes {
guard let textView = note.view?.textView else { continue }
if textView.isFirstResponder {
textView.resignFirstResponder()
return
}
}
}
#objc
func tappedOnStickyView(_ sender: UITapGestureRecognizer) {
guard let stickyView = sender.view as? StickyNoteView else { return }
stickyView.textView.becomeFirstResponder()
}
//- Tag: PanOnStickyView
fileprivate func panStickyNote(_ sender: UIPanGestureRecognizer, _ stickyView: StickyNoteView, _ panLocation: CGPoint) {
messageLabel.isHidden = true
let feedbackGenerator = UIImpactFeedbackGenerator()
switch sender.state {
case .began:
// Prepare the taptic engine to reduce latency in delivering feedback.
feedbackGenerator.prepare()
// Drag if the gesture is beginning.
stickyView.stickyNote.isDragging = true
// Save offsets to implement smooth panning.
guard let frame = sender.view?.frame else { return }
stickyView.xOffset = panLocation.x - frame.origin.x
stickyView.yOffset = panLocation.y - frame.origin.y
// Fade in the widget that's used to delete sticky notes.
trashZone.fadeIn(duration: 0.4)
case .ended:
// Stop dragging if the gesture is ending.
stickyView.stickyNote.isDragging = false
// Delete the sticky note if the gesture ended on the trash widget.
if stickyView.isInTrashZone {
deleteStickyNote(stickyView.stickyNote)
// ...
} else {
attemptRepositioning(stickyView)
}
// Fades out the widget that's used to delete sticky notes when there are no sticky notes currently being dragged.
if !stickyNotes.contains(where: { $0.isDragging }) {
trashZone.fadeOut(duration: 0.2)
}
default:
// Update the sticky note's screen position based on the pan location, and initial offset.
stickyView.frame.origin.x = panLocation.x - stickyView.xOffset
stickyView.frame.origin.y = panLocation.y - stickyView.yOffset
// Give feedback whenever the pan location is near the widget used to delete sticky notes.
trashZoneThresholdFeedback(sender, feedbackGenerator)
}
}
/// Sticky note pan-gesture handler.
/// - Tag: PanHandler
#objc
func panOnStickyView(_ sender: UIPanGestureRecognizer) {
guard let stickyView = sender.view as? StickyNoteView else { return }
let panLocation = sender.location(in: arView)
// Ignore the pan if any StickyViews are being edited.
for note in stickyNotes where note.isEditing { return }
panStickyNote(sender, stickyView, panLocation)
}
func deleteStickyNote(_ note: StickyNoteEntity) {
guard let index = stickyNotes.firstIndex(of: note) else { return }
note.removeFromParent()
stickyNotes.remove(at: index)
note.view?.removeFromSuperview()
note.view?.isInTrashZone = false
}
/// - Tag: AttemptRepositioning
fileprivate func attemptRepositioning(_ stickyView: StickyNoteView) {
// Conducts a ray-cast for feature points using the panned position of the StickyNoteView
let point = CGPoint(x: stickyView.frame.midX, y: stickyView.frame.midY)
if let result = arView.raycast(from: point, allowing: .estimatedPlane, alignment: .any).first {
stickyView.stickyNote.transform.matrix = result.worldTransform
} else {
messageLabel.displayMessage("No surface detected, unable to reposition note.", duration: 2.0)
stickyView.stickyNote.shouldAnimate = true
}
}
fileprivate func trashZoneThresholdFeedback(_ sender: UIPanGestureRecognizer, _ feedbackGenerator: UIImpactFeedbackGenerator) {
guard let stickyView = sender.view as? StickyNoteView else { return }
let panLocation = sender.location(in: trashZone)
if trashZone.frame.contains(panLocation), !stickyView.isInTrashZone {
stickyView.isInTrashZone = true
feedbackGenerator.impactOccurred()
} else if !trashZone.frame.contains(panLocation), stickyView.isInTrashZone {
stickyView.isInTrashZone = false
feedbackGenerator.impactOccurred()
}
}
#objc
func tappedReset(_ sender: UIButton) {
reset()
}
}
and this is the full code for the ViewController file
/*
See LICENSE folder for this sample’s licensing information.
Abstract:
Main view controller for the AR experience.
*/
import UIKit
import RealityKit
import Combine
import ARKit
import MultipeerConnectivity
class ViewController: UIViewController, ARSessionDelegate {
// MARK: - Class variable declarations
#IBOutlet var arView: ARView!
#IBOutlet weak var messageLabel: MessageLabel!
var trashZone: GradientView!
var shadeView: UIView!
var resetButton: UIButton!
var keyboardHeight: CGFloat!
var stickyNotes = [StickyNoteEntity]()
var subscription: Cancellable!
//added Sat May 28 5:12pm
var multipeerSession: MultipeerSession?
// end of added Sat May 28 5:12pm
//added Sat May 28 5:12pm
// A dictionary to map MultiPeer IDs to ARSession ID's.
// This is useful for keeping track of which peer created which ARAnchors.
var peerSessionIDs = [MCPeerID: String]()
var sessionIDObservation: NSKeyValueObservation?
var configuration: ARWorldTrackingConfiguration?
// end of added Sat May 28 5:12pm
// MARK: - View Controller Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
subscription = arView.scene.subscribe(to: SceneEvents.Update.self) { [unowned self] in
self.updateScene(on: $0)
}
arViewGestureSetup()
overlayUISetup()
arView.session.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Add observer to the keyboardWillShowNotification to get the height of the keyboard every time it is shown
let notificationName = UIResponder.keyboardWillShowNotification
let selector = #selector(keyboardIsPoppingUp(notification:))
NotificationCenter.default.addObserver(self, selector: selector, name: notificationName, object: nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
arView.session.delegate = self
// Prevent the screen from being dimmed to avoid interuppting the AR experience.
UIApplication.shared.isIdleTimerDisabled = true
// Turn off ARView's automatically-configured session
// to create and set up your own configuration.
arView.automaticallyConfigureSession = false
configuration = ARWorldTrackingConfiguration()
// Enable a collaborative session.
configuration?.isCollaborationEnabled = true
// Enable realistic reflections.
configuration?.environmentTexturing = .automatic
// Begin the session.
arView.session.run(configuration!)
// Use key-value observation to monitor your ARSession's identifier.
sessionIDObservation = observe(\.arView.session.identifier, options: [.new]) { object, change in
print("SessionID changed to: \(change.newValue!)")
// Tell all other peers about your ARSession's changed ID, so
// that they can keep track of which ARAnchors are yours.
guard let multipeerSession = self.multipeerSession else { return }
self.sendARSessionIDTo(peers: multipeerSession.connectedPeers)
}
// Start looking for other players via MultiPeerConnectivity.
multipeerSession = MultipeerSession(receivedDataHandler: receivedData, peerJoinedHandler:
peerJoined, peerLeftHandler: peerLeft, peerDiscoveredHandler: peerDiscovered)
//arView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:))))
messageLabel.displayMessage("Tap the screen to place cubes.\nInvite others to launch this app to join you.", duration: 60.0)
}
//peerDiscovered
func peerDiscovered(_ peer: MCPeerID) -> Bool {
guard let multipeerSession = multipeerSession else { return false }
if multipeerSession.connectedPeers.count > 3 {
// Do not accept more than four users in the experience.
messageLabel.displayMessage("A fifth peer wants to join the experience.\nThis app is limited to four users.", duration: 6.0)
return false
} else {
return true
}
}
// end of added Sat May 28 5:12pm
/// - Tag: PeerJoined
// added Sat May 28 5:12pm
func peerJoined(_ peer: MCPeerID) {
messageLabel.displayMessage("""
A peer has joined the experience.
Hold the phones next to each other.
""", duration: 6.0)
// Provide your session ID to the new user so they can keep track of your anchors.
sendARSessionIDTo(peers: [peer])
}
// end of added Sat May 28 5:12pm
// added Sat May 28 5:12pm
func peerLeft(_ peer: MCPeerID) {
messageLabel.displayMessage("A peer has left the shared experience.")
// Remove all ARAnchors associated with the peer that just left the experience.
if let sessionID = peerSessionIDs[peer] {
removeAllAnchorsOriginatingFromARSessionWithID(sessionID)
peerSessionIDs.removeValue(forKey: peer)
}
}
// end of added Sat May 28 5:12pm
//added Sat May 28 5:12pm
func receivedData(_ data: Data, from peer: MCPeerID) {
if let collaborationData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ARSession.CollaborationData.self, from: data) {
arView.session.update(with: collaborationData)
return
}
// ...
let sessionIDCommandString = "SessionID:"
if let commandString = String(data: data, encoding: .utf8), commandString.starts(with: sessionIDCommandString) {
let newSessionID = String(commandString[commandString.index(commandString.startIndex,
offsetBy: sessionIDCommandString.count)...])
// If this peer was using a different session ID before, remove all its associated anchors.
// This will remove the old participant anchor and its geometry from the scene.
if let oldSessionID = peerSessionIDs[peer] {
removeAllAnchorsOriginatingFromARSessionWithID(oldSessionID)
}
peerSessionIDs[peer] = newSessionID
}
}
// end of added Sat May 28 5:12pm
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
messageLabel.displayMessage("Established joint experience with a peer.")
// ...
}
func updateScene(on event: SceneEvents.Update) {
let notesToUpdate = stickyNotes.compactMap { !$0.isEditing && !$0.isDragging ? $0 : nil }
for note in notesToUpdate {
// Gets the 2D screen point of the 3D world point.
guard let projectedPoint = arView.project(note.position) else { return }
// Calculates whether the note can be currently visible by the camera.
let cameraForward = arView.cameraTransform.matrix.columns.2.xyz
let cameraToWorldPointDirection = normalize(note.transform.translation - arView.cameraTransform.translation)
let dotProduct = dot(cameraForward, cameraToWorldPointDirection)
let isVisible = dotProduct < 0
// Updates the screen position of the note based on its visibility
note.projection = Projection(projectedPoint: projectedPoint, isVisible: isVisible)
note.updateScreenPosition()
}
}
func reset() {
guard let configuration = arView.session.configuration else { return }
arView.session.run(configuration, options: .removeExistingAnchors)
for note in stickyNotes {
deleteStickyNote(note)
}
}
func session(_ session: ARSession, didFailWithError error: Error) {
guard error is ARError else { return }
let errorWithInfo = error as NSError
let messages = [
errorWithInfo.localizedDescription,
errorWithInfo.localizedFailureReason,
errorWithInfo.localizedRecoverySuggestion
]
let errorMessage = messages.compactMap({ $0 }).joined(separator: "\n")
DispatchQueue.main.async {
// Present an alert informing about the error that has occurred.
let alertController = UIAlertController(title: "The AR session failed.", message: errorMessage, preferredStyle: .alert)
let restartAction = UIAlertAction(title: "Restart Session", style: .default) { _ in
alertController.dismiss(animated: true, completion: nil)
self.reset()
}
alertController.addAction(restartAction)
self.present(alertController, animated: true, completion: nil)
}
}
override var prefersStatusBarHidden: Bool {
return true
}
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
private func sendARSessionIDTo(peers: [MCPeerID]) {
guard let multipeerSession = multipeerSession else { return }
let idString = arView.session.identifier.uuidString
let command = "SessionID:" + idString
if let commandData = command.data(using: .utf8) {
multipeerSession.sendToPeers(commandData, reliably: true, peers: peers)
}
}
private func removeAllAnchorsOriginatingFromARSessionWithID(_ identifier: String) {
guard let frame = arView.session.currentFrame else { return }
for anchor in frame.anchors {
guard let anchorSessionID = anchor.sessionIdentifier else { continue }
if anchorSessionID.uuidString == identifier {
arView.session.remove(anchor: anchor)
}
}
}
}
Update: I spoke to a Staff Engineer on Apple's RealityKit team who explained to me that what I was trying to accomplish is not feasible because the note had an embedded subclass that is not 'codable' as per Swift's Codable Protocol
I will have to rebuild the note differently than the example i had been working with to ensure it fits within the Codable protocol which will then ensure the data can travel across the network via Multipeer Connectivity Framework.
Let say I want to add a new item in Playlist entity of CoreData and put it in background thread and push back it to main thread then reflect it on tableView. Well, that code is working fine without background thread implementation.
But when I apply below background kinda code, after createPlaylist is executed, tableView becomes to empty space(without any items showed up), though print(self?.playlists.count) gives the correct rows count.
When dealing with GCD, I put some heavy code in background queue and push back to main queue for UI update in same closure. But it seems not worked here, I google a quit of time but still cannot anchor the issue.
import UIKit
import CoreData
class PlayListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var songs = [Song]()
var position = 0
let container = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
private var playlists = [Playlist]()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 1, alpha: 1)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "playlistCell")
configureLayout()
getAllPlaylists()
}
// MARK: Core data functions
func getAllPlaylists() {
do {
let context = self.container.viewContext
playlists = try context.fetch(Playlist.fetchRequest())
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
}
print("count: \(playlists.count)")
// printThreadStats()
} catch {
print("getAllPlaylists failed, \(error)")
}
}
func createPlaylist(name: String) {
container.performBackgroundTask { context in
let newPlaylist = Playlist(context: context)
newPlaylist.name = name
do {
try context.save()
self.playlists = try context.fetch(Playlist.fetchRequest())
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
print(self?.playlists.count)
}
} catch {
print("Create playlist failed, \(error)")
}
}
}
// MARK: tableView data source implementation
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return playlists.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let playlist = playlists[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "playlistCell", for: indexPath)
cell.textLabel?.text = playlist.name
// cell.detailTextLabel?.text = "2 songs"
return cell
}
auto generated fetchRequest and Property defining
import Foundation
import CoreData
extension Playlist {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Playlist> {
return NSFetchRequest<Playlist>(entityName: "Playlist")
}
#NSManaged public var name: String?
}
For the first call of func getAllPlaylists(), you are calling this on main thread from viewDidLoad(). So following lines are executed on main thread.
let context = self.container.viewContext
playlists = try context.fetch(Playlist.fetchRequest())
Next time inside the createPlaylist method, you are performing add playlist task in background context (not on main thread). So following lines are executed on background thread.
self.playlists = try context.fetch(Playlist.fetchRequest())
Also note that, first time we are using viewContext to fetch playlists and second time a backgroundContext. This mix up causes the UI to not show expected result.
I think these two methods could be simplified to -
func getAllPlaylists() {
do {
let context = self.container.viewContext
playlists = try context.fetch(Playlist.fetchRequest())
// DispatchQueue.main.async not necessary, we are already on main thread
self.tableView.reloadData()
print("count: \(playlists.count)")
} catch {
print("getAllPlaylists failed, \(error)")
}
}
func createPlaylist(name: String) {
container.performBackgroundTask { context in
let newPlaylist = Playlist(context: context)
newPlaylist.name = name
do {
try context.save()
DispatchQueue.main.async { [weak self] in
self?.getAllPlaylists()
}
} catch {
print("Create playlist failed, \(error)")
}
}
}
After 5 hours' digging today, I found the solution. I'd like put my solution and code below, because the stuff about "How to pass NSManagedObject instances between queues in CoreData" is quite rare && fragmentation, not friendly to newbies of SWIFT.
The thing is we want to do heavy CoreData task on background thread and reflect the changes in UI on foreground(main thread). Generally, we need to create a private queue context(privateMOC) and perform the heavy CoreData task on this private context, see below code.
For reuse purpose, I put CoreData functions separately.
import UIKit
import CoreData
struct CoreDataManager {
let managedObjectContext: NSManagedObjectContext
private let privateMOC = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let coreDataStack = CoreDataStack()
static let shared = CoreDataManager()
private init() {
self.managedObjectContext = coreDataStack.persistentContainer.viewContext
privateMOC.parent = self.managedObjectContext
}
func fetchAllPlaylists(completion: #escaping ([Playlist]?) -> Void) {
privateMOC.performAndWait {
do {
let playlists: [Playlist] = try privateMOC.fetch(Playlist.fetchRequest())
print("getAllPlaylists")
printThreadStats()
print("count: \(playlists.count)")
completion(playlists)
} catch {
print("fetchAllPlaylists failed, \(error), \(error.localizedDescription)")
completion(nil)
}
}
}
func createPlaylist(name: String) {
privateMOC.performAndWait {
let newPlaylist = Playlist(context: privateMOC)
newPlaylist.name = name
synchronize()
}
}
func deletePlaylist(playlist: Playlist) {
privateMOC.performAndWait {
privateMOC.delete(playlist)
synchronize()
}
}
func updatePlaylist(playlist: Playlist, newName: String) {
...
}
func removeAllFromEntity(entityName: String) {
...
}
func synchronize() {
do {
// We call save on the private context, which moves all of the changes into the main queue context without blocking the main queue.
try privateMOC.save()
managedObjectContext.performAndWait {
do {
try managedObjectContext.save()
} catch {
print("Could not synchonize data. \(error), \(error.localizedDescription)")
}
}
} catch {
print("Could not synchonize data. \(error), \(error.localizedDescription)")
}
}
func printThreadStats() {
if Thread.isMainThread {
print("on the main thread")
} else {
print("off the main thread")
}
}
}
And Apple has a nice template for it Using a Private Queue to Support Concurrency
Another helpful link: Best practice: Core Data Concurrency
The real tricky thing is how to connect it with your view or viewController, the really implementation. See below ViewController code.
// 1
override func viewDidLoad() {
super.viewDidLoad()
// some layout code
// execute on background thread
DispatchQueue.global().async { [weak self] in
self?.fetchAndReload()
}
}
// 2
private func fetchAndReload() {
CoreDataManager.shared.fetchAllPlaylists(completion: { playlists in
guard let playlists = playlists else { return }
self.playlists = playlists
})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
// 3
#objc func createNewPlaylist(_ sender: Any?) {
let ac = UIAlertController(title: "Create New Playlist", message: "", preferredStyle: .alert)
ac.addTextField { textField in
textField.placeholder = "input your desired name"
}
ac.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
ac.addAction(UIAlertAction(title: "Done", style: .default, handler: { [weak self] _ in
guard let textField = ac.textFields?.first, let newName = textField.text, !newName.isEmpty else { return }
// check duplicate
if let playlists = self?.playlists {
if playlists.contains(where: { playlist in
playlist.name == newName
}) {
self?.duplicateNameAlert()
return
}
}
DispatchQueue.global().async { [weak self] in
CoreDataManager.shared.createPlaylist(name: newName)
self?.fetchAndReload()
}
}))
present(ac, animated: true)
}
Let me break down it:
First in viewDidload, we call fetchAndReload on background thread.
In fetchAndReload function, it brings out all the playlist(returns data with completion handler) and refresh the table on main thread.
We call createPlaylist(name: newName) in background thread and reload the table on main thread again.
Well, this is the 1st time I deal with Multi-threading in CoreData, if there is any mistake, please indicate it. Allright, that's it! Hope it could help someone.
I'm trying to share a record with other users in CloudKit but I keep getting an error. When I tap one of the items/records on the table I'm presented with the UICloudSharingController and I can see the iMessage app icon, but when I tap on it I get an error and the UICloudSharingController disappears, the funny thing is that even after the error I can still continue using the app.
Here is what I have.
Code
var items = [CKRecord]()
var itemName: String?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = items[indexPath.row]
let share = CKShare(rootRecord: item)
if let itemName = item.object(forKey: "name") as? String {
self.itemName = item.object(forKey: "name") as? String
share[CKShareTitleKey] = "Sharing \(itemName)" as CKRecordValue?
} else {
share[CKShareTitleKey] = "" as CKRecordValue?
self.itemName = "item"
}
share[CKShareTypeKey] = "bundle.Identifier.Here" as CKRecordValue
prepareToShare(share: share, record: item)
}
private func prepareToShare(share: CKShare, record: CKRecord){
let sharingViewController = UICloudSharingController(preparationHandler: {(UICloudSharingController, handler: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
let modRecordsList = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: nil)
modRecordsList.modifyRecordsCompletionBlock = {
(record, recordID, error) in
handler(share, CKContainer.default(), error)
}
CKContainer.default().privateCloudDatabase.add(modRecordsList)
})
sharingViewController.delegate = self
sharingViewController.availablePermissions = [.allowPrivate]
self.navigationController?.present(sharingViewController, animated:true, completion:nil)
}
// Delegate Methods:
func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
print("saved successfully")
}
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
print("failed to save: \(error.localizedDescription)")// the error is generated in this method
}
func itemThumbnailData(for csc: UICloudSharingController) -> Data? {
return nil //You can set a hero image in your share sheet. Nil uses the default.
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return self.itemName
}
ERROR
Failed to modify some records
Here is what I see...
Any idea what could be wrong?
EDIT:
By the way, the error is generated in the cloudSharingController failedToSaveShareWithError method.
Looks like you're trying to share in the default zone which isn't allowed. From the docs here
Sharing is only supported in zones with the
CKRecordZoneCapabilitySharing capability. The default zone does not
support sharing.
So you should set up a custom zone in your private database, and save your share and records there.
Possibly it is from the way you're trying to instantiate the UICloudSharingController? I cribbed my directly from the docs and it works:
let cloudSharingController = UICloudSharingController { [weak self] (controller, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
guard let `self` = self else {
return
}
self.share(rootRecord: rootRecord, completion: completion)
}
If that's not the problem it's something with either one or both of the records themselves. If you upload the record without trying to share it, does it work?
EDIT TO ADD:
What is the CKShareTypeKey? I don't use that in my app. Also I set my system fields differently:
share?[CKShare.SystemFieldKey.title] = "Something"
Try to add this to your info.plist
<key>CKSharingSupported</key>
<true/>
I'm using marmelroy/Zip framework to zip/unzip files in my project, and JGProgressHUD to show the progress of the operation.
I'm able to see the HUD if I try to show it from the ViewDidLoad method, but if I use it in the closure associated to the progress feature of the quickZipFiles method (like in the code sample), the hud is shown just at the end of the operation.
I guess this could be related to a timing issue, but since I'm not too much into completion handlers, closures and GDC (threads, asynchronous tasks, etc.) I would like to ask for a suggestion.
Any ideas?
// In my class properties declaration
var hud = JGProgressHUD(style: .dark)
// In my ViewDidLoad
self.hud.indicatorView = JGProgressHUDPieIndicatorView()
self.hud.backgroundColor = UIColor(white: 0, alpha: 0.7)
// In my method
do {
self.hud.textLabel.text = NSLocalizedString("Zipping files...", comment: "Zipping File Message")
self.hud.detailTextLabel.text = "0%"
if !(self.hud.isVisible) {
self.hud.show(in: self.view)
}
zipURL = try Zip.quickZipFiles(documentsList, fileName: "documents", progress: { (progress) -> () in
let progressMessage = "\(round(progress*100))%"
print(progressMessage)
self.hud.setProgress(Float(progress), animated: true)
self.hud.textLabel.text = NSLocalizedString("Zipping files...", comment: "Zipping File Message")
self.hud.detailTextLabel.text = progressMessage
if (progress == 1.0) {
self.hud.dismiss()
}
})
} catch {
print("Error while creating zip...")
}
ZIP Foundation comes with built-in support for progress reporting and cancelation.
So if you can switch ZIP library, this might be a better fit for your project. (Full disclosure: I am the author of this library)
Here's some sample code that shows how you can zip a directory and display operation progress on a JGProgressHUD. I just zip the main bundle's directory here as example.
The ZIP operation is dispatched on a separate thread so that your main thread can update the UI. The progress var is a default Foundation (NS)Progress object which reports changes via KVO.
import UIKit
import ZIPFoundation
import JGProgressHUD
class ViewController: UIViewController {
#IBOutlet weak var progressLabel: UILabel!
var indicator = JGProgressHUD()
var isObservingProgress = false
var progressViewKVOContext = 0
#objc
var progress: Progress?
func startObservingProgress()
{
guard !isObservingProgress else { return }
progress = Progress()
progress?.completedUnitCount = 0
self.indicator.progress = 0.0
self.addObserver(self, forKeyPath: #keyPath(progress.fractionCompleted), options: [.new], context: &progressViewKVOContext)
isObservingProgress = true
}
func stopObservingProgress()
{
guard isObservingProgress else { return }
self.removeObserver(self, forKeyPath: #keyPath(progress.fractionCompleted))
isObservingProgress = false
self.progress = nil
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(progress.fractionCompleted) {
DispatchQueue.main.async {
self.indicator.progress = Float(self.progress?.fractionCompleted ?? 0.0)
if let progressDescription = self.progress?.localizedDescription {
self.progressLabel.text = progressDescription
}
if self.progress?.isFinished == true {
self.progressLabel.text = ""
self.indicator.progress = 0.0
}
}
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
#IBAction func cancel(_ sender: Any) {
self.progress?.cancel()
}
#IBAction func createFullArchive(_ sender: Any) {
let directoryURL = Bundle.main.bundleURL
let tempArchiveURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString).appendingPathExtension("zip")
self.startObservingProgress()
DispatchQueue.global().async {
try? FileManager.default.zipItem(at: directoryURL, to: tempArchiveURL, progress: self.progress)
self.stopObservingProgress()
}
}
}
Looking at the implementation of the zip library, all of the zipping/unzipping and the calls to the progress handlers are being done on the same thread. The example shown on the home page isn't very good and can't be used as-is if you wish to update the UI with a progress indicator while zipping or unzipping.
The solution is to perform the zipping/unzipping in the background and in the progress block, update the UI on the main queue.
Assuming you are calling your posted code from the main queue (in response to the user performing some action), you should update your code as follows:
// In my class properties declaration
var hud = JGProgressHUD(style: .dark)
// In my ViewDidLoad
self.hud.indicatorView = JGProgressHUDPieIndicatorView()
self.hud.backgroundColor = UIColor(white: 0, alpha: 0.7)
self.hud.textLabel.text = NSLocalizedString("Zipping files...", comment: "Zipping File Message")
self.hud.detailTextLabel.text = "0%"
if !(self.hud.isVisible) {
self.hud.show(in: self.view)
}
DispatchQueue.global().async {
defer {
DispatchQueue.main.async {
self.hud.dismiss()
}
}
do {
zipURL = try Zip.quickZipFiles(documentsList, fileName: "documents", progress: { (progress) -> () in
DispatchQueue.main.async {
let progressMessage = "\(round(progress*100))%"
print(progressMessage)
self.hud.setProgress(Float(progress), animated: true)
self.hud.textLabel.text = NSLocalizedString("Zipping files...", comment: "Zipping File Message")
self.hud.detailTextLabel.text = progressMessage
}
})
} catch {
print("Error while creating zip...")
}
}
My Game Center Authentication is not working. When I build and run, it won't show my username.. has signed in. Also, when I try to add my score I get a screen that says "no data availible". Heres my code.
override func viewDidLoad() {
super.viewDidLoad()
gcAuthPlayer()
}
#IBAction func GCButton(sender: AnyObject) {
saveHighScore(GameScene().highScoreNumer)
showLeaderBoard()
if GameScene().currentScore > GameScene().highScoreNumer{
saveHighScore(GameScene().currentScore)
}
}
func showLeaderBoard(){
let viewController = self.view.window?.rootViewController
let gcvc = GKGameCenterViewController()
gcvc.gameCenterDelegate = self
viewController?.presentViewController(gcvc, animated: true, completion: nil)
}
func saveHighScore(number: Int){
if GKLocalPlayer.localPlayer().authenticated{
let scoreReporter = GKScore(leaderboardIdentifier: "myleaderboard")
scoreReporter.value = Int64(number)
let scoreArray : [GKScore] = [scoreReporter]
GKScore.reportScores(scoreArray, withCompletionHandler: nil)
}
}
func gcAuthPlayer(){
let localPlayer = GKLocalPlayer.localPlayer()
localPlayer.authenticateHandler = {
(view, error) in
if view != nil{
self.presentViewController(view!, animated: true, completion: nil)
}else{
print(GKLocalPlayer.localPlayer().authenticated)
}
}
}
func gameCenterViewControllerDidFinish(gameCenterViewController: GKGameCenterViewController) {
gameCenterViewController.dismissViewControllerAnimated(true, completion: nil)
}
This code makes no sense
saveHighScore(GameScene().highScoreNumer)
showLeaderBoard()
if GameScene().currentScore > GameScene().highScoreNumer{
saveHighScor
You are creating a new instance of GameScene everytime you try to update the score and therefore your score is nil
I would need to see some more code but for now you need to change the score property in your game scene. For example make it a static property so you can get it in other classes.
class GameScene: SKScene {
static var currentScore = 0
static var highscoreNumber = 0
}
Than in your Scenes or ViewController you can get it like so
GameScene.currentScore = 5
GameScene.highscoreNumber = 5
Just remember that you have to reset the score to 0 everytime you restart your gameScene because it a static property.
GameScene.currentScore = 0
GameScene.highscoreNumber = 0
Than your code to post the score should look like this
saveHighScore(GameScene.highScoreNumer)
showLeaderBoard()
if GameScene.currentScore > GameScene.highScoreNumer{
saveHighScor
Your score reporting code should also handle the error and actually do the completion handler. So change it to something like this.
/// Save leaderboard progress
func reportLeaderboardProgress(value: Int, leaderboardID: String) {
let scoreReporter = GKScore(leaderboardIdentifier: leaderboardID)
scoreReporter.value = Int64(value)
GKScore.reportScores([scoreReporter]) { error in // Trailing Closure syntax
if let error = error {
print(error.localizedDescription)
return
}
print("Reported leaderboard progress \(value) to leaderboardID \(leaderboardID)")
}
}
It is also a good idea to move that code into another class to keep your overall code cleaner and more reusable.
For a nice and simple example check this helper on gitHub.
https://github.com/jackcook/GCHelper
Let me know how it goes.