NavigationLink下に配置したButtonのタップが反応しない

SwiftUIでちょっと困ったこと。 Listの中にボタンを配置した時に意図通りにならなくてなんでかなーとなった。

こんな風に、ただボタンを置いただけのときは問題なかった。ボタンをタップすると action は実行される。

struct ContentView: View {
    let items = ["AAA", "BBB", "CCC"]
    
    var body: some View {
        NavigationView {
            List {
                ForEach(items, id: \.self) { item in
                    Button(action: {
                        print("Hello world!")  // 反応する!
                    }) {
                        Text(item)
                    }
                }
            }
            .navigationBarTitle("Test")
        }
    }
}

だけど、各行を NavigationLink で包むとボタンのタップに反応しなくなっちゃった。 (あと何故かボタンの色も黒くなってる。)

struct ContentView: View {
    let items = ["AAA", "BBB", "CCC"]
    
    var body: some View {
        NavigationView {
            List {
                ForEach(items, id: \.self) { item in
                    NavigationLink(destination: Text(item)) {
                        Button(action: {
                            print("Hello world!") // 反応しない!
                        }) {
                            Text(item)
                        }
                    }
                }
            }
            .navigationBarTitle("Test")
        }
    }
}

f:id:gibachan03:20191022163118p:plain

意図としては、ボタンをタップしたら action の中が実行され、ボタンの外側をタップしたら遷移するようにしたかった。

結局、 onTapGesture() でタップを検出するようにして解決(?)した。

Button(action: {
    print("Hello world!") // 反応しない!
}) {
    Text(item)
}
.onTapGesture {
    print("Hello world!") // これなら反応する!
}

環境: Xcode11.1, Swift5

MOBILE CREW NIIGATAに行ってきた

新潟最大級のモバイルアプリカンファレンス!? ということでMOBILE CREW NIIGATAに参加してきた!

f:id:gibachan03:20191013163057p:plain:w400

感想

まず、新潟にモバイルアプリのエンジニアがこんなにいたのかーというのが一番の驚き。 今回スポンサーとなられてた中にも知らない企業さんがあって、新潟でもこんなにモバイルアプリ開発頑張ってるんだなぁというのを認識できたのが嬉しかった。やっぱりWebの人が多くてネイティブは少なめだったかな。

あと新潟わりとラーメンおすすめで、山形の次くらいの盛り上がりらしい。ラーメン食べたい。

セッション

フラーとANDROIDアプリの共創ヒストリー

地方のIT会社がどんなふうに開発をしているかの割と泥臭い話。いい。

長岡花火アプリの話面白かったけど、イベントアプリの怖さを垣間見れた。
イベントアプリはイベント当日にならないと使われない => その時になって発覚する問題 みたいな。
やっぱりそんな場合はネイティブよりWebアプリの方がトラブル対応の点ではずっと有利だよなぁ、最近はAndroidでも審査で時間かかっちゃうみたいだし。

LIVEDATA WITH DATABINDING実用レポート

AndroidGoogle製ライブラリを導入したおはなし。
普段あんまりAndroidさわらないので雰囲気だけ楽しんだ。

Androidのビューを宣言的に書けるようになる Jetpack Compose の存在は知らなかった。 iOSに比べたらXMLでのビュー記述はまだずいぶんマシじゃないかと思ってた。けど、iOSのSwifUIと同じようにAndroidも同じような方向性になってるっぽい。

アプリアーキテクチャ概論

アーキテクチャ選定の基準とその評価をどうするかというトーク。 どこまで行っても答えのない課題だけど、それを考えるためのいくつかの基準が明確になった。

地方IT企業の戦略を広げる技術選択としてのREACT NATIVE

一般的にクロスプラットーフォームでの開発って、モデル部分は共有してもプラットフォーム毎のUIまで同じにしてしまうのはバッドプラクティスのように言われてるけど、ここでは逆のパターン。

ビジネスモデルによってはAndroid/iOSでできるだけUIを合わせた方が有効なケースもあるという目から鱗な興味深いトークだった。

このトークの後で聞けた話で、React Nativeに対してFlutterとかどうなの?という問いに対し、JSと比べてDartは潰しが効きづらく、メンバーのキャリアを考えるとそっちを選択するモチベーションが薄かったという回答でなるほどだった。

