20190724のSwiftに関する記事は8件です。

[swift] DateFormatterのインスタンス生成は遅い

開発中のアプリのパフォーマンスの改善のために、Instrumentsで→↓→↓→↓→↓しているときに、DateのExtensionの処理がちょくちょく出てきた。

調べてた結果↓

Cache Formatters for Efficiency
Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, it is typically more efficient to cache a single instance than to create and dispose of multiple instances. One approach is to use a static variable.

by Data Formatting Guide - developer.apple.com

DateFormatter のインスタンス生成は、軽い処理ではないため、何度も使われる箇所では単一のDateFormatterのインスタンスをキャッシュして使いまわそうとのこと。。。

パフォーマンス計測

パフォーマンスコード

// DateFormatter ではなく ISO8601DateFormatter を使用 ?
extension Date {

    private struct Formatter {
        static let iso8601Formatter: ISO8601DateFormatter = ISO8601DateFormatter()
    }

    var ISO8601String: String {
        let formatter = ISO8601DateFormatter()
        return formatter.string(from: self)
    }

    var ISO8601StringWithCache: String {
        return Formatter.iso8601Formatter.string(from: self)
    }
}


let count = 10000
let date = ISO8601DateFormatter().date(from: "1990-01-01T00:00:00+0900")!

var start = Date()
(1...count).forEach { _ in
    _ = date.ISO8601String
}
print("毎回インスタンスを生成: \(Date().timeIntervalSince(start))")


start = Date()
(1...count).forEach { _ in
    _ = date.ISO8601StringWithCache
}

print("インスタンスを生成キャッシュ: \(Date().timeIntervalSince(start))")

結果

毎回インスタンスを生成: 3.0555260181427
インスタンスを生成キャッシュ: 0.5390740633010864

まとめ

  • 定期的にパフォーマンス計測をして、アプリのパフォーマンス改善をしてみましょう
  • DateのExtensionを確認してみよう
  • むやみなFormatterのキャッシュは注意

参考

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

"Reference to property '(property name)' in closure requires explicit 'self.' to make capture semantics explicit"でコンパイルエラーになる 

あるあるだと思うのですが。。。

Class Map 

    var image = UIImage()
    let url = "https://〜"
    let overlay = MKTileOverlay(urlTemplate: url)

     overlay.loadTile(at: path!) { (Data, Error) in            
        image = UIImage(data: Data!)!    //!!Error!!
        return
    }

こんな書き方をすると、

"Reference to property 'image' in closure requires explicit 'self.' to make capture semantics explicit"

と怒られてしまいます。
修正方法は簡単で、Xcodeに言われるがまま、怒られた変数にselfをつければいいです

Class Map 

    var image = UIImage()
    let url = "https://〜"
    let overlay = MKTileOverlay(urlTemplate: url)

     overlay.loadTile(at: path!) { (Data, Error) in            
        self.image = UIImage(data: Data!)!    //OK
        return
    }

で、これは何が怒られているかというと、クロージャの中で変数の参照を持つため、
循環参照になっていないか意識させるために、Swiftの文法上Selfの指定が必須になっています。

loadTile(at:result:) についてはこちら

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

デバッグ時だけログを吐き出す便利なラッパークラス

本記事を書く背景

QiitaでNSLogを使ったデバッグログの吐き方についての記事をみて、
本番ユーザさんにもNSLogだと参照される危険があるな〜と感じたので、
個人的に使っているデバッグ時にだけログを吐き出すログ用のラッパーを紹介します。

環境

  • Xcode 10.2
  • Swift 5.0

実装

プリプロセッサマクロを利用してDEBUGに設定しているスキームのときだけログを吐き出すようなメソッドとなっています。
printの部分はよしなにNSLogに差し替えても構いません。
通常エラーが発生してほしくない場合にログを吐き出す場合はerrorのメソッドを利用すると良いです。
発生した ファイル名、メソッド、行数を自動で吐き出します。

以下、テンプレとしてお使いいただければと思います。

struct Logger {

