20200213のSwiftに関する記事は6件です。

Swiftでドローンを飛ばしてみる

Swiftでドローンを飛ばしてみる

この記事はSwift愛好会vol49@JapanTaxiでの発表の詳細の記事です。
発表のスライドはこちらです。
上記のスライドにもあるドローンを使ったエンタメに興味をもち、また愛好会の発表のネタにもなりそうだったので、Telloのドローンを買って、遊んでみました。

仕組み

下の図のように、iPhoneなどの端末とドローンをUDP通信で接続します。その後指定のコマンドを送ることで、ドローンがそのコマンド通りに動いてくれます。
image.png

実装

@eito_2さんこちらの記事を参考にし、Network FrameworkのNWNetworkを使用して、実装しました。WWDC2019でそれに関する動画がありましたので、興味のある方はどうぞ。
まずはドローンのアドレス/ポート情報をまとめた構造体とコマンド一覧を定義したenumを準備します。コマンドは一部しか定義していないので、利用可能なコマンド一覧をみたい方は、SDK2.0 User Guideをご確認ください。

struct TelloConstants {
    static let ipAddress = "192.168.10.1"
    static let port: UInt16 = 8889
}

enum TelloCommands {
    case start
    case takeoff
    case land
    case emergency
    // Min: 20, Max: 500(cm)
    case left(x: Int)
    case right(x: Int)
    case forward(x: Int)
    case back(x: Int)
    case up(x: Int)
    case down(x: Int)

    // Min: 1, Max: 360(degree)
    case rotateRight(x: Int)
    case rotateLeft(x: Int)

    case flip(direction: TelloDirection)
}

extension TelloCommands {
    var asString: String {
        switch self {
        case .start: return "command"
        case .land: return "land"
        case .takeoff: return "takeoff"
        case .emergency: return "emergency"
        case .left(let x): return "left \(x)"
        case .right(let x): return "right \(x)"
        case .forward(let x): return "forward \(x)"
        case .back(let x): return "back \(x)"
        case .up(let x): return "up \(x)"
        case .down(let x): return "down \(x)"
        case .rotateRight(let x): return "cw \(x)"
        case .rotateLeft(let x): return "ccw \(x)"
        case .flip(let direction): return "flip \(direction.asString)"
        }
    }
}

enum TelloDirection {
    case left
    case right
    case forward
    case back

    var asString: String {
        switch self {
        case .left: return "l"
        case .right: return "r"
        case .forward: return "f"
        case .back: return "b"
        }
    }
}

次にドローンとの接続と実際にコマンドを送信する実装です。
init時に、stateUpdateHandlerにクロージャーをセットすることで、ドローンとの接続状況を確認できるようにしています。
またメソッドとしては、以下の3つを用意しました。
1.コマンドのenumを受け取り、コマンド実行後のレスポンスを受信後、指定のクロージャにResult型で結果を通知する。
2.コマンドを受け取り、その実行結果をSingleで返す(1のラッパーメソッド)。
3.コマンドの配列とコマンド実行間隔の秒数を受け取り、そのコマンドを順に実行し、その実行結果をObservableで返す。

class TelloManager {
    let connection = NWConnection(host: .init(TelloConstants.ipAddress), port: .init(integerLiteral: TelloConstants.port), using: .udp)

    init() {
        connection.stateUpdateHandler = { [unowned self] state in
            switch state {
            case .setup:
                print("Setup")
            case .waiting(let error):
                print(error)
            case .preparing:
                print("preparing")
            case .ready:
                self.connection.start(queue: .global())
                print("ready")
            case .failed(let error):
                print(error)
            case .cancelled:
                print("cancelled")
            @unknown default:
                fatalError("Unknown error")
            }
        }
    }

  //1.コマンドのenumを受け取り、コマンド実行後のレスポンスを受信後、指定のクロージャにResult型で結果を通知する。
    func send(command: TelloCommands, completion: @escaping ((Result<String, Error>) -> Void)) {
        print("Commands: ", command.asString)
        let message = command.asString.data(using: .utf8)
        connection.send(content: message, contentContext: .defaultMessage, isComplete: true, completion: .contentProcessed({ error in
            if let error = error {
                completion(.failure(error))
            }
        }))
        connection.receive(minimumIncompleteLength: 0, maximumLength: Int(Int32.max)) { (data, context, isComplete, error) in
            if !isComplete, let error = error {
                completion(.failure(error))
            } else if let data = data, let message = String(data: data, encoding: .utf8) {
                completion(.success(message))
            } else {
                print("Unknown error")
            }
        }
    }

