20201119のiOSに関する記事は7件です。

Flutter実装時にお世話になったページ 2020/11/19

なにか

毎回、同じ問題に、同じ検索して、同じページで解決してるので。。。

Image.networkNetworkImage で画像が表示されない場合がある

特に画像が大量にある画面でたまに表示できてない画像があった。で、ログが流れる。

════════ Exception caught by image resource service ════════════
Connection closed before full header was received, uri = https://xxxxxx

Stack Overflow "Network Image - Connection closed before full header was received..."

これで解決する前に、同じページの別のコメントや

I globally overrode the HttpClient

別ISSUEのコメントに出てきた flutter_imageNetworkImageWithRetryを使ってみた。

Stack Overflow "Flutter how to handle Image.network error (like 404 or wrong url)"

これらは、解決に至らなかったけど、HttpClientのOverrideは今後役に立ちそうだし、flutter_imageの考え方は結構好き。

で、最終的にURLをHTTPにするという残念な対応になってしまっているので、根本解決されたときに戻しやすいようにしておいた。

Androidの通知用アイコンを設定したい

個人的には Android Asset Studioがありがたかった。

Qiita "Android Push通知のアイコンを作成 + 設定"

video_playerで連続再生している動画を、画面遷移時に止めたい。

もともと、inview_notifier_listでスクロールINしたときに再生開始にしていたんだけど、このコメント見て visibility_detectorに変えた。

Stack Overflow "How to pause flutter video(video_player plugin) when navigating from page to another"

最終的に、RouteObserverRouteAwareは使用せずに、didChangeDependencies内で、現在画面に表示中かどうかを取得してハンドリングするようにした。

そのWidgetが画面に描画されているかどうかを取得したい

Stack Overflow "How to check if a widget/page is rendered?"

さいごに

本当に、先人たちありがとう。

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

【第4回】初心者二人で0から麻雀アプリ開発

第3回の記事はこちら

改版履歴

2020年11月19日投稿

第4回会議(2020年11月13日)

議題

・画面デザイン詰める
・実装してみたい機能
・来週からの活動
・プロジェクト全体の大まかな期間

画面デザイン

画面のデザインはgoogledrive内のdraw.ioを使用して作りました。この時、ホーム画面から成績入力画面に移動する際に、画面全体で遷移させるのか、画面を部分的に遷移させるのか悩みました。部分的に遷移するのが今の流行ということなので、部分遷移でいこう!ということになりました。また、ポップアップで入力画面を立ち上げるという案も出ました。

実装してみたい機能

今回の会議で、お互いにやってみたいことが結構出てきました。
・テーマ
最近のアプリはダークテーマとか、ホワイトテーマとか自分の好みで切り替えられるようにしたい。デフォルトであるらしい…❓

・githubの芝みたいなもの
打数に応じてカレンダーに色づけするみたいなことができればモチベーションの維持につながるのではないかと思い実装してみたいと思いました。

来週からの活動

今回から、各々テーマを決めて、次の会議までに実装の方法を調べてくるという方法をとります。
今週のテーマはそれぞれ
・テーブル表示の実装について   
・ヘッダーとフッターの実装について
・スコア表機能の実装 
ということになりました。

プロジェクトの展望と今後の予定について

要件定義と基本設計 1ヶ月

画面遷移図の作成 2ヶ月くらい

詳細設計 1ヶ月 (勉強会)、クラス図作成、処理の順番、変数名など

実装1ヶ月 (ソースコードを書く)
テスト1ヶ月半 
アプリをリリース
という大雑把な計画を立てました。
できるだけ多くの機能を実装してみたいと思っていますが、最低限挫折しないということを目標に頑張っていきます。

終わりに

雑記みたいな感じになってしまいました。速くqiitaっぽくcode載せたいです。(切実)

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

XGBoostをiOSアプリで動作させる

Kaggleなどのコンペで人気のXGBoostをiOSアプリで動作させたので、その方法を紹介します。

XGBoostとは

勾配ブースティング決定木 (GBDT, Gradient Boosting Decision Tree)を実装したライブラリです。

GBDTとは

勾配ブースティング決定木とは、手短に説明すると『決定木を複数組み合わせたアンサンブル学習の一種で、勾配降下法を用いて学習を行うもの』です。

決定木とは、木構造を用いて回帰や分類を行う手法です。アンサンブル学習は、複数の決定木を用いて予想の精度を上げる手法です。