  /// デバッグ時だけログを吐き出すメソッド
  static func debug(_ item: Any) {
    #if DEBUG
    print(item)
    #endif
  }

  /// エラーパターンに埋め込むメソッド
  static func error(file: String = #file, function: String = #function, line: Int = #line, _ message: String = "") {
    #if DEBUG
    let consolLog = "file: \(file)\n function: \(function)\n line: \(line)\n message: \(message)\n"
    print(consolLog)
    #endif
  }

}

最後に

もっとこうした方がいいよ、間違えている箇所あるよ
等はご指摘いただけると助かります!

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

関数の引数の強参照で引き起こされるMemoryleakを防ぐ

先日メモリリークの調査をしたのですが、かなり見つけづらいパターンだなと思ったので、共有します。

実際に起きていた場所は、下記のようなUIView内でした。

Swiftにおけるメモリリーク、参照カウント、強参照については別記事を参考にしていただけると幸いです。

実際の発生コード

クラス名等は変更しています。

ButtonView.swift
...

//常にViewControllerの下部に浮かせて表示したいボタン
class ButtonView: UIView {
    private let disposeBag = DisposeBag()
    ...

    func putBottom(to viewController: UIViewController) {
        viewController.view.addSubview(self)
        self.translatesAutoresizingMaskIntoConstraints = false
        self.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor).isActive = true
        self.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor).isActive = true
        self.heightAnchor.constraint(equalToConstant: 70).isActive = true

        //今回はRxSwiftを利用した例ですが、破棄されないクロージャであればなんでも良いです。
        viewController.view.rx.methodInvoked(#selector(viewController.view.safeAreaInsetsDidChange))
            .take(1)
            .subscribe(onNext: { [weak self] _ in
                guard let self = self else { return }
                viewController.view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive  = true
            })
            .disposed(by: disposeBag)
    }

    ...

}
MyViewController.swift
class MyViewController: UIViewController {
    private var buttonView = ButtonView.instantiate()

    ...

    override func viewDidLoad() {
        super.viewDidLoad()
        buttonView.putBottom(to: self)
    }

    ...
}

一見ButtonView側でも [weak self] していて安全な書き方に見えますが、
この≈はいつまでも破棄されず、メモリリークします。

原因:引数をクロージャ内でキャプチャしていることによる循環参照

循環参照によるメモリリークの例

よくある循環参照によるメモリリークの例として、

let classA = ClassA()
let classB = ClassB()

classA.child = classB
classB.child = classA

みたいなコードを見ることがあると思いますが、これは、互いに参照をもってしまっているために
ARCの参照カウンターがゼロにならずにメモリが解放されない為に起こります。

今回の例

今回の例はそれがさらに複雑になっています。
MyViewController -> ButtonView の参照は明示的に書かれています。
では、ButtonView -> MyViewController の参照はどうでしょう?

原因箇所はここです

ButtonView.swift
   ...
   //今回はRxSwiftを利用した例ですが、破棄されないクロージャであればなんでも良いです。
   viewController.view.rx.methodInvoked(#selector(viewController.view.safeAreaInsetsDidChange))
            .take(1)
            .subscribe(onNext: { [weak self] _ in
                guard let self = self else { return }
                //クロージャ内で関数の引数であるviewControllerをキャプチャしている
                viewController.view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive  = true
            })
            .disposed(by: disposeBag)
    }
    ...

}

このクロージャはdisposeBagが破棄されたタイミング、つまりButtonViewの破棄されるタイミングで参照がリセットされますが、今回の例ではクロージャ内で引数であるviewControllerがクロージャ内でキャプチャされています。
これにより、暗黙的に ButtonView -> MyViewController の参照が生まれていて、循環参照となっていました。

修正と対策

viewControllerを引数に渡す関数設計をやめる

今回の例では、実はそもそもviewControllerを引数に取る必要がありませんでした笑
なので、下記のような修正でメモリリークは解消されます。

ButtonView.swift
...

