20190701のiOSに関する記事は8件です。

AutoLayoutで横画面の時だけのレイアウト?できらぁ!

え!? AutoLayoutで横画面の時だけのレイアウトを?

StoryboardでAutoLayoutを設定する際に、縦画面、横画面ではなく、画面の縦(Height)もしくは横(Width)が短い(Compact)か長い(Regular)かどうかで制約を設定していきます。CompactかRegularかは端末によって異なります。

layout.png

  • Storyboardを使わずコードで縦画面、横画面それぞれのレイアウトを書くことはできそうです(なんとなく大変そうですが)

Storyboardの設定方法

実際のStoryboardで設定する時の画面です。詳細は書きませんが、AutoLayoutの制約を縦(Height) or 横(Width)が短い場合(Compact) or 長い場合(Regular) or どちらでも(Any)かで制約を決めていきます。デフォルトは縦も横もAnyの場合ですね。

  • フォントとか色とかも場合を分けて設定できます

image.png

  • 左端に「+」ボタンがある項目は、そのボタンを押すと設定できます。

次に端末ごとの縦横の長さを表にしました。

※同じ解像度のものは省略
※Split Viewの時も省略

縦向き

iPhone

機種 Width Hight
iPhone 4s Compact Regular
iPhone SE Compact Regular
iPhone 8 Compact Regular
iPhone 8 Plus Compact Regular
iPhone XS Compact Regular
iPhone XR Compact Regular
iPhone XS Max Compact Regular

iPad

機種 Width Hight
iPad Pro 9.7 inch Regular Regular
iPad Pro 10.5 inch Regular Regular
iPad Pro 12.9 inch Regular Regular
iPad Pro 11 inch Regular Regular
iPad Pro (3rd) 12.9 inch Regular Regular

横向き

iPhone

機種 Width Hight
iPhone 4s Compact Compact
iPhone SE Compact Compact
iPhone 8 Compact Compact
iPhone 8 Plus Regular Compact
iPhone XS Compact Compact
iPhone XR Regular Compact
iPhone XS Max Regular Compact

iPad

機種 Width Hight
iPad Pro 9.7 inch Regular Regular
iPad Pro 10.5 inch Regular Regular
iPad Pro 12.9 inch Regular Regular
iPad Pro 11 inch Regular Regular
iPad Pro (3rd) 12.9 inch Regular Regular

横向きの時だけのレイアウトはどうするの?

iPhoneの場合

色々パターンはありそうですが、そのうちの1つのパターンとして以下のような設定があります。

  • WidthをAny、HeightをAnyでのAutoLayoutの制約で縦画面を含むデフォルトのレイアウトを作る
  • WidthをAny、HeightをCompactでのAutoLayoutの制約で横画面のレイアウトを作る

iPadの場合

横画面だけってのはできなさそうですね。。。

iPadは縦向きでも横向きでもiPhoneに比べて正方形に近いので、縦画面も横画面も同じレイアウトで作ってね、って感じですかね

iPadだけまた別のレイアウトにしたい場合は、WidthをRegular、HeightをRegularのパターンでレイアウトを作れば良いですね :ok_hand:

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

[python+swift] リアルタイムで信号色を認識するiOSアプリを作った

作ったもの

カメラを向けると、信号が青か赤か判定するアプリ

実機テスト風景↓(めちゃ重い)
信号を赤なら赤線、青なら青線、点滅中は黒線で囲う。
ダウンロード.gif

ざっくり処理の流れ

判定APIをpythonで自作して、swiftで毎秒画像をpostする感じ。
レスポンスは、信号の色と枠線付きの画像を返す。

画像をpostすると、、
red_image.jpeg

aws rekognitionで信号を読み取り、openCVで信号切り取り&色の識別
croped.jpg

信号と同じ色の枠線をPILで引く

drawed.jpg

APIコード

from PIL import Image, ImageDraw
import boto3
import cv2
import numpy as np
import json
import io
import base64
from flask import Flask, request, jsonify, make_response, send_file, Response

app = Flask(__name__)

#信号の色を判定。
def __color_dicision(image):

    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV_FULL)
    h = hsv[:, :, 0]
    s = hsv[:, :, 1]
    v = hsv[:, :, 2]

    mask = np.zeros(h.shape, dtype=np.uint8)
    mask[((45 < h) & (h < 60)) & (s > 150) & (v > 90)] = 60
    mask[((h < 8) | (h > 160)) & (s > 150) & (v > 94)] = 255

    color_list = [j for i in mask.tolist() for j in i]
    if color_list.count(255) > 20:
        return "red"
    elif color_list.count(60) > 20:
        return "blue"
    else:
        return "black"

#信号に枠線を引く
def __draw_border(photo, left_coordinate, top_coordinate, width, height, color_dicision):
    if color_dicision == "red":
        fill = (225, 0, 0)
    if color_dicision == "blue":
        fill = (100, 225, 100)
    if color_dicision == "black":
        fill = (0, 0, 0)

    draw = ImageDraw.Draw(photo)
    draw.line((left_coordinate, top_coordinate, left_coordinate +
               width, top_coordinate), fill=fill, width=8)
    draw.line((left_coordinate, top_coordinate, left_coordinate,
               top_coordinate + height), fill=fill, width=8)
    draw.line((left_coordinate, top_coordinate + height, left_coordinate +
               width, top_coordinate + height), fill=fill, width=8)
    draw.line((left_coordinate + width, top_coordinate, left_coordinate +
               width, top_coordinate + height), fill=fill, width=8)
    return photo


