I need to rewrite that bunch of code from Objective-c to swift:
JSContext *jsContext = [webView valueForKeyPath:#"documentView.webView.mainFrame.javaScriptContext"];
jsContext[#"loaded"] = ^{
NSLog(#"Content Loaded!");
};
The question is about getting context when webView is loaded.
Found a solution for swift:
if let jsContext = svtwebView?.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as? JSContext {
let loaded: #convention(block) () -> Void = {
print("Loaded:")
}
jsContext.setObject(loaded, forKeyedSubscript: "loaded" as NSCopying & NSObjectProtocol)
jsContext.evaluateScript("loaded")
}
Related
I'm trying to use – performSelectorOnMainThread:withObject:waitUntilDone: for a Cocoa application that I'm developing in Swift. I need the application to wait until the job is done. Anyway, I have the following lines of code.
func recoverData(path:String) -> Void {
let sheetRect:NSRect = NSMakeRect(0,0,400,114)
let progSheet:NSWindow = NSWindow.init(contentRect:sheetRect, styleMask:NSTitledWindowMask,backing:NSBackingStoreType.Buffered,`defer`:true)
let contentView:NSView = NSView.init(frame:sheetRect)
let progInd:NSProgressIndicator = NSProgressIndicator.init(frame:NSMakeRect(190,74,20,20))
progInd.style = NSProgressIndicatorStyle.SpinningStyle
let msgLabel:NSTextField = NSTextField.init(frame:NSMakeRect(20,20,240,46))
msgLabel.stringValue = "Copying selected file..."
msgLabel.bezeled = false
msgLabel.drawsBackground = false
msgLabel.editable = false
msgLabel.selectable = false
contentView.addSubview(msgLabel)
contentView.addSubview(progInd)
progSheet.contentView = contentView
self.window.beginSheet(progSheet) {(NSModalResponse returnCode) -> Void in
progSheet.makeKeyAndOrderFront(self)
progInd.startAnimation(self)
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority,0)) {
//////////////////////////////////////////////////////////////////////////////////////////////////
self.performSelectorOnMainThread(Selector(self.readData(path)),withObject:path,waitUntilDone:true)
//////////////////////////////////////////////////////////////////////////////////////////////////
}
dispatch_async(dispatch_get_main_queue()) {
progInd.indeterminate = true
self.window.endSheet(progSheet)
progSheet.orderOut(self)
}
}
}
func readData(path:String) -> Void {
print("Hello!?")
}
I'm not sure how I pass path to readData. Xcode requires me to set the argument to something other than nil or nothing. In Objective-C, it would be
[self performSelectorOnMainThread:#selector(readData:) withObject:path waitUntilDone:YES];
Anyway, the application never reaches readData. What am I doing wrong?
Thanks for help.
Why not
self.window.beginSheet(progSheet) {(returnCode) -> Void in
dispatch_async(dispatch_get_main_queue()) {
progInd.startAnimation(self)
self.readData(path)
progInd.indeterminate = true
}
}
At some point you have to call self.window.endSheet(progSheet) to dismiss the sheet and call the completion handler.
Edit:
I guess you actually mean something like this
...
self.window.beginSheet(progSheet) {(returnCode) -> Void in
progInd.stopAnimation(self)
progInd.indeterminate = true
}
progInd.startAnimation(self)
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority,0)) {
self.readData(path) {
dispatch_async(dispatch_get_main_queue()) {
self.window.endSheet(progSheet)
}
}
}
}
func readData(path:String, completion: (() -> Void)) {
print("Hello!?")
completion()
}
In CollectionView I am displaying datas from parse.com. Successfully retrieved. But unable to display in the cell. I am receiving error as Array outbound. I found out the mistake, parse is running as asynchronous. But, before parse end, collection view gets loaded. So I unable to display values in the cell. It is throwing an error. How to stop all the process until parse get loaded completely? Kindly guide me.
MY CODING IS BELOW:
//VIEWCONTROLLER having collectionView
var myList : NSArray = NSArray()
let obj_par = parse_retrive()
obj_par.parse_DB() //DATAS GOING TO RETRIVE FROM PARSE
story_collection_view.reloadData() //RELOADING COLLECTIONVIEW
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
println("fifth")
if(collectionView == date_collection_view)
{
:
:
return cell
}
else
{
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("story_read", forIndexPath: indexPath) as story_reading_cell
cell.story_title.text = myList.objectAtIndex(indexPath.row) as? String //ERROR: ARRAY OUTBOUND OF INDEX
return cell
}
}
//PARSE_RETRIVE CLASS
func parse_DB() {
println("SECOND 10")
par_query.findObjectsInBackgroundWithBlock({(NSArray objects, NSError error) in
if (error != nil) {
NSLog("error " + error.localizedDescription)
}
else {
println("SECOND 13")
let sql_store_obj1 = sql_to_store() //THIRD CLASS
self.parse_obj_arr = NSArray(array: objects)
var j : Int = self.parse_obj_arr.count
for (var i : Int = 0; i < j; i++) {
self.par_object = self.parse_obj_arr.objectAtIndex(i) as PFObject
self.sto = self.par_object["story_title"] as String //STORY TITLE FROM PARSE
self.sto_con = self.par_object["story_content"] as String //STORY CONTENT FROM PARSE
self.sto_tit.append(self.sto) //STORING IN ARRAY VAR
self.sto_cont.append(self.sto_con) //STORING IN ARRAY VAR
} //FOR ENDING
sql_store_obj1.sto_title_arr = self.sto_tit
sql_store_obj1.sto_content_arr = self.sto_cont
sql_store_obj1.parse_to_sql()
}//ELSE ENDING
}) //PARSE QUERY ENDING
println("SECOND")
}
//THIRD CLASS
func parse_to_sql() {
let appDel : AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let context : NSManagedObjectContext = appDel.managedObjectContext!
let ent = NSEntityDescription.entityForName("Sad", inManagedObjectContext: context)
var newStory = story_sad_managed(entity: ent!, insertIntoManagedObjectContext: context)
newStory.story_title = sto_title_arr
newStory.story_content = sto_content_arr
let request = NSFetchRequest(entityName: "STORY")
request.predicate = NSPredicate(format: "story_title = %#", sto_title_arr)
result = context.executeFetchRequest(request, error: nil)!
let sto_page_obj = story_page()
sto_page_obj.myList = result //1st CLASS ARRAY
}
NOTE: All code is in Objective-C, but the translation for this particular code should be trivial. If someone wants to edit my post to include the Swift code, please feel free, but please leave the Objective-C in the post.
What I do (for UICollectionView or UITableView) is create a property, normally called isLoading like this:
#property (assign, nonatomic) BOOL isLoading;
I normally initialize it like this:
- (void)viewDidLoad {
[super viewDidLoad];
// This could also be done in viewWillAppear/viewDidAppear
// based on your needs/desires
self.isLoading = NO;
[self loadData];
}
- (void)loadData {
if (self.isLoading == YES) {
// Just in case a "loadData" call is made while one is pending...
return;
}
// Prevent this method from firing again until
// data loading is done; prevent UICollectionView
// from attempting to display missing data
self.isLoading = YES;
// Do whatever you need to do...
// Clean up and prepare for UICollectionView
self.isLoading = NO;
[self.collectionView reloadData];
}
Now, the real secret is, of course, that you have to implement logic in your UICollectionViewDataSource methods to display data conditionally based upon self.isLoading, like this:
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
if (self.isLoading == YES) {
// You might want to return 0 or 1,
// depending on whether you have a "loading"
// placeholder cell. Assume you do:
return 1;
} else {
// Return whatever is the correct number here
}
}
Generally, that's all I need to get my screen to delay loading the UICollectionView until the Parse query has returned correctly.
When I have multiple queries that will run concurrently, all of which should return before I consider data loading to be complete, then I create a BOOL property for each of them, setting the flags as appropriate, and I funnel all query returns through a checkIfDone method. Something like this:
#property (assign, nonatomic) BOOL data1Loaded;
#property (assign, nonatomic) BOOL data2Loaded;
#property (assign, nonatomic) BOOL data3Loaded;
#property (assign, nonatomic) BOOL data4Loaded;
- (void)loadData {
if (self.isLoading == YES) {
return;
}
self.isLoading = YES;
self.data1Loaded = NO;
self.data2Loaded = NO;
self.data3Loaded = NO;
self.data4Loaded = NO;
// Call each of the separate data loading methods...
[self loadData1];
[self loadData2];
[self loadData3];
[self loadData4];
// Notice that I don't do any reloadData calls here...
}
- (void)loadData1 {
PFQuery *query = // Some query creation...
[query findObjectsInBackgroundWithBlock:
^(NSArray *objects, NSError *error) {
if (error != nil) {
// Handle "got error"
} else {
// Handle "got good data"
}
// Either way, #1 is done, so:
self.data1Loaded = YES;
// This pattern checks for completion and
// runs the completion code only when all
// 4 (in this case) items/queries have returned
[self checkIfDone];
}
];
}
- (void)checkIfDone {
if (self.data1Loaded == YES &&
self.data2Loaded == YES &&
self.data3Loaded == YES &&
self.data4Loaded == YES)
{
// Clean up and prepare for UICollectionView
self.isLoading = NO;
[self.collectionView reloadData];
}
}
Caveat: This assumes that any subsequent calls to loadData will occur in the background and that any subsequent calls to [collectionView reloadData] will only take place at the end of your query calls. If anything else might call reloadData in the meantime, you will need more advanced logic to ensure that the correct data is loaded correctly.
Side Note: When doing something like this, don't forget to show the user an indication that work is progressing. I like to use the open source MBProgressHUD. It's available here on GitHub. I have found it invaluable for doing exactly what you're talking about doing.
I am able to implement the new WebKit in 7.1 Deployment. I can use it without error on the devices running in iOS8 up. However, when the device falls below iOS8, my WKWebView becomes nil even after the initialization, my suspect was even if you silence webkit and successfully add it on your project and the deployment was 7.1, if the OS actually fall below iOS8 this WebKit becomes unvalable.
I want to confirm this error so I can proceed. Since this webkit was introduced as of the release of swift and iOS8. Thanks
Here is a simple example, where I create a new protocol and extend both UIWebView and WKWebView from the same protocol. With this, it makes a easy to keep track of both these views inside my view controller and both of these use common method to load from url, it makes easy for abstraction.
protocol MyWebView{
func loadRequestFromUrl(url: NSURL!)
}
extension UIWebView:MyWebView{
func loadRequestFromUrl(url: NSURL!){
let urlRequest = NSURLRequest(URL: url)
loadRequest(urlRequest)
}
}
extension WKWebView:MyWebView{
func loadRequestFromUrl(url: NSURL!){
let urlRequest = NSURLRequest(URL: url)
loadRequest(urlRequest)
}
}
// This is a simple closure, which takes the compared system version, the comparison test success block and failure block
let SYSTEM_VERSION_GREATER_THAN_OR_EQUAL: (String, () -> (), () -> ()) -> Void = {
(var passedVersion: String, onTestPass: () -> (), onTestFail: () -> ()) in
let device = UIDevice.currentDevice()
let version = device.systemVersion
let comparisonOptions = version.compare(passedVersion, options: NSStringCompareOptions.NumericSearch, range: Range(start: version.startIndex, end: version.endIndex), locale: nil)
if comparisonOptions == NSComparisonResult.OrderedAscending || comparisonOptions == NSComparisonResult.OrderedSame{
onTestPass()
}else{
onTestFail()
}
}
class ViewController: UIViewController{
var webView: MyWebView!
override func viewDidLoad() {
super.viewDidLoad()
SYSTEM_VERSION_GREATER_THAN_OR_EQUAL("8.0",
{
let theWebView = WKWebView(frame: self.view.bounds)
self.view.addSubview(theWebView)
self.webView = theWebView
},
{
let theWebView = UIWebView(frame: self.view.bounds)
self.view.addSubview(theWebView)
self.webView = theWebView
})
webView.loadRequestFromUrl(NSURL(string: "http://google.com"))
}
}
Original Question (see solution below):
I am trying to use the AddressBook.framework in my Swift App, but can't figure out how to implement the ABAddressBookRegisterExternalChangeCallback function.
In Objective-C, I just implement the callback as a C function and pass its pointer:
// somewhere in the initializer of the MyAddressBook class:
ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(nil, nil);
ABAddressBookRegisterExternalChangeCallback(addressBook, externalChangeCallback, (__bridge void *)(self));
// somewhere else in the MyAddressBook class:
void externalChangeCallback(ABAddressBookRef reference, CFDictionaryRef info, void *context)
{
[(__bridge MyAddressBook *)context addressBookDidChangeExternally];
}
- (void)addressBookDidChangeExternally
{
// good old Obj-C from here on!
}
In Swift, it is proving very difficult for me to handle C functions. I found that Apple added the ability to pass C function pointers around in beta 3, but how do I declare such a function? It would be good to use Swift's closure syntax, but is that even possible here?
This is where I create the ABAddressBookRef:
var addressBookRef: ABAddressBookRef = {
let addressBookRef: ABAddressBookRef = ABAddressBookCreateWithOptions(nil, nil).takeRetainedValue()
// TODO: how do I make this work?
let externalChangeCallback: ABExternalChangeCallback = {
println("Address book changed externally!")
}
ABAddressBookRegisterExternalChangeCallback(addressBookRef, externalChangeCallback, nil)
return addressBookRef
}()
So how can I implement this in Swift?
Solution (with flaws):
As suggested by pNre, this is how I implemented it now:
In Objective-C:
AddressBookExternalChangeCallback.h:
#import <AddressBook/AddressBook.h>
void registerExternalChangeCallbackForAddressBook(ABAddressBookRef addressBookRef);
AddressBookExternalChangeCallback.m:
#import "AddressBookExternalChangeCallback.h"
void addressBookExternalChangeCallback(ABAddressBookRef addressBookRef, CFDictionaryRef info, void *context)
{
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"AddressBookDidChangeExternallyNotification" object:nil];
});
}
void registerExternalChangeCallbackForAddressBook(ABAddressBookRef addressBookRef)
{
ABAddressBookRegisterExternalChangeCallback(addressBookRef, addressBookExternalChangeCallback, nil);
}
In Swift:
after importing bridging header:
registerExternalChangeCallbackForAddressBook(addressBookRef)
A notification is posted whenever the address book changes. Only #objc classes can register for notifications, though, so is there a way to call a Swift function or method instead?
ABExternalChangeCallback is defined as
typealias ABExternalChangeCallback = CFunctionPointer<((ABAddressBookRef!, CFDictionary!, UnsafeMutablePointer<()>) -> Void)>
From the Xcode release notes:
However, you cannot call a C function pointer (CFunctionPointer) or
convert a closure to C function pointer type.
This means you can't assign a block the way you're doing.
However you can bypass this limitation calling ABAddressBookRegisterExternalChangeCallback in an objc function and calling it from your swift code.
Swift 2.0
if let addressBook = ABAddressBookCreateWithOptions(nil, nil) {
let ref = addressBook.takeRetainedValue()
let callback: #convention(c) (addressBookRef: ABAddressBookRef!, info: CFDictionaryRef!, context: UnsafeMutablePointer<Void>) -> Void = {
(addressBookRef, info, context) in
// do the things you want
}
let addressBookChangeCallback = unsafeBitCast(callback, ABExternalChangeCallback.self)
ABAddressBookRegisterExternalChangeCallback(ref, addressBookChangeCallback, nil)
}
in swift 4.0
class ABAddressBookManager: NSObject {
static let shared = ABAddressBookManager()
private var addressBook: ABAddressBook?
override init() {
super.init()
let addressBookForObserving = ABAddressBookCreate().takeRetainedValue()
let context = Unmanaged.passUnretained(self).toOpaque()
DispatchQueue.main.async {
ABAddressBookRegisterExternalChangeCallback(addressBookForObserving, { (addressBook, info, context) in
guard let context = context else {
return
}
let manager = Unmanaged<ABAddressBookManager>.fromOpaque(context).takeUnretainedValue()
//call manager's method
}
}
self.addressBook = addressBookForObserving
}
}
I got the same issue. But I have cleared the cache by calling ABAddressBookRevert() in addressbook object.
Is there a way on iOS to get the user agent of the device? I don't want to hard code it since I need the user agent for all devices and I need to append the user agent to a URL.
Thanks.
A simpler way to ascertain the user agent in iOS is to get it directly from a UIWebView using the accepted answer to this SO post. To quote that answer:
The solution was to create a UIWebView and then just use javascript to pull out the user agent.
UIWebView* webView = [[UIWebView alloc] initWithFrame:CGRectZero];
NSString* secretAgent = [webView stringByEvaluatingJavaScriptFromString:#"navigator.userAgent"];
Mobile application in every request must send his User-Agent header with build version and device information
User agent configuration info
So, user agent should be like:
User-Agent: <AppName>/version (<system-information>) <platform> (<platform-details>) <extensions>
For iOS:
User-Agent: <AppName/<version> (<iDevice platform>; <Apple model identifier>; iOS/<OS version>) CFNetwork/<version> Darwin/<version>
How to get each of the components?
Headers Key - u can hardcode or use some constant values
AppName and version - grab from Info.plist
let infoPlist = try? PListFile<InfoPlist>()
let appName = infoPlist.data.bundleName
let version = infoPlist.data.versionNumber
let build = infoPlist.data.buildNumber
Info about Device
modelName - you can obtain like described here
let modelName = UIDevice.current.modelName
other components:
let platform = UIDevice.current.systemName
let operationSystemVersion = ProcessInfo.processInfo.operatingSystemVersionString
CFNetwork version
static var cfNetworkVersion: String? {
guard
let bundle = Bundle(identifier: "com.apple.CFNetwork"),
let versionAny = bundle.infoDictionary?[kCFBundleVersionKey as String],
let version = versionAny as? String
else { return nil }
return version
}
from here
Darwin Version
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.release)
let darvinVersionString = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8,
value != 0 else {
return identifier
}
return identifier + String(UnicodeScalar(UInt8(value)))
}
from here
Result:
MyApp/1.8.199 (iOS; iPhone XS; Version 13.3 (Build 17C45)) CFNetwork/1121.2.1 Darvin/19.3.0
You don’t actually need to make the request in order to get the user-agent. Just return NO from the following delegate method and retain the user-Agent header:
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
It might look something like this:
-(BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
userAgent = [[request valueForHTTPHeaderField:#"User-Agent"] copy];
NSLog(#"user-agent: %#", userAgent);
_webView.delegate = nil;
[_webView release];
return NO;
}
(iOS 8.0, *)
Since UIWebView is deprecated in iOS 12, you should use WKWebView instead.
Since WKWebView.evaluateJavaScript(_:) result is now async, this implementation solve a common requirement to have userAgent available in your own REST api call.
import WebKit
class UAString {
static var userAgent : String = ""
#discardableResult init(view parent: UIView) {
if UAString.userAgent.isEmpty {
let webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
webView.translatesAutoresizingMaskIntoConstraints = false
parent.addSubview(webView)
webView.evaluateJavaScript("navigator.userAgent") { result, _ in
UAString.userAgent = result as? String ?? ""
}
}
}
}
Now last part you can implement this class in your initial view controller as follow:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
UAString(view: self.view)
}
then you can access the attribute as UAString.userAgent
Objective-C
WKWebView *webView = [WKWebView new];
res = [webView valueForKey:#"userAgent"];
Swift
let ua = WKWebView().value(forKey: "userAgent")
Dont forget to import WebKit 😂
Swift 3.x, 4.x, 5.x, & above
As sometime traditional UIWebView get's memory leaked so instead use always WKWebView (far better from UIWebView)
import WebKit
var webViewForUserAgent: WKWebView?
and get userAgent by calling below function & you can also set it to your other variable
func getUserAgent() {
webViewForUserAgent = WKWebView() // must initialize
webViewForUserAgent?.evaluateJavaScript("navigator.userAgent") { (result, error) in
//
if error != nil {
print("Error occured to get userAgent")
return
}
//
if let unwrappedUserAgent = result as? String {
print("userAgent: \(unwrappedUserAgent)")
} else {
print("Failed to get userAgent")
}
}
}
A simpler way to ascertain the user agent in iOS is to get it directly from a UIWebView using the accepted answer to this SO post.But this way has two disadvantages:
1、UIWebView's first allocation may take too much time in initializing webview context.
2、the code must be executed in main thread. This may stuck main thread.
If you know the tricks of how to use private methods while avoiding the refusal of App Store Review.
You can try the following code:
#define CALL_PRIVATE_INSTANCEMETHOD(x,sel,q)\
{\
SEL selector = NSSelectorFromString([NSString stringWithFormat:#"%#",##sel]);\
if ([x respondsToSelector:selector]) {\
_Pragma("clang diagnostic push")\
_Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"")\
q=[x performSelector:selector];\
_Pragma("clang diagnostic pop")\
}\
}\
#define CALL_PRIVATE_CLASSMETHOD_ONEPARAM(x,sel,p,q)\
{\
SEL selector = NSSelectorFromString([NSString stringWithFormat:#"_%#:",##sel]);\
if ([x respondsToSelector:selector]) {\
_Pragma("clang diagnostic push")\
_Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"")\
q=[x performSelector:selector withObject:p];\
_Pragma("clang diagnostic pop")\
}\
}\
+ (NSString *)standardUserAgent{
NSString *buildVersion = nil;
CALL_PRIVATE_INSTANCEMETHOD([UIDevice currentDevice], buildVersion,buildVersion);
Class webViewCls = NSClassFromString([NSString stringWithFormat:#"%#%#",#"Web",#"View"]);
NSString *standardUA = nil;
NSString *versions = [NSString stringWithFormat:#"Mobile/%#",buildVersion];
CALL_PRIVATE_CLASSMETHOD_ONEPARAM(webViewCls, standardUserAgentWithApplicationName,versions,standardUA);
return standardUA;
}