ラベル swift の投稿を表示しています。 すべての投稿を表示
ラベル swift の投稿を表示しています。 すべての投稿を表示

2018年7月15日日曜日

UICollectionViewの並び替え(iOS11編)

「UICollectionViewの並び替え(よくある編)」のつづきです。


並び替えの出来るUICollectionViewは前回の方法で実現できますが、iOS11で追加されたドラッグ・アンド・ドロップの機能を使った方法もあります。

このドラッグ・アンド・ドロップ機能は、本来はiPad上で2つのアプリを開いた状態で、アプリをまたいだセルのドラッグ・アンド・ドロップで、データを受け渡すために導入されたものです。iPhoneでは、複数のアプリを開くことは出来ませんので、同じアプリ内でのドラッグ・アンド・ドロップになります。

今回は、このドラッグ・アンド・ドロップをUICollectionViewの中に適用することでセルの並び替えを実装します。


♪ ♪ ♪



まず、前回「UICollectionViewの並び替え(よくある編)」で作成した「とりあえずUICollectionViewを表示」のプロジェクトを用意します。ここにドラッグ・アンド・ドロップの処理を追加していきます。追加するのはUICollectionViewDragDelegateとUICollectionViewDropDelegateです。



ViewController+UICollectionViewDragDelegate.swift
ドラッグ開始(リフト)時の処理です。
indexPathでセルの位置がわかるので、ドラッグされるアイテムとしてUIDragItemの配列にして返します。



ViewController+UICollectionViewDropDelegate.swift
ドラッグ終了(ドロップ)時の処理です。



ViewController.swift
最後にデリゲートの設定を忘れないようにして、完成です。


iOS10までしか対応されていないiPhone5などがまだ現役だったりすることを考えると、ちょっと採用しづらいかもしれませんが、こっちにはこっちの利点があると思うので、この方法も選択肢のひとつとしておくのも良いのではないでしょうか。


iOS11のドラッグ・アンド・ドロップ機能は、この本が詳しいです。
iOS 11 Programming
  • 著者:堤 修一,吉田 悠一,池田 翔,坂田 晃一,加藤 尋樹,川邉 雄介,岸川克己,所 友太,永野 哲久,加藤 寛人,
  • 発行日:2017年11月16日
  • 対応フォーマット:製本版,PDF
  • PEAKSで購入する

2018年7月1日日曜日

UICollectionViewの並び替え(よくある編)

並び替えが出来るUICollectionViewに関する備忘録です。


まず、単純にUICollectionViewを表示するプロジェクトを作成しておきます。セルにラベルを用意して、番号を表示させているだけです。storyboardで、セルに色を付けておくと動作チェックがやりやすいかと思います。


ViewController.swift
ラベルに表示するデータを用意しています。
UICollectionViewDataSourceとUICollectionViewDelegateへの、selfのセットがありませんが、storyboard上で設定しておくと良いと思います。

右ドラッグで、クイっと。



ViewController+UICollectionViewDataSource.swift
セル上のラベルにインデックスを表示しています。


忘れがちですね、reuse idの設定



ViewController+UICollectionViewDelegate.swift
ラベルの表示だけなので、特に処理はありません。



CollectionViewCell.swift
セルにはラベルだけです。



これで実行すれば、とりあえずCollectionViewが表示されます。

とりあえず表示だけ


この状態のプロジェクトは次回にまた使うので、コミットをしておいてください。単純にコピーして取っておいてもかまいません。



♪ ♪ ♪



さて、ここから並べ替えが出来るように修正します。行うことは3つだけです。
  1. CollectionViewのセルの移動を可能にする。
  2. セルの移動処理を追加する。
  3. ロングタップのリスナーを登録する


ViewController+UICollectionViewDataSource.swift
collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath)でtrueを返して、セルの移動を可能にしています。

移動したときの挙動を、collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)に書きます。この例では元データの配列を移動しているだけです。



ViewController+LongPress.swift
CollectionViewの長押しのリスナーです。



ViewController.swift
あとは、ViewControllerのviewDidLoad()で、長押しのリスナーを登録します。
これで、セルの移動が可能になります。


めでたし、めでたし。で、「UICollectionViewの並び替え(iOS11編)」に続きます。


2018年4月1日日曜日

使ってみた(App Extension編) おまけ

「使ってみた(App Extension編) その5」からのつづきです。AppExtensionの話題からどんどん離れていきますが、今回はおまけです。


繰り返しになりますが、AppExtensionと収容アプリケーションは別アプリ扱いです。

そのため、実際のユースケースでは、AppExtensionを使った後ですぐに収容アプリケーションを起動するとは限りません。AppExtensionでのコンテンツの保存を繰り返した後に、収容アプリケーションを起動するというケースも普通にあるでしょう。

そこで、もうひと工夫して、AppExtensionでUserDefaultに保存するときに、Contents型の配列でため込んで、収容アプリケーションで取得するときにまとめて取得するようにします。

好都合なことに、Codableの配列はCodableなのでそのままエンコード、デコードできます。

UserDefaultは上書き保存ですから、AppExtensionでデータを保存するときには、いったんデータを取得してから、その配列に新たにデータを追加してから保存し直します。

これでAppExtensionでどんどん追加保存できます。
    /// コンテンツをApp Groupのユーザーデフォルトに保存
    ///
    /// - Parameter content:
    private func saveContent(content: String) {
        guard let userDefaults = UserDefaults.init(suiteName: "group.jp.blowbend.ios.test.AwesomeApp") else {
            print("userdefault: app group suite name is nil")
            return
        }
        
        //保存済みのデータの取得
        var contents: [Contents] = []
        if let object = userDefaults.object(forKey: "user default key content") {
            if let data: Data = object as? Data {
                do {
                    contents = try JSONDecoder().decode([Contents].self, from: data)
                } catch {
                    print("ActionViewController: JSONDecoder decode error")
                }
            }
        }
        
        let c = Contents(key: 123, text: content, date: Date())
        contents.append(c) //新たにデータを追加
        do {
            let data = try JSONEncoder().encode(contents) //Data型に変換
            userDefaults.set(data, forKey: "user default key content")
            userDefaults.synchronize()
        } catch {
            print("userdefault: contents encode error!!")
        }
    }

収容アプリケーションでデータを取得するときは、単純に配列で受け取ります。貯まっていたデータを受け取ったら、removeObjectでデータを削除して空にします。
    /// App Groupのユーザーデフォルトからコンテンツを取得
    private func getContentFromAppGroup() {
        guard let userDefaults = UserDefaults.init(suiteName: "group.jp.blowbend.ios.test.AwesomeApp") else {
            print("userdefault: app group suite name is nil")
            return
        }
        guard let content = userDefaults.object(forKey: "user default key content") ?? nil else {
            print("userdefault: no content")
            return
        }
        guard let data = content as? Data else {
            print("userdefault: no data")
            return
        }
        
        do {
            let contents = try JSONDecoder().decode([Contents].self, from: data) //Content型の配列で取得
            for c in contents {
                print("c.key =\(c.key)")
                print("c.text=\(c.text)")
                print("c.date=\(c.date)")
            }
        } catch {
            print("userdefault: JSONDecoder decode error")
        }

        userDefaults.removeObject(forKey: "user default key content") //すべてのデータの削除
    }