//常にViewControllerの下部に浮かせて表示したいボタン
class ButtonView: UIView {
    private let disposeBag = DisposeBag()
    ...

    func putBottom(to view: UIView) {
        view.addSubview(self)
        self.translatesAutoresizingMaskIntoConstraints = false
        self.leadingAnchor.constraint(equalTo:view.leadingAnchor).isActive = true
        self.trailingAnchor.constraint(equalTo:view.trailingAnchor).isActive = true
        self.heightAnchor.constraint(equalToConstant: 70).isActive = true

        //今回はRxSwiftを利用した例ですが、破棄されないクロージャであればなんでも良いです。
        view.rx.methodInvoked(#selector(view.safeAreaInsetsDidChange))
            .take(1)
            .subscribe(onNext: { [weak self] _ in
                guard let self = self else { return }
                view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive  = true
            })
            .disposed(by: disposeBag)
    }

    ...

}

MyViewController.view-> ButtonView への参照はないため、どちらもMyViewControllerのdeallocateのタイミングでメモリが解放されます。

関数内クロージャでの引数の参照を弱参照にする

ですが、UIView利用の仕方によってメモリリークするButtonViewはあまりいい設計とは言えません。
この場合、さらに関数内クロージャでの引数の参照を弱参照にすることでより安全になります。

ButtonView.swift
   ...
   viewController.view.rx.methodInvoked(#selector(viewController.view.safeAreaInsetsDidChange))
            .take(1)
            .subscribe(onNext: { [weak self, weak view] _ in
                guard let self = self, let view = view else { return }
                //クロージャ内で関数の引数であるviewControllerをキャプチャしている
                viewController.view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive  = true
            })
            .disposed(by: disposeBag)
    }
    ...

}

あまり見慣れない書き方ですが、クロージャ内で複数の引数を弱参照でキャプチャしたい場合は [weak self, weak view] とかけます。
この場合、仮に 引数のView -> ButtonViewの参照があった場合でも、メモリリークを起こすことなく利用できます

利用するViewController側での対策

利用するViewController側でも対策することができます。

MyViewController.swift
class MyViewController: UIViewController {
    weak var buttonView: ButtonView?

    ...

    override func viewDidLoad() {
        super.viewDidLoad()
        buttonView.putBottom(to: self)
    }

    ...
}

このように、自身を参照する可能性のあるプロパティに関して、弱参照で定義することができます。
しかし、どこからも参照がなくなってしまったViewに関しては、すぐにdeallocateしてしまうのでこの対策には注意が必要です.

まとめ

今回のようなメモリリークを起こさないために、書き手としては、
- プロパティに自身を渡すような関数には気をつける
- 引数の強参照をふせぐため、保持されるクロージャ内では引数の弱参照を心がける

といった必要を感じました。実際起きてしまうとかなり気づきにくい上、ButtonViewの利用法等、時間が経って発覚する場合があるので、なるべく事前に防ぐ工夫が必要です。

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

iOSアプリの起動方法を判別する

概要

アプリがホーム画面から起動されたのか、スキーム起動されたのかなど起動時の状態をアプリで判別する方法

活用ケース

  • ホーム画面からの起動の場合のみ何か処理を行う
  • アプリの起動方法を厳密に判定して処理を行う

実装

AppDelegatedidFinishLaunchingWithOptionsの引数であるlaunchOptionsにて判別を行います。
ホーム画面からアプリを起動した際にはnilとなっています。

func application(_: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // launchOptionsで起動方法を判別する
}

See: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622921-application
See: https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey

Sample

ホーム画面から起動した場合のlaunchOptions

nil

Safariからスキーム起動した場合のlaunchOptions

[__C.UIApplicationLaunchOptionsKey(_rawValue: UIApplicationLaunchOptionsURLKey): hoge://fuga?a=b
 __C.UIApplicationLaunchOptionsKey(_rawValue: UIApplicationLaunchOptionsSourceApplicationKey): com.apple.mobilesafari]

補足