考察 : モバイルエンジニアの機械学習との付き合い方

モバイルでMLの学習も推論も手軽にできるようになったのでいっぱい使ってみよう!ということでCoreMLのデモ見せてもらった。手軽すぎる。

手続き型処理と違ってMLの結果は確率的になるなど、必ずユーザー体験も変わるはず。今後はそれを加味してユーザー体験を作っていけるように、モバイルエンジニアもある程度の理解が必要ということらしい。

CoreMLはほんとに簡単そうだったので触ってみないとな。だけどML関係はちょっと試そうと思ってもデータ収集とか前処理とかが結構大変で、入り口がちょっと敷居高いんだよね。Kaggleからデータ引っ張ってくるのもいいらしい。

WEBベースでアプリライクなUI/UXが実現できる「PWA×SPA」という選択肢

ここまでくるとWebアプリもほとんどネイティブと区別つかない、というかそれで十分なケースが多くなるよなという感想。(という話を懇親会でもどなたか話してたな)

INTEGRATE YOUR APP TO MODERN WORLD

SwiftUIを実現するためにSwift5から入った新しい言語機能の解説と、 UIKit -> SwiftUI移行を見越して細かくUIコンポーネントを組み合わせようとういうTipsのおはなし。
ちょっと時間が足りなくて駆け足になってたけど面白かった。

SwiftUIが世に出る4年前からApple内部でプロジェクト開始してたらしい。 個人的にはXibやstoryboardよりコードでUI書くのが好きなので、早くSwiftUIが実用できるのを楽しみにしてる。あとXcodePreviewもちょっとまともに動いて欲しい・・・

長岡花火を支える技術

データ収集(事前に会場の視察とか、当日の駐車場状況の把握とか)の泥臭いはなしとか、華やかな花火大会の裏ですっごく大変な労力かけてサービスを提供してたんだなと。

長岡花火の観客席チケット取るの大変なんだけどここで耳寄り情報が!
ふるさと納税の御礼品として獲得する方法、あとは毎年花火を見れる会社があるとのこと、盲点!

締め

どのセッションも濃ゆい内容だったし、 懇親会で他の新潟のエンジニアの方とお話しできて楽しかった。 地方だとこんな催しなかなか無いので、ぜひ来年以降も続いてくれるといいな。

SwiftUIでモータルビューを閉じた時に何か処理を実行したい

SwiftUIでは、モーダルビューは sheet メソッドを使って表示します。

struct ContentView: View {
    @State private var presentModal = false
    
    var body: some View {
        VStack {
            Button(action: {
                self.presentModal = true
            }) {
                Text("Show modal")
            }
        }
        .sheet(isPresented: self.$presentModal) {
            Text("Modal View")  // このビューをモーダル表示
        }
    }
}

f:id:gibachan03:20190930201131g:plain:w200

で、遷移先のモーダルビュー側でユーザーが何かしらのアクション(フォームの入力とか)をしたらビューを閉じる、とかはよくありますよね。そして閉じたタイミングで、遷移元のビューで何かしらの処理を実行したい(画面を更新するなど)というのが今回の趣旨です。

要件としては以下の通り

  • モーダルビューが閉じられたタイミングで処理を実行する
  • その処理は遷移元のビュー側に記述したい(モーダルビューの中には書かない)
    • UIViewControllerviewWillAppear(_:) でやるようなイメージ。(iOS13からはfullscreenでないと呼ばれないけど・・)
  • SwiftUIではビューの閉じ方がいくつかあるので、そのどのケースでも同じ手法にしたい

モーダルビューを閉じる方法

モーダルビューを閉じるにはざっくり2通りの方法があって、

  • ビューを上から下へ引っ張るジェスチャーで閉じる
    • 標準で実装されてる
  • プログラムから閉じる
    • コードを自分で書く

このどちらのケースで閉じた場合でも、同じ方法で処理を実行したいです。

また、さらに プログラムから閉じる 方法にも2通りあるのでこれらについても後述します。(が、今回の趣旨においてはどちらも差はないです)

ダメだった方法