どうでしょうか。これでAppExtensionの呼び出しごとに、(実は動いていない)収容アプリケーションにデータを送っているかのように見せられますね。このあたりは、その処理に合わせて色々工夫のしどころかもしれません。

以上で、使ってみた(App Extension編)は終わりです。おつかれさまでした。


こんな風にAppExtensionを使ったアプリをリリースしました。よろしくね。


「おまけのオマケ」
App Extensionを含むアプリをiTunes Storeにリリースするときは、ビルド番号を収容アプリケーションとApp Extensionとで揃える必要があります。

もし違うままアップロードすると、

WARNING ITMS-90473: "CFBundleVersion Mismatch. The CFBundleVersion value '2' of extension 'AwsomeApp.app/PlugIns/AwsomeAppShare.appex' does not match the CFBundleVersion value '1' of its containing iOS application 'AwsomeApp.app'."

というワーニングが出ます。ワーニングなので実害はないのかもしれませんが、まぁ、不用なトラブルは避けた方がいいですよね。お気を付けください。


♪♪♪


こちらもどうぞ。
使ってみた(App Extension編) その1
使ってみた(App Extension編) その2
使ってみた(App Extension編) その3
使ってみた(App Extension編) その4
使ってみた(App Extension編) その5

iOS 11 Programming
  • 著者:堤 修一,吉田 悠一,池田 翔,坂田 晃一,加藤 尋樹,川邉 雄介,岸川克己,所 友太,永野 哲久,加藤 寛人,
  • 発行日:2017年11月16日
  • 対応フォーマット:製本版,PDF
  • PEAKSで購入する

2018年3月15日木曜日

使ってみた(App Extension編) その5

「使ってみた(App Extension編) その4」からのつづきです。

前回までで一通りAppExtensionから収容アプリケーションまで、コンテンツとしてString型のデータを受け渡しました。今回はAppExtensionとは直接関係ありませんが、データのシリアライズについて書いておこうと思います。

ここまでのサンプルでは、AppExtensionでコンテンツを保存して、収容アプリケーションの起動時に取得していました。

AppExtensionから複数のデータを受け渡したいこともあります。もちろん、UserDefaultのキーをそれぞれ振って、独立したデータとして扱うのもありですが、意味のあるまとまりのデータであれば、構造体やクラスにしてシリアライズして使いたいところです。


そこで、今回はContentsという構造体を作ってみました(ちなみにクラスでも同様です)。
import UIKit

struct Contents: Codable {
    
    // MARK: - PROPERTY

    var key: Int //何かのキー
    var text: String //内容
    var date: Date //日付

    // MARK: - INITIALIZER
    
    init() {
        self.key = 0 //何かのキー
        self.text = "" //内容
        self.date = Date() //作成日付
    }
    
    init(key: Int, text: String, date: Date) {
        self.key = key //何かキー
        self.text = text //内容
        self.date = date //日付
    }
}
自動でエンコード、デコードできる、Codableというプロトコルがポイントです。

Contents構造体がCodableを備えているのはもちろんですが、中に含まれるプロパティーもCodableである必要があります。

Contents構造体の中にInt型, String型, Date型のプロパティーがありますが、これらの型はすべてデフォルトでCodableなので問題ありません。

このContents.swiftを収容アプリケーションの中に作成します。そしてこのソースにAppExtensionからもアクセスできるように、AppExtensionのTargetのBuild PhasesのCompile Sourcesに指定します。

両方に同じソースを作る必要はありませんからね。



これでContents型が収容アプリケーション、AppExtensionの両方から使用できるようになりました。あとはエンコード・デコードしてUserDefaultに保存・取り出しをします。


AppExtensionでは、Contents型をData型にエンコードしてから、UserDefaultに格納します。
    /// コンテンツをApp Groupのユーザーデフォルトに保存
    ///
    /// - Parameter content:
    private func saveContent(content: String) {
        guard let userDefaults = UserDefaults.init(suiteName: "group.jp.blowbend.ios.test.AwesomeApp") else {
            print("userdefault: app group suite name is nil")
            return
        }
        
        let contents = Contents(key: 123, text: content, date: Date())
        do {
            let data = try JSONEncoder().encode(contents) //Data型に変換
            userDefaults.set(data, forKey: "user default key content") //データを保存
            userDefaults.synchronize()
        } catch {
            print("userdefault: contents encode error!!")
        }
    }


収容アプリケーションでは、UserDefaultからData型で取得してから、Contents型にデコードします。
    /// App Groupのユーザーデフォルトからコンテンツを取得
    private func getContentFromAppGroup() {
        guard let userDefaults = UserDefaults.init(suiteName: "group.jp.blowbend.ios.test.AwesomeApp") else {
            print("userdefault: app group suite name is nil")
            return
        }
        guard let content = userDefaults.object(forKey: "user default key content") ?? nil else {
            print("userdefault: no content")
            return
        }
        guard let data = content as? Data else {
            print("userdefault: no data")
            return
        }
        
        do {
            let content = try JSONDecoder().decode(Contents.self, from: data)
            print("content.key =\(content.key)")
            print("content.text=\(content.text)")
            print("content.date=\(content.date)")
        } catch {
            print("userdefault: JSONDecoder decode error")
        }

        userDefaults.removeObject(forKey: "user default key content") //無条件でデータの削除
    }

これで、構造体をシリアライズしての受け渡しはOKです。

今回はここまで。


「使ってみた(App Extension編) おまけ」に続きます。


♪♪♪


こちらもどうぞ。
使ってみた(App Extension編) その1
使ってみた(App Extension編) その2
使ってみた(App Extension編) その3
使ってみた(App Extension編) その4
使ってみた(App Extension編) おまけ

iOS 11 Programming
  • 著者:堤 修一,吉田 悠一,池田 翔,坂田 晃一,加藤 尋樹,川邉 雄介,岸川克己,所 友太,永野 哲久,加藤 寛人,
  • 発行日:2017年11月16日
  • 対応フォーマット:製本版,PDF
  • PEAKSで購入する

2018年3月1日木曜日

使ってみた(App Extension編) その4

「使ってみた(App Extension編) その3」からのつづきです。

前回はApp Groupの設定ができたところまででした。あとはUserDefaultで保存と読み出しを追加するだけです。


App Extension側でUserDefaultにコンテンツを保存します。
import UIKit
import Social
import MobileCoreServices