@app.route('/', methods=['GET', 'POST'])
def dicision():
    if request.files['image']:
        image = request.files['image']
    client = boto3.client('rekognition')
    #aws rekognitionへpost
    response = client.detect_labels(Image={'Bytes': image.read()})
    #画面に写っている物体の名前と位置が返ってくる
    for label in response['Labels']:
        if label['Name'] == "Traffic Light" and label['Confidence'] > 50 and label['Instances'] != []:
            res = label['Instances'][0]['BoundingBox']
            image = Image.open(image)
       #なぜか上の処理で画像が90度回転するので、直す。
            image = image.rotate(270, expand=True)
            im_size_width, im_size_height = image.size
            left_coordinate = res["Left"] * im_size_width
            top_coordinate = res["Top"] * im_size_height
            width = res["Width"] * im_size_width
            height = res["Height"] * im_size_height
            #信号のある箇所をくり抜く
            im_crop = image.crop(
                (left_coordinate, top_coordinate, left_coordinate + width, top_coordinate + height))
            im_cv = np.asarray(im_crop)
            color_dicision = __color_dicision(im_cv)
            drawed_image = __draw_border(
                image, left_coordinate, top_coordinate, width, height, color_dicision)

            imgByteArr = io.BytesIO()
            drawed_image.save(imgByteArr, format='JPEG')
            imgByteArr = imgByteArr.getvalue()

            imageStr = base64.b64encode(imgByteArr).decode("utf-8")

            return jsonify({'result': color_dicision, 'image': imageStr})

    print("何もなし")
    return jsonify({'result': 'no_signal', 'image': ''})


if __name__ == '__main__':
    app.run(host='0.0.0.0')

swift サイドは↓を参考に、Alamofire+codableで実装
https://qiita.com/shirahama_x/items/421d0d343d9629e66794

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

iOS Simulatorでタッチポインターを表示させる

iOS Simulator(シミュレーター)でタッチポインターを表示させる方法を共有します。 1

手順

plistにエントリを追加

defaults コマンドでエントリを追加して、シングルタッチ表示を有効にします。

$ defaults write com.apple.iphonesimulator ShowSingleTouches 1

Simulatorアプリを再起動

Simulatorアプリを再起動すると、タッチポインターが表示されるようになります。

以上


  1. Google検索結果にQiitaの記事『iOS Simulatorでタッチポインタを出す方法』を見つけたものの閲覧できない状態になっていたため投稿しました 

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

【Swift】チャットアプリで、iOSアプリ開発入門講座を考えてみた(※随時更新予定)

はじめに

今では、多くのアプリで取り入れられているチャットアプリを用いて、初心者でもiOSアプリ開発ができるように紹介していこうと思います。

※記事全体を通して分からないところ・誤りがあるところあれば、なんでもコメントください。

流れ

  1. 【Swift】初心者でも絶対にできる"Hello World" iOS アプリを作成 ~iOS アプリ開発超入門~
  2. 【Swift】Firebase Realtime Database を用いてチャットアプリを爆速コーディングしてみた。
    ここで少しだけUIをアレンジしたい方はこちら↓↓↓
    【Swift】チャットアプリのUIをちょっとだけ良くしてみた
  3. 【Swift5.0】UserDefaultsでパラメータを保存する~iOSアプリ開発入門~

ソースコードはこちらのGitHubからどうぞ

https://github.com/Tetsukick/Firebase-chat-iOS

エラー一覧

実際に実施して発生したエラーを記載しておきます。

Case1. Could not build Objective-C module 'Firebase'

Screen Shot 2019-07-02 at 11.54.14 AM.png

解決方法

「.xcodeproj」ファイルではなく、「.xcworkspace」ファイルから、Xcodeを開き直してください。
Screen Shot 2019-07-02 at 12.03.16 PM.png

説明

  • xcodeproj
    → メインプロジェクトとサブプロジェクトを管理することができるファイル
  • xcworkspace
    → 複数の同階層のプロジェクトを管理することができるファイル
    ※cocoaPodsを使用して、ライブラリを導入した場合は、同階層にPodsプロジェクトが作成され、そこにライブラリがbuildされているため、「xcworkspace」で開く必要があります。

case2. xcode-select: error: tool ‘xcodebuild’ requires Xcode, but active developer directory ‘/Library/Developer/CommandLineTools’ is a command line tools instance

解決方法

Xcodeのpathに誤りがあります。正しいパスを指定し直してください。

$ sudo xcode-select -s <xcode_folder_path>

例:

$ sudo xcode-select -s /Applications/Xcode.app

tips

複数のXcodeバージョンをインストールしている場合も上記のコマンドで切り替えることができます。
選択されているXcodeをもとにライブラリがインストールされるため、インストール前に以下のコマンドでpathを確認することをおすすめします。

$ xcode-select -print-path

case3. Thread 1: signal SIGABRT

Screen Shot 2019-07-03 at 10.17.49 AM.png

Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<AQI_Chat__.ViewController 0x7fd446813f80> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key textView2.'

解決方法

Main.storyboardファイルを開き、UIとコードの接続を確認します。
Screen Shot 2019-07-03 at 10.19.59 AM.png