    //2.コマンドを受け取り、その実行結果をSingleで返す(1のラッパーメソッド)。
    func send(command: TelloCommands) -> Single<String> {
        return Single<String>.create { [unowned self] single -> Disposable in
            self.send(command: command) { result in
                switch result {
                case .success(let message):
                    single(.success(message))
                case .failure(let error):
                    single(.error(error))
                }
            }
            return Disposables.create()
        }
    }

    //3.コマンドの配列とコマンド実行間隔の秒数を受け取り、そのコマンドを順に実行し、その実行結果をObservableで返す。
    func sends(parameters: (commands: [TelloCommands], interval: Int)) -> Observable<String> {
        let commandsObservables = parameters.commands.map(send)
            .map({ $0.asObservable().delay(.seconds(parameters.interval), scheduler: MainScheduler.instance) })
        return Observable.concat(commandsObservables)
    }
}

ViewControllerの実装は以下の通りです。
このドローンの制約上、最初にcommandというコマンドを最初に送信しないといけないので、setupというボタンをタップしてから、その後の操作に入るようになっています。その後takeoffボタンをタップしたら離陸、landボタンを押したら着陸、 pattern1を押したら、2秒間隔で「離陸→60cm上昇→30cm下降→左に100cm移動→右に100cm移動→前にフリップ→後ろにフリップ→着陸」が実行されます。ちなみにフリップはこんな動きをします。

class ViewController: UIViewController {

    @IBOutlet weak var setupButton: UIButton!
    @IBOutlet weak var takeOffButton: UIButton!
    @IBOutlet weak var landButton: UIButton!
    @IBOutlet weak var rightButton: UIButton!
    @IBOutlet weak var leftButton: UIButton!
    @IBOutlet weak var pattern1Button: UIButton!

    private let resultRelay = PublishRelay<String>()
    private let disposeBag = DisposeBag()
    private let tello = TelloManager()

    override func viewDidLoad() {
        super.viewDidLoad()

        setupButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance)
            .map({ _ in .start })
            .flatMap(tello.send)
            .bind(to: resultRelay)
            .disposed(by: disposeBag)
        takeOffButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance)
            .map({ _ in .takeoff })
            .flatMap(tello.send)
            .bind(to: resultRelay)
            .disposed(by: disposeBag)
        landButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance)
            .map({ _ in .land })
            .flatMap(tello.send)
            .bind(to: resultRelay)
            .disposed(by: disposeBag)
        rightButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance)
            .map({ _ in .right(x: 100) })
            .flatMap(tello.send)
            .bind(to: resultRelay)
            .disposed(by: disposeBag)
        leftButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance)
            .map({ _ in .left(x: 100) })
            .flatMap(tello.send)
            .bind(to: resultRelay)
            .disposed(by: disposeBag)
        pattern1Button.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance)
            .map({ _ -> [TelloCommands] in [.takeoff, .up(x: 60), .down(x: 30), .left(x: 100), .right(x: 100), .flip(direction: .forward), .flip(direction: .back), .land] })
            .map({ ($0, 2) })
            .flatMap(tello.sends)
            .bind(to: resultRelay)
            .disposed(by: disposeBag)
        resultRelay.asObservable()
            .subscribe(onNext: { message in
                print("Message: ", message)
            }, onError: { (error) in
                print("Error: ", error)
            })
            .disposed(by: disposeBag)
    }
}

まとめ

普段はモバイル内のみを動かすことが多いSwiftですが、ドローンを動かせたのは新鮮でした。
Try!Swift2019ではSwiftでラズパイを動かす発表もあったので、いずれそちらも触ってみたいです。
またこのドローンはカメラの情報も受け取れる仕組みになっているので、その情報を受け取って、写真や動画の撮影できるようにするのも、いずれ実装したいです(今回は時間がなくて、断念しました)。
ソースコードはこちらです。

参考

https://www.ryzerobotics.com/jp/tello
https://qiita.com/eito_2/items/0d9e2c92b0be0ea16e77
https://dl-cdn.ryzerobotics.com/downloads/Tello/Tello%20SDK%202.0%20User%20Guide.pdf

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

Firebase Authenticationを学習しよう(Facebookログイン編)

完成予想図

fb最新.gif

はじめに

FirebaseでSNSユーザー認証をしたい時に参考にしてください!

