20200912のiOSに関する記事は9件です。

本日学んだ、オプショナル型の基礎。【 ! の混同】

本日学んだ、オプショナル型の基礎をまとめます。
自分用の記録用の皮算用。

  ! を混同していた。

"!"はオプショナルではない、nilを許さないというのは間違いです。
このことも勘違いしている人が多いかもしれません。

勘違いしていました。

変数の宣言で型に !をつける場合、その変数は「オプショナル型」

なので、強制的アンラップの!とは別物らしい。

    // 変数の宣言で型に ! をつける場合、その変数は「オプショナル型」
    // (= 強制的アンラップの ! とは別物)
    var hoge: String! // nil

 暗黙的アンラップ型 (= IUO)

Implicitly Unwrapped Optional

  • オプショナル型の1つ (= nliを許容)
  • 使用するとき、必ず強制的アンラップ!をする。
  • つまり、自動でアンラップをしてくれる

但し、nilでアンラップすると、アプリが落ちる?ため、
100%絶対に値がある(= nilでない) 場合のみ、使用可能。

最初は nil で宣言したいが、使うときには絶対に値が入っている、という場合に使用。

 他のアンラップ方法

debugPrint(error?.localizedDescription ?? "")

guard letif letとかの「オプショナル バインディング」の他に、

  • オプショナル チェイニング(?)
  • nil結合演算子 (??)

があるみたいなので、以下にメモっておく。

 オプショナル チェイニング? (= Optional Chaining)

- 値がある場合

アンラップする。

但し、それに続くプロパティやメソッドの戻り値は「オプショナル型」になる。
(-> nilの場合 nil を返すため)

- nilの場合

nil を返す。それに続く処理をすべてキャンセル (-> なので安全)

 nil結合演算子? (nil-coalescing)

A ?? B ... Aに値があるとAをアンラップ。 Aが nil だとBを返す。  

SwiftのNil結合演算子

おしまい。

 参考サイト

どこよりも分かりやすいSwiftの"?"と"!"
-> LGTMの数が異常。

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

[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に表示させるところまでを紹介します。

作るもの

5d9a68e694de4c3cdb5af28bea23435d.gif
今回作るのは、

  • 「英語学習の際のトピック」を親の情報として、
  • 『単語(日本語・英語)』を子供と見立て、情報を同時に表示することです。

これは、入れ子構造(「ネストした」などとも言う)のJSONを分解して、親の情報と子の情報を同時に表示しています。

なお、今回のコードはこちらのGitHubから見ることが出来るので、コードだけ見たい人はどうぞ。

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. チュートリアルのコードを見てみよう

チュートリアルでは、こんな感じのファイルがありましたね。
image.png
こんなファイル構造になっていることがわかると思います。

Model_SwitUI_tutorial.group
- Landmark.swift
- UserData.swift
- Data.swift
- Hike.swift

2. 今回のModelの構成

今回は、このような「Model」のファイル構成にしてみました、少し無駄なファイルを減らしたので、少なくなっています。
image.png

Model_nestedJSONParse.group
- AudioContent.swift
- Data.swift
- UserData.swift

まずは、一番上のLandmark.swiftAudioContent.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
}

しかし、今回は複雑なコードになってしまう原因である fileprivateenum などは使わないことにします。

AudioContent.swift
import 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で出てきたような fileprivateenum を使うをやむ得なくなると思います。

そうなった場合は、quicktype では処理出来ない(文字列からでは画像を取り扱いのかはわからない)ので、他のサンプルコードを参考にやってみてください。


3. 「Model」に入っていたその他のファイル

その他のファイルで、説明していない Data.swiftUserData.swift があったと思います。

クリックしてData.swiftを見る
Data.swift
import 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.swift
import Combine
import SwiftUI

final class UserData: ObservableObject {
    @Published var showFavoritesOnly = false
    @Published var landmarks = landmarkData
    @Published var profile = Profile.default
}

正直難しいので、初心者の方が理解をするのは時間がかかるとは思いますが、簡単に説明すると

  1. JSONファイルを読み取るところ
  2. アプリ操作する人(User)のデータを処理するところ

という認識で問題ないです。

詳しい紹介はこちらの記事を参考にしてください
SwiftUI Tutorialsを読み解く

表示する

実装をしていきます。
ほとんど内部の処理は分離させていないので、もうちょっときれいに書けるはずです。