ブースティングというのは、アンサンブル学習の一種で、それぞれの学習器を直列に学習する手法です。それ以外のアンサンブル学習の手法としては学習器を並列に学習させるバギングなどがあります。例えば、ランダムフォレストはバギングの一種です。

勾配降下法とは、ディープラーニングでもよく用いられる手法で、重みを少しずつ更新して勾配が最小になるように学習する手法です。

図で説明します

駆け足過ぎたので、図で説明します。

例えば、決定木は下の図のようにして、その人がゲーム好きかどうかを予想します。

【決定木の例】

ブースティングを用いた場合は、複数の決定木を用いて予想値を修正していきます。

【ブースティング決定木の例】

最初の木で出したポイントと、2番目の木で出したポイントを足して、最終的なポイントを出しています。ここではポイントまでしか出していませんが、最終的にはこれを確率に変換します。

なぜこのようにすると予想の精度が上がるのか?についてはこちらに解説がありました。

Kaggle Masterが勾配ブースティングを解説する

学習は最初の木を学習させたあと、その予想結果との差に対して新たな木を作って学習させます。このように順番に学習していくので、「直列に学習する」と表現してます。

また、予想結果の差に対して新たに学習していくので、勾配に従って学習していくことになります。これが勾配降下法に相当します。

学習についてはこちらのブログがわかりやすかったです。

GBDTの仕組みと手順を図と具体例で直感的に理解する

XGBoostを使ってみる

Google Colabを使ってXGBoostを試してみます。ほぼこちらのブログの内容の写経になっています。

Python: XGBoost を使ってみる - 乳がんデータセットを分類してみる

乳がんデータセットを使って、特徴量から乳がんであるかを予想する二値分類問題です。

Notebook
import xgboost as xgb

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from matplotlib import pyplot as plt

dataset = datasets.load_breast_cancer()
X, y = dataset.data, dataset.target

X_train, X_test, y_train, y_test = train_test_split(X, y,
                                                        test_size=0.3,
                                                        shuffle=True,
                                                        random_state=42,
                                                        stratify=y)

clf = xgb.XGBClassifier(objective='binary:logistic',
                            n_estimators=1000)

evals_result = {}
clf.fit(X_train, y_train,
      eval_metric='logloss',
      eval_set=[
          (X_train, y_train),
          (X_test, y_test),
      ],
      early_stopping_rounds=10,
      callbacks=[
          xgb.callback.record_evaluation(evals_result)
      ],
)

y_pred = clf.predict(X_test)
acc = accuracy_score(y_test, y_pred)
print('Accuracy:', acc)

# 出力結果
# Accuracy: 0.9649122807017544

Core MLに変換する

モデルができたので、今度はCore MLに変換します。
Core ML ToolsはXGBoostの変換に対応しているので、簡単に変換することができます。

Notebook
pip install -U coremltools==4.0

import coremltools as ct

coreml_model = ct.converters.xgboost.convert(clf , mode='classifier')

coreml_model.save('xgboost_test.mlmodel')

iOSアプリで動作させる

iOSアプリで動作させます。Core MLを簡単に試せるリポジトリを作ったのでこれを使います。

TokyoYoshida/CoreMLSimpleTest

このリポジトリをCloneして、変換したモデルをドラッグ & ドロップします。

モデルの情報はこんな感じです。


早速予想してみましょう。
モデルに入力する情報(特徴量)が30もあるのでちょっと大変ですが、Google Colabでダウンロードしたデータセットから取得して、xgboost_testInputの初期化パラメータに与えます。

ViewController.swift
    @IBAction func buttonSimpleMLTapped(_ sender: Any) {
        let model = xgboost_test()
        let inputToModel: xgboost_testInput = xgboost_testInput(
            f0: 1.300e+01,
            f1: 2.078e+01,
            f2: 8.351e+01,
            f3: 5.194e+02,
            f4: 1.135e-01,
            f5: 7.589e-02,
            f6: 3.136e-02,
            f7: 2.645e-02,
            f8: 2.540e-01,
            f9: 6.087e-02,
            f10: 4.202e-01,
            f11: 1.322e+00,
            f12: 2.873e+00,
            f13: 3.478e+01,
            f14: 7.017e-03,
            f15: 1.142e-02,
            f16: 1.949e-02,
            f17: 1.153e-02,
            f18: 2.951e-02,
            f19: 1.533e-03,
            f20: 1.416e+01,
            f21: 2.411e+01,
            f22: 9.082e+01,
            f23: 6.167e+02,
            f24: 1.297e-01,
            f25: 1.105e-01,
            f26: 8.112e-02,
            f27: 6.296e-02,
            f28: 3.196e-01,
            f29: 6.435e-02
        )
        if let prediction = try? model.prediction(input: inputToModel) {
            print(prediction.target)
            print(prediction.classProbability)
        }
    }