メールアドレスとパスワードで認証するノーマルバージョンを試したい場合は、
https://firebase.google.com/docs/auth/ios/start?hl=ja(公式ドキュメント)

https://dev.classmethod.jp/smartphone/iphone/firebase-authentication-ios/
がわかりやすいです!

今回は、facebookログインをやってみます!

Firebase Authenticationとは

Firebase Authentication は、 Google のフェデレーション認証で、 Google 以外にも Facebook 、 GitHub 、 Twitter などのサードパーティの認証情報を使用して、クライアントアプリケーションのログインを行うことができます。また、フェデレーション ID 以外にもパスワード認証や電話番号認証にも対応しています。

※フェデレーションとは...一度認証を通れば、その認証情報を使って、許可されているすべてのサービスを使えるようにする仕組みのこと。

引用元
https://www.topgate.co.jp/firebase01-what-is-firebase

インポート

podfileを作成後、
firebaseやfacebook関連のものをファイルに取り込みましょう!

  pod 'Firebase/Auth'  
  pod 'FacebookSDK'
  pod 'Firebase/Core'
  pod 'FacebookCore'
  pod 'FacebookLogin'
  pod 'FacebookShare'
  pod 'SDWebImage/WebP'

Firebaseの設定

https://qiita.com/ShinokiRyosei/items/f71c73ab8b0de145c5bc
ここに設定手順が記載されているので参考にしてください!

https://qiita.com/Ritsuya/items/0ad42ae40337ae36f472
Firebase全般に関してわからない方はこちらを参照してみてください!

プロジェクトファイルに「GoogleService-Info」 のファイルを入れるときの注意点です!
「Copy items if needed」にチェックが入ってること
「GoogleService-Info(1)」となっている取り入れたファイル名をxcode内で「GoogleService-Info」に変更しましょう

を確認してファイルに取り入れましょう!

Facebookログインの設定

まず、Firebase上でFacebookログインを有効にします!
スクリーンショット 2020-02-13 1.17.44.png

右上で有効にした後に、アプリケーションIDとアプリシークレットを入力します!
これらは、facebook developerのサイトの番号を入力します。

スクリーンショット 2020-02-13 1.23.27.png
右上のマイアプリ=>アプリの作成
その次に出てくる、表示名の項目は自由に決めてオッケーです!(ただしfacebookというワードはNGのようです)

スクリーンショット 2020-02-13 1.25.41.png

プライバシーポリシーやカテゴリも決めておきましょう!
そして、これらの情報に対応する部分を先ほどのfirebase(アプリケーションIDとアプリシークレット)に登録しましょう!

そして、詳細設定で、赤枠の部分を変更します!
スクリーンショット 2020-02-13 1.31.33.png

次に、左側のプロダクトを追加します!
その中のFacebookログインの設定を行います。

次に、左側の設定の有効なOAuthリダイレクトURIというところに、firebaseのfacebookの部分のURLを載っけます。(写真赤枠)
スクリーンショット 2020-02-13 1.17.44.png

最後に、ここまで設定すれば、上のステータスをオンにすることができます!
オンに切り替えてください。

FirebaseとFacebookの導入

AppDelegate.swift
import Firebase
import FBSDKCoreKit //FacebookSDK

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        //追加
        FirebaseApp.configure()

        //Facebookログイン
        ApplicationDelegate.shared.application(application, didFinishLaunchingWithOptions: launchOptions)

        return true
    }

      //追加
    func application(_ application : UIApplication,open url: URL, sourceApplication: String?, annotation: Any)->Bool{
        return ApplicationDelegate.shared.application(application, open: url, sourceApplication: sourceApplication, annotation: annotation)
    }

      //追加
    func applicationDidBecomeActive(_ application: UIApplication) {
        AppEvents.activateApp()
    }

info.plistの設定

info.plistに以下の項目を追加し、facebook developerのサイトで設定したIDを赤い部分に入力しましょう
スクリーンショット 2020-02-13 16.05.23.png
FirebaseDisplayNameも、facebookDeveloperのサイトで設定したプロダクト名を入力してください。

Facebookログインボタンの表示

コードでボタンを配置します!
①必要なものをインポート
②ボタンをコード上で配置
③ログインボタンのプロトコル宣言をし、delegateを設定
④facebookログインに必要な関数をかく

import UIKit
import FBSDKCoreKit
import FBSDKLoginKit
import FacebookCore
import FacebookLogin
import Firebase


