- 投稿日:2020-09-12T23:42:10+09:00
本日学んだ、オプショナル型の基礎。【 ! の混同】
本日学んだ、オプショナル型の基礎をまとめます。
自分用の記録用の皮算用。! を混同していた。
"!"はオプショナルではない、nilを許さないというのは間違いです。
このことも勘違いしている人が多いかもしれません。勘違いしていました。
変数の宣言で型に
!
をつける場合、その変数は「オプショナル型」なので、強制的アンラップの
!
とは別物らしい。// 変数の宣言で型に ! をつける場合、その変数は「オプショナル型」 // (= 強制的アンラップの ! とは別物) var hoge: String! // nil暗黙的アンラップ型 (= IUO)
Implicitly Unwrapped Optional
オプショナル型
の1つ (= nliを許容)- 使用するとき、必ず強制的アンラップ
!
をする。- つまり、自動でアンラップをしてくれる
但し、nilでアンラップすると、アプリが落ちる?ため、
100%絶対に値がある(= nilでない) 場合のみ、使用可能。最初は nil で宣言したいが、使うときには絶対に値が入っている、という場合に使用。
他のアンラップ方法
debugPrint(error?.localizedDescription ?? "")
guard let
やif let
とかの「オプショナル バインディング」の他に、
- オプショナル チェイニング(
?
)- nil結合演算子 (
??
)があるみたいなので、以下にメモっておく。
オプショナル チェイニング? (= Optional Chaining)
- 値がある場合
アンラップする。
但し、それに続くプロパティやメソッドの戻り値は「
オプショナル型
」になる。
(-> nilの場合 nil を返すため)- nilの場合
nil を返す。それに続く処理をすべてキャンセル (-> なので安全)
nil結合演算子? (nil-coalescing)
A ?? B
... Aに値があるとAをアンラップ。 Aが nil だとBを返す。おしまい。
参考サイト
どこよりも分かりやすいSwiftの"?"と"!"
-> LGTMの数が異常。
- 投稿日:2020-09-12T18:37:13+09:00
[SwiftUI サンプルコードあり] 入れ子構造になったJSONをMVVMモデルを使って表示する方法
this article shows How to parse nested JSON on SwiftUI MVVM model.
if you can not read Japanese, you should use a translater. I've published Code on GitHub. check this out.
if you have any question, you can keep in touch with me from my Twitter (tkgshn)
はじめに
SwiftUIを使って、入れ子構造になっているJSONファイルをパース(分解)し、表示させるところまでを解説した記事です。
なぜこの記事を書いたのかというと、「入れ子構造になっているJSONファイルのパース」と「SwiftUIでの解説」が見つからなかったからです。2週間前の自分と同じ実装で悩んでいる人に向けて書いています。この記事では、Appleが公開しているSwiftUIのチュートリアルにそったMVVMモデルで、入れ子構造になったJSONをパース(分解)して、実際にViewに表示させるところまでを紹介します。
作るもの
- 「英語学習の際のトピック」を親の情報として、
- 『単語(日本語・英語)』を子供と見立て、情報を同時に表示することです。
これは、入れ子構造(「ネストした」などとも言う)のJSONを分解して、親の情報と子の情報を同時に表示しています。
SwiftUIを始める方は、こちらの Appleが公開しているSwiftUIのチュートリアル を最後まで完走していることをオススメします。
簡単に、データの構造を示してみるとこんな感じです。
なお、この図はGitHubある、drow.io から見ることができます。今回扱うデータの構造
audioContentData.json[ { "id": 0, "name": "人称", "description": "留学に行く際に空港で聞かれる内容をまとめた内容です", "phrases":{ "1": {"japanese": "Aゲートはどこですか?", "english": "Where is the Gate A ?"}, "2": {"japanese": "私は日本へ行きます", "english": "I'll go to Japan"} } }, { "id": 2, "name": "国と言語", "description": "国と言語を説明する際に使う英語をまとめました", "phrases": { "1": {"japanese": "私は日本出身です。", "english": "I'm from Japan"}, "2": {"japanese": "日本語は世界の言葉に比べて難しいです", "english": "Japanese is difficult than other languages"} } } ]JSONが入れ子構造になっていることがわかると思います。今回はこちらを説明していきます。
次に、SwiftUIのチュートリアルで使われたJSONを紹介していきます。SwiftUI チュートリアルで行っている方法
test.json[ { "name": "Turtle Rock", "category": "Rivers", "city": "Twentynine Palms", "state": "California", "id": 1001, "isFeatured": true, "isFavorite": true, "park": "Joshua Tree National Park", "coordinates": { "longitude": -116.166868, "latitude": 34.011286 }, "imageName": "turtlerock" }, { "name": "Silver Salmon Creek", "category": "Lakes", "city": "Port Alsworth", "state": "Alaska", "id": 1002, "isFeatured": false, "isFavorite": false, "park": "Lake Clark National Park and Preserve", "coordinates": { "longitude": -152.665167, "latitude": 59.980167 }, "imageName": "silversalmoncreek" } ]こちらがSwiftUI チュートリアルで使用されているJSONのファイルの一部抜粋です。一見入れ子構造になっている気がする、こちらの
coordinates
の要素ですが、"coordinates": {
"longitude": -116.166868,
"latitude": 34.011286
},コードを読み進めてみると、位置情報の取得に使われているだけで、入れ子構造で処理したい人が参考になるようなコードではありません。なので、無視してもらって大丈夫です。
対象読者
- 「SwiftUIからiOSでのアプリ開発を始めた」
- 「JSONなどのデータの取り扱いは初めて」
- 「とりあえずなんとか動いてほしい」
- 「ちょっと複雑なデータを持つアプリを作りたい」
という、2週間前の筆者の状況の方へ向けた記事です。
この記事では、ある程度「SwiftUIがどう動くか?」というのは分かってきたけど、自分のほしいものは微妙に作れない。という状態の人へオススメです。
そのため、初歩的なSwiftUIの表示などの説明はしていません。
かわりに、なるべく参考記事へのリンクやキーワードを載せました。まったくわからない人は、適宜キーワードを検索しつつ手を動かしてみてください!
では、やっていきましょう〜! ?
前提
SwiftUIの導入・基本はこちらの記事が参考になります。
データを作る
続いて、今回の目標は「データを表示させること」なので、表示させるものがなければ始まりません。
正直、表示するものはなんでもいいのですが今回は「英語学習アプリ」を想定して、以下のようなJSONを作ってみました。audioContentData.json[ { "id": 0, "name": "人称", "description": "留学に行く際に空港で聞かれる内容をまとめた内容です", "phrases":{ "1": {"japanese": "Aゲートはどこですか?", "english": "Where is the Gate A ?"}, "2": {"japanese": "私は日本へ行きます", "english": "I'll go to Japan"} } }, { "id": 2, "name": "国と言語", "description": "国と言語を説明する際に使う英語をまとめました", "phrases": { "1": {"japanese": "私は日本出身です。", "english": "I'm from Japan"}, "2": {"japanese": "日本語は世界の言葉に比べて難しいです", "english": "Japanese is difficult than other languages"} } } ]
名前(name)
や概要(description)
の下に、単語(phrases)があり、英語・日本語の文字列を持っている状態ですね。
- id
- name
- description
- japanese
- english
このサンプルデータはなんでもいいのですが、今回はこんな形で行こうと思います。
データ(JSON)を受け取るモデルを作成
この章では、「Model」という名前でまとめられたグループの説明をしていきます。
1. チュートリアルのコードを見てみよう
チュートリアルでは、こんな感じのファイルがありましたね。
こんなファイル構造になっていることがわかると思います。Model_SwitUI_tutorial.group- Landmark.swift - UserData.swift - Data.swift - Hike.swift
2. 今回のModelの構成
今回は、このような「Model」のファイル構成にしてみました、少し無駄なファイルを減らしたので、少なくなっています。
Model_nestedJSONParse.group- AudioContent.swift - Data.swift - UserData.swiftまずは、一番上の
Landmark.swift
とAudioContent.swift
の詳細から見ていきましょう。(ファイル名は違いますが、どちらも同じような働きをします)
クリックして
Landmark.swift
を見るLandmark.swift/* See LICENSE folder for this sample’s licensing information. Abstract: The model for an individual landmark. */ import SwiftUI import CoreLocation struct Landmark: Hashable, Codable, Identifiable { var id: Int var name: String fileprivate var imageName: String fileprivate var coordinates: Coordinates var state: String var park: String var category: Category var isFavorite: Bool var isFeatured: Bool var locationCoordinate: CLLocationCoordinate2D { CLLocationCoordinate2D( latitude: coordinates.latitude, longitude: coordinates.longitude) } var featureImage: Image? { guard isFeatured else { return nil } return Image( ImageStore.loadImage(name: "\(imageName)_feature"), scale: 2, label: Text(name)) } enum Category: String, CaseIterable, Codable, Hashable { case featured = "Featured" case lakes = "Lakes" case rivers = "Rivers" case mountains = "Mountains" } } extension Landmark { var image: Image { ImageStore.shared.image(name: imageName) } } struct Coordinates: Hashable, Codable { var latitude: Double var longitude: Double }しかし、今回は複雑なコードになってしまう原因である fileprivate や enum などは使わないことにします。
AudioContent.swiftimport SwiftUI // MARK: - AudioContent struct AudioContent: Codable, Identifiable { // コンテンツのid var id: Int // コンテンツのタイトル var name: String // コンテンツの概要 var description: String // フレーズの入れ子を作る var phrases: [String: Phrase] } // MARK: - Phrase struct Phrase: Codable { var japanese: String var english: String }その結果、こんな感じになります。
この時のモデルのコード生成とかは、https://app.quicktype.io/ を使えば、自動で生成できるので頑張らなくて大丈夫です。
使い方に関しては、以下の記事などを参考にしてください。
- JSON から各言語のコードを生成する quicktype の Haskell 出力を実装した
- 圧倒的捗り!!JSONデータからモデルを自動生成してくれるquicktypeが便利すぎるので紹介してみるなお、注意としてはサンプルよりも複雑なJSONを扱おうとすると、
Landmark.swift
で出てきたような fileprivate や enum を使うをやむ得なくなると思います。そうなった場合は、quicktype では処理出来ない(文字列からでは画像を取り扱いのかはわからない)ので、他のサンプルコードを参考にやってみてください。
3. 「Model」に入っていたその他のファイル
その他のファイルで、説明していない
Data.swift
とUserData.swift
があったと思います。
クリックして
Data.swift
を見るData.swiftimport Foundation import CoreLocation import SwiftUI let landmarkData: [Landmark] = load("landmarkData.json") let features = landmarkData.filter { $0.isFeatured } let hikeData: [Hike] = load("hikeData.json") func load<T: Decodable>(_ filename: String) -> T { let data: Data guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else { fatalError("Couldn't find \(filename) in main bundle.") } do { data = try Data(contentsOf: file) } catch { fatalError("Couldn't load \(filename) from main bundle:\n\(error)") } do { let decoder = JSONDecoder() return try decoder.decode(T.self, from: data) } catch { fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)") } } final class ImageStore { typealias _ImageDictionary = [String: CGImage] fileprivate var images: _ImageDictionary = [:] fileprivate static var scale = 2 static var shared = ImageStore() func image(name: String) -> Image { let index = _guaranteeImage(name: name) return Image(images.values[index], scale: CGFloat(ImageStore.scale), label: Text(name)) } static func loadImage(name: String) -> CGImage { guard let url = Bundle.main.url(forResource: name, withExtension: "jpg"), let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else { fatalError("Couldn't load image \(name).jpg from main bundle.") } return image } fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index { if let index = images.index(forKey: name) { return index } images[name] = ImageStore.loadImage(name: name) return images.index(forKey: name)! } }
クリックして
UserData.swift
を見るUserData.swiftimport Combine import SwiftUI final class UserData: ObservableObject { @Published var showFavoritesOnly = false @Published var landmarks = landmarkData @Published var profile = Profile.default }正直難しいので、初心者の方が理解をするのは時間がかかるとは思いますが、簡単に説明すると
- JSONファイルを読み取るところ
- アプリ操作する人(User)のデータを処理するところ
という認識で問題ないです。
詳しい紹介はこちらの記事を参考にしてください
SwiftUI Tutorialsを読み解く表示する
実装をしていきます。
ほとんど内部の処理は分離させていないので、もうちょっときれいに書けるはずです。ContentView.swiftimport SwiftUI struct ContentView: View { @EnvironmentObject var userData: UserData var audioContent: AudioContent var body: some View { NavigationView { List { ForEach(audioContentData) { audioContent in NavigationLink( destination: VStack{ Text(audioContent.name) .font(.largeTitle) .padding(.top) Text(audioContent.description) .padding([.top, .leading, .trailing]) Divider() .padding(.top) // 下に書いている PhraseRow(audioContent: audioContent) .padding(.top) }) { Text(audioContent.name) } } } .navigationBarTitle("all List!") } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { let userData = UserData() return ContentView(audioContent: userData.audioContents[0]) .environmentObject(UserData()) } } // MARK: - 単語を表示する部分 struct PhraseRow: View { var audioContent: AudioContent var body: some View { // Phraseが持っている個数分より多い数、ループを回すとクラッシュしてしまう ForEach(1..<3) { num in // 日本語を取得 VStack(alignment: .leading) { Text(self.audioContent.phrases[String(num)]!.japanese) .padding(.bottom) // 英語を取得 Text(self.audioContent.phrases[String(num)]!.english) } Divider() } } }タップして遷移すると、こちらのコードのおかげで
PhraseRow.swift// MARK: - 単語を表示する部分 struct PhraseRow: View { var audioContent: AudioContent var body: some View { // Phraseが持っている個数分より多い数、ループを回すとクラッシュしてしまう ForEach(1..<3) { num in // 日本語を取得 VStack(alignment: .leading) { Text(self.audioContent.phrases[String(num)]!.japanese) .padding(.bottom) // 英語を取得 Text(self.audioContent.phrases[String(num)]!.english) } Divider() } } }
- 親が持っている情報
audioContent.name
audioContent.description
- 子が持っている情報
audioContent.phrases[String(num)]!.japanese
audioContent.phrases[String(num)]!.english
が表示されています。
参考サイト
I wish to say big thanks for the Indian on stackover flow.
- 投稿日:2020-09-12T16:49:23+09:00
iPad画面サイズ、ピクセル数早見表
機種名 ハードウェア名 画面サイズ CSSピクセル デバイスピクセル デバイスピクセル比 アスペクト比 発売初期搭載iOS 最終対応iOS 発売年 メモ iPad iPad1,1 9.7 768x1024 768x1024 1 3:4 iPhoneOS3.2 iOS5.1.1 2010 iPad2 iPad2,1、iPad2,2、iPad2,3、iPad2,4 9.7 768x1024 768x1024 1 3:4 iOS4.3.3 iOS9.3.5 2011 iPad(3th Gen) iPad3,1、iPad3,3、iPad3,3 9.7 768x1024 1,536x2,048 2 3:4 iOS5.1 iOS9.3.6 2012 iPad(4th Gen) iPad3,4、iPad3,5、iPad3,6 9.7 768x1024 1,536x2,048 2 3:4 iOS6.0 iOS10.3.4 2012 iPad(5th Gen) iPad6,11、iPad6,12 9.7 768x1024 1,536x2,048 2 3:4 iOS10.2.1 ※ 2017 iPad(6th Gen) iPad7,5、iPad7,6 9.7 768x1024 1,536x2,048 2 3:4 iOS11.3 ※ 2018 iPad(7th Gen) iPad7,11、iPad7,12 10.2 820x1080 1620x2160 2 3:4 iPadOS13.0 ※ 2019 iPad Air iPad4,1、iPad4,2、iPad4,3 9.7 768x1024 1,536x2,048 2 3:4 iOS7.0 iOS12.4.6 2013 iPad Air2 iPad5,3、iPad5,4 9.7 768x1024 1,536x2,048 2 3:4 iOS8.1 ※ 2014 iPad Air(3th Gen) iPad11,3、iPad11,4 10.5 834x1112 1668x2224 2 3:4 iOS12.0 ※ 2019 iPad Pro (9.7inch) iPad6,3、iPad6,4 9.7 768x1024 1,536x2,048 2 3:4 iOS9.3 ※ 2016 iPad Pro(10.5inch) iPad7,3、iPad7,4 10.5 834x1112 1668x2224 2 3:4 iOS10.3.2 ※ 2017 iPad Pro(12.9inch) iPad6,7、iPad6,8 12.9 1024x1366 2048x2732 2 3:4 iOS9.1 ※ 2017 iPad Pro(11inch) iPad8,1、iPad8,2、iPad8,3、iPad8,4 11 834x1194 1668x2388 2 3:4.29 iOS12.1 ※ 2018 iPad Pro(12.9inch)(2nd Gen) iPad7,1、iPad7,2 12.9 1024x1366 2048x2732 2 3:4 iOS10.3.2 ※ 2018 iPad Pro(11inch)(2nd Gen) iPad8,9 11 834x1194 1668x2388 2 3:4.29 iPadOS13.4 ※ 2020 iPad Pro(12.9inch)(3th Gen) iPad8,5、iPad8,6、iPad8,7、iPad8,8 12.9 1024x1366 2048x2732 2 3:4 iOS12.1 ※ 2018 iPad Pro(12.9inch)(4th Gen) iPad8,11、iPad8,12 12.9 1024x1366 2048x2732 2 3:4 iPadOS13.4 ※ 2020 iPad mini iPad2,5、iPad2,6、iPad2,7 7.9 768x1024 768x1024 1 3:4 iOS6.0 iOS9.3.6 2012 iPad mini2 iPad4,4、iPad4,5、iPad4,6 7.9 768x1024 1536x2048 2 3:4 iOS7.0 iOS12.4.8 2013 iPad mini3 iPad4,7、iPad4,8、iPad4,9 7.9 768x1024 1536x2048 2 3:4 iOS8.1 iOS12.4.6 2014 iPad mini4 iPad5,1、iPad5,2 7.9 768x1024 1536x2048 2 3:4 iOS9.0 ※ 2015 iPad mini5 iPad11,1、iPad11,2 7.9 768x1024 1536x2048 2 3:4 iOS12.2 ※ 2019 ※※2020/9/11時点ではiPadOS13.7までアップデート可能
- 投稿日:2020-09-12T16:20:02+09:00
Inside Flutter Hooks
概要
Flutter Hooksを使う機会があり、すごい便利だなと思っていたのですが、
内部的にどんな風に実装されているのか掘り下げてみようかと思い、今回色々調べてみました。
(何か間違っていたりしたらコメントいただけると嬉しいです )Flutter Hooks とは?
React hooksをFlutterで実装したものになります。
作者はProvider等でおなじみのRemiさんです。
サンプルの実行環境
flutter: v1.20.3
flutter_hooks: 0.14.0Flutter Hooksの基本的な仕組み
useMemoized
を掘り下げる一番シンプルな
useMemoized
というhookを例にFlutter Hooksがどのような仕組みになっているのか追ってみたいと思います。そもそも
useMemoized
とは?
useMemoized
は何回ビルドが走っても初期値をキャッシュしてくれるhookです。↓簡単なサンプルとして現在時刻を
useMemoized
でキャッシュし、Floating Action Button
をタップする度に
カウンターが増えて再ビルドが走るようなサンプルを作成してみました。import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; void main() { runApp(MyApp()); } class MyApp extends HookWidget { @override Widget build(BuildContext context) { final DateTime now = useMemoized(() => DateTime.now()); // 初期値として現在日時を保存 final ValueNotifier<int> counter = useState<int>(0); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: Scaffold( body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( now.toString(), style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold), ), Text( counter.value.toString(), style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold), ) ], ), ), floatingActionButton: FloatingActionButton( onPressed: () => counter.value++, // カウンターが増えWidgetのビルドが走る child: Icon(Icons.add), ), )); } }↑ counterの値が更新されても初期値として設定した現在日時の値は変更されていないのが分かるかと思います。
(2020-09-11 11:21:40.... の箇所)
useMemoized
の実装は?こちら で実装されています。以下に関連箇所だけ抜き出しました。
T useMemoized<T>(T Function() valueBuilder, [List<Object> keys = const <dynamic>[]]) { return use(_MemoizedHook( valueBuilder, keys: keys, )); } class _MemoizedHook<T> extends Hook<T> { const _MemoizedHook( this.valueBuilder, { List<Object> keys = const <dynamic>[], }) : assert(valueBuilder != null, 'valueBuilder cannot be null'), assert(keys != null, 'keys cannot be null'), super(keys: keys); final T Function() valueBuilder; @override _MemoizedHookState<T> createState() => _MemoizedHookState<T>(); } class _MemoizedHookState<T> extends HookState<T, _MemoizedHook<T>> { T value; @override void initHook() { super.initHook(); value = hook.valueBuilder(); } @override T build(BuildContext context) { return value; } @override String get debugLabel => 'useMemoized<$T>'; }より詳細に見ていこうと思います。
useメソッド
先ずは
useMemoized
内で使用されているuse(_MemoizedHook(...
のuse
メソッド を掘り下げてみたいと思います。
このuse
メソッド は flutter_hooks/lib/src/framework.dart で以下の様に実装されています。R use<R>(Hook<R> hook) => Hook.use(hook);
Hook
というクラスのstatic メソッドuse
に引数のhook(ここでは_MemoizedHook)を渡して呼んでいます。
ここでHook
というクラスが出てきました。今度はこのHook
に着目したいと思います。
Hookクラス
Hookクラスはこちらに実装されています。
以下に要約したものを抜き出してみました。@immutable abstract class Hook<R> with Diagnosticable { const Hook({this.keys}); @Deprecated('Use `use` instead of `Hook.use`') static R use<R>(Hook<R> hook) { assert(HookElement._currentHookElement != null, ''' Hooks can only be called from the build method of a widget that mix-in `Hooks`. Hooks should only be called within the build method of a widget. Calling them outside of build method leads to an unstable state and is therefore prohibited. '''); return HookElement._currentHookElement._use(hook); } final List<Object> keys; @protected HookState<R, Hook<R>> createState(); }先程出てきた
Hook.use
に着目したいと思います。
@Deprecated
となっていて直接Hook.use
は呼ばずに先程のuseメソッド
を呼ぶようにとなっています。ここでは
HookElement._currentHookElement._use(hook)
が呼ばれており、
HookElement._currentHookElement
は後でも出てきますが こちらにstatic変数として定義されています。
HookElement._currentHookElement._use
は別途掘り下げるとしてcreateState
で生成されるHookState
を見てみます。
HookStateクラス
こちらに実装されています。こちらも要約したものを以下に抜き出してみました。
abstract class HookState<R, T extends Hook<R>> with Diagnosticable { @protected BuildContext get context => _element; HookElement _element; T get hook => _hook; T _hook; @protected void initHook() {} @protected void dispose() {} @protected R build(BuildContext context); @protected void didUpdateHook(T oldHook) {} void deactivate() {} void reassemble() {} @protected void setState(VoidCallback fn) { fn(); _element .._isOptionalRebuild = false ..markNeedsBuild(); } }こうして見ると
Hook
とHookState
の関係がStatefulWidget
とState
の関係に似てますね
HookState
内に先程出てきたHookElement
を保持し、BuildContext
としてgetできるようにしています。
後で出てきますが、HookElement
はComponentElement
をimplementしているのでBuildContext
として扱う事ができます。
詳しくはこちらを参照して下さい。
FlutterのBuildContextとは何か - Qiita
HookElement mixin
こちらに実装されています。こちらも要約して抜き出してみました。
HookElement
のuse
メソッドでの処理がFlutter Hooksのキモとなる処理になってきます。mixin HookElement on ComponentElement { static HookElement _currentHookElement; _Entry<HookState> _currentHookState; final LinkedList<_Entry<HookState>> _hooks = LinkedList(); LinkedList<_Entry<bool Function()>> _shouldRebuildQueue; LinkedList<_Entry<HookState>> _needDispose; bool _isOptionalRebuild = false; Widget _buildCache; @override Widget build() { // 色々な前処理 ... _currentHookState = _hooks.isEmpty ? null : _hooks.first; // ① HookElement._currentHookElement = this; // ② try { _buildCache = super.build(); } finally { // 後処理 .... } return _buildCache; } R _use<R>(Hook<R> hook) { if (_currentHookState == null) { _appendHook(hook); } else if (hook.runtimeType != _currentHookState.value.hook.runtimeType) { // ③ final previousHookType = _currentHookState.value.hook.runtimeType; _unmountAllRemainingHooks(); if (kDebugMode && _debugDidReassemble) { _appendHook(hook); } else { throw StateError(''' Type mismatch between hooks: - previous hook: $previousHookType - new hook: ${hook.runtimeType} '''); } } else if (hook != _currentHookState.value.hook) { final previousHook = _currentHookState.value.hook; if (Hook.shouldPreserveState(previousHook, hook)) { // ④ _currentHookState.value .._hook = hook ..didUpdateHook(previousHook); } else { _needDispose ??= LinkedList(); _needDispose.add(_Entry(_currentHookState.value)); _currentHookState.value = _createHookState<R>(hook); } } final result = _currentHookState.value.build(this) as R; _currentHookState = _currentHookState.next; // ⑤ return result; } }先程
HookState
で出てきたHookElement._currentHookElement
が定義されています。大まかな処理の流れ
_currentHookState
HookState
の LinkedListになっておりビルド中に useXXX で呼ばれた際の各HookStateの一覧を呼ばれた順で保持していますbuild メソッド
- ① : 前回Widgetのビルドが走った際の
HookState
のLinkedList のキャッシュがあれば_currentHookState
にセットしています- ② : staticな領域に現在build中のHookElementをセットしています
HookWidget
やStatefulHookWidget
クラスを使ったWidgetのビルドでは内部的にHookElement
を使用しているのでHookElement
のbuild()
メソッドが呼ばれます。use メソッド
- ③ : 前回ビルドした時のHookと今回ビルド中のHookの
runtimeType
が異なっている場合
- _currentHookStateに格納されているHookStateをすべてクリアします
- Debug中の場合(開発しててuseXXXを変更した等)今回Hookを新たに格納します
- ④ : 前回ビルド時のHookと異なるオブジェクトの場合
shouldPreserveState
メソッドでKeyが前回と異なっているかチェックを行います
- 異なっている場合
- 一旦以前のHookStateは破棄して今回のHookStateに入れ替えます
- 異なっていない場合
HookState
のdidUpdateHook
が呼ばれます- ⑤ : 次に備えて、
_currentHookState.next
で次のHookStateにLinkedList内の位置を移動させていますLinkedListを使用して前回ビルドのHookStateと比較する処理は flutter_hooksのREADMEにも載っていますが
React hooks: not magic, just arrays | by Rudi Yardley | Medium
こちらを読むとさらに理解が深まりそうでした。
useMemoized
に立ち返ってここで
useMemoized
内で呼ばれていたHook.use
に立ち返ってみるとHookElement._currentHookElement._use(hook)
が呼ばれていました。引数の
hook
は_MemoizedHook
が設定されuse
メソッドが呼ばれることになります。
_currentHookState
がnullの場合(初めてWidgetビルド中にuseXXXが呼ばれた場合)は_appendHook
が呼ばれてます。
_appendHook
は何をしているかというとextension on HookElement { HookState<R, Hook<R>> _createHookState<R>(Hook<R> hook) { assert(() { _debugIsInitHook = true; return true; }(), ''); final state = hook.createState() .._element = this .._hook = hook ..initHook(); assert(() { _debugIsInitHook = false; return true; }(), ''); return state; } void _appendHook<R>(Hook<R> hook) { final result = _createHookState<R>(hook); _currentHookState = _Entry(result); _hooks.add(_currentHookState); } }
Hookクラス
のcreateState
を呼び出してHookState
を作成し_currentHookState
に設定しています。
上記であった前回ビルド時のHookStateと比較する等の処理が終わったあと以下の処理が行われます。final result = _currentHookState.value.build(this) as R; _currentHookState = _currentHookState.next; return result;ここで
HookState
の buildメソッドを呼び出して戻りuseメソッドの戻り値として返しています。
useMemoized
の場合だと_MemoizedHookState
の buildが呼ばれることになり、
_MemoizedHookState
の buildは単に保存した値を返しているだけなので、いくらWidgetのビルドが走っても
更新されない保存された値を返し続けるという仕組みになっているようです@override T build(BuildContext context) { return value; }
HookWidget
最後にhookを使う側で必要な
HookWidget
クラスを見てみたいと思います。abstract class HookWidget extends StatelessWidget { const HookWidget({Key key}) : super(key: key); @override _StatelessHookElement createElement() => _StatelessHookElement(this); } class _StatelessHookElement extends StatelessElement with HookElement { _StatelessHookElement(HookWidget hooks) : super(hooks); }すごいシンプルで、
StatelessWidget
クラスを継承し、Elementを生成する際に
HookElement
を実装した_StatelessHookElement
を返すようになっています。また、
StatefulWidget
版も用意されている様でした。abstract class StatefulHookWidget extends StatefulWidget { const StatefulHookWidget({Key key}) : super(key: key); @override _StatefulHookElement createElement() => _StatefulHookElement(this); } class _StatefulHookElement extends StatefulElement with HookElement { _StatefulHookElement(StatefulHookWidget hooks) : super(hooks); }ここまで仕組みがどうなっているのか超ざっくり説明しました。
主な登場人物とざっくり相関図
これまでで登場してきたクラスやmixinを相関図にしてみました。
- Hook
- HookElement
- HookState
- HookWidget
- StatefulHookWidget (※今回は省いています)
※ 間違っていたらご指摘いただけると嬉しいです
他のhooks達
ここまでで何となくでも仕組みが理解できたので、他のhooksも見てみたいと思います。
useContext
実装はこちら
これは至ってシンプルで以下の様に実装されています。BuildContext useContext() { assert( HookElement._currentHookElement != null, '`useContext` can only be called from the build method of HookWidget', ); return HookElement._currentHookElement; }実装をみるとなぜbuild中じゃないと呼び出せないのか分かりますね
ちなみにHookElement._currentHookElement
が null になるタイミングはWidgetのビルドが終わったタイミングになります。
useEffect
抜き出したもの
void useEffect(Dispose Function() effect, [List<Object> keys]) { use(_EffectHook(effect, keys)); } class _EffectHook extends Hook<void> { const _EffectHook(this.effect, [List<Object> keys]) : assert(effect != null, 'effect cannot be null'), super(keys: keys); final Dispose Function() effect; @override _EffectHookState createState() => _EffectHookState(); } class _EffectHookState extends HookState<void, _EffectHook> { Dispose disposer; @override void initHook() { super.initHook(); scheduleEffect(); } @override void didUpdateHook(_EffectHook oldHook) { super.didUpdateHook(oldHook); if (hook.keys == null) { if (disposer != null) { disposer(); } scheduleEffect(); } } @override void build(BuildContext context) {} @override void dispose() { if (disposer != null) { disposer(); } } void scheduleEffect() { disposer = hook.effect(); } @override String get debugLabel => 'useEffect'; @override bool get debugSkipValue => true; }使い方としては
useEffect
第一引数で渡された処理が初回呼ばれて、以降は第二引数のKeyに変更が無い限り
処理が呼ばれる事はありません。useEffect(() { print('useEffect'); return () => print('dispose'); }, const []);↑の例だと第二引数のKeyに
const []
を指定しているので初回だけしかprint('useEffect');
は呼ばれません。
また第一引数の戻り値として終了処理をFunction()
として返せるのでもう一度処理が呼ばれる前にクリアさせたい等に使えそうです。Keyが変更されるサンプルとして
useMemoized
のサンプルにuseEffect
を呼ぶ処理を追加してみました。// ... 省略 class MyApp extends HookWidget { @override Widget build(BuildContext context) { final DateTime now = useMemoized(() => DateTime.now()); final ValueNotifier<int> counter = useState<int>(0); // ☆ここから追加 useEffect(() { print('useEffect'); return () => print('dispose'); }, [counter.value]); // ... 省略↑のサンプルを実行し + ボタンをタップすると
print('useEffect');
とprint('dispose');
が呼ばれるのが分かるかと思います。
useEffect
のしくみ初回呼ばれる
initHook
内でscheduleEffect
メソッドを呼び出しています。
scheduleEffect
メソッドがどうなっているかというとvoid scheduleEffect() { disposer = hook.effect(); }
useEffect
の第一引数で渡されたFunction()
を 呼び出し戻り値の dispose を内部で保存しています。
disposeはここではtypedef Dispose = void Function();
として定義されています。
このタイミングで初回の処理を呼び出しています。次に第二引数のKeyが変更された時点の処理を見てみたいと思います。
} else if (hook != _currentHookState.value.hook) { final previousHook = _currentHookState.value.hook; if (Hook.shouldPreserveState(previousHook, hook)) { _currentHookState.value .._hook = hook ..didUpdateHook(previousHook); } else { _needDispose ??= LinkedList(); _needDispose.add(_Entry(_currentHookState.value)); _currentHookState.value = _createHookState<R>(hook); } }既に説明した通り、
HookElement
のuse
メソッド内でKeyが変更されたかの判定をshouldPreserveState
で行っており
Keyが変更されている場合、新たにHookStateを作り直しています。
作り直す際にinitHook
が呼ばれ内部でscheduleEffect
を呼んでいます。
破棄された方のHookStateはbuildの最後でdispose
が呼ばれ、内部で保持していたdisposer()
を呼び出しています。
useState
抜き出したもの
ValueNotifier<T> useState<T>([T initialData]) { return use(_StateHook(initialData: initialData)); } class _StateHook<T> extends Hook<ValueNotifier<T>> { const _StateHook({this.initialData}); final T initialData; @override _StateHookState<T> createState() => _StateHookState(); } class _StateHookState<T> extends HookState<ValueNotifier<T>, _StateHook<T>> { ValueNotifier<T> _state; @override void initHook() { super.initHook(); _state = ValueNotifier(hook.initialData)..addListener(_listener); } @override void dispose() { _state.dispose(); } @override ValueNotifier<T> build(BuildContext context) { return _state; } void _listener() { setState(() {}); } @override Object get debugValue => _state.value; @override String get debugLabel => 'useState<$T>'; }こちらは既に
useMemoized
のサンプルで使ってましたが、こちらはValueNotifier
をラップし
扱いやすくしてくれているhooksになります。
useState
のしくみこちらはシンプルで
initHook
時にValueNotifier
を生成し、build時には生成したValueNotifier
を返しています。まとめ
基本的なhooksの仕組みを何となくでも把握しとけば、他のhooksもソースコードを読むことで
ある程度理解できるようになりました
今回のように一番シンプルなものから掘り下げていくのは余分なInputが少ない分理解しやすいですね。内部的な処理が分かっていれば、useContext をWidgetのビルドタイミング以外で使用したらダメだとか
事前に分かるので、広範囲でお世話になるライブラリ等は事前に内部がどんな風になっているのか把握しておくと、
トータル的にはハマる時間が無くなってスムーズかもしれませんまた次も機会があれば何か掘り下げようかと思います。
- 投稿日:2020-09-12T15:54:57+09:00
iOSのdidBecomeActive/willResignActiveを視覚的に把握する
モチベーション
ライフサイクルメソッドの説明は、解説付きでケースを列挙されることが多いですが、いまいちイメージがしにくいと感じることが多く感じます。特に、ActiveとInactiveのタイミングは分かりにくいことが多いのではないでしょうか。
この記事では、Sceneベースの各種ライフサイクルメソッドにprintを貼って、active/inactiveとはどのようなステートなのかを視覚的に把握します。(※タイミングの列挙はしません。1つ2つ取り上げるだけです)
解説
1. アプリの起動
ライフサイクル
シーンベースのライフサイクルでも、共通系は呼ばれます。なので
didFinishLauching
がまず始めに呼ばれます。
次に、SceneDelegate
のwillConnect
が呼ばれます。このメソッドは、システム・ユーザー起点にかかわらず、新しいシーンがアプリケーションに接続される直前に呼ばれます。そしてその後は順当にwillEnterForeground
,didBecomeActive
と呼ばれていきます。didFinishLaunching willConnect willEnterForeground didBecomeActive2. SplitViewの割合変化
もっとも分かりやすい状態遷移です。視覚的な状態と実際の状態が一致しており、直感的に理解できます。
中間状態
ライフサイクル
中間状態では、システムによるSplitViewの操作中で、アプリケーション自体の操作はできないため、画面は前面に出ていますが
Inactive
な状態です。didFinishLaunching willConnect willEnterForeground didBecomeActive willResignActive完了
ライフサイクル
割合がユーザーによって決められ、操作が終了すると
didBecomeActive
が呼ばれていることが分かります。注目すべきは、どちらの状態でも
foreground
であることは変わらず、active/inactive
だけの変化であるということです。didFinishLaunching willConnect willEnterForeground didBecomeActive willResignActive didBecomeActive3. SplitViewから閉じる
ライフサイクルメソッド
先程の中間状態からの復帰では
didBecomeActive
になりましたが、こちらはそのまま閉じてしまった(右側の比率を100%にした)ので、didEnterBackground
が呼ばれました。didFinishLaunching willConnect willEnterForeground didBecomeActive willResignActive didBecomeActive willResignActive didEnterBackgroundまとめ
「ForegroundなのにInactiveな状態ってどのようなものなのだろう?」という疑問に対して、これまで「システム的にはそういう状態が存在する」と受け入れて理解していた人も、上記のSplitViewの例を見れば「前面に出ているのに、アプリケーションにアクセスできない瞬間が存在する」というのを視覚的に把握できたかと思います。
他には、アプリ一覧を開いた時に
Inactive
になったりしますが、詳細は別の方の記事をご参照ください。実装
AppDelegate.swiftfunc application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. print("didFinishLauching") return true } // MARK: UISceneSession Lifecycle func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. print("configurationForConnecting") return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) }SceneDelegate.swiftimport UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let _ = (scene as? UIWindowScene) else { return } print("willConnect") } func sceneDidDisconnect(_ scene: UIScene) { // Called as the scene is being released by the system. // This occurs shortly after the scene enters the background, or when its session is discarded. // Release any resources associated with this scene that can be re-created the next time the scene connects. // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). print("didDisconnect") } func sceneDidBecomeActive(_ scene: UIScene) { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. print("didBecomeActive") } func sceneWillResignActive(_ scene: UIScene) { // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). print("wiiResignActive") } func sceneWillEnterForeground(_ scene: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. print("willEnterForeground") } func sceneDidEnterBackground(_ scene: UIScene) { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. // Save changes in the application's managed object context when the application transitions to the background. (UIApplication.shared.delegate as? AppDelegate)?.saveContext() print("didEnterBackground") } }
- 投稿日:2020-09-12T15:34:54+09:00
コードでSwiftのautolayoutを書く
Storyboardで画面のレイアウトやUI部品をグラフィカルに配置できますが、チームで協同作業する時や細かいレイアウトの調整が発生する場合はStoryboardだとどうしても支障が出ます。
autolayoutの標準APIについて、Appleが
NSLayoutConstraint
とNSLayoutAnchor
(ios9~)、Visual Format Language(VFL)
を提供しています。NSLayoutAnchor
はNSLayoutConstraint
よりコード量が少なくなり、制約・声明もはっきりするようになっています。VFL
はアスキーアートのように制約を定義することができる記法ですが、学習コストが高くかつリテラルな箇所が多いため、実行時にエラーが起こる恐れがあります(本文ではVFL
パターンを紹介しません)。1、NSLayoutConstraint
NSLayoutConstraintを使用するには、各々初期化する必要があります。
制約する属性ごとにNSLayoutConstraint
を作る必要があるため、コード量が増えます。
初期化メソッド:NSLayoutConstraint.init()
//UIImageViewを作成し、viewに追加する let layoutIcon = "layoutIcon" let layoutImg = UIImage(named: layoutIcon) let layoutView = UIImageView(image: layoutImg) layoutView.backgroundColor = .green layoutView.layer.cornerRadius = 10 //AutoresizingMaskをAutoLayoutの制約に置き換えるかどうか指定する値(必ずfalse) layoutView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(layoutView) //制約を追加 top:50 //パラメーター説明 item:制約オブジェクト attribute:制約する属性 relatedBy:制約タイプ toItem:制約の相手 attribute:制約相手に使用する属性 multiplier:乗数値 constant:定数値 let topConstraint = NSLayoutConstraint.init(item: layoutView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 50.0) //制約は定義した後、activateする必要あり topConstraint.isActive = true //制約を追加 left:50 let leftConstraint = NSLayoutConstraint.init(item: layoutView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1.0, constant: 50.0) leftConstraint.isActive = true //制約を追加 width:70 let widthConstraint = NSLayoutConstraint.init(item: layoutView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 70.0) widthConstraint.isActive = true //制約を追加 height:70 let heightConstraint = NSLayoutConstraint.init(item: layoutView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 70.0) heightConstraint.isActive = true
2、NSLayoutAnchor
iOS9からは
NSLayoutAnchor
を使いNSLayoutConstraint
オブジェクトを作ることができました。ただ直接にNSLayoutConstraint
オブジェクトを作ることではなく、UIView
あるいはその子クラスやUILayoutGuide
のあるanchor
プロパティー値を利用します。これらのプロパティーはautolayout中の主要NSLayoutAttribute
に相当します。
そのため、NSLayoutAnchor
の子クラスでNSLayoutAttribute
を設定してもいいです。
制約メソッド:xxxView.xxxAnchor.constraint( )
//紫色のUIImageViewを作成し、viewに追加する let purLayoutIcon = "layoutIcon" let purLayoutImg = UIImage(named: purLayoutIcon) let purLayoutView = UIImageView(image: purLayoutImg) purLayoutView.backgroundColor = .purple purLayoutView.layer.cornerRadius = 10 //AutoresizingMaskをAutoLayoutの制約に置き換えるかどうか指定する値(必ずfalse) purLayoutView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(purLayoutView) //制約を追加 top:150 left:50 width:70 height:70 purLayoutView.topAnchor.constraint(equalTo: view.topAnchor, constant: 150.0).isActive = true purLayoutView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 50.0).isActive = true purLayoutView.widthAnchor.constraint(equalToConstant: 70.0).isActive = true purLayoutView.heightAnchor.constraint(equalToConstant: 70.0).isActive = true
NSLayoutConstraint
よりNSLayoutAnchor
のほうは簡潔です。3、ScrollView中のautolayout
ScrollView
はセルフのcontentSize
があるため、autolayout使用時に留意したほうがいいです。
ScrollView
の子viewのサイズは即ちcontentSize
です。
制約はScrollView
の完全なレイアウトを決める必要があり、上下左右ともに必要です。また、ScrollView
の子viewの最大サイズも必ず制約条件を通じて確定値を得られます。//scrollViewを作成し、viewに追加する let scrollView = UIScrollView() scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.backgroundColor = .cyan //ページ単位のスクロールを可能する scrollView.isPagingEnabled = true view.addSubview(scrollView) scrollView.topAnchor.constraint(equalTo: purLayoutView.bottomAnchor, constant: 30.0).isActive = true scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true //safeAreaのボトム距離を使う scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true let bgColor = [UIColor.red,UIColor.green,UIColor.blue] var leftView:UIView? = nil for i in 0..<3{ let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = bgColor[i] scrollView.addSubview(view) if let left = leftView{ view.leftAnchor.constraint(equalTo: left.rightAnchor, constant: 0).isActive = true }else{ view.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 0).isActive = true } view.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0).isActive = true view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0).isActive = true view.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 1.0).isActive = true view.heightAnchor.constraint(equalTo: scrollView.heightAnchor, multiplier: 1.0).isActive = true leftView = view } //全ての子viewは上、下、左がscrollViewを相手として制約していたが、右のほうがまだ //右方向の制約を追加 if let left = leftView{ left.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: 0).isActive = true }
- 投稿日:2020-09-12T15:17:43+09:00
Core Imageによるぼかしのクオリティを高める
Core Imageを使うと、簡単かつ高速に画像ぼかすことができます。
ぼかし自体は
CIGaussianBlur
ひとつでできるのですが、以下の点に注意しておくことでより高いクオリティの結果を得ることができます。
- ぼかし後の画像サイズは、入力画像サイズと異なる
- なにもしないと、画像のエッジ付近がやや暗くなってしまう
CIImage#extent
について
CIImage#extent
は簡単に言うと、その画像のサイズを持つ出力矩形領域です。
例えば解像度480x640の画像から作られたCIImage
のextent
は(x: 0, y: 0, w: 480, h: 640)
となります。
しかしこれに、半径20
のCIGaussianBlur
を適用すると、CIImage#extent
は(x: -60, y: -60, w: 600, h: 760)
となり、入力画像よりも大きな画像が出力されていることがわかります。
これをImage ViewなどにAspect Fit表示すると、元画像より相対的に小さく表示されてしまうでしょう。入力画像と同じ大きさで表示したければ、
CIImage#cropped(to:)
などで入力画像のextent
で切り取る必要があります。ぼかし画像のエッジ付近が暗くなってしまう理由
ぼかしフィルタは一般的に、対象ピクセルを中心とした半径r内にあるピクセルの値(色)の加重平均を求めて生成します。
しかし画像のエッジ付近については、サンプリングする範囲が画像の外に及んでしまうため、その部分の値を「黒」とみなして計算した結果、暗い色になってしまうことがあります。この問題の対処方法のひとつとして CIAffineClamp を利用する方法があります。
↑のリファレレンスにもどんな結果が得られるか記載されていますが、 画像の外のピクセル値を、最寄りの四辺のピクセル値と同じとみなすようCore Imageに指示することができる のです。CropとClampedToExtentを適用したぼかしフィルタ
以上を踏まえたCore Imageによるぼかしフィルタは以下のようになります。
CoreImage_BetterBlur.swift// inputImage: CIImage, radius: CGFloat let outputImage = inputImage .clampedToExtent() // CIAffineClamp .applyingFilter( "CIGaussianBlur", parameters: [kCIInputRadiusKey: radius] ) .cropped(to: inputImage.extent)
- 左: clampedToExtentあり
- 右: clampedToExtentなし
- 投稿日:2020-09-12T14:36:25+09:00
Core Dataのtransformableな属性をセキュアに実装する
Core Dataのエンティティが持つ属性 (attribute) の型は整数、文字列、日付などいくつかの決まったものしかとることができませんが、Transformableを指定することで任意の型を
NSData
に変換して保存することができるようになっています。このとき保存したい型とNSData
の変換を担うのがvalue transformerです。Core Dataのtransformableを利用しているプロジェクトをXcode 12でビルドしたときに、以下のような警告が出るようになりました。
warning: Misconfigured Property: <NSManagedObjectのサブクラス>.<プロパティ> is using a nil or insecure value transformer. Please switch to NSSecureUnarchiveFromDataTransformerName or a custom NSValueTransformer subclass of NSSecureUnarchiveFromDataTransformerこれは、これまでのvalue transformerが
NSCoding
を使ったものであるため非推奨になっており、NSSecureCoding
をベースとしたNSSecureUnarchiveFromDataTransformer
に置き換える必要があるということです。今回この警告の対応のために調べた、
NSSecureUnarchiveFromDataTransformer
を使ってtransformableな属性をセキュアに実装する方法をまとめました。カスタムクラスが不要なパターン
NSSecureUnarchiveFromDataTransformer
が扱える型はallowedTopLevelClasses
で規定されていて、以下の通りです。NSArray, NSDictionary, NSSet, NSString, NSNumber, NSDate, NSData, NSURL, NSUUID, NSNullこれらの型の組み合わせで表現できるもの、例えば文字列の配列
[String]
の属性であれば、モデルエディタでTransformerの欄にNSSecureUnarchiveFromDataTransformerName
を指定するだけでOKです。カスタムクラスが必要なパターン
上に挙げた型以外の
NSSecureCoding
に準拠した標準の型(UIColor
など)や、NSSecureCoding
に準拠した独自の型を使う場合は、NSSecureUnarchiveFromDataTransformer
を継承したカスタムクラスを実装する必要があります。// Core Dataで利用するために @objc が必要です @objc final class SecureValueTransformer: NSSecureUnarchiveFromDataTransformer { // クラス名をvalue transformerのnameとします static let name = NSValueTransformerName(rawValue: String(describing: SecureValueTransformer.self)) // superが返すクラスの配列に追加する形で使用するクラスを指定します override static var allowedTopLevelClasses: [AnyClass] { super.allowedTopLevelClasses + [ UIColor.self, NSTimeZone.self, CustomTypeA.self, CustomTypeB.self, ] } // value transformerを登録する処理です。Core Data Stackの初期化前に呼びます。 public static func register() { let transformer = SecureValueTransformer() ValueTransformer.setValueTransformer(transformer, forName: name) } }モデルエディタではTransformerの欄に
name
で指定したクラス名を入力します。最後にCore Data Stackの初期化前に以下を実行して、value transformerを登録します。
SecureValueTransformer.register()参考リンク
- 投稿日:2020-09-12T12:54:42+09:00
SwiftUI用のジョイスティックUIライブラリ OMJoystickの使い方
OMJoystickはSwiftUI用のジョイスティックUIライブラリです。CocoaPodsとSwift Package Managerからダウンロードできます。
動作イメージ
インストール方法
CocoaPodsでPodfileに下記のように記述します。
pod 'OMJoystick'
動かし方
以下のコードで、デフォルト設定のまま動かすことができます。
import SwiftUI import OMJoystick struct ContentView: View { var body: some View { OMJoystick(colorSetting: ColorSetting()) { (joyStickState, stickPosition) in } } }アイコンやサイズ、色などの見た目を変えたい場合は、下記のように設定します。
import SwiftUI import OMJoystick import SFSafeSymbols struct ContentView: View { let iconSetting = IconSetting( leftIcon: Image(systemSymbol: .arrowLeft), rightIcon: Image(systemSymbol: .arrowRight), upIcon: Image(systemSymbol:.arrowUp), downIcon: Image(systemSymbol: .arrowDown) ) let colorSetting = ColorSetting(subRingColor: .red, bigRingNormalBackgroundColor: .green, bigRingDarkBackgroundColor: .blue, bigRingStrokeColor: .yellow) var body: some View { GeometryReader { geometry in VStack(alignment: .center, spacing: 5) { OMJoystick(isDebug: true, iconSetting: self.iconSetting, colorSetting: ColorSetting(), smallRingRadius: 70, bigRingRadius: 120 ) { (joyStickState, stickPosition) in }.frame(width: geometry.size.width-40, height: geometry.size.width-40) } } } }