アプリを実行するとボタンが複数出てくるので一番上のボタンを押します。

ボタンを押すと、Xcodeのコンソールに予想結果が出力されます。

コンソール出力
1
[0: 0.019117622730040473, 1: 0.9808823772699595]

うまく分類できているようです。

最後に

2020年11月現在、XGBoostをiOSで動かしたい人はほとんどいないのか、ネット上にはほとんど情報がありません。

また、Core ML Toolsのドキュメントもあまり親切ではないので、もしXGBoostをiOSで動かしたいという方の参考になりましたら幸いです。


NoteではiOS開発、とくにCoreML、ARKit、Metalなどについて定期的に発信していますので、フォローしていただけますと幸いです。
https://note.com/tokyoyoshida

Twitterでも発信しています。
https://twitter.com/jugemjugemjugem

参考資料

勾配ブースティング決定木についてはこちらのブログの説明が詳しかったです。

勾配ブースティング決定木ってなんぞや

ブースティングについては上のブログにはあまり説明がなく、こちらの資料がわかりやすかったです。

ブースティング入門

GBDTと勾配降下法の関係については、こちらのブログが参考になりました。

なんでXGBoostは分類問題の学習に対数損失を使うんですか?

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

[Swift][MVVM]関数型ViewModelのススメ

概要

本稿ではアプリアーキテクチャの1形式であるMVVMの、特にViewControllerViewModelの関連を記述するスタイルとして関数型ViewModelを紹介します。

参考リンク: Modeling Your View Models as Functions

前提知識

本稿を読む前提として、以下の知識があると理解しやすいです。
- iOSアプリ開発
- Swift 5
- RxSwift

MVVMを使ったアプリの実装例

いくつかのスタイルで簡単な機能のアプリを実装してみます。

仕様

まず、以下のようなアプリを考えてみましょう

  • 画面の中央にカウンターを表示する
  • +ボタンを押すとカウンターの数値が1増える
  • -ボタンを押すとカウンターの数値が1減る
  • 数値が偶数ならカウンターの文字色が黒に、奇数ならになる。

画面レイアウト

まずレイアウトを実装します。この時点ではボタンを押しても何も反応しません。

import UIKit

final class ViewController: UIViewController {
    private var countLabel: UILabel! // 合計カウント表示ラベル
    private var plusButton: UIButton! // +ボタン
    private var minusButton: UIButton! // -ボタン

    override func loadView() {
        super.loadView()
        view.backgroundColor = .white

        countLabel = UILabel()
        view.addSubview(countLabel)
        countLabel.translatesAutoresizingMaskIntoConstraints = false
        countLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        countLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true


        plusButton = UIButton(type: .system)
        plusButton.setTitle("+", for: .normal)
        view.addSubview(plusButton)
        plusButton.translatesAutoresizingMaskIntoConstraints = false
        plusButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        plusButton.bottomAnchor.constraint(equalTo: countLabel.topAnchor, constant: -10).isActive = true


        minusButton = UIButton(type: .system)
        minusButton.setTitle("-", for: .normal)
        view.addSubview(minusButton)
        minusButton.translatesAutoresizingMaskIntoConstraints = false
        minusButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        minusButton.topAnchor.constraint(equalTo: countLabel.bottomAnchor, constant: 10).isActive = true
    }

ViewControllerによる実装例

このアプリを、愚直にViewControllerだけで実装してみましょう。コードは以下のとおりです。

import UIKit

final class ViewController: UIViewController {
    // レイアウトは省略
    private var count: Int = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        plusButton.addTarget(self, action: #selector(plusButtonDidTap), for: .touchUpInside)
        minusButton.addTarget(self, action: #selector(minusButtonDidTap), for: .touchUpInside)
        updateCountLabel()
    }

    @objc func plusButtonDidTap() {
        count += 1
        updateCountLabel()
    }

    @objc func minusButtonDidTap() {
        count -= 1
        updateCountLabel()
    }

    private func updateCountLabel() {
        countLabel.text = "\(count)"
        countLabel.textColor = count %2 ==0
            ? UIColor.black
            : UIColor.red
    }
}

MVVM(Kickstarterスタイル)による実装例

https://github.com/kickstarter/ios-oss (Kickstarter社のリポジトリ)
https://qiita.com/muukii/items/045b12405f7acff1a9fd (KickstarterスタイルのMVVMについての解説記事)

こちらのリンクを参考に、同じ機能を実装してみましょう。

ViewModelは以下のように実装します。

import RxSwift
import RxCocoa

protocol ViewModelInputs {
    func plusButtonDidTap()
    func minusButtonDidTap()
}

protocol ViewModelOutputs {
    var count: Driver<Int> { get }
    var isEven: Driver<Bool> { get }
}

protocol ViewModelType {
    var inputs: ViewModelInputs { get }
    var outputs: ViewModelOutputs { get }
}

struct ViewModel: ViewModelType, ViewModelInputs, ViewModelOutputs {