起動時の振る舞いとなるため、デバッグにはConsoleを用いる必要があります。
下記を参考ください。
https://qiita.com/m_orishi/items/c5593878e1ab55d5c3f2

さいごに

ニーズがあるように感じたのですが、意外とまとまった記事がなく。
もっと良い判定方法があるよ!この方法だとこんな問題があるよ!など知見がありましたら教えて頂ければ嬉しいです。

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

Swift for TensorFlowでVAE

オフィシャルのswift-modelsリポジトリにautoencoderのサンプルが掲載されています。
https://github.com/tensorflow/swift-models/blob/2fa11ba1d28ef09454af9da77e22b585cf3e5b7b/Autoencoder/main.swift

ですがVAEのサンプルはなかったので上コードを改造して実装してみました。

以下S4TF及びVAEの基本的なことは知っているものとして書きます。
またコードの一部はkerasの実装を参考にしました。
https://keras.io/examples/variational_autoencoder/

完成したコードはこちらです。v0.4.0-rc2にて動作確認しています。
https://gist.github.com/t-ae/3cd33e4f0535b98c2df9bfeef49645e5

モデル定義

struct Encoder: Layer {
    typealias Input = Tensor<Float>
    typealias Output = Encoded

    var encoder1 = Dense<Float>(inputSize: imageHeight * imageWidth, outputSize: 128,
            activation: relu)
    var encoder2 = Dense<Float>(inputSize: 128, outputSize: 64, activation: relu)
    var encoder3 = Dense<Float>(inputSize: 64, outputSize: 12, activation: relu)

    var encoderMean = Dense<Float>(inputSize: 12, outputSize: 4, activation: identity)
    var encoderLogVar = Dense<Float>(inputSize: 12, outputSize: 4, activation: identity)

    @differentiable
    func callAsFunction(_ input: Input) -> Output {
        let intermediate = input.sequenced(through: encoder1, encoder2, encoder3)

        let mean = encoderMean(intermediate)
        let logVar = encoderLogVar(intermediate)

        return Encoded(mean: mean, logVar: logVar)
    }
}

struct Encoded: Differentiable {
    var mean: Tensor<Float>
    var logVar: Tensor<Float>
}

struct Decoder: Layer {
    typealias Input = Tensor<Float>
    typealias Output = Tensor<Float>

    var decoder1 = Dense<Float>(inputSize: 4, outputSize: 12, activation: relu)
    var decoder2 = Dense<Float>(inputSize: 12, outputSize: 64, activation: relu)
    var decoder3 = Dense<Float>(inputSize: 64, outputSize: 128, activation: relu)
    var decoder4 = Dense<Float>(inputSize: 128, outputSize: imageHeight * imageWidth,
        activation: tanh)

    @differentiable
    func callAsFunction(_ input: Input) -> Output {
        return input.sequenced(through: decoder1, decoder2, decoder3, decoder4)
    }
}

struct VAE: Layer {
    typealias Input = Tensor<Float>
    typealias Output = VAEResult

    var encoder = Encoder()
    var decoder = Decoder()

    @differentiable
    func callAsFunction(_ input: Input) -> Output {
        let encoded = encoder(input)

        let mean = encoded.mean
        let logVar = encoded.logVar

        let gaussian = Tensor<Float>(randomNormal: mean.shape)

        let std = exp(logVar/2)

        let images = decoder(gaussian * std + mean)

        return VAEResult(image: images, mean: mean, logVar: logVar)
    }
}

struct VAEResult: Differentiable {
    var image: Tensor<Float>
    var mean: Tensor<Float>
    var logVar: Tensor<Float>
}

もともとあったAutoencoderの代わりにVAEを定義しています。
Autoencoderではエンコーダとデコーダが結合した形でしたが、ここでは分割し、それをVAEにて統合しています。これはPythonのフレームワークを使うときにも言えますが、分割しておくとエンコーダ/デコーダ単体で使いたいときに便利です。