ContentView.swift
import 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()
        }
    }
}

最初のListは名前だけを取得するようにしました
image.png

タップして遷移すると、こちらのコードのおかげで

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()
        }
    }
}
  1. 親が持っている情報
    • audioContent.name
    • audioContent.description
  2. 子が持っている情報
    • audioContent.phrases[String(num)]!.japanese
    • audioContent.phrases[String(num)]!.english

が表示されています。

image.png

参考サイト

I wish to say big thanks for the Indian on stackover flow.

image.png

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

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までアップデート可能

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

Inside Flutter Hooks

概要

Flutter Hooksを使う機会があり、すごい便利だなと思っていたのですが、
内部的にどんな風に実装されているのか掘り下げてみようかと思い、今回色々調べてみました。
(何か間違っていたりしたらコメントいただけると嬉しいです :bow: )

Flutter Hooks とは?

React hooksをFlutterで実装したものになります。

作者はProvider等でおなじみのRemiさんです。

サンプルの実行環境

flutter: v1.20.3
flutter_hooks: 0.14.0

Flutter 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),
          ),
        ));
  }
}

実行結果
android.gif (68.2 kB)

↑ 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>';
}

より詳細に見ていこうと思います。:eyes:

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();
  }
}

こうして見ると HookHookState の関係が StatefulWidgetState の関係に似てますね :eyes:

HookState 内に先程出てきた HookElement を保持し、BuildContext としてgetできるようにしています。
後で出てきますが、HookElementComponentElement をimplementしているので BuildContext として扱う事ができます。
詳しくはこちらを参照して下さい。
FlutterのBuildContextとは何か - Qiita

HookElement mixin

こちらに実装されています。こちらも要約して抜き出してみました。
HookElementuse メソッドでの処理が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をセットしています

    HookWidgetStatefulHookWidget クラスを使ったWidgetのビルドでは内部的に HookElement を使用しているので HookElementbuild() メソッドが呼ばれます。

  • use メソッド

    • ③ : 前回ビルドした時のHookと今回ビルド中のHookの runtimeType が異なっている場合
      • _currentHookStateに格納されているHookStateをすべてクリアします
      • Debug中の場合(開発しててuseXXXを変更した等)今回Hookを新たに格納します
    • ④ : 前回ビルド時のHookと異なるオブジェクトの場合 shouldPreserveState メソッドでKeyが前回と異なっているかチェックを行います
      • 異なっている場合
        • 一旦以前のHookStateは破棄して今回のHookStateに入れ替えます
      • 異なっていない場合
        • HookStatedidUpdateHook が呼ばれます
    • ⑤ : 次に備えて、_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のビルドが走っても
更新されない保存された値を返し続けるという仕組みになっているようです :sparkles:

  @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 (※今回は省いています)

flutter_hooks相関図.png (48.9 kB)

※ 間違っていたらご指摘いただけると嬉しいです :bow:

他のhooks達

ここまでで何となくでも仕組みが理解できたので、他のhooksも見てみたいと思います。

useContext

実装はこちら
これは至ってシンプルで以下の様に実装されています。

BuildContext useContext() {
  assert(
    HookElement._currentHookElement != null,
    '`useContext` can only be called from the build method of HookWidget',
  );
  return HookElement._currentHookElement;
}

実装をみるとなぜbuild中じゃないと呼び出せないのか分かりますね :eyes:
ちなみに 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'); が呼ばれるのが分かるかと思います。
android2.gif (139.9 kB)

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);
      }
    }

既に説明した通り、HookElementuse メソッド内で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もソースコードを読むことで
ある程度理解できるようになりました :sparkles:
今回のように一番シンプルなものから掘り下げていくのは余分なInputが少ない分理解しやすいですね。

内部的な処理が分かっていれば、useContext をWidgetのビルドタイミング以外で使用したらダメだとか
事前に分かるので、広範囲でお世話になるライブラリ等は事前に内部がどんな風になっているのか把握しておくと、
トータル的にはハマる時間が無くなってスムーズかもしれません :sparkles:

また次も機会があれば何か掘り下げようかと思います。

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

iOSのdidBecomeActive/willResignActiveを視覚的に把握する

モチベーション

ライフサイクルメソッドの説明は、解説付きでケースを列挙されることが多いですが、いまいちイメージがしにくいと感じることが多く感じます。特に、ActiveとInactiveのタイミングは分かりにくいことが多いのではないでしょうか。