    var inputs: ViewModelInputs { return self }
    var outputs: ViewModelOutputs { return self }

    let count: Driver<Int>
    let isEven: Driver<Bool>

    private let plusButtonDidTapProperty = PublishSubject<Void>()
    private let minusButtonDidTapProperty = PublishSubject<Void>()

    init() {
        count = Observable.merge(plusButtonDidTapProperty.map { _ in 1 }, // +ボタンタップは1に変換する
                                 minusButtonDidTapProperty.map { _ in -1 }) // -ボタンタップは-1に変換する
            .scan(0, accumulator: +) // 0から開始して今までの合計値を出力する
            .startWith(0) // 初期値は0
            .asDriver(onErrorDriveWith: .never())

        isEven = count
            .map { $0 % 2 == 0 }
    }

    func plusButtonDidTap() { plusButtonDidTapProperty.onNext(()) }
    func minusButtonDidTap() { minusButtonDidTapProperty.onNext(()) }
}

ViewControllerは以下のように実装します。

import RxSwift
import RxCocoa

final class ViewController: UIViewController {
    // レイアウトは省略
    private let viewModel: ViewModelType
    private let disposeBag = DisposeBag()

    init(viewModel: ViewModelType = ViewModel()) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        plusButton.rx.tap
            .subscribe(onNext: { [viewModel] in viewModel.inputs.plusButtonDidTap() })
            .disposed(by: disposeBag)

        minusButton.rx.tap
            .subscribe(onNext: { [viewModel] in viewModel.inputs.minusButtonDidTap() })
            .disposed(by: disposeBag)

        viewModel.outputs
            .count
            .map { "\($0)" }
            .drive(countLabel.rx.text)
            .disposed(by: disposeBag)

        viewModel.outputs
            .isEven
            .map { $0 ? UIColor.black : UIColor.red }
            .drive(onNext: { [countLabel] in countLabel?.textColor = $0 })
            .disposed(by: disposeBag)
    }
}

国内の資料でiOS・Swift環境でMVVMというと、だいたいはこのようなKickstarterスタイルのMVVMが多い印象を受けます。
RxSwiftを使用するテンプレートとしては優れていますが、以下のような問題点があります。

  • Inputs,Outputsと宣言するプロトコルがやや多い。
  • InputsPropertyとの対応関係の記述が冗長。
  • 使用されないOutputs変数があってもコンパイラに警告されない。

MVVM(関数型スタイル)による実装例

https://medium.com/grailed-engineering/modeling-your-view-models-as-functions-65b58525717f

こちらの記事を参考に、関数型スタイルのMVVMを実装してみましょう。

ViewModelは以下のように実装します。リンク先の記事とは違いSwift5.2で追加されたcallAsFunctionを使っています。

import RxSwift
import RxCocoa

protocol ViewModelType {
    func callAsFunction(
        plusButtonDidTap: Observable<Void>,
        minusButtonDidTap: Observable<Void>
    ) -> (
        count: Driver<Int>,
        isEven: Driver<Bool>
    )
}

struct ViewModel: ViewModelType {
    func callAsFunction(
        plusButtonDidTap: Observable<Void>,
        minusButtonDidTap: Observable<Void>
    ) -> (
        count: Driver<Int>,
        isEven: Driver<Bool>
    ) {
        let count = Observable.merge(plusButtonDidTapProperty.map { _ in 1 }, // +ボタンタップは1に変換する
                                 minusButtonDidTapProperty.map { _ in -1 } ) // -ボタンタップは-1に変換する
            .scan(0, accumulator: +) // 0から開始して今までの合計値を出力する
            .startWith(0) // 初期値は0
            .asDriver(onErrorDriveWith: .never())

        let isEven = count
            .map { $0 % 2 == 0 }

        return (
            count,
            isEven
        )
    }
}

ViewControllerは以下のように実装します。

import RxSwift
import RxCocoa

final class ViewController: UIViewController {
    private let viewModel: ViewModelType
    private let disposeBag = DisposeBag()

