20200810のiOSに関する記事は7件です。

【入門】iOS アプリ開発 #2【SpriteKit を使う】

SpriteKit

iOS向けの2Dゲーム開発用として、SpriteKit というフレームワークが提供されている。

今回、古典的なピクセルアートのゲーム「パックマン」を作るため、スプライトのキャラクタをもっと簡単に扱えるようにしたい。

キャラクタは基本 16x16ドットで構成されており、テクスチャの切り替えアニメーションも考慮すると、オブジェクトで管理するのは大変そう。

そのため1つの大きなアセット画像を 16x16ドットで切り出して、テクスチャ番号で扱えるようにする。

スプライトもオブジェクト管理ではなく、スプライト管理番号で扱えるようなクラスを作成する。

Sprite Manager class の作成

スプライトを表示する API はシンプルに、

draw(0, x:100, y:50, texture:1)

とする。

また一定間隔(0.1s)でアニメーションする API は、

startAnimation(0, sequence: [1,2,3], timePerFrame: 0.1, repeat: true)

という感じにする。

このような API を持つ、次の CgAssetManager クラスを継承する CgSpriteManager クラスを作成する。

Asset Manager class の作成

テストで使用するアセット画像は下記で、これを 16x16ドットで切り出して、テクスチャ番号を割り当てる CgAssetManager クラスを作成する。

[spriteTest.png]

テクスチャ番号は左下から右上へ、下記のように割り当てる。

4 5 6 7
0 1 2 3

スプライト描画のプログラム

// Create a sprite manager object.
let sprite = CgSpriteManager(view: self, imageNamed: "spriteTest.png", width: 16, height: 16, maxNumber: 64)

// Draw a #0 sprite with #1 texture at (8,8) position.
sprite.draw(0, x: 8, y: 8, texture: 1)

CgSpriteManagerクラスは、"spriteTest.png" のアセット画像から 16x16ドットでテクスチャを切り出し、同時に最大64個のスプライトを描画するための管理番号を持つ SpriteManager オブジェクトを生成する。

SpriteManagerオブジェクトの draw API で、スプライト管理番号0とし、ピクセル座標(8, 8)にテクスチャ番号1のスプライトを描画する。

Background Manager class の作成

背景となるタイル描画も必要で、8x8ドットのピクセル文字も再現したい。

これらも同様に、大きな背景アセット画像から 8x8ドットのテクスチャを切り出し、テクスチャ番号で扱えるようにする。

背景のタイル描画 API は、

put(0, column: 2, row: 3, texture: 1)

とする。

また、ピクセル文字を表示する API は、

putString(0, column: 2, row: 4, string: "TEST")

という感じにする。

テストで使用する1つのアセット画像は下記で、SpriteManagerと同様に、8x8ドットに切り出してテクスチャ番号を割り当てる CgAssetManager クラスを継承して、CgBackgroundManager クラスを作成する。

[backgroundTest.png]

背景描画のプログラム

// Create a background manager object.
let background = CgBackgroundManager(view: self, imageNamed: "backgroundTest.png", width: 8, height: 8, maxNumber: 2)

// Draw a #0 background of (28x36) size at (14*8,18*8) position.
background.draw(0, x: 14*8, y: 18*8, columnsInWidth: 28, rowsInHeight: 36)

// Put a #1 texture on #0 background at (14,19).
background.put(0, column: 14, row: 19, texture: 1)

// Print text on #0 background at (8,18).
background.putString(0, column: 8, row: 18, string: "SPRITEKIT TEST", offset: -16*2 /* ASCII offset */)

CgBackgroundManagerクラスは、"backgroundTest.png" のアセット画像から 8x8ドットのテクスチャを切り出し、同時に最大2面の背景を描画するための管理番号を持つ BackgroundManager オブジェクトを生成する。

BackgroundManagerオブジェクトの draw API で、背景管理番号0座標(14*8, 18*8)に 28x36 のタイル面を生成・描画する。1つのタイルサイズは、テクスチャサイズの 8x8ドットとなる。

