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

【SwiftUI】BlendMode で View を合成する

はじめに

この記事では、Adobe Photoshop などのデザインツールでおなじみのブレンドモードについて扱います。
コードと実行結果は SwiftUI のものですが、ブレンドモードは Core Graphics が提供しているので、SwiftUI を使わない開発でも知識としては使えると思います。

さて、本題です。
SwiftUI で2つのビューをブレンドしてみます。こんな感じのイメージです。
はじめに.jpeg
緑が透過なビューの場合は単純に上に載せるだけなので、非透過な緑色のビューとの合成を考えます。
この場合のサンプルコードはこちら。

struct ContentView: View {
    var body: some View {
        ZStack {
            Image("lenna")
                .resizable()
                .scaledToFit()
            Rectangle()
                .fill(Color.green)
                .blendMode(.multiply) // ← ココ!
        }
        .edgesIgnoringSafeArea(.all)
    }
}

簡単な説明

  • ZStackImage の上に Rectangle を載せる
    • Image を配置
      • アスペクト比を維持してビューにフィット
    • Rectangle を配置
      • green で塗りつぶし
      • 乗算ブレンド
  • セーフエリアを無視して配置

実行結果

はじめに2.jpeg

BlendMode を指定して配置するだけなので、とても簡単ですが、次のことが気になりました。

  • 乗算ブレンド .multiply とはどのような合成方法なのか?
  • 他の BlendMode を指定した場合はどのような結果が得られるのか?

ということで、本記事では各種ブレンドモードについての情報と実際の表示結果を載せました。
表示結果は SwiftUI のプレビュー機能を使用しています。

環境

  • Xcode Version 11.5 (11E608c)

BlendMode

SwiftUI の API リファレンスには概要が記載されていなかったので、Core Graphics の API リファレンスも参照しました。次のように記載されていました。

You can find more information on blend modes, including examples of images produced using them, and many mathematical descriptions of the modes, in PDF Reference, Fourth Edition, Version 1.5, Adobe Systems, Inc. If you are a former QuickDraw developer, it may be helpful for you to think of blend modes as an alternative to transfer modes1

Adobe Systems の PDF Reference, Fourth Edition に正確な情報が記載されているとのことです。
この資料には、全てではないですが、各ブレンドモードの数式も記載がありました。

PDF Reference に記載のあるブレンドモード

BlendMode CGBlendMode 名称
normal kCGBlendModeNormal 通常
multiply kCGBlendModeMultiply 乗算
screen kCGBlendModeScreen スクリーン
overlay kCGBlendModeOverlay オーバーレイ
darken kCGBlendModeDarken 比較(暗)
lighten kCGBlendModeLighten 比較(明)
colorDodge kCGBlendModeColorDodge 覆い焼きカラー
colorBurn kCGBlendModeColorBurn 焼き込みカラー
softLight kCGBlendModeSoftLight ソフトライト
hardLight kCGBlendModeHardLight ハードライト
difference kCGBlendModeDifference 差の絶対値
exclusion kCGBlendModeExclusion 除外
hue kCGBlendModeHue 色相
saturation kCGBlendModeSaturation 彩度
color kCGBlendModeColor カラー
luminosity kCGBlendModeLuminosity 輝度

以下、数学的な説明が必要な場合は、PDF Reference, Fourth Edition で記載されていた次の変数を使います。

変数 意味
$C_b$ 背景の色(基本色)
$C_s$ 前面の色(合成色)
$C(C_b, C_s)$ ブレンド関数

上記変数は各色成分をもつベクトルです。また、今回扱う関数は、各色成分ごとに分離可能な関数です。

normal

通常

各ピクセルを編集またはペイントして結果色を作成します。これは、初期設定のモードです(通常モードは、モノクロ 2 階調画像やインデックスカラー画像で作業するときには、2 階調化と呼ばれます)。2

ブレンド関数は次の通りです。前面をそのまま反映します。

B(c_b,c_s) = c_s

Screen Shot 2020-07-12 at 18.43.59.png
Screen Shot 2020-07-12 at 19.47.38.png

multiply

乗算

各チャンネル内のカラー情報に基づき、基本色と合成色を乗算します。結果色は暗いカラーになります。どのカラーも、ブラックで乗算すると結果はブラックになります。どのカラーも、ホワイトで乗算した場合は変更されません。ブラックまたはホワイト以外のカラーでペイントしている場合、ペイントツールで繰り返しストロークを描くとカラーは徐々に暗くなります。この効果は、複数のマーカーペンで描画したような効果が得られます。3

ブレンド関数は次の通りです。背景カラーと前面カラーの RGB の構成要素をそれぞれ乗算します。

B(c_b,c_s) = c_b × c_s

背景ビューと前面ビューを入替えても同じ結果を得ることができます。

色の各成分は、0 以上 1 以下の小数です。特に ブラック(0, 0, 0), ホワイト(1, 1, 1) なので、
ブラックで乗算すると結果はブラック、ホワイトで乗算した場合は変更されないことがわかります。

また、0 以上 1 未満の数は乗算を繰り返すほど、0 に近づくので、
multiply ブレンドモードを繰り返すほど徐々に暗くなります。
Screen Shot 2020-07-12 at 18.44.21.png
Screen Shot 2020-07-12 at 19.47.52.png

screen

スクリーン

各チャンネル内のカラー情報に基づき、合成色と基本色を反転したカラーを乗算します。結果色は明るいカラーになります。ブラックでスクリーニングすると、カラーは変更されません。ホワイトでスクリーニングすると、ホワイトになります。この効果は、複数の写真スライドを重ね合わせて投影したような効果が得られます。4

ブレンド関数は次の通りです。

B(c_b,c_s) = 1 – (1–c_b)×(1–c_s)

背景カラーと前面カラーをそれぞれ反転させたものを乗算して、その結果の値を反転させます。

この式から、multiply と逆の性質を得ることができます。

  • ブラックとスクリーンをすると変更されない
  • ホワイトとスクリーンをすると結果はホワイト
  • 繰り返すほど徐々に明るくなる

Screen Shot 2020-07-12 at 18.44.40.png
Screen Shot 2020-07-12 at 19.48.02.png

overlay

オーバーレイ

基本色に応じて、カラーを乗算またはスクリーンします。基本色のハイライトおよびシャドウを保持しながら、パターンまたはカラーを既存のピクセルに重ねます。基本色は、置き換えられませんが、合成色と混合されて基本色の明るさまたは暗さを反映します。5

