- 投稿日:2020-05-17T22:14:57+09:00
SwiftでCore MIDI
週末急に思い立ってiPadでMIDIピアノの入力に反応するアプリを作ってみようと思ったのですが、Core MIDI周りの情報はObjective-C止まりでアップデートされていないものが多く、ましてSwiftとなると英語でもほとんど情報がないので結構困りました(そもそもAppleがMIDI関連の情報発信についてやる気ない)。ニーズが果たしてどれほどあるのかわかりませんが、一応メモとして残しておきます。
また、今回作成したバイナリはCatalyst経由でmacOS上でも動いたので、macの場合もSwiftを使う限りは同じやり方で行けると思います。
目標
MIDIキーボードから入力されたイベントに従ってiOSアプリ上で絵を出す。
iPadからMIDIピアノを鳴らすなどの送信系は今回扱っていません。流れ
Swiftと言ってもCore MIDIをつつく以上、大きな流れはObjective-Cの場合と変わりません。下記のサイトは10年以上前のものですが、大いに参考にさせていただきました。
やることをまとめると、
- MIDIデバイスの一覧を取得
- 使用したいデバイスに対するMIDIClientを作成
- MIDIClientで使うMIDIInputPort(データの受け口)を作成
- 使用したいデバイスのMIDIEndpointと、MIDIInputPortを接続
- 飛んでくるMIDIメッセージの処理
という感じです。
1. MIDIデバイスの一覧を取得
import CoreMIDI import os.log var numberOfSources = 0 var sourceName = [String]() func findMIDISources() { sourceName.removeAll() numberOfSources = MIDIGetNumberOfSources() os_log("%i Device(s) found", numberOfSources) for i in 0 ..< numberOfSources { let src = MIDIGetSource(i) var cfStr: Unmanaged<CFString>? let err = MIDIObjectGetStringProperty(src, kMIDIPropertyName, &cfStr) if err == noErr { if let str = cfStr?.takeRetainedValue() as String? { sourceName.append(str) os_log("Device #%i: %s", i, str) } } } }まず、
MIDIGetNumberOfSources()
関数を使用して、接続されているMIDIデバイス数を取得します。次にループ内でMIDIGetSource()
を呼び、インデックス番号に対応したMIDIデバイスを取得します。このデバイスに対して
MIDIObjectGetStringProperty()
を呼んであげることでデバイスの名称が取得できます。このデバイス名称は次のMIDIClientを作成する際に必要となります。注意点はCore MIDIはCore Foundationを使用するC言語ベースのフレームワークなので、文字列は
String
ではなくCFString
であり、文字列の取得はCFString
のポインタを渡す形で行う点です。取得されたCFString
は自動でメモリ管理されないため、takeRetainedValue()
を呼んだ上でSwiftの文字列に変換します。2. MIDIClientを作成
let name = NSString(string: sourceName[index]) var client = MIDIClientRef() var err = MIDIClientCreateWithBlock(name, &client, onMIDIStatusChanged) if err != noErr { os_log(.error, "Failed to create client") return } os_log("MIDIClient created") func onMIDIStatusChanged(message: UnsafePointer<MIDINotification>) { os_log("MIDI Status changed!") }次に取得したデバイス名から、MIDIClientを作成します。この際、ステータスが変化した際のコールバックを指定するのですが、これまでCore MIDIは
MIDIClientCreate()
という関数しかなく、コールバックもCの関数ポインタ形式で非常に面倒くさかったのですが、iOS 9.0からMIDIClientCreateWithBlock()
という関数が追加され、コールバックにSwiftのクロージャを指定できるようになったのでめちゃめちゃ分かりやすくなりました。Core MIDI全然アップデートねえじゃねえかとか言ってすみません。ここではonMIDIStatusChanged()
という関数を別に定義して、それを指定しています。3. MIDIInputPortを作成
let portName = NSString("inputPort") var port = MIDIPortRef() err = MIDIInputPortCreateWithBlock(client, portName, &port, onMIDIMessageReceived) if err != noErr { os_log("Failed to create input port") return } os_log("MIDIInputPort created") func onMIDIMessageReceived(message: UnsafePointer<MIDIPacketList>, srcConnRefCon: UnsafeMutableRawPointer?) { os_log("MIDI Message Received") }MIDIInputPortの作成も、
MIDIInputPortCreateWithBlock()
関数ができたのでそちらを使います。実際にMIDIメッセージを受信した際に呼ばれるのはここで指定したコールバックです。(MIDIメッセージの解析はまた後ほどで行います)4. MIDIEndpointとMIDIInputPortを接続
let src = MIDIGetSource(index) // MIDIEndpointRef err = MIDIPortConnectSource(port, src, nil) if err != noErr { os_log("Failed to connect MIDIEndpoint") return } os_log("MIDIEndpoint connected to InputPort")あとはそのままの流れで、最初に取得したMIDIEndpointと作成したMIDIInputPortを接続すればOKです。
5. MIDIメッセージの処理
最後にコールバックの中で受け取ったMIDIメッセージを解析し、内容に合わせた処理を行います。
func onMIDIMessageReceived(message: UnsafePointer<MIDIPacketList>, srcConnRefCon: UnsafeMutableRawPointer?) { let packetList: MIDIPacketList = message.pointee let n = packetList.numPackets //os_log("%i MIDI Message(s) Received", n) var packet = packetList.packet for _ in 0 ..< n { // Handle MIDIPacket let mes: UInt8 = packet.data.0 & 0xF0 let ch: UInt8 = packet.data.0 & 0x0F if mes == 0x90 && packet.data.2 != 0 { // Note On os_log("Note ON") let noteNo = packet.data.1 let velocity = packet.data.2 DispatchQueue.main.async { self.delegate?.noteOn(ch: ch, note: noteNo, vel: velocity) } } else if (mes == 0x80 || mes == 0x90) { // Note Off os_log("Note OFF") let noteNo = packet.data.1 let velocity = packet.data.2 DispatchQueue.main.async { self.delegate?.noteOff(ch: ch, note: noteNo, vel: velocity) } } let packetPtr = MIDIPacketNext(&packet) packet = packetPtr.pointee } }MIDIメッセージは
MIDIPacketList
というデータ型にまとめて入っており、MIDIPacketNext()
を呼んでやることで次のデータへのポインタが返ってくるようになっています。Appleのページによれば以下のような使い方が正しいそうです(そもそもSwiftじゃないですが...)。MIDIPacket *packet = &packetList->packet[0]; for (int i = 0; i < packetList->numPackets; ++i) { // ... packet = MIDIPacketNext (packet); }なんかこれだと最後に
MIDIPacketNext()
が一回余分に呼ばれてる感じがしますが、Appleがいいと言うのでたぶんいいのでしょう。データの中身は
.data.[0-255]
でアクセスできます。最初の1バイトが0x9nの場合ノートオン、0x8nの場合はノートオフで、nはチャンネル番号を表します。またノートオン/オフの場合は続く2バイトでノートナンバー(音高)とベロシティ(強さ)が取得できるので、これらの情報を使って音の高さ、強さに合わせた処理を書けばよいでしょう。プロトコルを定義しておいてdelegateを設定できるようにしておくと楽かもしれません。
- [参考] MIDIの学習
また、このコールバックですがメインスレッドとは別に呼ばれるっぽいので、UIやグラフィックに変更を加える際は、DispatchQueueなどを介してメインスレッドで実行されるように書いてあげるのが安全だと思われます。
まとめ
これらの処理をクラスにまとめると以下のような感じになります。
MIDIManager.swiftimport Foundation import CoreMIDI import os.log protocol MIDIManagerDelegate { func noteOn(ch: UInt8, note: UInt8, vel: UInt8) func noteOff(ch: UInt8, note: UInt8, vel: UInt8) } class MIDIManager { var numberOfSources = 0 var sourceName = [String]() var delegate: MIDIManagerDelegate? init() { findMIDISources() } func findMIDISources() { sourceName.removeAll() numberOfSources = MIDIGetNumberOfSources() os_log("%i Device(s) found", numberOfSources) for i in 0 ..< numberOfSources { let src = MIDIGetSource(i) var cfStr: Unmanaged<CFString>? let err = MIDIObjectGetStringProperty(src, kMIDIPropertyName, &cfStr) if err == noErr { if let str = cfStr?.takeRetainedValue() as String? { sourceName.append(str) os_log("Device #%i: %s", i, str) } } } } func connectMIDIClient(_ index: Int) { if 0 <= index && index < sourceName.count { // Create MIDI Client let name = NSString(string: sourceName[index]) var client = MIDIClientRef() var err = MIDIClientCreateWithBlock(name, &client, onMIDIStatusChanged) if err != noErr { os_log(.error, "Failed to create client") return } os_log("MIDIClient created") // Create MIDI Input Port let portName = NSString("inputPort") var port = MIDIPortRef() err = MIDIInputPortCreateWithBlock(client, portName, &port, onMIDIMessageReceived) if err != noErr { os_log("Failed to create input port") return } os_log("MIDIInputPort created") // Connect MIDIEndpoint to MIDIInputPort let src = MIDIGetSource(index) err = MIDIPortConnectSource(port, src, nil) if err != noErr { os_log("Failed to connect MIDIEndpoint") return } os_log("MIDIEndpoint connected to InputPort") } } func onMIDIStatusChanged(message: UnsafePointer<MIDINotification>) { os_log("MIDI Status changed!") } func onMIDIMessageReceived(message: UnsafePointer<MIDIPacketList>, srcConnRefCon: UnsafeMutableRawPointer?) { let packetList: MIDIPacketList = message.pointee let n = packetList.numPackets //os_log("%i MIDI Message(s) Received", n) var packet = packetList.packet for _ in 0 ..< n { // Handle MIDIPacket let mes: UInt8 = packet.data.0 & 0xF0 let ch: UInt8 = packet.data.0 & 0x0F if mes == 0x90 && packet.data.2 != 0 { // Note On os_log("Note ON") let noteNo = packet.data.1 let velocity = packet.data.2 DispatchQueue.main.async { self.delegate?.noteOn(ch: ch, note: noteNo, vel: velocity) } } else if (mes == 0x80 || mes == 0x90) { // Note Off os_log("Note OFF") let noteNo = packet.data.1 let velocity = packet.data.2 DispatchQueue.main.async { self.delegate?.noteOff(ch: ch, note: noteNo, vel: velocity) } } let packetPtr = MIDIPacketNext(&packet) packet = packetPtr.pointee } } }このクラスを使う側(ViewController)の処理としてはこんな感じです。
MyViewController.swiftclass MyViewController: UIViewController, MIDIManagerDelegate { private var midi: MIDIManager? override func viewDidLoad() { super.viewDidLoad() midi = MIDIManager() if 0 < midi!.numberOfSources { midi!.connectMIDIClient(0) midi!.delegate = self } } func noteOn(ch: UInt8, note: UInt8, vel: UInt8) { // ノートオン時の処理 } func noteOff(ch: UInt8, note: UInt8, vel: UInt8) { // ノートオフ時の処理 }不十分なところ多々あると思いますがご容赦ください!
AppleはMIDIのサポートちゃんと続けていってくれるのだろうか...。
- 投稿日:2020-05-17T17:19:08+09:00
UITableViewとUICollectionViewにおいてFallbackする方法
環境
Xcode: Version 11.3.1 (11C505)
Swift: 5
iOS: 13.2執筆きっかけ
先輩から「その方法ではUICollectionViewの場合はクラッシュするよ」と指摘されたので検証する
その当時のフォールバック方法
*一部省略しております
final class TableViewController: UIViewController { var mockError: String? // エラーを模しています private lazy var tableView: UITableView = { ・・・ ・・・ tableView.register(UITableViewCell.self, forCellReuseIdentifier: NSStringFromClass(UITableViewCell.self)) return tableView }() ・・・ ・・・ } extension TableViewController: UITableViewDataSource { ・・・ ・・・ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard mockError != nil else { return UITableViewCell() // これが実行される } return tableView.dequeueReusableCell(withIdentifier: NSStringFromClass(UITableViewCell.self), for: indexPath) } }上記の場合,mockErrorがnilであるため,
guard mockError != nil else { return UITableViewCell() }が実行されます.これは,Errorが発生した想定です.
TableViewの場合は,registerされていないCellを返してもクラッシュしません.UICollectionViewの場合
*一部省略しております
final class CollectionViewController: UIViewController { private lazy var collectionView: UICollectionView = { ・・・ ・・・ collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: NSStringFromClass(UICollectionViewCell.self)) return collectionView }() var mockError: String? ・・・ ・・・ } extension CollectionViewController: UICollectionViewDataSource { ・・・ ・・・ func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard mockError != nil else { return UICollectionViewCell() // これが実行される } let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(UICollectionViewCell.self), for: indexPath) cell.backgroundColor = .black return cell } }先程のコードをUICollectionViewに置き換えてみました.
上記を実行すると...Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'the cell returned from - collectionView:cellForItemAtIndexPath: does not have a reuseIdentifier - cells must be retrieved by calling - dequeueReusableCellWithReuseIdentifier:forIndexPath:'上記の通り,UICollectionViewの場合は,registerしたCell以外を返すとクラッシュします
よって,下記の用に書き換えますguard mockError != nil else { return collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(UICollectionViewCell.self), for: indexPath) }まとめ
今後TableViewの仕様が変わる可能性も考えて,フォールバック時にはregisterしたCellをどちらも返すようにします
- 投稿日:2020-05-17T16:58:13+09:00
【iOS】未経験者からのiOSアプリ開発入門
- 投稿日:2020-05-17T16:16:55+09:00
【iOS】iOSアプリ開発入門~ レイアウト編1~
前回はiOSアプリ開発をするためのXcode環境構築について投稿しました。
環境構築編:https://qiita.com/euJcIKfcqwnzDui/items/0bbde21f63825cb0cd88環境構築が終わったらさっそく実際にiOSアプリを作りましょう。
まずアプリ開発において一番重要になるのはレイアウト、つまり見た目です。
※レイアウトはエンジニアの間ではユーザインタフェース、特に省略してUIと呼びます。iOSのUIはInterface Builderを使う方法が最もメジャーです。というかほぼこれ一択に近いです。
(中にはコードでごりごり書いて作らないと気が済まないという方もいるようですが。。。)今回はiOSアプリにおけるUI作成方法について説明していきます。
Interface Builderとは
その名の通りアプリのUIを構築するためのツールです。
Xcodeの標準機能として提供されています。皆さんがよく見るアプリにはボタン、スイッチ、テキストなど様々なUIが配置されているかと思います。
これらは画面上のどこに、どのサイズで配置するか指定してあげる必要があります。
この指定をグラフィカルに、直感的に行うためのツールがInterface Builderです。グラフィカルに指定できる言われてもピンとこない方がいるかと思います。
例えばWebアプリのUI配置には以下のようなHTML書く必要があります。
Sample.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Interface Builderとの比較</title> </head> <body> <h1>TEST</h1> <p>HTMLの場合</p> </body> </html>あまり慣れていない方からするとこれだけでは何が表示されるかはわからないかと思います。
一方でInterface BuilderでのUI配置は以下のようにします。
これならパッと見て直感的にどんな画面が表示されるのかわかるのではないでしょうか?
というか実際特に処理を書いていなければこのまま画面に表示されます。
Interface Builderを使用すればマウス操作でこのようにUIを作成することが可能となります。余談ですがInterface Builderも内部的にはXMLと呼ばれるHTMLと似たような形式で扱っています。
「直感的に指定したものをXMLに変換してくれるツール」というように認識しておけばいいかと思います。ボタンを配置する
それでは実際にUIを配置していきましょう。
Interface BuilderはStoryboardというファイルを使用します。
まず前回作成したプロジェクトを開いてください。
左のファイル一覧にMain.storyboardを開きます。
次にボタンを配置します。
右上の[+]ボタンをクリックしてください。クリックすると配置できるUIのリストが表示されます。
[Button]を選択し、白い画面にドラッグしてください。
画面の上に[Button]というUIが配置されていればOKです。
これでボタンが配置されました。
一度シミュレータで実行して確認してみましょう。
シミュレータの画面にもボタンが追加されているかと思います。
またこのボタンをタップすると色が薄くなりボタンとして機能していることがわかります。
このようにして自由にUIを配置してアプリの見た目を作っていきます。
ボタンの色を変える
ボタンを配置することはできましたが、ユーザから見るとボタンをタップするまではこれがボタンなのかテキストなのか一見するだけではわかりません。
アプリのUI設計する上で重要なのはユーザが直観的に操作できることです。
どうすればいい感じのデザインになるのかというのは突き詰めると難しいところではありますが
AppleからHuman Interface Guidelinesとしてある程度の指針が定められています。
実際にリリースするアプリを作る際はこのあたりを参考にしてください。話は若干逸れましたがボタンらしく見せるための一番簡単な方法は色を付けてあげることです。
それでは実際に色を付けてあげましょう。
Main.storyboardを開いてください。Storyboardの左側を確認してください。
[View Controller Scene]というものがあり、階層構造になっていることがわかります。
ここにはこの画面に配置したUIが表示されていきます。
前項で追加したボタンも一番下の階層に含まれています。次にStoryboardの右側を確認してください。
こちらにはInspectorと呼ばれるメニューが表示されています。
このInspectorは配置したUIに対して細かく設定をするためのものです。下図のようにボタンを選択した状態でInspector上部右から3つ目のタブAttributes Inspectorを選択してください。(正確な名前はわかりません、適当に名付けています)
Attributes Inspectorではボタンの色やフォントの色、サイズ、種類などボタン自体の見た目に関する設定をすることができます。
背景色を指定する
Attributes Inspectorの設定の下の方に[Background]というものがあります。
これはボタンの背景色を指定する設定です。
初期状態ではDefaultとなっていますがボタンのデフォルト色は無色透明です。
そのため最初に配置したときは色がついていないというわけです。
ではこちらの色を変更してみます。
[Background]のDefaultとなっている部分あたりを選択してください。
選択するとプルダウン形式で色のメニューが表示されます。
真ん中あたりに表示されているDark Text ColorなどはXcodeが標準で提供している色です。
こちらを選択しても色を変えることはできます。
今回はせっかくなので自分で色を調整します。
プルダウンメニューの一番下の[Custom...]を選択してください。
Customメニューでは色を細かく作ることができます。
以下は一般的によく使う手順です。
上部タブの左から2つ目を選択してください。こちらからはRGB形式やCMYK形式で指定できます。
1つ下のプルダウンをクリックし[RGB Sliders]を選択してください。RGBで色を指定できるようになります。
スライダーもしくは値を入力しRGBの割合を調整します。
RGBは0〜255までの間で指定することができます。
例えば以下のような具合です。
黒:(R,G,B) = (0,0,0)
白:(R,G,B) = (255,255,255)
赤:(R,G,B) = (255,0,0)
黃:(R,G,B) = (255,255,0)
Attributes Inspectorの[Background]の項目に指定の色が表示されていれば成功です。
実行してシミュレータで確認してみてください。
さっきよりはボタンらしく見えるようになったのではないでしょうか?文字色を変更する
次は文字色を変更しましょう。基本的には背景色変更の手順と同様です。
Attributes Inspectorから[Text Color]の項目を選択します。
あとは先程の手順同様に好きな色を設定します。
最後に
今回はInterface Builderを使ったUIの作成方法や色の指定方法について説明しました。
これ以外にも細かい設定をしたり、プログラムから状態に応じて変更したりなど覚えることは多くあります。
iOS開発が初めての人はとっつきづらい部分もあるかもしれませんが慣れてしまえば便利だと感じるようになるかと思います。1つづつ消化していきましょう。
以上で今回の説明は終わります。
次回はUIのサイズや配置の設定の仕方について説明します。
レイアウト編2:https://qiita.com/euJcIKfcqwnzDui/items/31f6b11afb317275f684本連載ではプログラミング未経験からiOSアプリ開発が行えるようになることを目的としています。
今までの投稿をまとめていますのでこちらもご覧ください。
アジェンダ:https://qiita.com/euJcIKfcqwnzDui/items/0b480e96166e88945684
- 投稿日:2020-05-17T16:11:22+09:00
Xcodeプロジェクト作成時にMain.storyboardから脱却する方法
環境
Xcode: Version 11.3.1 (11C505)
Swift: 5
iOS: 13.2手順
- アプリ本体とTestのinfo.plistからMain storyboard file base nameの項目を排除する
- Main.storyboardを削除する
- SceneDelegateに追記する
swift/SceneDelegate.swiftfunc scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let scene = (scene as? UIWindowScene) else { return } window = { let window = UIWindow(windowScene: scene) window.rootViewController = ViewController() window.makeKeyAndVisible() return window }() }以上
- 投稿日:2020-05-17T14:26:19+09:00
Intuneで、iPhone・iPadのOSアップデートを強制する方法ないの?
前回、iPadのOSアップデートをブロックする方法ないの?では、OSアップデートさせたくなくてブロックする手段がほしくて、MDMから一定期間延期するするという方法等をまとめてみた。13の時は iPadOS のリリースとその対応でヤキモキしたけど、もう13リリースされて半年以上経った。
今回は「組織内に上がっていないデバイスがあるとバージョン違いの問い合わせも増えるし、もしやっていない人がいる場合には強制アップデートさせたい」というシナリオ。
というわけで、Intune で iOS / iPadOS を強制アップデートする方法しらべてみた。
MEM管理センター
Azure Portal でもまだいけるけど、8月にはMEM管理センターに移行されるらしい。私自身が慣れるためにも、こっちでベースで書きます。
※ Microsoft Endpoint Management 管理センター
MEM = クラウドベースのIntune + オンプレベースのSCCM、の両方をWebから一元管理できる、って捉えてる。ポリシーの作成
MEM https://endpoint.microsoft.com/ を開く。
デバイス - iOS または iPadOS のポリシーの更新 - プロファイルの作成 を選択
スケジュールは、曜日別で時間帯で設定できる。
たとえば、月曜は会議が多いので、強制の曜日から除外する、なんてことも可能。
対象グループ割り当てて、完了!
注意点
自動でアップデートを強制する、だけです。
手動で実施すればアップデートできるし、アップデートをブロックさせる、という使い方はできない。まとめ
できれば、組織内デバイスはできれば最新メジャーバージョンあるいは、それの1つ前以上がベター。というか、それ以前はサポートされないし。
13では、200MB以上のアプリや更新が、LTEでもできるようになっている。(WiFiが必須じゃない!)アプリのバージョンが古いとサービスに接続できなくなるケースもある(OutlookアプリでOffice365のメールに接続できないとか)
なので、アップデートを強制するためにも13以上が望ましいかな。
- 投稿日:2020-05-17T11:54:55+09:00
モバイルアプリに対する難読化/堅牢化
クラッキングの現状
近年様々の分野においてアプリのクラッキング被害が多く報告されています。これは、初心者にも簡単に使えるクラッキングツールが、インターネットで入手可能となり、攻撃者の数を急増したためです。
体表的になクラッキング被害は以下のような例を挙げられます。
- ゲームチート
- クラッキングにより不正ツールが作られ、課金回避や不正操作が横行します。
- 製造産業機器・ロボットのクローン
- 組込アプリのアルゴリズムを解析され、クローン製品を作られます。
- 金融・フィンテック 電子決済のなりますし
- アプリの改ざんにより、本人の認証の不正回避や、なりますしなどの原因となります。
- O2O 推薦者への報奨金のハッキング
- O2Oアプリの特性上、機密にすべきユーザの個人情報をアプリ内で扱うことがあります。ハッカーは、アプ リケーションの動作を理解し、簡単な操作でエミュレータから新しいユーザとして認識される方式を利用し、不当な利益を請求してきました。難読化/堅牢化とは
- 難読化
- プログラムにおいて、その内部的な動作の手続き内容・構造・データなどを人間が理解しにくい、あるいはそのようにるよう加工されたソースコードやマシンコードのこと- 堅牢化
- 改竄を検知してアプリを停止させる
- ロジック改竄検知、アプリ署名改竄検知、Root化/脱獄検知、デバッグモード検知、etc・・・サービス紹介(Msafe Technologyのseciron)
Msafe Technologyの堅牢化サービスはキャンペン実施しております。
ご希望のお客様はコーポレートサイト問い合わせフォームよりお問い合わせください。
Msafe Technology株式会社
- 投稿日:2020-05-17T03:31:10+09:00
子供と作る「おみくじ」アプリ on iPhone
対象者
- 幼児〜小学校低学年のお子さんと過ごす中で、
- 在宅勤務等でMacに向かわなければならないのに、
- 「なんかアプリ入れてよー」とiOSデバイス持ってせがまれる方
- Mac, iOSデバイス, Xcode, Lightningケーブル が手元にあることは必須
- ガチのアプリ開発者はもっと上手くやってください
背景と狙い
- この記事、プロジェクトは、Swiftのコーディングスキルを身につけるとか、開発とは何かといったテーマの学びを目的としていません。
- スーパーで売られている切り身のマグロが,元は一匹の魚であったと水族館で教えるような… 手元のiPadやiPhoneで動いているアプリが人のアイディアと努力によって組み立てられていることを実感してもらうような目的です。
- そして最大の目的は、仕事中に構って構って攻撃を受けないよう、興味を満たすか興味をそらすかすることです。
進め方(プロジェクトを成功させるために不可欠)
- まず最初に、お子さんに何も話す以前に徹夜でもなんでもして一通り動くようにする
- お子さんに必要な素材を作ってもらう
- お子さんと一緒にアプリとして組み立てて微調整する
- ビルドしてiOSデバイスに入れる
この順序がとても大事で、1ができないまま取り組むと、あなたがXcodeと試行錯誤している間にお子さんは興味を失う可能性が高いです。
それから、開発中に素材画像やコードを指で指し示すことがあるはずです。愛機の繊細なスクリーンのコーティングを守りたい方は、SideCarでiPadのスクリーンを見せながら作業しましょう…
開発する
進め方と前後しますが、説明として自然な流れでいきます…
素材の用意(進め方の#2)
以下の画像を用意します。
ファイル 必要数 サイズ メモ アプリのアイコン 5枚 120*120, 152*152, 167*167, 180*180, 1024*1024 ピクセル 様々なスクリーンに対応させるため複数用意するが、大きめの画像1枚を全てに使うこともできる 待ち受け中 1枚 1000*1000 ピクセル 起動直後にお神籤を引く前の状態で表示する画像 お神籤の結果 5枚〜 1000*1000 ピクセル 大吉〜凶 くらいまで、お好きなお神籤結果の分を用意する こんな感じで用意した。
ファイル名やフォルダ分けはXcodeに反映されないのでお好きにどうぞ。アプリの開発(進め方の#1)
0. Xcodeの準備
- AppStoreから「Xcode」をインストールする(モバイル回線の人は容量に注意)
- そこそこ時間がかかるのでしばらく待つ
- インストールされたら、開いて規約の同意やら権限付与やら画面の指示に従って済ませる
- メニューの「Xcode」→「Preferences...」を開き、「Accounts」タブで自分のApple IDが登録されていることを確認する。無ければ左下の「+」をクリックしてApple IDを追加する。
1. プロジェクトを作成する
Xcodeを開いて、メニューの「File」→「New」→「Project...」を開く
「iOS」タブの「Single View App」を選択して、「Next」をクリックする。
「Product Name」にプロジェクトの名前(半角英数にする。日本語は使わない)を入れて、「Team」は自分のApple IDの名前に「(Personal Team)」が付いたものを選ぶ。
「Include Unit Tests」と「Include UI Tests」はチェックを外しておく。
「Next」をクリックして先に進む。プロジェクトを保存する場所を聞かれるので、お好きな場所を指定する。
すると、以下の様な「Hello World」がSwiftで書かれた
ContentView.swift
ファイルが開く。2. コーディング頑張らず以下をコピペする
画面左のファイルのリストで
ContentView.swift
というファイルが選択されていることを確認して、以下の様に置き換える。置き換え対象が以下で…
struct ContentView: View { var body: some View { Text("Hello, World!") } }これに置き換える。
struct ContentView: View { @State var assetName = "waiting" @State var buttonLabel = "お神籤を引く" @State var isDrawn = false var body: some View { VStack { Image(self.assetName) Button(action: { if(self.isDrawn){ self.assetName = "waiting" self.isDrawn = false self.buttonLabel = "お神籤を引く" } else{ self.assetName = Int.random(in: 1 ... 5).description self.isDrawn = true self.buttonLabel = "再挑戦する" } }){ Text(buttonLabel) } .padding(.all) } } }そうすると、こんな感じになる。
3. 素材を登録する
画面左のファイルのリストで
Assets.xcassets
を選択する。
AppIcon
だけが存在するはずなので、「+」をクリックして「New Image Set」を選び、waiting
と1
から5
までの連番ができるように追加を繰り返す。一通り追加したら、素材をはめ込む。
素材は、ファイルをドラッグ&ドロップしていくだけでOK。お子さんが作った素材をはめ込む前に、仮の画像で試すこと。
幸いなことに、いらすとやさんにお神籤画像があった。4. ビルドする
Command
キーとR
キーを同時に押してビルドする。
一応ウィンドウ左上の「▶」ボタンでもできるけれど、ショートカット使う方が格好いいでしょう?すると、シミュレーター上でアプリが開いた状態になる。
「お神籤を引く」を押して、結果が表示されればOK。
ここまで、お子さんと取り組む前にできているとスムースなはず。
微調整(進め方の#3)
1. アプリ名を変える
画面左側のリストの1番目、プロジェクト名をクリックする。
「General」タブを開き、「TARGETS」でアプリのアイコンが付いたものを選択(初期状態はプロジェクト名)し、右側の「Display Name」を編集する。2. ボタンの文言を変える
「お神籤」が読めない歳の子や、お神籤より占いが良いとかあると思うので、以下の様に
ContentView.swift
の中で文言が書かれている部分のダブルクォーテーションの内側を変更する。ボタンの文言は3箇所に書かれている。
3. ボタンの色、形、大きさ を変える
これもやはり
ContentView.swift
の中に加筆する。文字の大きさであれば、
Text(buttonLabel)
の部分をText(buttonLabel).font(.largeTitle)
と追記したり、色も変えるならText(buttonLabel).font(.largeTitle).foregroundColor(Color.white)
のようにしてみる。ボタンの色は
.padding(.all)
の後ろに.background(Color.green)
を追記することで緑色になる。
更に.cornerRadius(10.0)
を追記すればボタンの角が丸くなる。4. 結果のバリエーションを増やす
もし結果を5通りより多くする場合、
Assets.xcassets
に登録した連番の画像を追加していく。
そして、ContentView.swift
の中で乱数を生成して画像を選ぶ処理で、乱数の生成範囲を広げる。
Int.random(in: 1 ... 5).description
と書かれている箇所があるので、 右側の数字を変える。一通りいじったら、
Command
+R
で実行してうまく動くことを確認する。iOSデバイスへの転送(進め方の#4)
最後に、完成したアプリを実機にインストールする流れとしては…
- MacとiOSデバイスをケーブルで接続する。
- コンピュータを信頼するかどうかとダイアログが出るので、信頼ボタンを押したりパスコードを入れたりする。
- Xcodeのウィンドウ上部に「iPhone SE (2nd generation)」のようにシミュレーターの機種名が表示されている部分をクリックし、最上段に実機の機種名が表示されるはずなので、それを選択する。
- 改めて
Command
+R
を押すと、ビルド後にアプリが実機に転送される。- 最初は実機でアプリが起動できないので、実機の「設定」アプリを開き、「一般」→「デバイス管理」を開く。
- 「デベロッパAPP」に「Apple Development: (メールアドレス)」の項目が出てくるのでタップし、「〜を信頼」をタップ、「信頼」ボタンをタップする
- ホーム画面に戻り、作成したアプリのアイコンをタップして動くことを確認する