20200517のSwiftに関する記事は12件です。

SwiftでCore MIDI

Untitled.mov.gif

週末急に思い立ってiPadでMIDIピアノの入力に反応するアプリを作ってみようと思ったのですが、Core MIDI周りの情報はObjective-C止まりでアップデートされていないものが多く、ましてSwiftとなると英語でもほとんど情報がないので結構困りました(そもそもAppleがMIDI関連の情報発信についてやる気ない)。ニーズが果たしてどれほどあるのかわかりませんが、一応メモとして残しておきます。

また、今回作成したバイナリはCatalyst経由でmacOS上でも動いたので、macの場合もSwiftを使う限りは同じやり方で行けると思います。

目標

MIDIキーボードから入力されたイベントに従ってiOSアプリ上で絵を出す。
iPadからMIDIピアノを鳴らすなどの送信系は今回扱っていません。

流れ

Swiftと言ってもCore MIDIをつつく以上、大きな流れはObjective-Cの場合と変わりません。下記のサイトは10年以上前のものですが、大いに参考にさせていただきました。

やることをまとめると、

  1. MIDIデバイスの一覧を取得
  2. 使用したいデバイスに対するMIDIClientを作成
  3. MIDIClientで使うMIDIInputPort(データの受け口)を作成
  4. 使用したいデバイスのMIDIEndpointと、MIDIInputPortを接続
  5. 飛んでくるMIDIメッセージの処理

という感じです。

midi.png

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を設定できるようにしておくと楽かもしれません。

また、このコールバックですがメインスレッドとは別に呼ばれるっぽいので、UIやグラフィックに変更を加える際は、DispatchQueueなどを介してメインスレッドで実行されるように書いてあげるのが安全だと思われます。

まとめ

これらの処理をクラスにまとめると以下のような感じになります。

MIDIManager.swift
import 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.swift
class 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のサポートちゃんと続けていってくれるのだろうか...。

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

関数(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

となる。

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

変数(Swift)

この記事について

Swiftの勉強をしています。
勉強の過程で得た知識を、忘れないようまとめておくのと共に、誰かの助けになればと思います。

Swiftにおける変数

Swiftでの変数の定義は

var 変数名:データ型 = 初期値

定数の場合は

let 定数名:データ型 = 初期値

で行う。

var(変数)の場合は、後から代入などで値を変更できるが、
let(定数)の場合は、最初に入れた値を変更できない。

データ型を省略して、

var 変数名 = 初期値
let 定数名 = 初期値

でも大丈夫。

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

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

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

【iOS】未経験者からのiOSアプリ開発入門

はじめに

はじめまして。
フリーランスエンジニアとして稼働しているなすびといいます。
プログラム未経験者にiOSアプリ開発を教える機会があったのでついでにiOSアプリ開発手順についてまとめています。

一応プログラムを全く触ったことがないという人に向け、なるべく細かく説明していますがわかりにくい部分があればご指摘いただけると幸いです。

Xcodeインストールからレイアウト、Swiftの処理、APIの実行等を投稿していきます。
付録としてプログラム言語の基礎文法などもまとめていければと思っています。

本投稿は各記事のアジェンダです。

アジェンダ

iOSアプリ開発入門

付録

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

【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)!

初心者視点で、機能だけに絞って一連の流れを記述しています。
訂正等あればぜひご指摘ください。

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

【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配置は以下のようにします。
スクリーンショット 2020-05-16 21.20.48.png

これならパッと見て直感的にどんな画面が表示されるのかわかるのではないでしょうか?
というか実際特に処理を書いていなければこのまま画面に表示されます。
Interface Builderを使用すればマウス操作でこのようにUIを作成することが可能となります。

余談ですがInterface Builderも内部的にはXMLと呼ばれるHTMLと似たような形式で扱っています。
「直感的に指定したものをXMLに変換してくれるツール」というように認識しておけばいいかと思います。

ボタンを配置する