VAEはサンプリングと損失の計算にエンコーダーから得られるmean, logVarを使うため、Encoderはこの2つを出力する必要があります。named tupleで出したいところですが@differentiableな関数の入出力はDifferentiableである必要があるため、ここではEncodedを定義しています。VAEResultについても同様です。

画像が[0, 1]範囲なのにDecoderの出力がtanhになっていますが、これは元のコードでもそうなっており、v0.4.0-rc2の時点ではsigmoidの勾配の実装にバグがあって正しく学習できないのでそのままにしているだけです。
https://github.com/tensorflow/swift-models/issues/179

損失関数

@differentiable
func loss(result: VAEResult, original: Tensor<Float>) -> Tensor<Float> {
    let reconstrcutionLoss = (result.image - original).squared().sum(alongAxes: 1)

    let klLoss = (1 + result.logVar - result.mean.squared() - exp(result.logVar))
        .sum(alongAxes: 1) * -0.5

    return (reconstrcutionLoss + klLoss).mean()
}

損失関数の実装はVAEResultと元画像のTensor<Float>を取ります。
var klLoss = (1 + result.logVar - result.mean.squared() - exp(result.logVar)); klLoss = klLoss.sum(alongAxes: 1); klLoss *= -0.5のようにvarに操作を加えていく書き方は@differentiableではできないようなので一文で書いています。毎回新しい変数を定義していけば分割はできますが……

学習部分

let ?model = vae.gradient { vae -> Tensor<Float> in
    let result = vae(x)
    return loss(result: result, original: x)
}
optimizer.update(&vae.allDifferentiableVariables, along: ?model)

損失関数は定義できているのであとは置きかえるだけです。

結果

50エポックやって最後の方の出力はこんなかんじになりました。
左が入力で右が出力です。

VAE出力

Swift for TensorFlowについて

まだ触りはじめたばかりですが、Pythonのフレームワークを使う場合と比べ圧倒的に書きやすいと感じます。
PythonのほうではPyCharmで開発していますが、補完で候補が出ないことがよくあるため、ドキュメントを検索しながらコードを書くということが多いです。一方S4TFではだいたいこんな名前だろ〜というのを打ったらちゃんと候補に出てくれるので楽です(Xcode/Swiftでの開発に慣れているというのも大きいかもしれませんが)。
さらに静的型付けのおかげで引数に何を渡せばいいかも明確なので、実装して走らせて初めて間違いに気付くというようなこともないです。

S4TFの正式リリースがいつになるかはわかりませんが、現状最も期待できる候補の一つなので、今後とも注視していきたいです。

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

Swiftでタイムアウト処理を実装する。

URLSessionを使用しているとき

timeout.swift
let path = "https://XXXXXXX"
let url = URL(string: path)!
var request = URLRequest(url: url)

// タイムアウトの時間を設定する
request.timeoutInterval = 600

let task = URLSession.shared.dataTask(with: request) { data, response, error in

  // レスポンス処理
}
task.resume()

下記から引用しています。
swiftで処理のタイムアウトを実現したい

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

チュートリアルから一歩踏み出したSwiftUIのCustom Viewの作り方ーその2(PreferenceKey編)

はじめに

前回のチュートリアルから一歩踏み出したSwiftUIのCustom Viewの作り方ーその1(GeometryReader編)から引き続きチュートリアルから一歩踏み出したSwiftUIのCustom Viewを作ってみたいと思います。

今回も右記の素晴らしいブログを参考にしています。GeometryReader to the Rescue

今回のCustom View

円をクリックすると円型の枠がオーバーレイのアニメーションをしながらクリックした側に移動します。
ezgif.com-resize-2.gif

大雑把なしくみ

まずはViewの構造をみてみましょう。
Screen Shot 2019-07-23 at 1.45.02 pm.png
緑と赤の円はHStack下にあって、それと円の周りを囲むアニメーションで移動するView(以下、円周View)がZStack下で重なっています。円周Viewトはクリックされた円の場所にオフセットを利用して移動します。
BGViewは円Viewのframe(サイズおよび位置)取得のための透明なViewでそれぞれの円Viewのbackgroundに設定されます

