20201011のSwiftに関する記事は10件です。

SwiftUIでNavigationBarのBackgroundColorなどを変更する方法

やりたいこと

SwiftUIでNavigationBarの背景色を変える
UIAppearanceは使いたくない(強い意志)

結論

  • UINavigationBar.appearance()を使わずにNavigationBarのBackgroundColorを変更
  • UIViewControllerRepresentableでNavigationBarのStyleを変えたいViewを生成
  • NavigationBarを使うViewの.background()に上記のViewを設定

準備

import SwiftUI
import UIKit

struct NavigationConfigurator: UIViewControllerRepresentable {
    var configure: (UINavigationController) -> Void = { _ in }

    func makeUIViewController(context: UIViewControllerRepresentableContext<NavigationConfigurator>) -> UIViewController {
        UIViewController()
    }
    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<NavigationConfigurator>) {
        if let nc = uiViewController.navigationController {
            self.configure(nc)
        }
    }

}

実際の使用例

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("Don't use .appearance()!")
            .navigationBarTitle("Try it!", displayMode: .inline)
            .background(NavigationConfigurator { nc in
                // UIKitでNavigationBarのスタイルを変更するのと同じ方法でOK
                nc.navigationBar.barTintColor = .blue
                nc.navigationBar.titleTextAttributes = [.foregroundColor : UIColor.white]
            })
        }
    }
}

プレビュー

ss20201011

環境

2020年10月11日時点
Xcode 12.0.1
Swift 5.3
これ以外の環境については特に検証してないのでわからないですが、
SwiftUIが動く環境であれば可能だと思い込んでます

NavigationBarの背景色を変えたい

とりあえず、さくっとググってみるといくつかの記事で、コンストラクタとかの初期化処理の際にUINavigationBar.appearance()でしかできないとかかんとか、、、:sob:

たしかに、UINavigationBar.appearance()でも変えれることは変えれるが、
画面ごとに色を変えるのが困難(ってかこれだけなら無理なのでは、、、)

Stack Overflowありがとう!

SwiftUI update navigation bar title color
ちゃんと、いい感じに変更する方法を教えてくださってる方がいらっしゃりました:smile:

この方法のデメリット

プレビューをライブモードにしないとスタイルが反映されないです
ライブモードにすればいいだけなので、どうでもよいですね

ってことで、 UIViewControllerRepresentable を使っていい感じに背景色を変更できました
まだまだ、SwiftUIは成長途中なので、この子を使いこなさないといけなさそう

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

[App groups] UserDefaults データにアクセスできないときに確認したこと

App groupsを使用した、UserDefaultsの使用で、データにアクセスできない問題が発生した。
私の場合は、Build Settings の Signing 設定のCode Signing Entitlement 設定がなかったことが問題だった。
通常、app groupsを有効にしgroupを指定したら自動的にできあがる?とおもうのだが
私の環境では(Xcode 12.0.1) では、自動的に設定されなかったので、他の正常に動いているTargetを参考にこの問題を確認し解決できた。
メモまでに。

Code Signing Entitlementを設定

まず、XXXXXX.entitlementsファイルを作成する。
XXXXXX.entitlements (XXXXXXは任意のファイル名) ファイルを作成。
内容は以下のplistデータで、group.my.appgroup.id の部分は、使用するapp group名を記述する。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.application-groups</key>
    <array>
        <string>group.my.appgroup.id</string>
    </array>
</dict>
</plist>

作成したentitlementsファイルを、プロジェクトに追加する。

次に、設定を行う。

app groupが使用できない問題となっているTARGETを選択し、
Build Settings の Signing の Code Signing Entitlement に、ファイル名を設定する。

image.png

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

【Swift】共用型の列挙型

1.はじめに


前回に列挙型について解説しましたが、今回は事前にお伝えした通り、共用型の列挙型について解説をしていこうと思います。