それでは実際にUIを配置していきましょう。
Interface BuilderはStoryboardというファイルを使用します。
まず前回作成したプロジェクトを開いてください。
左のファイル一覧にMain.storyboardを開きます。
スクリーンショット 2020-05-16 22.39.39.png

次にボタンを配置します。
右上の[+]ボタンをクリックしてください。クリックすると配置できるUIのリストが表示されます。
[Button]を選択し、白い画面にドラッグしてください。
画面の上に[Button]というUIが配置されていればOKです。
スクリーンショット 2020-05-17 1.32.45.png
スクリーンショット 2020-05-17 1.37.17.png

これでボタンが配置されました。
一度シミュレータで実行して確認してみましょう。
シミュレータの画面にもボタンが追加されているかと思います。
またこのボタンをタップすると色が薄くなりボタンとして機能していることがわかります。
スクリーンショット 2020-05-17 1.38.57.png

このようにして自由にUIを配置してアプリの見た目を作っていきます。

ボタンの色を変える

ボタンを配置することはできましたが、ユーザから見るとボタンをタップするまではこれがボタンなのかテキストなのか一見するだけではわかりません。

アプリのUI設計する上で重要なのはユーザが直観的に操作できることです。
どうすればいい感じのデザインになるのかというのは突き詰めると難しいところではありますが
AppleからHuman Interface Guidelinesとしてある程度の指針が定められています。
実際にリリースするアプリを作る際はこのあたりを参考にしてください。

話は若干逸れましたがボタンらしく見せるための一番簡単な方法は色を付けてあげることです。

それでは実際に色を付けてあげましょう。
Main.storyboardを開いてください。

Storyboardの左側を確認してください。
[View Controller Scene]というものがあり、階層構造になっていることがわかります。
ここにはこの画面に配置したUIが表示されていきます。
前項で追加したボタンも一番下の階層に含まれています。

次にStoryboardの右側を確認してください。
こちらにはInspectorと呼ばれるメニューが表示されています。
このInspectorは配置したUIに対して細かく設定をするためのものです。

下図のようにボタンを選択した状態でInspector上部右から3つ目のタブAttributes Inspectorを選択してください。(正確な名前はわかりません、適当に名付けています)
Attributes Inspectorではボタンの色やフォントの色、サイズ、種類などボタン自体の見た目に関する設定をすることができます。
スクリーンショット 2020-05-17 14.24.46.png

背景色を指定する

Attributes Inspectorの設定の下の方に[Background]というものがあります。
これはボタンの背景色を指定する設定です。
初期状態ではDefaultとなっていますがボタンのデフォルト色は無色透明です。
そのため最初に配置したときは色がついていないというわけです。
スクリーンショット 2020-05-17 15.03.15.png

ではこちらの色を変更してみます。
[Background]のDefaultとなっている部分あたりを選択してください。
選択するとプルダウン形式で色のメニューが表示されます。
真ん中あたりに表示されているDark Text ColorなどはXcodeが標準で提供している色です。
こちらを選択しても色を変えることはできます。
今回はせっかくなので自分で色を調整します。
プルダウンメニューの一番下の[Custom...]を選択してください。
スクリーンショット 2020-05-17 15.02.46.png

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)
スクリーンショット 2020-05-17 15.12.24.png

Attributes Inspectorの[Background]の項目に指定の色が表示されていれば成功です。
実行してシミュレータで確認してみてください。
さっきよりはボタンらしく見えるようになったのではないでしょうか?

文字色を変更する

次は文字色を変更しましょう。基本的には背景色変更の手順と同様です。
Attributes Inspectorから[Text Color]の項目を選択します。
あとは先程の手順同様に好きな色を設定します。
スクリーンショット 2020-05-17 16.09.26.png

最後に

今回はInterface Builderを使ったUIの作成方法や色の指定方法について説明しました。
これ以外にも細かい設定をしたり、プログラムから状態に応じて変更したりなど覚えることは多くあります。
iOS開発が初めての人はとっつきづらい部分もあるかもしれませんが慣れてしまえば便利だと感じるようになるかと思います。