class ShareViewController: SLComposeServiceViewController {

    //省略

    override func didSelectPost() {
        guard let inputItems = self.extensionContext?.inputItems as? [NSExtensionItem] else {
            return
        }
        guard let providers = inputItems[0].attachments as? [NSItemProvider] else {
            return
        }

        for provider in providers {
            if provider.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
                provider.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil, completionHandler: { (item, _) in
                    OperationQueue.main.addOperation {
                        if let textItem = item as? String {
                            print("??? AppExtension: textItem= \(textItem)") //printで出力
                            self.saveContent(content: textItem) //コンテンツをApp Groupのユーザーデフォルトに保存
                        }
                    }
                })
                break
            }
        }

        self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
    }

    //省略
    
    /// コンテンツをApp Groupのユーザーデフォルトに保存
    ///
    /// - Parameter content:
    private func saveContent(content: String) {
        guard let userDefaults = UserDefaults.init(suiteName: "group.jp.blowbend.ios.test.AwesomeApp") else {
            print("userdefault: app group suite name is nil")
            return
        }
        
        userDefaults.set(content, forKey: "user default key content")
        userDefaults.synchronize()
    }
}
AppGroupのUserDefaultといっても、UserDefault.init(suiteName:)でAppGroup IDを指定するだけで、ごくごく普通に使用できます。


コンテンツを受け取る側の収容アプリケーションですが、AppDelegateで取得することにします。

もちろん、処理として必要な場所であればどこでもかまわないですが、didFinishLaunchingWithOptionsとapplicationWillEnterForegroundの2カ所で行っておけば、アプリを開いたタイミングで取得できると思います。ここで取得しておけば、アプリ内で自由に取り回しやすいのではないでしょうか。
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        getContentFromAppGroup() //App Groupのユーザーデフォルトからコンテンツを取得
        
        return true
    }

    //省略

    func applicationWillEnterForeground(_ application: UIApplication) {
        getContentFromAppGroup() //App Groupのユーザーデフォルトからコンテンツを取得
    }

    //省略

    // MARK: - METHODS FOR GET DATA FROM AppGroup
    
    /// App Groupのユーザーデフォルトからコンテンツを取得
    private func getContentFromAppGroup() {
        guard let userDefaults = UserDefaults.init(suiteName: "group.jp.blowbend.ios.test.AwesomeApp") else {
            print("userdefault: app group suite name is nil")
            return
        }
        guard let content = userDefaults.object(forKey: "user default key content") ?? nil else {
            print("userdefault: no content")
            return
        }
        guard let textItem = content as? String else {
            print("userdefault: no content")
            return
        }

        userDefaults.removeObject(forKey: "user default key content") //無条件でデータの削除
        print("??? Containing App: textItem= \(textItem)") //printで出力
    }
}

コンテンツの2重取り込みを避けるために、取得できたか失敗したかに関わらず、無条件に削除しています。本来なら、取得したデータの内容をチェックしたりエラーハンドリングをきちんとしておくべきところですが、これはサンプルなので処理のシンプルさを優先して省略しました。

これを実行すると、データが受け渡されていることが確認できると思います。

今回はここまで。


「使ってみた(App Extension編) その5」に続きます。


♪♪♪


こちらもどうぞ。
使ってみた(App Extension編) その1
使ってみた(App Extension編) その2
使ってみた(App Extension編) その3
使ってみた(App Extension編) その5
使ってみた(App Extension編) おまけ

iOS 11 Programming
  • 著者:堤 修一,吉田 悠一,池田 翔,坂田 晃一,加藤 尋樹,川邉 雄介,岸川克己,所 友太,永野 哲久,加藤 寛人,
  • 発行日:2017年11月16日
  • 対応フォーマット:製本版,PDF
  • PEAKSで購入する

2018年2月15日木曜日

使ってみた(App Extension編) その3

「使ってみた(App Extension編) その2」からのつづきです。


前回はApp Extension内でコンテンツが取得できたところまででした。

App Extensionで取得したコンテンツを収容アプリケーションに受け渡すのには、App GroupでUserDefaultを使用します。データも少量ですし、セキュリティ的にもさほど重要ではないという前提です。

AppExtensionでクリティカルなデータを受け渡すケースがあるかどうかはわかりませんが、仮にそういった場合はKeychain Sharingを使うことになりそうです。すみません、使ったことがないので、よくわかりません。

いずれにしても、だめですよ、UserDefaultで重要なデータを扱っちゃ。


App Groupは複数のアプリでデータを共有する仕組みです。「使ってみた(App Extension編) その1」で書いたとおり、App Extensionと収容アプリケーションは独立しています(Bundle IDも違いますしね)。そのため、それぞれ別のアプリであるとして、App Groupを使用してデータを受け渡すわけです。


まず、収容アプリケーション側で、App Groupを設定します。

Targetで収容アプリケーションを指定して、CapabilitiesのApp GroupsのスイッチをONにします。



開発者IDで紐付けられたApp Group一覧が表示されますので、「+」を押して、新しいAppGroup IDを登録します。IDは自由に決められそうですが、group.で始まるのがお約束のようです。このAppGroup IDは自動的にApple Developerに登録されますので、ユニークである必要があり、Bundle IDを使うのが良いかもしれません。

group.+Bundle IDとか


同様に、TargetでApp Extensionを指定して、CapabilitiesのApp GroupsのスイッチをONにします。上で追加したAppGroup IDが一覧に追加されていますので、チェックをいれます。



これで、収容アプリケーションとApp Extensionが、"group.jp.blowbend.ios.test.AwesomeApp"というAppGroup IDで、データの共有をすることができようになりました。


あとはデータをUserdefault経由で受け渡すだけですが、今回はここまで。


「使ってみた(App Extension編) その4」に続きます。


♪♪♪


こちらもどうぞ。
使ってみた(App Extension編) その1
使ってみた(App Extension編) その2
使ってみた(App Extension編) その4
使ってみた(App Extension編) その5
使ってみた(App Extension編) おまけ

iOS 11 Programming
  • 著者:堤 修一,吉田 悠一,池田 翔,坂田 晃一,加藤 尋樹,川邉 雄介,岸川克己,所 友太,永野 哲久,加藤 寛人,
  • 発行日:2017年11月16日
  • 対応フォーマット:製本版,PDF
  • PEAKSで購入する


2018年2月1日木曜日

使ってみた(App Extension編) その2

「使ってみた(App Extension編) その1」からのつづきです。

前回はApp Extensionが起動されるところまででした。引き続き、App Extensionの設定と処理を見ていきます。

まず、App ExtensionのInfo.plistを修正します。

