scheduledTimer in swift 3 - timer

Is this the correct way to fire off a repeating timer in swift 3?
Note that this code has to run on iOS 8 as well.
Does the userInfo need to be a dictionary?
Can this code be improved?
```
class ABContactManager : NSObject {
func waitForContactsToLoad(_ onSuccess: #escaping (() -> ())) {
Timer.scheduledTimer(
timeInterval: 0.5,
target: self,
selector: #selector(ABContactManager.timerFired(timer:)),
userInfo: ["callback" : onSuccess],
repeats: true)
}
func timerFired(timer: Timer) {
logInfo("keep polling contacts")
if let userInfo = timer.userInfo as? [String : Any] {
if let onSuccess = userInfo["callback"] as? (() -> ()) {
if self.loadingStatus == .loaded {
logInfo("stop polling -- loaded contacts issue callback")
onSuccess()
timer.invalidate()
}
}
}
}
}
```

Related

My firebase data is geting fetched but is not appending into my array? [duplicate]

Currently I am attempting to push values into an array of note objects from firebase
The only issue is due to Firebases asynchronous nature, I am having trouble getting the main thread to wait until the fetch function is completed. I have viewed many answers on this site and I have read up on the Semaphores and Dispatch queue documentation however I cannot get this fetch to work. It appears that most of the people here are attempting to use a table view which I am not.
Here is the fetch code
func fetchUser(){
FIRDatabase.database().reference().child("notes").observe(.childAdded, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let user = noteClass(dictionary: dictionary)
self.coordinotes.append(user)
}
}, withCancel: nil)
}
I have removed all of my semaphore and dispatch main attempts due to none of them working. This function is called in my view did load. When i check the values of my array that i push them into 'coordinotes' the value is not yet placed in and i get an out of bounds error.
Rest of code
import UIKit
import MapKit
import CoreLocation
import Firebase
import FirebaseDatabase
struct PreferencesKeys{
static let savedItems = "savedItems"
}
class ViewController: UIViewController, CLLocationManagerDelegate{
let manager = CLLocationManager()
var coordinotes:[noteClass] = Array()
var latitude = Double()
var noteTime = noteBrain()
//Map
#IBOutlet weak var map: MKMapView!
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
let location = locations[0]
let myLocation:CLLocationCoordinate2D = CLLocationCoordinate2DMake(location.coordinate.latitude, location.coordinate.longitude)
let region:MKCoordinateRegion = MKCoordinateRegionMake(myLocation, noteTime.span)
map.setRegion(region, animated: true)
self.map.showsUserLocation = true
}
override func viewDidLoad()
{
super.viewDidLoad()
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Logout", style: .plain, target: self, action: #selector(handleLogout))
if FIRAuth.auth()?.currentUser?.uid == nil {
perform(#selector(handleLogout), with: nil, afterDelay: 0)
}
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
fetchUser()
loadAllCoordinotes()
}
func handleLogout() {
do {
try FIRAuth.auth()?.signOut()
} catch let logoutError {
print(logoutError)
}
let loginController = LoginController()
present(loginController, animated: true, completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func loadAllCoordinotes() {
let length = coordinotes.count - 1
map.addAnnotation(coordinotes[length])
}
func fetchUser(_ completion:#escaping ([noteClass] , _ success: Bool)-> Void){
let coordinotes = [noteClass]()
FIRDatabase.database().reference().child("notes").observe(.childAdded, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let user = noteClass(dictionary: dictionary)
coordinotes.append(user)
}
completion(coordinotes, true)
}, withCancel: nil)
}
and then you call it in viewDidLoad like this:
fetchUser { (coordinotes, success) in
if success {
self.coordinotes = coordinote
self.loadAllCoordinotes()
}
}

Passing global Arrays through functions is not working in swift

I have this program which takes values from firestore and puts them in an array. I have confirmed that these arrays have data inside of them by using test print functions. When I declare 2 global arrays ( gloabalGPA and globalSAT) and pass the value of the gpaColleges and the satColleges into them through a function everything works. I placed test print functions inside of the functions (the swithcSATArray and the switchGPAArray). However when I attempt to print these global variables again in a different function the print function prints out an empty array. like so: ( [] )
import UIKit
import FirebaseAuth
import FirebaseDatabase
import Firebase
import FirebaseFirestore
class ScoresViewController: UIViewController {
var docRef: DocumentReference!
let defaultStore = Firestore.firestore()
var globalGPA = [String]()
var globalSAT = [String]()
override func viewDidLoad() {
super.viewDidLoad()
let userID: String = (Auth.auth().currentUser?.uid)!
docRef = Firestore.firestore().document("Users/\(userID)")
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBOutlet weak var GpaScore: UITextField!
#IBOutlet weak var SATscore: UITextField!
#IBOutlet weak var ACT_Score: UITextField!
#IBAction func SubmitTapped(_ sender: Any) {
print("Submit Tapped")
let Sattext = SATscore.text
let Acttext = ACT_Score.text
let Gpatext = GpaScore.text
let gpaScore = Gpatext
let SatScore2 = Sattext
let Acttext2 = Acttext
let CombinedScores = Sattext! + Acttext!
if GpaScore.text == "" {
self.createAlert(titleText: "Error", messageText: "No Weighted GPA Entered")
}
else if CombinedScores == "" {
self.createAlert(titleText: "Error", messageText: "No SAT nor ACT Score Entered")
}
else{
let dataToSave: [String: Any] = ["GPA": gpaScore!, "SAT Score": SatScore2!, "ACT Score": Acttext2!]
docRef.setData(dataToSave) { (error) in
if let error = error {
print("error in sending data to fireStore: \(error.localizedDescription)")
}else {
print("Data was succesfully saved to FireStore")
}
}
self.presentLoggedInScreen()
sendToFireStore(gpa: gpaScore!, sat: SatScore2!)
self.addArrays()
}
}
func createAlert (titleText : String , messageText: String) {
let alert = UIAlertController (title: titleText, message: messageText, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Dissmis", style: .default, handler: { (action) in alert.dismiss(animated: true, completion: nil)
}))
self.present(alert, animated: true, completion: nil)
}
func presentLoggedInScreen() {
let storyboard:UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let HomeVC:HomeVC = storyboard.instantiateViewController(withIdentifier: "HomeVC") as! HomeVC
self.present(HomeVC, animated: true, completion: nil)
}
func sendToFireStore(gpa: String, sat: String) {
let db = Firestore.firestore()
var gpaColleges = [String]()
let gpaRef = db.collection("Colleges")
let query1 = gpaRef
.whereField("Average GPA", isLessThanOrEqualTo: gpa)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
gpaColleges.append(document.documentID)
}
self.switchGPAArray(gpa: gpaColleges)
}
}
var satColleges = [String]()
let satRef = db.collection("Colleges")
let query2 = satRef
.whereField("Average SAT Score", isLessThanOrEqualTo: sat)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
satColleges.append(document.documentID)
}
self.swithcSATArray(sat: satColleges)
}
}
}
func swithcSATArray(sat: Array<Any>) {
self.globalSAT = sat as! [String]
print("Printing inside of SAT function \(self.globalSAT)")
}
func switchGPAArray(gpa: Array<Any>) {
self.globalGPA = gpa as! [String]
print("Printing inside of GPA funtion \(self.globalGPA)")
}
func addArrays() {
print("INSIDE OF ADD ARRAYS SAT \(self.globalSAT)")
print("INSIDE OF ADD ARRAYS GPA \(self.globalSAT)")
}
}
Any help would be greatly appreciated.
First of all there is lot of code but little information about your problem. We don't even know which function you call first and what is the second one that prints empty?
I can edit this answer after you provide more details. But as far as I see, you have closures and the compiler won't wait closure to complete, to run the next line. If this is the problem you can use threads and run your code in main thread so you can be sure it is completed. Or you can use delegate pattern to be notified when it's complete.
Or just do this:
let dataToSave: [String: Any] = ["GPA": gpaScore!, "SAT Score": SatScore2!, "ACT Score": Acttext2!]
docRef.setData(dataToSave) { (error) in
if let error = error {
print("error in sending data to fireStore: \(error.localizedDescription)")
}else {
self.presentLoggedInScreen()
sendToFireStore(gpa: gpaScore!, sat: SatScore2!)
self.addArrays()
print("Data was succesfully saved to FireStore")
}
}
If no one above works, provide more details about your issue! :)