    init(viewModel: ViewModelType = ViewModel()) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    override func viewDidLoad() {
        super.viewDidLoad()

        let (
            count,
            isEven
        ) = viewModel(
            plusButtonDidTap: plusButton.rx.tap.asObservable(),
            minusButtonDidTap: minusButton.rx.tap.asObservable()
        )

        count
            .map { "\($0)" }
            .drive(countLabel.rx.text)
            .disposed(by: disposeBag)

        isEven
            .map { $0 ? UIColor.black : UIColor.red }
            .drive(onNext: { [countLabel] in countLabel?.textColor = $0 })
            .disposed(by: disposeBag)
    }
}

これまでのスタイルと違いViewModelの記述がシンプルになっていますね。

関数型MVVMを使うメリット・デメリット

主にKickStarterスタイルと比較した場合のメリット・デメリットが以下になります。

メリット

  • 宣言するプロトコル、メソッドが一つで済む。その他全体的な記述がシンプルになっている。
  • 一つの関数内にObservableのイベント遷移が集約されるため、実装の検証が容易。
  • ViewModelの戻り値に使用されていない変数があった場合、コンパイラが警告してくれる。

デメリット

  • KickStarterスタイルと比べてより宣言的な記述を強要するため、開発メンバーがRxSwiftに精通していることが必要とされる。
  • 一つの関数内の記述量が多いため、適切に処理を切り出さないと却って見通しが悪くなる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[swift]選択肢をもつダイアログの後に処理に応じて再びダイアログを表示して画面遷移したい

タイトル長くなりがち。

やりたい事

アカウント設定画面でログアウトボタンを押した時に、

  • 「ログアウトしますか?」というダイアログに「OK」/「キャンセル」の選択肢を用意。

  • 「OK」を押すと、APIでログアウト処理が行われる。
    →成功すると、「ログアウトしました」というダイアログが表示され、画面が遷移する。
    →失敗すると、「ログアウトに失敗しました」というアラートが表示される。

  • 「キャンセル」を押すと、最初のダイアログが消える。

っていう処理を行いたい:hugging:

とりあえず作ってみた

今回は、

  • ちゃんと読んでほしいアラートは手動で閉じる。
  • お知らせ程度のダイアログは勝手に消える。

みたいな感じでメソッドを使い分けています。

AccountViewController.swift
import UIKit
final class AccountViewController: UIViewController {
    @IBAction private func tappedLogoutButton(_ sender: UIButton) {
        let dialog: UIAlertController = UIAlertController(title: "ログアウト",
                                                          message: "ログアウトしますか?",
                                                          preferredStyle: .alert)
        dialog.addAction(UIAlertAction(title: "OK", style: .default) { _ in
            // 本来はAPI通信の結果をresultに入れています。(今回は割愛)
            let result:Bool = true
            switch result {
            case true:
                AlertUtil.showAlert(title: "Success", message: "ログアウトに成功しました。",viewController: self)
                // 以下に画面遷移処理を追加。(今回は割愛)
            case false:
                AlertUtil.showAlert(title: "ログアウト失敗", message: "ログアウトに失敗しました。", viewController: self)
            }
            dialog.addAction(UIAlertAction(title: "閉じる", style: .cancel))
            self.present(dialog, animated: true)
        })

    }

}
AlertUtil.swift
struct AlertUtil {
    static func showDialog(title: String, message: String, viewController: UIViewController) {
        let dialog = UIAlertController(title: title,
                                       message: message,
                                       preferredStyle: .alert)
        viewController.present(dialog, animated: true) {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
                dialog.dismiss(animated: true, completion: nil)
            }
        }
    }