put API は、背景管理番号 0 のタイル位置(14, 19)にテクスチャ番号 1 のタイルを描画する。

putString API では、背景管理番号 0 のタイル位置(8, 18)に "SPRITEKIT TEST" のタイルを描画する。先頭文字 "S" の ASCIIコードは 83 となり、offset: -16*2 を引いた 51番のテクスチャを描画する仕組みとなる。

テスト・プログラム

GitHub に公開しているテスト・プログラムを実行すると、以下のような画面が表示され、赤モンスターがアニメーションしながら移動する。

Asset/Sprite/Background Manager クラスは、SpritekitManager.swift ファイルにコーディングしている。コメント入れて 500行未満。

GameViewController.swift では、GameSceneのサイズを (28*8, 36*8)ドットに変更している。

class GameViewController: UIViewController {

    private var scene: SKScene!

    override func viewDidLoad() {
       super.viewDidLoad()

       if let view = self.view as! SKView? {

           let size = CGSize(width: 28*8, height: 36*8)
           scene = GameScene(size: size)

           // Set background color to black
           scene.backgroundColor = UIColor.black

           // Set the scale mode to scale to fit the window
           scene.scaleMode = .aspectFit

           // Present the scene
           view.presentScene(scene)

           view.ignoresSiblingOrder = true

           view.showsFPS = true
           view.showsNodeCount = true
       }
    }

GameScene.swift の didMove関数で Managerオブジェクトの生成と描画、update関数で赤モンスターを移動させている。

class GameScene: SKScene {

    private var sprite: CgSpriteManager!
    private var background: CgCustomBackground!

    override func didMove(to view: SKView) {

        // Create sprite and background objects.
        sprite = CgSpriteManager(view: self, imageNamed: "spriteTest.png", width: 16, height: 16, maxNumber: 64)
        background = CgCustomBackground(view: self, imageNamed: "backgroundTest.png", width: 8, height: 8, maxNumber: 2)

        // Draw cherries.
        sprite.draw(0, x: 8, y: 8, texture: 3)
        sprite.draw(1, x: 16*13+8, y: 8, texture: 3)
        sprite.draw(2, x: 8, y: 16*17+8, texture: 3)
        sprite.draw(3, x: 16*13+8, y: 16*17+8, texture: 3)

        // Draw and animate a Pacman.
        sprite.setPosition(4, x: 13*8+8, y: 16*12)
        sprite.setRotation(4, radians: CGFloat(90.0 * .pi / 180.0))
        sprite.startAnimation(4, sequence: [0,1,2], timePerFrame: 0.1, repeat: true)

        // Draw grids on #0 background.
        background.draw(0, x: 14*8, y: 18*8, columnsInWidth: 28, rowsInHeight: 36)
        background.setDepth(0, zPosition: 0)

        for y in 0 ..< 18  {
            for x in 0 ..< 14  {
                background.put(0, column: x*2, row: y*2, columnsInwidth: 2, rowsInHeight: 2, textures: [4,5,6,7])
            }
        }

        // Print text on #1 background.
        background.draw(1, x: 14*8, y: 18*8, columnsInWidth: 28, rowsInHeight: 36)
        background.setDepth(1, zPosition: 1)

        let asciiOffset = -16*2
        background.putString(1, column: 8, row: 18, string: "SPRITEKIT TEST", offset: asciiOffset)

        // Put a #63 texture on #1 background.
        background.put(1, column: 14, row: 19, texture: 128)
    }

    private var x: CGFloat = 0
    private var dx: CGFloat = 0

    override func update(_ currentTime: TimeInterval) {
        // Called before each frame is rendered

        // Move and animate a Ghost.
        if x == 0 {
            dx = 1
            sprite.startAnimation(5, sequence: [4,5], timePerFrame: 0.1, repeat: true)
        } else if x == 28*8 {
            dx = -1
            sprite.startAnimation(5, sequence: [6,7], timePerFrame: 0.1, repeat: true)
        }

        x += dx
        sprite.setPosition(5, x: x, y: 16*6)
    }
}

参考

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

WWDC2020での許諾周りのアップデートまとめ

概要

今年はプライバシー、権限周りで多くのアップデートがあったのでまとめてみます。

この記事は以下のWWDC 2020セッション動画、及び公式ドキュメントを元に書いています(ここで出てくるスクショも動画のものです)。
周辺知識も含めて深く知りたい方はご参考までに。

なお、IDFAに関してはすでに多くの記事が出ているので省きます。

所感

今までよりいっそう、「必要な分だけの権限をもらう」感が強くなった

背景