Screen Shot 2020-07-12 at 18.44.59.png
Screen Shot 2020-07-12 at 19.48.20.png

darken

比較(暗)

各チャンネル内のカラー情報に基づき、基本色または合成色のいずれか暗い方を結果色として選択します。合成色よりも明るいピクセルが置き換えられ、合成色よりも暗いピクセルは変更されません。6

ブレンド関数は次の通りです。

B(c_b, c_s) = min(c_b, c_s)

Screen Shot 2020-07-12 at 18.45.19.png
Screen Shot 2020-07-12 at 19.48.58.png

lighten

比較(明)

各チャンネル内のカラー情報に基づき、基本色または合成色のいずれか明るい方を結果色として選択します。合成色よりも暗いピクセルが置き換えられ、合成色よりも明るいピクセルは変更されません。7

ブレンド関数は次の通りです。

B(c_b, c_s) = max(c_b, c_s)

Screen Shot 2020-07-12 at 18.45.34.png
Screen Shot 2020-07-12 at 19.49.10.png

colorDodge

覆い焼きカラー

各チャンネルのカラー情報に基づき、基本色を明るくして基本色と合成色のコントラストを落とし、合成色を反映します。ブラックと合成しても変化はありません。8

Screen Shot 2020-07-12 at 18.45.48.png

colorBurn

焼き込みカラー

各チャンネルのカラー情報に基づき、基本色を暗くして基本色と合成色のコントラストを強くし、合成色を反映します。ホワイトで合成した場合は、何も変更されません。9

Screen Shot 2020-07-12 at 18.46.01.png

softLight

ソフトライト

合成色に応じて、カラーを暗くまたは明るくします。画像上でスポットライトを照らしたような効果が得られます。合成色(光源)が 50 %グレーよりも明るい場合、画像は覆い焼きされたかのように明るくなります。合成色が 50 %グレーよりも暗い場合、画像は焼き込んだように暗くなります。純粋な黒または白でペイントすると、その部分の明暗ははっきりしますが、純粋な黒または白にはなりません。10

Screen Shot 2020-07-12 at 18.46.14.png

hardLight

ハードライト

合成色に応じて、カラーを乗算またはスクリーンします。画像上で直接スポットライトを照らしたような効果が得られます。合成色(光源)が 50 %グレーよりも明るい場合、画像はスクリーンされたかのように明るくなります。これは、画像にハイライトを追加するときに役立ちます。合成色が 50 %グレーよりも暗い場合、画像は乗算されたかのように暗くなります。これは、画像にシャドウを追加するときに役立ちます。純粋なホワイトまたはブラックでペイントすると、純粋なホワイトまたはブラックになります。11

Screen Shot 2020-07-12 at 18.46.26.png

difference

差の絶対値

各チャンネル内のカラー情報に基づいて、合成色を基本色から取り除くか、基本色を合成色から取り除きます。明るさの値の大きい方のカラーから小さい方のカラーを取り除きます。ホワイトと合成すると基本色の値が反転しますが、ブラックと合成しても変化はありません。12

ブレンド関数は次の通りです。

B(c_b, c_s) = | c_b – c_s |

ホワイトとブレンドすると色反転ができます。

Screen Shot 2020-07-12 at 18.46.43.png
Screen Shot 2020-07-12 at 19.49.32.png

exclusion

除外

差の絶対値モードと似ていますが、効果のコントラストはより低くなります。ホワイトと合成すると、基本色の値が反転しますが、ブラックと合成しても変化はありません。13

Screen Shot 2020-07-12 at 18.46.59.png
Screen Shot 2020-07-12 at 19.49.47.png

hue

色相

ベースカラーの輝度と彩度、およびブレンドカラーの色相を持つ最終カラーが作成されます。14

saturation

彩度

基本色の輝度と色相および合成色の彩度を使用して、結果色を作成します。このモードで彩度ゼロ(グレー)の領域をペイントした場合は、何も変更されません。15

color

カラー

基本色の輝度と、合成色の色相および彩度を使用して、結果色を作成します。これにより、画像内のグレーレベルが保持され、モノクロ画像のカラー化およびカラー画像の階調化に役立ちます。16

luminosity

輝度

基本色の色相および彩度と、合成色の輝度を使用して、結果色を作成します。このモードでは、カラーモードの反対の効果が作成されます。17

サンプルコード

今回のスクリーンショットで使ったコードはこちら。

表示データ

Item.swift
import SwiftUI

struct Item: Hashable {
    let mode: BlendMode, name: String

    static var items: [Item] = [
        Item(mode: .normal,     name: ".normal"),
        Item(mode: .multiply,   name: ".multiply"),
        Item(mode: .screen,     name: ".screen"),
        Item(mode: .overlay,    name: ".overlay"),
        Item(mode: .darken,     name: ".darken"),
        Item(mode: .lighten,    name: ".lighten"),
        Item(mode: .colorDodge, name: ".colorDodge"),
        Item(mode: .colorBurn,  name: ".colorBurn"),
        Item(mode: .softLight,  name: ".softLight"),
        Item(mode: .hardLight,  name: ".hardLight"),
        Item(mode: .difference, name: ".difference"),
        Item(mode: .exclusion,  name: ".exclusion"),
    ]
}

画像と色のブレンド

ImageBlendView.swift
import SwiftUI

struct ImageBlendView: View {
    var color: Color
    var mode: BlendMode

    var body: some View {
        ZStack {
            Image("lenna").resizable().scaledToFit()
            Rectangle().fill(color).blendMode(mode)
        }
    }
}

struct ImageBlendViewSamples: View {
    var mode: BlendMode
    var colors: [Color] = [.clear, .red, .green, .blue, .black, .white]
    var body: some View {
        HStack(spacing: 0) { [colors, mode] in
            ForEach(0..<colors.count) { i in
                ImageBlendView(color: colors[i], mode: mode)
            }
        }
    }
}

// MARK: - PreviewProvider
struct ImageBlendViewSamples_Previews: PreviewProvider {
    static var previews: some View {
        Group { [items = Item.items] in
            ForEach(0..<items.count) { i in
                ImageBlendViewSamples(mode: items[i].mode)
                    .previewDisplayName(items[i].name)
            }
        }
        .previewLayout(.fixed(width: 100 * 6, height: 100))
    }
}

円のブレンド

import SwiftUI