この記事では、Sceneベースの各種ライフサイクルメソッドにprintを貼って、active/inactiveとはどのようなステートなのかを視覚的に把握します。(※タイミングの列挙はしません。1つ2つ取り上げるだけです)

解説

1. アプリの起動

スクリーンショット 2020-09-12 15.00.45.png

ライフサイクル

シーンベースのライフサイクルでも、共通系は呼ばれます。なのでdidFinishLauchingがまず始めに呼ばれます。
次に、SceneDelegatewillConnectが呼ばれます。このメソッドは、システム・ユーザー起点にかかわらず、新しいシーンがアプリケーションに接続される直前に呼ばれます。そしてその後は順当にwillEnterForeground, didBecomeActiveと呼ばれていきます。

didFinishLaunching
willConnect
willEnterForeground
didBecomeActive

2. SplitViewの割合変化

もっとも分かりやすい状態遷移です。視覚的な状態と実際の状態が一致しており、直感的に理解できます。

中間状態

スクリーンショット 2020-09-12 15.41.42.png

ライフサイクル

中間状態では、システムによるSplitViewの操作中で、アプリケーション自体の操作はできないため、画面は前面に出ていますがInactiveな状態です。

didFinishLaunching
willConnect
willEnterForeground
didBecomeActive
willResignActive

完了

スクリーンショット 2020-09-12 15.41.52.png

ライフサイクル

割合がユーザーによって決められ、操作が終了するとdidBecomeActiveが呼ばれていることが分かります。

注目すべきは、どちらの状態でもforegroundであることは変わらず、active/inactiveだけの変化であるということです。

didFinishLaunching
willConnect
willEnterForeground
didBecomeActive
willResignActive
didBecomeActive

3. SplitViewから閉じる

スクリーンショット 2020-09-12 15.31.50.png

ライフサイクルメソッド

先程の中間状態からの復帰ではdidBecomeActiveになりましたが、こちらはそのまま閉じてしまった(右側の比率を100%にした)ので、didEnterBackgroundが呼ばれました。

didFinishLaunching
willConnect
willEnterForeground
didBecomeActive
willResignActive
didBecomeActive
willResignActive
didEnterBackground

まとめ

「ForegroundなのにInactiveな状態ってどのようなものなのだろう?」という疑問に対して、これまで「システム的にはそういう状態が存在する」と受け入れて理解していた人も、上記のSplitViewの例を見れば「前面に出ているのに、アプリケーションにアクセスできない瞬間が存在する」というのを視覚的に把握できたかと思います。

他には、アプリ一覧を開いた時にInactiveになったりしますが、詳細は別の方の記事をご参照ください。

実装

AppDelegate.swift
    func 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.swift
import 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")

    }


}



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

コードでSwiftのautolayoutを書く

Storyboardで画面のレイアウトやUI部品をグラフィカルに配置できますが、チームで協同作業する時や細かいレイアウトの調整が発生する場合はStoryboardだとどうしても支障が出ます。

autolayoutの標準APIについて、AppleがNSLayoutConstraintNSLayoutAnchor(ios9~)、Visual Format Language(VFL)を提供しています。NSLayoutAnchorNSLayoutConstraintよりコード量が少なくなり、制約・声明もはっきりするようになっています。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
        }

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

Core Imageによるぼかしのクオリティを高める

Core Imageを使うと、簡単かつ高速に画像ぼかすことができます。


(↑はgif化しているので見た目がいまひとつですが...)

ぼかし自体は CIGaussianBlur ひとつでできるのですが、以下の点に注意しておくことでより高いクオリティの結果を得ることができます。

  • ぼかし後の画像サイズは、入力画像サイズと異なる
  • なにもしないと、画像のエッジ付近がやや暗くなってしまう

CIImage#extent について

CIImage#extent は簡単に言うと、その画像のサイズを持つ出力矩形領域です。
例えば解像度480x640の画像から作られた CIImageextent(x: 0, y: 0, w: 480, h: 640) となります。
しかしこれに、半径 20CIGaussianBlur を適用すると、 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なし

 

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

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()

参考リンク

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

SwiftUI用のジョイスティックUIライブラリ OMJoystickの使い方

OMJoystickはSwiftUI用のジョイスティックUIライブラリです。CocoaPodsとSwift Package Managerからダウンロードできます。

動作イメージ

image.png

インストール方法

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)
            }
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む