class ViewController: UIViewController , LoginButtonDelegate{

    let fbLoginButton: FBLoginButton = FBLoginButton()
    var displayName = String()
    var pictureURL = String()
    var pictureURLString = String()

    override func viewDidLoad() {
        super.viewDidLoad()

        fbLoginButton.delegate = self
        fbLoginButton.frame = CGRect(x: view.frame.size.width / 2 - view.frame.size.width / 4, y: view.frame.size.height / 4, width: view.frame.size.width / 2, height: 30)

        //許可するもの
        fbLoginButton.permissions = ["public_profile, email"]

        view.addSubview(fbLoginButton)

    }


    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        //ナビゲーションバーを消す
        navigationController?.isNavigationBarHidden = true
    }

    func loginButton(_ loginButton: FBLoginButton, didCompleteWith result: LoginManagerLoginResult?, error: Error?) {
           //loginする
        if error == nil{
            if result?.isCancelled == true{
                //キャンセルされた場合は何もしないで返す
                return
            }
        }

        let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString)

        //ここからfirebase
        Auth.auth().signIn(with: credential) { (result, error) in
            if let error = error {
                return
            }
            self.displayName = (result?.user.displayName)!
            //string型に強制変換
            self.pictureURLString = (result?.user.photoURL!.absoluteString)!
            //画像の大きさを変更(大きくした)
            self.pictureURLString = self.pictureURLString + "?type=large"

            //次の画面遷移で画像を表示させるために、userdefaultを用いて保存&ログイン保持
            let ud = UserDefaults.standard
            ud.set(1, forKey: "loginOK") //ログイン保持
            ud.set(self.pictureURLString, forKey: "pictureURLString")

            //次の画面の遷移先を指定
            let nextVC = self.storyboard?.instantiateViewController(identifier: "next") as! NextViewController
            self.navigationController?.pushViewController(nextVC, animated: true)

        }



       }

    func loginButtonWillLogin(_ loginButton: FBLoginButton) -> Bool {
        return true
    }
       func loginButtonDidLogOut(_ loginButton: FBLoginButton) {
           //ログアウト
       }




}


最後に

次の画面遷移で画像を表示させるのは、userdefaultから値を取ってきて代入するだけです!
facebookログインは、facebook developerのサイトの設定が少し多いですがぜひやってみてください!

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

Swiftでの繰り返し処理

swiftの繰り返し処理

アウトプットとして、初めてQittaで投稿します。もし間違えてるとこがありましたら、指摘して下さい!

処理を繰り返す for - in文

qiita.swift
for 変数 in 配列 {
   ステートメント
}
example.swift
//配列の定義
var numList = [2, 4, 6, 8, 10]
var sum = 0

for num in numList {
   sum += num
}
print("合計\(sum)") => 30

ちなみに変数numは繰り返し処理のなかで定義したので、外では使えません。

for - in で指定回数繰り返す

レンジから取り出した値を使わない場合、値を受け取る変数の代わりに _ を指定する

example.swift
var stars = ""
for _ in 1...5 {
   stars += "★"
   print(stars)
}
=>
  ★★
  ★★★
  ★★★★
  ★★★★★

レンジとは

レンジは整数の範囲を表します。二つの違いは終了値を含むか含まないか

レンジ.swift
開始値...終了値
1...5 //1 ~ 5

開始値..<終了値
1..< 5 //1 ~ 4
}

最後に

初めての投稿でわかりづらいかもしれませんが、これからも投稿を続けようと思いおいます。Swift勢のみなさん頑張りましょう!ありがとうございました

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

[Swift5]TableViewCellをスワイプして削除する際にアラートを出す(クロージャを引数に持つ便利なアラート関数)

はじめに

セルの削除など重要な処理をする際にアラートを表示することで不本意な操作を避けることができます。
この記事では使い回しができるアラート関数を実装し、それを利用してアラートを表示していきたいと思います。

使い回しができるアラート関数は1つのTableViewを使いまわすことなどにより、関数内の記述が場合分けによって増えてしまうのを防ぐ有効な手段となります。

目次

  • 環境
  • 実行例
  • 考え方
    • TableViewの実装
    • アラート関数の実装
  • ソースコード
  • 終わりに

環境

  • Xcode 11.2.1
  • Swift5

実行例

alertsample.gif

考えかた