sheet メソッドにはシートが閉じられた場合に呼ばれるクロージャを引数に指定できます。 ( onDismiss

public func sheet<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View where Content : View

この onDismissジェスチャーでビューを閉じた場合には呼ばれます。 しかしプログラムから閉じた場合には呼ばれないため、今回の要件には沿わないです。

良さそうな案

結局、上記のどの方法で閉じられた場合にもモーダルビュー側の onDisappear が呼ばれるので、それを起点にして目的の処理を実行します。以下はデリゲートパターンでの実装例です。

  • まず、モーダルビューが閉じられた場合に呼ばれるメソッドを持つprotocolを定義
protocol DismissHandlerDelegate {
    func handleDismiss()
}
  • 遷移元ビュー側で目的の処理を実装する
extension ContentView: DismissHandlerDelegate {  // ContentViewが遷移元ビュー
    func handleDismiss() {
        print("dimmissed!!!")  // 何かしらの処理を実行する
    }
}
  • モーダルビューが閉じる(= onDisappear )タイミングでそのデリゲートメソッドを呼ぶ
struct ModalWithDismissHandler: View {
    ...
    var delegate: DismissHandlerDelegate?
    ...    
    var body: some View {
        NavigationView {
            ...
        }
        .onDisappear { self.delegate?.handleDismiss() }
    }
}

これでジェスチャー ・プログラムのどちらで閉じても同じ処理を実行できるようになりました。

【余談】プログラムからモーダルビューを閉じる方法

遷移元のモーダル表示フラグを、遷移先のモーダルビュー側で折る

モーダルビューの定義は次の通り。 Binding<Bool> として遷移元のモーダル表示フラグを受け取れるようにしています。

struct ModalViewWithPresentedState: View {
    @Binding private var presented: Bool
    
    init(isPresented: Binding<Bool>) {
        self._presented = isPresented
    }
    
    var body: some View {
        NavigationView {
            Button(action: {
                self.presented = false   // ここでモーダル表示を折ることで、モーダルを閉じている
            }) {
                Text("Close")
            }
            .navigationBarTitle("Modal")
        }
    }
}

遷移元のモーダル表示処理はこんな感じ

struct ContentView: View {
    @State private var presentModal = false
    
    var body: some View {
        ....
        .sheet(isPresented: self.$presentModal) {
            ModalViewWithPresentedState(isPresented: self.$presentModal)
        }
    } 
}

@Environment(\.presentationMode) を使う

モーダルビューの定義は次の通り。

struct ModalViewWithEnvironment: View {
    @Environment(\.presentationMode) var presentationMode  // これ!
    
    var body: some View {
        NavigationView {
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()    // ここでモーダルを閉じている
            }) {
                Text("Close")
            }
            .navigationBarTitle("Modal")
        }
    }
}

遷移元のモーダル表示処理はこんな感じ。 モーダルビューへ変数を渡す必要がないのでシンプル。

struct ContentView: View {
    @State private var presentModal = false
    
    var body: some View {
        ....
        .sheet(isPresented: self.$presentModal) {
            ModalViewWithEnvironment()
        }
    } 
}

以上です。 すべてのコードは https://gist.github.com/gibachan/305903c9ee0042252d83661ac41007bf に置いてあります。

iOSDC Japan 2019 2日目に参加してきた

f:id:gibachan03:20190908204954j:plain:w300

前回に引き続き、2日目にも参加してきました。

まずは特に興味深かったトークについての感想をつらつらと。

Heart of Swift

@koherさんの、Swiftの言語仕様よりの解説トーク。Swiftがどういった思想のもと構築されているかみたいな。 最初に言っておくと、このトークが個人的に1番のヒットでした。

Swiftのる重要な思想として以下の2つがあって、それらがどのように実現されているか、コードに落としていくかというお話でした。

  • Value Semantics
  • Protocol Oriented Programming

これらはSwiftを書いていればあちこちで遭遇する考え方だけど、今までふんわりと理解した気になってやり過ごしていました。 トークではこれらを詳細に、よく整理して解説されててほんとにスッキリしました。これからコードを書く上でもほんと参考になります。 またSwift5.1で導入される some や、今後入るかもしれない any などについても詳細に解説してもらったおかげで、Xcode11時代にもすんなり対応できる気がしてます。

動画が公開されたら、間違いなくまた見直したいトークでした。凄く良かった!