Warningの箇所を削除して、必要に応じて、もう一度接続し直します。

tips

UIとコードの接続は、1対1である必要があります。
接続して、コードのみ削除すると、Storyboard上に接続関係だけが残ってしまい、エラーの原因になります。
また、接続させたコードの名前を変更すると関係性がなくなってしまい、こちらもエラーの原因になります。

Q&A

Question1. 「platform :ios, '10.0'」 は何?

iOS10以上を対象としてライブラリをインストールすることを意味します。

変数の名前の変更方法は?

変更したい変数を選択して、右クリックしてください。
以下の Refactor > Rename を選択します。
今回は試しに、textViewchatTextViewに変更します。

Screen Shot 2019-07-03 at 11.22.49 AM.png

Screen Shot 2019-07-03 at 11.24.06 AM.png

Screen Shot 2019-07-03 at 11.24.23 AM.png

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

iPadのShortcutsアプリで映画モードとノートモードをかんたんに切り替えられるようにした

何に困っていたかというと

iPadを使っている時間のほとんどは「Apple Pencilでノートをとっている」か「映画を観ている」の2択。

ノートをとっているときは、画面を暗く、音量を小さくしたい。
映画を観ているときは、画面を明るく、音量を大きくしたい。

スワイプしたら設定画面は出てくるけど、いちいち変えるの面倒くさいなあと。

iOSのShortcutsをつかう!!

iOSにはShortcutsというアプリがあるので、これを使うことにした。

やっていることはかんたんで、モードを選択し、それによって画面の明るさと音量を変えるだけ。

設定で「ウィジェットに追加」にしておくと、ホーム画面を右にスワイプして、このショートカットを呼び出せる。

べんりー。

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

【Swift5.0】UserDefaultsでパラメータを保存する~iOSアプリ開発入門~

はじめに

こちらの記事は、下記の記事のUIを変更していく過程を記載しています。
【Swift】Firebase Realtime Database を用いてチャットアプリを爆速コーディングしてみた。

この記事で分かること

  • UserDefaultsを使用した値の保存方法

スタート時のソースコード

ViewController.swift
import UIKit
import FirebaseDatabase

class ViewController: UIViewController {

    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var nameInputView: UITextField!
    @IBOutlet weak var messageInputView: UITextField!
    @IBOutlet weak var inputViewBottomMargin: NSLayoutConstraint!

    var databaseRef: DatabaseReference!

    override func viewDidLoad() {
        super.viewDidLoad()

        readData()

        addKeyboardShowHideObserver()

    }

    private func readData() {
        databaseRef = Database.database().reference()

        databaseRef.observe(.childAdded, with: { snapshot in
            dump(snapshot)
            if let obj = snapshot.value as? [String : AnyObject], let name = obj["name"] as? String, let message = obj["message"] {
                let currentText = self.textView.text
                self.textView.text = (currentText ?? "") + "\n\(name) : \(message)"
            }
        })
    }

    private func addKeyboardShowHideObserver() {
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
    }

    @IBAction func tappedSendButton(_ sender: Any) {
        view.endEditing(true)

        if let name = nameInputView.text, let message = messageInputView.text {
            let messageData = ["name": name, "message": message]
            databaseRef.childByAutoId().setValue(messageData)

            messageInputView.text = ""
        }
    }

    @objc func keyboardWillShow(_ notification: NSNotification){
        if let userInfo = notification.userInfo, let keyboardFrameInfo = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
            inputViewBottomMargin.constant = keyboardFrameInfo.cgRectValue.height
        }

    }

    @objc func keyboardWillHide(_ notification: NSNotification){
        inputViewBottomMargin.constant = 0
    }

}

1. ViewControllerにTextFieldのイベントを検知する権限を移譲する

ViewController.swiftの末尾に以下を追加。

ViewController.swift
extension ViewController: UITextFieldDelegate {

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        return true
    }
}

ViewDidLoad()内に以下を記載

ViewController.swift
nameInputView.delegate = self
ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()

        readData()

        addKeyboardShowHideObserver()

        nameInputView.delegate = self

    }

これで、UITextFieldが持つイベントをViewControllerで拾うことができるようになりました。

ちなみに、textFieldShouldReturnメソッドはUITextFieldの入力でEnterが押された事を検知するイベントです。

2. UserDefaultsのインスタンスを生成

ViewController.swift
let userDefaults = UserDefaults.standard
ViewController.swift
class ViewController: UIViewController {

    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var nameInputView: UITextField!
    @IBOutlet weak var messageInputView: UITextField!
    @IBOutlet weak var inputViewBottomMargin: NSLayoutConstraint!

    var databaseRef: DatabaseReference!

    let userDefaults = UserDefaults.standard

    override func viewDidLoad() {
        super.viewDidLoad()

        readData()

3. Enterが押されたタイミングでUserDefaultsに値を保存

ViewController.swift
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        guard let inputText = textField.text else {
            return true
        }
        userDefaults.set(inputText, forKey: "name")
        userDefaults.synchronize()

        return true
    }

4. UserDefaultsから値を読み込む

ViewController.swift
    fileprivate func readNameData() -> String {
        return userDefaults.object(forKey: "name") as? String ?? ""
    }

5. 読み込んだ値をUITextFieldに表示

ViewController.swift
nameInputView.text = readNameData()

これで完了!

6. 完成形のソースがこちら

ViewController.swift
import UIKit
import FirebaseDatabase

class ViewController: UIViewController {

    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var nameInputView: UITextField!
    @IBOutlet weak var messageInputView: UITextField!
    @IBOutlet weak var inputViewBottomMargin: NSLayoutConstraint!

