リバース・エンジニアリング

Flultterとテックブログと時々iOS

4. SwiftUI ListとNavigationViewでプッシュ遷移を実装する

SwiftUI での List と NavigationLink について調べてみました。
List は UIKit では UITableView に該当するコンポーネントです。
NavigationLink は NavigationView と一緒に使うコンポーネントで UIKit でいうことの push 遷移を実装できます。

今回は List を使ってリスト一覧を表示させてセルをタップしたら画面遷移して詳細画面にいくものを作ってみました。

先に作って見たもののスクリーンショットを貼っておきます。

トップ画面(ContentView) 詳細画面(DetailView)
f:id:qed805:20200725190526p:plain f:id:qed805:20200725190547p:plain

ソースコードはこちらです。
一覧を表示するデータのファイルを先に作っておきます。都道府県を配列に持たせました。

PrefectureData.swift

import Foundation

struct Prefecture: Identifiable {
    var id: Int
    let name: String
}

let prefectures: [Prefecture] = [
    Prefecture(id: 0, name: "北海道"),
    Prefecture(id: 1, name: "岩手県"),
    Prefecture(id: 2, name: "宮城県"),
    Prefecture(id: 3, name: "秋田県"),
    Prefecture(id: 4, name: "山形県"),
    Prefecture(id: 5, name: "福島県"),
    Prefecture(id: 6, name: "茨城県"),
    Prefecture(id: 7, name: "栃木県"),
    Prefecture(id: 8, name: "群馬県"),
    Prefecture(id: 9, name: "埼玉県"),
    Prefecture(id: 4, name: "千葉県"),
    Prefecture(id: 5, name: "東京都"),
    Prefecture(id: 6, name: "神奈川県"),
    Prefecture(id: 7, name: "新潟県"),
    Prefecture(id: 8, name: "富山県"),
    Prefecture(id: 9, name: "石川県"),
    Prefecture(id: 10, name: "福井県"),
    Prefecture(id: 11, name: "山梨県"),
    Prefecture(id: 12, name: "長野県"),
    Prefecture(id: 13, name: "岐阜県"),
    Prefecture(id: 14, name: "静岡県"),
    Prefecture(id: 15, name: "愛知県"),
]

これがテストデータになります。次に ContentView.swift に View を作ります。

ContentView.swift

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        NavigationView{
            List(prefectures) { prefecture in
                NavigationLink(destination: DetailView(prefecture: prefecture)) {
                    Text(prefecture.name)
                }
            }
            .navigationTitle("都道府県")
        }
    }
}

struct DetailView: View {
    
    var prefecture: Prefecture
    
    var body: some View {
        Text(prefecture.name)
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

画面遷移先の Viewとして DetailView を定義しました。
ContentView を見てみます。

NavigationView{
            List(prefectures) { prefecture in
                NavigationLink(destination: DetailView(prefecture: prefecture)) {
                    Text(prefecture.name)
                }
            }
            .navigationTitle("都道府県")
        }

このように

NavigationView > List > NavigatonLink (画面遷移先クラス) > Text

という階層になっているのが分かります。
NavigationView が UIKit の UINavigationView に該当する構造体かなと思ってます。
NavigationLink に destination のプロパティが存在します。destination は遷移先のクラスを代入すればOKです。
遷移先のクラスが DetailView で

var prefecture: Prefecture

のプロパティを定義しているので都道府県のインスタンスを渡してやりました。

次に UINavigationView の定義先をちらっと見てみます。

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 7.0, *)
public struct NavigationView<Content> : View where Content : View {

    public init(@ViewBuilder content: () -> Content)

    /// 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 = Never
}

このようにstruct で定義されていますね。
SwiftUI のコンポーネントが struct で出来ているのが分かります。

こんな感じで UIKit では UITableView (将来depricatedになるクラス)からセルをタップして画面遷移させてましたが、 SwiftUI では List を多様する将来が見えますね。

さらにiOS 14から Grid という UIKit でいうところの UICollectionView のコンポーネントも登場したので より一層複雑なUIをシンプルに組めるようになりました。

こんな感じで今日は終わろうと思います。

それでは、バイバイ。

3. SwiftUI VStackやHStackでのview配置の変更について調べてみた

今回はVStackに配置されているViewの位置の変更について観ていきます。

Flutterではそもそもプロパティとして用意されていた記憶があります。

tamappe.hatenadiary.com

こちらです。

VStack

VStack はViewを縦に並べるレイアウトコンポーネントです。 基本は中央寄せになっています。

struct ContentView: View {
    