Does Map method of Swift Array implemented concurrently?

Checked the doc, but not talks about many details of implementation. Wondering if for a large array, does it perform in concurrent way?
The map implementation of Array specifically doesn't perform any multithreading, but there's nothing that says you couldn't make a concurrent implementation. Here's one that's generalized to work with any Sequence:
import Dispatch
class SharedSynchronizedArray<T> {
var array = [T]()
let operationQueue = DispatchQueue(label: "SharedSynchronizedArray")
func append(_ newElement: T) {
operationQueue.sync {
array.append(newElement)
}
}
}
public struct ConcurrentSequence<Base, Element>: Sequence
where Base: Sequence, Element == Base.Iterator.Element {
let base: Base
public func makeIterator() -> Base.Iterator {
return base.makeIterator()
}
public func map<T>(_ transform: #escaping (Element) -> T) -> [T] {
let group = DispatchGroup()
let resultsStorageQueue = DispatchQueue(label: "resultStorageQueue")
let results = SharedSynchronizedArray<T>()
let processingQueue = DispatchQueue(
label: "processingQueue",
attributes: [.concurrent]
)
for element in self {
group.enter()
print("Entered DispatchGroup for \(element)")
var result: T?
let workItem = DispatchWorkItem{ result = transform(element) }
processingQueue.async(execute: workItem)
resultsStorageQueue.async {
workItem.wait()
guard let unwrappedResult = result else {
fatalError("The work item was completed, but the result wasn't set!")
}
results.append(unwrappedResult)
group.leave()
print("Exited DispatchGroup for \(element)")
}
}
print("Started Waiting on DispatchGroup")
group.wait()
print("DispatchGroup done")
return results.array
}
}
public extension Sequence {
public var parallel: ConcurrentSequence<Self, Iterator.Element> {
return ConcurrentSequence(base: self)
}
}
print("Start")
import Foundation
let input = Array(0..<100)
let output: [Int] = input.parallel.map {
let randomDuration = TimeInterval(Float(arc4random()) / Float(UInt32.max))
Thread.sleep(forTimeInterval: randomDuration)
print("Transforming \($0)")
return $0 * 2
}
print(output)
// print(output.parallel.filter{ $0 % 3 == 0 })
print("Done")