struct ColorBlendView: View {
    var mode: BlendMode
    var background: Color
    var colors: [Color]
    var body: some View {
        GeometryReader<AnyView> { [mode, colors] geometry in
            let edge = min(geometry.size.width, geometry.size.height)/2
            let offset = edge/3
            let arg: (Int) -> CGFloat = { 2*(.pi)*CGFloat($0)/CGFloat(colors.count) - (.pi)/2 }
            return AnyView(
                ZStack {
                    ForEach(0..<colors.count) { index in
                        Circle()
                            .fill(colors[index])
                            .frame(width: edge)
                            .offset(x: offset*cos(arg(index)), y: offset*sin(arg(index)))
                            .blendMode(mode)
                    }
                }
                .frame(width: geometry.size.width, height: geometry.size.height)
            )
        }
        .background(background)
        .edgesIgnoringSafeArea(.all)
    }
}

struct ColorBlendViewSamples: View {
    var mode: BlendMode
    var foregrounds: [[Color]] = [
        [.red, .green, .blue],
        .init(repeating: .green, count: 7)
    ]
    var backgrounds: [Color] = [
        Color(red: 0, green: 0, blue: 0),
        Color(red: 1, green: 1, blue: 1),
    ]
    var body: some View {
        HStack(spacing: .zero) { [mode, foregrounds, backgrounds] in
            ForEach(0..<backgrounds.count) { i in
                ForEach(0..<foregrounds.count) { j in
                    ColorBlendView(mode: mode, background: backgrounds[i], colors: foregrounds[j])
                }
            }
        }
    }
}

// MARK: - PreviewProvider
struct ColorBlendViewSamples_Previews: PreviewProvider {
    static var previews: some View {
        Group { [items = Item.items] in
            ForEach(0..<items.count) { i in
                ColorBlendViewSamples(mode: items[i].mode)
                    .previewDisplayName(items[i].name)
            }
        }
        .previewLayout(.fixed(width: 120 * 4, height: 120))
    }
}

参考

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

SwiftUI NavigationLinkの遷移時に別処理をかませる

備忘録。
この用途にあったメソッドがあった。

.simultaneousGestureをNavigationLinkにつけてあげれば良い↓

NavigationLink(destination: TradeView(trade: trade)) {
     Text("Trade View Link")
}.simultaneousGesture(TapGesture().onEnded{
     print("Hello world!")
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS13]キャッシュの設定について

どうやって設定するか

URLRequestを使っている場合

URLRequestクラスのcachePolicyプロパティより編集する。

参考:cachePolicy

Alamofireを使っている場合

やはりURLRequestにキャッシュポリシーを設定し、Alamofireに渡すことになる。

// URLRequestを作成する際に、cachePolicyでキャッシュを無効化に指定する.
let request = URLRequest(url: url!,
                         cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
                         timeoutInterval: 5)

// URLRequestをAlamofire.requestに渡す。
Alamofire.request(request).responseString(encoding: .utf8) { data in
  // なんらかの処理
}

参考: [Swift] Alamofireでキャッシュを無効化してリクエストを発行する

Moyaを使っている場合

やはりMoyaの機能としてキャッシュポリシーを変える機能はないため、URLRequestを使うことになる。MoyaのPluginTypeを利用すると便利。

MoyaCacheable.swift
protocol MoyaCacheable {
  typealias MoyaCacheablePolicy = URLRequest.CachePolicy
  var cachePolicy: MoyaCacheablePolicy { get }
}

MoyaCacheablePlugin.swift
final class MoyaCacheablePlugin: PluginType {

  ///リクエスト送信前に呼ばれるメソッド。リクエストを編集したい時に使える
  func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {

    if let moyaCachableProtocol = target as? MoyaCacheable {
      var cachableRequest = request

    ///カスタムのキャッシュポリシーを設定
      cachableRequest.cachePolicy = moyaCachableProtocol.cachePolicy
      return cachableRequest
    }
    return request
  }
}
Hoge.swift
//Hogeクラスをプロバイダに設定
let hogeProvider = MoyaProvider<Hoge>(plugins: [MoyaCacheablePlugin()])

extension Hoge: MoyaCacheable {
  var cachePolicy: MoyaCacheablePolicy {

      //採用したいキャッシュポリシーの定義
      return .reloadIgnoringLocalAndRemoteCacheData
    }
  }
}

あとは、hogeProviderを用いてリクエストを送れば、Hogeクラス内で設定したキャッシュポリシーが使われる。

参考:Make your Moya cacheable

設定の中身

cachePolicy 説明
useProtocolCachePolicy (デフォルト)特定のURLロード要求に対して、プロトコル実装で定義されているキャッシング・ロジックがあればそれを使用します。 HTTPまたはHTTPSの場合、RFC2616というロジックを使います。
reloadIgnoringLocalCacheData ローカルに保存されたキャッシュデータを無視して取得します。
reloadIgnoringLocalAndRemoteCacheData ローカルのキャッシュデータに加え、ネットワーク途中のプロキシなども含めてキャッシュを無視して取得します。iOS13以降より有効
returnCacheDataElseLoad キャッシュデータがあれば、その有効期限などは考慮せずキャッシュを使います。なければ、取得します。
returnCacheDataDontLoad キャッシュデータがあれば、その有効期限などは考慮せずキャッシュを使います。なければ、失敗になります。
reloadRevalidatingCacheData HEADリクエストを発行し更新を確認します、更新があればリクエストを発行します。

参考:【Swift 4.0】 APIでキャッシュを無視してリクエストする方法

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

Kerasで作成した回帰モデルをCoreML上で使用するまで

はじめに

iOSの機械学習フレームワークCoreMLではKerasで訓練済みの機械学習モデルを利用することができます。
本記事ではKerasによる回帰モデルをCoreML上で動くように変換し、実際に予測を行うまでの流れについて解説します。

大まかな流れとしては以下のようになります。

  • 1: Kerasで回帰モデルを作成/学習し、.h5ファイルとして出力する
  • 2: coremltoolsを用いて出力した.h5ファイルを.mlmodelファイルに変換する
  • 3: iOSのCoreML上で回帰モデルとして使用する

本記事における実行環境

  • Python: 3.7.7
  • Keras: 2.3.1
  • Swift: 5
  • iOS: 13.5
  • Xcode: 11.5

1: 回帰モデルを.h5ファイルとして出力

https://www.tensorflow.org/tutorials/keras/regression
今回は上記チュートリアルに基づいて自動車の燃費を予測する回帰モデルを作成し、.h5ファイルとして出力します。