1つづつ消化していきましょう。

以上で今回の説明は終わります。
次回はUIのサイズや配置の設定の仕方について説明します。
レイアウト編2:https://qiita.com/euJcIKfcqwnzDui/items/31f6b11afb317275f684

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

【Swift】プロパティ総まとめ~公式ドキュメントをもとに全部まとめてみた~

プロパティ

そもそもプロパティとは、なんぞやというところから

classやstruct,enumでよく見るこいつです。

class
class{
  let men: String = "男"
}

このように、プロパティは型(クラス、構造体、列挙型)を構成する要素の一つで、型もしくは型のインスタンスに紐づいた値を指します。
(ちなみに、型を構成する他の要素には、メソッド・イニシャライザ・ネスト・サブスクリプトなどがあります。)

本記事では、プロパティの公式ドキュメントの内容を網羅してまとめていきます!

本記事の構成

  • Stored Properties(ストアド・プロパティ)
    • Lazy Stored Propertiesz(レイジー・ストアドプロパティ)
  • Computed Properties(コンピューテッド・プロパティ)
  • Property Observers(プロパティオブザーバー)
  • Property Wrapper(プロパティラッパー)
  • Type Property

値を保有する ストアド・プロパティ

ストアド(Stored)は、「保有した」という意味です。
値には定数と変数があります。

  • letキーワード:定数を保持
  • varキーワード:変数の保持
StorerdProperties
struct StoredProperity{
  let test: Int 
  var test2: String
}

上記のようなものをよく見るパターンですね。
このストアド・プロパティを細分化するとレイジー・ストアドプロパティなるものがあります。

アクセスされるまで初期化を遅延させる レイジー・ストアドプロパティ

  • 初期値が、外部要因に依存している場合(インスタンスの初期化が完了するまで値がわからない)
  • 初期値に不必要(or 必要になるまで実行しなくて良い)で複雑な計算量が多いセットアップをする場合

などで役に立ちます!
lazy var ~のようにlazy修飾子をつけます。
再代入ができないletキーワードにはもちろん使えません。

lazyStoredProperty
class 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→新しい値が格納された直後に呼び出されます。古い値はデフォルトでoldValue

class 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の型名をつける

プロパティラッパーを使った場合

UsingPropertyWrapper
import 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

プロパティラッパーを使わなかった場合

NotUsingPropertyWrapper
import 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キーワードを用いることで、クラス自身に紐付ける。サブクラスでオーバーライドが可能
instanceProperty
struct 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さん

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

アニメ画像の昼/夜認識システムの作成:(3 / 3) それをもとに時間帯に合わせてmacOSの壁紙を変更する (Vision Framework)

本シリーズの記事一覧:

1.「Core ML」モデルを「Create ML」で既存のラベル付けされたアニメ画像を入力として用いてトレーニングする。
2. そのモデルと「Vision」フレームワークを用いて新規画像からラベルを取得する。
3. (本記事)それをもとに時間帯に合わせてmacOSの壁紙を変更する

意図:

Mac OSの壁紙として、時刻に合わせてさまざまなアニメ画像を設定するアプリの開発。

機械学習を使用して、アニメ画像に自動的に昼または夜としてのマーク付けを行う。

アニメ画像から昼夜の状態を認識する必要があるため、機械学習を取り入れます。
このアプリは、例えば日中には昼の場面のさまざまなアニメ画像を壁紙として設定します。そして夜間には、夜の場面のさまざまなアニメ画像を壁紙に設定します。

本記事

本稿では、壁紙を時間帯に合うアニメ画像に自動的に変えてくれるmacOS用のアプリケーションを開発する方法を説明します。

本記事の前項で、アニメ画像を入力すると「昼間」または「夜間」のラベルを出力できる機械学習モデルをすでに作成しました。また、入力された画像に対するラベルを取得するSwiftコードの書き方も学習しました。

