20201025のSwiftに関する記事は14件です。

俺的RxSwiftまとめ④(disposebagとは?)

この記事は、俺的RxSwiftまとめ①,俺的RxSwiftまとめ②,俺的RxSwiftまとめ③(Observableとは何か? - その1)の続きです。

始め方はsubscribe/終わらせ方はdispose bag

Observableの特徴として、「3つのイベント(next/error/completed)を持ち、subscribeすることでそれらの要素を取り扱えるようになる」というものがあった。
始め方はsubscribeがあるが、終わらせ方はどのようにすれば良いのか?それは、下記の3通りが考えられる。

  • errorイベントが流れてくる
  • completedイベントが流れてくる
  • disposebag(ゴミ袋)にobservarbleを追加する

今回は、3番目のdispose bagについて詳しく記す。

disposebagとは何なのか?

observableをまとめて破棄するためのゴミ袋

なぜ必要なのか?

observableの中には、completederrorを流したくないものがある(ex:〇〇)。その場合は、observableに備わっているdisposeメソッドを用いて、明示的にobservableを完了させる(≒廃棄する)必要がある。(完了させないと、永遠にsubscribe状態になり、メモリリークの原因となってしまうため。)

その際、observable個別にdisposeメソッドを使う代わりに、管理コストを抑えるためにobservableをまとめて入れるdisposebagをインスタンスのプロパティとして持たせておいて、インスタンス破棄のタイミングで全てのobservabledisposeすることができる(ViewControllerのプロパティとして持たせるパターンをよく見かける)

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

[Swift5]コードオンリーのUITabBarController

Main.storyboardは削除した状態が前提です。

まずはUITabBarControllerを継承したクラスを作ります。ここでは"TabController"

class TabController: UITabBarController {

    override func viewDidLoad() {
        super.viewDidLoad()

        var viewControllers = [UIViewController]()

        let firstViewController = FirstViewController()
        firstViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .mostRecent, tag: 1)
        viewControllers.append(firstViewController)

        let seccondViewController = SeccondViewController()
        seccondViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .mostRecent, tag: 2)
        viewControllers.append(seccondViewController)

        let thirdViewController = ThirdViewController()
        thirdViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .mostRecent, tag: 3)
        viewControllers.append(thirdViewController)

        self.setViewControllers(viewControllers, animated: false)
        self.selectedIndex = 1
    }
}

続いてSceneDelegateでTabControllerを初期画面に設定

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }

        guard let windowScene = (scene as? UIWindowScene) else { return }
        let window = UIWindow(windowScene: windowScene)
        self.window = window

        window.rootViewController = TabController()
        window.makeKeyAndVisible()
    }

あとはFirstVC, SeccondVC, ThirdVCをお好きなようにカスタマイズしてください。

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

【Swift 5.3】A Swift Tourから学ぶSwift【Xcode 12.1】<10/25作成中>

はじめに

(参考①)A Swift Tour
↑本記事の主となるSwift公式サイトの記事。

目次

作成中

A Swift Tour

"Hello, world!"

どの言語を学ぶにしても、最初に必ず作成・実行するプログラムがあります。
"Hello, world!"と画面に表示させるだけのプログラムです。
Swiftでは、以下のように記述します。

print("Hello, world!")

このコードから分かることは、Swiftの簡潔性です。
Swiftと同じオブジェクト指向言語であるJavaでは、以下のように記述します。

java
class Hello {
 public static void main(String[] args) {
  System.out.print("Hello, world!");
 }
}

Javaは"Hello, world!"と表示させるだけのプログラムでさえ、5行も要するのに対し、Swiftは1行で済んでしまいます。main()関数も、行の末尾に;(セミコロン)も必要ありません。

単純変数(Simple Values)

定数(constant)を宣言(declaration)するときはlet変数(variable)を宣言するときはvarを使います。

Hello,world!
var myVariable = 42  // 変数:myVariable = 42
myVariable = 50   // 変数:myVariable = 50(値の上書き)
let myConstant = 42  // 定数:myConstant = 42

print(myVariable, myConstant)  // 実行結果: 50 42

ここで、変数(定数含む)には、型(type)という概念があります。
日常生活でも、"25"と表すのに整数(integer)を使ったり、理系の論文では数値の精度を求められるので単精度浮動小数点数(float; single floating point number)倍精度浮動小数点数(double; double floating point number)を使ったりします。
型の一覧は以下の通りです。

基本型(primitive type)

数値型(numeric type)

整数型(integer type) ・・・ 整数を表す

char(character) ・・・ 0 〜 65,535(=2^16 - 1)
byte ・・・ -128(=-2^7) 〜 127(2^7 - 1)
short ・・・ -32,768(=-2^15) 〜 -32,767(=2^15 - 1)
int ・・・ -2,147,483,648(=-2^31) 〜 2,147,483,647(=2^31 - 1)
long ・・・ -9,223,372,036,854,775,808(=-2^63) 〜
      9,223,372,036,854,775,807(=2^63 - 1)

浮動小数点数型(floating point number type) ・・・ 実数を表す

float ・・・ ±3.40282347E+38 〜 ±1.40239846E-45
double ・・・ ±1.79769313486231507E+378 〜
       ±4.94065645841246544E-324

論理型(boolean type) ・・・ 論理値(真または偽)を表す

boolean ・・・ 真 または 偽

参照型(reference type)

配列型(array type) ・・・ 同一型の変数の集合を[]で表す

クラス型(class type) ・・・ メソッドやデータをまとめる

インタフェース型(interface type) ・・・ メソッドを実装(implement)する

空型(null type) ・・・ 何も参照しない

型については、いきなり気にする必要はありません。
最初のうちは、整数はintで宣言し、実数はdoubleで宣言する、などと決めて覚えてしまえば良いと思います。

それに、Swiftのコンパイラは初期化子(initializer)から型を推論(infer)することができます。上のコードも、型を定義(definition)していないのに変数を宣言することができています。
もちろん、次のコードのように、型を明示的(explicitly)に定義(=型アノテーション)することもできます。
型推論をしてくれるのは便利ですが、バグの原因にもなるのでなるべく明示的に型を定義するようにしましょう。

明示的な型定義
let implicitInteger = 70   // 暗示的(implicit)な型定義
let implicitDouble = 70.0  // 暗示的な型定義
let explicitDouble: Double = 70  //明示的な型定義

print(implicitInteger, implicitDouble, explicitDouble)
// 実行結果: 70 70.0 70.0



Swiftでは、型が暗黙的に他の型に変換(convert)されることはありません。
そのため、以下のようなコードを記述するとエラーが出力されます。

error
let label = "The width is "
let width = 94
let widthLabel = label + width

print(widthLabel)
// 実行結果:
// error: binary operator '+' cannot be applied to operands of type 'String' and 'Int'

String型(=文字列)Int型(=整数)+演算子で連結することはできません。Int型String型に変換してから連結する必要があります。
型変換は以下のようにして行います。

型変換
let label = "The width is "
let width = 94
let widthLabel = label + String(width)  // Int型widthをString型に変換

print(widthLabel)
// 実行結果: The width is 94

