SwiftUI NavigationView の Push/Pop をコードで実行する

SwiftUI の NavigationView の Push(進む)と Pop(戻る)をユーザのアクションではなく、コードで(pragmatically)実行する調査をしたサンプル。

サンプルはボタンアクションで Push/Pop するが、この調査結果により、コードで Push/Pop 実行できることが可能と分かった。


Push する方

NavigationLink の isActive を false -> true にして、次の画面へ行く

struct ContentView: View {
    @State private var navigationLinkIsActive = false
    
    var body: some View {
        NavigationView() {
            VStack {
                Button(action: {
                    self.navigationLinkIsActive = true
                }, label: {
                    Text("Push")
                })
                
                NavigationLink(destination: SubView(),
                               isActive: $navigationLinkIsActive,
                               label: {
                    EmptyView()
                })
            }
        }
        .navigationBarBackButtonHidden(true)
    }
}

Pop する方

presentationMode を使用する

import SwiftUI

struct SubView: View {
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        VStack {
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }, label: {
                Text("Pop")
            })
        }
        .navigationBarBackButtonHidden(true)
    }
}

SwiftUI NavigationLink の有効/無効を切り替える

SwiftUI で NavigationLink の有効/無効の切替サンプルを作った。


ざっくりいうと

NavigationLink は hidden にしておいて直接操作させず、代わりに Button のアクションを一枚噛ませて NavigationLink がアクションする、という流れにするとできた。

NavigationLink の $isActive を初期は false にしておいて、ボタンのタッチ後に true すると NavigationLink の画面遷移が動作するが、そのボタンのタッチで true にするかどうかをここでは Toggle で切替して試している。

コードはこんな感じになる。

import SwiftUI

struct ContentView: View {
    
    @State var switchState: Bool = false
    @State var showsAlert = false
    @State var pushActive = false
    
    var body: some View {
        NavigationView {
            VStack {
                
                Toggle(isOn: $switchState) {
                    Text("Enable NavigationLink")
                }.frame(width: 260)
                
                Spacer().frame(height: 20)
                
                Button(action: {
                    guard self.switchState else {
                        self.showsAlert = true
                        return
                    }
                    
                    self.pushActive = true
                }) {
                    Text("Next")
                }
                .alert(isPresented: self.$showsAlert) {
                    Alert(title: Text("Switch is disable."))
                }
                
                NavigationLink(destination: ContentView2(),
                               isActive: self.$pushActive) {
                    EmptyView()
                }.hidden()

                Spacer().frame(height: 32)
            }
            .padding()
            .navigationBarTitle(Text("Index"))
            .navigationBarHidden(false)
            
        }
    }
}

SwiftUI KeyboardObserving を使用して TextField がキーボードに隠れる問題に対応する

入力したい TextField にタッチする→表示されたキーボードにより TextField が隠れる→入力しづらい

という問題。

これは Objective-C 時代からあり、よく対処されてきた問題なのだが SwiftUI の場合は、
GitHub にある KeyboardObserving という OSS で対応できそうだ。

デモ動作はこんなかんじ。

導入のしかた

CocoaPods / Swift Package Manager に対応している。

Xcode を使用して [Project]->[Swift Packages] から登録すると楽だろう。

導入が終わったあとは

import KeyboardObserving

して

struct YourView: View {

  var body: some View {
    VStack {
      // Your Content Here
    }
    .keyboardObserving()
  }
}

こんな感じに .keyboardObserving() を Superview に追加することで完了。

これで動作を試すと、キーボードの上に TextField が来るように自動で調整される。
とても簡単に導入できた、すごい。

ちなみに Keyboard KeyboardObservingView というクラスを使う方法もあるらしく、くわしくはプロジェクトの README を参照すること。

SwiftUI ビュー内に直接配置するビューが多くてビルドエラーになる場合

現象

Xcode 10.15.1 / Swift 5.1.2

ドキュメントのどこに記載があるかは不明だが SwiftUI でビュー内に配置できるサブビューは 10 個までのようだ。

たとえば

struct ContentView: View {
    var body: some View {
        VStack {
            Text("1")
            Text("2")
            Text("3")
            Text("4")
            Text("5")
            Text("6")
            Text("7")
            Text("8")
            Text("9")
            Text("10")
            // Text("11")
        }
    }
}

上記のコードでは Text ビューを 10 個配置していてビルドができる状態である。

しかし、コメントアウトされている Text("11") を有効にして 11 個ビューを配置しようとすると、ビルドエラーになる。

厄介なのが、この時に出力されるビルドエラーは、直接的にビューの限度数について示したものではないから、この制約に気付きにくい。

例えば以下のエラーが出ることを確認した。ここからビュー数の制限に気付くのは不自然である。

'Int' is not convertible to 'CGFloat?'
Argument passed to call that takes no arguments

回避方法

1. ビューごとにまとめる

ビューをグルーピングして、ひとつのビューに直接配置するビューを減らす。

struct ContentView: View {
    var body: some View {
        VStack {
            Group {
                Text("1")
                Text("2")
                Text("3")
                Text("4")
                Text("5")
            }
            
            VStack {
                Text("6")
                Text("7")
                Text("8")
                Text("9")
                Text("10")
            }
            Text("11")
        }
    }
}

2. ForEach を使用する

ForEach を使った記述で、直接配置する方法を避ける。

struct ContentView: View {
    var body: some View {
        VStack {
            ForEach(1..<12) { i in
                Text("\(i)")
            }
        }
    }
}

SwiftUI @State/@ObservableObject/@EnvironmentObject のメモ

それぞれの違いなどをメモする。
間違いあるかもしれない。

@State