Pass an arrays row shown in an NSTableView to another array in Swift

I have 2 arrays
var messages = [Message]()
var screenMessages = [screenMessage]()
I have the messages array items in a NSTableView.. when I press an IBOutlet I would like to pass the items in that row to the screenMessages array to present in another NSTableView.
My NSTableView starts like so..
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
let result = tableView.makeViewWithIdentifier("cell", owner: self) as? secondviewTableCell
let mess = messages[row]
I've tried a number of ways of appending the screenMessages with the messages[row] but I can't put my finger on it. If anyone could demonstrate or point me in the right direction that would be brilliant.
Thank you.
Added more detail:
Screen one looks like so and when pressing the add button it should then pass that data from that row into screen twos tableview..
Screen two:
My View for screen one is as:
import Firebase
import Cocoa
var messages = [Message]()
var screenMessages = [screenMessage]()
class secondVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
#IBOutlet weak var tableView: NSTableView!
#IBOutlet weak var screenRefreshBtn: NSButton!
#IBOutlet weak var refreshButton: NSButton!
var senderImageUrl: String!
var ref: Firebase!
var messagesRef: Firebase!
func setupFirebase() {
messagesRef = Firebase(url: "https://url.firebaseio.com/screenmessages")
messagesRef.queryLimitedToLast(25).observeEventType(FEventType.ChildAdded, withBlock: { (snapshot) in
let text = snapshot.value["text"] as? String
let sender = snapshot.value["senderName"] as? String
let imageUrl = snapshot.value["profileImageURL"] as? String
let MediaType = snapshot.value["MediaType"] as! String
let fileUrl = snapshot.value["fileUrl"] as? String
let message = Message(text: text, sender: sender, imageUrl: imageUrl, MediaType: MediaType, fileUrl: fileUrl)
messages.append(message)
let screenmessage = screenMessage(text: text, sender: sender, imageUrl: imageUrl, MediaType: MediaType, fileUrl: fileUrl)
screenMessages.append(screenmessage)
switch MediaType{
case "TEXT":
print("text message")
case "PHOTO":
print("photo message")
default:
print("default")
}
self.tableView.reloadData()
})
}
override func viewDidLoad() {
super.viewDidLoad()
setupFirebase()
}
// MARK: - Table View
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return messages.count
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
let result = tableView.makeViewWithIdentifier("cell", owner: self) as? secondviewTableCell
let mess = messages[row]
if mess.text() == nil {
result?.textField?.alphaValue = 0
result!.sendertextView.stringValue = mess.sender()
let url = NSURL(string: mess.fileUrl()!)!
// Download task:
// - sharedSession = global NSURLCache, NSHTTPCookieStorage and NSURLCredentialStorage objects.
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (responseData, responseUrl, error) -> Void in
// if responseData is not null...
if let data = responseData{
// execute in UI thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let photo = NSImage(data: data)!
result?.mediaPhoto.image = photo
})
}
}
task.resume()
} else {
result!.textField!.stringValue = mess.text()!
result!.sendertextView.stringValue = mess.sender()
}
return result
}
#IBAction func addtablerow(object: NSButton) {
let row = tableView.rowForView( object as NSView )
if ( row > -1 ) {
}
}
And my second screen is:
import Cocoa
class screenVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
var addedObserver = false
#IBOutlet weak var tableView: NSTableView!
override func viewDidLoad() {
super.viewDidLoad()
refreshObs()
clearObs()
self.tableView.backgroundColor = NSColor.clearColor()
if let window = self.view.window {
// custom window here
window.level = Int(CGWindowLevelForKey(.FloatingWindowLevelKey))
} else {
addedObserver = true
self.addObserver(self, forKeyPath: "view.window", options: [.New, .Initial], context: nil)
}
}
func refreshList(notification: NSNotification){
self.tableView.alphaValue = 0
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
animateViewRefresh()
tableView.scrollToEndOfDocument(self)
}
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return screenMessages.count
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
let result = tableView.makeViewWithIdentifier("cell2", owner: self) as? screenviewTableCell
let mess = screenMessages[row]
result?.senderLabel.stringValue = mess.sender()
if mess.text() != nil {
result?.messageTextView.stringValue = mess.text()!
let url = NSURL(string: mess.imageUrl()!)!
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (responseData, responseUrl, error) -> Void in
if let data = responseData{
dispatch_async(dispatch_get_main_queue(), { () -> Void in
result?.avatarImage.image = NSImage(data: data)
})
}}
task.resume()
} else {
result?.messageTextView.alphaValue = 0
let mess = screenMessages[row]
let url = NSURL(string: mess.fileUrl()!)!
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (responseData, responseUrl, error) -> Void in
if let data = responseData{
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let photo = NSImage(data: data)!
result?.mediaPhoto.image = photo
})
}
}
let url2 = NSURL(string: mess.imageUrl()!)!
let task2 = NSURLSession.sharedSession().dataTaskWithURL(url2) { (responseData, responseUrl, error) -> Void in
if let data = responseData{
dispatch_async(dispatch_get_main_queue(), { () -> Void in
result?.avatarImage.image = NSImage(data: data)
})
}}
task.resume()
task2.resume()
}
return result
}
// MARK : Animate
func animateView(notification: NSNotification){
NSAnimationContext.runAnimationGroup({ (context) in
context.duration = 2
self.tableView.animator().alphaValue = 0
screenMessages.removeAll()
}, completionHandler: { () -> Void in
})}
func animateViewRefresh(){
NSAnimationContext.runAnimationGroup({ (context) in
context.duration = 4
self.tableView.animator().alphaValue = 1
}, completionHandler: { () -> Void in
})}
func refreshObs(){
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(screenVC.refreshList(_:)), name:"refreshMyTableView", object: nil)
}
func clearObs(){
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(screenVC.animateView(_:)), name:"clearMyTableView", object: nil)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if let window = self.view.window {
// custom window here
window.level = Int(CGWindowLevelForKey(.FloatingWindowLevelKey))
window.titlebarAppearsTransparent = true
window.movableByWindowBackground = true
window.opaque = true
window.backgroundColor = NSColor.clearColor()
}
}
deinit {
if addedObserver {
self.removeObserver(self, forKeyPath: "view.window")
}
}
}
I have tried a number of things such as 'screenMessages += messages(row)' and appending to add that row to the screenMessages array but I've had no luck.
Am I going about this in the right way or is there a better way of doing so?
Thank you.
To append an element from one array to another array just write
let index = index of element you need
let message = messages[index]
screenMessages.append(message)
If message is not the same type as the contents of the screenMessages array you will need to convert it, I would need more details of the types to help with that.
If you are having trouble passing the data to another ViewController I would need more information on the current architecture to give good advice, but for example you might define a protocol MessageDelegate that one of the controllers implements and the other has as a property.
update
If you update your data array for a table and want the new information to appear remember to call reloadData on the UITableView

