20200314のSwiftに関する記事は3件です。

Vue.js、Capacitorでアプリ開発

ウェブ開発ノウハウを利用してスマートフォンアプリの開発を行うハイブリッドアプリについて、ソリューションの一つとしてCapacitor+Vue.jsを考えてみました。
メリットは、最初のプロジェクトの作成が非常に簡単にできることですので、プロジェクトの作成手順を紹介します。

Vue.jsプロジェクトの作成

https://qiita.com/jjjjjj/items/a43922631f1350bb9c65
を参照

Capacitorのインストール

以下を参考にしました。
https://capacitor.ionicframework.jp/docs/getting-started

$ npm install --save @capacitor/core @capacitor/cli

Capacitor初期設定

$ npx cap init

index.htmlファイルの場所を変更

capacitor.config.jsonのwebDirをwwwからdistに変更する。

プラットフォームのインストール

$ npx cap add ios
$ npx cap add android

Vueプロジェクトをネイティブにコピーする

$ npx cap copy ios
$ npx cap copy android

IDEを開いてビルド、実行する

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

新しいプログラムファイルの作り方

file > new > file で新しいファイル選択画面を開き、
スクリーンショット 2020-03-14 15.48.15.png

cocoa touch class を選択。
スクリーンショット 2020-03-14 15.48.38.png

クラス名が既存のものとダブらないように書き換え、
スクリーンショット 2020-03-14 15.49.13.png

main story board にて関連づけたい画面を選択した状態でcustom classに先ほど書き換えたクラス名を入力。
スクリーンショット 2020-03-14 15.50.58.png

完了!

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

【SwiftUI】CustomViewを用いて不要なViewの再描画を避ける

SwiftUIは
宣言的に画面のコードを書くことができるため
UIKitと比べても可読性が高く扱いやすくなっていると感じています。

一方で
現時点では足りない機能(CollectionView的なものがないなど)や
動作がいまいちだなと感じる部分もあるかと思います。

その中の一つに
@Stateなどを用いた際に
値を変更する度に画面全体を再描画してしまうことで
変化のないViewも再描画してしまうという問題があります。

今回は
Viewが何を基準に行われ
必要ないViewの再描画をどう回避できるのかについて
下記の記事を参考に確認してみました。

https://medium.com/flawless-app-stories/swiftui-why-to-write-custom-views-for-better-performance-561962f1c268

サンプルコード

下記のコードを使用します。

struct SampleView: View {

    var texts = ["SwiftUI", "A new declarative UI framework", "Let us try to understand how SwiftUI renders its views"]

    @State var selected = 0
    @State var textFieldValue = ""
    @State var isSwitchOn = false

    var body: some View {
        VStack(spacing: 16) {
            TextField("Enter your name", text: $textFieldValue)
                .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
                .background(Color(UIColor.secondarySystemFill))
                .cornerRadius(4)

            Toggle(isOn: $isSwitchOn, label: {
                Text("A simple Switch")
            })

            Picker(selection: $selected, label: Text(""), content: {
                Text("Word").tag(0)
                Text("Phrase").tag(1)
                Text("Sentence").tag(2)
            }).pickerStyle(SegmentedPickerStyle())

            HStack {
                Text(texts[selected])
                Spacer()
                Button(action: {
                    print("just checking")
                }, label: {
                    Image(systemName: "plus.circle.fill")
                        .foregroundColor(.blue)
                })
            }
        }.padding()}
}

Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-14 at 02.29.46.png

検証内容

  1. 全てのViewにbreakpointを設定します。
  2. アプリを起動して、それぞれのViewを操作してみます。

ここからわかることは

TextFieldToggleButtonに操作に加えても
breakpointに止まりませんが
Pickerの値を変更すると
全てのbreakpointに止まります。
つまりbody全体が再描画されていることがわかります。

スクリーンショット 2020-03-14 2.32.39.png

原因

参考にしている記事によると
SwiftUIは親Viewの変数が(今回はselected)
子Viewに影響を与えるかどうか自体は把握しているものの
どのViewに影響を与えているかまでは把握していないため
body全体を再描画しているということだそうです。
(この記事を書いているのは2020/03/14です。)

実際に
Text(texts[selected])の部分を
Text("")に変更すると
body全体の再描画は発生しません。
(SwiftUIはPickerの変更がどこにも影響しないことがわかっているからです。)

解決策

CustomViewを定義することで
再描画されることを回避することができます。

例えば一番最初のTextFieldの部分を
CustomViewに切り出します。

struct SampleView: View {

    ...

    var body: some View {
        VStack(spacing: 16) {
            MyTextField(placholder: "Enter your name", value: $textFieldValue)
            ...
        }.padding()}
}