すべての人のためのアクセシビリティ対応

akatsuki174さんのアクセシビリティについてのトーク

speakerdeck.com

たぶんこの話で最も重要なのは、アクセシビリティは高齢者や障害者の方だけためではなく、すべての人が対象であるということ。 どんな人でも、どんな環境において快適にアプリを使ってもらうために様々な考慮が必要だということでした。

アクセシビリティ向上のためにiOSが提供する各種機能や、Accessibility InspectorのようなXcode付属のチェックツール、Voice Over、Voice Control、Siri shortcutなど、自分にとって今まであまり意識していなかった技術が盛りだくさんでした。

アクセシビリティの重要性について考えされられたトークでした。

以上、他にも別のトークと時間帯が被っていたり満員だったりで見れなかったトークもありました。動画が公開されるのが待ち遠しい!!!

あとセッションの他にもいろんな方とお話しさせてもらいました。参考になる話もいろいろ聞けて満足! WantedlyさんのブースではSwiftUIのハンドブックを頂けたのも嬉しかったです。(クイズには4問中1問外れて悔しかったw)

f:id:gibachan03:20190908204919j:plain:w200

初めて参加したiOSDCだったけど、めっちゃ楽しかったです。普段は触れることのない様々な技術の話を聞いて刺激にもなった、素晴らしいカンファレンスでした。次回も参加できたらいいなー

iOSDC Japan 2019 前夜祭&1日目に参加してきた

2019/9/5からのiOSDCに参加してきました。

iosdc.jp

全体的に普段の仕事ではあまり触れることのない技術についての話を聞けてとっても面白かった! 気楽にトークを聞ける雰囲気も素敵。

以下、見たトークの感想かんたんにまとめまっす。

前夜祭

初めて参加したiOSDC。17:00開場で最初のトークからアルコール解禁で気楽な感じで始まりました😁

スクリーン配信機能の実装が大変だったので知見をお伝えします

@FromAtomさんのiOSからの動画配信についてのトーク

speakerdeck.com

ReplayKitの存在すら知らなかったので興味深かった。iOSのバージョンアップとともに着実に機能も向上してるんですね。

実機でしかデバッグできないとこのと。プッシュ通知とかもそうだけど、結構めんどうなのでシミュレータでできるようになるといいんだけどなー。App Extensionのデバッグでbreak pointに止まらないとか気づかないと割とハマるのあるある。

GoogleSpeechToTextを活用して音声を動画にした話

@fairy_engineerさんの音声をテキスト変換して動画に合成する話。

GoogleSpeechToTextで音声からテキストに変換し、その後ユーザーが編集する。

単語の連結直したり、絵文字入れたり。動画の長さを20秒に限定したりUIを工夫したりと、一見ユーザーにとってめんどくさい作業を、いかに楽しくやってもらうか気を使っている。そういうの大変だけど、そうだよねって思った。

まだベータだけど英語なら複数の話者がいても判別できるらしいのすごいな。。

1日目

お昼のビビンバ弁当が美味しかった。種類もカニ・とり・ビビンバの中から選べたの何気に嬉しい。

縦書きエディタを6プラットフォームで開発してみて