そのためには、二つの円Viewの位置、サイズ情報(frame)を取得し、円周Viewを移動させるために、その位置情報を親Viewからアクセス出来なければなりません。またその位置情報はデバイスが回転した場合には変化するので、自動で変化時には位置情報の書き換えをする必要があります。

前回の記事を読んでいただいたはお分かりだと思いますが、frameの取得にはGeometryReaderを使います。その情報の共有と更新には今回のメイントピックPreferenceKeyを使います。

ちなみに、@Bindingを使って親ViewとGeometryReaderで取得した位置、サイズ情報を共有すればシンプルなのでは?と思われるかもしれませんが、Modifying state during view update, this will cause undefined behavior.というエラーが出て上手くいきません。

Binding.swift
DispatchQueue.main.async {
    self.rects[self.idx] = geometry.frame(in: .named("myCoordinate"))
}

のようにすれば一見うまくいくように見えますが、根本的な解決には至りません。

PreferenceKeyとは

ユーザーが作成したViewのデータ(Preference)を保持、親Viewからアクセスさせることを可能にするプロトコルです。下記を実装する必要があります。
- associatedtype Value
 保持するPreferenceのタイプ
- static var defaultValue: Self.Value
 そのPreferenceのデフォルト値
- static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
 親Viewからアクセスする時に、Preferenceを持つ複数の子ViewのPreferenceをどのようにまとめ上げるか。valueは今までの値、nextValueは次の値。
例えば、Arrayでまとめ上げるとすると、valueは今までのPreferenceを要素として持つ配列、nextValueは次の値なので、このクロージャをvalue.append(contentsOf: nextValue())とすれば全てのこのPreferenceを配列の要素としてまとめ上げ、親Viewからアクセスできることになります。

実装

では、早速今回のユーザーデータを作ってみましょう。Preferenceのデータとして使うにはEauatableである必要があります。中身はどの円Viewをタップしたか知るための番号(idx)とframeを保持するためのCGRect型のrectです。

PreferenceData.swift
struct PreferenceData: Equatable {
    let idx: Int
    var rect: CGRect
}

次に独自のPreferenceKeyを実装します。上記で説明した3つを実装します。今回は複数の子ViewのPreference(上記で作成したPreferenceDataタイプ)を保持したいので、配列としています。

CirclePreferenceKey.swift
struct CirclePreferenceKey: PreferenceKey {
    typealias Value = [PreferenceData]

    static var defaultValue: [PreferenceData] = []

    static func reduce(value: inout [PreferenceData], nextValue: () -> [PreferenceData]) {
        value.append(contentsOf: nextValue())
    }
}

上記で作成したPreferenceに円Viewのframeを設定しています。このView円Viewのbackgroundで円Viewと同じframeを持つ透明なViewです。
GeometryReaderを使って取得したViewのframeと番号(idx)をpreference modifierによって先ほど作ったpreferenceDataとして設定しています。
[PreferenceData(idx: self.idx, rect: geometry.frame(in: .named("myCoordination")))])
frame(in: .named("myCoordination"))は独自の座標系を設定しています。(後述)
これでpreferenceの値の設定が終わりました。

BGView.swift
struct BGView: View {
    let idx: Int
    var body: some View {
        GeometryReader { geometry in
            Circle()
                .fill(Color.clear)
                .preference(key: CirclePreferenceKey.self, value: [PreferenceData(idx: self.idx, rect: geometry.frame(in: .named("myCoordination")))])
        }
    }
}

読出し部分ですがその前にそれぞれの円Viewのbackgroudに上記のBGViewを設定しています。
そして読み出した値を保持するための配列を作成しています。
@State private var rects: [CGRect] = Array<CGRect>(repeating: CGRect(), count: 2)
次に円View達の親ViewであるHStackにてonPreferenceChange modifierを使ってpreferenceのデータを上記で作成した配列に突っ込みます。onPreferenceChangeはpreferencenのデータが変わるたびにコールされるので、例えば端末が回転して Viewの構成が変わった場合、自動的に値の更新が行われます。