    static func showAlert(title: String, message: String, viewController: UIViewController) {
        DispatchQueue.main.async {
            let alert = UIAlertController(title: title,
                                          message: message,
                                          preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "キャンセル",
                                          style: .default))
            viewController.present(alert, animated: true)
        }
    }
}

上のコードだと、

:raising_hand:
アラート関連の処理はAlertUtilにまとめちゃお!
でも、API通信の結果によってaddActionの処理を分けたいから・・・
最初のダイアログだけViewControllerで作っちゃお!

って感じ。

でも、他の部分ではアラート表示をAlertUtilで行なっているから、

なるべく統一していきたいな・・・

という事で:open_hands:

alertActionを引数にとる

スッキリ統一感のあるコードにするために、alertActionを引数にとる形に変更してみます。

AccountViewController.swift
import UIKit
final class AccountViewController: UIViewController {
    @IBAction private func tappedLogoutButton(_ sender: UIButton) {
        let alertAction: UIAlertAction = UIAlertAction(title: "OK", style: .default) { _ in
            // 本来はAPI通信の結果をresultに入れています。(今回は割愛)
            let result:Bool = true
            switch result {
            case true:
                AlertUtil.showAlert(title: "Success", message: "ログアウトに成功しました。",viewController: self)
                // 以下に画面遷移処理を追加。(今回は割愛)
            case false:
                AlertUtil.showAlert(title: "ログアウト失敗", message: "ログアウトに失敗しました。", viewController: self)
            }
        }
        AlertUtil.showChoiceDialog(title: "ログアウト", message: "ログアウトしますか?", viewController: self, alertAction: alertAction)
    }
}
AlertUtil.swift
struct AlertUtil {
    static func showDialog(title: String, message: String, viewController: UIViewController) {
        let dialog = UIAlertController(title: title,
                                       message: message,
                                       preferredStyle: .alert)
        viewController.present(dialog, animated: true) {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
                dialog.dismiss(animated: true, completion: nil)
            }
        }
    }

    static func showAlert(title: String, message: String, viewController: UIViewController) {
        DispatchQueue.main.async {
            let alert = UIAlertController(title: title,
                                          message: message,
                                          preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "キャンセル",
                                          style: .default))
            viewController.present(alert, animated: true)
        }
    }

    // このメソッドを追加
    static func showChoiceDialog(title: String, message: String, viewController: UIViewController, alertAction: UIAlertAction) {
        let dialog: UIAlertController = UIAlertController(title: title,
                                                          message: message,
                                                          preferredStyle: .alert)
        dialog.addAction(UIAlertAction(title: "キャンセル", style: .default))
        dialog.addAction(alertAction)
        viewController.present(dialog, animated: true)
    }
}

こうする事で、

showAlert系のメソッドは全てAleretUtilで作成する事に統一し、

AccountViewController内の記述もスッキリしました:ok_woman::sparkles:

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

【SwiftUI】RPGにあるような攻撃エフェクトを再現してみたい!

はじめに

こんにちは!@kanato4です。

仕事ではJavaがメインなのですが、プライベートではSwiftを勉強している若輩者です。ただ、Swiftははじめて間もないので基本的な文法しか分かりません。これからもっと知識をつける予定です。

さて、そんなSwift初心者の私ですが、何を思ったのか「思いきってSwiftUIを使って遊んでみよう!」と考えたわけです。

SwiftUIは2019年に発表されたフレームワークなのですが、従来のSwiftの書き方とかなり違ってくるという話だったので、「やっぱこれからiPhoneアプリ作るなら知っておかないとね☆」と挑戦したわけですが、案の定苦労しました。。

今回はそんな私がはじめてSwiftUIで作れた成果物?を折角なら見てもらいたいなぁと思い記事にしました。アウトプット大事!

完成したもの

まずは完成したものを見てやってください。
Image from Gyazo
内容は簡単なもので、ボタンを押すと爆発するアニメーションが表示されます。同時に、真ん中のイラストもボタンを押す前後で変化するようにしました。もういちどボタンを押すとリセットされます。

爆発は繰り返される事なく、一回だけ爆発して終了します。(意外とここで躓いた)

正直、Swiftを学び始めて間もないのでこれだけでも結構苦労しました。。
まだまだ課題は山積みといった感じですが、とりあえず形になって良かったなという感想です。

実際のコード

実際に実装したコードがこちらになります。

ContentView.swift
import SwiftUI

struct ContentView: View {
    //ステートプロパティ
    @State private var trigger = false

