- 投稿日:2020-10-11T22:02:51+09:00
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] }) } } }プレビュー
環境
2020年10月11日時点
Xcode 12.0.1
Swift 5.3
これ以外の環境については特に検証してないのでわからないですが、
SwiftUIが動く環境であれば可能だと思い込んでますNavigationBarの背景色を変えたい
とりあえず、さくっとググってみるといくつかの記事で、コンストラクタとかの初期化処理の際に
UINavigationBar.appearance()
でしかできないとかかんとか、、、たしかに、
UINavigationBar.appearance()
でも変えれることは変えれるが、
画面ごとに色を変えるのが困難(ってかこれだけなら無理なのでは、、、)Stack Overflowありがとう!
SwiftUI update navigation bar title color
ちゃんと、いい感じに変更する方法を教えてくださってる方がいらっしゃりましたこの方法のデメリット
プレビューをライブモードにしないとスタイルが反映されないです
ライブモードにすればいいだけなので、どうでもよいですねってことで、
UIViewControllerRepresentable
を使っていい感じに背景色を変更できました
まだまだ、SwiftUIは成長途中なので、この子を使いこなさないといけなさそう
- 投稿日:2020-10-11T21:45:29+09:00
[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 に、ファイル名を設定する。
- 投稿日:2020-10-11T18:50:45+09:00
【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.おわりに
今回は共用型の列挙型についての記事を書きましたが、プロトコルやオプショナル型の扱いについては、記事が少し長くなってしまうので今回は解説していません。
来週以降はクラスなどについての解説をしていこうと思います。
ここまで読んでくれた方、ありがとうございました。
- 投稿日:2020-10-11T16:56:26+09:00
VisionFrameworkとCoreMLで画像解析をするときの流れを図にしてみた
VisionFrameworkとCoreMLを使った画像の解析は、そんなに手順は多くないので、過去のコードを見ればなんとなく思い出せたりしますが、手元に図があると良いかもということで描いてみました。
画像解析と言ってもいろんなパターンがありますが、今回はAppleの公式サンプルであるRecognizing Objects in Live Captureの流れを起こしたものになります。
あと、こちらの記事はVisionFrameworkとCoreMLを使った画像の解析のコードを端的にまとめられていて参照するのにちょうどよいです。
VisionFrameworkとCoreMLを使用した画像解析iOS開発について発信しています。
NoteではiOS開発について定期的に発信していますので、フォローしていただけますと幸いです。
https://note.com/tokyoyoshidaTwitterでは簡単なtipsを発信しています。
https://twitter.com/jugemjugemjugem
- 投稿日:2020-10-11T16:25:32+09:00
FirebaseMLでテキスト認識を行う![SwiftUI]
はじめに
こんにちは、iOSアプリを最近開発している@RyosukeKamimuraと申します!
今回は、SwiftUIでテキスト認識を行っていきます!忙しい方に向けて結論から先に述べます!!
結論
結論は、3点あります。
1. FirebaseMLは日本語に認識する性能が、良好
2. 手書き文字の認識は少し難がある。
3. 1000リクエストまでは無料!以下、チュートリアル的に行います!
Firebaseプロジェクトの設定
①Firebaseと連携
Firebaseのセットアップから参照をお願いします。
Firebaseへの登録の共通部分であるためここでは省かせていただきます。②FirebaseコンソールでクラウドベースのAPIを有効にしてください
Cloud APIの使用状況を表示の隣のボタンをOnにしてください。
③Firebase公式ドキュメントを見ながら、テキスト認識を行う
MLKitを使用して画像内のテキストを認識するiOSSwiftUIで書きたかったので、Coordinatorをうまく噛み合わせてUILableをラップするように書きました!
(もっとこう書いたほうがいいよというご指摘コメントでお待ちしております!)今回の記事では、実際にテキスト認識を行う関数に注目して見てもらいたいです。
(Firebaseの公式ドキュメントでは少ないから)
- 関数の作成
RecognizedText.swiftfunc recognizedTextFunc() -> String { // この関数の中を記述していく }2.Visionインスタンスの初期化
RecogniedText.swiftfunc 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.swifttextRecognizer.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=17394b601591bcf3f06Githubに自分のコードも載っているのでGithub/RyosukeKamimuraもどうぞ!
最後まで読んでいただき、ありがとうございました!
- 投稿日:2020-10-11T10:31:05+09:00
[個人アプリ開発記]サイコロを操作し,底面を記録するARアプリ
はじめに
こんにちは。少し前になりますが,3つ目となるアプリをリリースしましたので,その機能や流れについて書こうと思います。なお一つ目,2つ目についてはそれぞれこちらとこちらの記事を読んでいただければ。SceneKitでの開発になりますので,UnityとSceneKitどちらを採用しようか迷っている方の参考にももしかしたらなるかもしれません。私は最初はUnityで製作していましたが,3Dプログラミングの使い勝手やコマンドの多さはUnityの方が優れていると思ったものの,プラグインが煩わしくなりSceneKitに変更しました。SceneKitはネット情報に溢れる情報こそ少ないですが,慣れてしまえば思い通りに操作できるかと思います。
アプリの紹介
機能としてはものすごく単純で,前後左右にサイコロを動かす(回す)と動かす前にサイコロがあった底面のマス目が記録されます。
苦労したところ
超単純な機能ですが,実現するためのアイディアを思いついたり,それらを実現するのはとても苦労しました。特にSceneKitは文献どころか日本語のサイトがほとんどありません。以下ぶつかった問題を挙げていきます。(みなさんであればどのように解決しますか?)
回転軸と中心を任意に設定するメソッドがない
動画のような動きはUnityであれば専用のメソッドが用意されているのですが,SceneKitにはありません。(自身の軸(x,y,z)を中心として回転するメソッドはもちろんあります。)
UnityでいうところのRotateAroundをSceneKitでも再現できた pic.twitter.com/6itcmFTaiE
— 二次曲線 (@Hyperbolic____) August 17, 2020配置したサイコロにとっての上下左右と,ユーザーが画面上で操作する上下左右が異なる
まず,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)に設定します。
2.形を持たないnodeを追加する
右下の➕ボタンを押し、nodeを追加します。
<untitled>
が追加されればOKです。(以下これをemptyNodeとします。)
右側に回転する動きを再現したいので、中心となる立方体の辺の中点にemptyNode(0.5,0,0.5)に移動します。
3.emptyNodeの階層下にboxを配置する
階層関係にあるnodeにおいて、子nodeは親nodeの動きの影響を受けます。boxをemptyNodeの下にドラッグ&ドロップし、子nodeとして設定します。
4.emptyNodeを回転させる
emotyNodeのz軸を-90度回転させることで、回転を表現することができます。
「配置したサイコロにとっての上下左右と,ユーザーが画面上で操作する上下左右が異なる」の解決方法
この問題はオブジェクトをのy軸をカメラ座標と同じにすることで解決しました。
「右」と「前->右->後ろ」では底面が異なる」の解決方法
この問題はサイコロを立方体として考えるのではなく,正方形が6枚組み合わさっているものとして捉えることで解決しました。もともと立方体(SCNBox)で回転を考えていたのですが,底面を判定できないことに気づき,正方形(SCNPlane)を組み合わせ,y座標(ワールド座標)が最も小さいものを記録すれば良いということに気づきました。ボタンを押すたびにy座標が最も小さいSCNPlaneを判定するので,それまでに押されたボタンに依存することなく記録することができます。
「2,3,6には底面の向きがあるため底面以外の面も取得しなければならない」の解決方法
この問題は上述し立方体を正方形が6枚組み合わせたものとして考えることに気づいた後にぶつかった問題です。底面の向きを決めるためには側面にある4つ面のうちどれかを取得しなければなりません。つまり,正方形のx座標をワールド座標で置き換えた時に最も大きくなるものを取得しなければなりません。さらに,サイコロのy座標は配置したサイコロにとっての上下左右と,ユーザーが画面上で操作する上下左右が異なる」問題を解決するために回転させているので,x座標はワールド座標への正射影を考える必要があります。
その時のノートはこんな感じ。
技術を得るために
数少ないネットの情報を漁りまくる
SceneKitに関する情報はめちゃくちゃ少ないです。SceneKitの基本的なことはこのサイトで,3dプログラミングについてはUnityで学びました。ARKitについてはBOOTHで販売されているこちらのテキストがすごく丁寧で分かり易かったです。
Qiitaでのアウトプット
SceneKitに関する情報を増やしたい!といった思いと備忘録がわりにQiitaにアウトプットしました。
- https://qiita.com/Hyperbolic_____/items/97dfc180299f03057178
- タップされるたびにnodeを置き換える方法
- viewの中心の座標を取得する
- ARオブジェクトを常にカメラの方向を向いた状態で配置する
- SceneKitでサイコロが転がるようなアニメーションを再現する
- StackViewでどのデバイスでもレイアウトが崩れない十字キーを作成する
- SCNPlaneに画像を貼り付ける
- Int型を拡張して3の倍数か判定するメソッドを作成する
最後に
SceneKitというわりとマイナーなフレームワークでの開発だったため,とにかくインプットよりもアウトプット中心の学習だったように思います。その中で三角比や場合わけ(最終的には256通りになった)など高校数学で学習したことが活かされる場面多々あり,嬉しく思いました。3Dプログラミングはとても難しいですが,その分思い通りに動いた時の達成感は筆舌に尽くしがたいものです。このアプリを通して得られた知見がこの先のアプリの開発で生かされたり,アウトプットした記事が誰かの役に立てば幸いです。
- 投稿日:2020-10-11T07:58:39+09:00
ARKitやSceneKitの図形をMetalシェーダーで変形させる方法
ARKitやSceneKitの図形を自由に変形させる方法を紹介します。
なお、この手法を使って最終的にはARで空中に水を浮かべるといった処理も作りましたので、こちらの記事もご覧下さい。
やり方
今回はこんな感じの球体を変形させていきます。
※下の画像は球体にライトを当てていないのでのっぺりしていますが球体です。
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")このあたりを詳しく知りたい方はこちらを読むと良いと思います。
Vertexシェーダーで変形してみる
まずは、簡単な変形をしてみます。
x座標にy座標分を足してみます。Shader.metalvertex 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.metalpos.x += cos(pos.y);面白いですね。cosを与えると-1〜1の範囲で波打つようになります。
こうした数式の考え方はこちらの本にわかりやすく書いてありました。
経過時間を与えてアニメーションさせてみる
さきほど、
pos.x += cos(pos.y);
としていたところを、次のように変更してみます。Shader.metalpos.x += cos(pos.y + globalData.time);最初に紹介した空中に水を浮かべる方法では、x, y, zのそれぞれについてこのような処理をすることで水のような感じを出しています。
NoteではiOS開発について定期的に発信していますので、フォローしていただけますと幸いです。
https://note.com/tokyoyoshidaTwitterでは簡単なtipsを発信しています。
https://twitter.com/jugemjugemjugem
- 投稿日:2020-10-11T01:10:59+09:00
if文について理解しよう!
今回は、ifについて学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承くださいif文とは?
ifを一言でいうと、条件の成否に応じて実行する構文のことです。
基本的な書き方は以下の通りですqiita.rbvarif 条件式 { 条件式がtrueの場合に実行される文 }次は簡単な例を見ていきましょう。
qiita.rbvar1.let value = 5 2.if value <= 10 { print("valueは10以下です") } 実行結果:valueは10以下ですこのコードを日本語に直してみると
qiita.rbvarもし、[valueの値が10以下だったら{}の中のメソッド実行してくださいね]という意味になります非常に簡単で、わかりやすいですよね!
ちなみに、if文の条件式はBool型を返す必要があります!では、さらにif文について深堀していきましょう!
else節とは?
else節は、条件が成立しなかった場合に実行する文のことを言います。
基本的な構文を見ていきましょう!qiita.rbvarif 条件式 { 条件式がtrueの場合に実行される文 }else{ 条件式がfalseの場合に実行される文 }次は簡単な例を見ていきましょう!
qiita.rbvar1.let value = 26 2.if value <= 10 { print("valueは10以下です") }else{ print("valueは10より大きいです") } //実行結果:valueは10より大きいです上記と同様に、このコードを日本語に直してみると
qiita.rbvarもし、[valueの値が10以下だったら{}の中のメソッド実行してくださいね。 そうでないなら、else{}の中のメソッドを実行してくださいという意味になります。また、else節にはif文をつなげて書くことができます。
qiita.rbvarif 条件式1 { 条件式1がtrueの場合に実行される文 }else if 条件式2{ 条件式1がfalseかつ、条件式2がtrueの場合に実行される }else{ 条件式1、条件式2の両方がfalseの場合に実行される }if-let文とは?
if-let文は、optional型の値の有無に応じて、分岐を行い値が存在する場合は、値の取り出しも同時に行える文のことです。
Optional型とは? Optional型を深堀したい人はこちら!
Optional(Wrapped)型を理解しよう!では、基本的な書き方を見ていきましょう!
qiita.rbvarif let 定数名 = Optional型の値{ 値が存在する場合に実行される文 }else{ 値が存在しない場合に実行される文}
次は、簡単な例を見ていきましょう。
qiita.rbvarlet optionalA =Optional("G") if let X = optionalA { print("値は\(X)") }else{ print("値が存在しません") 実行結果:値はGです最後に
今回は、if文についてアウトプットしていきました。
プログラミング初心者でも、理解しやすく、アプリ開発でも多用される構文なので、しっかり基礎をマスターしていきたいです。
- 投稿日:2020-10-11T01:07:26+09:00
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 .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 たぶん二度と使う気はしないですが、ざっと検索した限りだと見つけられなかったのでまとめておきます。
- 投稿日:2020-10-11T00:01:09+09:00
【入門】iOS アプリ開発 #11【ゲームのアレンジ(加速度センサーを使用した操作など)】
はじめに
公開されている仕様書をもとに作成したパックマンに、アレンジを加えてみたい。パックマンををスワイプだけでなくタッチや加速度センサーで操作できるようにし、オリジナルの迷路も追加してみる。ソースコードは GitHub に公開しているので参照してほしい。
コンフィグレーション・メニュー
操作や迷路を切り替えるためのコンフィグレーション・メニューを追加する。
追加する迷路は開発途中の下記、仕様書を参考にした。ソースコード
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アプリ開発入門は、これにて終了。
<読んでいただいた方、ありがとうございました〜>