    var databaseRef: DatabaseReference!

    let userDefaults = UserDefaults.standard

    override func viewDidLoad() {
        super.viewDidLoad()

        readData()

        addKeyboardShowHideObserver()

        nameInputView.delegate = self
        nameInputView.text = readNameData()

    }

    private func readData() {
        databaseRef = Database.database().reference()

        databaseRef.observe(.childAdded, with: { snapshot in
            dump(snapshot)
            if let obj = snapshot.value as? [String : AnyObject], let name = obj["name"] as? String, let message = obj["message"] {
                let currentText = self.textView.text
                self.textView.text = (currentText ?? "") + "\n\(name) : \(message)"
            }
        })
    }

    private func addKeyboardShowHideObserver() {
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
    }

    @IBAction func tappedSendButton(_ sender: Any) {
        view.endEditing(true)

        if let name = nameInputView.text, let message = messageInputView.text {
            let messageData = ["name": name, "message": message]
            databaseRef.childByAutoId().setValue(messageData)

            messageInputView.text = ""
        }
    }

    @objc func keyboardWillShow(_ notification: NSNotification){
        if let userInfo = notification.userInfo, let keyboardFrameInfo = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
            inputViewBottomMargin.constant = keyboardFrameInfo.cgRectValue.height
        }

    }

    @objc func keyboardWillHide(_ notification: NSNotification){
        inputViewBottomMargin.constant = 0
    }

    fileprivate func readNameData() -> String {
        return userDefaults.object(forKey: "name") as? String ?? ""
    }

}

extension ViewController: UITextFieldDelegate {

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        guard let inputText = textField.text else {
            return true
        }
        userDefaults.set(inputText, forKey: "name")
        userDefaults.synchronize()

        return true
    }
}

7. リファクタリング

  • Keyの定数化
  • 保存処理をメソッド化
  • 保存処理をボタン押下時にも追加
ViewController.swift
import UIKit
import FirebaseDatabase

class ViewController: UIViewController {

    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var nameInputView: UITextField!
    @IBOutlet weak var messageInputView: UITextField!
    @IBOutlet weak var inputViewBottomMargin: NSLayoutConstraint!

    var databaseRef: DatabaseReference!

    let userDefaults = UserDefaults.standard
    fileprivate let nameKey = "name"
    fileprivate let messageKey = "message"

    override func viewDidLoad() {
        super.viewDidLoad()

        readData()

        addKeyboardShowHideObserver()

        nameInputView.delegate = self
        nameInputView.text = readNameData()

    }

    private func readData() {
        databaseRef = Database.database().reference()

        databaseRef.observe(.childAdded, with: { snapshot in
            dump(snapshot)
            if let obj = snapshot.value as? [String : AnyObject], let name = obj[self.nameKey] as? String, let message = obj[self.messageKey] {
                let currentText = self.textView.text
                self.textView.text = (currentText ?? "") + "\n\(name) : \(message)"
            }
        })
    }

    private func addKeyboardShowHideObserver() {
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
    }

    @IBAction func tappedSendButton(_ sender: Any) {
        view.endEditing(true)

        if let name = nameInputView.text, let message = messageInputView.text {
            save(name: name)

            let messageData = [nameKey: name, messageKey: message]
            databaseRef.childByAutoId().setValue(messageData)

            messageInputView.text = ""
        }
    }

    @objc func keyboardWillShow(_ notification: NSNotification){
        if let userInfo = notification.userInfo, let keyboardFrameInfo = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
            inputViewBottomMargin.constant = keyboardFrameInfo.cgRectValue.height
        }

    }

    @objc func keyboardWillHide(_ notification: NSNotification){
        inputViewBottomMargin.constant = 0
    }

    fileprivate func readNameData() -> String {
        return userDefaults.object(forKey: nameKey) as? String ?? ""
    }

    fileprivate func save(name: String) {
        userDefaults.set(name, forKey: nameKey)
        userDefaults.synchronize()
    }

}

extension ViewController: UITextFieldDelegate {

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        guard let inputText = textField.text else {
            return true
        }
        save(name: inputText)

        return true
    }
}

GitHubはこちら

https://github.com/Tetsukick/Firebase-chat-iOS

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

Swiftのコードをすごろくにして、プログラミング初心者がプログラムの動作を体感的に学べるようにした

プゴロク

プログラミング × すごろく

数年前、娘とすごろくで遊んでいて、「すごろくってソースコードに似てるな」と思いました。すごろくのマスには色々な指示が書かれていて、ちょっと複雑なものだと「サイコロを振って 6 が出たら矢印の先のマスに進む。」のような感じです。これはまさに条件分岐です。

すごろく vs コード

すごろくなら幼稚園児でも理解できます。プログラムのコードをすごろくで表現すれば、プログラミング初心者がプログラムの動作を理解する助けになるかもしれないと思いました(ただし、普通のすごろくでは止まったマスに書かれた指示だけを実行しますが、プログラムは通過したすべての行を実行しないといけないので、プログラムをすごろくで表現すると、止まったマスだけでなく通過したマスに書かれた指示もすべて実行するルールですごろくを遊ぶ必要があります)。