デフォルトの状態では、デバッグ用にサポートするコンテンツの設定が'TRUEPREDICATE'(つまり何でもあり)となっていますので、これを変更します。実際、リリース時には必ず変更する必要があります。
--(省略)--
<key>NSExtension</key>
<dict>
 <key>NSExtensionAttributes</key>
 <dict>
  <key>NSExtensionActivationRule</key>
  <string>TRUEPREDICATE</string>
 </dict>
 <key>NSExtensionMainStoryboard</key>
 <string>MainInterface</string>
 <key>NSExtensionPointIdentifier</key>
 <string>com.apple.share-services</string>
</dict>
--(省略)--

今回はテキストデータのみを扱うことにするので、以下のように変更します。
--(省略)--
<key>NSExtension</key>
<dict>
 <key>NSExtensionAttributes</key>
 <dict>
  <key>NSExtensionActivationRule</key>
  <dict>
   <key>NSExtensionActivationSupportsText</key>
   <true/>
   <key>NSExtensionActivationSupportsTextWithMaxCount</key>
   <integer>1</integer>
  </dict>
 </dict>
 <key>NSExtensionMainStoryboard</key>
 <string>MainInterface</string>
 <key>NSExtensionPointIdentifier</key>
 <string>com.apple.share-services</string>
</dict>
--(省略)--


せっかくなのでアイコンも設定します。前回書いたように、Share Extensionの場合は、メニューのアイコンは、収容アプリケーションのアイコンが自動的に設定されますので、収容アプリケーション側のAssetsにアイコン画像を登録します。

収容アプリのアイコンを設定

アイコンが表示されました

※ちなみにAction Extensionの場合は、収容アプリケーションのアイコンを自動的に使用してくれないので、Extensionのターゲットの側にアイコンを用意します。


さぁ、やっとデータの取得です。App Extension側でシェアするコンテンツを取得します。ShareViewController.swiftの中を見ていきます。

isContentValid()の返値で、Postボタンの使用の可否を指定します。シェアするコンテンツのチェックなどをすると良いと思います。例えば取得するべき文字列が空の時はfalseを返すようにするとか。とりあえず今回は常にtrueを返すようにしておきます。

Postが押されたときのイベントはdidSelectPost()です。ここでコンテンツを取得します。試しにprint文で出力してみると。。。
import UIKit
import Social
import MobileCoreServices

class ShareViewController: SLComposeServiceViewController {

    override func isContentValid() -> Bool {
        // ここにチェックなど
        return true
    }

    override func didSelectPost() {
        guard let inputItems = self.extensionContext?.inputItems as? [NSExtensionItem] else {
            return
        }
        guard let providers = inputItems[0].attachments as? [NSItemProvider] else {
            return
        }

        for provider in providers {
            if provider.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
                provider.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil, completionHandler: { (item, _) in
                    OperationQueue.main.addOperation {
                        if let textItem = item as? String {
                            //printで出力
                            print("??? textItem= \(textItem)")
                        }
                    }
                })
                break
            }
        }

        self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
    }

    override func configurationItems() -> [Any]! {
        // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
        return []
    }
}

ちゃんと取得できています。
2018-01-26 01:12:27.499855+0900 AwesomeAppShare[7507:175699] [core] postButtonTapped
2018-01-26 01:12:27.507693+0900 AwesomeAppShare[7507:175699] [core] SLComposeServiceViewController-animateSendCard
2018-01-26 01:12:27.511739+0900 AwesomeAppShare[7507:175699] [core] SLComposeServiceViewController-keyboardDidChange
2018-01-26 01:12:27.861908+0900 AwesomeAppShare[7507:175699] [core] animateCardSend animation finished
??? textItem= The Dutch were in most of the Olympic sailing competitions represented by the Dutch Olympic Sailing Team.
2018-01-26 01:12:27.885422+0900 AwesomeAppShare[7507:175699] [core] SLComposeServiceViewController dealloc 
2018-01-26 01:12:27.887536+0900 AwesomeAppShare[7507:175699] [core] SLSheetRootViewController dealloc

App Extensionは、できるだけ軽量である必要があります。Share Extensionのような、いわゆる「出来合いのテンプレート」だとそのまま使えばいいですが、Action Extensionのように「やろうと思えば色々できる」ものは、ついつい処理を盛りがちになってします。

そうすると、ユーザーがホストアプリケーションの共有メニューでそのExtensionを選んだときの反応が悪くなって、UXが台無しになってしまいます。

"App Extensionは、動作が高速で軽量であるとユーザが感じるようにしてください。App Extensionは素早く起動するように(1秒を大きく下回るように)設計します。起動が遅すぎるExtensionはシステムにより停止されます。"
「App Extensionプログラミングガイド」App Extensionを開発するより

そういったわけで、Extensionのターゲット内はできるだけ簡素に、収容アプリケーションにデータを渡すだけにして、主立った処理は収容アプリケーションの側で行う必要があります。


今回はここまで。

「使ってみた(App Extension編) その3」に続きます。


♪♪♪ こちらもどうぞ。 使ってみた(App Extension編) その1 使ってみた(App Extension編) その3 使ってみた(App Extension編) その4 使ってみた(App Extension編) その5 使ってみた(App Extension編) おまけ

iOS 11 Programming
  • 著者:堤 修一,吉田 悠一,池田 翔,坂田 晃一,加藤 尋樹,川邉 雄介,岸川克己,所 友太,永野 哲久,加藤 寛人,
  • 発行日:2017年11月16日
  • 対応フォーマット:製本版,PDF
  • PEAKSで購入する


2017年12月15日金曜日

コンテキストメニューを置き換えてみた

作成中のアプリで、UITextViewのコンテキストメニューに機能を追加したいと思いまして。

こういうの

調べたところ、touchesBeganとcanPerformActionをオーバーライドすればカスタマイズできそうとのことなのでこんな感じにしてみました。
protocol CustomedMenuTextViewDelegate: class {
    func customedMenu1(selectedText: String)
    func customedMenu2(selectedText: String)
    func customedMenu3(selectedText: String)
}

class CustomedMenuTextView: UITextView {
    
    weak var customedMenuDelegate: CustomedMenuTextViewDelegate?