How do I cast the argument type within a closure signature in Swift?

I'm trying to write a light observer class in Swift (currently Swift 2). The idea is to use it within an Entity Component system, as a means for the components to communicate with one-another without being coupled together.
The problem I'm having is that all types of data could be communicated, a CGVector, an NSTimeInterval and so on. This means that the method being passed could have all kinds of type signatures (CGVector) -> Void, () -> Void etc.
I'd like to be able to store these varying signatures in an array, but still have some type safety. My thinking is that the type for the array would be (Any) -> Void or perhaps (Any?) -> Void, so that I can at least ensure that it contains methods. But I'm having trouble passing methods in this way: Cannot convert value of type '(CGVector) -> ()' to expected argument type '(Any) -> ()'.
First attempt:
//: Playground - noun: a place where people can play
import Cocoa
import Foundation
enum EventName: String {
case input, update
}
struct Binding{
let listener: Component
let action: (Any) -> ()
}
class EventManager {
var events = [EventName: [Binding]]()
func add(name: EventName, event: Binding) {
if var eventArray = events[name] {
eventArray.append(event)
} else {
events[name] = [event]
}
}
func dispatch(name: EventName, argument: Any) {
if let eventArray = events[name] {
for element in eventArray {
element.action(argument)
}
}
}
func remove(name: EventName, listener: Component) {
if var eventArray = events[name] {
eventArray = eventArray.filter(){ $0.listener.doc != listener.doc }
}
}
}
// Usage test
//Components
protocol Component {
var doc: String { get }
}
class Input: Component {
let doc = "InputComponent"
let eventManager: EventManager
init(eventManager: EventManager) {
self.eventManager = eventManager
}
func goRight() {
eventManager.dispatch(.input, argument: CGVector(dx: 10, dy: 0) )
}
}
class Movement: Component {
let doc = "MovementComponent"
func move(vector: CGVector) {
print("moved \(vector)")
}
}
class Physics: Component {
let doc = "PhysicsComponent"
func update(time: NSTimeInterval){
print("updated at \(time)")
}
}
class someClass {
//events
let eventManager = EventManager()
// components
let inputComponent: Input
let moveComponent = Movement()
init() {
inputComponent = Input(eventManager: eventManager)
let inputBinding = Binding(listener: moveComponent, action: moveComponent.move) // ! Cannot convert value of type '(CGVector) -> ()' to expected argument type '(Any) -> ()'
eventManager.add(.input, event: inputBinding)
}
}
let someInstance = someClass()
someInstance.inputComponent.goRight()
Throws the error Cannot convert value of type '(CGVector) -> ()' to expected argument type '(Any) -> ()'.
Second attempt
If I genericize the Binding struct to recognise different types of arguments I have a bit more luck. This version basically works, but the array holding the methods is now [Any] ( I'm not sure whether it's the attempt to cast Any back to the Binding struct that is causing the slightly odd error below Binary operator '!=' cannot be applied to two 'String' operands):
struct Binding<Argument>{
let listener: Component
let action: (Argument) -> ()
}
class EventManager {
var events = [EventName: [Any]]()
func add(name: EventName, event: Any) {
if var eventArray = events[name] {
eventArray.append(event)
} else {
events[name] = [event]
}
}
func dispatch<Argument>(name: EventName, argument: Argument) {
if let eventArray = events[name] {
for element in eventArray {
(element as! Binding<Argument>).action(argument)
}
}
}
func remove(name: EventName, listener: Component) {
if var eventArray = events[name] {
// eventArray = eventArray.filter(){ ($0 as! Binding).listener.doc != listener.doc } //Binary operator '!=' cannot be applied to two 'String' operands
}
}
}
Is there a way to do this and have the array hold methods of varying type signatures, something like [(Any?) -> ()] ?
Attempt 3...
Reading around, eg here http://www.klundberg.com/blog/capturing-objects-weakly-in-instance-method-references-in-swift/ it seems that my approach above will lead to strong reference cycles, and that what I need to do is pass the static method eg Movement.move rather than moveComponent.move. So the type signature I would be storing would actually be (Component) -> (Any?) -> Void rather than (Any?) -> Void. But my question still stands, I still would like to be able to store an array of these static methods with a bit more type-safety than just [Any].
One approach to casting the parameters of a closure, suggested in Mike Ash's blog that Casey Fleser linked to, is to "recurry"(?) it.
A genericised Binding class:
private class Binding<Argument>{
weak var listener: AnyObject?
let action: AnyObject -> Argument -> ()
init(listener: AnyObject, action: AnyObject -> Argument -> ()) {
self.listener = listener
self.action = action
}
func invoke(data: Argument) -> () {
if let this = listener {
action(this)(data)
}
}
}
And the event manager, without the recurrying:
class EventManager {
var events = [EventName: [AnyObject]]()
func add<T: AnyObject, Argument>(name: EventName, listener: T, action: T -> Argument -> Void) {
let binding = Binding(listener: listener, action: action) //error: cannot convert value of type 'T -> Argument -> Void' to expected argument type 'AnyObject -> _ -> ()'
if var eventArray = events[name] {
eventArray.append(binding)
} else {
events[name] = [binding]
}
}
func dispatch<Argument>(name: EventName, argument: Argument) {
if let eventArray = events[name] {
for element in eventArray {
(element as! Binding<Argument>).invoke(argument)
}
}
}
func remove(name: EventName, listener: Component) {
if var eventArray = events[name] {
eventArray = eventArray.filter(){ $0 !== listener }
}
}
}
This still produces the same error, of not being able to cast to AnyObject:
error: cannot convert value of type 'T -> Argument -> Void' to expected argument type 'AnyObject -> _ -> ()'.
Bu if we call the first part of the curried function, and enclose it within a new closure (I don't know if this has a name, I'm calling it "recurrying"), like this: action: { action($0 as! T) } then it all works (technique taken from Mike Ash). I guess this is a bit of a hack, in that Swift type safety is being circumvented.
I also don't really understand the error message: it's saying it can't convert T to AnyObject, but then accepts casting to T?
EDIT: updated with the complete code so far
edit2: corrected how events are appended
edit3: removing events now works
//: Playground - noun: a place where people can play
import Cocoa
import Foundation
enum EventName: String {
case input, update
}
private class Binding<Argument>{
weak var listener: AnyObject?
let action: AnyObject -> Argument -> ()
init(listener: AnyObject, action: AnyObject -> Argument -> ()) {
self.listener = listener
self.action = action
}
func invoke(data: Argument) -> () {
if let this = listener {
action(this)(data)
}
}
}
class EventManager {
var events = [EventName: [AnyObject]]()
func add<T: AnyObject, Argument>(name: EventName, listener: T, action: T -> Argument -> Void) {
let binding = Binding(listener: listener, action: { action($0 as! T) }) //
if events[name]?.append(binding) == nil {
events[name] = [binding]
}
}
func dispatch<Argument>(name: EventName, argument: Argument) {
if let eventArray = events[name] {
for element in eventArray {
(element as! Binding<Argument>).invoke(argument)
}
}
}
func remove<T: AnyObject, Argument>(name: EventName, listener: T, action: T -> Argument -> Void) {
events[name]? = events[name]!.filter(){ ( $0 as! Binding<Argument>).listener !== listener }
}
}
// Usage test
//Components
class Component {
weak var events: EventManager?
let doc: String
init(doc: String){
self.doc = doc
}
}
class Input: Component {
init() {
super.init(doc: "InputComponent")
}
func goRight() {
events?.dispatch(.input, argument: CGVector(dx: 10, dy: 0) )
}
func goUp() {
events?.dispatch(.input, argument: CGVector(dx: 0, dy: -5) )
}
}
class Movement: Component {
init() {
super.init(doc: "MovementComponent")
}
func move(vector: CGVector) {
print("moved \(vector)")
}
}
class Physics: Component {
init() {
super.init(doc: "PhysicsComponent")
}
func update(time: NSTimeInterval){
print("updated at \(time)")
}
func move(vector: CGVector) {
print("updated \(vector)")
}
}
// Entity
class Entity {
let events = EventManager()
}
class someClass: Entity {
// components
let inputComponent: Input
let moveComponent: Movement
let physicsComponent: Physics
override init() {
inputComponent = Input()
moveComponent = Movement()
physicsComponent = Physics()
super.init()
inputComponent.events = events
events.add(.input, listener: moveComponent, action: Movement.move)
events.add(.input, listener: physicsComponent, action: Physics.move)
}
}
let someInstance = someClass()
someInstance.inputComponent.goRight()
//moved CGVector(dx: 10.0, dy: 0.0)
//updated CGVector(dx: 10.0, dy: 0.0)
someInstance.events.remove(.input, listener: someInstance.moveComponent, action: Movement.move)
someInstance.inputComponent.goUp()
//updated CGVector(dx: 0.0, dy: -5.0)
someInstance.events.remove(.input, listener: someInstance.physicsComponent, action: Physics.move)
someInstance.inputComponent.goRight()
// nothing
A different approach to how to store a collection of different type signatures. Instead of using generics, casting, or type erasure, use an enum with associated types representing each type of signature you want to use eg
enum Signature{
case cgvector(CGVector -> Void)
case nstimeinterval(NSTimeInterval -> Void)
The disadvantage is that the enum captures a strong reference to the method. However (I need to take this out of a playground to test it more), this doesn't seem to create a strong reference cycle. You can set the containing entity to nil and all of its components appear to be deinitialized. I'm not quite sure what's happening there. To me this enum approach seems cleaner than putting a generic wrapper in an array of AnyObject and have to cast and type-erase constantly.
Comments and criticisms welcome.
/*:
## Entity - Component framework with a notification system for decoupled communications between components
### Limitations:
1. Closure class stores a strong reference to the components. But, a strong reference cycle is not created.
2. A given class instance (component) can only subscribe to a given event with one method.
*/
import Cocoa
import Foundation
enum EventName: String {
case onInput
}
//A type-safe wrapper that stores closures of varying signatures, and allows them to be identified by the hashValue of its owner.
class Closure {
enum Signature {
case cgvector(CGVector -> Void)
case nstimeinterval(NSTimeInterval -> Void)
func invoke(argument: Any){
switch self {
case let .cgvector(closure): closure(argument as! CGVector)
case let .nstimeinterval(closure): closure(argument as! NSTimeInterval)
}
}
}
var method: Signature
weak var owner: Component?
init(owner: Component, action: Closure.Signature) {
method = action
self.owner = owner
}
}
// Entity
class Entity {
var components = Set<Component>()
private var events = [EventName: [Closure]]()
deinit {
print("Entity deinit")
}
// MARK: component methods
func add(component: Component){
components.insert(component)
component.parent = self
}
func remove(component: Component){
unsubscribeFromAll(component)
components.remove(component)
}
func remove<T: Component>(type: T.Type){
guard let component = retrieve(type) else {return}
remove(component)
}
func retrieve<T: Component>(type: T.Type) -> T? {
for item in components {
if item is T { return item as? T}
}
return nil
}
// MARK: event methods
func subscribe(listener: Component, method: Closure.Signature, to event: EventName ){
let closure = Closure(owner: listener, action: method)
// if event array does not yet exist, create it with the closure.
if events[event] == nil {
events[event] = [closure]
return
}
// check to make sure this listener has not subscribed to this event already
if ((events[event]!.contains({ $0.owner! == listener })) == false) {
events[event]!.append(closure)
}
}
func dispatch(argument: Any, to event: EventName ) {
events[event]?.forEach(){ $0.method.invoke(argument) }
}
func unsubscribe(listener: Component, from name: EventName){
//events[name]? = events[name]!.filter(){ $0.hashValue != listener.hashValue }
if let index = events[name]?.indexOf({ $0.owner! == listener }) {
events[name]!.removeAtIndex(index)
}
}
func unsubscribeFromAll(listener: Component){
for (event, _) in events {
unsubscribe(listener, from: event)
}
}
}
//Components
class Component: Hashable {
weak var parent: Entity?
var doc: String { return "Component" }
var hashValue: Int { return unsafeAddressOf(self).hashValue }
deinit {
print("deinit \(doc)")
}
}
func == <T: Component>(lhs: T, rhs: T) -> Bool {
return lhs.hashValue == rhs.hashValue
}
//: #### Usage test
class Input: Component {
override var doc: String { return "Input" }
func goRight() {
parent?.dispatch(CGVector(dx: 10, dy: 0), to: .onInput )
}
func goUp() {
parent?.dispatch(CGVector(dx: 0, dy: -10), to: .onInput )
}
}
class Movement: Component {
override var doc: String { return "Movement" }
func move(vector: CGVector) {
print("moved \(vector)")
}
}
class Physics: Component {
override var doc: String { return "Physics" }
func update(time: NSTimeInterval){
print("updated at \(time)")
}
func move(vector: CGVector) {
print("updated \(vector)")
}
}
// an example factory
var entity: Entity? = Entity()
if let instance = entity {
// a couple of ways of adding components
var inputComponent = Input()
instance.add(inputComponent)
instance.add(Movement())
instance.add(Physics())
var m = instance.retrieve(Movement.self)
instance.subscribe(m!, method: .cgvector(m!.move), to: .onInput)
let p = instance.retrieve(Physics.self)!
instance.subscribe(p, method: .cgvector(p.move), to: .onInput)
inputComponent.goRight()
inputComponent.goUp()
instance.retrieve(Input.self)?.goRight()
instance.remove(Movement.self)
m = nil
inputComponent.goRight()
}
entity = nil //not a strong ref cycle
I fell into that situation but i found a cool solution
with an anonymous inline function, it is like mapping
here is an example
var cellConfigurator: ((UITableViewCell, _ index: IndexPath) -> Void)?
func setup<CellType: UITableViewCell>(cellConfig: ((CellType, _ index: IndexPath) -> ())?)
{
// this mini function maps the closure
cellConfigurator = { (cell: UITableViewCell, _ index: IndexPath) in
if let cellConfig = cellConfig, let cell = cell as? CellType {
cellConfig(cell, index)
}
else
{ print("-- error: couldn't cast cell") }
}
}

Resources