フロー

  1. ユーザーが多くのアニメ画像が保存されたフォルダーを選択します。
  2. プログラムが機械学習を用いて各画像に対してラベル(昼間または夜間)を割り当てます。
  3. プログラムが、ランダムな画像を壁紙として設定するために毎時間繰り返し呼び出されるタイマーを開始します。システム時間を確認し、昼間であれば昼間の画像をランダムに選択し、夜間であれば夜間の画像をランダムに選択します。

では始めましょう

フォルダーの選択とファイルの読み込み

前記事のデモアプリケーションで示した通り、ユーザーがファイルを選択できるようにするには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)
        }
    }
}

これで完成です!時間帯に応じて壁紙を変えるアプリが出来上がりました!

:sunny:

iOS開発、Swift、プログラミングに関することなら何でも、遠慮なくTwitterで質問してください。
@MaShunzhe

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

アニメ画像の昼/夜認識システムの作成:(3 / 3) それをもとに時間帯に合わせてmacOSの壁紙を変更する

本シリーズの記事一覧:

1.「Core ML」モデルを「Create ML」で既存のラベル付けされたアニメ画像を入力として用いてトレーニングする。
2. そのモデルと「Vision」フレームワークを用いて新規画像からラベルを取得する。
3. (本記事)それをもとに時間帯に合わせてmacOSの壁紙を変更する

意図:

Mac OSの壁紙として、時刻に合わせてさまざまなアニメ画像を設定するアプリの開発。

機械学習を使用して、アニメ画像に自動的に昼または夜としてのマーク付けを行う。

アニメ画像から昼夜の状態を認識する必要があるため、機械学習を取り入れます。
このアプリは、例えば日中には昼の場面のさまざまなアニメ画像を壁紙として設定します。そして夜間には、夜の場面のさまざまなアニメ画像を壁紙に設定します。

本記事

本稿では、壁紙を時間帯に合うアニメ画像に自動的に変えてくれるmacOS用のアプリケーションを開発する方法を説明します。

本記事の前項で、アニメ画像を入力すると「昼間」または「夜間」のラベルを出力できる機械学習モデルをすでに作成しました。また、入力された画像に対するラベルを取得するSwiftコードの書き方も学習しました。

フロー

  1. ユーザーが多くのアニメ画像が保存されたフォルダーを選択します。
  2. プログラムが機械学習を用いて各画像に対してラベル(昼間または夜間)を割り当てます。
  3. プログラムが、ランダムな画像を壁紙として設定するために毎時間繰り返し呼び出されるタイマーを開始します。システム時間を確認し、昼間であれば昼間の画像をランダムに選択し、夜間であれば夜間の画像をランダムに選択します。

では始めましょう

フォルダーの選択とファイルの読み込み

前記事のデモアプリケーションで示した通り、ユーザーがファイルを選択できるようにするには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)
        }
    }
}

これで完成です!時間帯に応じて壁紙を変えるアプリが出来上がりました!

:sunny:

iOS開発、Swift、プログラミングに関することなら何でも、遠慮なくTwitterで質問してください。
@MaShunzhe

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

子供と作る「おみくじ」アプリ on iPhone

対象者

  • 幼児〜小学校低学年のお子さんと過ごす中で、
  • 在宅勤務等でMacに向かわなければならないのに、
  • 「なんかアプリ入れてよー」とiOSデバイス持ってせがまれる方
  • Mac, iOSデバイス, Xcode, Lightningケーブル が手元にあることは必須
  • ガチのアプリ開発者はもっと上手くやってください

背景と狙い

  • この記事、プロジェクトは、Swiftのコーディングスキルを身につけるとか、開発とは何かといったテーマの学びを目的としていません。
  • スーパーで売られている切り身のマグロが,元は一匹の魚であったと水族館で教えるような… 手元のiPadやiPhoneで動いているアプリが人のアイディアと努力によって組み立てられていることを実感してもらうような目的です。
  • そして最大の目的は、仕事中に構って構って攻撃を受けないよう、興味を満たすか興味をそらすかすることです。