    override func touchesBegan(_ touches: Set, with event: UIEvent?) {
        let menuController = UIMenuController.shared
        menuController.setTargetRect(CGRect.zero, in: self)
        menuController.arrowDirection = .down
        
        let menuItems = [
            UIMenuItem.init(title: "Menu1", action: #selector(menu1Selected(sender: ))),
            UIMenuItem.init(title: "Menu2", action: #selector(menu2Selected(sender: ))),
            UIMenuItem.init(title: "Menu3", action: #selector(menu3Selected(sender: ))),
            ]
        menuController.menuItems = menuItems
        menuController.setMenuVisible(true, animated: true)
    }
    
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        if action == #selector(menu1Selected(sender: )) ||
           action == #selector(menu2Selected(sender: )) ||
           action == #selector(menu3Selected(sender: )) {
            return true
        }
        
        return false
    }
    
    // MARK: - ACTION EVENT METHOD
    
    @objc private func menu1Selected(sender: Any) {
        guard let _ = sender as? UIMenuController else {
            print("error1")
            return
        }
        
        let selectedText = self.text(in: self.selectedTextRange!)!
        if let d = customedMenuDelegate {
            d.customedMenu1(selectedText: selectedText) //選択された文字列を返す
        }
    }
    
    @objc private func menu2Selected(sender: Any) {
        guard let _ = sender as? UIMenuController else {
            print("error2")
            return
        }
        
        let selectedText = self.text(in: self.selectedTextRange!)!
        if let d = customedMenuDelegate {
            d.customedMenu2(selectedText: selectedText) //選択された文字列を返す
        }
    }
    
    @objc private func menu3Selected(sender: Any) {
        guard let _ = sender as? UIMenuController else {
            print("error3")
            return
        }
        
        let selectedText = self.text(in: self.selectedTextRange!)!
        if let d = customedMenuDelegate {
            d.customedMenu3(selectedText: selectedText) //選択された文字列を返す
        }
    }
}

呼び出す側はこうします。

class ViewController: UIViewController, CustomedMenuTextViewDelegate {

    @IBOutlet weak var textView: CustomedMenuTextView!
    
    // MARK: - UIViewController: RESPONDING TO VIEW EVENTS
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        textView.isEditable = false
        textView.customedMenuDelegate = self
    }

    // MARK: - UIViewController: HANDLING MEMORY WARNINGS
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // MARK: - METHODS FOR CustomedMenuTextViewDelegate
    
    func customedMenu1(selectedText: String) {
        print("Menu1: selected text = " + selectedText)
    }
    
    func customedMenu2(selectedText: String) {
        print("Menu2: selected text = " + selectedText)
    }
    
    func customedMenu3(selectedText: String) {
        print("Menu3: selected text = " + selectedText)
    }
}

もう少し汎用性を持たせたいところですが、とりあえず動作するようなので我慢します。


こうなりました

Appleによる正規の方法ではなさそうです。

WKWebView(deprecatedだけどUIWebViewでもいいけど)でも、同じことをしたかったのですが、この方法ではダメでした。残念。

良い方法ないですかね?


「使ってみた(App Extension編) その1」に続きます。

iOS 11 Programming
  • 著者:堤 修一,吉田 悠一,池田 翔,坂田 晃一,加藤 尋樹,川邉 雄介,岸川克己,所 友太,永野 哲久,加藤 寛人,
  • 発行日:2017年11月16日
  • 対応フォーマット:製本版,PDF
  • PEAKSで購入する


2017年11月15日水曜日

Swift、フォー!

「フォー!」って叫んでたあの芸人さん、最近見ませんね。僕があまりテレビを見ないから、そう思うだけかもしれませんけど。


Xcode9になって、Swift4が使えるようになりました。

Swift3で書かれたプロジェクトを開くと、Swift4への変換のメッセージがでます。

なんだかframeworkやCocoaPodsも巻き込んでいきそうな感じです。以前のSwiftのアップデート時には、派手にソースが変更されて冷や汗をかきました。

今回ももしやと、びくびくしながら続けていくと、大した変更はありませんでした。

変換された部分は、#selector()で指定しているイベントのアクションの関数の定義に、@objcが付けられた程度です。

    //略
    let nc = NotificationCenter.default
    nc.addObserver(self,
                   selector: #selector(Foo.bar),
                   name: "piyo"
                   object: nil)
    //略
    @objc func bar() {
        //略
    }
    //略

しかし、なぜ@objcなんでしょうか。このプロジェクトはすべてSwiftで書かれていますし、何だか腑に落ちないです。どこから来たのobjc属性。

しかも追加された@objcの箇所にワーニングが出力されます。何か間違ってませんか?


Selector Expression
A selector expression lets you access the selector used to refer to a method or to a property’s getter or setter in Objective-C.

#selector(method name)
#selector(getter: property name)
#selector(setter: property name)

The method name and property name must be a reference to a method or a property that is available in the Objective-C runtime. The value of a selector expression is an instance of the Selector type.

(“The Swift Programming Language (Swift 3.1)”/Apple Inc.より)


ふむふむ。


セレクタはObjective-Cの概念であるため、Selector型を生成するにはメソッドがObjective-Cから参照可能である必要があります。メソッドをObjective-Cから参照可能にするには、objc属性を指定します。

(Swift実践入門/石川洋資,西山勇世 P289より)


なるほど。

どこかからObjctive-Cが湧いてきたわけではなく、そもそもセレクタはObjecive-Cのランタイムにおける概念なのですね。

これはSwift4の書式に変換してくれたというより、本来必要だった@objcを、厳しくなったコンパイラのために付けてくれたということなのでしょう。


試しに、@objcを削除してみるとエラーになります。

Argument of '#selector' refers to instance method 'bar()' that is not exposed to Objective-C
Add '@objc' to expose this instance method to Objective-C


ちなみに、objc属性の箇所のワーニングですが、

The use of Swift 3 @objc inference in Swift 4 mode is deprecated. Please address deprecated @objc inference warnings, test your code with “Use of deprecated Swift 3 @objc inference” logging enabled, and then disable inference by changing the "Swift 3 @objc Inference" build setting to "Default" for the "xxxxxx" target.

これは、TARGETSのBuild Settingsをswiftで検索して、Swift 3 @objc Inference(Onになっていると思います)を、Defaultにすると消えます。



これで安心してSwift4を使えますね。




2017年10月15日日曜日

帰ってきた凛としてSwift(くるくる編)

泣きながらiPhoneX対応をしています。なんであんなにワーニングが出るんでしょうか、Xcode9のStroyboard。

ウェブページを表示する画面を作っていたのですが、また見慣れないSwiftLintのワーニングが出たので、シリーズ続行です。


UIWebViewがdeprecatedになったので、WKWebViewを使うことにしました。

ページのロード中を示すインジケータの表示ですが、UIWebViewならUIWebViewDelegateを実装すれば開始と終了のイベントが取得できるので、丸いアニメーションインジケータを表示するのが簡単だと思います。WKWebViewでも、WKNavigationDelegateを実装すれば同様にコントロールできます。

(※ 今回のソースは全てSwift4で、最小限動くところまで削るためにターゲットをiOS11.0以降にしました。ご注意を。)

import UIKit
import WebKit

class SomeWeb4ViewController: UIViewController, WKNavigationDelegate {

    @IBOutlet weak var webView: WKWebView!
    @IBOutlet weak var indicatorView: UIActivityIndicatorView! //くるくるインジケータ
    