(@496_さんの縦書きエディタのトーク。10年以上開発を続けてるらしい、すごい!

speakerdeck.com

デスクトップはwxWidgetsクロスプラットフォーム対応してるけど、モバイルはそれぞれネイティブ実装。だけど共通のテキスト処理エンジンはC++で書いてるらしい。

すべてのプラットフォームでの見た目の統一や印刷と同じ見た目での編集のために自前でテキスト表示を書いてる。

UILabelやUITextViewが見えないところで頑張ってくれてる、テキスト処理のいろんな奥深い話を聞けて面白かった。

Swiftクリーンコードアドベンチャー ~日々の苦悩を乗り越え、確かな選択をするために~

@stzn3さんのトークで、Swiftのジェネリクスプロトコルを上手に使ってクリーンなコードにしようというお話。

speakerdeck.com

全くそうだよねー、そんな感じでやっていきたいよねーと思って聞いてた。みんな同じように悩みながらコード書いてるんだなと。 Qiitaにも結構たくさん補足の記事を書いててくれているので後でチェックしないと。

Swift Playgrounds でタートルグラフィックスしよう!🐢

@temokiさんのカメでお絵描きするツールのトーク

github.com

LOGOとかちょっと触ったことあったので懐かしかった! 実際の教育現場でも活用されてるとのこと。

Swift Playgroundで動作するPlayground Bookなるものの存在も初めて知りました。 結構いろんなことできそう。

LT

LTも興味深い話が多かった。日本のサマータイムがString->Date変換できないとかLLDBではvコマンドが便利だとか。 いろいろ気を使っても上がらなかったプッシュ通知の許諾率が、アプリ起動時に許可ダイアログを出すだけで大きく向上した話とか😂

以上!明日も楽しみだー!

SwiftUI & Combine の調べたことメモ

iOS13の正式リリースが間近ですね。 そろそろ話題のSwiftUIやCombine frameworkにキャッチアップしないと、と思い徐々に触り始めています。 その際に「これどう書くんだろう?」と気になったことを雑にメモしておきます。

また、以下のコードはXcode11 beta6で動作を確認しています。

相対的なサイズを指定してレイアウトする

Viewのサイズを指定する場合には以下の frame でサイズを指定します。

Text("Hello World").frame(width: 100, height: 200)

しかし、このように特定のサイズではなく相対的なサイズを指定したい時があります。 その際には GeometryReader が使えます。

struct ContentView: View {
    var body: some View {
        GeometryReader { geometry in
            VStack {
                // Textの高さを 2:3 で振り分ける
                Text("Hello")
                    .frame(width: geometry.size.width,
                           height: geometry.size.height / 5 * 2)
                    .background(Color.blue)
                Text("World")
                    .frame(width: geometry.size.width,
                           height: geometry.size.height / 5 * 3)
                    .background(Color.green)
            }
        }
    }
}

Listでページネーション

複数ページのデータを取得する場合、UIKitのUITableViewであれば、スクロール量や終端付近のセルが表示されるタイミングで次ページのデータを取得すると思います。 SwiftUIのListでは、Listの各要素のonAppearでその処理をさせるのが手っ取り早そうでした。

struct ContentView: View {
    @ObservedObject private var viewModel = ViewModel()
    
    var body: some View {
        List {
            ForEach(self.viewModel.values, id: \.self) { value in
                HStack {
                    Text("Value:")
                    Text("\(value)")
                }
                .onAppear {
                    // リストの要素が表示されるときに、それが最後の要素であれば次のページを取得する
                    if self.viewModel.values.last == value {
                        print("Fetch next")
                        self.viewModel.fetchNextPage()
                    }
                }
            }
            
        }
        .onAppear {
            // Listが表示されるときに最初のページを取得する
            self.viewModel.fetchFirstPage()
        }
    }
}

Listの要素を編集

要素を削除

onDeleteを設定するだけで、要素をスワイプして削除できるようになります。

struct ContentView: View {
    @State private var fruits = [
        "apple",
        "banana",
        "strawberry",
        "grape",
        "lemon",
    ]
    
    var body: some View {
        List {
            ForEach(fruits, id: \.self) { fruit in
                Text(fruit)
            }
            // onDeleteがあると、セルをスワイプで削除できるようになる
            .onDelete { (indexSet) in
                // セル削除の処理を書く
                self.fruits.remove(atOffsets: indexSet)
            }
        }
    }
}

要素を移動

削除と同様に、移動をさせるためにはonMoveを設定します。 編集モードに切り替える必要があるので、ナビゲーションバーにEditButtonを追加しています。

struct ContentView: View {
    @State private var fruits = [
        "apple",
        "banana",
        "strawberry",
        "grape",
        "lemon",
    ]
    
    var body: some View {
        NavigationView {
            List {
                ForEach(fruits, id: \.self) { fruit in
                    Text(fruit)
                }
                .onMove { (fromOffsets, toOffset) in
                    // セル移動の処理を書く
                    self.fruits.move(fromOffsets: fromOffsets, toOffset: toOffset)
                }
            }
            // NavigationBarに編集ボタンを追加。タップでセルを移動できるようになる。
            .navigationBarItems(trailing: EditButton())
        }
    }
}

Publisherのデバッグにprint(_:to:)が便利

Publisherがどんな値を流しているかを確認するのにprintを1行差し挟むだけなのでお手軽です。

NotificationCenter.default
            .publisher(for: Notification.Name("Hello"))
            .print("テスト")   // これを入れるだけ
            .filter { $0.object != nil }
            .map { $0.object as! String }
            .assign(to: \.value, on: self)
            .store(in: &cancellables)

// テスト: receive subscription: (NotificationCenter Observer)
// テスト: request unlimited
// テスト: receive value: (name = Hello, object = Optional(world), userInfo = nil)

既存の非同期処理をPublisherに変換

非同期にAPIを叩くような処理では、結果をコールバックで受け取ることは多いかと思います。

func someAPI(completion: @escaping (String) -> Void) {
    // ... なんかいろいろやる
    completion("Hello world")  // 終わったらコールバックで結果を返す
}

これをPublisherにしたいときはFutureでラップします。

func someAPIPublisher() -> AnyPublisher<String, Never> {
        return Future<String, Never> { (promise) in
            someAPI { (value) in
                promise(.success(value))
            }
        }
        .eraseToAnyPublisher()
    }

で、これを使うとこんな感じに書けます。

someAPIPublisher()
            .assign(to: \.value, on: self)
            .store(in: &cancellables)

Optimizing Collectionsを読んだ

Swift情報満載のobjc.ioで販売されている電子書籍のこちらを読みました。

www.objc.io

内容

Swiftで次のデータ構造を順に実装しながら、各種操作のパフォーマンスとその最適化方法について解説されています。

  • ソート済み配列 (Sorted array)
  • 順序集合 (Ordered set)
  • 赤黒木 (Red black tree)
  • B木 (B tree)

図やグラフもあって理解を助けてくれました。

特徴

Attabenchというツールを使って実際に計算時間を測定しながらパフォーマンスの遷移を確認しているので、その改善過程がわかりやすいです。アルゴリズム以外にもCPUのキャッシュなどによる影響についても触れられています。

また、各データ構造を実装するに当たりCopy On Writeも満たすように実装しているため、その部分でも勉強になりました。

その他学んだこと

CustomStringConvertibleプロトコル

CustomStringConvertibleプロトコルのdescriptionプロパティを実装しておくと、printなどでわかりやすく表示させることができます。

extension SortedSet {
    public var description: String {
        let contents = self.lazy.map { "\($0)" }.joined(separator: ",")
        return "[\(contents)]"
    }
}

同様にCustomPlaygroundQuickLookableプロトコルの実装により、Playgroundでの表示をわかりやすく変更できます。

Collectionプロトコル

Swiftで独自オブジェクトをコレクションとして扱うには、Collectionプロトコルに準拠する必要があります。 具体的には以下の実装が最低限必要です。

これだけで、for-inでの列挙やmap、filterなどの便利な処理が使えるようになります。

// 単純なコレクション
struct MinimumCollection {
    var values: [Int] = []
    
    init() {}
    init(values: [Int]) { self.values = values }
}

// Collectionプロトコル準拠
extension MinimumCollection: Collection {
    var startIndex: Int {
        return 0
    }
    
    var endIndex: Int {
        return values.count
    }
    
    subscript(i: Int) -> Int {
        return values[i]
    }
    
    func index(after i: Int) -> Int {
        return values.index(after: i)
    }
}

// 使用例
var c = MinimumCollection(values: [1, 2, 3, 4, 5, 6, 7])

print("First value is \(c.first!)")
print("Number of collection is \(c.count)")

for v in c {
    print("value: \(v)")
}

c.filter { $0 % 2 == 0 }
    .map { print("\($0) is even number.") }

isKnownUniquelyReferenced

書籍ではCopy On Writeを実装するために利用されていました。

この関数では、ある変数が参照しているオブジェクトが複数の参照を持つかを確認できます。関数がtrueを返す場合、その変数のみが参照していることを示します。その為、そのオブジェクトを直接変更しても他への影響がないことを確認できます。 もし他の変数からも参照されている場合(falseが返る場合)、オブジェクトを直接変更してしまうと、他の変数からも参照しているオブジェクトが変更されてしまうことになります。そのため、変更を行う際には予めそのオブジェクトをコピーしてから変更することで、Copy On Writeを満たすことになります。