iOS 8 + Swiftで、performSelector:withObject:がない!!

國居貴浩氏のiPhoneアプリ開発のコツとツボ35のサンプルを、Swiftで書き換えることを試みています。

「Q05 UIViewにボタンの機能を追加するには?」 の「3 ターゲット/アクションデザインパターの実装」のサンプルを書き換えようとしました。ところが、Swiftでは、performSelector:withObject:メソッドがないんですね。メモリーリークを起こすことがあるとかで廃止になったようです。ググると、How to implement callback/selector with performSelector in swift?が引っかかりました。dispatch_afterを使って遅延実行を行うといいようです。参考にして実装してみました。

Button.swift

import UIKit

class Button: UIView {

var target:NSObject?
var action: Selector?

func myMethod(firstParam: String, setCallbackObject obj: AnyObject, withMySelector selector: Selector) {
if obj.respondsToSelector(selector) {
var myTimer: NSTimer = NSTimer.scheduledTimerWithTimeInterval(0.0, target: obj, selector: selector, userInfo: nil, repeats: false)
myTimer.fire()
} else {
println("Warning: does not respond to given selector")
}
}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
}
*/

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
self.myMethod("thisfirstis", setCallbackObject: target!, withMySelector: action!);
}

そして、AppDelegate.swift


func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
// Override point for customization after application launch.
self.window?.backgroundColor = UIColor.whiteColor()

let button = Button(frame: CGRectMake(100, 100, 100, 100));
button.backgroundColor = UIColor.blueColor();
self.window?.addSubview(button);

button.target = self
button.action = "touched";

self.window?.makeKeyAndVisible()
return true
}

func touched() {
self.window?.backgroundColor = UIColor.blackColor()
}

これでうまくいきました。しかし、次の「4 ターゲット/アクションデザインパターンで色パレットを作る」の実装をやろうとすると、上の方法ではうまくいきません。push:メソッドは、Buttonインスタンスを引数に取るからです。いろいろ調べたら、Alternative to performSelector in Swift?にヒントがあり、アクションメソッドが引数を持つ場合は、dispatch_afterとNSThreadのdetachNewThreadSelectorを使うといいようです。そこで実装しました。

Button.swift

import UIKit

class Button: UIView {

var target: NSObject?
var action: Selector?

/*
* iOS 8 + Swiftでは、performSelector:withObjectが使えないので、dispatch_afterを使用。
* 引数を持つアクションのケースでは、NSThread.detachNewThreadSelector を使用。
* http://stackoverflow.com/questions/24158427/alternative-to-performselector-in-swift
* Buttonクラスは、AppDelegateクラスの詳細(push:のこと)を知らない。
*/
func myMethod(aSelector: Selector, anObject: NSObject, sender: Button) {
if (anObject.respondsToSelector(aSelector)) {
let delegateObj = anObject as AppDelegate
let delay = 0.02 * Double(NSEC_PER_SEC)
var time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
NSThread.detachNewThreadSelector(aSelector, toTarget:delegateObj, withObject: sender)
})
} else {
println("Warning: does not respond to given selector")
}

}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
}
*/

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
self.myMethod(action!, anObject: target!, sender: self)
}
}

そして、AppDelegate.swift

var window: UIWindow?
var indicator: UIView?

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
// Override point for customization after application launch.
self.window?.backgroundColor = UIColor.whiteColor()

var r: CGRect = CGRectMake(100, 100, 44, 44)
indicator = UIView(frame: r)
if (indicator != nil) {
self.window?.addSubview(indicator!)
indicator?.backgroundColor = UIColor.blackColor()
}

r = CGRectOffset(r, r.size.width + 10, 0)
for var i = 0; i < 7; i++ { var bt: Button = Button(frame: r) bt.backgroundColor = UIColor(hue: CGFloat(i)/7.0, saturation: 1.0, brightness: 1.0, alpha: 1.0) bt.target = self bt.action = "push:" self.window?.addSubview(bt) r = CGRectOffset(r, 0, r.size.height) } self.window?.makeKeyAndVisible() return true } func push(sender: Button) { indicator?.backgroundColor = sender.backgroundColor }

引用ページでは、detachNewThreadSelectorのtoTargetの引数は、selfですが、今の場合、ターゲットがAppDelegateなので、Buttun.swiftで、anObject をAppDelegateにキャストして、引数に入れるのがキモです。この実装方法にたどり着くまで、Buttonクラスは、AppDeleagteクラスのpushメソッドの引数のことをどうやって知るのだろうと疑問に思っていました。http://stackoverflow.com/questions/24985716/in-swift-how-to-call-method-with-parameters-on-gcd-main-thread などを参考にすると、anObjectをAppDelegateにキャストすると、Buttonクラスで、AppDelegateクラスにあるpush:メソッドを呼ぶことはできます。しかし、これはButtonクラスがAppDelegateクラスの実装に依存するので、アカンやつです。替わりに、ここで採用した実装方法だと、Buttonクラスは、AppDelegateクラスの詳細な実装、push:メソッドのことを知らないので、ターゲット/アクションデザインパターンになっています。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です