    override func viewDidLoad() {
        super.viewDidLoad()

        indicatorView.hidesWhenStopped = true //くるくるしてないときは、非表示
        
        //webページのロード
        webView.navigationDelegate = self
        let myURL = URL(string: "https://www.apple.com/")
        let myRequest = URLRequest(url: myURL!)
        webView.load(myRequest)
    }
    
    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        indicatorView.startAnimating() //くるくる開始
    }
    
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        indicatorView.stopAnimating() //くるくる停止
    }
}
まぁ、見たとおりです。WKNavigationDelegateで、Webページのロードの開始と停止のイベントを取得しています。処理もシンプルで分かりやすいですね。

ただ、WKWebViewはKVOに対応しているので、もう少し気の利いた制御をするのがお約束なのでしょうか。そんな情報が多いです。そこで、それらを真似てナビゲーションバーの下に、横に伸びるインジケータ表示にしました。

import UIKit
import WebKit

class SomeWeb2ViewController: UIViewController {

    @IBOutlet weak var webView: WKWebView!
    
    var progressView = UIProgressView()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        //プログレスバー
        let progFrame = CGRect(x: 0.0,
                               y: self.navigationController!.navigationBar.frame.size.height - 2.0,
                               width: self.navigationController!.navigationBar.frame.size.width,
                               height: 10.0)
        progressView = UIProgressView(frame: progFrame)
        progressView.progressViewStyle = .bar
        self.navigationController?.navigationBar.addSubview(progressView)
        
        //ここで、webViewのobserverを設定して
        webView.addObserver(self, forKeyPath: "loading", options: .new, context: nil)
        webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)

        //webページのロード
        let myURL = URL(string: "https://www.apple.com/")
        let myRequest = URLRequest(url: myURL!)
        webView.load(myRequest)
    }
    
    //ここで進捗を受信
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "estimatedProgress"{
            progressView.setProgress(Float(webView.estimatedProgress), animated: true)
        } else if keyPath == "loading"{
            UIApplication.shared.isNetworkActivityIndicatorVisible = webView.isLoading
            if webView.isLoading {
                progressView.setProgress(0.1, animated: true) //プログレスの開始
            } else {
                progressView.setProgress(0.0, animated: false) //プログレスを消去
            }
        }
    }
    
    deinit {
        webView.removeObserver(self, forKeyPath: "estimatedProgress")
        webView.removeObserver(self, forKeyPath: "loading")
    }
}
webViewに、"loading"と"estimatedProgress"のobserverを付加して、ロードの開始、進捗、終了を取得しています。

ちなみにdeinitにある、observerの削除がないと落ちるとのことですが、11.0では落ちなかったです。だから不要、というわけでもなさそうですが。

もちろんこれでちゃんと動作するのですが、SwiftLintがワーニングを出力します。


・block_based_kvo
Block Based KVO Violation: Prefer the new block based KVO API with keypaths when using Swift 3.2 or later. (block_based_kvo)


new block based KVO API with keypaths ってなに?って感じですが、 どうやらWKWebViewに付加したobserverを func observeValue(){} で受信する方法は古いようです。

そこで、Adopting Cocoa Design Patternsの「Key-Value Observing」を参考にして、 NSKeyValueObservationを使った方法に書き換えてみました。
import UIKit
import WebKit

class SomeWeb3ViewController: UIViewController {
    
    @IBOutlet weak var webView: WKWebView!
    var progressView = UIProgressView()
    
    var obsLoading: NSKeyValueObservation? //←これ
    var obsEstimatedProgress: NSKeyValueObservation? //←これ
    
    override func viewDidLoad() {
        super.viewDidLoad()

        //プログレスバー
        let progFrame = CGRect(x: 0.0,
                               y: self.navigationController!.navigationBar.frame.size.height - 2.0,
                               width: UIScreen.main.bounds.size.width,
                               height: 10.0)
        progressView = UIProgressView(frame: progFrame)
        progressView.progressViewStyle = .bar
        self.navigationController?.navigationBar.addSubview(progressView)
        
        //ここから→
        obsLoading = observe(\.webView.loading) { _, _ in
            UIApplication.shared.isNetworkActivityIndicatorVisible = self.webView.isLoading
            if self.webView.isLoading {
                self.progressView.setProgress(0.1, animated: true) //プログレスの開始
            } else {
                self.progressView.setProgress(0.0, animated: false) //プログレスを消去
            }
        }
        obsEstimatedProgress = observe(\.webView.estimatedProgress) { _, _ in
            self.progressView.setProgress(Float(self.webView.estimatedProgress), animated: true)
        }
        //←ここまで

        let myURL = URL(string: "https://www.apple.com/")
        let myRequest = URLRequest(url: myURL!)
        webView.load(myRequest)
    }
}
Swift4におけるKVOのベストプラクティスがよくわかっていないので、本当にこれが正しいのか自信がないのですが、とりあえずワーニングが消えて、ちゃんと動きました。内心、ちょっと怪しいとは感じています。ま、そのときはそのときで随時修正します。

それにしても、なにこのバックスラッシュ。Swiftこわい。


♪♪♪


こちらもどうぞ。
凛としてSwift
凛としてSwift (血闘編ノ壱)
凛としてSwift (血闘編ノ弐)
凛としてSwift (血闘編ノ参)


2017年5月15日月曜日

凛としてSwift (血闘編ノ弐)

SwiftLintネタ「凛としてSwift (血闘編ノ壱)」からの続きです。


・trailing_semicolon

いうまでもなく、文末のセミコロンですね。
気をつけていたんですが意外に多かったです。Objective-C混じりのプロジェクトなので、反射的に入力してしまっていたんですね。
self.imageFoo.alpha = 0.3; //むむ、セミコロンが!
self.imageBar.alpha = 0.3;
self.imageFoo.alpha = 0.3 //取りましたよ
self.imageBar.alpha = 0.3


・vertical_whitespace

2行以上、空の行がつづく時に出ます。
意図的に2行以上空けることはないので、これもうっかりですね。気がついていませんでした。
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    } 


    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return notes.count
    }


・trailing_comma

配列の最後の要素の後ろにカンマをつけるか否かです。
これは何というか、コーディングの流派というか、どっちもアリですし、だからこそSwiftの文法では両方OKになっているのだと思いますが、SwiftLintのデフォルトではカンマなしのようです。ここはSwiftLintに従うことにします。
        accidentials = [
            "picker_label_none",
            "picker_label_natural",
            "picker_label_sharp",
            "picker_label_flat", //←ここのカンマ
        ]
        accidentials = [
            "picker_label_none",
            "picker_label_natural",
            "picker_label_sharp",
            "picker_label_flat" //←取りましたよ

        ]


・colon

コロンの後には空白を1個入れましょう、ですね。はい、その通りですね。
let hogeRequest:HogeRequest = HogeRequest()
let hogeRequest: HogeRequest = HogeRequest()


・unused_closure_parameter