struct MyTextField: View {
    var placholder: String
    @Binding var value: String

    var body: some View {
        TextField(placholder, text: $value)
            .padding(EdgeInsets(top: 8, leading: 16,
                                bottom: 8, trailing: 16))
            .background(Color(UIColor.secondarySystemFill))
            .cornerRadius(4)
    }
}

これで上記の検証内容を実行してみます。

するとPickerの値を変更した際に
MyTextFieldのbreakpointには止まりません。

スクリーンショット 2020-03-14 2.56.52.png

なぜ?

SwiftUIはEquatableに適合しているViewを見て
bodyに変更があるかどうか見ており
Pickerに変更があっても
MyTextFieldbodyには変更がないため
再描画が発生しなかったということだそうです。

TextField自体はEquatableに適合していませんが

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct TextField<Label> : View where Label : View {

    /// Declares the content and behavior of this view.
    public var body: some View { get }

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    public typealias Body = some View
}

今回はTextFieldLabelText
TextEquatableに適合しているため
その差分を比較しているのではないかと思っています。

/// A view that displays one or more lines of read-only text.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct Text : Equatable {

    /// Creates an instance that displays `content` verbatim.
    @inlinable public init(verbatim content: String)

    /// Creates an instance that displays `content` verbatim.
    public init<S>(_ content: S) where S : StringProtocol

    /// Creates text that displays localized content identified by a key.
    ///
    /// - Parameters:
    ///     - key: The key for a string in the table identified by `tableName`.
    ///     - tableName: The name of the string table to search. If `nil`, uses
    ///       the table in `Localizable.strings`.
    ///     - bundle: The bundle containing the strings file. If `nil`, uses the
    ///       main `Bundle`.
    ///     - comment: Contextual information about this key-value pair.
    public init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)

    /// Returns a Boolean value indicating whether two values are equal.
    ///
    /// Equality is the inverse of inequality. For any values `a` and `b`,
    /// `a == b` implies that `a != b` is `false`.
    ///
    /// - Parameters:
    ///   - lhs: A value to compare.
    ///   - rhs: Another value to compare.
    public static func == (a: Text, b: Text) -> Bool
}

ここら辺いまいち不明瞭なので
もしご存知の方がいらっしゃいましたら
教えていただけるとうれしいです??‍♂️

変更の影響がある部分を切り出す

上記のMyTextFieldのようにToggleの部分を切り出すことで
TogglePickerの変更で再描画しなくなります。

しかし
これだとViewが追加されるたびに
CustomViewを追加していくことになります。
(可読性などを考えるとそれは良い選択かもしれませんが)

そうである場合にも
あるViewの変更が
関係のないViewにまで影響を及ぼすのは
あまり良いとは思われません。

そこでお互いに影響を与える部分(PickerHStack)を切り出してみます。

struct SampleView: View {
    ...
    var body: some View {
        VStack(spacing: 16) {

            // 確認のため最初の形に戻しています
            TextField("Enter your name", text: $textFieldValue)
                .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
                .background(Color(UIColor.secondarySystemFill))
                .cornerRadius(4)
            ...
            MyPickerTextField()
        }.padding()}
}

struct MyPickerTextField: View {
    var texts = ["SwiftUI", "A new declarative UI framework", "Let us try to understand how SwiftUI renders its views"]

    @State var selected = 0

    var body: some View {
        VStack(spacing: 16) {
            Picker(selection: $selected, label: Text(""), content: {
                Text("Word").tag(0)
                Text("Phrase").tag(1)
                Text("Sentence").tag(2)
            }).pickerStyle(SegmentedPickerStyle())

            HStack {
                Text(texts[selected])
                Spacer()
                Button(action: {
                    print("just checking")
                }, label: {
                    Image(systemName: "plus.circle.fill")
                        .foregroundColor(.blue)
                })
            }
        }
    }
}

ここで再び検証内容を実行してみます。

するとbreakpointはMyPickerTextFieldの中でしか止まらなくなり
親Viewのbody全体の再描画は発生しなくなったことがわかりました。

スクリーンショット 2020-03-14 3.42.24.png

結果として変更の影響範囲を必要な部分にだけに抑えることができました。

まとめ

SwiftUIの再描画について見てみました。

Viewが複雑になってくると
body全体の再描画を繰り返し行うことで
パフォーマンスに大きな影響を与える可能性も出てきます。

Viewの表示速度などで
気になる部分がある場合
こういう点を調査してみるのも何かの解決につながるかもしれませんね?

何か間違いなどございましたら
ご指摘いただけますとうれしいです??‍♂️

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