Here is how I implement an inline callback assignment in Objective-C:
self.beforeAdjustViews = ^ (UIView* btnView)
{
// do something
};
How do I implement the same thing in Swift 5?
You can either assign directly
self.beforeAdjustViews = { (button) in
// Write Some Code
}
or create a reference variable to this call back - usually better when you have a lot of properties or trying to keep you code simple and beautiful
let beforAdjustViewsCallBack: (btnView: UIView) -> Void = { button in
// Write Some Code
}
Also keep in mind that the same rules for retain cycles are applied, So if you don't want to keep a strong reference of the self inside the block, you will need to use either [weak self] or [unowoned self] based on your use case.
You can try
self.beforeAdjustViews = { [weak self] (btnView) in
///
}
Related
I'm developing an iOS app in Swift4 with an Object-C framework called 'YapDatabase'. There is an Object-C function with a block like this in class 'YapDatabaseConnection':
- (void)readWithBlock:(void (^)(YapDatabaseReadTransaction *transaction))block;
I use the function in this way:
static func readNovelIds() -> [String]? {
let account = XFAccountManager.share().account
var events: [XFNovelClickEvent]?
OTRDatabaseManager.shared.readOnlyDatabaseConnection?.read({ (transaction) in
events = XFNovelClickEvent.allNovelClickEvents(accountId: account.uniqueId, transaction: transaction)
})
guard let clickEvents = events else {
return nil
}
let readNovelsIds = clickEvents.map {
$0.bookId ?? ""
}
return readNovelsIds
}
I thought the closure will be executed immediately after the 'events' parameter declared. In fact, the closure doesn't be executed before result returns. To search the reason, I open the file named 'YapDatabaseConnection.h(Interface)' generated by Xcode (with cmd+shift+o), found the function has been translate to Swift in this way:
open func read(_ block: #escaping (YapDatabaseReadTransaction) -> Void)
So, how do I use this function in a #noescap way?
As the caller, you can't change when the closure is executed. That's up to the read() function. If you control that function, you'll need to modify it to call the closure immediately. If you don't control it, then you can't modify how it behaves.
You can convert an asynchronous call into a synchronous call using a DispatchGroup as described in Waiting until the task finishes. However, you can't make a database call on the main queue; you risk crashing the app. As a general rule, you should just use async calls in this case (i.e. make readNovelIds also be asynchronous and take a completion handler).
The reason why Xcode bridged the objective-c block as #escaping is because the block may be executed after the function return.
Since you don’t own YapDatabase, you couldn’t modify the source code to make it non-escaped, so you may wanna make your readNovelIds function takes a closure as parameter and pass the return value through closure.
static func readNovelIds(resultHandler: #escaping ([String]?) -> ()) {
let account = XFAccountManager.share().account
var events: [XFNovelClickEvent]?
OTRDatabaseManager.shared.readOnlyDatabaseConnection?.read({ (transaction) in
events = XFNovelClickEvent.allNovelClickEvents(accountId: account.uniqueId, transaction: transaction)
if let clickEvents = events {
let readNovelsIds = clickEvents.map {
$0.bookId ?? ""
}
resultHandler(readNovelsIds)
}
resultHandler(nil)
})
}
If the method is in fact synchronous (i.e. it will not allow the block to escape its context), the Objective C header method should be decorated with NS_NOESCAPE. Looking at the documentation (which does say it is synchronous), and the implementation, it should be annotated that way.
- (void)readWithBlock:(void (NS_NOESCAPE ^)(YapDatabaseReadTransaction *transaction))block;
That, I believe, should allow the Swift interface importer to add the #noescaping declaration. You should probably file a bug request on the YapDatabase project; they can change it there.
The Apple documentation shows an unsettling blank space under the 'Creating a Dictionary' section of the UIKit reference here.
Has anyone found a replacement for the NSDictionaryOfVariableBindings macro, or are we expected to just write our own?
EDIT - According to this perhaps the right approach is to write a global function to handle this? Looks like complex macros are out entirely.
According to Apple source code:
NSDictionaryOfVariableBindings(v1, v2, v3) is equivalent to [NSDictionary dictionaryWithObjectsAndKeys:v1, #"v1", v2, #"v2", v3, #"v3", nil];
So in Swift you can do the same using:
let bindings = ["v1": v1, "v2": v2, "v3": v3]
NSDictionaryOfVariableBindings is, as you say, a macro. There are no macros in Swift. So much for that.
Nonetheless, you can easily write a Swift function to assign string names to your views in a dictionary, and then pass that dictionary into constraintsWithVisualFormat. The difference is that, unlike Objective-C, Swift can't see your names for those views; you will have to let it make up some new names.
[To be clear, it isn't that your Objective-C code could see your variable names; it's that, at macro evaluation time, the preprocessor was operating on your source code as text and rewriting it — and so it could just use the text of your variable names both inside quotes (to make strings) and outside (to make values) to form a dictionary. But with Swift, there is no preprocessor.]
So, here's what I do:
func dictionaryOfNames(arr:UIView...) -> Dictionary<String,UIView> {
var d = Dictionary<String,UIView>()
for (ix,v) in arr.enumerate(){
d["v\(ix+1)"] = v
}
return d
}
And you call it and use it like this:
let d = dictionaryOfNames(myView, myOtherView, myFantasicView)
myView.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat(
"H:|[v2]|", options: nil, metrics: nil, views: d)
)
The catch is that it is up to you to realize that the name for myOtherView in your visual format string will be v2 (because it was second in the list passed in to dictionaryOfNames()). But I can live with that just to avoid the tedium of typing out the dictionary by hand every time.
Of course, you could equally have written more or less this same function in Objective-C. It's just that you didn't bother because the macro already existed!
That functionality is based on macro expansion which is currently not supported in Swift.
I do not think there is any way to do something similar in Swift at the moment. I believe you cannot write your own replacement.
I'm afraid you'll have to manually unroll the dictionary definition, even if it means repeating each name twice.
So I hacked something together which seems to work:
func dictionaryOfVariableBindings(container: Any, views:UIView...) -> Dictionary<String, UIView> {
var d = Dictionary<String, UIView>()
let mirror = Mirror(reflecting: container)
let _ = mirror.children.compactMap {
guard let name = $0.label, let view = $0.value as? UIView else { return }
guard views.contains(view) else { return }
d[name] = view
}
return d
}
Usage:
let views = dictionaryOfVariableBindings(container: self, views: imageView)
ObjC runtime to the rescue!
i created an alternate solution, but it only works if each of the views are instance variables of the same object.
func DictionaryOfInstanceVariables(container:AnyObject, objects: String ...) -> [String:AnyObject] {
var views = [String:AnyObject]()
for objectName in objects {
guard let object = object_getIvar(container, class_getInstanceVariable(container.dynamicType, objectName)) else {
assertionFailure("\(objectName) is not an ivar of: \(container)");
continue
}
views[objectName] = object
}
return views
}
can be used like this:
class ViewController: UIViewController {
var childA: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.redColor()
return view
}()
var childB: UIButton = {
let view = UIButton()
view.setTitle("asdf", forState: .Normal)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.blueColor()
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(childA)
self.view.addSubview(childB)
let views = DictionaryOfInstanceVariables(self, objects: "childA", "childB")
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[childA]|", options: [], metrics: nil, views: views))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[childB]|", options: [], metrics: nil, views: views))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[childA][childB(==childA)]|", options: [], metrics: nil, views: views))
}
}
unfortunately you still have to type the variable name in as a string, but it will at least assert if there is a typo. this definitely won't work in all situations, but helpful nonetheless
Based on https://stackoverflow.com/a/55086673/1058199 by cherpak-evgeny, this UIViewController extension assumes that the container is self, the current viewController instance.
extension UIViewController {
// Alex Zavatone 06/04/2019
// Using reflection, get the string name of the UIView properties passed in
// to create a dictionary of ["viewPropertyName": viewPropertyObject…] like
// Objective-C's NSDictionaryForVariableBindings.
func dictionaryOfBindings(_ arrayOfViews:[UIView?]) -> Dictionary<String, UIView> {
var bindings = Dictionary<String, UIView>()
let viewMirror = Mirror(reflecting: self)
let _ = viewMirror.children.compactMap {
guard let name = $0.label, let view = $0.value as? UIView else { return }
guard arrayOfViews.contains(view) else { return }
bindings[name] = view
}
return bindings
}
}
Use it like so from within your viewController:
let viewArray = [mySwitch, myField, mySpinner, aStepper, someView]
let constraintsDictionary = dictionaryOfBindings(viewArray)
Tested in Xcode 10.2.1 and Swift 4.2.
Many thanks to Cherpak Evgeny for writing it in the first place.
Once you've stored all your views as properties, you could also use reflection like so:
extension ViewController {
func views() -> Dictionary<String, AnyObject> {
var views = dictionaryOfProperties()
views.forEach {
if !($1 is UIView) {
views[$0] = nil
}
}
return views
}
}
extension NSObject {
func dictionaryOfProperties() -> Dictionary<String, AnyObject> {
var result = Dictionary<String, AnyObject>()
let mirror = Mirror(reflecting: self)
for case let(label?, value) in mirror.children {
result[label] = value as? AnyObject
}
return result
}
}
I am using a nsurlssesion based wrapper class written in objective-c in my swift project. Everthing is working except I am not able to undertand how the closure works in swift.
In my swift viewcontroller:
DownloadManager().downloadFile(forURL: url, progressBlock: { (progress) -> () in
print("current progress is \(progress)")
}, completionBlock: { (completion) in
print("is completed : \(completion)")
}, enableBackgroundMode: false)
In my Downloadmanager class (which is objc based) whenever nsurlssession delegate is called this happens:
dispatch_async(dispatch_get_main_queue(), ^(void) {
if(download.progressBlock){
download.progressBlock(progress); //exception when progressblock is nil
}
});
Download object have a block type property called progressBlock :
typedef void(^TWRDownloadProgressBlock)(CGFloat progress);
Output:
current progress is 0.0908259372799894
current progress is 0.272477811839968
current progress is 0.363303749119957
current progress is 0.454129686399947
current progress is 0.544955623679936
current progress is 0.635781560959925
current progress is 0.726607498239915
current progress is 0.817433435519904
current progress is 1.0
Flow of the code:
NSURlsession calls it's delegate method.
DowloadManager updates Download object's progress variable.
Somehow in my viewcontroller I can see the updated value of the progress variable inside the closure.
Question:
How is point-3 happening?
Is this the standard way closure behaves in swift? Specifically, I would like to know if the closure is automatically called everytime Dowload object changes?
According to The Swift Guide:
Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages.
Closures can capture and store references to any constants and variables from the context in which they are defined.
In this case, a closure is defined within the view controller, then passed
around and called later.
The flow would be as follows:
Make a new instance of TWRDownloadManager() and call its downloadFile method.
In this call, pass the progressBlock: ((CGFloat) -> Swift.Void) closure (it takes CGFloat as a parameter and returns nothing).
Down the line, this block is what gets assigned to TWRDownloadObject().progressBlock
Further down the line, NSURLSession Delegate method calls download.progressBlock(progress), which simply calls the closure from #2 and passes the CGFloat value as its parameter.
Here is quick Playground example:
import Foundation
import PlaygroundSupport
import TWRDownloadManager
PlaygroundPage.current.needsIndefiniteExecution = true
let downloadManager = TWRDownloadManager()
let url = "http://download.thinkbroadband.com/200MB.zip"
let progressClosure: (CGFloat) -> () = { (progress) in
print("current progress is \(progress)")
}
let completionClosure: (Bool) -> () = { (completion) in
print("is completed : \(completion)")
}
downloadManager.downloadFile(
forURL: url,
progressBlock: progressClosure,
completionBlock: completionClosure,
enableBackgroundMode: false
)
So far tvOS supports two ways to make tv apps, TVML and UIKit, and there is no official mentions about how to mix up things to make a TVML (that is basically XML) User Interface with the native counter part for the app logic and I/O (like playback, streaming, iCloud persistence, etc).
So, which is the best solution to mix TVML and UIKit in a new tvOS app?
In the following I have tried a solution following code snippets adapted from Apple Forums and related questions about JavaScriptCore to ObjC/Swift binding.
This is a simple wrapper class in your Swift project.
import UIKit
import TVMLKit
#objc protocol MyJSClass : JSExport {
func getItem(key:String) -> String?
func setItem(key:String, data:String)
}
class MyClass: NSObject, MyJSClass {
func getItem(key: String) -> String? {
return "String value"
}
func setItem(key: String, data: String) {
print("Set key:\(key) value:\(data)")
}
}
where the delegate must conform a TVApplicationControllerDelegate:
typealias TVApplicationDelegate = AppDelegate
extension TVApplicationDelegate : TVApplicationControllerDelegate {
func appController(appController: TVApplicationController, evaluateAppJavaScriptInContext jsContext: JSContext) {
let myClass: MyClass = MyClass();
jsContext.setObject(myClass, forKeyedSubscript: "objectwrapper");
}
func appController(appController: TVApplicationController, didFailWithError error: NSError) {
let title = "Error Launching Application"
let message = error.localizedDescription
let alertController = UIAlertController(title: title, message: message, preferredStyle:.Alert ) self.appController?.navigationController.presentViewController(alertController, animated: true, completion: { () -> Void in
})
}
func appController(appController: TVApplicationController, didStopWithOptions options: [String : AnyObject]?) {
}
func appController(appController: TVApplicationController, didFinishLaunchingWithOptions options: [String : AnyObject]?) {
}
}
At this point the javascript is very simple like. Take a look at the methods with named parameters, you will need to change the javascript counter part method name:
App.onLaunch = function(options) {
var text = objectwrapper.getItem()
// keep an eye here, the method name it changes when you have named parameters, you need camel case for parameters:
objectwrapper.setItemData("test", "value")
}
App. onExit = function() {
console.log('App finished');
}
Now, supposed that you have a very complex js interface to export like
#protocol MXMJSProtocol<JSExport>
- (void)boot:(JSValue *)status network:(JSValue*)network user:(JSValue*)c3;
- (NSString*)getVersion;
#end
#interface MXMJSObject : NSObject<MXMJSProtocol>
#end
#implementation MXMJSObject
- (NSString*)getVersion {
return #"0.0.1";
}
you can do like
JSExportAs(boot,
- (void)boot:(JSValue *)status network:(JSValue*)network user:(JSValue*)c3 );
At this point in the JS Counter part you will not do the camel case:
objectwrapper.bootNetworkUser(statusChanged,networkChanged,userChanged)
but you are going to do:
objectwrapper.boot(statusChanged,networkChanged,userChanged)
Finally, look at this interface again:
- (void)boot:(JSValue *)status network:(JSValue*)network user:(JSValue*)c3;
The value JSValue* passed in. is a way to pass completion handlers between ObjC/Swift and JavaScriptCore. At this point in the native code you do all call with arguments:
dispatch_async(dispatch_get_main_queue(), ^{
NSNumber *state = [NSNumber numberWithInteger:status];
[networkChanged.context[#"setTimeout"]
callWithArguments:#[networkChanged, #0, state]];
});
In my findings, I have seen that the MainThread will hang if you do not dispatch on the main thread and async. So I will call the javascript "setTimeout" call that calls the completion handler callback.
So the approach I have used here is:
Use JSExportAs to take car of methods with named parameters and avoid to camel case javascript counterparts like callMyParam1Param2Param3
Use JSValue as parameter to get rid of completion handlers. Use callWithArguments on the native side. Use javascript functions on the JS side;
dispatch_async for completion handlers, possibly calling a setTimeout 0-delayed in the JavaScript side, to avoid the UI to freeze.
[UPDATE]
I have updated this question in order to be more clear. I'm finding a technical solution for bridging TVML and UIKit in order to
Understand the best programming model with JavaScriptCode
Have the right bridge from JavaScriptCore to ObjectiveC and
viceversa
Have the best performances when calling JavaScriptCode from Objective-C
This WWDC Video explains how to communicate between JavaScript and Obj-C
Here is how I communicate from Swift to JavaScript:
//when pushAlertInJS() is called, pushAlert(title, description) will be called in JavaScript.
func pushAlertInJS(){
//allows us to access the javascript context
appController!.evaluateInJavaScriptContext({(evaluation: JSContext) -> Void in
//get a handle on the "pushAlert" method that you've implemented in JavaScript
let pushAlert = evaluation.objectForKeyedSubscript("pushAlert")
//Call your JavaScript method with an array of arguments
pushAlert.callWithArguments(["Login Failed", "Incorrect Username or Password"])
}, completion: {(Bool) -> Void in
//evaluation block finished running
})
}
Here is how I communicate from JavaScript to Swift (it requires some setup in Swift):
//call this method once after setting up your appController.
func createSwiftPrint(){
//allows us to access the javascript context
appController?.evaluateInJavaScriptContext({(evaluation: JSContext) -> Void in
//this is the block that will be called when javascript calls swiftPrint(str)
let swiftPrintBlock : #convention(block) (String) -> Void = {
(str : String) -> Void in
//prints the string passed in from javascript
print(str)
}
//this creates a function in the javascript context called "swiftPrint".
//calling swiftPrint(str) in javascript will call the block we created above.
evaluation.setObject(unsafeBitCast(swiftPrintBlock, AnyObject.self), forKeyedSubscript: "swiftPrint" as (NSCopying & NSObjectProtocol)?)
}, completion: {(Bool) -> Void in
//evaluation block finished running
})
}
[UPDATE] For those of you who would like to know what "pushAlert" would look like on the javascript side, I'll share an example implemented in application.js
var pushAlert = function(title, description){
var alert = createAlert(title, description);
alert.addEventListener("select", Presenter.load.bind(Presenter));
navigationDocument.pushDocument(alert);
}
// This convenience funnction returns an alert template, which can be used to present errors to the user.
var createAlert = function(title, description) {
var alertString = `<?xml version="1.0" encoding="UTF-8" ?>
<document>
<alertTemplate>
<title>${title}</title>
<description>${description}</description>
</alertTemplate>
</document>`
var parser = new DOMParser();
var alertDoc = parser.parseFromString(alertString, "application/xml");
return alertDoc
}
You sparked an idea that worked...almost. Once you have displayed a native view, there is no straightforward method as-of-yet to push an TVML-based view onto the navigation stack. What I have done at this time is:
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.appController?.navigationController.popViewControllerAnimated(true)
dispatch_async(dispatch_get_main_queue()) {
tvmlContext!.evaluateScript("showTVMLView()")
}
...then on the JavaScript side:
function showTVMLView() {setTimeout(function(){_showTVMLView();}, 100);}
function _showTVMLView() {//push the next document onto the stack}
This seems to be the cleanest way to move execution off the main thread and onto the JSVirtualMachine thread and avoid the UI lockup. Notice that I had to pop at the very least the current native view controller, as it was getting sent a deadly selector otherwise.
I'm trying to use a control called MZFormSheetController in swift. In the example given it provides a property as a completion handler, if I understand correctly, but I'm having difficulties translating it in Swift. Any help would be appreciated.
This is in the obj-c example.
controller.didPresentContentViewControllerHandler = ^(UIViewController *content) {
NSLog(#"DID PRESENT");
[self setNeedsStatusBarAppearanceUpdate];
};
I tried many variations and did an extensive search in the web but I could not find anything that could help me so I'm stuck here
controller.didPresentContentViewControllerHandler = (content:UIViewController() -> () {
println("did present1")
})
Here are the relevant docs: Cocoa Docs:: MZFormSheetPresentationController:: didPresentContentViewControllerHandler
If you need to access the view controller then do it like this,
controller.didPresentContentViewControllerHandler = {
controller in
println("did present1")
}
Or if you dont need the reference to the view controller, you can simply do,
let controller = Controller()
controller.didPresentContentViewControllerHandler = {
_ in
println("did present1")
}
Try add a variable after opening brace
controller.didPresentContentViewControllerHandler = {
vc in
println("did present1")
})