肇鑫的技术博客

业精于勤,荒于嬉

微博账户过期提示算法的改进

对于使用RestAPI的应用,微博强制用户必须每30天登录一次。我原本的思路是类似这样的:

设备1:@小明 x号登录,x+27天后开始提醒。
设备2:@小明 y号登录,y+27天后开始提醒。(x < y)

但是实际使用中我发现,当x+30之后,在不重新登录账户@小明的情况下,还是可以通过@小明的验证信息发送微博。

分析

表面上,设备1上的授权是过期的,但是为什么还能继续发微博呢?我使用微博提供的API查询了token的信息。结果显示,实际上的设备1上的授权时间,要比x+30要长。

我分析,可是这样的。我原本以为微博服务器会记录每次用户登录的授权,即

设备1:@小明 x号登录,x+30授权结束。
设备2:@小明 y号登录,y+30授权结束。 (x<y)

但实际上,为了方便,微博根本没有记录每次的过期时间,而是每次小明通过同一个应用的授权进行登录时,就自动延长了该授权对应的时间。所以实际上发生的,可能是这样的:

设备1:@小明 x号登录,x+30授权结束。
设备2:@小明 y号登录,y+30授权结束。 (x<y)
因为x<y,所以设备1和2上的@小明,都变成y+30授权结束。

结论

根据上面的分析,新算法就变成了,当x+27时,先向微博服务器进行查询,查看剩余时间是否不足3天,如果超出,则先不进行提示,而是更新下一个时点,这样用户就可以少登录几次了。

The Real Reason of Set NSMenuDelegate in Interface Builder Not Working

When implemented the "Open Recent..." menu item under "File" menu, I connected the menu of "Open Recent"'s delegate to "AppDelegate" class. However, the delegate functions never called.

nsmenudelegate_issue_with_interface_builder

I tried to connect the NSMenuDelegate with the main menu, the result was the same.

I looked this issue up in stackoverflow site. There are many questions on this. Someone says that "Menu delegates are not used that often, so Apple hasn't made them too easy to set up in Interface Builder. Instead, do this in awakeFromNib:".

I don't accept the theory. But the answer does imply that setting NSMenuDelegate in code works.

Then I looked up on how to implementing "Open Recent..." menu. I found this post, Respond to Open Recent clicks in NSMenu. I was glad that I could use NSDocumentController to get the feature of "Open Recent...".

But the routine approach is not suit for my app.

For apps like Pages or Numbers, the files they open varies every time on names. However, for Xliff Tool, the files it opens are exported by Xcode, and the names are fixed. So when using the default approach of NSDocumentController, the recent files shown may be the same.

open_recent_issue

As you can see in above picture, there are two files with the same name. In fact they are in different paths. I need to show the files in full paths instead of just filenames.

So the question is back again. I have to make functions to conform to NSMenuDelegate.

What the magic Apple does to make the "Open Recent..." menu working?

Apple must have implemented its own class that conforms to NSMenuDelegate protocol. Since in Interface Builder I could not find any, I would debug it on AppDelegate's applicationDidFinishLaunching(_:) function.

I connect a @IBOutlet of menu of "Open Recent..." to "AppDelegate", and found that when app runs, the openRecentMenu has been set a NSMenuDelegate called NSDocumentControllerSubMenuDelegate.

open_recent_menu_outlet

After another digging, I found that NSDocumentControllerSubMenuDelegate is a hidden class that should not be used by third-party developers.

Answer

The answer of NSMenuDelegate set in Interface Builder not working is that for mechanism of magic like "Open Recent...", Apple resets the NSMenuDelegate of all menus under an app's main menu.

Anyone who wants to use a NSMenuDelegate, should set it in code.

My Own Solution

After set the NSMenuDelegate, the rest is easy.

extension AppDelegate:NSMenuDelegate {
    func menuNeedsUpdate(_ menu: NSMenu) {
        let clearMenuMenuItem = menu.items.last!
        let urls = NSDocumentController.shared.recentDocumentURLs
        let menuItems = urls.map {
            NSMenuItem(title: $0.path, action: #selector(openFile(_:)), keyEquivalent: "")
        }
        
        menu.items = [
            menuItems,
            [NSMenuItem.separator(), clearMenuMenuItem]
        ].flatMap({$0})
    }
}

Others

The differences between @ojbc and @IBAction through an interesting bug/feature of Interface Builder.

The Differences between @ojbc and @IBAction through an Interesting Bug/feature of Interface Builder

Last week I started an open source app Xliff Tool. It was an app to help developers translate Xliff files exported by Xcode.

We know that for the main menu of an app, its menu items look up the responder chain to find actions to perform. So those functions need to be dynamic. For example, a "Open..." menu item under "File" menu, it looks up for a function called openDocument(_:), so I could create a function in AppDelegate.swift like this:

@objc func openDocument(_ sender: Any?) {
    ...
}

However, when I created another customized menu item of my own, I could not find the function name in first responder with Interface Builder.

@objc func openDatabaseDirectory(_ sender: Any?) {
    ...
}

The fix was easy, just changed @objc to @IBAction, and the function name would be shown.

@IBAction func openDatabaseDirectory(_ sender: Any?) {
    ...
}

Q1: Why the prior @objc function was shown but the latter wasn't?

The answer is the prior @objc function wasn't shown. What was shown in Interface Builder is another @IBAction from NSDocumentController.

/* The action of the File menu's Open... item in a document-based application. The default implementation of this method invokes -beginOpenPanelWithCompletionHandler:, unless -fileNamesFromRunningOpenPanel is overridden, in which case that method is invoked instead for backward binary compatibility with Mac OS 10.3 and earlier. If an array other than nil is obtained from that call, it invokes -openDocumentWithContentsOfURL:display:completionHandler: for each URL and, if an error is signaled for any of them, presents the error in an application-modal panel.
*/
@IBAction open func openDocument(_ sender: Any?)

So in Interface Builder the @IBAction function was shown and in app runtime, the dynamic function on the first responder is the @objc one.

Both @IBAction and @objc are dynamic/objective-c functions, one difference is that the prior can be shown in Interface Builder.

An interesting bug/feature of Interface Builder

There was a "Save as..." menu item under "File" menu, connecting to saveAs(_:), I firstly implemented that function

@objc func saveAs(_ sender: Any?) {
    ...
}

Then I changed the "Save As..." menu item to "Export Xliff File...", and refactored the name of the function as well.

@objc func exportXliffFile(_ sender: Any?) {
    ...
}

Then I ran the app, the @objc func exportXliffFile(_ sender: Any?) still worked.

interface_builder_issue

Let me explain this. Though in the above picture it was shown that the menu item "Export Xliff File..." is connected to "exportXliffFile:" function of first responder. As we explained in Q1, there was no corresponding @IBAction function named "exportXliffFile:", so no one could choose "exportXliffFile:" in Interface Builder at all.

Q2: Then why was "exportXliffFile:" connected in Interface Builder and it still worked?

The answer is the operations that I did.

  1. There is @IBAction in NSDocument.saveAs(_:). So @objc func saveAs(_ sender: Any?) worked.
  2. When refactored in Xcode, both the name of @objc func saveAs(_ sender: Any?) and the connection "saveAs:" in Interface Builder were changed.
  3. The name of @IBAction in NSDocument.saveAs(_:) was unchanged as it was in a readonly header.
  4. When the app ran, the menu item looked up for a function called "exportXliffFile:" and found.

You can not pick a @objc function in Interface Builder, but if you refactor its name from a @IBAction, it will still work.

Others

The Real Reason of Not Working NSMenuDelegate with Interface Builder