    var body: some View {
        VStack {
            Text("爆破スイッチ")
                .font(.largeTitle)
            Spacer()
            ZStack{
                //三項演算子(条件式 ? true時の値 : false時の値)
                Image(self.trigger ? "computer_note_bad" :"computer_man")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 200, height: 200)
                //triggerがtrueだったら
                if trigger{
                    //構造体を呼び出す
                    LoadingView()
                }
            }
            Spacer()
            Button(action: {
                //クロージャ内ではプロパティを参照する際には自分自身を指すselfを指定
                //toggleメソッドはブール値(Bool型)の値を反転させる
                self.trigger.toggle()
            }) {
                //三項演算子(条件式 ? true時の値 : false時の値)
                Text(self.trigger ? "もういちど" : "ばくはつ!")
            }
            .font(.largeTitle)
            .foregroundColor(.red)
            .padding()
        }
    }
}

struct LoadingView: View {
    //ステートプロパティ
    @State private var index = 0
    //mapで1~16の数字に処理を適用し、その処理を施した配列imagesを作成する
    private let images = (1...16).map { UIImage(named: "explotion_\($0)")! }
    //指定された時間の間隔で現在の日時への接続を繰り返す
    private var timer = Timer.publish(every: 0.05, on: .main, in: .default).autoconnect()