State - SwiftUI | Apple Developer Documentation

SwiftUI Framework に含まれる。

概要

@State の値が変更されると、ビューの表示は再計算される。
State からバインディングを取得するには binding プロパティか $ プレイフィックス演算子を使用する。($ を使用するパターンが多く見受けられる)

アクセス範囲

@State プロパティにアクセスするには、そのプロパティを持つビューのみ。(原理的にはそのビューの func を外部のビューが呼び出して、プロパティを変更する、はできる)

まとめ

有効範囲は小さく、ローカル。
@State は特定のビューに属する形のデータに使用する。
例えば、ビュー内のボタンを押して、表示を変更する、など。

@ObservableObject

ObservableObject - Combine | Apple Developer Documentation

Combine Framework に含まれる。

概要

複数のビューでデータを共有する場合は @State の代わりに @ObservableObject を使用する。

@State と似ているが、クラスのインスタンスを作成し、独自のプロパティを作成する必要がある。

監視対象オブジェクトがデータが変更されたことをビューに通知する方法はいくつかあるが、最も簡単なのは @Published プロパティラッパーを使用すること。@ObservableObject の変更はすベて自動で通知される。

アクセス範囲

複数ビューからアクセス可能。

まとめ

@State より有効範囲が広い。

例えば施設データがあり、それを List 表示する View とその先の詳細を表示する View がある場合などは施設データは @ObservableObject を使用すると良い。

@EnvironmentObject

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

SwiftUI Framework に含まれる。

概要

@State が変更時にビュー更新を自動的に発生させる。
@ObservableObject が変更時にビューの更新を発生させる可能性がある。

@EnvironmentObject はすべてのビューが読み取り可能な、アプリ全体で使用できる共有データ。

まとめ

@State, @ObservableObject よりも有効範囲が広い。
アプリ全体で使用するデータに使用すると良い。

全体まとめ

有効範囲の広さ

@EnvironmentObject > @ObservableObject > @State

適切な使い道

  • @State
    • ひとつのビュー内の値変更と表示の変更
  • @ObservableObject
    • 複数のビュー内での共有するデータ(変更通知も可能)
  • @EnvironmentObject
    • アプリ全体で共有する値

参考

映画『ショーシャンクの空に』の感想

1994年 アメリ

原作 スティーヴン・キング
出演 ティム・ロビンズ、モーガン・フリーマン




スティーヴン・キング原作の映画は面白いものが多い。
たとえば以下。

スタンド・バイ・ミー

グリーンマイル

ミスト

ミスト (字幕版)

ミスト (字幕版)

ドリームキャッチャー

痩せゆく男

そして、この「ショーシャンクの空に」はキング原作の映画の中でも特に印象に残る。




この映画に描かれている人間と、アメリカの風景が好きだ。

映画のテーマは、おおざっぱに言うと、「人間の生き方」だと思う。

刑務所という粗暴で汚れた場所で、どう生きるか?

ティム・ロビンスが演じるアンディは、いままでは学歴があり知性ある人々のなかで落ち着いた生活をしていたが、冤罪で投獄され、荒い犯罪者とともに収監生活を送ることを余儀なくされた。

しかし、刑務所のなかでもアンディは「知性」や「文化」など人間的な暮らしを志向するため、高校を出ていない若い囚人に勉強を教えて高卒資格を取らせたり、ミニ図書館を作ったりする。

それにより、今まで粗暴だった刑務所内の空間が徐々に「人間らしい暮らし」ができる空間に変わっていく。

だが、品がよく学歴があり、周囲とちがうアンディは刑務所内で目立ち、騒動にもまきこまれる。悪い囚人たちに暴行、性的レイプされる日々。

また、銀行員の経歴から、刑務所の所長に強要されて、刑務所ぐるみの囚人の労働対価搾取ビジネスを手伝い、所長の隠し財産づくりに任される。

とことん、人間としての尊厳をアンディは奪われる。しかし、彼は静かに諦めずに脱獄作戦を継続し、ついに自由な世界へもどる。

ひどい環境でも人間のプライドを捨てずに、目標を成し遂げるアンディに共感し、感動する映画だ。




もうひとりの囚人の話もしよう。モーガン・フリーマン演じるレッドという囚人がいて、この映画の準主役だ。

三十代程度の若いアンディに対して、レッドは六十代程度を思わせる中年の囚人だ。

アンディが刑務所のなかで、ほかの囚人や看守と衝突するのと対照に、レッドは長いこと刑務所にいて「慣れて」おり、環境に適応し居場所をつくっていた。

実際、レッドが仮出所を望んでいない描写があることから、いわば刑務所の外の人生を「諦めた」人物として描かれている。

しかし、刑務所でアンディを助けたりと、悪意ある人間ではなく、心のどこかで、ひっそりと自由を求めいている。

だから、この映画を観た我々はまず素直に、人間の尊厳と自由を求めてもがくアンディに感情移入するが、現実に屈しいろいろと諦めて、現状を快適にしながらも良心を捨てずに生きるレッドにも感情移入する。

人間としての根底にある自由に対する生き方が、ふたつあり、どちらも理解でき感情移入できる。

だから、この映画は見終わったあと、極端な立場に立つことなく、バランスのよいさわやかな感想をもてるのだろう。

【SwiftUI】TextField で金額を入力する

NumberFomatter を TextField に設定して、以下の動作を確認

  • 米ドル、ユーロ、日本円入力のテスト
  • アルファベットなどの金額ではない入力は、キーボードを閉じたタイミングで消され、訂正される
  • 入力可能な金額の最高値
  • 入力可能な小数点以下の桁数


サンプルコードは以下。