前回の記事と非常に深い関わりがあるので、まだご覧になっていない方はこちらの記事もご覧ください。
(https://qiita.com/0901_yasyun/items/a87c49438db68241778b)

2.共用型の列挙型の概要について


共用型の列挙型は、実体型を指定しないシンプルな列挙型と、複数の異なるタプルの構造を併せ持つことができる型です。
この型にはいく通りかの異なるデータを、共通の概念でまとめて扱えるようにする狙いがあります。

まず、共用型の列挙型の定義の概要を次に示します。
シンプルな列挙型のケース名後ろにタプルの宣言を付けた形になっています。

enum 型名 {
    case ケース名 タプル型宣言
    case ケース名 タプル型宣言
    ... ...

次に具体的な定義例を示します。
この例では、Webページなどの作成などで使用する色の指定は、文字列の色名、16進数によるカラーコードなどで行われますが、どの形式でも色を指定するという目的は同じなので、これを1つのデータ型で表すことを考えます。

enum WebColor {
    case name(String)       // 色の名前
    case code(String)       // 16進カラーコード
    case white, black, red  // よく使う色
}

この定義を使って、様々なインスタンスを生成できます。

let background = WebColor.name("indigo")   // インディゴブルー
let turquoise: WebColor = .code("#40E0D0") // ターコイズ
let textColor = WebColor = .black          // 黒

タプル型の構造に記述される情報を以下では付加情報、またはペイロードと呼びます。
共用型の定義には、実体型を記述することはできません。
値型共用型の列挙型は別のものとして記述しなくてはなりません。

前回のシンプルな列挙型値型の列挙型では要素同士を比較することができましたが、共用型の列挙型の場合、自分で==演算子を定義するか、プロトコルEqutableを採用しない限り、相互に比較はできません

if turquoise == WebColor.code("#40E0D0")  // エラー。比較できない

もう少し複雑な例を示します。
次の例では、市営地下鉄で販売されている数種類の切符やプリペイドカードのどれもが、自動改札機で処理できるとし、切符やカードの種類を列挙型で定義しました。

enum Ticket {
    case 切符(Int, Bool, 回数券:Bool)  // 普通券:価格、小人、回数券かどうか
    case カード(Int, Bool)             // プリペイドカード:残高、小人
    case 敬老バス                      // 敬老バス
}

caseに記述する括弧の中はタプルで、項目にキーワードを付けることもできます。
このような列挙型に対する処理を書き分けるために、switch文を使います。
caseで定数や変数へ値を代入でき、付加情報を使わない場合には、下の「.カード」のように括弧以降を記述しないこともできます

以下では、caseの後に置かれる構造に関する条件の記述caseパターンと呼びます。
また、where以下に条件を書くことができるのはタプルを扱う場合と同じです。
次の例を確認してください。

switch t {
case let .切符(fare, flag, _):
    print("普通券: \(fare) " + (flag ? "小人" : "大人"))
case .敬老バス:
    print("敬老バス")
case .カード(let r, true) where r < 110:  // 小人の最低運賃
    print("カード: 残高不足")
case .カード(let r, false) where r < 230: // 大人の最低運賃
    print("カード: 残高不足")
case .カード:
    print("カード")

タプルをswitch文で使う場合と同様、付加情報を定数に代入するにはletを()の中に置く方法と外側に置く方法が使えます。

case let .切符(fare, flag, _):     // または
case .切符(let fare, let flag, _):

キーワード付きのタプルの場合、caseパターン内にはラベルを書かなくて大丈夫ですが、書くこともできます。
letとともに使う場合、letを書く場所に注意が必要です。

case let .切符(fare, _, 回数券: flag):     // または
case .切符(let fare, _, 回数券: let flag):

3.if-case文について


与えられたタプルが、複数の条件のどれに一致するかを調べるには、switch文が便利に利用できました。
しかし、特定の1つの条件のみに一致するかどうか調べたいという場合もあります。

例えば、先ほどの例で用いたTicket型の変数pが、プリペイドカードかどうかだけ調べたいとします。しかし、if文で次のようには記述できません。

if p == .カード {              // エラー。この記述はできない
    print("プリペイドカード")
}

switch文なら次のように記述できますが、使わないとわかっているdefault節も書かなければならず、少々面倒です。

switch p {
case .カード: print("プリペイドカード")
default: break
}

そこで、こういった場合でも容易に記述できる構文が用意されています。
上の例は次のように記述できます。この構文をif-case文と呼ぶことがあります。

if case .カード = p {
    print("プリペイドカード")

条件はまずcaseパターンを記述し、「=」と対象となる式を置きます。
caseパターンと式が一致するかどうかは、switch文の場合と同様に処理されます。
switch文の場合と異なり、付加的な条件はwhere節ではなく、カンマ「,」で区切って後ろに続けます。

この記法はif文、while文、およびguard文の条件の書き方で説明したものと同じで、カンマで区切って、一般の式、オプショナル束縛構文などを記述可能です。

if caseパターン = 式, 条件 {.....}

次の例では、変数tの内容が残額が1200円以上のプリペイドカードかどうかを調べます。

if case .カード(let y, _) = t, y >= 1200 { .....

少し複雑な例として、辞書のインスタンスからTicket型の要素を取り出し、その情報がパターンに一致しているかを調べるという条件を記述してみましょう。

let tickets:[String:Ticket] = [ "志倉" : .切符(260, false, 回数券: true), 
               "佐々木" : .切符(220, false, 回数券:false)]
let name = "佐々木"
if let t = tickets[name], case .切符(220, _, _) = t {
    print("220円券")
}

この例の場合、辞書から得た情報がオプショナル型なので、まずオプショナル束縛構文で定数tにTicket型のインスタンスを得ます。
nilの場合はそこでifの条件が不成立となりますが、nilでなければ、次に定数tが220円の切符かどうか調べます。

なお、上のif文は「?」を使うと次のように記述することもできます。

if case .切符(220, _, _)? = tickets[name] {
    print("220円券")
}

4.for-in文でcaseパターンを使う


for-in文でも、caseパターンを利用できます。
これまでに説明したfor-in文と同様に、配列などのSequenceプロトコルに適合した式をinの次に置き、インスタンスを次々に取り出します。

この書き方では、取り出されたインスタンスとcaseの次のパターンが一致した場合にだけ、コードブロックを実行します。
実行のための条件をさらに記述する必要があれば、where節を追加することもできます。

for case パターン in 式 where 式 {
    文... ...
}

たとえば、Ticket型の要素を持つ配列passesから次々にインスタンスを取り出して、220円より高い切符の情報を表示するには次のようにします。

for case let .切符(fare. child, coupon) in passes where fare > 220 {
    var k = coupon ? "回数券" : "普通券"
    if child { k += "(小人)" }
    print(k, "\(fare)円")
}

この文は次のように記述するのと同じです。

for t in passes {
    switch t {
    case let .切符(fare, child, coupon):
        if fare > 220 {
            var k = coupon ? "回数券" : "普通券"
            if child { k += "(小人)" }
            print(k, "\(fare)円")
        }
    default: break
    }
}

この構文でオプショナル型に当てはまる「?」を利用すると、オプショナル型が含まれるデータ列をうまく扱うことができます。
また、letではなく、varを使うこともできます。

5.再帰的な列挙型


共用型の列挙型で、タプルの内部に自分自身を要素として指定したい場合がありえます。
例えば次のような場合を考えてみます。

enum メッセージ {
    case 文書(String, String)     // 差出人、文書
    case データ(String, [Int8])   // 差出人、データ列
    case 転送(String, メッセージ)  // 差出人、メッセージ
}

このデータの宣言は間違っていないようにも見えますが、Swiftでは扱うことができません。

Swiftでは列挙型値型のデータですので、直感的に説明するならば、負荷情報は列挙型のデータを表すメモリ位置に直接並べて格納されています。
上記のように自分自身を含む、つまり再帰的なデータ構造はデータを格納するために必要なメモリ容量がコンパイル時に決定することができません

これに対して参照型のデータは、メモリ上のどこかに存在する実体を間接的に参照するためのための情報なので、メモリの問題はありません。

しかし、再帰的な列挙型が使えると便利な場面も多いので、Swiftでは列挙型自分自身を付加情報に含む場合、該当するcaseの前にindirectというキーワードを記述します
これを間接指定と呼びます。indirect、つまり間接であるとは、その部分だけポインタのように間接参照を行うことを意味します。

今の話を以下の例に示します。
CustomStringConvertibleプロトコルを採用し、printで表示できるようにしています。

enum メッセージ : CustomStringConvertible {
    case 文書(String, String)              // 差出人、文書
    case データ(String, [Int8])            // 差出人、データ列
    indirect case 転送(String, メッセージ)  // 差出人、メッセージ

    ver description: String {
        switch self {
        case let .文書(from, str): return from + "(" + str + ")"
        case let .データ(from, _): return from + "[データ]"
        case let .転送(from, msg): return from + "←\(msg)"
        }
    }
}

実行例を示します。

let m1 = メッセージ.文書("伊藤", "休みます")
let m2 = メッセージ.転送("白石", m1)
let m3 = メッセージ.転送("山田", m2)
print(m3)   // "山田←白石←伊藤(休みます)"を出力

再帰的なcaseパターンがいくつかある場合には、enumの前にindirectを置いて全体に間接指定を行うことができます。

6.おわりに


今回は共用型の列挙型についての記事を書きましたが、プロトコルやオプショナル型の扱いについては、記事が少し長くなってしまうので今回は解説していません。
来週以降はクラスなどについての解説をしていこうと思います。
ここまで読んでくれた方、ありがとうございました。

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

VisionFrameworkとCoreMLで画像解析をするときの流れを図にしてみた

VisionFrameworkとCoreMLを使った画像の解析は、そんなに手順は多くないので、過去のコードを見ればなんとなく思い出せたりしますが、手元に図があると良いかもということで描いてみました。

画像解析と言ってもいろんなパターンがありますが、今回はAppleの公式サンプルであるRecognizing Objects in Live Captureの流れを起こしたものになります。

あと、こちらの記事はVisionFrameworkとCoreMLを使った画像の解析のコードを端的にまとめられていて参照するのにちょうどよいです。
VisionFrameworkとCoreMLを使用した画像解析


image.png

iOS開発について発信しています。
NoteではiOS開発について定期的に発信していますので、フォローしていただけますと幸いです。
https://note.com/tokyoyoshida

Twitterでは簡単なtipsを発信しています。
https://twitter.com/jugemjugemjugem

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

FirebaseMLでテキスト認識を行う![SwiftUI]

はじめに

こんにちは、iOSアプリを最近開発している@RyosukeKamimuraと申します!
今回は、SwiftUIでテキスト認識を行っていきます!

忙しい方に向けて結論から先に述べます!!

結論

結論は、3点あります。
1. FirebaseMLは日本語に認識する性能が、良好
2. 手書き文字の認識は少し難がある。
3. 1000リクエストまでは無料!

以下、チュートリアル的に行います!

Firebaseプロジェクトの設定

①Firebaseと連携

Firebaseのセットアップから参照をお願いします。
Firebaseへの登録の共通部分であるためここでは省かせていただきます。

②FirebaseコンソールでクラウドベースのAPIを有効にしてください

Cloud APIの使用状況を表示の隣のボタンをOnにしてください。

drawing

③Firebase公式ドキュメントを見ながら、テキスト認識を行う
MLKitを使用して画像内のテキストを認識するiOS

SwiftUIで書きたかったので、Coordinatorをうまく噛み合わせてUILableをラップするように書きました!
(もっとこう書いたほうがいいよというご指摘コメントでお待ちしております!)

今回の記事では、実際にテキスト認識を行う関数に注目して見てもらいたいです。
(Firebaseの公式ドキュメントでは少ないから)

  1. 関数の作成
RecognizedText.swift
func recognizedTextFunc() -> String {
  // この関数の中を記述していく
}

2.Visionインスタンスの初期化

RecogniedText.swift
func recognizedTextFunc() -> String {

  //Visionインスタンスの初期化
  let vision = Vision.vision()
  let options = VisionCloudTextRecognizer(options: options)

  // 言語の優先順位を追加
  options.languageHints = ["en", "ja"]
  let textRecognizer = vision.cloud
  vision.cloudTextRecognizer(options: options)

  //"logo"画像を入れる
  let visionImage = VisionImage(image: UIImage(named: "logo")!)
}

3.resultを取得するクロージャを作成

RecognizedText.swift
textRecognizer.process(visionImage) { result, error in
    guard error == nil, let result = result else {
        return
    }
    //let resultText = result.text
    for block in result.blocks {
        //let blockText = block.text
        //let blockConfidence = block.confidence
        //let blockLanguages = block.recognizedLanguages
        //let blockCornerPoints = block.cornerPoints
        //let blockFrame = block.frame
        for line in block.lines {
            //let lineText = line.text
            //let lineConfidence = line.confidence
            //let lineLanguages = line.recognizedLanguages
            //let lineCornerPoints = line.cornerPoints
            //let lineFrame = line.frame
            for element in line.elements {
                let elementText = element.text
                //let elementConfidence = element.confidence
                //let elementLanguages = element.recognizedLanguages
                //let elementCornerPoints = element.cornerPoints
                //let elementFrame = element.frame   
                //self.recognizedText.wrappedValue = elementText
                //self.text = elementText
                //出来たテキストをリストに格納する
                getTexts.append(elementText)
            }
        }
    }
    //出力できたテキストを1つの文字列にする
    self.recognizedText = getTexts.joined(separator: "")
}       

これで関数が出来て文字を取り出すことが出来ました!
UIを作って実行してみましよう!

このようにうまく認識できました!

概要と結果の詳細をSeverless LT #3で発表させていただいたので、詳しく見たい!という方はどうぞ!
スライド : https://slidelive.jp/slide/17468048a2f1b6ed57c?event_id=17394b601591bcf3f06

Githubに自分のコードも載っているのでGithub/RyosukeKamimuraもどうぞ!

最後まで読んでいただき、ありがとうございました!

参考
Firebase公式チュートリアル
Swift実践入門

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

[個人アプリ開発記]サイコロを操作し,底面を記録するARアプリ

はじめに

こんにちは。少し前になりますが,3つ目となるアプリをリリースしましたので,その機能や流れについて書こうと思います。なお一つ目,2つ目についてはそれぞれこちらこちらの記事を読んでいただければ。SceneKitでの開発になりますので,UnityとSceneKitどちらを採用しようか迷っている方の参考にももしかしたらなるかもしれません。私は最初はUnityで製作していましたが,3Dプログラミングの使い勝手やコマンドの多さはUnityの方が優れていると思ったものの,プラグインが煩わしくなりSceneKitに変更しました。SceneKitはネット情報に溢れる情報こそ少ないですが,慣れてしまえば思い通りに操作できるかと思います。

アプリの紹介

機能としてはものすごく単純で,前後左右にサイコロを動かす(回す)と動かす前にサイコロがあった底面のマス目が記録されます。
 

苦労したところ

超単純な機能ですが,実現するためのアイディアを思いついたり,それらを実現するのはとても苦労しました。特にSceneKitは文献どころか日本語のサイトがほとんどありません。以下ぶつかった問題を挙げていきます。(みなさんであればどのように解決しますか?)

回転軸と中心を任意に設定するメソッドがない

動画のような動きはUnityであれば専用のメソッドが用意されているのですが,SceneKitにはありません。(自身の軸(x,y,z)を中心として回転するメソッドはもちろんあります。)

配置したサイコロにとっての上下左右と,ユーザーが画面上で操作する上下左右が異なる

まず,3Dプログラミングには主に空間の絶対的な座標となるワールド座標と,配置されたオブジェクトがそれぞれ持つローカル座標,ローカル座標のうち,オブジェクトがカメラである時のカメラ座標,そしてデバイス上のスクリーン座標があります。参考ARKitではワールド座標がAR画面が起動した時のカメラ座標となります。

(右下の赤がワールド座標)
このアプリではスクリーンを画面をタップした時にその場所から平面が検出されれば画面上にサイコロが配置されます。この時,サイコロの座標の向き(x軸x,y軸,z軸)はワールド座標と同じになります。しかし,カメラ座標は起動後動かさないわけにはいかないので(平面を検出するためにデバイスを動かさなければならないため)ワールド座標のx,y,z軸からずれています。そのため,サイコロを右に動かそうと思い,右ボタンを押しても前に動いている,などといったことが起こってしまいます。

「右」と「前->右->後ろ」では底面が異なる

これは当たり前といえば当たり前なのですが,同じ場所に辿りつくための行き方は無限にあり,ボタンを押した順序で一意に定まります。ボタンを押した回数で定まるわけではないです。そのため,ボタンを押すごとにサイコロの底面を取得しなければなりません。

2,3,6には底面の向きがあるため底面以外の面も取得しなければならない

この問題は上の問題の解決方法が思いつき,実装した後に発生した問題です。そのためこの問題を説明するには上の問題の解決方法について触れなければならないので,ここでは割愛します。

解決するためのアイディアや方法

「回転軸と中心を任意に設定するメソッドがない」の解決方法

以下のやり方で任意の回転軸と中心でオブジェクトを回転させる(サイコロが転がるアニメーションを作成する)ことができました。
その時のノートはこんな感じ。

1.boxを追加する

右上の➕からboxを選択し、ドラッグ&ドロップします。その後、position(座標)を(0,0.5,0)、size(大きさ)を(1,1,1),Eular(回転)を(0,0,0)に設定します。
スクリーンショット 2020-08-31 16.14.23.jpg

2.形を持たないnodeを追加する

右下の➕ボタンを押し、nodeを追加します。<untitled> が追加されればOKです。(以下これをemptyNodeとします。)
右側に回転する動きを再現したいので、中心となる立方体の辺の中点にemptyNode(0.5,0,0.5)に移動します。
スクリーンショット 2020-08-31 16.15.06.jpg

3.emptyNodeの階層下にboxを配置する

階層関係にあるnodeにおいて、子nodeは親nodeの動きの影響を受けます。boxをemptyNodeの下にドラッグ&ドロップし、子nodeとして設定します。
スクリーンショット 2020-08-31 16.15.44.jpg

4.emptyNodeを回転させる

emotyNodeのz軸を-90度回転させることで、回転を表現することができます。
スクリーンショット 2020-08-31 16.16.04.jpg スクリーンショット 2020-08-31 16.16.22.jpg

「配置したサイコロにとっての上下左右と,ユーザーが画面上で操作する上下左右が異なる」の解決方法

この問題はオブジェクトをのy軸をカメラ座標と同じにすることで解決しました。

「右」と「前->右->後ろ」では底面が異なる」の解決方法

この問題はサイコロを立方体として考えるのではなく,正方形が6枚組み合わさっているものとして捉えることで解決しました。もともと立方体(SCNBox)で回転を考えていたのですが,底面を判定できないことに気づき,正方形(SCNPlane)を組み合わせ,y座標(ワールド座標)が最も小さいものを記録すれば良いということに気づきました。ボタンを押すたびにy座標が最も小さいSCNPlaneを判定するので,それまでに押されたボタンに依存することなく記録することができます。

「2,3,6には底面の向きがあるため底面以外の面も取得しなければならない」の解決方法

この問題は上述し立方体を正方形が6枚組み合わせたものとして考えることに気づいた後にぶつかった問題です。底面の向きを決めるためには側面にある4つ面のうちどれかを取得しなければなりません。つまり,正方形のx座標をワールド座標で置き換えた時に最も大きくなるものを取得しなければなりません。さらに,サイコロのy座標は配置したサイコロにとっての上下左右と,ユーザーが画面上で操作する上下左右が異なる」問題を解決するために回転させているので,x座標はワールド座標への正射影を考える必要があります。
その時のノートはこんな感じ。

技術を得るために

数少ないネットの情報を漁りまくる

SceneKitに関する情報はめちゃくちゃ少ないです。SceneKitの基本的なことはこのサイトで,3dプログラミングについてはUnityで学びました。ARKitについてはBOOTHで販売されているこちらのテキストがすごく丁寧で分かり易かったです。

Qiitaでのアウトプット

SceneKitに関する情報を増やしたい!といった思いと備忘録がわりにQiitaにアウトプットしました。

最後に

SceneKitというわりとマイナーなフレームワークでの開発だったため,とにかくインプットよりもアウトプット中心の学習だったように思います。その中で三角比や場合わけ(最終的には256通りになった)など高校数学で学習したことが活かされる場面多々あり,嬉しく思いました。3Dプログラミングはとても難しいですが,その分思い通りに動いた時の達成感は筆舌に尽くしがたいものです。このアプリを通して得られた知見がこの先のアプリの開発で生かされたり,アウトプットした記事が誰かの役に立てば幸いです。

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

ARKitやSceneKitの図形をMetalシェーダーで変形させる方法

ARKitやSceneKitの図形を自由に変形させる方法を紹介します。

なお、この手法を使って最終的にはARで空中に水を浮かべるといった処理も作りましたので、こちらの記事もご覧下さい。

ARKit + Metalで空中に水を浮かべる方法

やり方

今回はこんな感じの球体を変形させていきます。
※下の画像は球体にライトを当てていないのでのっぺりしていますが球体です。

SceneKitのマテリアルをMetalシェーダーで描画させる方法

球体のノードを作り、SCNProgramを使ってMetalのシェーダーの関数を設定すると、シェーダーに描画させることができます。

GameViewController.swift
// 球体をノードに追加する
let sphereNode = SCNNode()
sphereNode.geometry = SCNSphere(radius: 2)
sphereNode.position.y += Float(0.05)
sphereNode.name = "my_node"

// Metalのシェーダーを指定する
let program = SCNProgram()
program.vertexFunctionName = "vertexShader"
program.fragmentFunctionName = "fragmentShader"
sphereNode.geometry?.firstMaterial?.program = program
// 経過時間の情報をシェーダーに渡す
let time = Float(Date().timeIntervalSince(startDate))
globalData.time = time
let uniformsData = Data(bytes: &globalData, count: MemoryLayout<GlobalData2>.size)
sphereNode.geometry?.firstMaterial?.setValue(uniformsData, forKey: "globalData")

このあたりを詳しく知りたい方はこちらを読むと良いと思います。

Metal入門

Vertexシェーダーで変形してみる

まずは、簡単な変形をしてみます。
x座標にy座標分を足してみます。

Shader.metal
vertex ColorInOut vertexShader(VertexInput2          in       [[ stage_in ]],
                               constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                               constant NodeBuffer& scn_node [[ buffer(1) ]],
                               device GlobalData2 &globalData [[buffer(2)]])
{
    // 出力用変数
    ColorInOut out;
    // 頂点の座標情報
    float3 pos = in.position;
    // 頂点のxにyを足している。ここが変形処理の実体
    pos.x += pos.y;
    // SceneKit用のMVP変換をする
    float4 transformed = scn_node.modelViewProjectionTransform * float4(pos, 1.0);
    // 変換後の座標を出力変数に入れる
    out.position = transformed;
    // テクスチャー座標は何もせずに出力変数に入れる
    out.texCoords = in.texCoords;

    // 出力する
    return out;
}

このようになります。

y座標の値が大きくなるほど(y座標は下から上に大きくなる)、x座標が大きくなっている(左から右に大きくなる)のがわかります。

なお、シェーダーの宣言の方法や、modelViewProjectionTransformについては、先程紹介したMetal入門に詳しくありますので、そちらをご覧ください。

cosを与えて変形させてみる。

さきほど、pos.x += pos.y;としていたところを、次のように変更してみます。

Shader.metal
    pos.x += cos(pos.y);

このようになります。

面白いですね。cosを与えると-1〜1の範囲で波打つようになります。

こうした数式の考え方はこちらの本にわかりやすく書いてありました。

Unityでわかる!ゲーム数学

経過時間を与えてアニメーションさせてみる

さきほど、pos.x += cos(pos.y);としていたところを、次のように変更してみます。

Shader.metal
    pos.x += cos(pos.y + globalData.time);

こんな感じで面白い動きをします。
qiita用動画.gif

最初に紹介した空中に水を浮かべる方法では、x, y, zのそれぞれについてこのような処理をすることで水のような感じを出しています。

NoteではiOS開発について定期的に発信していますので、フォローしていただけますと幸いです。
https://note.com/tokyoyoshida

Twitterでは簡単なtipsを発信しています。
https://twitter.com/jugemjugemjugem

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

if文について理解しよう!

今回は、ifについて学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承ください

if文とは?

ifを一言でいうと、条件の成否に応じて実行する構文のことです。
基本的な書き方は以下の通りです

qiita.rbvar
if 条件式 { 条件式がtrueの場合に実行される文 }

次は簡単な例を見ていきましょう。

qiita.rbvar
1.let value = 5
2.if value <= 10 {

print("valueは10以下です")

}

実行結果valueは10以下です

このコードを日本語に直してみると

qiita.rbvar
もし[valueの値が10以下だったら{}の中のメソッド実行してくださいね]という意味になります

非常に簡単で、わかりやすいですよね!
ちなみに、if文の条件式はBool型を返す必要があります!

では、さらにif文について深堀していきましょう!

else節とは?

else節は、条件が成立しなかった場合に実行する文のことを言います。
基本的な構文を見ていきましょう!

qiita.rbvar
if 条件式 { 条件式がtrueの場合に実行される文

 }else{
条件式がfalseの場合に実行される文
}

次は簡単な例を見ていきましょう!

qiita.rbvar
1.let value = 26
2.if value <= 10 {

print("valueは10以下です")

}else{

print("valueは10より大きいです")

}
//実行結果:valueは10より大きいです

上記と同様に、このコードを日本語に直してみると

qiita.rbvar
もし[valueの値が10以下だったら{}の中のメソッド実行してくださいね
そうでないならelse{}の中のメソッドを実行してくださいという意味になります

また、else節にはif文をつなげて書くことができます。

qiita.rbvar
if 条件式1 {
 条件式1がtrueの場合に実行される文

 }else if 条件式2{
条件式1がfalseかつ条件式2がtrueの場合に実行される

}else{
条件式1条件式2の両方がfalseの場合に実行される
}

if-let文とは?

if-let文は、optional型の値の有無に応じて、分岐を行い値が存在する場合は、値の取り出しも同時に行える文のことです。

Optional型とは? Optional型を深堀したい人はこちら!
Optional(Wrapped)型を理解しよう!

では、基本的な書き方を見ていきましょう!

qiita.rbvar
if let 定数名 = Optional型の値{
値が存在する場合に実行される文

}else{
値が存在しない場合に実行される文

}

次は、簡単な例を見ていきましょう。

qiita.rbvar
let optionalA =Optional("G")

if let X = optionalA {

print("値は\(X)")

}else{
print("値が存在しません")

実行結果:値はGです

最後に

今回は、if文についてアウトプットしていきました。
プログラミング初心者でも、理解しやすく、アプリ開発でも多用される構文なので、しっかり基礎をマスターしていきたいです。

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

UITextContentType の rawValue 一覧。

UITextContentType.init(rawValue: ) を使いたかったんですけど、rawValue が若干違っていたので自分用にリスト化しておきます。

UITextContentType rawValue
.addressCity address-level2
.addressCityAndState address-level1+2
.addressState address-level1
.countryName country-name
.creditCardNumber cc-number
.emailAddress email
.familyName family-name
.fullStreetAddress street-address
.givenName given-name
.jobTitle organization-title
.location location
.middleName additional-name
.name name
.namePrefix honorifix-prefix
.nameSuffix honorifix-suffix
.newPassword new-password
.nickname nickname
.oneTimeCode one-time-code
.organizationName organization
.password password
.postalCode postal-code
.streetAddressLine1 address-line1
.streetAddressLine2 address-line2
.sublocality address-level3
.telephoneNumber tel
.URL url
.username username

たぶん二度と使う気はしないですが、ざっと検索した限りだと見つけられなかったのでまとめておきます。

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

【入門】iOS アプリ開発 #11【ゲームのアレンジ(加速度センサーを使用した操作など)】

はじめに

公開されている仕様書をもとに作成したパックマンに、アレンジを加えてみたい。パックマンををスワイプだけでなくタッチや加速度センサーで操作できるようにし、オリジナルの迷路も追加してみる。ソースコードは GitHub に公開しているので参照してほしい。

Image2.png

コンフィグレーション・メニュー

操作や迷路を切り替えるためのコンフィグレーション・メニューを追加する。
追加する迷路は開発途中の下記、仕様書を参考にした。

Image3.png

ソースコード

CgSceneCreditMode にメニューを追加する。クレジットを入れて暫くすると case 1 でメニューの入り口が表示される。そこをタッチするとメニューに入れるようにした。

CgSceneFrameクラスの構造は、以前の【入門】iOS アプリ開発 #5【シーケンスの設計】を参照してほしい。

/// Credit Mode
class CgSceneCreditMode : CgSceneFrame {

    enum EnEvent: Int {
        case EnterConfig = 3
        case Operation = 5
        case ExtraMode = 6
        case DebugMode = 7
        case Language = 8
        case ResetSetting = 9
        case ExitConfig = 10
        case None
    }

    private let table_enterConfiguration: [(Int,Int,Int,Int,EnEvent)] = [
        (  26, 34, 28, 36, .EnterConfig)
    ]

    private let table_setConfiguration: [(Int,Int,Int,Int,EnEvent)] = [
        (  26, 34, 28, 36, .ExitConfig),
        (   4, 25, 28, 27, .Operation),
        (   4, 20, 28, 22, .ExtraMode),
        (   4, 15, 28, 17, .DebugMode),
        (   4, 10, 28, 12, .Language),
        (   4,  5, 28,  7, .ResetSetting)
    ]

    private var table_search: [(column0:Int,row0:Int,column1:Int,row1:Int,event:EnEvent)] = []
    private var configMode: Bool = false

    /// Event handler
    /// - Parameters:
    ///   - sender: Message sender
    ///   - id: Message ID
    ///   - values: Parameters of message
    override func handleEvent(sender: CbObject, message: EnMessage, parameter values: [Int]) {
        if message == .Touch {
            let position = CgPosition.init(x: CGFloat(values[0]), y: CGFloat(values[1]))
            let event = search(column: position.column, row: position.row)
            if event == .None {
                if !configMode {
                    stopSequence()
                }
            } else {
                goToNextSequence(event.rawValue)
            }
        }
    }

    /// 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:
                configMode = false
                table_search = []
                clear()
                printFrame()
                printPlayerScore()
                printHighScore()
                printCredit()
                printRounds()
                background.print(0, color: .Orange, column: 6, row: 19, string: "PUSH START BUTTON")
                background.print(0, color: .Cyan, column: 8, row: 15, string: "1 PLAYER ONLY")

                if context.language == .English {
                    background.print(0, color: .Pink, column: 1, row: 11, string: "BONUS PAC-MAN FOR 20000 ]^_")
                    background.print(0, color: .Purple, column:4, row: 4, string: "@ 2020 HIWAY.KIKUTADA")
                } else {
                    background.print(0, color: .Pink, column: 1, row: 11, string: "BONUS PAC-MAN FOR 10000 ]^_")
                    background.print(0, color: .Purple, column: 7, row: 7, string: "@ #$%&'()*  2020")   // NAMACO
                }
                goToNextSequence(after: 60*16*2)

            case 1:
                table_search = table_enterConfiguration
                background.print(0, color: .White, column:  27, row: 35, string: "$")
                goToNextSequence()

            case 2:
                // wait for event
                break;

            case 3:
                configMode = true
                table_search = table_setConfiguration
                background.fill(0, texture: 0)
                printFrame()
                printPlayerScore()
                printHighScore()
                printCredit()
                background.print(0, color: .White, column: 27, row: 35, string: "#")
                background.print(0, color: .Red,   column:  8, row: 31, string: "CONFIGURATION")
                background.print(0, color: .White, column:  4, row: 26, string: "OPERATION")
                background.print(0, color: .White, column:  4, row: 21, string: "EXTRA MODE")
                background.print(0, color: .White, column:  4, row: 16, string: "DEBUG MODE")
                background.print(0, color: .White, column:  4, row: 11, string: "LANGUAGE")
                background.print(0, color: .White, column:  4, row:  6, string: "SETTING")
                print_operation(color: .Pink)
                print_extraMode(color: .Pink)
                print_debugMode(color: .Pink)
                print_language(color: .Pink)
                print_resetSetting(color: .Pink)
                goToNextSequence()

            case 4:
                // wait for event
                break;

            case 5: // operation
                context.operationMode = context.operationMode.getNext()
                print_operation(color: .Yellow)
                goToNextSequence(4)

            case 6: // ExtraMode/
                context.extraMode = context.extraMode.getNext()
                print_extraMode(color: .Yellow)
                goToNextSequence(4)

            case 7: // DebugMode
                context.debugMode = context.debugMode.getNext()
                print_debugMode(color: .Yellow)
                goToNextSequence(4)

            case 8: // Language
                context.language = context.language.getNext()
                print_language(color: .Yellow)
                goToNextSequence(4)

            case 9: // ResetSetting
                context.resetSetting = context.resetSetting.getNext()
                print_resetSetting(color: .Yellow)
                goToNextSequence(4)

            case 10:
                context.saveConfiguration()
                goToNextSequence(0)

            default:
                clear()
                // Stop and exit running sequence.
                return false
        }

        return true
    }

    func clear() {
        background.fill(0, texture: 0)
    }

    func print_operation(color: CgCustomBackgroundManager.EnBgColor) {
        let str = context.operationMode.getString()
        background.print(0, color: color, column: 16, row: 26, string: str)
    }

    func print_extraMode(color: CgCustomBackgroundManager.EnBgColor) {
        let str = context.extraMode.getString()
        background.print(0, color: color, column: 16, row: 21, string: str)
    }

    func print_debugMode(color: CgCustomBackgroundManager.EnBgColor) {
        let str = context.debugMode.getString()
        background.print(0, color: color, column: 16, row: 16, string: str)
    }

    func print_language(color: CgCustomBackgroundManager.EnBgColor) {
        let str = context.language.getString()
        background.print(0, color: color, column: 16, row: 11, string: str)
    }

    func print_resetSetting(color: CgCustomBackgroundManager.EnBgColor) {
        let str = context.resetSetting.getString()
        background.print(0, color: color, column: 16, row: 6, string: str)
    }

    func search(column: Int, row: Int) -> EnEvent {
        var event: EnEvent = .None
        for i in 0 ..< table_search.count {
            let t = table_search[i]
            if (t.column0 <= column && t.row0 <= row) && (t.column1 >= column && t.row1 >= row) {
                event = t.event
                break
            }
        }
        return event
    }

}

handelEventメソッドで Touch のイベントを受信したら、search関数で押された範囲をチェックして該当していれば、そのシーケンスの case を実行する。

設定値は CgContextクラスで定義しておく。

class CgContext {

    enum EnOperationMode: Int {
        case Swipe = 0, Touch, Accel

        func getString() -> String {
            switch self {
                case .Swipe: return "(SWIPE)"
                case .Touch: return "(TOUCH)"
                case .Accel: return "(ACCEL)"
            }
        }

        func getNext() -> EnOperationMode {
            switch self {
                case .Swipe: return .Touch
                case .Touch: return .Accel
                case .Accel: return .Swipe
            }
        }
    }

    enum EnLanguage: Int {
        case English = 0, Japanese

        func getString() -> String {
            switch self {
                case .English:  return "(ENGLISH) "
                case .Japanese: return "(JAPANESE)"
            }
        }

        func getNext() -> EnLanguage {
            switch self {
                case .English: return .Japanese
                case .Japanese: return .English
            }
        }
    }

    enum EnOnOff: Int {
        case Off = 0, On

        func getString() -> String {
            switch self {
                case .On:  return "(ON) "
                case .Off: return "(OFF)"
            }
        }

        func getNext() -> EnOnOff {
            switch self {
                case .On: return .Off
                case .Off: return .On
            }
        }
    }

    enum EnSetting: Int {
        case Clear = 0, Keep

        func getString() -> String {
            switch self {
                case .Clear: return "(CLEAR)"
                case .Keep:  return "(KEEP) "
            }
        }

        func getNext() -> EnSetting {
            switch self {
                case .Clear: return .Keep
                case .Keep: return .Clear
            }
        }
    }

    var operationMode: EnOperationMode = .Swipe
    var extraMode: EnOnOff = .Off
    var debugMode: EnOnOff = .Off
    var resetSetting: EnSetting = .Clear
    var language: EnLanguage = .Japanese

    // 以下、省略

加速度センサーによる操作

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()

    // MotionManager for accel
    private var motionManager: CMMotionManager!

    override func didMove(to view: SKView) {

        //  Create and start game sequence.
        gameMain  = CgGameMain(skscene: self)
        gameMain.startSequence()

        // Create motion manager
        motionManager = CMMotionManager()
        motionManager.accelerometerUpdateInterval = 0.05

        motionManager.startAccelerometerUpdates(
            to: OperationQueue.current!, withHandler: {
                (accelData: CMAccelerometerData?, errorOC: Error?) in self.sendAccelEvent(acceleration: accelData!.acceleration)
            }
        )

    }

    func sendAccelEvent(acceleration: CMAcceleration){
        let x_diff: Int = Int(acceleration.x * 100)
        let y_diff: Int = Int(acceleration.y * 100)

        if abs(x_diff) > abs(y_diff) {
            gameMain.sendEvent(message: .Accel, parameter: [Int(x_diff > 0 ? EnDirection.Right.rawValue : EnDirection.Left.rawValue)])
        } else {
            gameMain.sendEvent(message: .Accel, parameter: [Int(y_diff > 0 ? EnDirection.Up.rawValue : EnDirection.Down.rawValue)])
        }
    }

GameSceneクラスで CMMotionManager を生成し、値を取得する周期を accelerometerUpdateIntervalメンバに設定、
コールバック・メソッドに sendAccelEvent を設定しておく。

sendAccelEvent メソッドは 0.05s周期で呼ばれ、この中で加速度センサーの傾きから方向を算出し、スワイプ操作と同様に gameMain.sendEvent でオブジェクトにイベント送信する。

class CgPlayer : CgActor {

    /// Event handler
    /// - Parameters:
    ///   - sender: Message sender
    ///   - id: Message ID
    ///   - values: Parameters of message
    override func handleEvent(sender: CbObject, message: EnMessage, parameter values: [Int]) {
        guard !deligateActor.isDemoMode() else { return }
        switch message {
            case .Accel where deligateActor.isOperationMode(mode: CgContext.EnOperationMode.Accel): fallthrough
            case .Swipe where deligateActor.isOperationMode(mode: CgContext.EnOperationMode.Swipe):
                if let direction = EnDirection(rawValue: values[0]) {
                    targetDirecition = direction
                }

            case .Touch where deligateActor.isOperationMode(mode: CgContext.EnOperationMode.Touch):
                setTargetPosition(x: values[0], y: values[1])
                targetDirecition = decideDirectionByTarget(forcedDirectionChange: true)
                position.amountMoved = 0

            default:
                break
        }
    }

CgPlayerクラスの handleEvent の中で、コンフィグレーション・メニューで設定した操作が有効ならば(deligateActor.isOperationMode(mode: CgContext.EnOperationMode.Accel))、加速度センサーのイベントを受け付けるようにする。

こちらは以前の、【入門】iOS アプリ開発 #6【キャラクタの操作】を参照してほしい。

オリジナル迷路の追加

CgSceneMazeクラスの getMazeSourceメソッドを、メニューの値で迷路データを切り替えるようにする。この迷路データの作りは古典的だが意外と簡単。

    func getMazeSource() -> [String] {

        let mazeSource: [String] = [
            "aggggggggggggjiggggggggggggb",
            "e111111111111EF111111111111f",
            "e1AGGB1AGGGB1EF1AGGGB1AGGB1f",
            "e3E  F1E   F1EF1E   F1E  F3f",
            "e1CHHD1CHHHD1CD1CHHHD1CHHD1f",
            "e11111111111111111111111111f",
            "e1AGGB1AB1AGGGGGGB1AB1AGGB1f",
            "e1CHHD1EF1CHHJIHHD1EF1CHHD1f",
            "e111111EF1111EF1111EF111111f",
            "chhhhB1EKGGB1EF1AGGLF1Ahhhhd",
            "     e1EIHHD2CD2CHHJF1f     ",
            "     e1EF          EF1f     ",
            "     e1EF QhUWWVhR EF1f     ",
            "gggggD1CD f      e CD1Cggggg",
            "____  1   f      e   1  ____" ,
            "hhhhhB1AB f      e AB1Ahhhhh",
            "     e1EF SggggggT EF1f     ",
            "     e1EF          EF1f     ",
            "     e1EF AGGGGGGB EF1f     ",
            "aggggD1CD1CHHJIHHD1CD1Cggggb",
            "e111111111111EF111111111111f",
            "e1AGGB1AGGGB1EF1AGGGB1AGGB1f",
            "e1CHJF1CHHHD2CD2CHHHD1EIHD1f",
            "e311EF1111111  1111111EF113f",
            "kGB1EF1AB1AGGGGGGB1AB1EF1AGl",
            "YHD1CD1EF1CHHJIHHD1EF1CD1CHZ",
            "e111111EF1111EF1111EF111111f",
            "e1AGGGGLKGGB1EF1AGGLKGGGGB1f",
            "e1CHHHHHHHHD1CD1CHHHHHHHHD1f",
            "e11111111111111111111111111f",
            "chhhhhhhhhhhhhhhhhhhhhhhhhhd"
        ]

        let mazeSourceExtra1: [String] = [
                "aggggjiggggggjiggggggjiggggb",
                "e1111EF111111EF111111EF1111f",
                "e1AB1EF1AGGB1CD1AGGB1EF1AB1f",
                "e3EF1EF1E  F1111E  F1EF1EF3f",
                "e1CD1CD1CHHD1AB1CHHD1CD1CD1f",
                "e111111111111EF111111111111f",
                "kGGB1AGGB1AGGLKGGB1AGGB1AGGl",
                "YHJF1EIHD1CHHJIHHD1CHJF1EIHZ",
                "e1EF1EF111111EF111111EF1EF1f",
                "e1EF1EKGGGGB1EF1AGGGGLF1EF1f",
                "e1CD1CHHHHHD2CD2CHHHHHD1CD1f",
                "e11111111          11111111f",
                "e1AB1AGGB QhUWWVhR AGGB1AB1f",
                "e1EF1CHHD f      e CHHD1EF1f",
                "e1EF11111 f      e 11111EF1f",
                "kGLF1AGGB f      e AGGB1EKGl",
                "YHHD1EIHD SggggggT CHJF1CHHZ",
                "e1111EF11          11EF1111f",
                "e1AGGLF1AGGGGGGGGGGB1EKGGB1f",
                "e1CHHJF1CHHHHJIHHHHD1EIHHD1f",
                "e1111EF111111EF111111EF1111f",
                "kGGB1EKGGGGB1EF1AGGGGLF1AGGl",
                "YHHD1CHHHHHD2CD2CHHHHHD1CHHZ",
                "e111111111111  111111111111f",
                "e1AGGGB1AGGGGGGGGGGB1AGGGB1f",
                "e1CHHJF1CHHHHJIHHHHD1EIHHD1f",
                "e3111EF111111EF111111EF1113f",
                "kGGB1EF1AB1AGLKGB1AB1EF1AGGl",
                "YHHD1CD1EF1CHHHHD1EF1CD1CHHZ",
                "e1111111EF11111111EF1111111f",
                "chhhhhhhnmhhhhhhhhnmhhhhhhhd"
        ]

        return context.extraMode == CgContext.EnOnOff.Off ? mazeSource : mazeSourceExtra1
    }

まとめ

今回パックマン・ゲームのアレンジとして以下を追加した。
 - コンフィグレーション・メニュー
 - 加速度センサー、タッチ操作
 - オリジナル迷路

最後に

パックマンのゲームを題材にして、SwiftによるiOSプログラミングを勉強してきた。
ソースコードはコメント含む 5000行程度で、アーケードゲームに相当するクオリティとアレンジを簡単に実現することができた。

コロナ禍のどこに行けない夏休みから始まったが、また一つプログラミングの面白さを味わう良い機会ともなった。

全11回のiOSアプリ開発入門は、これにて終了。

<読んでいただいた方、ありがとうございました〜>

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