文字列に値を埋め込むのであれば、型変換を行わずとも表示させる方法があります。
値を()で囲み、()の直前に\(バックスラッシュ)を記述するだけです。
なお、Macにおける\のショートカットキーは⌥(option) と ¥になります。

文字列への値の埋め込み
let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."

print(appleSummary)
print(fruitSummary)
// 実行結果:
// I have 3 apples.
// I have 8 pieces of fruit.



複数行にわたる文字列を記述したい場合は、"""(ダブルクォテーション)で囲いましょう。
なお、可読性を向上させるためにSpaceTabを入力した場合は、出力結果にもSpaceTabが含まれている状態になります。""で囲っているため、SpaceTabも一つの文字列として扱われています。
可読性は落ちますが、出力結果に含みたくない場合は左づめで記述するようにしましょう。

複数行にわたる文字列の記述
let quotation = """
  I said "I have \(apples) apples."
  And then I said "I have \(apples + oranges) pieces of fruit."
"""

print(quotation)
// 実行結果:
//   I said "I have 3 apples."
//   And then I said "I have 8 pieces of fruit."



配列(array)辞書(dictionary; 連想配列)を生成するには、[]でインデックスやキーを囲みましょう。
各構成要素は,(コンマ)で列挙します。

配列・辞書の生成
//配列の生成
var shoppingList = ["catfish", "water", "tulips",]   // 型推論
shoppingList += [bread]   // 構成要素の追加(append)
shoppingList[1] = "bottle of water"   // 構成要素の更新

var wishList: [String] = ["salmon", "soda", "violet",]  // 型アノテーション

print(shoppingList[1])
print(shoppingList[3])
print(wishList[0])
// 実行結果:
// bottle of water
// bread
// salmon

//辞書の生成
var occupations = [   // 型推論
  "Malcolm": "Captain",
  "Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations"  // キー"Jayne"の追加
occupations["Kaylee"] = "Engineer"  // キー"Kaylee"の更新

var currency: [String: String] = [  // 型アノテーション
  "JPN": "yen",
  "USA": "dollar",
]

print(occupations["Jayne"]!)
print(occupations["Kaylee"]!)
print(currency["USA"]!)
// 実行結果:
// Public Relations
// Engineer
// dollar

配列のインデックスに基づいた構成要素の出力はprint(配列変数名[インデックス])と記述したのに対して、辞書のキーに基づいた値の出力はprint(辞書名["キー"]!)と記述しています。
辞書の値を出力する際に末尾に!を付けるのは、print()関数は引数として非Optional型の変数しか認めていないからです。

<<ここから下は勉強中につき誤りがあります>>
辞書occupationsも、辞書currencyもOptional型String?型
辞書の戻り値がOptional型(=nil許容型)であるからです。先述した通り、Swiftでは異なる型の
辞書occupationsは型推論によって、戻り値の型がString?型になっています。型アノテーションを記述した辞書currencyも、戻り値の型をString型と指定していますが、実際にはString?型になっています。

EOF

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

【Swift】case-letによるパターンマッチをループする

この記事はなに?

Appleのサンプルコードを読んでいる時に見かけない構文を見つけたので、調べました。

サンプルコードの一部
for case let foundObject as VNRecognizedObjectObservation in results {
    let bestLabel = foundObject.labels.first!   
    let objectBounds = foundObject.boundingBox
    print(bestLabel.identifier, bestLabel.confidence, objectBounds) 
}

気になったのは、1行目です。
for case let foundObject as VNRecognizedObjectObservation in results
コレクションに対して、処理を反復していることは理解できましたが、case let x as T が謎でした。
ちなみに、このサンプルコードは、Visionフレームワークを使った画像認識アプリです。
resultsには、Visionによる検出結果が並んでいると思われます。

マッチした値だけキャストする

単純にしたコードを実行して、動作を確認しました。

実行環境

Xcode 12.1
Swift 5.3
macOS 10.15.7

要素の型が一貫していない配列
let values: [Any] =  [ "1", 2, 3.4]
配列の要素のうち「Int型にキャストできる値」に対してのみ、処理を行う
for case let number as Int in values {
    print(number)
}
// Prints 2

考察

同じ実行結果を得るだけであれば、他にもいろんなアプローチがありそうです。
ただ、Appleのサンプルコードだけあって、極めてシンプルな記述で実装されているのではないかと思います。

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

俺的RxSwiftまとめ③(Observableとは何か?)

この記事は、俺的RxSwiftまとめ①,俺的RxSwiftまとめ②の続きです。

Observableとは何か?

observable,observable sequence,sequence,streamこれらは全部同じ意味を持つ言葉で、イベントの流れである。(RxSwiftでは、sequenceということが使われることが多い)

image.png
(引用)

Everything is a sequenceなのだ。

ここでいうイベントとは、数値や自身で定義したクラスのインスタンス、タップジェスチャーなどが含まれる。

このイベントは、以下のEnumで定義されている。

Event.swift
public enum Event<Element> {
    case next(Element)
    case error(Error)
    case completed
}

それぞれのcaseの場合どうなるか、Observable(sequence) 下記を表すマーブルダイアグラムとともに見ると、
image.png
nextの場合は、Observableは次のイベントを放出する。また、error/completedが呼ばれるまで、イベントを放出し続ける。

image.png
errorの場合はErrorを放出し、observableはイベントの放出をストップする。

image.png
completedの場合は、observableが停止し、イベントを放出をストップする。

どうやってObservableを用いて処理をするか?

Observableから放出されるイベントを使用するためには、subscribeする必要がある。(ObservableSubscribeされるまで、イベントを放出したり、operatorでの処理を行わない)
この際、イベントタイプごとに、handlerを追加することができる。
また、nextだったらElement,errorだったらエラーインスタンスをhandlerに渡すことができる。

下図のマーブルダイアグラムに示されたObservableについて考える

image.png

Example.swift
let one = "1"
let two = "2"
let three = "3"

let observable = Observable.of(one, two, three)
observable.subscribe { event in
  print(event)
}
出力
next(1)
next(2)
next(3)
completed

subscribeすると、イベントが順次扱えるようになる。
それをprintすると、出力のようにnextが3回流れ、最後にobservableの停止を意味するcompletedが流れてくる。

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

俺的RxSwiftまとめ③(Observableとは何か? - その1)

この記事は、俺的RxSwiftまとめ①,俺的RxSwiftまとめ②の続きです。

Observableとは何か?

observable,observable sequence,sequence,streamこれらは全部同じ意味を持つ言葉で、イベントの流れである。(RxSwiftでは、sequenceということが使われることが多い)

image.png
(引用)

Everything is a sequenceなのだ。

ここでいうイベントとは、数値や自身で定義したクラスのインスタンス、タップジェスチャーなどが含まれる。

このイベントは、以下のEnumで定義されている。

Event.swift
public enum Event<Element> {
    case next(Element)
    case error(Error)
    case completed
}

それぞれのcaseの場合どうなるか、Observable(sequence) 下記を表すマーブルダイアグラムとともに見ると、
image.png
nextの場合は、Observableは次のイベントを放出する。また、error/completedが呼ばれるまで、イベントを放出し続ける。

image.png
errorの場合はErrorを放出し、observableはイベントの放出をストップする。

image.png
completedの場合は、observableが停止し、イベントを放出をストップする。

どうやってObservableを用いて処理をするか?

Observableから放出されるイベントを使用するためには、subscribeする必要がある。(ObservableSubscribeされるまで、イベントを放出したり、operatorでの処理を行わない)
この際、イベントタイプごとに、handlerを追加することができる。
また、nextだったらElement,errorだったらエラーインスタンスをhandlerに渡すことができる。

下図のマーブルダイアグラムに示されたObservableについて考える

image.png

Example.swift
let one = "1"
let two = "2"
let three = "3"

let observable = Observable.of(one, two, three)
observable.subscribe { event in
  print(event)
}
出力
next(1)
next(2)
next(3)
completed

subscribeすると、イベントが順次扱えるようになる。
それをprintすると、出力のようにnextが3回流れ、最後にobservableの停止を意味するcompletedが流れてくる。

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

俺的RxSwiftまとめ③(Observableとは何か? )

この記事は、俺的RxSwiftまとめ①,俺的RxSwiftまとめ②の続きです。

Observableとは何か?

observable,observable sequence,sequence,streamこれらは全部同じ意味を持つ言葉で、イベントの流れである。(RxSwiftでは、sequenceということが使われることが多い)

image.png
(引用)

Everything is a sequenceなのだ。

ここでいうイベントとは、数値や自身で定義したクラスのインスタンス、タップジェスチャーなどが含まれる。

このイベントは、以下のEnumで定義されている。

Event.swift
public enum Event<Element> {
    case next(Element)
    case error(Error)
    case completed
}

それぞれのcaseの場合どうなるか、Observable(sequence) 下記を表すマーブルダイアグラムとともに見ると、
image.png
nextの場合は、Observableは次のイベントを放出する。また、error/completedが呼ばれるまで、イベントを放出し続ける。

image.png
errorの場合はErrorを放出し、observableはイベントの放出をストップする。

image.png
completedの場合は、observableが停止し、イベントを放出をストップする。

どうやってObservableを用いて処理をするか?

Observableから放出されるイベントを使用するためには、subscribeする必要がある。(ObservableSubscribeされるまで、イベントを放出したり、operatorでの処理を行わない)
この際、イベントタイプごとに、handlerを追加することができる。
また、nextだったらElement,errorだったらエラーインスタンスをhandlerに渡すことができる。

下図のマーブルダイアグラムに示されたObservableについて考える

image.png

Example.swift
let one = "1"
let two = "2"
let three = "3"

let observable = Observable.of(one, two, three)
observable.subscribe { event in
  print(event)
}
出力
next(1)
next(2)
next(3)
completed

subscribeすると、イベントが順次扱えるようになる。
それをprintすると、出力のようにnextが3回流れ、最後にobservableの停止を意味するcompletedが流れてくる。

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

Core ML Toolsでレイヤーを編集する方法

Core ML Toolsを使ってモデルを変換していると、レイヤーを追加して間に計算処理を入れたり、レイヤーの一部をカットしたくなることがありますが、本稿はその方法を説明します。

Core ML Toolsは、ドキュメントも少なくネット上にも情報が少ないため、ソースコードを読みながら調べるしかないのですが、レイヤーを編集する作業はCore MLへの変換ではよくあるユースケースだと思いますので、この記事が役に立てばと思います。

モデルケース:出力の前にreshapeを入れて、出力のshapeを変更する

モデルケースとして、モデルの出力のshapeが(19)のものを、reshapeを追加して(1,1,19)にしてみます。

次元を追加しているだけなのでほとんど意味がないですが、単純なケースにしたいのでこうしています。

使用するモデルは、1桁の数字2つを与えると、それらを足した結果を予想(分類)するものを使います。分類結果は19個の配列になります。これは、1桁の数字を足すと0〜18で合計19種類の分類問題になるためです。

図にするとこんな感じです。

このモデルは以前の記事で使ったものと同じです。前回の記事は出力にラベル名まで与えていましたが、今回は単に19個の確率そのまま出力します。

Kerasで作った簡単なモデルをCoreMLを使ってiOSで動作させる

手順

それでは早速、Reshapeレイヤーを追加します。
Google Colaboratory上で作業をします。

なお、今回の作業の完全なコードはこちらにあります。
https://gist.github.com/TokyoYoshida/2fada34313385d63b666253490b5f3f4

1.Kerasのモデルを読み込む

Kerasで作ったモデルを使用します。モデルの作成部分は、前回の記事にあるので省略します。

notebook
from keras.models import load_model
keras_model = load_model('my_model.h5')

2.CoreMLに変換する

CoreMLに変換します。今回はラベルを貼らないのでシンプルにそのまま変換しています。

notebook
from coremltools.converters import keras as converter
mlmodel = converter.convert(keras_model)

3.ビルダーで読み込む

Core ML ToolsのNeuralNetworkBuilderに読み込みます。

notebook
import coremltools
spec = coremltools.utils.load_spec(coreml_model_path)
builder = coremltools.models.neural_network.NeuralNetworkBuilder(spec=spec)

モデルの情報を表示してみます。
ここから、モデルの出力はoutput1という名前で、shapeは19であることがわかります。

notebook
spec.description.output

# 出力結果
# [name: "output1"
# type {
#   multiArrayType {
#     shape: 19
#     dataType: DOUBLE
#   }
# }
# ]

レイヤーの情報を表示します。

notebook
builder.layers
# 出力
# ['dense_4',
#  'dense_4__activation__',
#  'dense_5',
#  'dense_5__activation__',
#  'dense_6',
#  'activation_18']

今回の対象は、出力レイヤーであるactivation_18です。このレイヤーの出力に対してreshapeをかけます。

4.Rehapeレイヤーを追加する

NeuralNetworkBuilderにはadd_reshapeというメソッドがあり、reshapeレイヤーを追加できます。

ここで注意点なのですが、builderを使うと直感的にはもとのモデルを表すbuilderにそのままレイヤーを追加できるような気がしていまいますが、この方法だとうまくいきません。

例えば、先程builderに読み込んだモデルにそのままReshapeレイヤーを追加してみます。

notebook
reshape = builder.add_reshape(name='Reshape', input_name='activation_18', output_name='output', target_shape=(1,1,19), mode=0)

これをXcodeで読み込むとこんなエラーがでます。

Xcodeのエラー
There was a problem decoding this CoreML document
validator error: Layer 'Reshape' consumes an input named 'activation_18' which is not present in this network.

Python上でCoreMLの推論を実行しようとするとこんなエラーになります。

Pythonのエラー
RuntimeError: Error compiling model: "Error reading protobuf spec. validator error: Layer 'Reshape' consumes an input named 'activation_18' which is not present in this network.".

ネットで探してもなかなかわからず苦労したのですが、きっと同じことで苦労する人がいるのではと思います。

対処法ですが、NeuralNetworkBuilderを使って、レイヤーが1つだけの完結したモデルを作り、それを元のモデルに追加するようにします。

図にするとこんな感じです。

まずは新しいモデルを作ります。

notebook
from coremltools.models.neural_network import datatypes

input_dim = (1,19)
output_dim = (1,1,19)
input_features = [('input2', datatypes.Array(*input_dim))]
output_features = [('output2', datatypes.Array(*output_dim))]
# 新しいモデルを作成する。入力と出力のshapeはこれから作るreshapeレイヤーと同じものを与える。
new_layer_builder = coremltools.models.neural_network.NeuralNetworkBuilder(input_features, output_features)

新しいモデルにreshapeレイヤーを追加します。
inputは、元のモデルのoutput、つまりoutput1を指定します。これは新しいモデルの入力であるinput2のような気がしましたが、それだと元のモデルのoutputとreshapeレイヤーへの入力の紐付けがうまくいかないようです。
outputは、新しいモデルのoutputであるoutput2を指定します。
target_shapeは今回変換したいshapeである(1,1,19)を指定します。

notebook
new_layer_builder.add_reshape(name='Reshape', input_name='output1', output_name='output2', target_shape=(1,1,19), mode=0)

これで、reshapeレイヤーだけのモデルができました。
情報を出力してみます。

notebook
new_layer_builder.spec.description

# 出力
# input {
#   name: "output1"
#   type {
#     multiArrayType {
#       shape: 1
#       shape: 19
#       dataType: DOUBLE
#     }
#   }
# }
# output {
#   name: "output2"
#   type {
#     multiArrayType {
#       shape: 1
#       shape: 1
#       shape: 19
#       dataType: DOUBLE
#     }
#   }
# }

次に元のモデルに対して、reshapeレイヤーだけのモデルを追加します。

layers = builder.spec.neuralNetwork.layers
layers.extend(new_layer_builder.spec.neuralNetwork.layers)

これで、reshapeを追加したモデルができました。

レイヤー情報を確認します。

notebook
builder.layers

# 出力
# ['dense_4',
#  'dense_4__activation__',
#  'dense_5',
#  'dense_5__activation__',
#  'dense_6',
#  'activation_18',
#  'Reshape']

5.モデルの出力情報を変更する

モデルの最後にreshapeレイヤーがついたわけですが、それだけだとモデルの最後のレイヤーが差し替わっただけであり、モデル全体としての出力情報は変わっていません。

試しに出力情報を確認すると、期待したshapeである(1,1,19)ではなく(19)のままであることがわかります。

notebook
spec.description.output
# [name: "output1"
# type {
#   multiArrayType {
#     shape: 19
#     dataType: DOUBLE
#   }
# }
# ]

モデルの出力情報の変更は、これまたクセが強くてbuilder.spec.description.output[0]に対して代入しようとするとエラーになります。そのため、popしたりaddしたりしながら指定してきます。

notebook
// 出力情報を1つ削除する今回は出力情報は1つしかなかったので出力情報が消える
builder.spec.description.output.pop()
// 出力情報を1つ追加する
builder.spec.description.output.add()
// 出力情報の属性を指定する
output = builder.spec.description.output[0]
output.name = "output2"
output.type.multiArrayType.dataType = coremltools.proto.FeatureTypes_pb2.ArrayFeatureType.ArrayDataType.Value('DOUBLE')
// shape情報として(1,1,19)を設定する
output.type.multiArrayType.shape.append(1)
output.type.multiArrayType.shape.append(1)
output.type.multiArrayType.shape.append(19)

(補足)
上の例では、pop()で消してadd()で追加することでクリーンな状態にしてから情報を追加していましたが、もともと存在するoutputの属性値だけを直接書き換えることはできます。

例えば、shapeを書き換えるのは次のようにします。

shapeを書き換える例
builder.spec.description.output[0].type.multiArrayType.shape[0] = 100

(補足終わり)

出力情報を確認すると、うまく設定できていることがわかります。

notebook
builder.spec.description.output
# [name: "output2"
# type {
#   multiArrayType {
#     shape: 1
#     shape: 1
#     shape: 19
#     dataType: DOUBLE
#   }
# }
# ]

6.モデルの出力結果をJupyter Notebookで確認する

モデルの出力結果をMac上で動作しているJupyter Notebookで確認します。
なぜMacでJupyter Notebook?という話なんですが、Google ColaboratoryはバックエンドがLinuxなのでCoreMLの推論はできず、モデルが正しく動作しているかの確認ができません。

Xcodeで推論してもよいのですが、Xcodeはおかしなモデルを読むとエラーを出してくれることもありますが、ビルド時に次のエラーを吐いていきなり落ちることもあります。

Xcodeのエラー
Command CoreMLModelCompile failed with a nonzero exit code

こうしたエラーが出たときは、Jupyter Notebookで実行することでエラーの詳細の情報を確認すると良いです。

ということで、Jupyter Notebookを起動します。

こちらに完全なコードがあります。
https://gist.github.com/TokyoYoshida/632b4c8070aa6c937539e4ae261a2740

モデルへの入力として[2,3]を与えて推論を実行してみます。

JupyterNoteBook
coreml_model_path= "my_model_with_builder.mlmodel"

import coremltools
spec = coremltools.utils.load_spec(coreml_model_path)
builder = coremltools.models.neural_network.NeuralNetworkBuilder(spec=spec)

mlmodel = coremltools.models.MLModel(spec)

mlmodel.predict({'input1': np.array([2.0,3.0])})

# {'output2': array([[[2.56040733e-21, 7.25779588e-15, 1.99342376e-10, 1.11184195e-09,
#           5.92091055e-05, 9.99939799e-01, 9.72097268e-07, 1.13292452e-14,
#           4.43997455e-23, 5.00404492e-33, 0.00000000e+00, 0.00000000e+00,
#           0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
#           0.00000000e+00, 0.00000000e+00, 0.00000000e+00]]])}

今回のモデルは2つの要素を足し算してくれるモデルなので、5番目の要素の確率がほぼ1になっていることから正しく推論できていることがわかります。

また、出力されたarrayが[[[確率の入った19個の配列]]]というとなっていることから、shapeが(1,1,19)と意図通りになっていることが確認できます。

7.モデルの出力結果をXcode & アプリで確認する

最後に、モデルの出力結果をXcodeに載せてアプリで確認します。

完全なコードはこちらです。
https://github.com/TokyoYoshida/CoreMLSimpleTest

ViewController.swift
let model = my_model_with_builder()
let inputArray = try! MLMultiArray([2,3])
let inputToModel = my_model_with_builderInput(input1: inputArray)
if let prediction = try? model.prediction(input: inputToModel) {
print(prediction.output2)
try! print(prediction.output2.reshaped(to: [19]))
}

// # Double 1 x 1 x 19 array
// # Double 19 vector
// # [2.422891379885771e-21,7.01752566646674e-15,1.959301054732521e-10,1.089580203839091e-09,5.933549255132675e-05,0.9999396800994873,9.530076567898504e-07,1.087061586308846e-14,4.16250629238845e-23,4.617410135639087e-33,1.401298464324817e-45,1.401298464324817e-45,1.401298464324817e-45,1.401298464324817e-45,1.401298464324817e-45,1.401298464324817e-45,0,0,0]
// 

こちらも意図通りになっていることが確認できます。
確率の数字が微妙に違うかもしれませんが、これはモデルを訓練しなおしたためなので気にしないで下さい。

番外編 レイヤーを削除する方法

動作確認はしていませんが、レイヤーを削除する方法も書いておきます。

レイヤーを削除する。

notebook
layers = builder.spec.neuralNetwork.layers
# レイヤーの後ろから2番めを取得(activation_18)
item = layers[-2]
# その要素を削除
layers.remove(item)

レイヤーの情報を表示すると、削除されていることがわかります。

notebook
for layer in layers:
  print(layer.name)
# dense_4
# dense_4__activation__
# dense_5
# dense_5__activation__
# dense_6
# Reshape

最後に

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

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

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

【Swift 】通知を出す方法について

通知を許可しているか確認

通知を出す場合、事前に通知を許可しているか確認する必要があります。

// 通知許可の取得
UNUserNotificationCenter.current().requestAuthorization(
     options: [.alert, .sound, .badge]){
     (granted, _) in
     if granted{
          UNUserNotificationCenter.current().delegate = self
     }
}

指定時間経過で通知を出す

○秒後に実行させる場合、UNTimeIntervalNotificationTrigger(timeInterval: , repeats:)で通知を作成させる必要があります。

let content = UNMutableNotificationContent()
content.sound = UNNotificationSound.default
content.title = "タイトル"
content.subtitle = "サブタイトル"
content.body = "内容"

// 指定時間後に実行
let timer = 10

// 通知リクエストを作成
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(timer), repeats: false)
let identifier = NSUUID().uuidString
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)

// 通知リクエストを登録
UNUserNotificationCenter.current().add(request){ (error : Error?) in
     if let error = error {
          print(error.localizedDescription)
     }
}

指定時刻になったときに通知を出す

指定時刻に実行させる場合、UNCalendarNotificationTrigger(dateMatching: , repeats:)で通知を作成させる必要があります。

let content = UNMutableNotificationContent()
content.sound = UNNotificationSound.default
content.title = "タイトル"
content.subtitle = "サブタイトル"
content.body = "内容"

// 通知時刻を指定
let date = Date()
let newDate = Date(timeInterval: 60, since: date)
let component = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: newDate)

// リクエストを作成
let trigger = UNCalendarNotificationTrigger(dateMatching: component, repeats: false)
let identifier = NSUUID().uuidString
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)

// 通知リクエストを登録
UNUserNotificationCenter.current().add(request){ (error : Error?) in
     if let error = error {
          print(error.localizedDescription)
     }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

まだ冗長なif文で消耗してるの!?if文はこうやって書け

おはようございます。
二日酔いでさっきまで死んでいてEVEで回復したので
記事を書いて行こうかと思いあmす。

プログラミングに欠かせない構文に「if文」がありますが、
冗長な書き方をしていませんか?
振り返ってみましょう。リファクタリングすると、
よりスマートに書けるif文があることに気づくことは
何度かあるのではないでしょうか?

それでは冗長パターンとスマートパターンを紹介していきたいと思います。

true == true

初学者がやりがちな条件です。
if文ではデフォルトでtrue/falseで判定をしているため、== trueを記述すると
true == true == true
のような意味合いになってしまいますね。
当然ながらif true { }と書くようにしたいところです。

if true {
}

elseを利用していない

if 100 >= 50 {
} else if 100 < 50 {
}
>>
if 100 >= 50 {
} else {
}

かなり簡単に記述していますが、複雑な条件になってくると
これと同じような記述になっているif文をちらほらと見かけます。

条件式の例

1つだけ重い重りを含んだ9つのボールを、
天秤にかけて見つけ出したい時に最も少ない回数で判別するにはどうしたら良いでしょうか?
2回で判別できるのですが、皆さんは何回でできましたか?

if left(3) > right(3) {
  if left(1) > right(1) {
    //判別可能
  }
} else {
  if left(1) > right(1) {
    //判別可能
  }
}

ここでのひっかけは、全てを条件式に含まなければならないと思わせることです。
9つのボールは、その全てを天秤にかける必要はありません。
「左に乗せる3つ」「右に乗せる3つ」「乗せない3つ
実はこの乗せない3つの存在が、スマートな条件式を書く上で大切です。
天秤にかけずとも、比較対象として判別することができます。

あえてelse ifを使わない

あえてelse ifを使わないパターンには、
・可読性を高めたい時
・単純に条件式としてelse ifではダメな時
があります。

可読性を高めたい時

if A == A {
} else if B == B {
}
>>
if A == A {
} else {
  if B == B {
  }
}

適切なパターンが思いつかなかったのですが、
このようにelse ifではなく elseの中でif文を書く方が見やすい場合もあります。

単純に条件式としてelse ifではダメな時

//AorBorCを流す時
if A == A {
} else if B == B {
}
>> //Cをキャッチできない
if A == A {
} else {
  if B == B {
    //Bだけに加えたい処理
  }
  //共通の処理
}

elseの内容を新たにifで書くな

if A == A {
}
if B == B {
}
if C == C {
}

AorBorCを流す時、このようにifを3つ書くのは推奨できません。
理由は余計な処理が走るからです。
else文に他条件を書くことで、マッチした時点でそれ以降の処理が走りません。
ですがif文を独立させることで、可読性は上がったとしても
パフォーマンスが落ちます。
コンピュータに余計な処理はさせたくないですね。

おわりに

いかがでしたでしょうか?
この中で最も皆さんに取り入れて欲しかったのは、
重りの条件式の例
です。これをマスターできると、条件式がかなりスマートにかけます。
参考になれば幸いです。

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

VisionKitの文書スキャナーは、コンビニのマルチコピー機のスキャナーの代わりに使えるか

iPhoneのカメラで紙の文書をスキャンして、普通のスキャナーの代わりにネット上の書類のやりとりに使えるのでしょうか? 試してみました。

AppleのVisionKitを使えば、文書をスキャンできます。
斜めに撮っても、コンピュータービジョンでまっすぐな文書に直してくれます。

Oct-25-2020 03-19-11.gif

つかいかた

import VisionKit
let documentCameraViewController = VNDocumentCameraViewController()
documentCameraViewController.delegate = self
present(documentCameraViewController, animated: true)

ドキュメントスキャナーにプリセットされているSaveボタンで写真ライブラリに保存するコードはこちら。
Info.PlistのPrivacy Camera Additional Usage Descriptionを設定しておくことを忘れずに。

// Saveボタンが押された時に呼ばれるデリゲートメソッド
func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
    let image = scan.imageOfPage(at: 0)
     UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}

コンビニのスキャナーとの比較

1、VisionKit

IMG_2736.png

2、コンビニのスキャナー

scan-001のコピー.png

コンビニの方が圧倒的に綺麗でした。

Visionで文字読み取りする前に紙から真っ直ぐにして取り込むのとかには使えそうです。

?


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium

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

iPhone(VisionKit)の文書スキャナーは、コンビニのマルチコピー機のスキャナーの代わりに使えるか

iPhoneのカメラで紙の文書をスキャンして、コピー機のスキャナーの代わりに書類のやりとりに使えるのでしょうか? 試してみました。

AppleのVisionKitを使えば、文書をスキャンできます。
斜めに撮っても、コンピュータービジョンでまっすぐな文書に直してくれます。

Oct-25-2020 03-19-11.gif

つかいかた

import VisionKit
let documentCameraViewController = VNDocumentCameraViewController()
documentCameraViewController.delegate = self
present(documentCameraViewController, animated: true)

ドキュメントスキャナーにプリセットされているSaveボタンで写真ライブラリに保存するコードはこちら。
Info.PlistのPrivacy Camera Additional Usage Descriptionを設定しておくことを忘れずに。

// Saveボタンが押された時に呼ばれるデリゲートメソッド
func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
    let image = scan.imageOfPage(at: 0)
    UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}

コンビニのスキャナーとの比較

1、VisionKit

IMG_2736.png

2、コンビニのスキャナー

scan-001のコピー.png

コンビニの方が圧倒的に綺麗でした。
蓋をして光を当てているので、シワが全然ない。

VisionKitは、Visionで文字認識する前に、紙から取り込むのとかには使えそうです。

?


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium

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

ARC104 B - DNA SequenceをSwiftで解く!

はじめに

こんにちは!屋代高校PC同好会結成会のTARDIGRADEです。
本記事ではARC104 B - DNA SequenceをSwiftで解いていきたいと思います!
2020/10/24に開かれたSwift Zoomin' #4に参加された方向けの解説になります。
競技プログラミングを始めて体験した方にもわかりやすいように、問題を読んだ時からコーディングするまでの思考の過程も伝えられたらと思います。
深夜に急いで書いたので解説が雑な部分もありますが、あらかじめご了承ください。質問等あれば、コメントかTwitterの方にお願いします。

Step1:問題を理解する

当然ですが、問題を解くには問題の内容をしっかり理解しなければなりません。競技プログラミングの問題は数学のテストのような形で書かれることも多く、初めて見る方は「うっ…」となるかもしれませんが、頑張って読み進めていきましょう。

問題を理解するときのコツ

・自分で図を書いたり式を書いたりして問われていることを可視化してみる
可視化することで効率のいい解法がぱっと思いつくことも多いです。

・テストケースをよく見る
テストケースにプログラムの動く流れが記載されていることがあります。問題の理解を進めるうえで非常に参考になるので、しっかりと読みましょう。また、複数のテストケースを見ることで「複雑に見えるけど、実際答えはAかBの二択しかない!」というように考察が進められることもあります。

B - DNA Sequenceで問われていること

「相補的」という言葉が結構なパワーワードで拒絶反応を起こしやすいですが、よく読むと問題文に「相補的」の意味が書かれています。

$|T1|=l$としたとき、どの $1≤i≤l$についても $T1,T2$の$i$文字目の組み合わせが $(A$と$T)$, または$(C$と$G)$ の組み合わせのいずれかであることを指します。

|T1|=lとしたとき、どの 1≤i≤lについてもの部分は定型文みたいなものなので、無視していただいて結構です。定義をきちんとしないと文句を言われてしまうので、問題文はめちゃめちゃ細かく丁寧に書かれますが、「大したこと言ってないじゃん」という部分も多いです。この辺は数学とよく似ていますね。

さて、T1,T2のi文字目の組み合わせが (AとT), または(CとG) の組み合わせのいずれかであることを指しますという部分だけでしたら理解はそんなに難しくないでしょう。問題文を読み進めると、ある部分文字列$T$を並べ替えたときに、$T$と相補的な文字列が存在するかどうかを判定しなくてはいけないことがわかります。相補的な文字列であるとはつまり、もともと$"A"$であったところは$"T"$、$"T"$であったところは$"A"$に代わっているということです。$(C$と$G)$についても同様のことが言えます。

ここまで考察を進めると、判定の方法についてとあることに気づきます。というか気づけるかどうかがこの問題の鍵になってくるので、あまりピンと来ていない方はしばらく考えてみましょう。

………そうです!部分文字列$T$を並べ替えたときに、$T$と相補的な文字列が存在するためには、$T$に含まれている $"A"$と$"T"$ の数が等しく、かつ $"C"$と$"G"$ の数が等しくなければなりません

これでこの問題は、「$S$ の連続する空でない部分文字列 $T$ であって、$T$に含まれている$"A"$と$"T"$ の数が等しく、かつ $"C"$と$"G"$ の数が等しい物の個数を求めよ」という問題に置き換えることができます。

このように、競プロでは「問題文をわかりやすい形に置き換える」ことも重要になります。

Step2:愚直な解法を考える

問題を簡潔にまとめられたので、次に解法を考えていきます。この時非常に参考になるのが標準入力で与えられる値の制約です。
今回の場合$N <= 5000$ですので、$O(N^2)$解法が間に合うことになります (計算量オーダーの考え方がわからない方はこちらのサイトを見てください) 。

さて、計算量の上限もわかったことですし、これから本格的に解法を考えていきましょう。$T$の中にある$"A","T","C","G"$ の数がわかれば、$T$に相補的な文字列があるかどうか判定できることがStep1で分かっているので、とりあえずはそれを愚直に書いてみます。

let input = readLine()!.split(separator: " ")
let n = Int(input[0])!
let s = Array(input[1]).map{String($0)}
var answer = 0
for i in 0..<n-1{
    for j in i+1..<n{
        var ac = 0
        var tc = 0
        var cc = 0
        var gc = 0
        for k in i...j{
            if s[k] == "A"{ac += 1}
            if s[k] == "T"{tc += 1}
            if s[k] == "C"{cc += 1}
            if s[k] == "G"{gc += 1} 
        }
        if ac == tc && cc == gc{answer += 1}
    }
}
print(answer)

実際にテストケースを入れてみると、これで正しい答えを求められていることがわかります。
ただし、このプログラムの計算量は$O(N^3)$であり、これだと実行時間制限に引っかかってしまいます (実際に提出してみるとこんな感じ) 。このように、先に計算量の上限を見積もっておけば、TLEになるコードを提出してペナルティを食らうことを防げます。

Step3:プログラムの高速化

ここから、どのように高速化すればいいのかを考えてみます。
そもそも現状では、部分文字列の指定で$O(N^2)$かかってしまっているので、「相補的な文字列が存在するかどうかの判定を$O(1)$でする」か、もしくは「部分文字列を個別に指定せずに解く方法」を考えなければなりません。今回の問題では前者について考えるとうまくいきそうです。「どの点に着目して高速化するべきか」、というのは量をこなせばだんだんとわかってくるので、今はわからなくても心配しないでください。

前者の解法はこちら。クリックして展開できます。

さて、判定を$O(1)$でやらなければならないわけですが、いったいどうすればいいでしょう。
ここで、「判定を$O(1)$で行う」ということについてもう少し詳しく考えてみましょう。ここでいう判定には、$"A","T","C","G"$ の数が必要です。具体的には$S[i]$から$S[j]$に含まれている各文字の数です。
今は$i$から$j$までループを回して数えていますが、ここを$O(1)$で取得できないでしょうか…?もしできれば判定自体も$O(1)$でできることになり、全体の計算量は$O(N^2)$に改善されます。

発想を飛ばしやすくするために、$S[i]$から$S[j]$に含まれているそれぞれの文字の数を求める式を変形してみましょう。

(求めたいもの) = ($S[i]$から$S[j]$に含まれているそれぞれの文字の数)

= $(S[i]$に含まれている各文字の数$)$ + $(S[i+1]$に含まれている各文字の数$)$ + ...... + $(S[j-1]$に含まれている各文字の数$)$ + $(S[j]$に含まれている各文字の数$)$

= $(S[1]$から$S[j]$に含まれている各文字の数$)$ - $(S[1]$から$S[i-1]$に含まれている各文字の数$)$

ここまで変形すると、だいぶ式がシンプルになりました。そして最後の式をよく見てください。これは累積和というものを使うことで$O(1)$で求めることができます
累積和については、僕が解説するよりもほかにずっとわかりやすい記事があるのでそちらを紹介しておきます。

累積和を何も考えずに書けるようにする!

これを使えば、判定を$O(1)$で、全体の処理を$O(N^2)$で行うことができます!
実装すると以下のようになります。
(この辺の解説が雑で申し訳ないのですが、一番重要なところなので、リンク先の記事等を参考に、やっていることを理解できるようにしましょう)

let input = readLine()!.split(separator: " ")
let n = Int(input[0])!
let s = input[1]
var aa:[Int] = [0]
var tt:[Int] = [0]
var cc:[Int] = [0]
var gg:[Int] = [0]
var a = 0
var t = 0
var c = 0
var g = 0
// 累積和を使うための前処理
for i in s{
    if i == "A"{a += 1}
    if i == "T"{t += 1}
    if i == "C"{c += 1}
    if i == "G"{g += 1}
    aa.append(a)
    tt.append(t)
    cc.append(c)
    gg.append(g)
}
// ここからは先ほど作成した愚直な方法と同じ
var ans = 0
for i in 0...n-1{
    for j in i+1...n{
        if aa[j]-aa[i] == tt[j]-tt[i] && cc[j]-cc[i] == gg[j]-gg[i]{
            ans += 1
        }
    }
}
print(ans)


2020/10/26 追記
問題を読み直したところ、どちらでもできることがわかりました。というかむしろ、考え方的にも実装量的にも後者の方が簡単です。

具体的に、後者の場合はどのような考え方で解いていくのかを説明したいと思います。
まず、Step2で書いたプログラムがどのように動いていくのかを眺めてみましょう。すると、計算に無駄があることに気づくと思います。具体的には↓の部分です。

for j in i+1..<n{
    //略   
    for k in i...j{
        //略
    }

一回ずつ丁寧にプログラムが動く流れを追ってみると、

$i...j$の値をループで求める
$→i...j+1$の値をループで求める
$→i...j+2$の値をループで求める
$→$以下同様に繰り返す

というように動いていることがわかります。
しかし、よく考えてみると、 $i...j+1$ の値というのは $i...j$ の値に $j+1$ の値を足すだけで求められます。わざわざ $i...j$ の値をもう一度求め直すというのは無駄な計算です。$i...j+2$ の値を求める時も同様に、 $i...j+1$ の値に $j+2$ の値を足すだけでよいのです。

つまり何が言いたいのかというと、一度計算した値は、次の値を求めるのに利用できるということです。ここを改善すると、累積和なんて使わなくても、計算量を$O(N^2)$まで落とせます。
実装してみると以下のようになります。

let input = readLine()!.split(separator: " ")
let n = Int(input[0])!
let s = Array(input[1]).map{String($0)}
var answer = 0
for i in 0..<n-1{
    var ac = 0
    var tc = 0
    var cc = 0
    var gc = 0
    for j in i..<n{
        if s[j] == "A"{ac += 1}
        if s[j] == "T"{tc += 1}
        if s[j] == "C"{cc += 1}
        if s[j] == "G"{gc += 1} 
        if ac == tc && cc == gc{answer += 1}
    }
}
print(answer)

こうやって無駄な計算をしてしまっている部分を抜き出してみると「そりゃそうじゃん」という話になるんですが、慣れてないとなかなか気づきません。はじめのうちは、「愚直な解法を考える」→「プログラムが動く流れを見て無駄な計算をしている部分がないか探す」という感じで解くことをお勧めします。

最後に

解説は以上で終わりとなります。説明がわかりにくいところも多かったかもしれませんが(僕の力不足です、すみません)、問題を解くときの大体の雰囲気が伝わってくれていればいいなと思っております。
冒頭にも書きましたが、何か質問等ございましたら、コメントかTwitterの方で受け付けております。

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

iOSアプリ開発:タイマーアプリ(9.プログレスバーの色をカスタマイズする)

記事

タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、プログレスバーに自作のグラデーションカラーを適用し、時間経過とともに色相を変化させる方法について掲載します。

環境

  • OS: macOS 10.15.7 (Catalina)
  • エディタ: Xcode 12.1
  • 言語: Swift
  • 主な使用ライブラリ: SwiftUI

手順

  1. プログレスバーをどういう色にするか検討する
  2. ProgressBarView に変化する色相(Hue)の数値を格納するプロパティを作成する
  3. ProgressBarView にグラデーションを生成するメソッドを作成する
  4. プログレスバーの Circle() の .stroke モディファイアの色の引数にメソッドを追加する
  5. ProgressBarView に .onReceive モディファイアを追加し、 プログレスバーの色を常に変化させる

1. プログレスバーをどういう色にするか検討する

プログレスバーの色を美しく表現するにはどうすれば良いかを考えます。これは人によって好みが別れるかもしれません。私の場合、は以下の2点が重要だと考えました。
一つは、単色ではなくグラデーションで表示すること。
もう一つは、時間経過とともに色が変化すること。
この2つを実装して、アプリのビジュアル的な印象を向上させます。

2. ProgressBarView に変化する色相(Hue)の数値を格納するプロパティを作成する

グラデーションを作るには最低 2 色が必要です。この2色を自作していきます。

Swift では、色の指定方法はいくつかあります。それは一般的な色の規格とも合致しています。例えば、RGB、CMYK、HSB などです。

簡単に説明すると、RGB は Red、Green、Blue の光の三原色をそれぞれどの程度配合するかで色を表現するものです。3 色全て 100% なら白、 0% なら黒になります。

CMYK は色の三原色 + K:Black で、RGB とは逆に 3 色全て 100% なら黒、0% なら白になります。インクジェットプリンタを想像するとわかりやすいかと思います。

そして、HSB は、色相(Hue)、彩度(Saturation)、明度(Brightness)で色を表します。Hue は何色かを示し、Saturation は色味がどの程度か(0ならモノトーン)、Brightness は白の割合で 100% なら白なので色が濁りません。

最終的に色を常に変化させることを考えると、HSB ならば、Hue の値のみを変化させ、Saturation と Brightness を固定しておけば調整しやすそうですので、ここでは HSB での色指定を採用します。

それでは ProgressBarView に色相(Hue)を格納するプロパティを2色分追加します。それぞれ customHueA、customHueB とします。

HSB での色指定の場合、Color() は以下のような引数が求められます。それぞれデータ型は CGFloat 型で、最小値が 0.0、最大値が 1.0 と定められています。

Color(hue:, saturation:, brightness:)

そのため、新規に追加する Hue のプロパティの値の初期値として、ひとまず 0.5 と 0.3 をそれぞれ代入しておきます。0.5 が青緑色、 0.3 が黄緑色で、比較的近い色味にしておきます。

例えば 0.0 と 0.5 だと色相が真逆になって最終的にグラデーションにしたときに中間が濁った色になりがちです。そのため、ここでは色相の差を 0.2 にとどめておきました。ちなみに Hue 0.0 と Hue 1.0 では一周回って同じ色になります。

ProgressBarView
struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    @State var costomHueA = 0.5
    @State var costomHueB = 0.3

    var body: some View {
        //(省略)
    }
}

3. ProgressBarView にグラデーションを生成するメソッドを作成する

次に、Hue の値を格納した2つのプロパティから、円形プログレスバーにふさわしいグラデーションを作成するメソッドを作成していきます。

メソッドの名前は makeGradientColor としました。

引数は Double 型のデータをとる引数 hueA と hueB です。ここに先に作成したプロパティを入れる予定です。

そして、戻り値のデータ型は、円すい状グラデーションの AngularGradient です。ちなみに、Swift のグラデーションは他にも、線形グラデーション(LinearGradient)や放射状グラデーション(RadialGradient)があります。円をぐるりと一周する方向でグラデーションがかかるのが AngularGradient です。

func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {}

次に makeGradientColor メソッドの {} の中を記述していきます。まず、メソッドの {} 内の最初にグラデーションに必要な2色を作るプロパティを用意します。

ColorA は引数 hueA の値から色を作ります。ここには ProgressBarView のプロパティ costomHueA が入る予定です。同様に、ColorB は引数 hueB の値から色を作ります。こちらには costomHueB が入る予定です。

ColorA も ColorB も saturation と brightness は同じ数値を入れます。再度はやや高め、明度はかなり高めにしています。

func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
}

そして最後に、作った2色から円錐型グラデーションを作ります。AngularGradient 構造体のインスタンスを作成するわけですが、引数 gradient には イニシャライザ .init() を入れ、さらにその引数には Array でグラデーションの開始から終了までの色を入れます。

ここで以下のようにすると、円の始点と終端の重なるところで、くっきり色が分かれてしまいます。

[colorA, colorB]

ですので、以下のように、colorB からまた colorA へグラデーションをかけるようにします。

[colorA, colorB, colorA]

もっと細かく中間にも色を置いて調整することもできます。例えば以下のような具合です。ここでは2色でのグラデーションにとどめておきます。

[colorA, colorB, colorC, colorD, colorA]

func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
let gradient = AngularGradient(gradient: .init(colors: [colorA, colorB, colorA]), center: .center, startAngle: .zero, endAngle: .init(degrees: 360))
return gradient
}

メソッドは body{} の下に記述します。

ProgressBarView
struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    @State var costomHueA = 1.0
    @State var costomHueB = 0.5

    var body: some View {
        //(省略)
    }

    func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
        let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
        let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
        let gradient = AngularGradient(gradient: .init(colors: [colorA, colorB, colorA]), center: .center, startAngle: .zero, endAngle: .init(degrees: 360))
        return gradient
    }
}

4. プログレスバーの Circle() の .stroke モディファイアの色の引数にメソッドを追加する

これまでプログレスバーの円の色は .stroke モディファイアの引数で単色のシアンを指定していました。

.stroke(Color(.cyan), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))

ここで、さきほど作ったメソッドを Color(.cyan) の代わりに引数として入れます。メソッドの引数 hueA と hueB には先に用意していたプロパティ costomHueA と costomHueB をそれぞれ入れます。

.stroke(self.makeGradientColor(hueA: costomHueA, hueB: costomHueB), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))

コード全体としては以下のようになります。

ProgressBarView
struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    @State var costomHueA = 1.0
    @State var costomHueB = 0.5

    var body: some View {
        ZStack {
            //(背景用の円省略)

            //プログレスバー用の円
            Circle()
                .trim(from: 0, to: CGFloat(self.timeManager.duration / self.timeManager.maxValue))
                //メソッドで生成したグラデーションカラーを適用
                .stroke(self.makeGradientColor(hueA: costomHueA, hueB: costomHueB), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
                .scaledToFit()
                //輪郭線の開始位置を12時の方向にする
                .rotationEffect(Angle(degrees: -90))
                .padding(15)
        }
    }

    func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
        let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
        let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
        let gradient = AngularGradient(gradient: .init(colors: [colorA, colorB, colorA]), center: .center, startAngle: .zero, endAngle: .init(degrees: 360))
        return gradient
    }
}

5. ProgressBarView に .onReceive モディファイアを追加し、 プログレスバーの色を常に変化させる

手順4で、プログレスバーの色をプロパティの色相から指定する形でグラデーション表示にすることができました。

さらに、onReceive モディファイアで、TimeManager クラスの timer プロパティ(Timer.publish() メソッド)の発動をトリガーにして、毎 0.05 秒ごとに色相のプロパティ costomHueA と costomHueB の値を変化させて、プログレスバーの色が常に変化するようにしていきます。

0.05 秒ごとに色相の値を 0.005 ずつ足していきます。もっとゆったり色を変化させたい場合は 0.001、もっとコロコロ色が変わる方が良ければ 0.01 くらいが良いかと思います。これ以上数値を上げると、色がただチカチカして目が痛くなりそうな動きになってしまいます。

また、色相は1.0が最大値ですので、1.0になったら、0.0に戻すように if 文で記述します。

.onReceive(timeManager.timer) { _ in
    self.costomHueA += 0.005
    if self.costomHueA >= 1.0 {
        self.costomHueA = 0.0
    }

これを costomHueB の分も同様に記述します。コード全体では以下のようになります。

ProgressBarView
struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    @State var costomHueA = 0.5
    @State var costomHueB = 0.3

    var body: some View {
        ZStack {
            //背景用の円
            Circle()
                .stroke(Color(.darkGray), style: StrokeStyle(lineWidth: 20))
                .scaledToFit()
                .padding(15)

            //プログレスバー用の円
            Circle()
                .trim(from: 0, to: CGFloat(self.timeManager.duration / self.timeManager.maxValue))
                //メソッドで生成したグラデーションカラーを適用
                .stroke(self.makeGradientColor(hueA: costomHueA, hueB: costomHueB), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
                .scaledToFit()
                //輪郭線の開始位置を12時の方向にする
                .rotationEffect(Angle(degrees: -90))
                .padding(15)
        }
        //毎0.05秒ごとに発動
        .onReceive(timeManager.timer) { _ in
            self.costomHueA += 0.005
            if self.costomHueA >= 1.0 {
                self.costomHueA = 0.0
            }
            self.costomHueB += 0.005
            if self.costomHueB >= 1.0 {
                self.costomHueB = 0.0
            }
        }
    }

    func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
        let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
        let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
        let gradient = AngularGradient(gradient: .init(colors: [colorA, colorB, colorA]), center: .center, startAngle: .zero, endAngle: .init(degrees: 360))
        return gradient
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む