たとえば、次のようにすごろくのマスの中にコードと日本語の両方で指示を書けば、コードを読めない人でもコードとその意味を対応付けて学ぶことができます。

コードをすごろくで表現

また、実際にコマを動かしながらすごろくを遊ぶことで、プログラムの動作をトレースし、プログラムがどのように動くかを体感することができます。プログラミング学習の初歩でつまづいてしまったという話をよく聞きますが、すごろくという目に見える形でプログラムの動作を繰り返し体験することで、感覚的に理解できるようになるかもしれません。

そのようなアイデアに基づいて、プログラミング初心者のためのすごろく 『プゴロク』 を作りました。学習効果を考えると手を動かして遊べる物理的な紙のすごろくの方が良さそうですが、レイアウトやデザインを変えながら実験し、コンセプトを検証するにはソフトウェアの方が手軽です。僕のスキルセットを元に検討し、 iOS アプリとしてプゴロクを開発しました

ビジュアルプログラミングとの比較

テキストで書かれたコードは初心者や子供にとってわかりづらいので、よりわかりやすい表現をしようという方向性は、 Scratch 等のビジュアルプログラミング言語と似ています。たとえば、↑で挙げたコードを Scratch で表すと次のようになります。

先のコードを Scratch で表現

Scratch のコードはプゴロク同様にビジュアルに表現されていますが、その表現手法には大きな違いがあります。

プゴロクの一つのマスはコードの 1 行に対応していて、まるでプログラムをステップ実行するかのようにコマを進めます。条件分岐やループなどで行をジャンプする箇所には矢印が描かれ、プログラムがどのような順序で実行されるかを直接的に表します。たとえば、 while 文は次のように表されます。

while 文のプゴロクでの表現

一方で、 Scratch のコードはより構造的です。 Scratch のブロックは構文木のノードに対応しており、コードの構造を直接的に表します。

プログラミングに慣れた人から見ると、どれも本質的には同じことを表しており、表面的な表現手法の違いにしか見えないでしょう。プゴロクや Scratch だとわかるけど↓のコードだとわからないのがなぜか理解に苦しむかもしれません。

if ? != 6 {
    ...
}

しかし、人間は(というと主語が大きいですが、少なくとも僕は)自分が慣れていない表現に接すると、途端に認知能力が落ちてしまいます。たとえば、僕はプログラミングで慣れ親しんだ概念であるにも関わらず、ラムダ計算の記法(↓など)になかなか馴染めませんでした。

(λxy. x − y) 2 3

そう考えると、すごろくという慣れ親しんだ表現は、テキストによるコードはもちろんのこと、 Scratch のようなブロックによる表現と比べても、初心者や子供にとって理解しやすい可能性があります。

もしその仮説が正しいなら、プゴロクの表現をベースにしたビジュアルプログラミング言語を考えてみるのもおもしろそうなテーマです。それはつまり、すごろくを作ることを通してコードを書くということです。

コードを書いてプログラムを作るというとハードルが高そうですが、すごろくを作るのなら子供の遊びです。子供たちは、「スタートに戻る」のようなループも含めて、その挙動をイメージしながらすごろくを作ることができます。もしかすると、すごろくを作るという形式であれば、プログラミングのハードルを下げることができるかもしれません。

コードをすごろくで表現する

一口にコードをすごろくで表現すると言っても検討することは色々あります。

たとえば、 if 文の矢印をどのように引くのかという一点をとっても、他との一貫性を考えて決めなければなりません。

if の矢印の引き方

どのような表現が必要かは言語によっても異なります。たとえば、 Python では } がありません。 Python の while 文をすごろくで表現しようとすると次のようになるでしょう。プゴロクは、現状では Swift しかサポートしていませんが、後述のように他言語のサポートも検討中です。