ただし、チュートリアルのコードをそのまま実行してできたモデルをCoreMLで使用できる形式に変換しようとするとエラーが発生します。KerasをTensorflow経由でimportしていることが関係しているようです。
そのためKerasを直接importする形に書き直して実行します。下記コードをご参照ください。

from __future__ import absolute_import, division, print_function, unicode_literals

import pathlib
import pandas as pd

import keras
from keras.models import Sequential
from keras.layers import Dense
from keras import optimizers

dataset_path = keras.utils.get_file("auto-mpg.data", "https://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data")

column_names = ['MPG','Cylinders','Displacement','Horsepower','Weight',
                'Acceleration', 'Model Year', 'Origin'] 
raw_dataset = pd.read_csv(dataset_path, names=column_names,
                      na_values = "?", comment='\t',
                      sep=" ", skipinitialspace=True)
dataset = raw_dataset.copy()

dataset = dataset.dropna()
origin = dataset.pop('Origin')
dataset['USA'] = (origin == 1)*1.0
dataset['Europe'] = (origin == 2)*1.0
dataset['Japan'] = (origin == 3)*1.0

train_dataset = dataset.sample(frac=0.8,random_state=0)
test_dataset = dataset.drop(train_dataset.index)

train_stats = train_dataset.describe()
train_stats.pop("MPG")
train_stats = train_stats.transpose()

train_labels = train_dataset.pop('MPG')
test_labels = test_dataset.pop('MPG')

def norm(x):
  return (x - train_stats['mean']) / train_stats['std']
normed_train_data = norm(train_dataset)
normed_test_data = norm(test_dataset)

def build_model():
  model = Sequential([
    Dense(64, activation='relu', input_shape=[len(train_dataset.keys())]),
    Dense(64, activation='relu'),
    Dense(1)
  ])
  optimizer = optimizers.RMSprop(0.001)
  model.compile(loss='mse',
                optimizer=optimizer,
                metrics=['mae', 'mse'])
  return model

model = build_model()

class PrintDot(keras.callbacks.Callback):
  def on_epoch_end(self, epoch, logs):
    if epoch % 100 == 0: print('')
    print('.', end='')

EPOCHS = 1000

history = model.fit(
  normed_train_data, train_labels,
  epochs=EPOCHS, validation_split = 0.2, verbose=0,
  callbacks=[PrintDot()])

# これでモデルを.h5ファイルに出力します。
model.save("SampleReg.h5")

2: .h5ファイルを.mlmodelファイルに変換

先ほど作成した.h5ファイルをCoreMLで扱える.mlmodelファイルに変換します。
まずは変換に必要なcoremltoolsをPythonにインストールします。
$ pip install -U coremltools
また、TensorflowやKerasが手元のPython環境にインストールされていない場合は改めてインストールしておいてください。

インストールが完了したら、下記のコードを実行することで.h5ファイルを.mlmodelファイルに変換することができます。

import coremltools

coreml_model = coremltools.converters.keras.convert('SampleReg.h5')
coreml_model.save('SampleRegonCoreML.mlmodel')

3: CoreMLでモデルを使う

Xcodeを起動しiOSプロジェクトを新規に作成し、ファイル一覧に先ほど作成した.mlmodelファイルを追加します。
この.mlmodelファイルをクリックすると、以下の図のようにモデルの概要が表示されます。

スクリーンショット 2020-07-12 13.15.48.png

入力(Inputs)として9つの変数を取り、出力(Outputs)として1つの変数を出すモデルであることが確認できます。
MultiArrayというのはMLMultiArrayというCoreMLのクラスを指すようで、これはモデルの入出力を扱うための多次元配列のクラスになります。形状(Shape)やデータ型を指定して配列を作ることができます。

それではこのモデルを使って実際に予測を行います。下記のコードをご参照ください。

import CoreML

// .mlmodelファイルを追加した時点でモデルと同じ名前のクラスが自動的に生成され、
// さらにそれらの入力データ/出力データを扱うクラスも作られます(名前は「モデル名+Input/Output」)。
let model = SampleRegonCoreML()

// MLMultiArrayはこのように配列形状とデータ型を指定して初期化することができます。
// let inputArray = try! MLMultiArray(shape: [9], dataType: MLMultiArrayDataType.double)
// 一方で従来の配列から直接作ることもできます。今回は元のデータセットから抜き出した1データについて直打ちしてみました。
let inputArray = try! MLMultiArray([1.483887,1.578444,2.890853,1.925289,-0.559020,-1.604642,0.774676,-0.465148,-0.495225])
// 上の配列を用意したモデルの入力としてあてがいます。
let inputToModel: SampleRegonCoreMLInput = try! SampleRegonCoreMLInput(input1: inputArray)
// 予測を行い、その結果を出力します。
if let prediction = try? model.prediction(input: inputToModel) {
    print(prediction.output1)
}

Keras側で同データについてmodel.predict()した結果と、上記コードによって出力される結果が一致することを確認します。

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

[SwiftUI]無駄にAnyViewを使っていませんか?そのAnyView、消せるかもしれません。

型情報を消せるAnyView、便利でついつい使ってしまいますがなるべくなら使いたく無いところ。

別メソッドにしたケース

bodyの中が複雑になった時に、次のように別メソッドに分けることがあります。
some Viewでは返せないのでAnyViewにして型を揃えています。
(some Viewで返そうとすると
Function declares an opaque return type, but the return statements in its body do not have matching underlying typesと言われてコンパイルできない)

var body: some View {
  rootView
}

var rootView: AnyView {
  if isAuthorized {
    return AnyView(HomeView())  
  } else {
    return AnyView(LoginView())
  }
}

このようなケースの場合、rootViewをViewBuilderにすることでAnyViewを消すことができます。