進め方(プロジェクトを成功させるために不可欠)

  1. まず最初に、お子さんに何も話す以前に徹夜でもなんでもして一通り動くようにする
  2. お子さんに必要な素材を作ってもらう
  3. お子さんと一緒にアプリとして組み立てて微調整する
  4. ビルドしてiOSデバイスに入れる

この順序がとても大事で、1ができないまま取り組むと、あなたがXcodeと試行錯誤している間にお子さんは興味を失う可能性が高いです。

sidecar.png

それから、開発中に素材画像やコードを指で指し示すことがあるはずです。愛機の繊細なスクリーンのコーティングを守りたい方は、SideCarでiPadのスクリーンを見せながら作業しましょう…

開発する

進め方と前後しますが、説明として自然な流れでいきます…

素材の用意(進め方の#2)

以下の画像を用意します。

ファイル 必要数 サイズ メモ
アプリのアイコン 5枚 120*120, 152*152, 167*167, 180*180, 1024*1024 ピクセル 様々なスクリーンに対応させるため複数用意するが、大きめの画像1枚を全てに使うこともできる
待ち受け中 1枚 1000*1000 ピクセル 起動直後にお神籤を引く前の状態で表示する画像
お神籤の結果 5枚〜 1000*1000 ピクセル 大吉〜凶 くらいまで、お好きなお神籤結果の分を用意する

こんな感じで用意した。
ファイル名やフォルダ分けはXcodeに反映されないのでお好きにどうぞ。

asset_files.png

アプリの開発(進め方の#1)

0. Xcodeの準備

  • AppStoreから「Xcode」をインストールする(モバイル回線の人は容量に注意)
  • そこそこ時間がかかるのでしばらく待つ
  • インストールされたら、開いて規約の同意やら権限付与やら画面の指示に従って済ませる
  • メニューの「Xcode」→「Preferences...」を開き、「Accounts」タブで自分のApple IDが登録されていることを確認する。無ければ左下の「+」をクリックしてApple IDを追加する。

1. プロジェクトを作成する

Xcodeを開いて、メニューの「File」→「New」→「Project...」を開く
スクリーンショット 2020-05-17 2.03.38.png

「iOS」タブの「Single View App」を選択して、「Next」をクリックする。

new_project.png

「Product Name」にプロジェクトの名前(半角英数にする。日本語は使わない)を入れて、「Team」は自分のApple IDの名前に「(Personal Team)」が付いたものを選ぶ。
「Include Unit Tests」と「Include UI Tests」はチェックを外しておく。
「Next」をクリックして先に進む。

project_settings.png

プロジェクトを保存する場所を聞かれるので、お好きな場所を指定する。

すると、以下の様な「Hello World」がSwiftで書かれた ContentView.swift ファイルが開く。

swift.png

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)

        }

    }
}

そうすると、こんな感じになる。

edited.png

3. 素材を登録する

画面左のファイルのリストで Assets.xcassets を選択する。

assets.png

AppIcon だけが存在するはずなので、「+」をクリックして「New Image Set」を選び、 waiting1 から 5 までの連番ができるように追加を繰り返す。

一通り追加したら、素材をはめ込む。
素材は、ファイルをドラッグ&ドロップしていくだけでOK。

asset.png

お子さんが作った素材をはめ込む前に、仮の画像で試すこと。
幸いなことに、いらすとやさんにお神籤画像があった。

4. ビルドする

Command キーと R キーを同時に押してビルドする。
一応ウィンドウ左上の「▶」ボタンでもできるけれど、ショートカット使う方が格好いいでしょう?

すると、シミュレーター上でアプリが開いた状態になる。

スクリーンショット 2020-05-17 3.03.11.png

「お神籤を引く」を押して、結果が表示されればOK。

スクリーンショット 2020-05-17 3.03.54.png

ここまで、お子さんと取り組む前にできているとスムースなはず。