CirclePreferenceView.swift
@State private var activeIdx: Int = 0
@State private var rects: [CGRect] = Array<CGRect>(repeating: CGRect(), count: 2)

HStack {
    Circle()
        .fill(Color.green)
        .frame(width: 100, height: 100)
        .background(BGView(idx: 0))
        .tapAction {
            self.activeIdx = 0
        }
        .padding()
        Circle()
            .fill(Color.pink)
            .frame(width: 150, height: 150)
            .background(BGView(idx: 1))
            .tapAction {
                self.activeIdx = 1
            }
            .padding()
    }
    .onPreferenceChange(CirclePreferenceKey.self) {  preference in
        for p in preference {
            self.rects[p.idx] = p.rect
        }
    }

円Viewタップ時に、上記で取得した円Viewのframe情報を元に円周Viewの位置、大きさを変更しアニメーションさせます。

stroke.swift
Circle()
    .stroke(Color.blue, lineWidth: 10)
    .frame(width: rects[activeIdx].size.width, height: rects[activeIdx].size.height)
    .offset(x: rects[activeIdx].minX , y: rects[activeIdx].minY)

全体のコード

詳しくは説明しませんが、座標系を統一するためにZStackに.coordinateSpace(name: "myCoordination")で独自の座標系を設定し、frame取得時に利用しています。

.swift
import SwiftUI

struct PreferenceData: Equatable {
    let idx: Int
    var rect: CGRect
}

struct CirclePreferenceKey: PreferenceKey {
    typealias Value = [PreferenceData]

    static var defaultValue: [PreferenceData] = []

    static func reduce(value: inout [PreferenceData], nextValue: () -> [PreferenceData]) {
        value.append(contentsOf: nextValue())
    }
}

struct CirclePreferenceView: View {

    @State private var activeIdx: Int = 0
    @State private var rects: [CGRect] = Array<CGRect>(repeating: CGRect(), count: 2)
    @State var isStarted:Bool = false

    var body: some View {

        ZStack(alignment: .topLeading) {
            HStack {
                Circle()
                    .fill(Color.green)
                    .frame(width: 100, height: 100)
                    .background(BGView(idx: 0))
                    .tapAction {
                        self.isStarted = true
                        self.activeIdx = 0
                    }
                    .padding()
                    Circle()
                        .fill(Color.pink)
                        .frame(width: 150, height: 150)
                        .background(BGView(idx: 1))
                        .tapAction {
                            self.isStarted = true
                            self.activeIdx = 1
                        }
                        .padding()
                }
                .onPreferenceChange(CirclePreferenceKey.self) {  preference in
                    for p in preference {
                        self.rects[p.idx] = p.rect
                    }
                }

Circle()
    .stroke(Color.blue, lineWidth: 10)
    .frame(width: rects[activeIdx].size.width, height: rects[activeIdx].size.height)
    .offset(x: rects[activeIdx].minX , y: rects[activeIdx].minY)
    .animation(.linear(duration: isStarted ? 0.5 : 0))

        }.coordinateSpace(name: "myCoordination")
    }
}

struct BGView: View {
    let idx: Int
    var body: some View {
        GeometryReader { geometry in
            Circle()
                .fill(Color.clear)
                .preference(key: CirclePreferenceKey.self, value: [PreferenceData(idx: self.idx, rect: geometry.frame(in: .named("myCoordination")))])
        }
    }
}


#if DEBUG
struct CirclePreferenceView_Previews: PreviewProvider {
    static var previews: some View {
        CirclePreferenceView()
    }
}
#endif

最後に

PreferenceKey、ちょっと理解しづらいところもありますが、色々使えそうです。
座標系のせいか何故かLiveViewではエラーが出て表示できません。シミュレーターか実機で確認してください。
View名がContnetViewではないので、SceneDelegateのwindow.rootViewControllerの書き換えを忘れないように。(CirclePreferenceView)

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