@ViewBuilder
var rootView: some View {
      if isAuthorized {
        HomeView()
      } else {
        LoginView()
      }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS14】Widget(WidgetKit) まとめ

※この記事は2020/7/12時点のBeta版の情報を元に作成しています。

iOS14から新しく登場した
WidgetKitを使ってWidgetを作成する機会があり
その際に調べたことや触ってみてわかったことをまとめました。

下記のWWDC2020のセッションを参考にしています。

Meet WidgetKit
https://developer.apple.com/videos/play/wwdc2020/10028/

Widgets Code-along, part 1: The adventure begins
https://developer.apple.com/videos/play/wwdc2020/10034/

Widgets Code-along, part 2: Alternate timelines
https://developer.apple.com/videos/play/wwdc2020/10035/

Widgets Code-along, part 3: Advancing timelines
https://developer.apple.com/videos/play/wwdc2020/10036/

Add configuration and intelligence to your widgets
https://developer.apple.com/videos/play/wwdc2020/10194/

Build SwiftUI views for widgets
https://developer.apple.com/videos/play/wwdc2020/10033/

Design great widgets
https://developer.apple.com/videos/play/wwdc2020/10103/

Widgetとは?

iOS13では
ホーム画面から左にスワイプすると出てくる
通知センターに設定できるViewで
TodayExtensionを利用して
Viewの表示や内容の更新したり
簡単なタスクを実行することができました。

image.png

https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Today.html

それが
iOS14ではホーム画面にも表示ができるようになり
機能もさらに豊富になりました。

iPadやMacでも
同じように表示することができます。

widget.001.png

iOS14で登場したAppライブラリという機能で
アプリのアイコンをライブラリフォルダに
収納することができるようになったことで
ホーム画面をより自由にカスタマイズできるようになり
Widgetを使うことで
より必要な情報を手軽に確認できるようになりました。

スクリーンショット 2020-07-11 11.06.47.png

Widgetのゴール

WWDCのセッションの中で
下記の3点が
Widgetが目指すものとして掲げられています。

image.png

Glanceable

パッと見てすぐに欲しい情報が確認できる

というような意味です。

https://www.dictionary.com/browse/glanceable

通常ユーザがホーム画面に止まるのは
ほんのわずかな時間です。

そのため
Widgetで何か操作を求めるなど
情報をパッと確認できること以外を行おうとするべきではない
と言っています。

Appleのアプリでは
下記のような情報が表示されるようになっています。

image.png

Relevant

ユーザの関心があるものを適切な時間に表示する

ということのようです。

例えば

  • 朝出かける前には今日の天気を知りたい
  • 日中の仕事中はタスクのTodoを忘れないようにリマインダーをチェックしたい
  • 寝る前にはリラックスできる音楽が聞きたい

などをWidgetでは実現することができます。

また
iPhoneの画面で
全てのWidgetを表示するのはサイズに限界があり
それを解消するために
スマートスタック
という新しい機能が追加されました。

スマートスタック

Widgetのコレクションを
一つのWidgetサイズの中に納め
システムが適切な時間に適切な表示をするように
自動で表示内容を変更できる機能です。


自身でスワイプして表示を変えたり
自動回転をオフにすることもできます。

最終的には
システムで何を表示するかの判断はされますが
開発側で何を優先的に表示させたいのかの設定もできます。

※詳細は後ほど記載します。

Personalize

使用しているユーザに合わせてカスタマイズできる

iOS14のWidgetでは
ユーザがWidgetに表示できる内容をカスタマイズできます。

例えば
天気アプリのWidgetで
自分が住んでいる地域の情報が知りたいとします。

そこでWidgetをタップすると
地域を選択できるリストが表示されます。

widget6.001.png

このように
ユーザが本当に必要としている情報が提供できる機会を
設けることが大切になります。

WidgetKit

こういったWidgetを実現するために
WidgetKitという新しいフレームワークが登場しました。

いくつかWidgetKitのコンセプトが紹介されています。

SwiftUIでマルチプラットホームを簡単に実現する

iOS、iPadOS、macOSどれでも同じ様に表示できるように
現時点でマルチプラットホームを実現する一番簡単な方法として
SwiftUIで実装するようになっています。

さらに
SwiftUIは
DynamicTypeやDarkModeへもほぼ自動で対応してくれます。

準備ができたら表示する

ユーザは
平均で1日90回ホームスクリーンを訪れるそうですが
そのほとんどが一瞬です。

その際に
ローディングインジケータがずらりと並んでいたら
なんの有益な情報も提供できません。

スクリーンショット 2020-07-12 10.26.58.png

そこでWidgetKitは
バックグラウンドでViewを生成して返すようにしています。

そして
Timelineという一つの時間軸の上で
適切なタイミングでViewを構築して表示します。

image.png

そしてこれをフレームワーク側で管理することで
この構築したViewを他の箇所でも再利用しています。

Widgetを追加する時
WidgetGalleryと呼ばれるモーダルの中でWidgetを選択します。

そこに表示されるViewは

  • 事前に用意したView

または

  • すでに構築が完了しているView

が表示されます。

スクリーンショット 2020-07-11 12.16.23.png

すでに構築されたViewを利用することで
実際にホーム画面に置いた時に
Widgetがどう表示されるかを
ユーザは確認することができます。

情報の更新ができる

Widgetを更新するためには

  • アプリからWidgetを更新する
  • 定期的なWidgetの更新をTimelineに設定する

という方法があります。

例えば
カレンダーアプリのWidgetでは
予定のタイミングでWidgetの表示が更新されるように
Timelineに設定をしています。

しかし
途中で予定が変わりアプリのスケジュールを変更したタイミングで
Widgetの更新も行われ
Timelineの設定が更新されます。

スクリーンショット 2020-07-11 12.30.18.png

スクリーンショット 2020-07-11 12.31.07.png

WidgetKitの使い方

ここからはWidgetKitについて見ていきます。

WidgetKitには下記のようなコンセプトがあります。

  • Kind
  • Configuration
  • SupportedFamilies
  • Placeholder

Kind

WidgetKitでは一つのextensionの中で
複数のWidgetを構築することができるようになっています。

スクリーンショット 2020-07-11 12.40.28.png

例えば
株価のアプリでは

  • 複数の株価の概要を確認できるWidget
  • 一つの株価の詳細を確認できるWidget

があります。

widget2.001.png

Configuration

WidgetKitには

  • StaticConfiguration
  • IntentConfiguration

があります。

StaticConfiguration

ユーザがカスタマイズする必要がないものなどに使用します。

例えば
Fitnessアプリの場合
すでにパーソナライズされた情報が表示され
ユーザが設定を変更する必要がないため
StaticConfigurationを使用しているようです。

スクリーンショット 2020-07-11 12.52.16.png

IntentConfiguration

ユーザが設定を変更することができます。

widget3.001.png

Listの部分をタップすると選択可能な一覧が表示され
Widgetに表示される値の種類が変更できます。

これはSiriKit intent definitionファイルを定義することで
固定の選択肢を提供することができます。

さらに
iOS14の新機能
in-app Intent handling
を使って動的に選択肢を作成することもできます。

SupportedFamilies

WidgetKitでは
3種類のサイズのWidgetを作成できます。

  • systemSmall
  • systemMedium
  • systemLarge

デフォルトでは全てのサイズをサポートしています。

image.png

Placeholder

実装する際に必須のViewの一つで

端末の設定の変更時に利用される(文字サイズ変更など)時に
呼ばれるとセッションでは言っていましたが

ほとんど使われることはなく
このタイミングで呼ばれるという保証はない
とも言っています。

ポイントとしては
このWidgetが何を表示するものなのかを表す
ことが大事だそうです。

そして
ユーザデータは含めずデフォルトの内容を含める
ことが推奨されていました。

実装例

ここからはコードを見ていきます。


開発を行うにあたって
Xcode12 Beta1では
シミュレータでWidgetが利用できません(Widgetのリストに表示されない)のでご注意ください。
Beta2では表示されるようになりました。

まずはWidgetを定義します。

image.png

上記で紹介した
KindやPlaceholder、
実際にデータを表示するViewを提供するクロージャを引数に
Configurationを作成しています。

Providerは具体的にViewを提供する方法を定義します。
具体的には後に登場するコードをご覧ください。

下記のような構成になっています。

WidgetKit-Architecture@2x.png

Viewを構築するときのポイント

ここから
より詳細にViewを提供するコードを見ていきますが
その前にViewを構築していく際のポイントについて見てみます。

下記の3つのWidgetは
必要最低限な情報を表示しており
タップするとアプリを開いて詳細画面へ遷移します。

スクリーンショット 2020-07-11 14.58.13.png

大事なのは

状態を持たないUI

を構築することです。

Widgetでは
スクロールや動画の再生などの操作はできません。

これは上記でも記載した
ユーザがホーム画面にいる時間の短さなどによる特徴だと考えられます。

その代わりに
Widgetをタップすることで
アプリにDeep Linkすることができます。

image.png

systemSmallはWidget全体で一つのリンクになっており
タップするとアプリへ直接遷移します。

systemMediumsystemLargeでは
個々のViewにwidgetURLというメソッドを使うことで
別々のリンクを設定してアプリに遷移することができます。

https://developer.apple.com/documentation/swiftui/view/widgeturl(_:)

スクリーンショット 2020-07-11 15.16.21.png

スクリーンショット 2020-07-11 15.15.57.png

そしてアプリ側ではonOpenURLメソッドを使って
Deep Linkをハンドリングできます。

https://developer.apple.com/documentation/swiftui/view/onopenurl(perform:)

ドキュメントトップ
https://developer.apple.com/documentation/widgetkit


リンク先への遷移に失敗すると
アプリのトップページに遷移しました。

Viewを提供するコード

それでは
コードを見ていきます。

Viewを提供する箇所は3つあります。

  • Placeholder
  • Snapshot
  • Timeline

Placeholderについては
すでに見ましたので
下の2つについて見ていきます。

Snapshot

システムが
今すぐに1つのViewを必要としている時に
提供するViewです。

スクリーンショット 2020-07-11 15.38.24.png

ただし
これはダミーデータではなく
実際のWidgetで利用するデータです。

なので
SnapshotとTimelineの最初のViewは
多くの場合で同じになります。

同時に
WidgetGalleyに表示されるViewになり
ユーザがWidgetを追加した時に
最初に表示されるViewにもなります。

Timeline

複数のViewと日付の組み合わせで
いつどのViewをWidgetに表示したいか
を設定します。

スクリーンショット 2020-07-11 15.48.24.png

こうすると

フレームワークが
Viewをシリアライズ化してディスクに保存し
必要な時になったらレンダリングを行う

という仕組みになっているようです。

image.png

Timelineには
一般的には1日分の内容を設定するべきだそうですが
場合によってはより更新が必要なものもあります。

その場合
Reloads
と呼ばれるコンセプトに基づいて
表示の更新が実行されます。


ドキュメントにも詳細が記載されていますのでそちらをご参照ください。
https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date

Reloads

フレームワークが
デバイスにあるそれぞれのWidgetを起動し
新しいTimelineを提供するように要求します。

Reloadsを行うことで
ユーザに最新の情報を提供していることを保証できます。

image.png


Reloadsに関しては
ドキュメントにも詳しい記載があるのでそちらもご参照ください
https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date

具体的にはTimelineProviderを利用します。

スクリーンショット 2020-07-11 16.00.57.png

https://developer.apple.com/documentation/widgetkit/timelineprovider

主に日付を持ったTimelineEntryがあり
https://developer.apple.com/documentation/widgetkit/timelineentry

Contextにはその時のデバイスの情報などが含まれています。

  • systemSmall or systemMedium or systemLarge
  • Widgetのサイズや位置
  • Widgetが表示される際に設定されている全てのenvironmentの情報
  • WidgetがWidgetGalleryに表示されるかどうか

https://developer.apple.com/documentation/widgetkit/timelineprovidercontext

例えば
Widgetを表示するのに
外部からリソースを取得することが必要な場合
WidgetGalleryに表示されるまでに時間がかかったり
エラーが発生すると表示されないことがあります。
(その場合真っ黒な画面になります。)

そこで
isPreviewメソッドを活用してtrueの場合には
staticなデータを返却するなどの対策を行うことができます。

snapshot

snapshotメソッドでは
1つのTimelineEntry

timeline

timelineメソッドでは
複数のTimelineEntryTimelinePolicyを添付した
Timeline

返します。

下記が具体的な定義方法の例です。

スクリーンショット 2020-07-11 16.20.53.png

TimelinePolicy

TimelinePolicyには3種類あります。

image.png

atEnd

提供した最後のEntryが表示された際に
フレームワークにreloadを依頼します。

after(date: Date)

指定した日時に
フレームワークにreloadを依頼します。

never

reloadをしないようにフレームワークに依頼します。

https://developer.apple.com/documentation/widgetkit/timelinereloadpolicy

下記はドキュメントのイメージで
どのようにreloadが発生するのかが示されています。

最初にProviderから
3つのTimelineEntryを持ち
atEndをRefresh Policyに設定したTimelineを提供しています。

そうすると
最後のTimelineEntryが表示されたタイミングで
フレームワークがreloadを呼ぶことで
再びProviderから
新しいTimelineを提供しています。
neverをRefresh Policyに設定しているため
表示はずっと変わらなくなります。

image.png

注意点

ただし注意しなければいけない点としては
この設定通りには動かない可能性があります。

ReloadsはTimelinePolicyを基に
どのくらいWidgetが見られているかや
システムの状態を考慮して
最終的にフレームワーク側でreloadのタイミングを決めます。

また
システムの時間が急に大きく変わった場合などに
強制的にreloadする場合もあります。


Betaだからかもしれませんが
このタイミングはかなりバラつきがあり
どういうタイミングで発生するのかが
いまいちつかめませんでした。

アプリからも更新できる

上記では
フレームワークにreloadのタイミングを委ねる方法でしたが
アプリからWidgetを明示的に更新する方法もあります。

例えば
アプリがバックグラウンド通知を受け取った時や
アプリ内でユーザがデータを更新した場合などが
考えられます。

image.png

WidgetCenterを使用します。

アプリに紐づいている特定のWidgetのみを更新したり
全てのWidgetを更新することもできます。
また現在のConfigurationを知ることもできます。

スクリーンショット 2020-07-11 16.41.32.png

https://developer.apple.com/documentation/widgetkit/widgetcenter

注意点

ただし何でもかんでも更新するのではなく
Widgetに表示する内容に関連する更新が行われた際にのみ
更新するように考慮する必要があると言っていました。

バックグラウンドのURLSessionからデータを受け取る

Timelineを構築する際に
サーバからデータを取得する場合があると思います。

その際にバックグラウンドでAPIの呼び出しを行い
onBackgroundURLSessionEvents(matching:_:)
でリクエストの結果を受け取ることもできます。


ドキュメントやセッションでは言及されていますが
実際に試したところイベントを受け取ることができませんでした。
サンプルでも記載がないのでまだわかっておらず
引き続き調べています。

もしご存知の方いらっしゃいましたら
教えていただけましら幸いです??‍♂️

https://developer.apple.com/documentation/widgetkit/intentconfiguration/onbackgroundurlsessionevents(matching:_:)-78bry

注意点

ただし
こちらもリクエスト数なども考慮する必要があります。

Widget用に複数のリクエストを1つにまとめるなどの
対策が必要になるかもしれません。

アプリがバックグラウンドで動いている際のreloadのタイミングは
システムに委ねられます。

Reloadsを行う際には

プロセスやリクエストを効率的に行うこと。
Widgetは毎秒単位のずっと動いているようなものではありません。

Widgetの性質によって
どのくらいのReloadsが必要になるかを見積もり
考慮した設計をしましょう。

PersonalizationとIntelligence

Widgetをよりユーザに使いやすくするための
主に2つのコンセプトがあります。

  • Intents
  • Relevance

Intents

Intens.frameworkを利用して
ユーザにどのようにWidgetを表示するかの
選択肢を提供できます。

すでにSiriとShortcutsを一緒に利用する際に
活用されていた技術が
iOS14ではWidgetでも利用できるようになりました。

widget3.001.png


以下で簡単に実装方法を記載していますが
詳細に関してはドキュメントをご参照ください。
https://developer.apple.com/documentation/widgetkit/making-a-configurable-widget

Intent Definitionファイルの生成

File > New File and select SiriKit Intent Definition File
から作成できます。

※ドキュメントの図です。
image.png

ファイルを作成すると内部で
この内容にそったクラスが自動で生成されます。
CoreDataのxcdatamodeldと似たような仕組みのようです。

コード

IntentConfiguration
IntentTimelineProviderを活用します。

スクリーンショット 2020-07-11 17.23.17.png

スクリーンショット 2020-07-11 17.25.18.png

https://developer.apple.com/documentation/widgetkit/intentconfiguration
https://developer.apple.com/documentation/widgetkit/intenttimelineprovider

Intents Extensionを使って動的に選択肢を生成する

in-app Intent handling
というiOS14の新機能を使って
動的に選択肢を生成することもできます。

その場合
Intents Extensionをターゲットに追加し
IntentHandlerを実装する必要があります。

下記のような形で実装します。

class IntentHandler: INExtension, SelectCharacterIntentHandling {
    func provideCharacterOptionsCollection(for intent: SelectCharacterIntent, with completion: @escaping (INObjectCollection<GameCharacter>?, Error?) -> Void) {

        // Iterate the available characters, creating
        // a GameCharacter for each one.
        let characters: [GameCharacter] = CharacterDetail.availableCharacters.map { character in
            let gameCharacter = GameCharacter(
                identifier: character.name,
                display: character.name
            )
            gameCharacter.name = character.name
            return gameCharacter
        }

        // Create a collection with the array of characters.
        let collection = INObjectCollection(items: characters)

        // Call the completion handler, passing the collection.
        completion(collection, nil)
    }
}

そしてIntentTimelineProviderの中で
結果を使用します。

struct CharacterDetailWidget: Widget {
    var body: some WidgetConfiguration {
        IntentConfiguration(
            kind: "com.mygame.character-detail",
            intent: SelectCharacterIntent.self,
            provider: CharacterDetailProvider(),
            placeholder: CharacterPlaceholderView()
        ) { entry in
            CharacterDetailView(entry: entry)
        }
        .configurationDisplayName("Character Details")
        .description("Displays a character's health and other details")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}
struct CharacterDetailProvider: IntentTimelineProvider {
    func timeline(for configuration: SelectCharacterIntent, with context: Context, completion: @escaping (Timeline<CharacterDetailEntry>) -> ()) {
        // Access the customized properties of the intent.
        let characterDetail = lookupCharacterDetail(for: configuration.character.name)

        // Construct a timeline entry for the current date, and include the character details.
        let entry = CharacterDetailEntry(date: Date(), detail: characterDetail)

        // Create the timeline and call the completion handler. The .never reload 
        // policy indicates that the containing app will use WidgetCenter methods 
        // to reload the widget's timeline when the details change.
        let timeline = Timeline(entries: [entry], policy: .never)
        completion(timeline)
    }
}

Relevance

冒頭の方でも紹介しましたスマートスタックの中で
システムは合理的にユーザが関心が高いであろう
Widgetを表示するようにします。

その際の評価方法をアプリやWidgetを通じて
変更することができます。

主に2つの方法があります。

  • アプリ内のユーザの操作からShortcutsにdonationする
  • WidgetのTimelineEntryTimelineEntryRelevanceを設定する

Shortcutsにdonation

ユーザの操作をINIntentとして
donationしておくことで
同じINIntentだと判断された際に
スタックの中のWidgetが表示される可能性が高まります。

image.png

下記の例では

  • 使用したカード
  • 購入した商品カテゴリー

を表示するWidgetですが

月曜日と火曜日に
同じカードで同じ商品カテゴリーのものを購入したアクションを
donationしておきます。

そうすると
金曜日に同じような操作をした際に
その購入履歴のWidgetが表示されやすくなります。

image.png

TimelineEntryにTimelineEntryRelevanceを設定

TimelineEntryには
TimelineEntryRelevanceを使って
重みをつけることができます。

image.png

image.png

https://developer.apple.com/documentation/widgetkit/timelineentryrelevance

scorefloat
これまでに提供した全てのEntryとの比較値として利用されます。

ドキュメントにも下記のような記載があります。

The score is a value 
you choose that indicates the relevance of the widget, 
relative to entries across timelines that the provider creates.

例えば
下記のように購入した金額をscoreに設定することで
購入金額の高いものが優先的に表示されるようになります。

image.png

duration
設定されたEntryが
Widgetに表示する時間になってから
どのくらいそのscoreが有効であるかを示す値です。

例えば
下記の例はバスケットボールの試合のスコアを表示するWidgetです。

試合の開始時にscore加え
試合が終了するまでの3時間(duration = 3hrs)を設定しています。
以降がnil(設定なし)なのは
Relevanceに影響を与えないことを示しています。

一つ前で0としているのは
次のRelevanceが来るまでは
このscore(= 0)を継続して使用することを示しています。

スクリーンショット 2020-07-11 18.14.19.png

良いWidgetを設計するために

ここからは良いWidgetを設計するためのポイントを
セッションの内容から見てみたいと思います。

Design great widgets(優れたWidgetの設計)のセッションの中で
大きく2つに分けて紹介されていました。

  • Ideation(何を表示するのか)
  • Creation(どう表示するのか)


Creationに関しては
HIG(Human Interface Guideline)と内容が似ていましたので
ドキュメントをご参照して頂けましたら幸いです。

https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/widgets

Ideation

大きく分けて3つについて
述べられています。

  • Principles
  • Editing
  • Multiples

Principles

大事なポイントとして以下の3点が挙げられています。

スクリーンショット 2020-07-12 5.11.29.png

Personal

よりユーザに特化した情報を提供することで
アプリとユーザの感情的な繋がりを強くする。

Information

多くの情報の中から
一番重要な情報の概要を選んで提供する。

Contextual

ユーザがアプリで繰り返し操作して得ているような情報(必要としている情報)
を提供する。

必要な情報は時と場合によって異なるので
それに応じた情報が提供できるようにする。

カレンダーアプリは下記のようにスケジュールに応じて
表示方法を変えています。

一番重要な情報として
直近のスケジュールの内容と時刻と場所を表示します。

また
スケジュールの量や祝日に合わせて表示方法を変えたり
連絡先から知り合いの誕生日を表示することができます。

widget4.001.png

天気アプリでは
例えば雷雨が発生している場合は
より細かい分ごとの降水量を表示して
何時ごろに雨が止むのかが確認できるようになります。

widget5.001.png

Editing

iOS14の新機能で
ユーザがWidgetに設定できる内容を
カスタマイズできます。

これはWidgetにどのように情報を表示するのかに
関わってきます。

冒頭の方でも紹介しましたが
天気アプリでは表示する地域を選択できます。

widget6.001.png

さらに同じ種類の複数のWidgetを表示して
他の地域の天気を表示することもできます。

image.png

これが実現できることで
1つのWidgetに複雑な情報を詰め込む必要はなく
シンプルでわかりやすい表示を可能にします。

Multiples

1つのアプリの中で
複数の種類のWidgetを作成することも可能です。

例えば
株価アプリでは
ウォッチリストのサマリーを表示するWidgetと
一つの株価を細かく表示するWidgetが
用意されています。

どういった情報をユーザがすぐにチェックしたいかどうかを考え
複数のWidgetを用意するべきかどうかを検討します。

スクリーンショット 2020-07-12 9.07.33.png

Widget作成に役立つView

最後にセッションの中で紹介されていた
Widget作成において役に立つViewを2つ紹介します。

Text init(_:style:)

SwiftUIのテキストに新しいイニシャライザが追加され
DateからTextを生成できるようになりました。

init(_ date: Date, style: Text.DateStyle)

これは指定した時間からの経過時間を自動で表示してくれます。
Widgetは常時動いているものではありませんが
このTextを活用することで
リアルタイムに起動しているように見せることができます。

※セッションの画像です。

スクリーンショット 2020-07-12 9.44.11.png

またString Interpolationを活用することで
表示のローカライズも可能です。

スクリーンショット 2020-07-12 9.45.50.png

https://developer.apple.com/documentation/swiftui/text/init(_:style:)

ContainerRelativeShape

HIGにも記載されていますが
WidgetのCorner Radiusと
コンテンツのCorner Radiusは
一致させることが推奨されています。

しかし
この値はデバイスサイズに変わってくるため
手動で計算するのは大変難しいです。

スクリーンショット 2020-07-12 9.49.18.png

そこで今回
ContainerRelativeShapeが追加されました。

これは親ViewのCorner Radiusに合わせるように
フレームワーク側で自動で計算してくれるようになります。

https://developer.apple.com/documentation/swiftui/containerrelativeshape

まとめ

iOS14のWidgetについて見てきました。
これまでのアプリのアイコンが並んだホーム画面から
よりユーザが必要としている情報をすぐに確認できるようになるのは
便利そうだなと感じています。

一方で
必要ないものはユーザからどんどん遠ざかっていくのではないかとも感じており
Widgetを使うことでユーザに利便性を高めていくことは
大事になってくるのだろうと思いました。

まだまだ不明な点が多く
実際にReloadの(timelineメソッドが呼ばれる)タイミングで
通知音を出して確認してみましたが
予定の時間に更新がされなかったり
予期せぬタイミングで更新されたりします。


ロック画面から復帰したら鳴ったり
他のアプリ削除したら鳴ったり
ウィジェットを追加していなくても鳴ったり...など

ただ使い続けていると
Timelineに設定した内容に関しては
だんだん時間通りに通知が来るようになってきたなとも感じています。
アプリの使用頻度も関わっているようなので
それが影響しているのかなと思っています。

データの更新とViewの更新のタイミングも
必ずしも一致しているわけではありませんでした。

また
スマートスタックを利用した時に
どのくらいの頻度で自分のWidgetが表示されるのか
Relevanceの有効性がどのくらいなのかなど
まだまだわからないことがあるので
これからも触ってみて色々調べていきたいなと思います?

何か間違いなどございましたら
教えていただけますと助かります??‍♂️

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