20200823のiOSに関する記事は8件です。

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で表示してあげればよく、デグレも起こりにくい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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は通常;黒、ダークモード:白

プレビュー

通常モード

ダークモード

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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を使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PC内のファイルをスマホからも編集したい。

はじめに

PCのVSCodeで編集したファイルを、スマホのPWEditorでも編集できる環境を整えたので、共有します。

環境

  • PC:Windows10
  • スマホ:iOS13.6

参考記事と相違点

おすすめのPCとスマホ間のメモ連携方法

ほぼ↑の記事の通りですが、2020年8月現在、PWEditorにGoogleでログインできなくなっているため、Googleドライブの代わりにDropboxを利用したら上手くいきました。

おわりに

メモ書きが捗ります。お試しあれ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

何もしてないのに 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 upgrade
  • iOS Deployment Target12.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.plistiOS 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.plistMinimumOSVersionが追従していないっぽい?

結論

  • 動けばいいんだよガハハハ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【入門】iOS アプリ開発 #6【キャラクタの操作】

はじめに

今回はパックマンのキャラクタ操作を作成する。操作はスワイプで行い、以下が完成イメージ。ソースコードはGitHub に公開しているので参照してほしい。

※YouTube動画
IMAGE ALT TEXT HERE

仕様書

注目すべきところはスピードレベルの仕様。単に1ドットずつ動かすなら簡単だが、微妙なスピード調整の仕様があり、ゲームの難易度を調整している。

Spec1.png

エサを食べる、食べない、パワーエサを食べた状態などで、移動スピードが異なる。仕様段階でここまで定義していたとは奥深い。

Spec3.png

「スピードの数字は、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行程度。
操作のパフォーマンスは問題ない状況。

次はモンスターを作成していく。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む