- 投稿日: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-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-09T15:20:42+09:00
CarthageでChartsを導入してみる
Carthageでグラフ描画ライブラリChartsを導入したときのメモ。
はじめてのCarthageだったので、その導入から。インストール
$ brew install carthageCartfile作成
$ cd path/to/repository $ code CartfileCartfilegithub "danielgindi/Charts" == 3.4.0ライブラリをビルド
$ carthage update --platform iOS
Carthage/Build/iOS/
にビルド成果物があれば成功。$ ls ./Carthage/Build/iOS/ 059E4A44-BC91-38F6-B36D-D72B91FDE6E5.bcsymbolmap 5A112AED-A5E6-34A0-9C82-74EF7032628F.bcsymbolmap Charts.framework Charts.framework.dSYMビルド成果物をプロジェクトに追加
TARGETSの中から、ターゲットを選択
→ Generalタブの中のFrameworkds, Libraries, and Embedded Content
の+
をクリック
→Add Other
をクリック
→Add Files
をクリック
→./Carthage/Build/iOS/Charts.framework
を選択Run Scriptを追加
TARGETSの中から、ターゲットを選択
→ Build Phasesタブの+
をクリック
→New Script Phase
を選択
→ 下記のコマンドを記述/usr/local/bin/carthage copy-frameworks→ 次にInput Filesに以下を追加
$(SRCROOT)/Carthage/Build/iOS/Charts.frameworkソースコードでimportしてみる
import Charts
を記述、ビルドしてエラーが出なければおk
- 投稿日:2020-02-09T14:46:27+09:00
UIImgaePickerControllerでビデオを撮ろうとすると強制終了する
ちょっと忘れないようにメモ
エラー内容
プロパティに
Privacy - Camera Usage Description(カメラ) を設定したが
UIImagePickercontrollerのmediaTypeをビデオにして起動すると強制終了するThis app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSMicrophoneUsageDescription key with a string value explaining to the user how the app uses this data.
Privacy - Microphone Usage Description (マイク)の方もプロパティに設定すると強制終了しなくなった。
ビデオを撮るときににはマイクとカメラの二つの使用許可を撮る必要がある。
参考
[iOS 10] 各種ユーザーデータへアクセスする目的を記述することが必須になるようです
http://dev.classmethod.jp/smartphone/iphone/ios10-privacy-data-purpose-description/
- 投稿日: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:36:49+09:00
WebView開くと大量のエラーログが出たので調べた
どんなエラーか
エラー内容は以下の通り
[Process] kill() returned unexpected error 1
これが10ミリ秒ごとくらいに発生するので他のログが埋れて非常に面倒。発生条件
ログが大量に出るだけでアプリ自体がクラッシュしたりはないので
気づくまで結構経ってたんですが、どうやらWebViewを使用している画面で発生する様子WebviewのあるViewコントローラーへの遷移時、Webviewのスクロール時
あとWebView内(html)でダイアログを出してる間とかにも発生。調べてみる
Google先生にエラー内容を聞いてみると同様の現象が発生している人がいっぱいいた。
てっきり自分がなんか実装ミスったかな?とかWebコンテンツ側がバグってんのかな?
とかFireBaseの所為か?とか思ってたけど、どうやらWebKitのバグの様子
雑に実装してたツケが回ってきたかと思ったけどそうじゃなくてよかったAppleDeveloperFormsでも同様の質問がされてて、13.3でも修正は入っていないみたい。
chintan100
Dec 11, 2019 11:26 AM
(in response to neal1)Still exists in 13.3!
How much more time before the WebKit fix is integrated into iOS?!
Super frustrating bug!
わかるよchitan100まさにSuper frustrating bugだいつ修正される?
WebKit Bugzillaで見ると、既に報告/修正済み
? 原因と修正内容ということはどのバージョンで修正が入ったものがリリースされるかだけども
WebKitのリリースノートを確認すると...
WebKit Release Noteおや?
This release covers WebKit revisions 249750-250329.
This release covers WebKit revisions 249750-250329. ?
? ? ?
250329.なおっていないんだけど...
どういうことですか....まとめ
・バグの原因はWebkitにある
・すでに修正はコミット済み
・修正入りバージョンのリリースはされていない(リリースしてるってけどなおっていない?)結論:修正バージョンリリース待ち!
修正バージョンリリースまではLogに何かしら分かりやすい目印でもつけて
Filterとかで対応するしかなさそうですね。
- 投稿日: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などに使う