    var body: some View {
        VStack {
            Text("あいうえお")
                .background(Color.yellow)
            Text("かきくけこ")
                .background(Color.red)
            Text("さしすせそ")
                .background(Color.green)
            Text("たちつてと")
                .background(Color.blue)
        }
    }
}

f:id:qed805:20200725135118p:plain:w300

VStackでの上下の配置を制御する場合はSpacer()を使えばいいそうです。

struct ContentView: View {
    
    var body: some View {
        VStack {
            Text("あいうえお")
                .background(Color.yellow)
            Text("かきくけこ")
                .background(Color.red)
            Text("さしすせそ")
                .background(Color.green)
            Text("たちつてと")
                .background(Color.blue)
            Spacer()
        }
    }
}
下にSpacer() 上にSpacer()
f:id:qed805:20200725135436p:plain:w300 f:id:qed805:20200725135514p:plain:w300

子Viewの幅がそれぞれ異なっている場合に子Viewの左右揃えについてはalignmentで制御します。

VStack(alignment: .leading) { } // 左寄せ
VStack(alignment: .trailing) { } // 右寄せ
alignment = leading) alignment = trailing)
f:id:qed805:20200725140123p:plain:w300 f:id:qed805:20200725140200p:plain:w300

このalignmentの定義にジャンプするとHorizontalAlignmentだったのですね。

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension HorizontalAlignment {

    /// A guide marking the leading edge of the view.
    public static let leading: HorizontalAlignment

    /// A guide marking the horizontal center of the view.
    public static let center: HorizontalAlignment

    /// A guide marking the trailing edge of the view.
    public static let trailing: HorizontalAlignment
}

つまりVStackのAlignment は水平方向に対する揃えを制御していました。

HStack

HStack はViewを横に並べるレイアウトコンポーネントです。 同じく基本は中央寄せになっています。

struct ContentView: View {
    
    var body: some View {
        HStack {
            Text("あいうえお")
                .background(Color.yellow)
            Text("かきくけこ")
                .background(Color.red)
            Text("さしすせそ")
                .background(Color.green)
            Text("たちつてと")
                .background(Color.blue)
            
        }
        .background(Color.gray)
    }
}

f:id:qed805:20200725140420p:plain:w300
HStack (背景色はグレー)

HStackのalignmentはVerticalAlignmentと言われる構造体でした。

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension VerticalAlignment {

    /// A guide marking the top edge of the view.
    public static let top: VerticalAlignment

    /// A guide marking the vertical center of the view.
    public static let center: VerticalAlignment

    /// A guide marking the bottom edge of the view.
    public static let bottom: VerticalAlignment

    /// A guide marking the topmost text baseline view.
    public static let firstTextBaseline: VerticalAlignment

    /// A guide marking the bottom-most text baseline in a view.
    public static let lastTextBaseline: VerticalAlignment
}

HStackにalignmentを使って宣言すると

struct ContentView: View {
    
    var body: some View {
        HStack (alignment: .top) {
            Text("あいうえお")
                .background(Color.yellow)
                .frame(height: 200)
            Text("かきくけこ")
                .background(Color.red)
            Text("さしすせそ")
                .background(Color.green)
            Text("たちつてと")
                .background(Color.blue)
            
        }
        .frame(maxWidth: .infinity, maxHeight: 200)
        .background(Color.gray)
    }
}

これで次のように見えます。

f:id:qed805:20200725141821p:plain:w300

VerticalAlignment の値を変更すると次のようになりました。

(alignment: .top) (alignment: .center) (alignment: .bottom)
f:id:qed805:20200725141821p:plain:w300 f:id:qed805:20200725141947p:plain:w300 f:id:qed805:20200725142028p:plain:w300

Flutterの考え方に近いものを感じるけど、書き方に慣れるのに時間がかかりそうな予感です。 表でまとめるとSwiftUIとFlutterで近いものを挙げてみました。

SwiftUI Flutter
VStack Column
HStack Row

こんな感じの理解です。

FlutterだとひたすらViewを左右に並べたい場合はRowで、上下に並べたい場合はColumnを使って配置していきました。 SwiftUIだとVStackとHStackを交互に並べて組み立てていけば良いわけですね。

