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

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で購入する


2018年1月15日月曜日

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

「コンテキストメニューを置き換えてみた」の続編になります。

UITextViewのコンテキストメニューのカスタマイズはできたものの、WKWebViewではできなかったというところで終わってました。何とかならないかなぁと思っていたのです。

あ、App Extensionで「共有」に追加でき、、、?

それや!

App ExtensionはiOS8で追加された機能ですが、使ったことがなかったので思いつかなかったです。

ここではApp Extensionについてのざっくりとした話題だけですので、詳細についてはAppleの「App Extensionプログラミングガイド」を参照してください。

--

登場人物は3人です。

  1. ホストアプリケーション(Host app)
  2. App Extension
  3. 収容アプリケーション(Containing app)

1.のホストアプリケーション(Host app)は、例えばSafariやメモappなど、App Extensionを呼び出す側のアプリです。

3.の収容アプリケーション(Containing app)は、App Extensionを備えたアプリで、いわゆる「アプリ」です。

App Extensionは、収容アプリケーションのプロジェクトに含まれていますが、収容アプリケーションとは独立して動きます。

ホストアプリケーションは、収容アプリケーションを呼び出すのではなく、App Extensionを呼び出してやりとりするだけで、収容アプリケーションが起動されるわけではないというところが肝でしょうか。

--

さっそく、テストアプリを作ってみます。

ホストアプリケーションはとりあえず、Safariを使います。
収容アプリケーションは、AwesomeAppというアプリにしてみます。


いつもどおり、普通にAwesomeAppというプロジェクトを作成したら、App Extensionを新しいターゲットとして追加します。シンプルにShare Extensionにします。

File > New > Target で、Share Extensionを選択します。

ターゲットとして追加

Share Extensionを選択


ターゲット名は自由に決められますが、「収容アプリケーション+機能」がわかりやすいと思いますので、AwesomeAppShareにしました。



AwesomeAppShareのテストのスキームを作成するか否かのダイアログが表示されるので、'Activate'を選択します。

Activateを選択
ターゲットが追加されました

このApp Extensionのターゲットの中には、3つのファイルが生成されています。

  • ViewController
  • Storyboard
  • info.plist
--

ひとつ、はまりどころとしては、Deployment Targetがあります。

App Extensionと収容アプリケーションのDeployment Targetが同じになっているか確認するのを忘れないでください。

ターゲットの追加時には、デフォルトのDeployment Targetになります。既存のアプリにターゲットを追加するときには、同じにならないことがありますので、要確認です(テストで、はまりましたよ)。

--


たったこれだけですが、とりあえず動きます。


スキームを「(E)AwesomeAppShare」に設定して、実行します。

(E)AwesomeAppShareに変更


ホストアプリケーションが選択できるので、Safariを選んでRunを押します。



Safari(ホストアプリケーション)が起動するので、適当な文言を選択してから、Shareを選択します。

Shareを選択


AwesomeAppShareのアイコンがあるので、タップ。

※ここではアイコンがデフォルトのものになっていますが、収容アプリケーションにアイコンが設定されていれば、そのアイコンが自動的に表示されます。



Share Extensionが起動します。

選択した文言が取得できています


コーディングなしで、ここまでできました。ただ、ここで動いているのはあくまでShare Extension(AwesomeAppShare)です。収容アプリケーション(AwesomeApp)は、全く動いてません。念のため。


今回はここまで。


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


♪♪♪


こちらもどうぞ。
使ってみた(App Extension編) その2
使ってみた(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を使えますね。