- 投稿日:2020-02-26T18:27:01+09:00
BitriseでAdHocアプリ配布するまで
ゴール
PRのMerge時に自動でアプリを作成し、SlackでQRコードを共有する。
前提
- Bitrise のアカウントを持っている。
Distribution
のP12ファイルを持っている。Ad Hoc
のProvisioning Profile
が作成されている。準備
アーカイブ時に使用される
Provisioning Profile
をAd Hoc
のものに設定する。
説明が面倒なので省略(そのうち追記するかも...)Bitrise の App を作成
1. プライバシーの設定
2. リポジトリの選択
3. リポジトリの設定
4. ブランチの設定
5. 配布方法の設定
6. 画像を選択
7. Webhookの設定
8. 初回ビルド
WorkFlow
の作成特に理由がなければ各
Step
のVersion
をAlways latest
(常に最新) に変更してしまいましょう
Script
、Xcode Test
は今回は使わないので削除
QRコード生成の
Step
を追加
Deploy to Bitrise.io
のStep
の後ろに QRコード生成のStep
を追加
Slack
のStep
を追加
Cache:Push
の後にSlack
のStep
を追加
Webhook URL
を設定QRコードをSlackのメッセージに含める
保存
Provisioning Profile
と証明書
を設定
Provisioning Profile
と証明書
を設定
ビルド
実機にインストール
インストールページを開く
構成プロファイルをダウンロード
構成プロファイルをインストール
設定アプリを開くと「プロファイルがインストールされました」と表示されているので、タップ
「インストール」をタップ
アプリをダウンロード
インストールページを開くとインストールが可能になっているので、「Install」をタップ
「OK」をタップ
「インストール」をタップ
アプリがインストールされました
トリガーを設定
WorkFlow
が完成したので、最後にトリガーを設定しましょう。
Triggers
タブを開くトリガーを設定
ブランチ名と
WorkFlow
を設定し、「Done」をクリック
設定を保存
あとがき
アプリの配布はいくつか種類がありますが、今回紹介した
Bitrise
のものはファイルの追加などがなく、Bitrise
上で完結していることがメリットかと思います。
よろしければ使ってみてください。
- 投稿日:2020-02-26T15:37:47+09:00
iOSアプリ申請リジェクト Apple Developer Program account under investigation
下記の理由でアプリ申請がリジェクトされる
We are unable to continue this app’s review because your Apple Developer Program account is currently under investigation for not following the App Store Review Guidelines’ Developer Code of Conduct.
googleで翻訳すると、
お使いのApple Developer Programアカウントは、App Storeレビューガイドラインのデベロッパー行動規範に従っていないため、現在調査中のため、このアプリのレビューを続行できません。
ネットで解決方法を調べると
- 2019年初期から同様の事象が多発している。
- 個人開発者によく起きる
- かつ、1つ目のアプリ申請で起きやすい
- ゲームアプリに起こりやすい
- Appleの調査中は、開発者は何も出来ない(別のアプリのアップデートもできなくなる)
- 調査期間は、半年以上と言う人もいる
- 2020年1月時点では、調査期間が1週間から1ヶ月で推移していらしい
参考1
https://forums.developer.apple.com/thread/116331参考2
https://qiita.com/gureta/items/b21b264dfa95051e67bb試したこと
申請したアプリをapp store connectから削除して、新しいappとして再申請(開発者アカウントは同じ)
結果
2回目のWe are unable to continue this app’s review because...
同じ事象でリジェクトされる。ただただ、待つしかなくなる。
開発環境
- 個人の開発者アカウント
- 初めてのアプリ申請
- firebaseを使ったSNSアプリ
- ホームページあり
経緯
2月21日
- 15:49 アプリ申請提出
- 18:41 ステータスが In Reviewに更新
- 20:51 申請のリジェクト通知
2月22日
ネットでリサーチした結果、リジェクトされたアプリを削除
2月24日
申請内容を見直してみて、再度新規のアプリとして申請し直す。
- 20:07 申請提出
2月25日
- 4:32 ステータス In Review
2月26日
- 2:30 2回目のリジェクト(理由は、1回目と同じApple Developer Program account is currently under investigation)
- 投稿日:2020-02-26T13:56:53+09:00
XCTest(Unit Test)で明示的に書かれていないいくつかの仕様
単体テストで画面遷移もやっちゃおう、と思って格闘したときの覚書です。
アプリの起動はテスト実行時一回のみ行われる
テスト対象のアプリは、テストの実行時一回のみ行われます。
@testable import appつまりAppDelegateは初回のテストケースで一度だけ走ります。
各テストケースでは走リません。デフォルトのテストケースの実行順序はクラス名順 -> テストケースメソッド名順
テストケースの実行順序は名前の順になります。
個人的にはクラスの中で、上から下に舐めていって欲しい気持ちがあったのですが、それは仕様みたいです。
テストケースの実行をランダムにするオプションはあるんですが、デフォルトは名前順。基本テストケースの実行順序に依存するようなテストコードを書くな、というのはあるんでしょうけど、
何らか制約が生じてしまうときは、名前で縛るしかないのかなあという感じです。
たとえばtest_1_何かの確認
、test_2_何かの確認
みたいな。あるテストケースでアプリに加えた変更は次のテストケースに引き継がれる
テストケースのたびにアプリの状態はイニシャルしてしまって構わないのですが、
多分パフォーマンスの問題だと思うんですが、イニシャルはされず、次に引き継がれます。
別にModelクラスならそんなに困ることはないんですが、UIが関わるクラスだと結構キツイです。tearDown()でちゃんと後始末して、常にきれいな状態で次のテストケースに引き継げるように記述できれば良いんですが、
テストのことをあまり考えずにつくったアプリだとキツイときもあって、みんなどうしてんだろうなあって思いました。Class単位にsetup()/tearDown()ができる
これは明示的に書かれていないということはなく、
テストの本読むとXCTestのライフサイクルとしてちゃんと書かれていますが、
サンプルコートの中になくて、使わないので盲点になっていました。XCTestCaseのclassメソッドsetup()/tearDown()はテストclassに対して一回。
通常のsetup()/tearDown()はテストケースに対して一回。
何を言っているのかというと、XCTestCaseを継承したクラスclass override func setUp() { <#code#> } override func setUp() { // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. } class override func tearDown() { <#code#> }この一番上と一番下のクラスメソッドはクラス中で一回だけ、
二番目、三番目のメソッドは各テストケースごとに実行されます。僕は画面遷移する前にアプリが持っている情報をテスト前に退避して、
テストでは仮のデータ突っ込んでめちゃくちゃにして、テスト後は退避したものをきれいに戻す、みたいな書き方をしようとしたんですが、
使ってみたらクラスメソッドだと子クラスの中に書いた変数が使えないので、退避先がなくて実現しませんでした。本気であまり使い所ないかもしれませんね……
UIテストだとアプリ自体の起動・終了が制御できる
アプリの状態をイニシャルできないというのは、UIテストだったら、アプリの起動・終了が制御できるので、多少は良いかなと思います。
XCUIApplication().launch()画面遷移ならむしろUIテストで書くのが筋なんですかね?
- 投稿日:2020-02-26T13:11:11+09:00
Swift で UITextView の URL リンクを短縮表示させる。
0.はじめに
Swift で UITextView に表示される URL リンクを短縮表示させたかったので、やってみました。
ついでに、キーワード検索時の強調表示もできる様にしてます。
以下の記事を参考にさせて頂きました。
感謝 ♪♪♪
?♂️?♂️?♂️
1.コード
今回は、UITextView の Extension として、
setAttributedText
という関数を追加しました。パラメータの説明は、以下。
text: String
: [元となるテキスト文字列]shortUrlMaxNum: Int = Int()
: [短縮されるURLの最大文字数]keyword: String? = nil
: [検索キーワード]keywordBackgroundColor: UIColor = .yellow
: [検索キーワードの強調される背景色]UITextView+Extension.swift// // UITextView+Extension.swift // import UIKit extension UITextView { func setAttributedText(text: String, shortUrlMaxNum: Int = Int(), keyword: String? = nil, keywordBackgroundColor: UIColor = .yellow) -> Void { var tempText: String = text var links: [(url: URL, range: NSRange)] = [] if shortUrlMaxNum > 0 { if let _detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) { var truncateCount: Int = 0 for link in _detector.matches(in: tempText, range: NSMakeRange(0, tempText.utf16.count)) { let startIndex = tempText.utf16.index(tempText.startIndex, offsetBy: (link.range.location - truncateCount)) let endIndex = tempText.utf16.index(startIndex, offsetBy: link.range.length) let _tempText = tempText[startIndex..<endIndex] if let _url = link.url, let _ = _url.host, _tempText == _url.absoluteString { var range: NSRange = link.range if _url.absoluteString.count > shortUrlMaxNum { let replacementString: String = _url.absoluteString.prefix(shortUrlMaxNum) + "..." range = NSMakeRange(link.range.location - truncateCount, replacementString.count) tempText.replaceSubrange( Range<String.Index>(NSMakeRange(range.location, link.range.length), in: tempText)!, with: replacementString) truncateCount += (link.range.length - replacementString.count) } else { range = NSMakeRange(link.range.location - truncateCount, _url.absoluteString.count) } links.append((url: _url, range: range)) } } } } let attrText: NSMutableAttributedString = NSMutableAttributedString(string: tempText, attributes: [ .font : self.font as Any, .foregroundColor : self.textColor as Any, ]) for link in links { attrText.addAttribute(.link, value: link.url.absoluteString, range: link.range) } if let _keyword = keyword { if let _regex = try? NSRegularExpression(pattern: _keyword) { for match in _regex.matches(in: text, range: NSMakeRange(0, text.count)) as [NSTextCheckingResult] { attrText.addAttribute(.backgroundColor, value: keywordBackgroundColor, range: match.range) } } } self.attributedText = attrText } }
で、こんな感じに表示されます。
99.ハマりポイント
短縮URL後のテキスト文字列の編集が、結構面倒臭かったですね…。バグってないかちょっと心配…。
あと、
tempText.utf16
を使っている箇所がありますが、これは絵文字対策で入れたものです。これも、結構面倒臭かった…。???
XX.まとめ
もしかしたら、バグあるかもしれないので、実装する場合は、自己責任でお願いします!
?♂️?♂️?♂️
あと、もっと簡単な方法があれば、教えて頂けるとと嬉しいです!
以上、ご参考になれば ♪♪♪
???
- 投稿日:2020-02-26T01:32:51+09:00
【ARKit】3Dアニメーションの切り替え
はじめに
状況に応じてアニメーションを切り替えたかったので、そのメモ。
実行環境
- Xcode 11.2.1
- MagicaVoxel 0.99.4
- Mixamo
やりたいこと
- iPhoneのミュージックなど、他のアプリでの音楽が再生/停止の検知。
- 再生中はキャラクタが踊り、停止すると踊りを止めるようにアニメーションを切り替える。 (昔あった玩具のフラワーロック的なイメージ)
Mixamoで作るアニメーションは、1つの3Dモデルに1つのアニメーションを割り当てるので、座ってる状態(sitting.scn)と踊っている状態(swing.scn)の二つのシーンからnodeを取りだして、差し替えることでアニメーションを切り替えることにしました。
他のアプリでのAVAudioSessionの状態を検知する
他のアプリでの音楽が再生/停止されたかは、AVAudioSession.silenceSecondaryAudioHintNotificationで 検知できるようなので、通知されたときに呼び出される関数(handleSecondaryAudio)をNotificationCenterに登録します。
silenceSecondaryAudioHintNotificationの登録処理func setupNotifications() { // AVAudioSession:初期化 try! AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.ambient) try! AVAudioSession.sharedInstance().setActive(true) // 他アプリでのサウンド再生/停止を監視 let nc = NotificationCenter.default nc.addObserver(self, selector: #selector(self.handleSecondaryAudio), name: AVAudioSession.silenceSecondaryAudioHintNotification, object: nil) }setupNotificationsは、viewDidLoadなどで呼び出してておきます。また、AVAudioSession.silenceSecondaryAudioHintNotification は、再生/停止が変化したの通知のため、初回のみ AVAudioSession.sharedInstance().isOtherAudioPlaying でアプリ起動時の再生/停止状態を判断して初期化します。
再生/停止検知の初期化let val_idle:String? = "sitting" let val_swing:String? = "swing" var selectedItem: String? override func viewDidLoad() { super.viewDidLoad() //(中略) setupNotifications() if AVAudioSession.sharedInstance().isOtherAudioPlaying { // 再生中!! print("---再生中") selectedItem = val_swing } else { print("---停止中") selectedItem = val_idle } }他のアプリでの再生/停止検知時の処理
notification.userInfoの AVAudioSession.SilenceSecondaryAudioHintType を取りだして、その値で再生(.begin)、停止(.end)を判断します。
そして切り替えるシーン名を変更して(selectedItem)、キャラクタのnodeを差し替える処理(changeAllNodes())を呼び出します。
再生/停止検知時の処理/// 他アプリでのサウンド再生/停止を監視 @objc func handleSecondaryAudio(notification: Notification) { // Determine hint type print("---check audio") guard let userInfo = notification.userInfo, let typeValue = userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey] as? UInt, let type = AVAudioSession.SilenceSecondaryAudioHintType(rawValue: typeValue) else { return } if type == .begin { // Other app audio started playing - mute secondary audio. print("---audio:begin") // キャラクタのnodeを差し替える selectedItem = val_swing changeAllNodes() } else if type == .end { // Other app audio started playing - mute secondary audio. print("---audio:stop") // キャラクタのnodeを差し替える selectedItem = val_idle changeAllNodes() } else { // Other app audio stopped playing - restart secondary audio. } }キャラクタのnodeを差し替える
self.mainSceneView から1つずつノードを取りだして、現在表示されているノード(prevItem)を新しいノード(selectedItem)に差し替えます。
キャラクタのnodeを差し替える// キャラクタのnodeを差し替える func changeAllNodes() -> Void { var prevItem:String? if (selectedItem == val_swing){ prevItem = val_idle } else { prevItem = val_swing } // 追加されている全ての子ノードを1つずつ取り出す for Node : AnyObject in self.mainSceneView.scene.rootNode.childNodes{ if (Node as! SCNNode).name == nil { print("name is nil") } else { if Node.name == prevItem{ if let selectedItem = self.selectedItem { // .scnファイルから新しい3Dモデルのノードを作成 let scene = SCNScene(named: "art.scnassets/\(selectedItem).scn") let newNode = (scene?.rootNode.childNode(withName: selectedItem, recursively: false))! // 3Dモデルの配置(現在と同じ位置に) newNode.position = Node.position // 3Dモデルのサイズを変更(現在と同じサイズに) newNode.scale = Node.scale // 3Dモデルに名前をつける(削除用) newNode.name = selectedItem // 新しい3Dモデルのノードに差し替える self.mainSceneView.scene.rootNode.replaceChildNode(Node as! SCNNode, with: newNode) // // シーンに追加 // self.mainSceneView.scene.rootNode.addChildNode(newNode) // // // 古い3Dモデルを削除 // Node.removeFromParentNode(); } } } }※最初は、新しいノードを追加>古いノードを削除としていましたが、replaceChildNode(_:with:)で差し替えできるとがわかったので、書き換えました。
完成
できました!
音楽の再生/停止をコントロールセンターで行うと、切り替え時にタイムラグがあるので、リモコン付きのイヤフォンで再生/停止を行うと挙動がわかりやすいです。
最後にスクリプト全体を載せておきます。
ViewController.swiftimport UIKit import ARKit import AVFoundation class ViewController: UIViewController{ @IBOutlet weak var mainSceneView: ARSCNView! let configuration = ARWorldTrackingConfiguration() // let val_idle:String? = "idle" let val_idle:String? = "sitting" let val_swing:String? = "swing" var selectedItem: String? override func viewDidLoad() { super.viewDidLoad() initialize() registerGestureRecognizers() setupNotifications() if AVAudioSession.sharedInstance().isOtherAudioPlaying { // 再生中!! print("---再生中") selectedItem = val_swing } else { print("---停止中") selectedItem = val_idle } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } func setupNotifications() { // AVAudioSession:初期化 try! AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.ambient) try! AVAudioSession.sharedInstance().setActive(true) // 他アプリでのサウンド再生/停止を監視 let nc = NotificationCenter.default nc.addObserver(self, selector: #selector(self.handleSecondaryAudio), name: AVAudioSession.silenceSecondaryAudioHintNotification, object: nil) } /// ARSCNiew初期化設定 func initialize (){ self.mainSceneView.debugOptions = [ARSCNDebugOptions.showWorldOrigin, ARSCNDebugOptions.showFeaturePoints] self.configuration.planeDetection = .horizontal self.mainSceneView.session.run(configuration) self.mainSceneView.autoenablesDefaultLighting = true } /// メインのビューのタップを検知するように設定する func registerGestureRecognizers() { let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapped)) self.mainSceneView.addGestureRecognizer(tapGestureRecognizer) // ロングプレスイベントハンドラの登録 let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressView)) self.mainSceneView.addGestureRecognizer(longPressGesture) } @objc func tapped(sender: UITapGestureRecognizer) { // タップされた位置を取得する let sceneView = sender.view as! ARSCNView let tapLocation = sender.location(in: sceneView) // タップされた位置のARアンカーを探す let hitTest = sceneView.hitTest(tapLocation, types: .existingPlaneUsingExtent) if !hitTest.isEmpty { // タップした箇所が取得できていればitemを追加 self.addItem(hitTestResult: hitTest.first!) } } // 長押しでキャラクタを削除する @objc func longPressView(sender: UILongPressGestureRecognizer) { print("----長押し!") if sender.state == .began { let location = sender.location(in: self.mainSceneView) let hitTest = self.mainSceneView.hitTest(location) print("---hitTest:%d",hitTest) if let result = hitTest.first { if ((result.node.name) != nil){ print("--.name:%d",result.node.name!) } if ((result.node.parent!.name) != nil){ print("--parent.name:%d",result.node.parent!.name!) } // 3Dアニメーションモデルは、複数パーツで構成されるため、親ノードの名前で判定・削除する if result.node.parent!.name == selectedItem { result.node.parent!.removeFromParentNode(); } } } } /// アイテム配置メソッド func addItem(hitTestResult: ARHitTestResult) { if let selectedItem = self.selectedItem { // .scnファイルから新しい3Dモデルのノードを作成 let scene = SCNScene(named: "art.scnassets/\(selectedItem).scn") let node = (scene?.rootNode.childNode(withName: selectedItem, recursively: false))! // 現実世界の座標を取得 let transform = hitTestResult.worldTransform let thirdColumn = transform.columns.3 // 3Dモデルの配置 node.position = SCNVector3(thirdColumn.x, thirdColumn.y, thirdColumn.z) // 3Dモデルのサイズを変更 node.scale = SCNVector3(0.05, 0.05, 0.05) // 3Dモデルに名前をつける node.name = selectedItem // シーンに追加 self.mainSceneView.scene.rootNode.addChildNode(node) } } /// 他アプリでのサウンド再生/停止を監視 @objc func handleSecondaryAudio(notification: Notification) { // Determine hint type print("---check audio") guard let userInfo = notification.userInfo, let typeValue = userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey] as? UInt, let type = AVAudioSession.SilenceSecondaryAudioHintType(rawValue: typeValue) else { return } if type == .begin { // Other app audio started playing - mute secondary audio. print("---audio:begin") // キャラクタのnodeを差し替える selectedItem = val_swing changeAllNodes() } else if type == .end { // Other app audio started playing - mute secondary audio. print("---audio:stop") // キャラクタのnodeを差し替える selectedItem = val_idle changeAllNodes() } else { // Other app audio stopped playing - restart secondary audio. } } // キャラクタのnodeを差し替える func changeAllNodes() -> Void { var prevItem:String? if (selectedItem == val_swing){ prevItem = val_idle } else { prevItem = val_swing } // 追加されている全ての子ノードを1つずつ取り出す for Node : AnyObject in self.mainSceneView.scene.rootNode.childNodes{ if (Node as! SCNNode).name == nil { print("name is nil") } else { if Node.name == prevItem{ if let selectedItem = self.selectedItem { // .scnファイルから新しい3Dモデルのノードを作成 let scene = SCNScene(named: "art.scnassets/\(selectedItem).scn") let newNode = (scene?.rootNode.childNode(withName: selectedItem, recursively: false))! // 3Dモデルの配置(現在と同じ位置に) newNode.position = Node.position // 3Dモデルのサイズを変更(現在と同じサイズに) newNode.scale = Node.scale // 3Dモデルに名前をつける(削除用) newNode.name = selectedItem // 新しい3Dモデルのノードに差し替える self.mainSceneView.scene.rootNode.replaceChildNode(Node as! SCNNode, with: newNode) // // シーンに追加 // self.mainSceneView.scene.rootNode.addChildNode(newNode) // // // 古い3Dモデルを削除 // Node.removeFromParentNode(); } } } } } }まとめ
Mixamoで作るモデルが、1つの3Dモデルに1つのアニメーションなので、このような方法を取りましたが、別の3Dアプリなどで複数のアニメーションを1つのオブジェクトに内包させることができたりするんですかね?(Unityのようにアニメーション設定そのものを別ファイルとして、モデルに適用するような)
もし他にいい方法があるようなら、教えてください。
以下の記事が参考になりました。ありがとうございます。