  • アプリがアクセスできる情報量とユーザ体験のバランスを取りやすくしたい
  • iOS 13までは許諾アラートが出てきたらYesと答えないとアプリのコア機能を使えないこともあった

写真

写真共有アプリの例

iOS 13の場合、写真ライブラリ全体へのアクセス権限を求めることになる。

iOS 14からはLimited Photos Libraryが使えるようになり、ユーザが選択した写真しかアプリからアクセスできない状態を作ることができる。ちなみにフォーラムの回答によると、「選択した写真のみ」の選択肢はオプトアウトできないとのこと。さらに、今回の変更は、新しいAPIや機能を採用していないアプリでも、iOS 14端末だったら影響を受けてしまう。

「Select Photos」を選択するとこのようなピッカー(PHPickerViewController)が表示される。このときピッカーは別プロセスで起動されており、選択し終わった写真しか知ることができない。

PHPicker

PHPickerUIImagePickerControllerに置き換わる新しいフレームワーク。PHPickerには検索機能、複数選択機能などを備わっている。加えて、写真ライブラリへのアクセス権を必要としないので、許諾アラートを表示する必要もない。アプリがすべての写真にアクセスする必要がない限りPHPickerを使うことが推奨されている。

アクセスレベル

ステータスは既存のものに加え、.limitedステータスが増えている。→PHAuthorizationStatus
これとは別にiOS 14からはPHAccessLevelというアクセスレベルを指定するenumが登場した。.addOnlyを指定した場合は左、.readWriteを指定した場合は右の許諾アラートになる。
スクリーンショット 2020-08-09 22.49.41.png

今までのステータス確認APIには更新が入り、アクセスレベルの指定が必要になっている。ユーザが.limitedになっているかどうかは以下のように確認する。
スクリーンショット 2020-08-09 23.10.18.png
許諾リクエスト処理は、ステータス問い合わせと同じようにアクセスレベルを指定する。これにより許諾アラートが表示されるようになる。
スクリーンショット 2020-08-09 23.21.55.png

従来のAPIは将来的に非推奨なAPIになる。従来のAPIのままだと、ユーザが「選択した写真のみ」で許可した場合、.limitedではなく.authorizedを返してしまう。
基本的には.authorized.limitedでは同じように動くべきだが、.limitedではユーザのアルバムを作成、取得できなかったり、クラウド上のコンテンツやアルバムにアクセスできなかったりするので注意が必要。

許諾範囲の変更

設定アプリから変更することができる。
スクリーンショット 2020-08-09 21.34.05.png
新しいAPIに何も対応を入れていないアプリで、ユーザが「選択した写真のみ…」にした場合は、アプリのライフサイクルごとに一度、PhotoKitのコンテンツにアクセスする時に、現在の選択範囲を維持するか、変更するかを問うアラートが出てくる。
スクリーンショット 2020-08-09 21.40.16.png
選択UIを手動で表示させて許諾範囲を変更する方法もある。
スクリーンショット 2020-08-10 12.02.24.png
ある記事によると、フォトライブラリへのフルアクセスを必要とするアプリは、ステータスが.limitedの場合、この選択UIを表示させるためのボタンを表示しないといけないといけないと書いているが、それに該当しそうな公式文章は見つけられなかった。

許諾アラートを毎回表示させないためには

PHPhotoLibraryPreventAutomaticLimitedAccessAlertinfo.plistに書いておけば、アクセスの拡張を促すアラートが自動で出てくることはなくなる。

位置情報

近くにいる人に写真を送るアプリの例

写真を共有したい友達を見つけるために位置情報を使用するアプリを考える。iOS 13では以下の許諾アラートだった。

iOS 14ではおおよその位置情報だけをアプリと共有できる機能が追加される。Precise設定をOFFにすると、おおよその位置情報の更新のみアプリに通知される。

ユーザは設定アプリから設定を変更することもできる。
スクリーンショット 2020-08-09 17.39.30.png

Apple Mapsの例

Apple Mapsでは「おおよその位置情報の共有」を選択した人には、アプリ起動時にマップ上部に目立つボタンを表示している。このボタンをタップすると許諾アラートが表示される。
スクリーンショット 2020-08-09 15.28.33.png
ユーザがアプリ内を回遊し始めるとこのボタンは目立たなくなる。
スクリーンショット 2020-08-09 17.21.22.png

App clips

App clipのユースケースに特化して新しい位置情報アクセスが設計された。App clipsでは正確な位置情報へのアクセスを行わなくても、例えば間違った店舗のウェイティングリストに入ってないかなどの確認はできる。App clipsが特定の場所で起動されたかどうかをシステムに尋ねる程度なら、許諾アラートを出す必要はない。

許諾状況の確認

「いつ」「どの程度」という2軸で許諾状況を確認することができる。
スクリーンショット 2020-08-09 17.53.12.png

一時的に正確な位置情報をリクエストする

もし正確な位置情報を必要とする機能があるアプリで、ユーザがおおよその位置情報しか共有していない場合は、ユーザに一時的に許諾のアップデートを依頼することができる。
スクリーンショット 2020-08-10 22.42.20.png
実装としては以下の通り。
スクリーンショット 2020-08-11 9.21.41.png
一時的に正確な位置情報を求める理由はinfo.plistNSLocationTemporaryUsageDescriptionDictionaryに書いておく。
スクリーンショット 2020-08-11 9.22.08.png

連絡先

写真共有アプリの例

手動で共有したい友達を選ぶために連絡先情報を使用するアプリを考える。iOS 13までは以下の許諾アラートが出るケースもあった。

とはいえCNContactPickerViewControllerを使っていれば特に許諾を取ることなく連絡先データへのアクセスが可能だった。
iOS 14からは、TextFieldに特定のtextContentTypeを指定していた場合、QuickTypeバーに連絡先データベースから取ってきた情報がサジェストされるようになる。

スクリーンショット 2020-08-10 23.05.00.png

NearbyInteraction

NearbyInteractionフレームワークを使うときはBluetoothやネットワークアクセスの使用許可をとる必要はない。その代わり、アプリは1回限りのセッションベースのアクセスを要求する。あるユーザのデバイスと、その近くにいる別のデバイスの両方が許可をしたら、デバイス間がどれくらい離れているのか、どの方向にいるのかなどを知ることができる。この許可は、アプリがフォアグラウンドで使用され続けている限り有効になっている。
スクリーンショット 2020-08-09 15.10.07.png

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

ターミナル、要らないのかな【Xcode, iOS】

今日学んだGit情報を、Outputするだけの簡素な投稿。?
今後Gitを学ぶときの為の、自分用リンクとかも貼る。

