- 投稿日:2020-08-23T21:29:55+09:00
DHHのクロスプラットフォームアプリ・アーキテクチャ
DHH(Ruby on Rails作者)による「Basecamp」スマホアプリのアーキテクチャ解説記事について、所感をメモ。
記事
Basecamp 3 for iOS: Hybrid Architecture
https://m.signalvnoise.com/basecamp-3-for-ios-hybrid-architecture/所感
ざっくりいえば「画面遷移はネイティブ / 画面の中身はHTML」というように役割分担したアーキテクチャになっている(実際はもっと細かいが)。HTMLを使ったスマホアプリはUXが問題になりやすいが、これによってうまく回避している。
確かに静的なテキストや図、ちょっとしたUIなどを表示するだけであれば、HTMLもネイティブもほぼ区別はつかない。むしろWebフロントエンドのための数々のフレームワークを活用でき、ネイティブより早くきれいにできる場合すらある。しかし、ネイティブのインタラクションや画面遷移をHTMLが模倣することは難しい(AndroidのバックキーやiOSのスワイプバックの扱いの問題もある)。そこで画面遷移周りをネイティブに任せることで問題を回避している。
基本的にReact NativeやFlutter等のフレームワークは使わない構成ではあるが、フレームワークの寿命に引っ張られないで済む。iOSとAndroidからWebViewがなくならない限り可能な構成である。
Webアプリともコードが共通化できる利点もある。
また、ほかのアプリへ画面を移植しなければならない場合もやりやすいと考えられる。たとえば、「Unity製ゲームへ画面を移植してほしい」と依頼された場合、React NativeやFlutterで書かれた画面であれば、プロジェクト構成を大きくいじることになる。かなりの時間がかかってデグレもあり得る。しかし、HTMLで書かれていればWebViewで表示してあげればよく、デグレも起こりにくい。
- 投稿日:2020-08-23T19:58:14+09:00
[SwiftUI]プレビューをDark Modeにする
SwiftUIのプレビューでDark Modeを試したい時のメモです。
ソース
import SwiftUI struct ContentView: View { var body: some View { ZStack { Color.init("Background") .edgesIgnoringSafeArea(.all) Text("Hello, World!") .foregroundColor(Color("TextPrimary")) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() .environment(\.colorScheme, .dark) } }ポイントは
.environment(\.colorScheme, .dark)で指定しているところです。
通常は.lightでダークモードは.darkを指定します。
切り替えることですぐに確認ができます。ソースで使っている色については
- Backgroundは通常:白、ダークモード:黒
- Textは通常;黒、ダークモード:白
プレビュー
- 投稿日:2020-08-23T19:04:28+09:00
CIImageをCVPixelBufferに
CIImageをCVPixelBufferにします。
1、CVPixelBufferをつくります。
var pixelBuffer: CVPixelBuffer? let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary let width:Int = 256 let height:Int = 256 // 欲しいサイズ CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, attrs, &pixelBuffer)2,CVPixelBufferにCIImageをレンダリングします。
let context = CIContext() context.render(ciImage, to: pixelBuffer!)extensionにしておくと、CIImage.pixelBuffer( )でかんたんにPixelBufferを取得できます。
extension CIImage { func pixelBuffer(cgSize size:CGSize) -> CVPixelBuffer? { var pixelBuffer: CVPixelBuffer? let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary let width:Int = Int(size.width) let height:Int = Int(size.height) CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, attrs, &pixelBuffer) // put bytes into pixelBuffer let context = CIContext() context.render(self, to: pixelBuffer!) return pixelBuffer } }
Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。
- 投稿日:2020-08-23T18:38:39+09:00
PC内のファイルをスマホからも編集したい。
- 投稿日:2020-08-23T16:41:31+09:00
[Bitrise]2FAが有効になっているアカウントでAppStoreConnectにアップロード
BitriseでAppStoreにアップロードする時、2FA(二要素認証)が有効化されていると、メールアドレスとパスワードの2つだけではアップロードできません。
2FAをせずにAppStoreConnectと通信するためにはもう1つ情報が必要になります。
何度か調べたりしていたので、メモ。Appleアカウントの設定
- https://appleid.apple.com/account/home にログイン
- 「セキュリティ」セクションで、「App 用パスワード」の下の「パスワードを生成」をクリック
- 生成された文字列をコピー
Bitriseの設定
- Workflow Editorを開く
SecretsタブでkeyがAPPSTORE_CONNECT_APP_PASSWORD(例)とvalueとして先ほどコピーした文字列を入力
- AppStoreConnect用メールアドレスとパスワードはすでに設定しているとします
Deploy to iTunes Connect - Application Loaderステップの中のApplication Specific Passwordの項目に先ほど設定した$APPSTORE_CONNECT_APP_PASSWORDと入力これで2FAアカウントを使い、BitriseからAppStoreConnectにアップロードできます。
参考
https://support.apple.com/ja-jp/HT204397
https://bannzai.hatenadiary.jp/entry/2019/10/22/005621
- 投稿日:2020-08-23T09:50:26+09:00
[iOS] [Android] [Crashlytics] Fabric SDKを使っているアプリはそろそろヤバイ
Crashlyticsを利用中のアプリについて、Fabric SDKを組み込んでいる場合、2020年11月15日までにSDKを入れ替えてリリースしないと、Firebase Crashlyticsでクラッシュレポートを見ることができない、ということになります。
Firebase Crashlyticsの公式ドキュメントより:
注: Fabric SDK は非推奨になりました。
Fabric SDK がアプリのクラッシュを報告するのは 2020 年 11 月 15 日までとなります。
この日をもって、Fabric SDK と古いバージョンの Firebase Crashlytics SDK はアプリのクラッシュの送信を停止します。
引き続き Firebase コンソールでクラッシュ レポートを表示するには、Firebase Crashlytics SDK のバージョンを Android の場合は 17.0.0 以降に、iOS の場合は 4.0.0 以降に、Unity の場合は 6.15.0 以降にアップグレードしてください。https://firebase.google.com/docs/crashlytics/get-started?hl=ja
Firebase Crashlytics SDKへの移行手順はこちらにあります。
https://firebase.google.com/docs/crashlytics/upgrade-sdk?hl=ja
- 投稿日:2020-08-23T03:15:22+09:00
何もしてないのに ERROR ITMS-90208 が出てデプロイできなくなったとき
現象
- ビルド自体は通る
- 既存のFlutterアプリをTestFlightに飛ばそうとしたら次のエラーが出る。
ERROR ITMS-90208: "Invalid Bundle. The bundle Runner.app/Frameworks/App.framework does not support the minimum OS Version specified in the Info.plist."何もしていないが、もしかすると関係があるかもしれない行為
flutter upgradeiOS Deployment Targetを12.4に変更した ← どうみてもこれ環境
[✓] Flutter (Channel dev, 1.22.0-1.0.pre, on Mac OS X 10.15.5 19F101, locale ja-JP) [✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3) [✓] Xcode - develop for iOS and macOS (Xcode 11.6) [✓] Android Studio (version 4.0) [✓] VS Code (version 1.48.0) [✓] Connected device (2 available)対策
{FlutterアプリのRoot}/ios/AppFrameworkInfo.plistをiOS Deployment Targetと同じ値(12.4)に変更{FlutterアプリのRoot}/ios/Flutter/App.frameworkを一度削除してから、もう一度フルビルド$ cd your-nice-flutter-app # MinimumOSVersionを書き換えて $ code ios/AppFrameworkInfo.plist $ rm -rf ios/Flutter/App.framework $ flutter clean $ flutter build ios --release #link時にApp.frameworkが作り直される $ cd ios # ご安全にデプロイ! $ fastlane beta推測
iOS Deployment Targetのバージョンを変更したときに、App.framework/Info.plistのMinimumOSVersionが追従していないっぽい?結論
- 動けばいいんだよガハハハ
- 投稿日:2020-08-23T01:11:37+09:00
【入門】iOS アプリ開発 #6【キャラクタの操作】
はじめに
今回はパックマンのキャラクタ操作を作成する。操作はスワイプで行い、以下が完成イメージ。ソースコードはGitHub に公開しているので参照してほしい。
仕様書
注目すべきところはスピードレベルの仕様。単に1ドットずつ動かすなら簡単だが、微妙なスピード調整の仕様があり、ゲームの難易度を調整している。
エサを食べる、食べない、パワーエサを食べた状態などで、移動スピードが異なる。仕様段階でここまで定義していたとは奥深い。
「スピードの数字は、1フレームに1ドット移動するスピードを”16”として、2ドット移動を”32”として、他はそれに準じて分割。」と書いてあるので、1ドット進むスピードを 16 として、作成していく。
また仕様書には記載がないが、パックマンが迷路を曲がる時はモンスターより早く内側に曲り、曲りながら移動することでモンスターから引き離せるチューニングがされているようだ。本当に奥深い。
スワイプ操作の作成
スワイプ操作をパックマン・オブジェクトに伝えるのは、
sendEvent(message: .Swipe, parameter: [direction])というようにしたい。
イベントは、迷路シーンの中で生成したパックマン・オブジェクトへ通知する。
大もととなる GameScene クラスの touchesBegan, touchesEnded メソッドにスワイプ動作のコードを実装する。gameMain オブジェクトの sendEvent を呼ぶ。
class GameScene: SKScene { /// Main object with main game sequence private var gameMain: CgGameMain! /// Points for Swipe operation private var startPoint: CGPoint = CGPoint.init() private var endPoint: CGPoint = CGPoint.init() override func didMove(to view: SKView) { // Create and start game sequence. gameMain = CgGameMain(skscene: self) gameMain.startSequence() } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { // Get start touchpoint for swipe. if let t = touches.first { let location = t.location(in: self) startPoint = CGPoint(x: location.x, y: location.y) } } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { // Get end touchpoint for swipe. if let t = touches.first { let location = t.location(in: self) endPoint = CGPoint(x: location.x, y: location.y) let x_diff = endPoint.x - startPoint.x let y_diff = endPoint.y - startPoint.y // Send swipe message to GameMain. if abs(x_diff) > abs(y_diff) { gameMain.sendEvent(message: .Swipe, parameter: [Int(x_diff > 0 ? EnDirection.Right.rawValue : EnDirection.Left.rawValue)]) } else { gameMain.sendEvent(message: .Swipe, parameter: [Int(y_diff > 0 ? EnDirection.Up.rawValue : EnDirection.Down.rawValue)]) } } }CgGameMain class
gameMain: CgGameMain (オブジェクト:クラス)は、前回のアーキテクチャでいう、root のオブジェクトとして作成した。
gameMain の sendEvent メソッドは、gameMain が持つアクティブ(enabled=true)なオブジェクトにメッセージを送信する。
以下のコードにより、scene_maze(迷路を描画するシーン) が startSequence() により enabled=true となり該当する。
/// Main sequence of game scene. class CgGameMain : CgSceneFrame { private var scene_attractMode: CgSceneAttractMode! private var scene_maze: CgSceneMaze! private var scene_intermission1: CgSceneIntermission1! private var scene_intermission2: CgSceneIntermission2! private var scene_intermission3: CgSceneIntermission3! init(skscene: SKScene) { super.init() // Create SpriteKit managers. self.sprite = CgSpriteManager(view: skscene, imageNamed: "pacman16_16.png", width: 16, height: 16, maxNumber: 64) self.background = CgCustomBackgroundManager(view: skscene, imageNamed: "pacman8_8.png", width: 8, height: 8, maxNumber: 2) self.sound = CgSoundManager(binding: self, view: skscene) self.context = CgContext() scene_attractMode = CgSceneAttractMode(object: self) scene_maze = CgSceneMaze(object: self) scene_intermission1 = CgSceneIntermission1(object: self) scene_intermission2 = CgSceneIntermission2(object: self) scene_intermission3 = CgSceneIntermission3(object: self) } /// Handle sequence /// To override in a derived class. /// - Parameter sequence: Sequence number /// - Returns: If true, continue the sequence, if not, end the sequence. override func handleSequence(sequence: Int) -> Bool { switch sequence { case 0: // Start maze sequence. scene_maze.startSequence() goToNextSequence() default: // Forever loop break } // Continue running sequence. return true } }迷路シーンクラスでは、初期化時に新規に作成したパックマンのクラス CgPlayer を CgSceneMaze に関連付け(binding)て生成し、イベントが通知できるようにする。
そのため、CgPlayerクラスの基底クラスは CbObject となっている。
シーケンス実装の handleSequenceメソッド内でパックマンを player.start()
(enabled=true)にすると、CgPlayer内の updateメソッドがフレーム毎に呼ばれて移動処理を更新する仕組みとなる。class CgSceneMaze: CgSceneFrame, ActorDeligate { var player : CgPlayer! convenience init(object: CgSceneFrame) { self.init(binding: object, context: object.context, sprite: object.sprite, background: object.background, sound: object.sound) player = CgPlayer(binding: self, deligateActor: self) } /// Handle sequence /// To override in a derived class. /// - Parameter sequence: Sequence number /// - Returns: If true, continue the sequence, if not, end the sequence. override func handleSequence(sequence: Int) -> Bool { switch sequence { case 0: drawBackground() let _ = setAndDraw() printPlayers() printBlinking1Up() drawPowerFeed(state: .Blinking) player.reset() player.start() goToNextSequence() case 1: // Foever loop break // // Round clear animation(Maze flashes) // case 10: blinkingTimer = 104 // 104*16ms = 1664ms goToNextSequence() case 11: if blinkingTimer == 0 { goToNextSequence() } else { let remain = blinkingTimer % 26 if remain == 0 { drawMazeWall(color: .White) } else if remain == 13 { // 13*16ms = 208ms drawMazeWall(color: .Blue) } blinkingTimer -= 1 } case 12: // Stop and exit running sequence. return false default: // Stop and exit running sequence. return false } // Play BGM if player.timer_playerWithPower.isCounting() { sound.playBGM(.BgmPower) } else { sound.playBGM(.BgmNormal) } // Continue running sequence. return true }CgPlayer class
パックマンの CgPlayerクラスは、CbContainer & CbObjectクラスを継承した CgActorクラスを継承している。handleEventメソッドをオーバーライドすれば、Swipe イベントを取得できる。これがやりたいためにクラス構成を設計してきた。
/// Player(Pacman) class derived from CgAcotr class CgPlayer : CgActor { enum EnPlayerAction: Int { case None, Stopping, Walking, Turning, EatingDot, EatingPower, EatingFruit } var targetDirecition: EnDirection = .Stop var actionState: EnPlayerAction = .None var timer_playerWithPower: CbTimer! var timer_playerNotToEat: CbTimer! override init(binding object: CgSceneFrame, deligateActor: ActorDeligate) { super.init(binding: object, deligateActor: deligateActor) timer_playerWithPower = CbTimer(binding: self) timer_playerNotToEat = CbTimer(binding: self) actor = .Pacman sprite_number = actor.getSpriteNumber() enabled = false } // ============================================================ // Event Handler // ============================================================ /// Event handler /// - Parameters: /// - sender: Message sender /// - id: Message ID /// - values: Parameters of message override func handleEvent(sender: CbObject, message: EnMessage, parameter values: [Int]) { switch message { case .Swipe: if let direction = EnDirection(rawValue: values[0]) { targetDirecition = direction } default: break } } /// Update handler /// - Parameter interval: Interval time(ms) to update override func update(interval: Int) { if actionState == .Turning { turn() } else { if canMove(to: targetDirecition) { direction.set(to: targetDirecition) } else { direction.update() if canTurn() { actionState = .Turning direction.set(to: targetDirecition) return } } move() } }handleEventメソッドでパックマンの移動方向を設定し、updateメソッドで設定された移動方向の処理を行う。
まとめ
今回、パックマンのキャラクタ操作を実装するにあたり、以下のファイル(クラス)を新規で作成した。
GameMain.swift(CgGameMain)
GamePlayer.swift(CgPlayer)
GameActor.swift(CgActor,CgPosition,CgDirection)
GameContext.swift(CbContext)ソースコードは全体で 3000行程度。
操作のパフォーマンスは問題ない状況。次はモンスターを作成していく。




