- 投稿日: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-17T20:25:05+09:00
関数(Swift)
この記事について
Swiftの勉強をしています。
勉強の過程で得た知識を、忘れないようまとめておくのと共に、誰かの助けになればと思います。関数の定義
関数の定義は
func 関数名(引数名1: データ型, 引数名2: データ型, ...,) -> 返り値のデータ型 { 関数の処理 return 返り値 }と行う。
例として、2つの少数の足し算を行い、少数の解が帰ってくる関数を定義する。
関数名をadd, 引数をx(Double型)とy(Double型)としてfunc add(x: Double, y: Double) -> Double { return x + y }となる。
実際に使うとlet sum = add(x: 2.10, y: 6.12) print("x + y = \(sum)")で、出力は
x + y = 8.22となる。
- 投稿日:2020-05-17T19:54:15+09:00
変数(Swift)
- 投稿日:2020-05-17T19:23:20+09:00
cloudfirestore 複数ドキュメント 取得でnot equalを使う方法
not equalがない
firestoreにはnot equalがないようです。
whereでlessthanとgraterthanを組み合わせればいけると書いてあったのですが
文字列の場合だといけなかった。。。のでfilterを使って対処struct UserData{ let uuid:String let userName:String let nowLatitude: Double let nowLongitude: Double let updatedAt: Timestamp init(uuid:String,userName:String,nowLatitude: Double, nowLongitude: Double, updatedAt: Timestamp) { self.uuid = uuid self.userName = userName self.nowLatitude = nowLatitude self.nowLongitude = nowLongitude self.updatedAt = updatedAt } init(document: [String: Any]) { uuid = document["uuid"] as? String ?? "" userName = document["user_name"] as? String ?? "" nowLatitude = document["now_latitude"] as? Double ?? 0 nowLongitude = document["now_longitude"] as? Double ?? 0 updatedAt = document["updated_at"] as? Timestamp ?? Timestamp() } }てなかんじで辞書を作っていれば
以下のようにフィルターをかけてあげればできるdb.collection("users") .addSnapshotListener { querySnapshot, error in guard let documents = querySnapshot?.documents else { print("Error fetching documents: \(error!)") return } print("userDateReload") self.usersList = documents .filter{UserData(document:$0.data()).uuid != self.AppDelegateDatas.UUID } .map { UserData(document: $0.data()) } print(self.usersList) }
- 投稿日:2020-05-17T16:58:13+09:00
【iOS】未経験者からのiOSアプリ開発入門
- 投稿日:2020-05-17T16:49:38+09:00
【Swift】各情報をアプリ内に保存&取り出す方法
ユーザー情報など、他の画面で同じ情報を出力したい時に使用します。
用途:SNS、日記などユーザー名を例にコードを記載します。
①保存する箇所・キー値を指定して、保存する。
//ユーザー名をアプリ内に保存する UserDefaults.standard.set(プロパティ名.プロパティの中の変える場所, forKey: "キーの名前”)※画像の場合、加えてバイナリデータというデータ型に変換が必要
//プロフィール画像をアプリ内に保存する let data = 保存したいプロパティ名.image?.jpegData(compressionQuality: 0.1) UserDefaults.standard.set(data, forKey: "キーの名前")②取り出したい画面(コントローラー)で、受け取りたい箇所に合わせて、プロパティを設定する。
例、プロフィール画像(Data型→UIImage型)、ユーザー名(String型)
→画像表示するためには、Data型からUIImage型に変更が必要なためvar passedProfileImageData = Data() var passedProfileImage = UIImage() var passedUserName = String()③userDefaultからキー値・何型で受け取るかを指定して、取り出す。
//userDefaultsから取り出す時は以下のように記述する if UserDefaults.standard.object(forKey: "キーの名前") != nil { userName = UserDefaults.standard.object(forKey: “キーの名前") as! String④画像の場合、Data型からUIImage型に変更する
//Data型からUIImage型に変更する passedProfileImage = UIImage(data: passedProfileImageData)!初心者視点で、機能だけに絞って一連の流れを記述しています。
訂正等あればぜひご指摘ください。
- 投稿日: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
- 投稿日:2020-05-17T09:59:45+09:00
【Swift】プロパティ総まとめ~公式ドキュメントをもとに全部まとめてみた~
プロパティ
そもそもプロパティとは、なんぞやというところから
classやstruct,enumでよく見るこいつです。
classclass{ let men: String = "男" }このように、プロパティは型(クラス、構造体、列挙型)を構成する要素の一つで、型もしくは型のインスタンスに紐づいた値を指します。
(ちなみに、型を構成する他の要素には、メソッド・イニシャライザ・ネスト・サブスクリプトなどがあります。)本記事では、プロパティの公式ドキュメントの内容を網羅してまとめていきます!
本記事の構成
- Stored Properties(ストアド・プロパティ)
- Lazy Stored Propertiesz(レイジー・ストアドプロパティ)
- Computed Properties(コンピューテッド・プロパティ)
- Property Observers(プロパティオブザーバー)
- Property Wrapper(プロパティラッパー)
- Type Property
値を保有する ストアド・プロパティ
ストアド(Stored)は、「保有した」という意味です。
値には定数と変数があります。
- letキーワード:定数を保持
- varキーワード:変数の保持
StorerdPropertiesstruct StoredProperity{ let test: Int var test2: String }上記のようなものをよく見るパターンですね。
このストアド・プロパティを細分化するとレイジー・ストアドプロパティなるものがあります。アクセスされるまで初期化を遅延させる レイジー・ストアドプロパティ
- 初期値が、外部要因に依存している場合(インスタンスの初期化が完了するまで値がわからない)
- 初期値に不必要(or 必要になるまで実行しなくて良い)で複雑な計算量が多いセットアップをする場合
などで役に立ちます!
lazy var ~
のようにlazy修飾子をつけます。
再代入ができないletキーワードにはもちろん使えません。lazyStoredPropertyclass data{ var fileName = "data.txt" } class getData{ lazy var importer = data() var data = [String]() } let lazyStoredProperty = getData() //ここでは、まだimporterは初期化されない print(lazyStoredProperty.importer) //ここで初めてimporterにアクセスして初期化が行われる値の変化を観察・応答する プロパティ・オブザーバ
レイジー以外のストアド・プロパティを対象に追加できます。
プロパティ・オブザーバを追加することで、値の変化前・変化後それぞれのタイミングで処理応答をすることができます。具体的には、
willSet→値が格納される直前に呼び出されます。新しい値はデフォルトでnewValue
didSet→新しい値が格納された直後に呼び出されます。古い値はデフォルトでoldValueclass test{ var A:Int = 0 { willSet(newValue){ print("値が\(newValue)に更新されます。") } didSet { print("\(A)が新しい値で、\(oldValue)が古い値です。") } } }値を保有しない計算型 コンピューテッド・プロパティ
コンピューテッド(computed)は、「計算された」という意味です。
ゲッター/セッターを用いて、他のプロパティーの値から間接的に値を取得・設定する。ゲッター・セッターについて簡単に記述すると、
- ゲッター
- 他のプロパティの値を元に処理をし、自身のプロパティの値を取得する(処理が単一の場合returnを省略できる)
- セッター
- 自身の新しい値(デフォルトでは、「newValue」)を元に処理をし、他のプロパティの値を更新する
基本的にゲッターは必須だが、セッターは任意です。
セッターがない場合は、読み取り専用コンピューテッド・プロパティと呼ばれます。class computedProperty { var name = "田中さん" var circumstance = "田中さんはget待ちだ" var GetSet: String { get { if name == "田中さん"{ return "田中さんをGetした" } set { if newValue == "田中さんをGetした"{ circumstance = "「田中さんはGetされた」とsetされた" } }複数のプロパティの処理を管理する プロパティ・ラッパー
プロパティラッパーは、一言でういとプロパティに関する処理を他の型に移譲するものです。
このプロパティラッパーをつかうことによって、個別の実装ケースに対してハードコーディングする必要がなくなります。プロパティラッパーの使い方
- Property Wrapper にしたい型に
@propertyWrapper
をつける- Property Wrapper 型には
wrappedValue
というインスタンスプロパティを持つ- Property Wrapper を使用したい他の型のプロパティの頭に
@propertyWrapperの型名
をつけるプロパティラッパーを使った場合
UsingPropertyWrapperimport UIKit var str = "Hello, playground" @propertyWrapper struct TwelveOrLess { private var number: Int init(){self.number = 0} var wrappedValue:Int { //このwrappedValueが get {return number} set {number = min(newValue, 12)} // 上限値を12とする } } struct SmallRectangle { @TwelveOrLess var height: Int @TwelveOrLess var weight: Int } var rectangle = SmallRectangle() print(rectangle.height) // 出力:0 rectangle.height = 10 print(rectangle.height) //出力:10 rectangle.height = 14 print(rectangle.height) //出力:12プロパティラッパーを使わなかった場合
NotUsingPropertyWrapperimport UIKit var str = "Hello, playground" struct TwelveOrLess { private var number: Int init(){self.number = 0} var wrappedValue:Int { get {return number} set {number = min(newValue, 12)} // 上限値を12とする } } struct SmallRectangle { private var _height = TwelveOrLess() private var _weight = TwelveOrLess() var height: Int{ get {return _height.wrappedValue} set {_height.wrappedValue = newValue} } var weight:Int { get {return _weight.wrappedValue} set {_weight.wrappedValue = newValue} } } var rectangle = SmallRectangle() print(rectangle.height) // 出力:0 rectangle.height = 10 print(rectangle.height) //出力:10 rectangle.height = 14 print(rectangle.height) //出力:12このように、複数のプロパティに対して同様な処理を行いたい場合などでは、コードをすっきりさせることができます!
今回記事で触れた部分はごく一部に過ぎず、プロパティラッパーはまだまだ奥が深いです。
もっと詳しく勉強したい方はプロポーザルを見ることをお勧めします!
プロパティラッパーのプロポーザル型自身に紐づく タイププロパティ(スタティックプロパティ・クラスプロパティ)
インスタンスプロパティ・スタティックプロパティ・クラスプロパティ
- インスタンスプロパティ:インスタンスのプロパティにアクセス
- スタティックプロパティ:
staticキーワード
を用いることで、その型自身とプロパティを紐付ける。サブクラスでオーバーライドは不可能- クラスプロパティ:
classキーワード
を用いることで、クラス自身に紐付ける。サブクラスでオーバーライドが可能instancePropertystruct staticProperty { var name: String = "Aさん" // インスタンスプロパティ static var name2:String = "Bさん" //スタティックプロパティ } let staticPropertyInstance = A() print(staticPropertyInstance.name) //出力:Aさん print(staticProperty.name) //エラー print(staticPropertyInstance.name2) //エラー print(staticProperty.name2) //出力:Bさん class classProperty { var name:String = "Cさん" //インスタンスプロパティ class var name2:String = "ClassCさん" //クラスプロパティ } let classPropertyInstance = classProperty() print(classPropertyInstance.name) //Cさん print(classProperty.name) //エラー print(print(classPropertyInstance.name2) //エラー print(classPropertyInstance.name2) //ClassCさん
- 投稿日:2020-05-17T07:18:09+09:00
アニメ画像の昼/夜認識システムの作成:(3 / 3) それをもとに時間帯に合わせてmacOSの壁紙を変更する (Vision Framework)
本シリーズの記事一覧:
1.「Core ML」モデルを「Create ML」で既存のラベル付けされたアニメ画像を入力として用いてトレーニングする。
2. そのモデルと「Vision」フレームワークを用いて新規画像からラベルを取得する。
3. (本記事)それをもとに時間帯に合わせてmacOSの壁紙を変更する意図:
Mac OSの壁紙として、時刻に合わせてさまざまなアニメ画像を設定するアプリの開発。
機械学習を使用して、アニメ画像に自動的に昼または夜としてのマーク付けを行う。
アニメ画像から昼夜の状態を認識する必要があるため、機械学習を取り入れます。
このアプリは、例えば日中には昼の場面のさまざまなアニメ画像を壁紙として設定します。そして夜間には、夜の場面のさまざまなアニメ画像を壁紙に設定します。本記事
本稿では、壁紙を時間帯に合うアニメ画像に自動的に変えてくれるmacOS用のアプリケーションを開発する方法を説明します。
本記事の前項で、アニメ画像を入力すると「昼間」または「夜間」のラベルを出力できる機械学習モデルをすでに作成しました。また、入力された画像に対するラベルを取得するSwiftコードの書き方も学習しました。
フロー
- ユーザーが多くのアニメ画像が保存されたフォルダーを選択します。
- プログラムが機械学習を用いて各画像に対してラベル(昼間または夜間)を割り当てます。
- プログラムが、ランダムな画像を壁紙として設定するために毎時間繰り返し呼び出されるタイマーを開始します。システム時間を確認し、昼間であれば昼間の画像をランダムに選択し、夜間であれば夜間の画像をランダムに選択します。
では始めましょう
フォルダーの選択とファイルの読み込み
前記事のデモアプリケーションで示した通り、ユーザーがファイルを選択できるようにするには
NSOpenPanel
を使います。前回は画像を一枚だけ選択しましたが、今回は、多くの画像が保存されているフォルダーを選択できるように変更します。@IBAction func browseFile(sender: AnyObject) { let openPanel = NSOpenPanel(); openPanel.title = "Select a file" openPanel.canChooseDirectories = true //フォルダ openPanel.canChooseFiles = false //ファイル openPanel.allowsMultipleSelection = false openPanel.allowedFileTypes = ["jpg", "png", "jpeg"] openPanel.begin { (result) -> Void in if(result == NSApplication.ModalResponse.OK){ let path = openPanel.url!.path self.processFiles(path: path) } } }関数
processFiles
において、まず当該フォルダーに含まれる全てのファイルを取得し、その後各ファイルに対して機械学習関数detectScene
を呼び出します。func processFiles(path: String){ let fm = FileManager.default do { let items = try fm.contentsOfDirectory(atPath: path) for item in items { let completePath = path + "/" + item if let image = CIImage(contentsOf: URL(fileURLWithPath: completePath)) { //これは画像ファイルです imageCount += 1 detectScene(image: image, path: completePath) } } } catch { // ディレクトリの読み取りに失敗しました } }機械学習 (Vision Framework)
まず、昼間の画像群へのパスと夜間の画像群へのパスを示す2つの変数を設定しましょう。
var dayImages = [String]() var nightImages = [String]() var imageCount = 0これで、本記事の第2部で行ったように、機械学習による認識コードを書く準備が整いました。
func detectScene(image: CIImage, path: String) { // MLモデルを読み込む guard let model = try? VNCoreMLModel(for: モデルファイルの名前().model) else { fatalError() } // Visionリクエストを作成する let request = VNCoreMLRequest(model: model) { [weak self] request, error in guard let results = request.results as? [VNClassificationObservation], let topResult = results.first else { fatalError("予期しない結果") } let detectedResult = topResult.identifier if detectedResult == "昼間"{ self!.dayImages.append(path) } else if detectedResult == "夜"{ self!.nightImages.append(path) } //Check if the progress finished if (processedCount == self!.imageCount){ self!.finishedML() } } let handler = VNImageRequestHandler(ciImage: image) DispatchQueue.global(qos: .userInteractive).async { do { try handler.perform([request]) } catch { print(error) } } }毎時間の壁紙変更
まず、タイマー変数を作成します:
var wallpaperTimer: Timer!タイマーのスケジュールを設定します
wallpaperTimer = Timer.scheduledTimer(withTimeInterval: 3600, repeats: true) { (timer) in self.changeWallPaper() }そして壁紙を3600秒(60秒*60分)ごとに変更します。
func changeWallPaper(){ let now = Date() let calender = Calendar.current let hour = calender.component(Calendar.Component.hour, from: now) //Day if 8 <= hour && hour <= 18{ if let random = self.dayImgAry.randomElement() { self.setWallpaper(wallpaperPath: random) } } else { if let random = self.nightImgAry.randomElement() { self.setWallpaper(wallpaperPath: random) } } }func setWallpaper(wallpaperPath: String){ let sharedWorkspace = NSWorkspace.shared let screens = NSScreen.screens let wallpaperUrl = URL(fileURLWithPath: wallpaperPath) var options = [NSWorkspace.DesktopImageOptionKey: Any]() options[.imageScaling] = NSImageScaling.scaleProportionallyUpOrDown.rawValue options[.allowClipping] = true for screen in screens{ do { try sharedWorkspace.setDesktopImageURL(wallpaperUrl, for: screen, options: options) } catch { print(error) } } }これで完成です!時間帯に応じて壁紙を変えるアプリが出来上がりました!
iOS開発、Swift、プログラミングに関することなら何でも、遠慮なくTwitterで質問してください。
@MaShunzhe
- 投稿日:2020-05-17T07:18:09+09:00
アニメ画像の昼/夜認識システムの作成:(3 / 3) それをもとに時間帯に合わせてmacOSの壁紙を変更する
本シリーズの記事一覧:
1.「Core ML」モデルを「Create ML」で既存のラベル付けされたアニメ画像を入力として用いてトレーニングする。
2. そのモデルと「Vision」フレームワークを用いて新規画像からラベルを取得する。
3. (本記事)それをもとに時間帯に合わせてmacOSの壁紙を変更する意図:
Mac OSの壁紙として、時刻に合わせてさまざまなアニメ画像を設定するアプリの開発。
機械学習を使用して、アニメ画像に自動的に昼または夜としてのマーク付けを行う。
アニメ画像から昼夜の状態を認識する必要があるため、機械学習を取り入れます。
このアプリは、例えば日中には昼の場面のさまざまなアニメ画像を壁紙として設定します。そして夜間には、夜の場面のさまざまなアニメ画像を壁紙に設定します。本記事
本稿では、壁紙を時間帯に合うアニメ画像に自動的に変えてくれるmacOS用のアプリケーションを開発する方法を説明します。
本記事の前項で、アニメ画像を入力すると「昼間」または「夜間」のラベルを出力できる機械学習モデルをすでに作成しました。また、入力された画像に対するラベルを取得するSwiftコードの書き方も学習しました。
フロー
- ユーザーが多くのアニメ画像が保存されたフォルダーを選択します。
- プログラムが機械学習を用いて各画像に対してラベル(昼間または夜間)を割り当てます。
- プログラムが、ランダムな画像を壁紙として設定するために毎時間繰り返し呼び出されるタイマーを開始します。システム時間を確認し、昼間であれば昼間の画像をランダムに選択し、夜間であれば夜間の画像をランダムに選択します。
では始めましょう
フォルダーの選択とファイルの読み込み
前記事のデモアプリケーションで示した通り、ユーザーがファイルを選択できるようにするには
NSOpenPanel
を使います。前回は画像を一枚だけ選択しましたが、今回は、多くの画像が保存されているフォルダーを選択できるように変更します。@IBAction func browseFile(sender: AnyObject) { let openPanel = NSOpenPanel(); openPanel.title = "Select a file" openPanel.canChooseDirectories = true //フォルダ openPanel.canChooseFiles = false //ファイル openPanel.allowsMultipleSelection = false openPanel.allowedFileTypes = ["jpg", "png", "jpeg"] openPanel.begin { (result) -> Void in if(result == NSApplication.ModalResponse.OK){ let path = openPanel.url!.path self.processFiles(path: path) } } }関数
processFiles
において、まず当該フォルダーに含まれる全てのファイルを取得し、その後各ファイルに対して機械学習関数detectScene
を呼び出します。func processFiles(path: String){ let fm = FileManager.default do { let items = try fm.contentsOfDirectory(atPath: path) for item in items { let completePath = path + "/" + item if let image = CIImage(contentsOf: URL(fileURLWithPath: completePath)) { //これは画像ファイルです imageCount += 1 detectScene(image: image, path: completePath) } } } catch { // ディレクトリの読み取りに失敗しました } }機械学習 (Vision Framework)
まず、昼間の画像群へのパスと夜間の画像群へのパスを示す2つの変数を設定しましょう。
var dayImages = [String]() var nightImages = [String]() var imageCount = 0これで、本記事の第2部で行ったように、機械学習による認識コードを書く準備が整いました。
func detectScene(image: CIImage, path: String) { // MLモデルを読み込む guard let model = try? VNCoreMLModel(for: モデルファイルの名前().model) else { fatalError() } // Visionリクエストを作成する let request = VNCoreMLRequest(model: model) { [weak self] request, error in guard let results = request.results as? [VNClassificationObservation], let topResult = results.first else { fatalError("予期しない結果") } let detectedResult = topResult.identifier if detectedResult == "昼間"{ self!.dayImages.append(path) } else if detectedResult == "夜"{ self!.nightImages.append(path) } //Check if the progress finished if (processedCount == self!.imageCount){ self!.finishedML() } } let handler = VNImageRequestHandler(ciImage: image) DispatchQueue.global(qos: .userInteractive).async { do { try handler.perform([request]) } catch { print(error) } } }毎時間の壁紙変更
まず、タイマー変数を作成します:
var wallpaperTimer: Timer!タイマーのスケジュールを設定します
wallpaperTimer = Timer.scheduledTimer(withTimeInterval: 3600, repeats: true) { (timer) in self.changeWallPaper() }そして壁紙を3600秒(60秒*60分)ごとに変更します。
func changeWallPaper(){ let now = Date() let calender = Calendar.current let hour = calender.component(Calendar.Component.hour, from: now) //Day if 8 <= hour && hour <= 18{ if let random = self.dayImgAry.randomElement() { self.setWallpaper(wallpaperPath: random) } } else { if let random = self.nightImgAry.randomElement() { self.setWallpaper(wallpaperPath: random) } } }func setWallpaper(wallpaperPath: String){ let sharedWorkspace = NSWorkspace.shared let screens = NSScreen.screens let wallpaperUrl = URL(fileURLWithPath: wallpaperPath) var options = [NSWorkspace.DesktopImageOptionKey: Any]() options[.imageScaling] = NSImageScaling.scaleProportionallyUpOrDown.rawValue options[.allowClipping] = true for screen in screens{ do { try sharedWorkspace.setDesktopImageURL(wallpaperUrl, for: screen, options: options) } catch { print(error) } } }これで完成です!時間帯に応じて壁紙を変えるアプリが出来上がりました!
iOS開発、Swift、プログラミングに関することなら何でも、遠慮なくTwitterで質問してください。
@MaShunzhe
- 投稿日: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: (メールアドレス)」の項目が出てくるのでタップし、「〜を信頼」をタップ、「信頼」ボタンをタップする
- ホーム画面に戻り、作成したアプリのアイコンをタップして動くことを確認する
- 投稿日:2020-05-17T03:17:27+09:00
[xcode11][swift] storyboardで作る scrollView 備忘録
つまづいたので備忘録
storyboardの配置
1,ViewにScrollViewを設置しオートレイアウト設定
2,Exit下にScrollView内に表示させたいViewを設置 高さを600に設定
3,今回はStackViewを使用 オートレイアウトはbottom以外の3点を設定
4,StackView内のViewを設置 高さを500に設定
画面View範囲外まで及ばないとスクロールされない
5,わかりやすいように、背景色を変更コード
追記
Exit下のViewの名前をcontentViewに変更しました
名前の変更はコードには影響しませんが、下のコードでOutletを接続する際の誤解を防ぐためです。ViewController.swiftimport UIKit class ViewController: UIViewController { @IBOutlet weak var scrollView: UIScrollView! @IBOutlet var stackView: UIStackView! @IBOutlet var contentView: UIView! override func viewDidLoad() { super.viewDidLoad() // コンテンツViewを載せる contentView.frame.origin = CGPoint.zero scrollView.addSubview(contentView) self.stackView.layoutIfNeeded() //重要// } override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() // コンテンツの幅を画面に合わせる contentView.frame.size.width = scrollView.frame.width // コンテンツの高さを調整する contentView.frame.size.height = stackView.frame.height // スクロール範囲をコンテンツに合わせる scrollView.contentSize = contentView.frame.size } }コンテンツの幅のサイズなどを調整すると横のスライドはなくすことが出来ます
以上です。