2. SwiftUIでカウンターアプリを作ってみる

今回はSwiftUIでカウンターアプリを作ってみた。

使ったコンポーネント

  • VStack (縦に並べるもの)
  • Text (カウンター表示用)
  • Button (タップしてカウンターを増減させる)

出来上がった画面は次の通りである。

f:id:qed805:20200724175818p:plain:w300
カウンターアプリ

書いたソースコードはこちらです。

ContentView.swift

import SwiftUI

class Counter: ObservableObject {

    @Published var count: Int = 0
    
    func increment() {
        count += 1
    }
}

struct ContentView: View {
    @ObservedObject var counter = Counter()
    
    var body: some View {
        VStack {
            Text("count: \(counter.count)")
            
            Text("カウンター")
                .bold()
                .italic()
                .foregroundColor(.white)
                .font(.title)
                .fontWeight(.bold)
                .background(Color.yellow)
            
            Button(action: {
                counter.increment()
            }, label: {
                Text("カウント+")
            })
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

やりたかったのはButtonのactionで関数呼びをしたかっただけです。 ですが、ContentViewがstruct(構造体)で、struct内で関数宣言してButtonのaction内で呼び出ししようとしてもエラーが発生して呼び出せない。 そこでどうすれば関数呼び出しができるのかを検索したら見つかりました。

ObservableObjectを継承したクラスを作成してこのクラスでロジックを持たせたらいいみたい。

ちなみにこのロジッククラスをContentViewクラスでプロパティとして持たせようと

@ObservedObject let counter = Counter()

で宣言しても

Property wrapper can only be applied to a 'var'

と怒られてビルドに失敗しました。 あとはこのcounterのプロパティをButtonのactionで呼び出しして、increment()の関数を呼び出せば関数呼び出しができるようになります。

1. SwiftUI Textの使い方

今日からしばらくSwiftUIのコンポーネントについて学習していきます。

【目次】

Textの使い方

今回は Text についてです。

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .bold() // 太文字
            .italic() // イタリック
            .underline() // 下線
            .foregroundColor(.green) // テキストカラー
            .font(.title) // フォントの種類
            .fontWeight(.bold) // Weight
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

こちらをビルドすると下のようになります。

f:id:qed805:20200723121534p:plain:w300
Textのスクリーンショット

プロパティの定義

次にTextのプロパティを調べに行きます。

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension Text {
   public func foregroundColor(_ color: Color?) -> Text

   public func font(_ font: Font?) -> Text

   public func fontWeight(_ weight: Font.Weight?) -> Text

   public func bold() -> Text

   public func italic() -> Text

   public func strikethrough(_ active: Bool = true, color: Color? = nil) -> Text

   public func underline(_ active: Bool = true, color: Color? = nil) -> Text

   public func kerning(_ kerning: CGFloat) -> Text

   public func tracking(_ tracking: CGFloat) -> Text

   public func baselineOffset(_ baselineOffset: CGFloat) -> Text
}

メソッドを見ると返り値は Textになっていることが分かりますね。
ということは、

Text("Hello, world!")
.メソッド

これはメソッドチェーン的にメソッドがTextを返しているので、最後までTextであることが分かりました。

今更ながら2020年前半を振り返る

【目次】

振り返り

こんばんは、Tamappeです。
もう7月も中盤に差し掛かっていますが、まだ2020年の前半を振り返っていませんでしたので今日は2020年前半の振り返りを行います。

1月に行ったこと

ふと、何を思ったのかいきなりFlutterを触ってみたくなった。
僕の生活スタイルは1年間で1つのテーマを学習するスタイルです。その1つのテーマをFlutterに決めたのが1月でした。
ただFlutterを学習するだけなのはもったいないのでなんかテーマを決めたのが

iOSエンジニア視点でFlutterでアプリ開発を学習してみたら?」

ということで割と面倒くさいFlutterでiOSアプリを開発する学習記録を残していくことにした。
これなら近い将来FlutterをするiOSアプリエンジニアが増えてきたら僕のブログが読まれるじゃん!と勝手に思ってた。

ちなみにブログを続ける自信がなかったので1ヶ月ぐらい隠れて記事を投稿してた。

tamappe.hatenadiary.com

それからブログを投稿する習慣を付けたかったのでとりあえず2日に1回更新するように心掛けていた。

このときに参考にしてた本はこちらの本です。

Android/iOSクロス開発フレームワーク Flutter入門

Android/iOSクロス開発フレームワーク Flutter入門

この本は昨年、会社の福利厚生で購入した本だった。
去年(2019年)はそろそろFlutterがiOSアプリエンジニアでも流行り出すでしょということで予め購入していました。
その後積ん読になった・・・。

この本で学習した知識を通してさらにUIKitだったらこう作るよね?
という視点で記事を書いていた。

1月は7記事ぐらい投稿していた記憶があります。

2月に行ったこと

2月はまだコロナウイルスが騒がれていなかったような記憶がある。
ダイヤモンド・プリンセス号でコロナウイルスの集団培養が起きてコロナウイルスの感染力を目の当たりにしたような印象だった。

まだまだマスクなしで外出していた時期だった。
2月のブログの更新は2日に1回ペースでこっそり続けていた。

中旬ぐらいにコンパスイベントpotatotipsのイベントがあったのでそれに参加したのを覚えている。

potatotips #68

potatotips.connpass.com

初のpotatotipsでの登壇でした。
iOS Tips発表枠で参加しました。確かUIAlertControllerの制御をテーマにして登壇しました。
Androidの登壇者はFlutterネタが多かったです。

あまりに多かったので、このタイミングでノリでツイッターで実はFlutterを学習していることをツイートしてしまいました。
これで逃げられなくなったのを覚えている。
2月の終わり頃にはFlutterの本が終盤でRSSアプリを作ってた。

tamappe.hatenadiary.com

3月に行ったこと

この頃になると日本でもコロナウイルスの流行が見逃せなくなったからか世間が騒がしくなってましたね。
どっちかというとパニックになっていた印象があります。
マスクは2月上旬で既に売り切れで仕入れることができなくなってたので引き続きコンビニや薬局で品切れになってました。

アプリ開発はFlutterでの操作に慣れてきた頃でSketchで4択クイズアプリを作る企画をしていた。

Flutter本が完了してUdemyで手頃な動画を探してその動画に沿ってコードを写経していた。

参考にしていた動画はこちらです。

www.udemy.com

動画を見ながらデザインをアレンジして4択クイズアプリを開発しながら記事を作っていた。
書き慣れてきたので土日に記事を作り溜めして2日に1回ペースで記事を投稿するようになった。

tamappe.hatenadiary.com

そういえば、2月まで1日平均3時間以上Switchのスプラトゥーン2をプレイしていたゲーム中毒問題は 会社の人にSwitch本体ごと譲渡したことで解消されたな。

プレイ時間はたしか3年間でトータル2000時間には達すると思うので、 この時間を別のことに集中してたらまた違った人生を歩んでいたに違いないと思っている。
今振り返ればブログに投資していたら何記事出来たことやらと思っている。

4月に行ったこと

4月に東京都で緊急事態宣言が発令されたので会社も突如リモートワークが導入されて在宅ワークになった。
世間的にはリモートワークとテレワークという言葉が流行りだした時期です。
気分の切り替えとして朝イチにレッドブルを飲む習慣を付けた。

この時は会社も世間も状況の入れ替わりが激しなったので業務後にしていた事を覚えていなかった。
人生でそうそうない在宅ワークが始まったのでYouTubeで動画を見るようになった。(これが一番今でも影響を受けている)

印象に残っているのはYouTubeにFlutterのライブコーディングの動画が沢山あったので、 これからFlutterを触る方はYouTubeの動画だけで十分な気がしていた。

Flutterでのアプリ開発はネタ切れ感があったので上司に相談したところ、 AppleMusicのクローンアプリを作ってみればということでSketchでデザインを作ってクローンアプリ開発を始めました。

tamappe.hatenadiary.com

5月に行ったこと

5月のゴールデンウィークまではひたすら色々なブログを書きまくっていた。
スプラトゥーン2のプレイ時間をブログ記事執筆に当てていれば・・・

とこのときほど後悔したことはなかった。。。

5月中旬はiOSアプリエンジニアの堤さんがキャンプファイヤーでなんかするということだったので、 日頃お世話になったような気持ちになったので寄付しました。
寄付してからオンラインサロンだったことに気づいてとりあえず登録作業だけ済まして放置してた。

Flutterでのアプリ開発はAppleMusicクローンアプリ開発が飽きてきた頃で オリジナルアプリとしてナンバーズアプリを作ることに決めた。

今振り返れば、AppleMusicアプリの延長でYouTubeAPIを使って動画のまとめアプリに改変すればよかったと反省している。

tamappe.hatenadiary.com

この時が一番Flutterでのアプリ開発で楽しかった時期だったかもしれない・・・。

6月に行ったこと

(編集中)

全体の振り返り

思い返せば、2020年の前半は

  • Flutterでのアプリ開発のスタート
  • ブログ活動の本格開始
  • オンラインサロンでの活動
  • ゲーム中毒の解消

となかなか人生の分岐点になりそうなイベントばかりが起こっている。
半年前はまだスプラトゥーン2でゲーム中毒になっていたんだな。。。
あのゲームは極めれば極めるほどストレスが溜まるというなかなかの鬼畜ゲームだったのでそれを辞められたのは凄い大きい。

ブログ活動にFlutterなんで2020年は生活習慣がガラッと変わった年になったと思う。
ということで締まりが悪いですが色々振り返れたんでこういうのも悪くないと思いました。

それでは、バイバイ!

今後このブログの情報発信の方向性とFlutterの所感について

【目次】

今後このブログでの情報発信の方向性

はてなではまだ公表はしていないが、僕は最近noteで記事を書くようになりました。

note.com

んで、noteで最近僕が堤さんが主催するサロンに入っていることを書きました。

note.com

実は僕はiOSアプリエンジニアの堤さんのサロンに入っています。堤さんといえば、iOSアプリ界隈ではトップクラスのエンジニアの方でこの界隈ではとても有名な方です。そのサロンで出会ったiOSアプリを作りたいと思っているサロンメンバーにiOSアプリ開発のイロハについて教えています。

こんな感じです。

堤さんのサロンに入り口はこちらです。

community.camp-fire.jp

今回の記事はこのサロンの紹介ではないです。

サロンの内容と期待していること

僕がこのサロンに期待していることって簡単に説明すると

  • エンジニアの友人を作ること
  • エンジニアとしてのプレゼンスをもうちょっと広めること

である。

ちなみにサロンというと怪しい活動をしているような印象を与えてしまうらしいです。
イメージとしては僕は部活動的なものと思っている。
スポーツで言えば社会人フットサルみたいなサークル活動のオンライン版みたいなものと思ってくれたらOKです。

それで昨日サロン主の堤さんが「エンジニア人生をより良くするための発信力向上講座」の第1回目のイベントをしてくれました。

イベントの内容はオリエンテーション的なもので講座の内容や、
今「自分が目指しているエンジニア像のゴール」を元にして、これからの活動の方向性などを簡潔にアドバイスして頂きました。

簡潔にと書きましたが、3時間もののイベントなので内容が濃いです。

ブログの方向性について

そのイベントでこれから1年間ぐらいの僕のブログの方向性が決まりました。

1年ぐらい「自分の強み」を見つけるために、またFlutterに関する内容とプラスしてSwiftUIについて学んだことを記事にしていきたいと思ってます。

情報発信についてはどうやれば、他のエンジニアと差別化できるんだろうとずっとモヤモヤしながら漠然としていた。

正直、今の興味の方向性はiPadApple Pencilに向いてますが、これは一旦保留にするかもしれません。

iOSエンジニアとFlutter

これからの方向性にあるFlutterは間違いなくiOSアプリ開発でも浸透してくるだろうから、iOSエンジニアとしてFlutterと向き合う必要があるかなと。

去年AppleがSwiftUIという宣言的UIフレームワークを発表しました。

このSwiftUIはどう考えてもGoogle製のFlutterの影響が受けていることが分かります。

ということはAppleも少なからずFlutterを意識しちゃっているのが目に取れます。

その背景はこれまでの10年間ぐらいGoogle がモバイルの分野でAppleに負け続けたのと、オラクルとのJavaAPIの仕様権限論争で一度敗訴してしまったのが要因かなと思ってます。

なので次の10年こそはGoogleがモバイルの分野で勝ちたいと思ってるんじゃないかな。

その一歩がFlutterと勝手に思っている。

そんな背景からFlutterの情報発信をしていきたいな。