クロージャの使っていないパラメータをアンダースコアで省略すべきと。ふむ。
        UIView.animate(withDuration: TimeInterval(CGFloat(0.3)),
                       animations: { () -> Void in
                        pickerView?.center = offScreenCenter
        },
                       completion: { (Bool) -> Void in
                        pickerView?.removeFromSuperview()
        })
        UIView.animate(withDuration: TimeInt
erval(CGFloat(0.3)),
                       animations: { () -> Void in
                        pickerView?.center = offScreenCenter
        },
                       completion: { (_) -> Void in //←Boolをアンダースコアに変更
                        pickerView?.removeFromSuperview()
        })


・weak_delegate

delegateパターンでは、循環参照によるメモリリークを避けるために弱参照にしなさいとのこと。
weakをつければいいのですが、それだけだと、"'weak' may only be applied to class and class-bound protocol types, not 'xxxDelegate'"のエラーが出るので、プロトコルにclassキーワードを追加して、クラス専用プロトコルにします。
protocol FooViewControllerDelegate {
    func fooViewDidDissmissed()
}

class FooViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {

    var delegate: FooViewControllerDelegate? //←強参照
    var selectedPractice: Practice? //選択されているPractice
protocol FooViewControllerDelegate: class { //←クラス専用プロトコル
    func fooViewDidDissmissed()
}

class FooViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {

    weak var delegate: FooViewControllerDelegate? //←弱参照にする
    var selectedPractice: Practice? //選択されているPractice


・control_statement

これもまたObjective-C的うっかりです。
            switch (noteLanguage) { //←あれ、カッコが!
            case .english:
                name = "E♭"
            case .italian:
                name = "Mi♭"
            /* 省略 */
            case .japanese:
                name = "E♭"
            }
            switch noteLanguage { //←消しました
            case .english:
                name = "E♭"
            case .italian:
                name = "Mi♭"
            /* 省略 */
            case .japanese:
                name = "E♭"
            }


・redundant_optional_initialization

オプショナル型の初期値はnilなので、わざわざnilで初期化する必要はないよ、です。
class HogeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    var finishTime: Date? = nil //←nilで初期化は冗長
class HogeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    var finishTime: Date? //←初期化を消しました


・syntactic_sugar

いろんなケースがあるのかもしれませんが、ここではArray<hoge>()のシンタックス・シュガーである、[hoge]を使いなさいとのこと。

どうでもいいことですけど、syntactic_sugarなので「シンタクティック・シュガー」なんじゃないかなぁと思って辞書を引いたら、syntacticの訳に「シンタックスの」がありますね。syntaxの形容詞がsyntacticらしいです。ひょっとしてシンタックス・シュガーって和製英語だったりして?