    var body: some View {
       return Image(uiImage: images[index])
           .resizable()
           .scaledToFit()
           .frame(width: 300, height: 300, alignment: .center)
           //パブリッシャー(timer)によって発行されたデータを検出したときの処理
           .onReceive(
               timer,
               perform: { _ in
                   //クロージャ内ではプロパティを参照する際には自分自身を指すselfを指定
                   self.index = self.index + 1
                   if self.index >= 16 {
                       //timerの自動接続を停止
                       self.timer.upstream.connect().cancel()
                       self.index = 0
                   }
               }
           )
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

使用した画像はこのような感じです。
Image from Gyazo
コード中のtoggleメソッドでcomputer_mancomputer_note_badを切り替えています。

爆発エフェクトの画像はexplotion_1からexplotion_16に分けています。
explotion_1には、いわゆる透明な画像が入っている状態ですね。

@State private var trigger = false

@Stateと書かれている部分はステートプロパティと呼ばれるものです。
この属性を宣言する事でSwiftUIがプロパティを自動で監視してくれて、その値が変更されると自動でビューを更新してくれる偉いやつです。ステートプロパティへのアクセスはコンテンツビューの内部からのみに限定される事が望ましいため、privateと言うアクセス修飾子を指定しています。また、privateの装飾子を指定する事で外部からアクセスができなくなるため、必ず初期化をする必要があります。

アニメーションの再現方法としては、番号が振ってある爆発の画像を変数imagesに格納し、timerが接続されるたびにindexの数字が加算されていき、indexの数字に対応した爆発の画像をViewに返すという処理を繰り返して再現しています。

また、アニメーションの処理が記述されている構造体のif文の次の1文が大事。

self.timer.upstream.connect().cancel()

この1文で接続をキャンセルしないと永遠と爆発してしまいます。
Image from Gyazo
ノートPCが可哀想ですね。。

ここで、爆発の画像の最初の1つ(explotion_1)を透明の画像にした理由なのですが、上記の1文でキャンセルするとアニメーションは停止するのですが、最初のアニメーション画像が呼び出された状態でストップしてしまいます。
Image from Gyazo
explotion_1に爆発の画像を入れると中途半端に爆発して止まってしまいます。。

そのため、最初の画像は透明にしてます。単にコードの出来がよくない可能性もありますが、やむなくこのような形にしました。無念。

おわりに

滅茶苦茶読みづらかったでしょうが、ここまでお付き合いいただきありがとうございました。

Swiftは分からないことだらけですが、きちんとアウトプットをやっていって、ゆくゆくは個人開発したものをストアで公開できるぐらいになりたいと思います。

またちょこちょこSwiftUIで遊んでみたいなぁ。

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

iOSアプリ開発から公開までの流れ(Pingを作ってみる)

2015年頃から「Pythonista3 (Python 統合開発環境の iOS アプリ)」でアプリを作成して遊んできました。
しかし、ネイティブな iOS アプリ (や Android アプリ) も作ってみたい!そして世界に向けてアプリを発信したい!
という欲求が急に沸き立ち、MacBook を購入しました。
まずはリハビリを兼ねて Ping を作成します。

本稿は、Xcode を使用したアプリの作成から公開までの足跡と備忘録になります。

目次

Qiita で既出の手順は限定共有記事にしてあります。
新しい UI フレームワーク SwiftUIソケットプログラミングでハマったところがあれば、そのへんとあとPing の仕組み あたりを一般公開しよう思います。

  1. Xcodeプロジェクトの作成    (限定共有記事)
  2. iOSアプリのビルドと起動確認    (限定共有記事)
  3. GitHubリポジトリの作成    (限定共有記事)
  4. Gitリポジトリへのコミットとプッシュ    (限定共有記事)
  5. SF Symbolsのインストールと使用方法    (限定共有記事)
  6. アプリアイコンの作成と設定    (限定共有記事)
  7. スプラッシュ画面の作成と設定
  8. アプリ利用規約の作成と表示    (限定共有記事)
  9. マルチ言語への対応    (限定共有記事)
  10. SwiftによるICMPソケット通信の実装と考察
  11. ソケットプログラミング 2
  12. ソケットプログラミング 3
  13. ソケットプログラミング N
  14. Bluetooth な Ping の実装
  15. SIGINT の送信とシグナルハンドラ処理
  16. ジャイロスコープによる傾き検知
  17. Apple Developer Program への登録    (ここが一番の難関)
  18. アプリを利用してみたくなる App Store での見せ方ってどうすればいいの?
  19. アプリ申請
  20. アプリ公開
  21. 所感と今後について

補足:
各記事を執筆したら、どんどんここにリンクを貼っていきます。
開発進行に伴い、目次構成も更新されていきます。

前提条件

以下に示すものを用意しましょう。

  • Mac (macOS は、Catalina もしくは Big Sur の最新バージョン)
  • Xcode (最新バージョン)
  • iPhone(iOS は最新バージョンを推奨)
  • Apple ID(開発向けサブアカウントを別途作成するのが望ましいそう)
  • GitHub アカウント (ローカルリポジトリだけのソース管理でも良いけど、ソースコードは誰かの役に立つかもしれないので)
  • 約1万円 (アプリを公開するときに必要らしい) 1

私の iPhone は 11 の無印。私の Mac はコレ。
前提条件.jpg

要件定義

アプリの機能をできるだけ事前に決めましょう。
「誰向け」の「どんな特徴」のアプリにするのか。オンリーワンなアプリなのか。類似のアプリ達とどこが違うのか。

私の Ping アプリの場合は、こんな感じで差異化を図ります。

低水準 API で通信・処理内容を可視化し、利用者 (エンジニア) の Ping 理解度向上をサポート

たかが Ping されど Ping
結局ネットワークで問題が発生したら、一番活躍するのが Ping だと思うんです。仕組みが理解されればもっと活躍するはず!

アプリ要件は以下のとおり。

  • 搭載機能は、Ping と Traceroute だけのシンプルイズベスト
  • 低水準 POSIX Socket API とシステムコールトレースによる処理内容の可視化
  • ICMP / UDP 両プロトコルに対応、マルチキャスト対応、その他主要なオプション機能を提供
  • モバイルらしく Bluetooth にも対応 (WindowsOS はソケットで操作できるみたいだけど iOS はどうなんだろう)
  • 「Swift」言語と「SwiftUI」で最新トレンドをキャッチアップ 2
  • マルチ言語 (英語と日本語) による画面表示と利用規約で海外エンジニアにも積極アピール
  • iOS 14.0 以降

ソースコード

誰も興味はないでしょうけど^^
https://github.com/manabapp/SocPing/tree/main/SocPing/

さいごに

Pythonista3 での UI の書き方と SwiftUI は共通点が多く、結構サクサク作り込みができてます。
あと、Google でヒットする情報量が SwiftUI が圧倒的。ほとんどコピペでいけますね。
逆に Swift の書き方がとても難しいです。

はじめの投稿はこの辺で。初めての Qiita への投稿でした!
では、これから執筆と開発を並行してやっていきます。


  1. 私はクレジットカード持ってないので、果たしてアプリを公開できるのかとても心配。この記事完結しないかも。 

  2. 言語 (Swift or Objective-C) と UI (SwiftUI or Storyboard) は後戻りできない重要な選択になりそうです。この記事では最新の組み合わせでいきます。 

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