微調整(進め方の#3)

1. アプリ名を変える

画面左側のリストの1番目、プロジェクト名をクリックする。
「General」タブを開き、「TARGETS」でアプリのアイコンが付いたものを選択(初期状態はプロジェクト名)し、右側の「Display Name」を編集する。

app_name.png

2. ボタンの文言を変える

「お神籤」が読めない歳の子や、お神籤より占いが良いとかあると思うので、以下の様に ContentView.swift の中で文言が書かれている部分のダブルクォーテーションの内側を変更する。

before.png

after.png

ボタンの文言は3箇所に書かれている。

スクリーンショット 2020-05-17 3.09.37.png

3. ボタンの色、形、大きさ を変える

これもやはり ContentView.swift の中に加筆する。

文字の大きさであれば、
Text(buttonLabel) の部分を Text(buttonLabel).font(.largeTitle) と追記したり、色も変えるなら Text(buttonLabel).font(.largeTitle).foregroundColor(Color.white) のようにしてみる。

スクリーンショット 2020-05-17 3.14.02.png

スクリーンショット 2020-05-17 3.14.10.png

ボタンの色は .padding(.all) の後ろに .background(Color.green) を追記することで緑色になる。
更に .cornerRadius(10.0) を追記すればボタンの角が丸くなる。

スクリーンショット 2020-05-17 3.15.51.png

スクリーンショット 2020-05-17 3.16.00.png

4. 結果のバリエーションを増やす

もし結果を5通りより多くする場合、 Assets.xcassets に登録した連番の画像を追加していく。
そして、 ContentView.swift の中で乱数を生成して画像を選ぶ処理で、乱数の生成範囲を広げる。

Int.random(in: 1 ... 5).description と書かれている箇所があるので、 右側の数字を変える。

スクリーンショット 2020-05-17 3.19.01.png

スクリーンショット 2020-05-17 3.19.30.png

一通りいじったら、 Command + R で実行してうまく動くことを確認する。

iOSデバイスへの転送(進め方の#4)

最後に、完成したアプリを実機にインストールする流れとしては…

  1. MacとiOSデバイスをケーブルで接続する。
  2. コンピュータを信頼するかどうかとダイアログが出るので、信頼ボタンを押したりパスコードを入れたりする。
  3. Xcodeのウィンドウ上部に「iPhone SE (2nd generation)」のようにシミュレーターの機種名が表示されている部分をクリックし、最上段に実機の機種名が表示されるはずなので、それを選択する。
  4. 改めて Command + R を押すと、ビルド後にアプリが実機に転送される。
  5. 最初は実機でアプリが起動できないので、実機の「設定」アプリを開き、「一般」→「デバイス管理」を開く。
  6. 「デベロッパAPP」に「Apple Development: (メールアドレス)」の項目が出てくるのでタップし、「〜を信頼」をタップ、「信頼」ボタンをタップする
  7. ホーム画面に戻り、作成したアプリのアイコンをタップして動くことを確認する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[xcode11][swift] storyboardで作る scrollView 備忘録

つまづいたので備忘録

storyboardの配置

1,ViewにScrollViewを設置しオートレイアウト設定
2,Exit下にScrollView内に表示させたいViewを設置 高さを600に設定
3,今回はStackViewを使用 オートレイアウトはbottom以外の3点を設定
4,StackView内のViewを設置 高さを500に設定
画面View範囲外まで及ばないとスクロールされない
5,わかりやすいように、背景色を変更

スクリーンショット 2020-05-17 3.41.15.png

スクリーンショット 2020-05-17 3.41.26.png

コード

追記
Exit下のViewの名前をcontentViewに変更しました
名前の変更はコードには影響しませんが、下のコードでOutletを接続する際の誤解を防ぐためです。

ViewController.swift
import 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
    }
}

ezgif.com-video-to-gif.gif

コンテンツの幅のサイズなどを調整すると横のスライドはなくすことが出来ます

以上です。

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