    var transposingInstrumentKeys = Array<(TransposingInstrumentKey, String)>() //Arrayですね
    var transposingInstrumentKeys: [(TransposingInstrumentKey, String)] = [] //←配列っぽくね


・unused_enumerated

enumeratedで変数が使われてないから、`.indices`とか使ってね、です。

こんな感じでenumeratedで回したけど、結局変数が使われないときにワーニングが出ます。これはコンパイラによるワーニングです。
"Immutable value 'h' was never used; consider replacing with '_' or removing it"
for (i, h) in hoge.enumerated() {
    /* 省略 */
    hoge[i] = piyo
    /* 省略 */
}

じゃあ、アンダースコアにすればいいかと修正すると、Lintがunused_enumeratedを検出します。
for (i, _) in hoge.enumerated() { // ←hをアンダースコアに
    /* 省略 */
    hoge[i] = piyo
    /* 省略 */
}

なるほどそりゃそうだ、です。
for i in hoge.indices { // ←ずぼらせずに、indicesを使いなさい
    /* 省略 */
    hoge[i] = piyo
    /* 省略 */
}


・implicit_getter

コンピューテッドプロパティにおいて、セッタが不要なケースではgetキーワードが省略可能です。省略可能なものは省略してスッキリさせときや、です。
    var hoge: Int {
        get {
            return fuga + 10
        }
    }

もちろん、get{}を削除します。
    var hoge: Int {
        return fuga + 10
    }


まだまだ続くよ。「凛としてSwift (血闘編ノ参)」につづく。


♪♪♪


こちらもどうぞ。
凛としてSwift
凛としてSwift (血闘編ノ壱)
凛としてSwift (血闘編ノ参)
帰ってきた凛としてSwift(くるくる編)


2017年4月15日土曜日

凛としてSwift (血闘編ノ壱)

割と凹みやすいアルマイトな性格です。

いくつかの軽い(Swift成分の低い)プロジェクトにSwiftLintを導入してみて、なんとなく把握できたので、次はSwift率の高いプロジェクトにSwiftLintを導入してみることにしました。

予想通り、数え切れないワーニングとエラーが吐かれますね。ため息が出ますね。当然これをちくちくと直していかなければならないのですが、先頭からひとつひとつやっていると、最後にたどり着く前にメンタルがやられそうです。

そこでXcodeの問題ナビゲーターからいったん目をそらして、個々の対処法について考えてみます。
  1. 素直に修正する。
  2. 特定の箇所だけLintの例外にする。
  3. ymlファイルを編集して、Lintのルールを緩める。
  4. 放置する。
  5. SwiftLintの導入を断念する。
まず5.は避けたいですね。せっかくなんだし。何より、現行のソースがLint適用済みになれば、その後はソースを修正する都度Lintが指摘してくれるわけですから、辛いのは最初だけです。

次に4.は「ダメ、絶対!」ですね。これから先ソースを修正していくのに、修正しなければならないエラーやワーニングが、Lintのワーニングに埋もれてしまってはバグの温床になります。

そうなると1.2.3になりますが、できるだけ1.で頑張って、ちゃんとした理由がある時にかぎって2.、それでもしょうがないときは3.ということにします。

そうしよう、そうしよう。

で、そらしていた目をワーニングの山に向けます。先頭から順にやっていくのも芸がないですよね。それに全て修正し終わってからgitのコミットをしていたのでは粒度が大きくなるので、うっかりエンバグしていたときに、追いかけるのが大変になりそうです。そこで少しだけ工夫をします。

ymlファイルを編集して、エラー/ワーニングの元になっているルールを一時的に全てdisabledにします。そしてエラー/ワーニングがなくなった状態から、disabledにしたルールをひとつずつ有効にして、同じワーニング毎に潰していきます。これなら修正対象は全て同じ種類になるので、作業が楽になります。上記1,2,3の判断もしやすくなりますし、さらにコミットの粒度も下がります。

ymlファイルを抜粋すると、こんな感じですか。

これで、エラー/ワーニングが出力されなくなったので、機械的に修正できる簡単なものから手をつけていきます。

opening_braceを、disabled_rulesから削除すると、こういったところが指摘されます。

えー、Paul Hegarty先生だってこう書いてたぢゃん。 Objective-Cの流れでこうなんじゃないのー、って愚痴りながら修正します。


この他にもこういうところにワーニングが出ますね。

If文の条件にカッコが必要ないことなど、スッキリした文法になっている分、各語句の前後や、{ } の前後の空白はきちんと入れましょうということですね。


「凛としてSwift (血闘編ノ弐)」につづく。


こんな風に手を入れているチューナーアプリです。よろしくね。


♪♪♪


こちらもどうぞ。
凛としてSwift
凛としてSwift (血闘編ノ弐)
凛としてSwift (血闘編ノ参)
帰ってきた凛としてSwift(くるくる編)


2017年3月15日水曜日

凛としてSwift

“Swiftize”という謎単語を思いつきました。

僕の作っているiOS用アプリは大部分がObjective-Cで書かれています。もちろん新たに追加する部分はSwiftで書いているので、全体の2, 3割がSwiftで書かれているでしょうか。それでSwiftのバージョンが上がるたびに派手にエラー・警告が吐かれるという罰ゲームを受けています。

仕様変更しすぎでは。。。

そんなことからObjective-Cで書かれた処理をあえてSwiftで書き直すのは、まだ時期尚早なのでは、と手をつけていませんでした。でも、ひとつのプロジェクトでSwiftとObjective-Cが混ざった状態というのは、著しく生産性が落ちるのです。頭がパッパと切り替わらないのです。おっさんなのです。

今から思えば、中途半端に混ぜるようなことはしないで、一気に書き換えるようにすれば良かったなぁ。

でもSwiftもバージョン3になりましたしね、ここは一念発起して少しづつ"Swiftize"していこうと決めました。

そこで、どうせなら綺麗なソースを書きたいので、SwiftLintを導入することにしました。

SwiftLint本体はHome Brewでインストールしました。
$ brew install swiftlint
==> Downloading https://homebrew.bintray.com/bottles/swiftlint-0.16.1.sierra.bottle.tar.gz
######################################################################## 100.0%
==> Pouring swiftlint-0.16.1.sierra.bottle.tar.gz
🍺  /usr/local/Cellar/swiftlint/0.16.1: 37 files, 13.6M
$ swiftlint version
0.16.1
$ 

次に、Xcodeのプロジェクトを開いて、TARGETS > Build Phases で、Run Script Phaseを追加します。

1. TARGETSを選択します。


2. Build Phasesで”+”をクリックします。


3. New Run Script Phaseを選びます。


4. Run Scriptが追加されるので、開きます。



5. Swift LintのREADME.mdにある、シェルスクリプトをコピペします。
if which swiftlint >/dev/null; then
  swiftlint
else
  echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi

6. プロジェクトのディレクトリに、.swiftlint.ymlを追加してLintのルールをカスタマイズします。
$ cd HelloSwiftLint/
$ ls -la
total 8
drwxr-xr-x  7   foobar  staff   238  2  3 04:02 .
drwxr-xr-x  8   foobar  staff   272  2 17 00:09 ..
-rw-r--r--  1   foobar  staff  1465  2  3 04:02 .swiftlint.yml     ←これを追加します
drwxr-xr-x  7   foobar  staff   238  2  2 03:03 HelloSwiftLint
drwxr-xr-x  5   foobar  staff   170  2  2 02:42 HelloSwiftLint.xcodeproj
drwxr-xr-x  4   foobar  staff   136  2  2 02:49 HelloSwiftLintTests
drwxr-xr-x  4   foobar  staff   136  2  2 02:49 HelloSwiftLintUITests
$
.swiftlint.ymlの中身は、やはりREADME.mdをお手本にして修正を加えます。
最低限設定するべき箇所は、included:とexcluded:です。
included:にはチェックするソースへのPATHを指定します。
これはプロジェクトの直下にある、プロジェクトと同名のディレクトリを指定すればいいと思います。(これを指定しないとプロジェクトの下全部が対象になるのかな? そんな感じがするんだけど)
excluded:には逆にチェックしたくないファイルへのPATHを指定します。
CocoaPodsを使用しているならPodsとか。あと、include:で指定した下に、何か外部のライブラリを置いたような時、それを除外するよう指定できます。

ルールのカスタマイズとは言っても、特別なことはしていません。ただ、デフォルトではかなり厳しいチェックが行われますので、ルールを少し緩めています。

disabled_rules:には、チェックしない項目を指定します。
ここには1行の長さをチェックしないように追加しました。ご存知のように、AppleのAPIはのけぞる長さですからね。

type_body_length:は1個の型の定義(クラスとかね)の行数、file_length:は1ファイルの行数です。
これは適当に大きめに変更します。ただ、ソースにルールを合わせたのでは、本末転倒になりますからほどほどに。

type_name:は型名(クラスとか)の長さ、variable_name:は変数名の長さです。
型名はともかく、変数名は1文字から使いたいのでmin_length:は指定なしにしてあります。


あ、 gitを使っているなら、.gitignoreに.swiftlint.ymlを追加しておくのもいいですよ。
(省略)
#Swift Lint
.swiftlint.yml
( 省略)


6. .swiftlint.ymlができたら、 プロジェクトを開いてビルドします。
警告が、だばぁ〜

7. ワーニングが山ほど出ますので、涙をこらえながら、ひとつひとつ修正していきます。

ちょっと気になったのは、警告対象の箇所を修正しても、警告を示す黄色の表示が消えないことがあったことです。開いているのとは別のファイルに警告が残っているときに、その警告が開いているファイルに表示されるということが、しばしば起こるようです。

修正したのに警告表示が消えないなぁ、というような時は一旦そのファイルを閉じて、Xcodeの左の問題ナビゲーターに表示されているワーニングメッセージをダブルクリックして、その警告が指しているSwiftファイルを開きなおすと、うまく問題の箇所に黄色の表示が現れると思います。

※2017/05/16追記
この不具合はSwiftLintをバージョン0.18.1に更新したら起きなくなりました。どうやら修正されたようです。


正直、Lintが指摘する書き方に対して、「えー、そこはこう書くだろぉ〜、普通ぅ〜」というようなことは、ままありますが、そんなもん慣れです。その書き方に慣れてさえしまえば、ちょっとした書き違い(間違いじゃなくて)がなくなって、可読性がよくなります。こういうことの積み重ねがバグの少ないプログラムに繋がっていくんだろうな、って思ってます。

、、、たぶんね(白目)。

しかしSwift、もう少しコンパイルが早くなりませんかね?「凛としてSwift (血闘編ノ壱)」に続きます。


♪♪♪


こちらもどうぞ。
凛としてSwift (血闘編ノ壱)
凛としてSwift (血闘編ノ弐)
凛としてSwift (血闘編ノ参)
帰ってきた凛としてSwift(くるくる編)