単純な if だけでなく、 else を伴う if についても考えると矢印の引き方はもっとややこしくなります。 Swift では } else { のように 1 行で書くことが一般的ですが、すごろくとして表現するには }else { の二つのマスに分割するのが良さそうです。

if else の矢印の引き方

また、上記の表現では矢印が同じ階層に複数本並行して走っていますが、一つの階層に最大何本の矢印を描画しなければならないでしょうか。色々なケースを検討した結果、最大 3 本描画できれば良さそうでした(下図の赤丸の箇所)。

ここまで検討してきた表現を用いて、色々な制御構造を表すと次のようになります。 breakcontinue のように階層を飛び越えるジャンプや、 switch, do/try/catch のような多くのジャンプを伴う制御構造も表現することができます。

関数についても、本流のすごろくとは別のすごろく片として表現し、関数呼び出しはそれらをつなぐ矢印で表現できそうです。

複数箇所から同じ関数が呼ばれている場合、関数を抜けて呼び出し元に戻るときに、適切な呼び出し元に戻らなければなりません。上図のように、矢印を色分けすることで呼び出し元との対応がわかりやすくなります。

その他に検討しなければならないのが変数の表現です。変数については、ボードゲームのスコアボードが参考になります。

ボードゲームのスコアボードでは、 0, 1, 2, 3, ... とスコアが書かれたマスが用意されていて、そこにコマを置くことで現在のスコアを表現することが多いです。スコアの増減はコマを動かすことで表します。これはスコアという変数を扱っているのと同じです。

たとえば、 a という Int 型の変数をスコアボード形式で表すと次のようになります。

このような検討の結果、初歩的なプログラムをすごろくとして表現することは現実的に可能だという結論に至りました。

どのようなコードをすごろくにするか

コードをどのようにすごろくで表すかという表現の問題とは別に、どのようなコードをすごろくにするかも重要です。色々なすごろくを作っても良いですが、まず初めに遊んでほしい定番のすごろくが示された方が、ユーザーにとってはわかりやすいと思います。

検討の結果、次のような要素は必須だろうと考えました。

  • 条件分岐
  • 繰り返し(ループ)
  • 変数

さすがにこの三つの要素を欠いては、プログラミングというものをイメージしづらいと思います(一部の関数型言語などは事情が異なりますが、ここでは一般的な手続き型言語を想定しています)。

配列や関数、制御構造のネストなども検討しましたが、理解のための難易度が高まりますし、すごろくとしての表現も複雑になってしまいます。まず初めに遊んで楽しんでもらうには、あまり欲張って詰め込みすぎずに上記の 3 要素にとどめておく方が良さそうです。

これら 3 要素を含んだコードを作るのは簡単ですが、単にそれらの要素を含んでいるだけでなく、意味のあるコードをすごろくにしたいと考えました。コード自体に意味がないと、プゴロクを通してコードの挙動が理解できるようになっても、それが実際に意味のあるプログラムとしてどのように機能するのかイメージできないと思います。プログラミング学習の第一歩としてプゴロクを遊んでもらうことを期待するなら、意味のあるコードをすごろくにすることは欠かせないと思いました。

また、すごろくとして遊ぶからには、単に遊びとして見てもおもしろい方が望ましいです。普通のすごろくと違ってプゴロクでは通過したすべてのマスに書かれた指示を実行するため、どのマスに止まるかというドキドキがありません。単にサイコロを振って進むだけでは、大きな目をたくさん出した人が勝つことになってしまいます。マスの指示でサイコロを振らせるなどして何らかのランダム性を持たせなければなりません。加えて、すごろくとして遊ぶのに適切な時間で終わるように長さも考慮する必要があります。

これらの条件を元に検討した結果、ユークリッドの互除法(二つの自然数の最大公約数を求める最古のアルゴリズム)を実装したコードをすごろくにすることにしました。↓がそのコードです1

var ?: Int { return (1...6).randomElement()! }

var a = ?
print(a)

var b = ? + ?
print(b)

if a < b {
    let t = b
    b = a
    a = t
}

while b != 0 {
    let t = b
    b = a % b
    a = t
}

print(a)

Swift では ? を含む一部の絵文字を識別子として使えます。また、グローバルな Computed Property を作ることができます。そのため、 ↑のコードは実際に実行可能です。

このコードを実行すると、 ab の二つの自然数がランダムに決定・表示され、ユークリッドの互除法でそれらの最大公約数を求めた上で最後に結果が表示されます。これをすごろくとして遊ぶと、サイコロを振って a, b の値を決めるため、その結果次第で後の挙動が変化し、「大きな目をたくさん出した人が勝つ」だけにはなりません。また、 ab がランダムに決定されることで、様々な自然数の組み合わせに対してユークリッドの互除法で最大公約数を求められることも体験してもらえます。

b の初期値はサイコロを 2 回振った合計なので a の初期値より大きくなりやすいですが、運良く ab 以上になれば if{} をスキップできます。すごろくの序盤ではそれが重要そうに見えますが、しかしもっと重要なのがその後の while ループを何回繰り返さなければならないかです。何回ループするかは実際に計算してみないとわかりづらいので、コマを進めながらドキドキすることになります。

お世辞にもこのすごろくがめちゃくちゃおもしろいとは言えませんが、教材としては最低限のゲーム性を確保できたかなと思います。実際に娘と遊んでみましたが、後日「プゴロクやらせてー!」と言ってくる程度には楽しんでくれたようです。

上記のコードをすごろくにしたものの画像は巨大すぎて掲載できないので、興味のある方はアプリをダウンロードして確認してみて下さい。

余談ですが、 Swift で ? を識別子として使えるのは重要です。 Swift と併せて Python と JavaScript を最初からサポートすることを検討していたのですが、 Python や JS で ? が識別子として使えないことをどう扱うか(わかりやすさ優先で実行可能なコードを諦めて ? を使うか、実行可能なことにこだわって dice()saikoro() のような関数を作るか)の結論が出ず、ひとまず Swift だけをサポートすることにしました。

紙のすごろくの作成

まずは iOS アプリとしてプゴロクを作りましたが、教材としての効果を考えると、アプリに加えて物理的な紙のすごろくも作りたいと考えています。

アプリでは、サイコロを振ると自動的にコマが動かされます。それを見てプログラムの動作を観察することはできますが、自分の手でコードの意味を考えながらコマを動かすのとでは得られる理解の深さが違うでしょう。紙のすごろくで遊んでもらってこそ、本当に「プログラムの動作を体感的に学べる」ものになると思います。

しかし、物理的なすごろくとなると印刷にもコストがかかりますし、配布もアプリと同じようにはいきません。そのあたりの問題は棚上げして、ひとまずは紙のプゴロクのプロトタイプ作成に取り組みたいと考えています。

なお、仮に紙のプゴロクが普及しても、アプリ版は正しい遊び方を確認するのに有用なんじゃないかと思います。僕はときどきボードゲームで遊びますが、紙版で遊んだ後でアプリ版をプレイしたときにルールを間違えていたことに気付くという経験を何度かしています。来年( 2020 年)から小学校でのプログラミング必修化が始まりますが、学校の先生はプログラミングの専門家ではありません。プゴロクをアンプラグドなプログラミング教育の手段として活用してもらうことまで考えれば、プログラミングに精通した人がいなくても正しい遊び方を確かめるアプリという方法があることは重要だと思います2

レイアウトとレンダリング

アプリにせよ、紙のすごろくにせよ、何らかの方法ですごろくを描画しなければなりません。プゴロクのマスや矢印の描画を考えると、規則性が強く、同じようなパターンが繰り返し出現するので、イラレ等で作るよりもコードで生成した方が労力が小さそうです。

すごろくの描画は次の要件を満たす必要があります。

  1. マスの中にはテキスト(コードや日本語での指示)が書かれ、その長さに応じてマスの height が伸び縮みする
  2. テキストが短い場合にマスが縦に潰れてしまわないように、マスのアスペクト比( width : height または width / height )が一定値より大きくならないようにする
  3. 各種要素間に適切な余白を指定できる
  4. 任意のマス間で矢印を描画できる
  5. (印刷を見据えると)ベクター形式で書き出し可能か、印刷に耐えうる高解像度で描画できる

マスの柔軟なレイアウトと矢印の複雑なレンダリングを実現するために、僕のスキルセットで最適と思われた Auto Layout + Core Graphics を選択しました。 1, 2, 3 を Auto Layout で対応し、 4 を Core Graphics で対応します。

5 については、 UIView からベクター形式で書き出すことはできないですが、単純なデザインなので計算された座標を元にベクター形式で書き出すコードを書くことは難しくないだろうと考えました(こちらは現時点で未検証で、紙版のプロトタイプ作成のためにこれから取り組む予定です)。

マスのレイアウト

Auto Layout によるマスのレイアウトは↓のように実現できます。

マスの制約

マスのアスペクト比について、 10 : 8 より横長にならないようにする( <= 10 / 8 の) constraint (制約)を設定するのと同時に、 priority (優先度)を下げて == 10 / 8 の constraint も追加するのがポイントです。このときに、 == 10 / 8 の priority は UILabel の content compression resistance priority よりも小さな値にする必要があります。

UILabel は保持するテキストのフォントや長さに応じたサイズ intrinsic content size を持っています。たとえば、 width を固定するとテキストの長さに応じて height が変化します。 UILabel の intrinsic content size の priority は content compression resistance priority (伸びる場合の priority )と content hugging priority (縮む場合の priority )によって決定されます。今は伸びる場合を考えているので、もし == 10 / 8 の priority を content compression resistance priority 以上に設定してしまうと、テキストの長さに応じて UILabelheight が伸びるよりもアスペクト比が 10 : 8 になることが優先されてしまいます。つまり、テキストが長くてもマスの hieght が伸びなくなってしまいます。

Auto Layout を使えば、親子・隣接 view 間はもちろん、同じ view ツリーに属していれば階層を飛び越えて constraint を設定できたり、任意の対象の間で、 Y == a * X + b (または、 == の代わりに <=, >=X, Y は制約の対象)の形で自由に制約を記述できるなど、柔軟なレイアウトが可能です。柔軟性が高いと、少々考慮漏れがあっても後からリカバリーしやすいです。今回も、スタートのマスと scroll view の左右中心をそろえて表示するというやや複雑な制約の考慮が漏れていましたが、後から簡単に対応することができました。

矢印の描画

UIViewdraw(_: CGRect) メソッドをオーバーライドすることで、 view をカスタマイズして任意の描画が可能になります。

たとえば、 Core Graphics を用いて右上から左下に赤い線を引くだけの view を↓のように作ることができます。

@IBDesignable class CustomView: UIView {
    override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext() else {
            return
        }

        context.setLineWidth(6.0)
        context.setStrokeColor(UIColor.red.cgColor)

        context.move(to: CGPoint(x: bounds.maxX, y: bounds.minY))
        context.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
        context.strokePath()
    }
}