実行例のようなプログラムを実装するにあたって説明しなければならない要素が3つあります。
TableViewSegmentControlアラート関数です。
今回はアラート関数に関する説明を重点的に行いたいので、その他の要素については軽く触れる程度にします。
また、SegmentControlに関しては必ずしも必要なわけではないので今回は説明しませんが、とても分かりやすい記事があるので共有しておきます。

TableViewの実装

はじめに、TableViewを削除可能にするための実装の説明を少しします。
以下のコードでセルの編集許可を与えることで可能になります。

func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
}

またセルに対する編集が行われた場合に呼び出されるイベント関数が以下になります。

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {...}

アラート関数の実装

さて本題ですね。クロージャさえ理解できればあとは簡単です。

アラート関数は以下のように表現されます。

private func alert(alertTitle: String, alertMessage: String, okTitle: String, cancelTitle: String, Closure: @escaping ((Bool) -> Void)) {
        var isOK = false

        let alert: UIAlertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle:  UIAlertController.Style.alert)
        for subview in (alert.view.subviews) {
            subview.backgroundColor = UIColor(red: 50/255, green: 143/255, blue: 145/255, alpha: 1)
            subview.layer.cornerRadius = 20
            subview.alpha = 1
        }
        // OK
        let defaultAction: UIAlertAction = UIAlertAction(title: okTitle, style: UIAlertAction.Style.default, handler:{
            (action: UIAlertAction!) -> Void in
            isOK = true
            Closure(isOK)
        })
        // キャンセル
        let cancelAction: UIAlertAction = UIAlertAction(title: cancelTitle, style: UIAlertAction.Style.cancel, handler:{
            (action: UIAlertAction!) -> Void in
            isOK = false
            Closure(isOK)
        })

        alert.addAction(cancelAction)
        alert.addAction(defaultAction)
        present(alert, animated: true, completion: nil)
    }

引数にアラートのタイトル、アラートメッセージ、OKのタイトル、キャンセルのタイトル、操作時に呼び出す処理(クロージャ)を持っていることが分かりますね。

引数のクロージャに@escapingが付くのは、このクロージャがdefaultActioncancelActionのクロージャで呼ばれるためです。
これは非同期処理によって引数のクロージャが解放されるのを防ぐという意味ですね。

このように実装することでアラート関数を短く呼び出すことが可能となります。

では呼び出す側を見てみましょう。

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {

        func update(_ isOK: Bool) {
            if(isOK) {
                //省略
                let indexSet = NSMutableIndexSet()
                indexSet.add(indexPath.section)
                tableView.deleteSections(indexSet as IndexSet, with: UITableView.RowAnimation.automatic)
                tableView.reloadData()
            }
        }


        switch self.mySegmentControl.selectedSegmentIndex {
        case 0:
            alert( alertTitle: "本当ニ33娘ヲ削除シマスカ?",
                   alertMessage: "bye~(- o -)",
                   okTitle: "スル!",
                   cancelTitle: "シナイ",
                   Closure: { isOK in update(isOK) })
        case 1:
            alert( alertTitle: "本当に22娘を削除しますか?",
                   alertMessage: "goodbye~(o w o)",
                   okTitle: "します!",
                   cancelTitle: "しません",
                   Closure: { isOK in update(isOK) })
        default: break
        }
    }

このようにスッキリ記述することができました。

ソースコード

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {

        func update(_ isOK: Bool) {
            if(isOK) {
                switch mySegmentControl.selectedSegmentIndex {
                case 0:
                    if let i = showndata.firstIndex(of: "33娘") {
                        showndata.remove(at: i)
                    }
                case 1:
                    if let i = showndata.firstIndex(of: "22娘") {
                        showndata.remove(at: i)
                    }
                default:
                    break
                }
                let indexSet = NSMutableIndexSet()
                indexSet.add(indexPath.section)
                tableView.deleteSections(indexSet as IndexSet, with: UITableView.RowAnimation.automatic)
                tableView.reloadData()
            }
        }


        switch self.mySegmentControl.selectedSegmentIndex {
        case 0:
            alert( alertTitle: "本当ニ33娘ヲ削除シマスカ?",
                   alertMessage: "bye~(- o -)",
                   okTitle: "スル!",
                   cancelTitle: "シナイ",
                   Closure: { isOK in update(isOK) })
        case 1:
            alert( alertTitle: "本当に22娘を削除しますか?",
                   alertMessage: "goodbye~(o w o)",
                   okTitle: "します!",
                   cancelTitle: "しません",
                   Closure: { isOK in update(isOK) })
        default: break
        }
    }


    private func alert(alertTitle: String, alertMessage: String, okTitle: String, cancelTitle: String, Closure: @escaping ((Bool) -> Void)) {
        var isOK = false

        let alert: UIAlertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle:  UIAlertController.Style.alert)
        for subview in (alert.view.subviews) {
            subview.backgroundColor = UIColor(red: 50/255, green: 143/255, blue: 145/255, alpha: 1)
            subview.layer.cornerRadius = 20
            subview.alpha = 1
        }
        // OK
        let defaultAction: UIAlertAction = UIAlertAction(title: okTitle, style: UIAlertAction.Style.default, handler:{
            (action: UIAlertAction!) -> Void in
            isOK = true
            Closure(isOK)
        })
        // キャンセル
        let cancelAction: UIAlertAction = UIAlertAction(title: cancelTitle, style: UIAlertAction.Style.cancel, handler:{
            (action: UIAlertAction!) -> Void in
            isOK = false
            Closure(isOK)
        })

        alert.addAction(cancelAction)
        alert.addAction(defaultAction)
        present(alert, animated: true, completion: nil)
    }

