- 投稿日:2020-02-09T23:41:29+09:00
Share Extensionのハマりポイント
はじめに
初の個人アプリを App Store に審査出したら 30 分足らずでリリースされてビビったからその全てをお伝えしますの記事を見てShare Extensionいいなぁと思い自分のアプリにもつけてみよう!と思ったのですが色々ハマりました...
対象
- iOS13.0以上
- macOS Catalina
Share Extensionの実装
ここを参考に実装をしました。
Share Extension の追加自体は簡単でした。
ShareViewController
の POST 機能とかいらなかったので参考記事に書いてあったように下記のように修正して画面を作成しました。ShareViewController.swift- class ShareViewController: SLComposeServiceViewController { + class ShareViewController: UIViewController {画面は下記のようなものでテキストのみ共有するようにしました。(上下に
TextView
を置いています。上が共有文字列で下はまだ実装中)共有メニューに表示されない
なぜか共有メニューにアプリが表示されない時がありました
info.plist<key>NSExtensionAttributes</key> <dict> <key>NSExtensionActivationRule</key> <dict> <key>NSExtensionActivationSupportsText</key> <true/> </dict> </dict>上記のようにしてると共有メニューに表示されず参考記事のように SUBQUERY にすると表示されるようになりました
info.plist<key>NSExtensionActivationRule</key> <string>SUBQUERY (extensionItems,$extensionItem, SUBQUERY ($extensionItem.attachments,$attachment, (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text" || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text") ).@count == $extensionItem.attachments.@count).@count == 1 </string>画面が閉じれない
iOS Mac 上記のように iOS 版は半モーダルなので下にスワイプすれば閉じれますが Mac 版の場合は閉じることができません
こちらも参考記事に閉じる方法が書いてありました
閉じるボタンを用意して下記のようにすれば閉じることができました
@IBAction private func cancel(_ sender: Any) { extensionContext!.completeRequest(returningItems: nil, completionHandler: nil) }共有メニューのローカライズ
iOS Mac 上記のように iOS 版は共有メニューにもアプリのターゲットで設定した Display Name が表示されます(ローカライズも対応)が Mac 版は Share Extension のターゲットで設定した Display Name が表示されています
Share Extension のターゲットに InfoPlist.strings を下記のように追加しました。
InfoPlist.stringsCFBundleDisplayName = "DNA Converter"; // (Japaneseの方は "DNA変換" と設定)これで Mac版 でもアプリ同様共有メニューにも日本語表示なら「DNA変換」英語表記なら「DNA Converter」と表示されるようになりました
(どっかに変数を設定してアプリのターゲットと共通化すべきなのかも?)
共有した値が取得できない
致命的なのですが Mac 版で共有した値が取得できませんでした
共有したテキストの取得処理は下記のように実装しました。
let extensionItem: NSExtensionItem = extensionContext?.inputItems.first as! NSExtensionItem guard let itemProvider = extensionItem.attachments?.first else { // ここでなんかエラー処理 return } let publicText = String(kUTTypeText) // "public.text" if itemProvider.hasItemConformingToTypeIdentifier(publicText) { itemProvider.loadItem(forTypeIdentifier: publicText, options: nil, completionHandler: { (data, error) in guard let text = data as? String else { // ここでなんかエラー処理 return } // ここでなんか処理 }) return } let publicPlainText = String(kUTTypePlainText) // "public.plain-text" if itemProvider.hasItemConformingToTypeIdentifier(publicPlainText) { itemProvider.loadItem(forTypeIdentifier: publicPlainText, options: nil, completionHandler: { (data, error) in guard let text = data as? String else { // ここでなんかエラー処理 return } // ここでなんか処理 }) return } // ここでなんかエラー処理iOS では下記のように
extensionContext?.inputItems.first
のNSExtensionItemAttachmentsKey
に値が入っているのですがpo extensionContext?.inputItems ▿ Optional<Array<Any>> ▿ some : 1 element - 0 : <NSExtensionItem: 0x6000007e4260> - userInfo: { NSExtensionItemAttachmentsKey = ( "<NSItemProvider: 0x600002edc690> {types = (\n \"public.plain-text\"\n)}" ); NSExtensionItemAttributedContentTextKey = {length = 317, bytes = 0x7b5c7274 66315c61 6e73695c 616e7369 ... 20417070 6c65207d }; "com.apple.UIKit.NSExtensionItemUserInfoIsContentManagedKey" = 0; }Mac では下記のように値が入っていません...
po extensionContext?.inputItems ▿ Optional<Array<Any>> ▿ some : 1 element - 0 : <NSExtensionItem: 0x6000018d45c0> - userInfo: { NSExtensionItemAttachmentsKey = ( ); NSExtensionItemAttributedContentTextKey = {length = 315, bytes = 0x7b5c7274 66315c61 6e73695c 616e7369 ... 30204170 706c657d }; }色々試してみましたが Mac 版では値が取得できませんでした。
extensionContext?.inputItems.first
のNSExtensionItemAttributedContentTextKey
からがんばれば取得できそうですがなんか違うと思いあきらめました。ここにきて思ったのが Mac 版だけ動作がおかしいのでそもそも Share Extension が Mac に対応してないんじゃね?という疑惑です。(なんか知ってる人いたら教えて下さい)
ということで私は Mac 版の Share Extension はあきらめました
Mac版で取得できた値(20200211追記)
コメントでURLは取れると聞いたので他の値が取れるかみてみました。
info.plist を下記に設定して
extensionContext?.inputItems
をログで出してみました。info.plist<key>NSExtensionAttributes</key> <dict> <key>NSExtensionActivationRule</key> <string>TRUEPREDICATE</string> </dict>URL
▿ Optional<Array<Any>> ▿ some : 1 element - 0 : <NSExtensionItem: 0x6000013e8660> - userInfo: { NSExtensionItemAttachmentsKey = ( "<NSItemProvider: 0x600003aeddc0> {types = (\n \"public.url\"\n)}", "<NSItemProvider: 0x600003aedea0> {types = (\n \"public.image\"\n)}" ); }Image
▿ Optional<Array<Any>> ▿ some : 1 element - 0 : <NSExtensionItem: 0x600002034630> - userInfo: { NSExtensionItemAttachmentsKey = ( "<NSItemProvider: 0x60000092f3a0> {types = (\n \"public.image\"\n)}" ); }Movie
▿ Optional<Array<Any>> ▿ some : 1 element - 0 : <NSExtensionItem: 0x6000013d1ef0> - userInfo: { NSExtensionItemAttachmentsKey = ( "<NSItemProvider: 0x600003ae0230> {types = (\n \"com.apple.quicktime-movie\",\n \"public.file-url\",\n \"public.url\"\n)}" ); }▿ Optional<Array<Any>> ▿ some : 1 element - 0 : <NSExtensionItem: 0x6000013c54f0> - userInfo: { NSExtensionItemAttachmentsKey = ( "<NSItemProvider: 0x600003ae43f0> {types = (\n \"public.url\"\n)}", "<NSItemProvider: 0x600003ae5ff0> {types = (\n \"com.adobe.pdf\"\n)}" ); }まとめ
Type 結果 text url image movie テキストだけ取れない
さいごに
Mac 版はなにか特殊な設定がいるんでしょうか?調べた限り有力な情報は得られませんでした...
情報求ム!!
- 投稿日:2020-02-09T23:38:47+09:00
ARKit 3のMotion CaptureでVRMを動かすために、関節の回転差分を計算する
前置き
ARKit3では、これまでの表情のキャプチャに加えて全身の姿勢や関節の回転を推定するモーションキャプチャが実装されました。
モーションキャプチャの簡単な解説は、WWDCの「Bringing People into AR」が分かりやすいです。Bringing People into AR
https://developer.apple.com/videos/play/wwdc2019/607/ARKit3 のモーションキャプチャでは、91点の関節を認識し3Dアバターの動きと同期することができます。
動きを同期する上で一番簡単な方法は、各関節ノードの名前とtransformが指定された値になったアバターを利用することです。
これは公式サンプルの「Capturing Body Motion in 3D」に含まれているrobot.usdzを見ると分かりやすいです。
https://developer.apple.com/documentation/arkit/capturing_body_motion_in_3dでは、自前のモデルなど関節名やtransformの異なるモデルを操るにはどうすれば良いでしょうか?
一つは @nkjzm さんの記事のように、対応する構造にエクスポートする方法がありますARKit 3のMotion CaptureでVRMを動かす【Unity】
https://qiita.com/nkjzm/items/d4379d5fd018de67a082そしてもう一つは、上記記事でも触れられていますが各関節の差分から回転を調整して適用する方法です。
今回はVR向け3DアバターファイルフォーマットであるVRMに対して、ARKitで取得した動きを同期する方法を解説します。
ARSkelton3DとVRMの違い
ARSkelton3DはARKitを用いて取得できる人体の構造です。VRMは同じく3Dアバターの構造ですが、両者はいくつかの異なる点が存在します。
骨格数
肩や首などを表す骨格数は
ARSkelton3D VRM 91 54 と異なります。背骨や首が細かく分けられているARSkelton3Dに対してキャラクターを扱うVRMはシンプルな構造になっています
各関節の名称
VRMでは各関節は、自由に名称を付けることができます。
モデルによって右肩一つ取ってもRightSholderやSholderRなど製作者によって名称が異なります。
これらの関節位置と名前はボーンマッピングによって決定されます。一方のARSkelton3Dは、全ての名称が決まっており次のような文字列で取得することができます。
T-Poseの角度
T-Poseは、一般的には両手を水平にあげた状態のことを指します。
VRMでは、それぞれの関節のtransformがidentity(つまり初期状態)の時に、このT-Poseとなります。
ARSkelton3Dの場合、全ての関節のtransformをidentityにすると次のような体制になります。
https://forum.unity.com/threads/example-rig-for-3d-human-skeleton.696512/#post-5158382そして、この体制に
ARSkeletonDefinition.defaultBody3D.neutralBodySkeleton3Dを適用すると、T-Poseになります。
これはつまり、ARSkelton3DがT-Poseの時に各関節がidentityではないということを表しています。pivotの位置
全体のノードの中心を表すpivotは、VRMの場合は足下が、ARSkelton3Dの場合はhipsが中心になります。
localTransform / modelTransformの違い
ARSkelton3Dからは、各関節のtransformが取得できるのですが、localTransform / modelTransformの2種類を得ることができます。
modelTransform
modelTransformは、pivotであるhipsからの移動と回転を持つtransformです。
ここで注意しなくてはならないのは、modelTransformは移動量を持つためそのままアバターに適用すると、腕や背格好が実際の人間と同じ割合になってしまいます。
VRMの場合はキャラクターを扱うことが多く、実際の体型と同じとは限らないのでこのtransformを扱うのはあまり適さないかと思います。localTransform
localTransformは、ある関節ノードの親ノードに対する回転情報のみを持ったtransformです。
基本的にはこのtransformを利用しますが、関節数が異なる箇所に関しては正しく差分を出さないとpivotから遠ざかるほどに誤差が生まれ最終的な姿勢が全く異なるものになってしまうので注意が必要です。関節の回転差分の出し方
ARSkelton3DのlocalTransformは、純粋な回転量(ここではar_quartanionとします。)+ニュートラル状態に持っていくための回転量の合成であると考えられます。
localTransform = ar_quartanion * neutral_quartanionでは、vrmの関節はneutral_quartanionを引いたar_quartanionだけを適用すれば良いでしょうか?
実はそうではありません。
ar_quartanionで与えられる回転は初期状態に依存するので、T-Pose時点で関節がidentityであるvrmと、neutralが与えられているARSkelton3Dでは合成結果が異なってしまうのです。そこで、一度各関節がARSkelton3Dのtransformになるようにquartanionを与え、その上でar_quartanionを与えて、最後にARSkelton3Dのquartanion分を戻すことでvrmでも同じ方向に回転を与えることができます。
回転を戻すにはinverseを合成してあげれば良いです。arOrientaion = neutralOrientation.inverse * transformOrientation target.simdOrientation = vrmNeutralOrienation * arOrientation * vrmNeutralOrienation.inverseでは、各関節をARSkelton3Dのtransformにするにはどのような計算をすれば良いでしょうか。
単純にlocalTransformを与えると、親やその親の回転を考慮出来ないため愚直に全てのノードの親を辿って計算する必要があります。
ただ、この計算は初回に1度だけしておけば良いのでレンダリングへの影響はほとんどありません。
親のtransformのindexはparentIndices
で取得できるので利用します。
parentIndexが存在しない場合は-1が返ってきます。
VRMはpivotの回転がARSkelton3Dと前後逆なので、この時点でy軸180度の回転をかけてあげます。lazy var vrmNeutralJointLocalTransforms: [simd_float4x4] = { let defaultBody3D = ARSkeletonDefinition.defaultBody3D let neutralJointLocalTransforms = defaultBody3D.neutralBodySkeleton3D!.jointLocalTransforms var vrmNeutralJointLocalTransforms: [simd_float4x4] = [] for (var index, localTransform) in neutralJointLocalTransforms.enumerated() { var transforms: [simd_float4x4] = [localTransform] while let parentIndex = defaultBody3D.parentIndices[safe: index], parentIndex > 0 { let parentLocalTransform = neutralJointLocalTransforms[parentIndex] transforms.insert(parentLocalTransform, at: 0) index = parentIndex } masterRotation: do { let rotate = simd_quaternion(.pi, simd_float3(0, 1, 0)) transforms[0] = simd_matrix4x4(rotate) * transforms[0] } vrmNeutralJointLocalTransforms.append(transforms.reduce(simd_float4x4(1), *)) } return vrmNeutralJointLocalTransforms }()完成!
以上の計算を行うことで、ARBodyTranckingでVRMを動作させることが出来るようになりました。
VRMを使った配信アプリvearでは、これらの処理を行ってモーションキャプチャを実装しています。(v1.2から実装)
興味のある方は是非使ってみてください!
https://apps.apple.com/us/app/vear/id1490697369
- 投稿日:2020-02-09T22:57:00+09:00
[Appendix]Realmが実機で動かなくなった!?
Appendix
本日、Realmを使ってるアプリを実機で起動したら以下のエラーが出ました。
その解決方法をざっと書きます。dyld: Library not loaded: @rpath/Realm.framework/RealmXCode上では「dyld`__abort_with_payload:」から始まるファイルが開かれており、「Thread 1: signal SIGABRT」のメッセージが。
原因は分からずでしたが解決方法は、
1.PodFileを更新
2.pod updateを実行
でした。pod initによって作られたPodFileの
PodFileuse_frameworks!を
PodFile# use_frameworks! use_modular_headers!に変更して、そのプロジェクトフォルダに対して
pod updateを実行するだけです。
※pod installした時と同じような感じ。ターミナル見てたらRealmが4.3.1から4.3.2に変わってたから
それでなんか修正が必要になったんですかねー。PodFileを更新せずにupdateするだけだと、Realmのversionは上がってましたがエラーメッセージは変わらなかったので、PodFileにuse_moduleなんたらっていうのは入れなくちゃいけないみたいです。
こんなんわかるかい・・・
- 投稿日:2020-02-09T20:00:14+09:00
sizeThatFitの使い方, 幅固定 高さ可変なUILabelの作り方
使い方
意外と日本語の記事がなかったので?書きました。
let maxSize = CGSize(width: 100,height:100) let size = __UIパーツ__.sizeThatFits(maxSize) __UIパーツ__.frame = CGRect(origin: CGPoint(x:0, y: 0), size: size)maxSizeで最大の幅と高さを指定しておきます。
その範囲内でいい感じ設定してほしいので、それをsizeThatFitsの引数として代入すると、いい感じに調整してくれたサイズが返ってきます。
この場合は width,heightがそれぞれ100以下の範囲でいい感じに値を返してくれます。
あとはそれを代入すればOK!具体例(UILabel) 幅を指定値以下で高さを可変
let label = UILabel() label.numberOfLines = 0 label.text = "lorem ipsum ..........something" //最大値の設定。 幅固定で高さはいい感じにしたい、と言う場合はこのように高さの最大値を無限大に let maxSize = CGSize(width: self.view.frame.width - 100, height: CGFloat.greatestFiniteMagnitude) let size = label.sizeThatFits(maxSize) //後でcenterを設定するためCGPointのx、yはどんな値でもよき label.frame = CGRect(origin: CGPoint(x:0, y: 0), size: size) label.center = self.view.centerこれでtextが全てすっぽり収まるけれど、大きすぎないちょうどよいUILabelサイズが計算されます。
- 投稿日:2020-02-09T19:08:51+09:00
Google Maps SDK for iOS で経路案内を実装する
はじめに
Google が提供している Directions API と Google Maps SDK for iOS を使用して現在地から目的の場所までのルートを表示してみたいと思います
ドキュメントはこちら
開発環境
Xcode:11.1(11A1027) Swift 5 iOS:13.1実装
こちらの Google Maps SDK for iOS で現在地を表示 の続きから記載しています
モデルの作成
まず、経路検索APIのレスポンスをパースするための struct を定義します
Directions API のレスポンス形式
経路検索APIのレスポンス形式は以下のようなものになっています
{ "status": "OK", "geocoded_waypoints" : [ { "geocoder_status" : "OK", "place_id" : "ChIJ7cv00DwsDogRAMDACa2m4K8", "types" : [ "locality", "political" ] }, { "geocoder_status" : "OK", "place_id" : "ChIJ69Pk6jdlyIcRDqM1KDY3Fpg", "types" : [ "locality", "political" ] }, { "geocoder_status" : "OK", "place_id" : "ChIJgdL4flSKrYcRnTpP0XQSojM", "types" : [ "locality", "political" ] }, { "geocoder_status" : "OK", "place_id" : "ChIJE9on3F3HwoAR9AhGJW_fL-I", "types" : [ "locality", "political" ] } ], "routes": [ { "summary": "I-40 W", "legs": [ { "steps": [ { "travel_mode": "DRIVING", "start_location": { "lat": 41.8507300, "lng": -87.6512600 }, "end_location": { "lat": 41.8525800, "lng": -87.6514100 }, "polyline": { "points": "a~l~Fjk~uOwHJy@P" }, "duration": { "value": 19, "text": "1 min" }, "html_instructions": "Head \u003cb\u003enorth\u003c/b\u003e on \u003cb\u003eS Morgan St\u003c/b\u003e toward \u003cb\u003eW Cermak Rd\u003c/b\u003e", "distance": { "value": 207, "text": "0.1 mi" } }, ... ... additional steps of this leg ... ... additional legs of this route "duration": { "value": 74384, "text": "20 hours 40 mins" }, "distance": { "value": 2137146, "text": "1,328 mi" }, "start_location": { "lat": 35.4675602, "lng": -97.5164276 }, "end_location": { "lat": 34.0522342, "lng": -118.2436849 }, "start_address": "Oklahoma City, OK, USA", "end_address": "Los Angeles, CA, USA" } ], "copyrights": "Map data ©2010 Google, Sanborn", "overview_polyline": { "points": "a~l~Fjk~uOnzh@vlbBtc~@tsE`vnApw{A`dw@~w\\|tNtqf@l{Yd_Fblh@rxo@b}@xxSfytA blk@xxaBeJxlcBb~t@zbh@jc|Bx}C`rv@rw|@rlhA~dVzeo@vrSnc}Axf]fjz@xfFbw~@dz{A~d{A|zOxbrBbdUvpo@` cFp~xBc`Hk@nurDznmFfwMbwz@bbl@lq~@loPpxq@bw_@v|{CbtY~jGqeMb{iF|n\\~mbDzeVh_Wr|Efc\\x`Ij{kE}mAb ~uF{cNd}xBjp]fulBiwJpgg@|kHntyArpb@bijCk_Kv~eGyqTj_|@`uV`k|DcsNdwxAott@r}q@_gc@nu`CnvHx`k@dse @j|p@zpiAp|gEicy@`omFvaErfo@igQxnlApqGze~AsyRzrjAb__@ftyB}pIlo_BflmA~yQftNboWzoAlzp@mz`@|}_ @fda@jakEitAn{fB_a]lexClshBtmqAdmY_hLxiZd~XtaBndgC" }, "warnings": [ ], "waypoint_order": [ 0, 1 ], "bounds": { "southwest": { "lat": 34.0523600, "lng": -118.2435600 }, "northeast": { "lat": 41.8781100, "lng": -87.6297900 } } } ] }モデルの定義
この中で経路案内に必要な情報のみをパースするようにします
struct Direction: Codable { let routes: [Route] } struct Route: Codable { let legs: [Leg] } struct Leg: Codable { /// 経路のスタート座標 let startLocation: LocationPoint /// 経路の目的地の座標 let endLocation: LocationPoint /// 経路 let steps: [Step] enum CodingKeys: String, CodingKey { case startLocation = "start_location" case endLocation = "end_location" case steps } } struct Step: Codable { let startLocation: LocationPoint let endLocation: LocationPoint enum CodingKeys: String, CodingKey { case startLocation = "start_location" case endLocation = "end_location" } } struct LocationPoint: Codable { let lat: Double let lng: Double }Directions APIから経路を取得し、マップ上に表示
ViewController に以下のようにAPI通信部分を実装していきます
/// 経路検索APIのエンドポイント. 経路検索APIはSDKとして提供されていないため、エンドポイントはベタ書きになります let baseUrl = "https://maps.googleapis.com/maps/api/directions/json" /// 仮の目的地の座標(適当に GINZA SIX にしてみています) let ginzaSixLocation = "35.669798,139.7639302" // ~~~~~~~ 省略 ~~~~~~~ // 現在地から目的地までのルートを検索する private func getDirection(destination: String, start startLocation: String, completion: @escaping (Direction) -> Void) { guard var components = URLComponents(string: baseUrl) else { return } components.queryItems = [ URLQueryItem(name: "key", value: GOOGLE_API_KEY), URLQueryItem(name: "origin", value: startLocation), URLQueryItem(name: "destination", value: destination) ] guard let url = components.url else { return } let task = URLSession.shared.dataTask(with: url) { (data, response, error) in if let data = data { let decorder = JSONDecoder() do { let direction = try decorder.decode(Direction.self, from: data) completion(direction) } catch { print(error.localizedDescription) } } else { print(error ?? "Error") } } task.resume() } private func showRoute(_ direction: Direction) { guard let route = direction.routes.first, let leg = route.legs.first else { return } let path = GMSMutablePath() for step in leg.steps { // steps の中には曲がるところの座標が入っているので、 // 曲がるところの座標を線で結んでいく path.add(CLLocationCoordinate2D(latitude: step.startLocation.lat, longitude: step.startLocation.lng)) path.add(CLLocationCoordinate2D(latitude: step.endLocation.lat, longitude: step.endLocation.lng)) } // 曲がるところを結んだ線を Map 上に表示する let polyline = GMSPolyline(path: path) polyline.strokeWidth = 4.0 polyline.map = mapView } } extension ViewController: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { switch status { case .authorizedAlways, .authorizedWhenInUse: // 位置情報の取得が許可されていたら現在地を取得 manager.requestLocation() case .notDetermined: manager.requestWhenInUseAuthorization() default: return } } /// 位置情報取得の認可状態が変化した際とCLLocationManagerのインスタンスが生成された際に呼び出されるメソッド func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last else { return } let startLocation = "\(location.coordinate.latitude),\(location.coordinate.longitude)" getDirection(destination: ginzaSixLocation, start: startLocation, completion: { [weak self] direction in DispatchQueue.main.async { self?.showRoute(direction) } }) } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print(error.localizedDescription) } }この状態でビルドを行うと、以下のように表示されているかと思います
縮尺を調節
上記のスクリーンショットの状態だと、広域すぎてルートがわかりにくいです
なので、経路の全域をちょうど表示できるようなところまでマップを拡大する処理を入れていきますprivate func showRoute(_ direction: Direction) { guard let route = direction.routes.first, let leg = route.legs.first else { return } // ~~~~~~ 省略 ~~~~~~ polyline.map = mapView // 以下、追記 updateCameraZoom(startLat: leg.startLocation.lat, startLng: leg.startLocation.lng, endLat: leg.endLocation.lat, endLng: leg.endLocation.lng) } private func updateCameraZoom(startLat: Double, startLng: Double, endLat: Double, endLng: Double) { let startCoordinate = CLLocationCoordinate2D(latitude: startLat, longitude: startLng) let endCoordinate = CLLocationCoordinate2D(latitude: endLat, longitude: endLng) let bounds = GMSCoordinateBounds(coordinate: startCoordinate, coordinate: endCoordinate) let cameraUpdate = GMSCameraUpdate.fit(bounds, withPadding: 16.0) mapView.moveCamera(cameraUpdate) }上記のように、表示領域を指定してあげると画面内に収まるように自動で縮尺を調節してくれるので、経路が見やすくなります
終わりに
今回は徒歩での経路案内を設定していますが、Directions API のパラメータに設定している
"mode": "walking"
を"mode": "drive"
に変えてあげると車での経路検索も可能になりますコードの全容は github に載せています
https://github.com/nwatabou/TestCurrentLocation参考
- 投稿日:2020-02-09T17:49:04+09:00
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Strategy/State~
この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況や
Swiftのコアライブラリやフレームワークで使われているパターンに
着目してデザインパターンを学び直してみた記録です。関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターンStrategy/Stateパターン概要
- 条件によって違う処理内容を外部クラスに追い出し、どのクラスを使うかを実行時に選択するパターンです。
- 具体的には、switch文の「caseが多い/caseごとの処理内容が濃い」場合に、caseごとにクラスを分離させることが多いと思います。
- 「ユニットテストしやすく」「拡張がしやすい(影響範囲を限定できる)」というメリットがあります。
- GoFのデザインパターンでは振る舞いに関するパターンに分類されます。
- StrategyとStateは設計としては同じで、作成側の意図が何にあるかの違いです(と私は解釈しています)。
- Strategyは性質の違いによる振る舞いの切り替え
- Stateは状態の変化による振る舞いの切り替え
Strategyパターンの使い所
- switch文の caseが多い/caseごとの処理内容が濃い 場合
Stateパターンの使い所
私なりの見解では、普通のiOSアプリでは使いどころを見つけるのはなかなか難しいと思います。
一番利用したい場面は『データの「取得開始前」「取得中」「取得成功」「取得エラー」という状態の変化でViewを更新する』ですが、UIKitがそのような設計にマッチしないためです。
サンプルコード
Xcode 11.3 / Swift 5.1 です。
Playgroundにコピペすれば動作します。Strategyパターンを適用しないサンプル
// 認証パラメータ struct AuthInfo { var id = "" var password = "" var token = "" init(id: String, password: String) { self.id = id self.password = password } init(token: String) { self.token = token } } // 認証管理クラス final class AuthManager { enum AuthType { case idPassword case token } static func authenticate(by type: AuthType, with authInfo: AuthInfo) { switch type { case .idPassword: if authInfo.id == "id" && authInfo.password == "password" { print("ID Password: auth success") } else { print("ID Password: invalid id or password") } case .token: if authInfo.token == "token" { print("Token: auth success") } else { print("Token: invalid token") } } } } AuthManager.authenticate(by: .idPassword, with: AuthInfo(id: "id", password: "password")) // ID Password: auth success AuthManager.authenticate(by: .token, with: AuthInfo(token: "token")) // Token: auth successStrategyパターンを適用したサンプル
// 認証パラメータ struct AuthInfo { var id = "" var password = "" var token = "" init(id: String, password: String) { self.id = id self.password = password } init(token: String) { self.token = token } } // MARK: - Protocol // 認証プロトコル protocol AuthStrategy { func authenticate(_ authInfo: AuthInfo) } // MARK: - Context // 認証を実行する役割 struct AuthContext { let strategy: AuthStrategy func execute(with authInfo: AuthInfo) { strategy.authenticate(authInfo) } } // MARK: - Concreate Strategies // IDパスワード認証 struct IdPasswordAuthStrategy: AuthStrategy { func authenticate(_ authInfo: AuthInfo) { if authInfo.id == "id" && authInfo.password == "password" { print("ID Password: auth success") } else { print("ID Password: invalid id or password") } } } // トークン認証 struct TokenAuthStrategy: AuthStrategy { func authenticate(_ authInfo: AuthInfo) { if authInfo.token == "token" { print("Token: auth success") } else { print("Token: invalid token") } } } // MARK: - Usage var context = AuthContext(strategy: IdPasswordAuthStrategy()) context.execute(with: AuthInfo(id: "id", password: "password")) // ID Password: auth success context = AuthContext(strategy: TokenAuthStrategy()) context.execute(with: AuthInfo(token: "token")) // Token: auth success
- 投稿日:2020-02-09T17:45:21+09:00
Xcode 11.4アップデートまとめ!
Xcode11.4 Betaが開発者向けに公開された。
Relese Note
変更
- macOS Catalina 10.15.2以降を実行しているMacが必要。
- iOS 13.4, iPadOS 13.4, tvOS 13.4, watchOS 6.2, macOS Catalina 10.15.4に対応したバージョン。
- IBで制約をつける際に0を入力して「Standard」になるバグを修正
新規
- Swift 5.2にアップデート
- SSLの証明書をシミュレーターにたいして
ドラッグ&ドロップ
でインストール可能に- iOSシミュレータがプッシュ通知のシミュレーション可能に
callAsFunction
が追加- IB(インターフェースビルダー)にシステムグレーが追加
adder.callAsFunction(10)をadder(10)と省略してかけるようになった。
struct Adder { var base: Int func callAsFunction(_ x: Int) -> Int { return x + base } } var adder = Adder(base: 3) adder(10) // returns 13, same as adder.callAsFunction(10)
- simctlキーチェーンサブコマンドをサポート
xcrun simctl keychain <device> add-root-cert my-selfsigned.cer
- simctlプライバシー権限の変更をサポート
# アクセスできるようにする xcrun simctl privacy <device> grant photos com.example.app # 権限をデフォルトにリセットする xcrun simctl privacy <device> reset all com.example.app参考記事
最後に
- ベータ版ではなく正式版が出た際にもう一度更新します。
- 投稿日:2020-02-09T16:34:36+09:00
UTIを調べるCLIツールをSwiftで作ったメモ書き
UTIって何?
Uniform Type Identifier(UTI)はデータ(エンティティ)のタイプ(種類、型)を一意に識別する文字列である。 Wikipedia
UTI調べて
public.jpeg
であれば、a.jpegファイルでb.jpgであろうとJPEGファイルということがわかる。UTI調べて何するの?
作ろうとしていたツールで、NSOpenPanelから拡張子が特定ものだけしか選択できないようにしたかった結果、UTIを指定する必要があった。
Apple独自の拡張子とかは特にUTIがわからないので、困りがち。
movie.mov
はUTIがcom.apple.quicktime-movie
だったりするし…。なんでCLI作ったん?
プログラムでUTIを調べることは簡単にできたものの、UTI必要になったタイミングでそのプログム毎回buildするの?って思ったら面倒じゃん。何回も調べるかと言われたらわからないけど、terminalでファイルを引数に渡してすぐ調べられた方が楽じゃん。と思って、CLI作った。
CLIの作り方(Swift製)
この2つでほぼ問題なく出来る。
install方法は、
make install
でやりたかったので、以前お世話になったSimulatorsを参考に必要部分だけ変更して作成。上記内容を参考に作っていたけど、release buildしたものを実行しようとすると次のerrorが出て上手くいかなかった(
suti
は今回作ったCLIのbin名)dyld: Library not loaded: @rpath/libswiftCore.dylib Referenced from: /usr/local/bin/suti Reason: image not found調べた結果、buildの仕方が変わって
❌beforeswift build -c release -Xswiftc -static-stdlib⭕️afterswift build -c release
となる。
Swift ForumのHow to `Always Embed Swift Standard Libraries`?に書いてあった。
SUTI
そして出来上がったのが、
SUTI
(Search Uniform Type Identifierの略)example$ suti ~/Desktop/sample.jpg UTI is public.jpeg
注意点
- 10行レベルのコードなのでテストコード書いてない
- UTI調べていて、1点問題があって、拡張子の偽装は見抜けない。
元が
a.jpeg
のファイルの拡張子を変えてa.png
としてsutiに渡したところUTI is public.pngと表示されてしまった。あくまで、拡張子からUTIを判別することしか出来ない。
ただ、これでApple独自の拡張子からUTIを調べられるようになった。
- 投稿日:2020-02-09T14:38:58+09:00
UITextViewの特定の文字列を下線付きリンクにする
開発環境: Xcode11.3/Swift5.1.3
概要
今回は文字色の指定、フォント指定、リンク化や下線などの複数の要素を簡単に追加していくため、
NSAttributedStringではなくNSMutableAttributedStringを利用した実装をしていきます。GitHub: UITextViewWithLink
ひとまず完成GIF
通信が遅いのはご愛敬ということで...
スカイスパがとても良さそう
実装
下準備
ViewControllerのメンバ変数に使用する
-UITextView
- 表示したい文
- 下線付きリンクにしたい特定の文字列
を定義しておきます。(URLも定義しておいた方がいいのでは)MainViewController.swiftprivate let textViewWithLink = UITextView() private let mainText = "そろそろ横浜スカイスパと戸越銀座温泉に行きたい" private let skySpaText = "横浜スカイスパ" private let togoshiGinzaBathText = "戸越銀座温泉"UITextViewの設定
今回の目的はタップした際に、リンクからWebページに遷移することなので、
isEditable
やisScrollEnabled
は一応無効にしておきます。
(isEditable = true
でもリンクから遷移できたので、DelegateやRx等を利用すれば
打ち込まれた文字列を評価して、特定の文字だけリンク化させることもできますね)MainViewController.swifttextViewWithLink.text = mainText textViewWithLink.isScrollEnabled = false textViewWithLink.isEditable = falseNSMutableAttributedString
表題を達成するためのクラスです。説明は公式ドキュメントを参照してください。
Attributeの追加
こんな関数を用意しています。
下線付きリンクを設定したい文字列を含むUITextViewを引数に取り、
関数内部で特定の文字列に加工を施していきます。MainViewController.swiftprivate func addLinkAttributes(target textView: UITextView) { // NSMutableAttributedStringのinitializerに渡す文字列を、UITextViewから取得 guard let targetText = textView.text else { return } let attributedString = NSMutableAttributedString(string: targetText) // 追加したいAttributeを追加していく attributedString.addAttribute(.foregroundColor, // 文字の色 value: UIColor.label, range: NSString(string: targetText).range(of: targetText)) attributedString.addAttribute(.font, // フォント value: UIFont.boldSystemFont(ofSize: 28), range: NSString(string: targetText).range(of: targetText)) attributedString.addAttribute(.link, // リンク化 value: "https://www.skyspa.co.jp/", range: NSString(string: targetText).range(of: skySpaText)) attributedString.addAttribute(.underlineStyle, // 下線 value: NSUnderlineStyle.single.rawValue, range: NSString(string: targetText).range(of: skySpaText)) attributedString.addAttribute(.link, // リンク化 value: "http://togoshiginzaonsen.com/", range: NSString(string: targetText).range(of: togoshiGinzaBathText)) attributedString.addAttribute(.underlineStyle, // 下線 value: NSUnderlineStyle.single.rawValue, range: NSString(string: targetText).range(of: togoshiGinzaBathText)) // 対象となるUITextViewのattributedTextプロパティにAttributeを追加したattributedStringを格納する textView.attributedText = attributedString }...うぅん...なんだか繰り返し似た処理書いてる...面倒臭さい...
大丈夫です、こんな悩みを解決する術もちゃんと用意されています
addAttribute(_:value:range:)
先ほど利用していたメソッドです。(公式ドキュメント)。
Attributeをひとつずつ追加していく場合に利用します。
が、先ほどのコードの様に
同じ特定の文字列に対して複数のAttributeを追加する際には、繰り返し記述が必要で若干不便です。addAttributes(_:range:)
そんな上記の悩みを解決するメソッドです。(公式ドキュメント)
MainViewController.swiftprivate func addLinkAttributes(target textView: UITextView) { guard let targetText = textView.text else { return } let attributedString = NSMutableAttributedString(string: targetText) attributedString.addAttributes([.foregroundColor: UIColor.label, .font: UIFont.boldSystemFont(ofSize: 28)], range: NSString(string: targetText).range(of: targetText)) attributedString.addAttributes([.link: "https://www.skyspa.co.jp/", .underlineStyle: NSUnderlineStyle.single.rawValue], range: NSString(string: targetText).range(of: skySpaText)) attributedString.addAttributes([.link: "http://togoshiginzaonsen.com/", .underlineStyle: NSUnderlineStyle.single.rawValue], range: NSString(string: targetText).range(of: togoshiGinzaBathText)) textView.attributedText = attributedString }第一引数に追加したいAttributeをまとめて渡します。
引数の型は[NSAttributedString.Key : Any]
で、keyとvalueを指定するだけです。
だいぶスッキリしましたね。気を付けること
NSMutableAttributedString
を利用して文字列を加工する場合、
事前にUITextViewに設定したフォントサイズや色などは上書きされるので、
その分のAttributeを追加する必要があります(今回で言うとこの.foregroundColor
と.font
)。後記
UITextViewのリンクでの画面遷移が若干遅い気がしたので、タイムラグを減らそうと
UILabelとUITapGestureRecognizerを用いての実装も試しました。
けれども、NSLayoutManager
やNSTextContainer
など
登場するクラスも増え、座標取得する必要も出てきたので
大人しく標準仕様に従う方が良さそうです。
(気になる方はこちらの記事をご覧ください。)
- 投稿日:2020-02-09T11:28:03+09:00
AutoLayOutを使ったUIScrollViewの作り方
AutoLayOutを使ったUIScrollViewの作り方
https://github.com/objcToScript/scrollSample
ViewControllerのサイズをFreeDomにする 高さを設定する ここでは2000px
UIScrollViewを配置する。高さを設定する。ここでは2000px
UIScrollViewの中にUIViewを入れる。必ず名前を変更してください。 ここではContentViewという名前にします。高さを設定します。ここでは2000px
左のパネルと右のパネル ここを中心に使っていきます。
├□┤ボタンは混乱するのもとなのであまり使わないほうがいい
├△┤ボタンはupdate clearなどに使います
ContentViewからUIScrollViewに制約をつけます。紐付け方
ContentViewを選択する。
Cntrlキーを押しながらUIScrollViewに線を紐づけます。
上下左右にUIScrollViewに制約を紐づけます
WidthもUIScrollViewに制約を追加します。
紐づけると画像のように白い点が付きます。
同じようにUIScrollViewから上の階層のUIViewにも制約を紐づけます。
上下左右に一番上のUIViewに制約を紐づけます
画像のような状態にします。余分な制約数値が入ってしまった場合は、右のパネルで修正します。余分な数字に0を入力します。
Reverseを使うと左のパネルの数値が見やすくなります。
ContentViewの中に必ず何かviewを置きます。つっかい棒的なものがないとうまくスクロールできません
うまくやるコツ
左と右のAutoLayOutのパネルを主に使う
制約付けも左のパネルでCtrlキーを押してドラッグして制約づけする
制約の値の修正は右のパネルでやる
├□┤ボタンはあまり使わないほうがよい
├△┤ボタンはupdate clearなどに使う
- 投稿日:2020-02-09T08:48:05+09:00
NWPusherで手軽にiPhone実機へプッシュ通知を送り、テストをする
iPhoneアプリのデバッグ過程でプッシュ通知をテスト端末に送る必要があったのですが、
NWPusherのおかげで手軽に何度もプッシュ通知を送ることができ、とても捗りました!NWPusherをインストールする
GitHubのページは以下です。
GitHub - noodlewerk/NWPusher: OS X and iOS application and framework to play with the Apple Push Notification service (APNs)Readmeに記載されていますが、Homebrew caskでのインストールがオススメです。
brew cask install pusher
Homeberwを使わない場合は最新のバイナリーがダウンロードできます。
Latest Release · noodlewerk/NWPusher · GitHubインストールできたら、NWPusher(以下Pusher)を起動します。
p12ファイルのインポート
p12ファイルが手元にない場合は、作成するか、権限のある人に依頼しましょう!
【iOS】複雑な証明書周りをあっさり整理してみた – ゆるtech。p12ファイルを入手できたら、
PusherアプリのImport PKCS #12 file (.p12)...
からインポートします。デバイストークンの入手
プッシュ通知の送り先を識別するための端末個別のトークンが必要なので、下記のオーバーライドメソッドで取得します。
8桁×8の英数字文字列がコンソールに出力されるのでコピーしましょう。Swift
AppDelegate.swiftfunc application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // ① プッシュ通知利用許可のリクエスト UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in guard granted else { return } DispatchQueue.main.async { // ② プッシュ通知利用の登録 UIApplication.shared.registerForRemoteNotifications() } } return true } // ③ プッシュ通知の利用登録が成功した場合(引数でデバイストークンが取れる) func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { print("Device token: \(deviceToken)") } // ④ (省略可)プッシュ通知の利用登録が失敗した場合はこちらが呼ばれる func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { print("Failed to register to APNs: \(error)") }Objective-C
AppDelegate.m- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // ① プッシュ通知利用許可のリクエスト [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound) completionHandler:^(BOOL granted, NSError * _Nullable error) { if (granted) { dispatch_async(dispatch_get_main_queue(), ^{ // ② プッシュ通知利用の登録 [UIApplication.sharedApplication registerForRemoteNotifications]; }); } }]; return YES; } // ③ プッシュ通知の利用登録が成功した場合(引数でデバイストークンが取れる) - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { NSLog(@"Device token: %@", deviceToken); } // ④ (省略可)プッシュ通知の利用登録が失敗した場合はこちらが呼ばれる - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { NSLog(@"Failed to register to APNs: %@", error); }デバイストークンの入力
コピーしたデバイストークン(8桁×8の英数字文字列)をPusherアプリに入力します。
8桁ごとにスペースで区切られたままでも大丈夫です。
- Expiry(有効期限)
- Priority(優先度)
を設定することもできますが、デフォルトの
None
でも問題ありません。書かなかったこと
以下のことは試していないです。
GitHubのReadmeに詳しくHow-toが書いてありました。
- CocoaPodsまたはCarthageを用いて、iPhoneアプリにフレームワークを入れればiPhoneからプッシュ通知を送ることができる。
- iOS, macOS で、プログラムを書いて通知を送信するためのフレームワークとしても使用できる。
参考リンク