custom view

これを使えば矢印の描画も簡単です。各マスの座標は Auto Layout が constraint を元に自動的に決定してくれるので、 draw(_:) メソッドでその座標を元に矢印を引くだけです。

レイアウトとレンダリングの課題

小学生に使ってもらうことを考えるとマスの中の文章にふりがなを付けたかったんですが、 UILabel の技術的制約で簡単にはできませんでした。今後なんとかして対応したいと考えています。

おまけ: 物理演算と 3D レンダリング

プゴロクの iOS アプリを作る上でどうしてもやってみたかったことが、物理演算を使ってサイコロを振ることです。

物理演算でサイコロを振る

1 から 6 までの整数をランダムに一つ選べばいいだけなのでやりすぎ感はありますが、すごろくをプレイしてる感じを表現したくてやりました。 SceneKit を使えば物理演算も 3D レンダリングも簡単です。

意外と面倒なのが、出た目が何かを計算することです。人間が見れば一目瞭然ですが、出目を求めるために具体的に何を計算すれば良いかを考えるとそこまで単純ではありません。プゴロクでは、サイコロの座標系における各面の方向を表すベクトルと鉛直上向きのベクトルの成す角を計算して、最小のものを選択するようにしました。

let directions: [SCNVector3] = [
    SCNVector3(0, 1, 0),
    SCNVector3(0, 0, 1),
    SCNVector3(1, 0, 0),
    SCNVector3(-1, 0, 0),
    SCNVector3(0, 0, -1),
    SCNVector3(0, -1, 0),
]