終わりに

クロージャって便利ですね。
セルを削除する際のデータに対する処理が複雑になればなるほど、スッキリ記述する重要性は高まっていくと思います。
そのような時にこの記事が役に立てば幸いです。

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

チュートリアルでありがちなテキストが少しずつ表示されていくUILabelを実装する方法

うまく表現できませんが、チュートリアルなんかでよく、「カタカタカタカタ」と少しずつテキストが表示されていくシーンがあるじゃないですか!(伝わってください)

今回はそれを実装しました。
既存のライブラリで存在していたら教えてください

少しずつテキストを表示して、表示中にタップされたらテキストを全部まとめて表示するUILabelを実装しました。

// テキストを表示中を表すフラグと、早く表示するためのフラグです
fileprivate var isPlaying = false
fileprivate var isFast = false

fileprivate var tutorialLbl: UILable!

override func viewDidLoad() {
    super.viewDidLoad()

    // タップを検知
    self.singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(self.singleTap(_:)))
    self.view.addGestureRecognizer(self.singleTapGesture)
}

// 画面をタップした際に発火する関数
@objc func singleTap(_ sender: UITapGestureRecognizer){

    // 一度タップされていてテキストがカタカタと表示中のとき、表示速度を早めるフラグを立てる
    if self.isPlaying {
        self.isFast = true
        return
    }
    self.isPlaying = true

    // このメソッドはよしなに
    showTutorial()
}

fileprivate func showTutorial() {
    // ラベルをテキトーに作成
    self.tutorialLbl = UILabel()
    self.tutorialLbl.frame = CGRect(x: 0, y: self.view.bounds.maxY - 200, width: self.view.bounds.width, height: 200)
    self.view.addSubview(self.tutorialLbl)

    let text = "アイウエオかきくけこさしすせそ"

    // ここで表示
    self.showTextDynamically(label: self.tutorialLbl, text: Array(text), idx: 0)
}

fileprivate func showTextDynamically(label: UILabel, text: [Character], idx: Int) {
    var waitTime = 0.0
    // 表示速度を早めるフラグが立っていないとき、0.1秒間に一文字表示するよう待ち時間を設定
    if !self.isFast {
        waitTime = 0.1
    }
    // テキストを表示しきったときリターン
    if text.count == idx {
        self.isPlaying = false
        self.isFast = false
        return
    }
    if label.text == nil {
        label.text = ""
    }
    // 0.1秒遅延して再帰する
    DispatchQueue.main.asyncAfter(deadline: .now() + waitTime) {
        // テキストをlabelに追加する
        let c = text[idx]
        label.text! += String(c)
        self.showTextDynamically(label: label, text: text, idx: idx + 1)
    }
}

こういうのってtimerを使うのが一般的なんでしょうか。

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

Swift:WKWebViewでローカルファイルを開く時のTips

import Cocoa
import WebKit

class ViewController: NSViewController {

    var webView: WKWebView? {
        return self.view as? WKWebView
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        guard let url = Bundle.main.url(forResource: "LocalFile", withExtension: "html") else { return }
        self.webView?.loadFileURL(url, allowingReadAccessTo: url)
    }

}

こんな感じでBundle内のHTMLファイルをWebViewに表示するとき、SandBoxNetwork: Outgoing Connections (client)にチェックを入れないとコンテンツが読み込まれない。

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