 ターミナル、要らないのかな。

先程、こちらの記事を読みました。
Xcode9時代のゆるふわGitHub生活

先日こんな記事を書いたけど、やはりコマンドは要らないのかな。
GitHub奮闘記②【手順まとめ】

でも、iOS以外の場合や、Xcode内の Git機能でも出来ないこともある場合を考えると、
コマンドは依然必要だったりするのでしょうか。

ご意見あればコメントください。

 鍵作成。

リモートリポジトリへのアクセス権限がなく失敗したので、鍵作成しました。
とても簡単に出来た。?

Permission denied (publickey). fatal: Could not read from remote repository. 

 「鍵」とは

公開鍵(英:public key)とは?

 作成手順

【git エラー解決策】Permission denied (publickey).

 git pullは使う必要がないらしい。(fetchとmergeを使う)

pull = fetch + merge origin/master

git fetchの理解からgit mergeとpullの役割

Git pullを使うべきでない3つの理由

git fetchと git merge [リモートブランチ] で最新版を取得する。

コマンドは増えますが、より「今自分が何をやっているのか」という事が明確になり、

初心者キラーのConflictが起きた時も、具体的に何と何が衝突しているのか分かりやすくなります。

Gitの理解も深まるので、ぜひfetch - mergeの手順を覚えましょう!