let transform = diceNode.presentation.transform
let upDirection = SCNVector3(transform.m12, transform.m22, transform.m32)
let (index, _) = directions
    .map { $0.angle(from: upDirection) }
    .enumerated()
    .min { $0.1 < $1.1 }!
let number =  index + 1

また、 SceneKit を使っていたらタイトル画面もかっこよく 3D にしたくなってきて、実際にすごろくを遊ぶときは 2D なのに、タイトル画面のためだけに無駄にすごろくを 3D 化してしました(本投稿冒頭の画像です)。

さらに、 Web サイトも 3D にしたいという誘惑には勝てず、 WebGL / three.js で 3D 化しました。 WebGL / three.js は初めて触りましたが、ブラウザ上に簡単に 3D コンテンツが表示できて楽しかったです。

まとめ

  • Swift のコードをすごろくにした( iOS アプリ)
  • プログラミング初心者がすごろくで遊びながらプログラムの動作を学べる
  • ビジュアルプログラミングの表現形式の一緒として考えてもおもしろい
  • ユークリッドの互除法のコードをすごろくにすると楽しい
  • 学習効果を考えると紙のすごろくは欠かせないので紙版も作りたい
  • 3D は楽しい

  1. ただし、 ? を宣言している 1 行目についてはすごろくから除外しました。スタートのマスにこのコードを入れることも検討しましたが、コードだけあって説明がないのでは混乱を招くだけになりそうなので、どこか別の場所で暗黙的に宣言されていることにしました。ただ、このままではすごろくに書かれたコードをそのままコピペしても実行できません。プゴロクのアプリには、すごろくのすべてのマスのコードを抜き出して並べて表示するページがあるので、そのページ上では注釈をつけた上で ? の宣言も表示するようにしました。 

  2. 小学校のプログラミング必修化は、算数などの既存教科の中でプログラミングを取り扱うことになっているので、プゴロクが活用できるかは未知数です。 

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

Generics or Protocol associated type? それが問題だ?

はじめに

「Swiftでは、任意の型に対応させられるように、Genericsを使って型を抽象化できるのか。。。?」
「なんと、Protocolでもassosiated typeで任意の型を表現できるらしい!?」

「。。。。。。。。。。。。。」

「で、どっち使えばいいんだ???」

※以下 PAT = Protocol associated type

検証 (Swift5, Xcode10.2.1)

例えば下記のようにポケモン(もはや恒例)を表す Pokemon というプロトコルとそれに準拠した Pikachu があるとする。
今回はポケモンの名前を出力してくれる PokemonDescriptor を作りたい。
PokemonDescriptor は、Pokemon プロトコルに準拠しているものなら何でも出力できる。

protocol Pokemon {
    var name: String { get }
}

struct Pikachu: Pokemon {
    let name: String
}

PATを使った場合

protocol CharacterDescribable {
    associatedtype Character
    func describe(_ character: Character)
}

struct PokemonDescriptor: CharacterDescribable {
    typealias Character = Pokemon
    func describe(_ character: Character) {
        print("\(character.name)だよ〜")
    }
}

let pikachu = Pikachu(name: "ピカチュウ")

let pokeDescriptor = PokemonDescriptor()

pokeDescriptor.describe(pikachu)

// output: ピカチュウだよ〜

Genericsを使った場合

struct PokemonDescriptor {
    func describe<T: Pokemon>(_ pokemon: T) {
        print("\(pokemon.name)だよ〜")
    }
}

let pikachu = Pikachu(name: "ピカチュウ")

let pokeDescriptor = PokemonDescriptor()

pokeDescriptor.describe(pikachu)

// output: ピカチュウだよ〜

何が違うのか?

プログラム自体がしていることは同じ。
違いはそれぞれの describe() メソッドが実行される方法(Method dispatch)。

Method dispatchとは
メソッドを呼び出すときにどの実装が実行されるかを選択/解決するシステム。
コンパイラがコンパイル時にそれを解決するのがstatic dispatch。実行時に解決するのはdynamic dispatch。

Qiita内だと、この記事とかSwiftのMethod dispatchについて詳しく書かれてます?

GenericsとPATにおいて、前者で定義されたメソッドはstatic dispatchで実行されるが後者で定義されたメソッドはdynamic dispatchで実行される。
staticはコンパイル時にどのメソッドを走らせるか事前に決めるので実行速度が速い、ただしメモリーをより多く消費する。
逆にdynamicはプログラム実行中に動的に決定するので、その分実行速度は落ちるが効率的にメモリーを使用できる。

結論✍️

  • ケースバイケース
  • どちらでもよく、簡単なコードならとりあえず実行速度の速いGenericsを検討する
  • ただし、PATの方が表現力や可読性が高い(そもそもSwiftはProtocol oriented)
  • 自分はPATでやりたい

最後に

自分でもまだしっくり来てないです。誰かアドバイスください!!(説教でも可?)

参考資料

SwiftにおけるMethod Dispatchについて - Qiita
Method Dispatch in Swift – RPLabs – Rightpoint Labs

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