 UserNameと、Nameの違い。

左側のプロフィール欄にて、

Kazuki Matsumotoが「Name」で、
kazuki-userが「UserName」です。

スクリーンショット 2020-08-10 20.47.12.png

 結論。

  • UsernameはID
  • Nameは本名やニックネーム

 細かく。

  • Usernameは重複不可で、ユーザー識別子としてプロフィールページのURLなどにも。
  • Nameは重複可で、システム的にはユーザー検索時くらいにしか使われていないらしい。

 ちなみに。

GithubのリポジトリURLは、https://github.com/アカウント名/リポジトリ名の形で構成されています。

おしまい。

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

【Swift】オプショナル束縛構文の使い方

1.はじめに


前回オプショナル型の基本についての記事を書きましたが、今回はオプショナル型を実装するうえで、コードを読みやすく、スマートに書くことができる、オプショナル束縛構文について解説しようと思います。
前回の記事はこちらです。
(https://qiita.com/0901_yasyun/items/d64986bdfe917d90382c)

2.オプショナル束縛構文とは


まず以下のif文の条件を見てください。

let year : Int? = Int("2000")   
if let y = year {       //ここのif文の書き方に注目
    print("ハレー彗星は\(y + 61)年に来る")
}else{
    print("エラー")
}

この条件の書き方は、if文とwhile文の条件部にのみ記述することができる、特別な構文です。これをオプショナル構文、またはif-let文と呼びます。
このコードだと、もし変数yearがnilではない値を持っていたら、if文の条件判定は真となります。

3.コードを書く上でのメリット


通常通りであれば、まずオプショナル型の式の値がnilなのかどうかをチェックし、その後にまったくの別で、その値を使用したコードを書いていくことになります。

しかしこの書き方を使うことで、オプショナル型の式の値がnilではなかった場合、その値をチェックしたif文のthen節で、そのまま使うことができます。

4.オプショナル束縛構文の使い方


基本的な構文の使い方は、2で示したコードの通りです。
気を付けることとしては、2のコードにおいてyearの値がnilであれば、if文の条件は偽でelse節が実行されますが、その場合定数yはelse節では使うことができません。

オプショナル束縛構文の条件が成立した場合、条件部で定義された変数などはthen節でしか参照することはできません。これを利用してこの変数などに、元のオプショナル型の変数などと同じ名前を使うことができます。以下のコードを見てください。

let halley : Int? = Int("2000") //定数halleyはInt?型
if var halley = halley {        //変数halleyはInt型であり、then節内でのみ有効
    print("ハレー彗星は\(halley)年に来た")
    halley += 61
    print("次は\(halley)年だと予想される")
}

なお、先ほども述べましたが、オプショナル束縛はif文やwhile文の条件部に記述したときのみ、特別な意味を持つので注意してください。

5.オプショナル束縛と条件式


複数のオプショナル型の値を使って処理を行いたい場合、if-let文をネストすることもできますが、カンマで区切って複数の条件を並べることができます。以下のコードを見てください。

if let sapporo = Int("1972"), let nagano = Int("1998") {
    print("\(nagano - sapporo) years.")
}

オプショナル型の値だけでなく、その他の条件もカンマで区切って記述することができます。

var nagano = 1998
if nagano < 2000, let tokyo = Int("2020"), tokyo > nagano {
    print("\(tokyo - nagano)")     //22と出力される
}

条件を複数書いた場合、このコードだとnaganoが2000未満の場合のみ、カンマの右側のオプショナル束縛の評価に進みます。オプショナル式がnilではなかった場合、定数tokyoにInt型の値が代入され、さらに右の条件式が評価されます。

このように、一番左側から条件の評価が始まり、条件式が偽になるか、オプショナル式がnilだった時点で評価は終了し、ifの条件は偽となります。
また、左側で値が代入された定数や変数を、右側の式で使うこともできます。

おわりに


今回はコードをきれいに書く上で必須ともいえる、オプショナル束縛構文の使い方について解説しました。コードの可読性を高めることは、自分の書いたコードを世の中に発信する際にとても重要なことなので、ぜひこの書き方を利用してみてください。

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

[SwiftUI]R.swiftを使えるようにする

R.swiftではまだSwiftUIに対応していないのですが、本家issueに対策があったので書いてみました。
実際に使ってみたところ、かなり快適です。

import Rswift
import SwiftUI

extension FontResource {
    func font(size: CGFloat) -> Font {
        Font.custom(fontName, size: size)
    }
}

extension ColorResource {
    var color: Color {
        Color(name)
    }
}

extension StringResource {
    var localizedStringKey: LocalizedStringKey {
        LocalizedStringKey(key)
    }

    var text: Text {
        Text(localizedStringKey)
    }
}

extension ImageResource {
    var image: Image {
        Image(name)
    }
}

使う際は以下のようにする。

let font = R.font.myFunnyFontBold.font(size: 35)
let color = R.color.listItemTitle.color
let text = R.string.localizable.hello_world.text
let image = R.image.random_image.image

https://github.com/mac-cain13/R.swift/issues/600

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

FlutterでAndroidビルドのみ失敗するようになる場合の対処法

現象

Androidでデバッグビルドしてデバッグ実行していたのに、突然ビルドが出来なくなることがある。
エラーは私の場合、firebase_analytics:webの依存関係が解決できない的なメッセージ。

FAILURE: Build failed with an exception.

* What went wrong:
Could not determine the dependencies of task ':firebase_analytics:compileDebugAidl'.
> Could not resolve all task dependencies for configuration ':firebase_analytics:debugCompileClasspath'.
   > Could not resolve project :firebase_analytics_web.
     Required by:
         project :firebase_analytics
      > Unable to find a matching configuration of project :firebase_analytics_web:
          - None of the consumable configurations have attributes.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 25s
Finished with error: Gradle task assembleDebug failed with exit code 1

環境など

ツールなど バージョンなど
MacBook Air Early2015 macOS Mojave 10.14.6
Android Studio 3.6.1
Java 1.8.0_131
Flutter 1.12.13+hotfix.9
Dart 2.7.2
Xcode 11.3.1

対処法

https://github.com/FirebaseExtended/flutterfire/issues/1615
こちらにある、flutter pub cache repairを実行して解消しました。

必ず解決できるとは限らないようですが、今のところ私は現象が発生した2回中2回とも、この方法で解消しました。

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

Udemyのswiftコース(英語のやつ)でiOSアプリ開発を学ぶ その4

勉強するコースはこれです:point_down:
https://www.udemy.com/course/ios-13-app-development-bootcamp/

進捗

セクション4の途中まで終了(全部で36セクション)(ひとまずベイビーステップですよね)
サイコロのアプリを作りつつ、その中で変数(variables)と配列(arrays)について学習した。

感想

配列や変数の基礎については知っているつもりだけど、良いおさらいでした:man_tone1:
今まで自分がしてきた学習は日本語での本や授業の内容だったので、英語で学ぶとまた新鮮な部分がありますね。
「英語だとこう言うのか!」とか「英語だったらこういう風に解説すればいいのか!」とか目から鱗な内容もありました。
すでにswiftに詳しい人でも受講すると英語の勉強になっていいかも。
また、英単語や英語での表現に詳しければ英語で内容を調べやすいので、開発中に分からないところでつまづいたときにこの学習経験が役に立ちそうな気がする。

ジョークは、Angela先生から昇給(a raise)と配列(arrays)をかけたジョークがnice jokeだった!
section 4で出てくるので、このコース始めた人はぜひチェックしてほしい!
笑点だったら座布団1枚もらえるやつでした:thumbsup:

今後の予定

できる範囲でコースを進める。
実際のアプリ開発よりも学習することのほうに時間をかけるかも。

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