20200115のiOSに関する記事は8件です。

Siri に Slackbot が投稿した AWS 請求額を読み上げる仕事をさせる

はじめに

iPhone を使い始めて10年弱、 Siri が全く働いていないことに気付きました。
スマートスピーカーが流行っている昨今、自分の Siri にももっと活躍して欲しいので Slackbot が投稿した AWS 請求額を読み上げてもらうことにしました。

結果から見せるとこんな感じです。
料金を聞くと...
スクリーンショット 2020-01-14 22.12.01.png

Slackに投稿した内容を読み上げてくれました。
スクリーンショット 2020-01-14 22.12.21.png
では、作り方諸々を紹介していきます。

構成

構成はこんな感じになります。
アーキテクチャ.png

それぞれの役割を紹介していきます。

  • Siri
    • 音声呼び出しによるショートカットアプリの実行
    • Slack に投稿された AWS 請求額の読み上げ
  • ショートカットアプリ
    • SlackAPI を使用して投稿
    • SlackAPI を使用して最新の投稿を JSON で取得
    • 取得された JSON 情報から必要な内容を抽出
  • Slackbot
    • Lambda の呼び出し
  • Lambda
    • AWS 請求額の取得

処理内容

今回はショートカットアプリに重点を置いて説明していきます。
Lambda と Slackbot についてはそれぞれ以下の記事にて詳細を記載しているので参照してください。
Lambda:(Python)AWSの請求金額を取得する
Slackbot:echobotを作成し、Slackに通知する

それでは、ショートカットを作成していきます。
まず、ショートカット名を決めます。
Siri にショートカット名を呼びかけると登録しているショートカットを実行してくれます。
なので今回は『今月の料金は』というショートカット名にします。
(『AWSの料金は』としたかったのですが Siri が AWS の部分を理解してくれませんでした...)

続いて、各処理で使用する変数を定義していきます。

  • token:SlackAPI で使用する token (今回は Legacy tokens を使用しています。)
  • channel_nm:SlackAPI で投稿するチャンネル名
  • channel_id:SlackAPI で最新の投稿を取得する対象のチャンネルID
  • text:Slack に投稿する文章

変数が設定できたら、 Slack 投稿用 API の URL を作成し、実行します。
1.png
2.png
API を実行すると、 AWS 請求額取得 Lambda が実行されます。結果がSlackに投稿されるまで時間がかかるので待機させます。
3.png
待機後、 Slack の最新投稿取得APIを実行していきます。
投稿用 API の時と同様に URL を作成し、APIを実行します。
4.png
5.png
最新投稿取得 API を実行すると以下のような JSON が返却されます。

{"messages":
    [{
        "bot_id":"XXXXXXXX",
        "ts":"1579095866.000200",
        "attachments":
        [{
            "color":"36a64f",
            "id":1,
            "fallback":" ・AWS Cost Explorer: 1.04 USD\n ・Tax: 0.10 USD",
            "pretext":"01月01日から01月14日の請求額は、1.14 USDです。",
            "text":" ・AWS Cost Explorer: 1.04 USD\n ・Tax: 0.10 USD"
        }],
        "type":"message",
        "subtype":"bot_message",
        "text":""
    }],
    "has_more":true,
    "ok":true,
    "channel_actions_ts":null,
    "channel_actions_count":0}

ここから pretext の値を変数に追加していきます。
6.png
7.png
8.png
9.png
10.png
11.png
これで Siri に読み上げてもらう内容が取得できました。
最後に読み上げる文章をテキストに起こし、読み上げのアクションを設定します。
12.png
完成です。
Siri に向かって『 Hey Siri! 』と話しかけましょう!

おわりに

これで Siri が一つ仕事を覚えてくれました。
この構成を使えばもっといろいろな事ができると思います。
今回は Slack + Lambda をショートカットアプリから呼び出しましたが、 アプリの Pythonista3 から Python を実行しても面白い事ができると思います。

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

【swift5】自作電卓アプリを作ってみた

まずは全体像から

GIFがこちら

uMMu8OnW.gif

全体のデザインがこちら

スクリーンショット 2020-01-15 22.03.16.png

全体のコード

githubはこちらから
https://github.com/sventouz/calculator

import UIKit

class ViewController: UIViewController {

    var numberOnScreen:Int = 0
    var previousNumber:Int = 0
    var performingMath = false
    var operation = 0

    @IBOutlet weak var label: UILabel!

    @IBAction func numbers(_ sender: UIButton) {

        if performingMath == true {
            label.text = String(sender.tag-1)
            numberOnScreen = Int(label.text!)!
            performingMath = false
        }
        else {
            label.text = label.text! + String(sender.tag-1)
            numberOnScreen = Int(label.text!)!
        }

    }

    @IBAction func buttons(_ sender: UIButton) {

        if label.text != "" && sender.tag != 11 && sender.tag != 16{
            previousNumber = Int(label.text!)!
            if sender.tag == 12{ // ÷
                label.text = "÷";
            }
            else if sender.tag == 13{  // ×
                label.text = "×";
            }
            else if sender.tag == 14{  // -
                label.text = "-";
            }
            else if sender.tag == 15{  // +
                label.text = "+";
            }
            operation = sender.tag
            performingMath = true;
        }
        else if sender.tag == 16 // = が押された時の処理
        {
            if operation == 12{
                label.text = String(previousNumber / numberOnScreen)
            }
            else if operation == 13{
                label.text = String(previousNumber * numberOnScreen)
            }
            else if operation == 14{
                label.text = String(previousNumber - numberOnScreen)
            }
            else if operation == 15{
                label.text = String(previousNumber + numberOnScreen)
            }
        }
        else if sender.tag == 11{ // C が押された時の処理
            label.text = ""
            previousNumber = 0;
            numberOnScreen = 0;
            operation = 0;
        }

    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

コードを説明していきます

コードの接続

まずコードでは表示されていませんが、0〜9の数字を

@IBAction func numbers(_ sender: UIButton) {
}

のなかにドラッグアンドドロップします。

こちらも同様に+, -, ÷, × をドラッグアンドドロップ。

@IBAction func buttons(_ sender: UIButton) {
}

IBアクションではありません。connect actionです。

tag追加

tagを追加していきます。

0には1を

1には2をつけていき9に10がつけばOK

次は

Cに11

÷に12

と続き

=が16になれば完璧です。

続き

@IBAction func numbers(_ sender: UIButton) {
  // この中が数字をクリックしたときに動く場所
}
@IBAction func buttons(_ sender: UIButton) {
  // この中が数字以外の四則関数をクリックしたときに動く場所
}

あとは全体のコードを見ながら各自コードを解読していってください。

感想

電卓なんか簡単でしょ?って思っていたのですが難しかったです。笑

もっとシンプルなのを作っていき最終的には複雑なものを作っていければいいと思う。

その過程ではアウトプットを忘れずに!

参考

https://blog.codecamp.jp/iphone-app-develope-original-calculator

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

【swift5】自作電卓アプリを作ってみよう(完全版)

まずは全体像から

GIFがこちら

uMMu8OnW.gif

全体のデザインがこちら

スクリーンショット 2020-01-15 22.03.16.png

全体のコード

githubはこちらから
https://github.com/sventouz/calculator

import UIKit

class ViewController: UIViewController {

    var numberOnScreen:Int = 0
    var previousNumber:Int = 0
    var performingMath = false
    var operation = 0

    @IBOutlet weak var label: UILabel!

    @IBAction func numbers(_ sender: UIButton) {

        if performingMath == true {
            label.text = String(sender.tag-1)
            numberOnScreen = Int(label.text!)!
            performingMath = false
        }
        else {
            label.text = label.text! + String(sender.tag-1)
            numberOnScreen = Int(label.text!)!
        }

    }

    @IBAction func buttons(_ sender: UIButton) {

        if label.text != "" && sender.tag != 11 && sender.tag != 16{
            previousNumber = Int(label.text!)!
            if sender.tag == 12{ // ÷
                label.text = "÷";
            }
            else if sender.tag == 13{  // ×
                label.text = "×";
            }
            else if sender.tag == 14{  // -
                label.text = "-";
            }
            else if sender.tag == 15{  // +
                label.text = "+";
            }
            operation = sender.tag
            performingMath = true;
        }
        else if sender.tag == 16 // = が押された時の処理
        {
            if operation == 12{
                label.text = String(previousNumber / numberOnScreen)
            }
            else if operation == 13{
                label.text = String(previousNumber * numberOnScreen)
            }
            else if operation == 14{
                label.text = String(previousNumber - numberOnScreen)
            }
            else if operation == 15{
                label.text = String(previousNumber + numberOnScreen)
            }
        }
        else if sender.tag == 11{ // C が押された時の処理
            label.text = ""
            previousNumber = 0;
            numberOnScreen = 0;
            operation = 0;
        }

    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

コードを説明していきます

コードの接続

まずコードでは表示されていませんが、0〜9の数字を

@IBAction func numbers(_ sender: UIButton) {
}

のなかにドラッグアンドドロップします。

こちらも同様に+, -, ÷, × をドラッグアンドドロップ。

@IBAction func buttons(_ sender: UIButton) {
}

IBアクションではありません。connect actionです。

tag追加

tagを追加していきます。

0には1を

1には2をつけていき9に10がつけばOK

次は

Cに11

÷に12

と続き

=が16になれば完璧です。

続き

@IBAction func numbers(_ sender: UIButton) {
  // この中が数字をクリックしたときに動く場所
}
@IBAction func buttons(_ sender: UIButton) {
  // この中が数字以外の四則関数をクリックしたときに動く場所
}

あとは全体のコードを見ながら各自コードを解読していってください。

感想

電卓なんか簡単でしょ?って思っていたのですが難しかったです。笑

もっとシンプルなのを作っていき最終的には複雑なものを作っていければいいと思う。

その過程ではアウトプットを忘れずに!

参考

https://blog.codecamp.jp/iphone-app-develope-original-calculator

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

Universal LinksのTeam ID指定が他と違う場合がある

iOSのUniversal Linksを設定していたが、シミュレータでは正しく挙動するものの実機では一切動かないということが起こった。

アプリ側でちゃんとEntitlementsの値がセットされているかを確認するために、codesignコマンドでEntitlementsを確認してみると…

$ codesign -d --entitlements :- MyApp.app 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>application-identifier</key>
    <string>BBBBBBBBBB.net.iseteki.myapp</string>
    <key>aps-environment</key>
    <string>development</string>
    <key>com.apple.developer.applesignin</key>
    <array>
        <string>Default</string>
    </array>
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:myapp.example.com</string>
        <string>activitycontinuation:myapp.example.com</string>
    </array>
    <key>com.apple.developer.team-identifier</key>
    <string>AAAAAAAAAA</string>
    <key>get-task-allow</key>
    <true/>
</dict>
</plist>

…おわかりいただけたであろうか?

application-identifierのprefixとteam-identifierが一致してない!

昔から作られているアプリはApp ID Prefixが異なる場合がある

昔、iOS Developer Portal(当時名称)でApp IDを作る際にApp ID Prefixを新規に生成することができた。現在はTeam ID固定となっていてこの設定をすることはできないようになっているが、昔から存在するApp IDの場合はこのBundle ID Prefixの指定が残っている。

App IDに指定されているApp ID PrefixはApp IDの設定画面で確認できる。

スクリーンショット 2020-01-15 13.21.34.png

App ID Prefixの末に (Team ID) と書かれていない場合は、Team IDとは異なるApp ID Prefixになっている。

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

Universal LinksのPrefix指定が他と違う場合がある

iOSのUniversal Linksを設定していたが、シミュレータでは正しく挙動するものの実機では一切動かないということが起こった。

  • apple-site-association には、[Team ID].[Bundle ID] の形式でapplinksを記載していた
  • App Search API Validation Tool では Error no apps with domain entitlements と出ていたが、これは関係がないという記事が多く見られた。
  • branch のバリデータ では正しく設定されていると出ていた

アプリ側でちゃんとEntitlementsの値がセットされているかを確認するために、codesignコマンドでEntitlementsを確認してみると…

$ codesign -d --entitlements :- MyApp.app 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>application-identifier</key>
    <string>BBBBBBBBBB.net.iseteki.myapp</string>
    <key>aps-environment</key>
    <string>development</string>
    <key>com.apple.developer.applesignin</key>
    <array>
        <string>Default</string>
    </array>
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:myapp.example.com</string>
        <string>activitycontinuation:myapp.example.com</string>
    </array>
    <key>com.apple.developer.team-identifier</key>
    <string>AAAAAAAAAA</string>
    <key>get-task-allow</key>
    <true/>
</dict>
</plist>

…おわかりいただけたであろうか?

application-identifierのprefixとteam-identifierが一致してない!

昔から作られているアプリはApp ID Prefixが異なる場合がある

昔、iOS Dev CenterでApp IDを作る際にApp ID Prefixを新規に生成することができた。現在はTeam ID固定となっていてこの設定をすることはできないようになっているが、昔から存在するApp IDの場合はこのBundle ID Prefixの指定が残っている。

App IDに指定されているApp ID PrefixはApp IDの設定画面で確認できる。

スクリーンショット 2020-01-15 13.21.34.png

App ID Prefixの末に (Team ID) と書かれていない場合は、Team IDとは異なるApp ID Prefixになっている。

apple-site-association に指定するBundle IdentifierのPrefixをここに表示されているApp ID Prefixに修正することで問題なく動作するようになった。

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

Universal LinksのApp ID PrefixがTeam IDでない場合がある

iOSのUniversal Linksを設定していたが、シミュレータでは正しく挙動するものの実機では一切動かないということが起こった。

  • apple-site-association には、[Team ID].[Bundle ID] の形式でapplinksを記載していた
  • App Search API Validation Tool では Error no apps with domain entitlements と出ていたが、これは関係がないという記事が多く見られた。
  • branch のバリデータ では正しく設定されていると出ていた

アプリ側でちゃんとEntitlementsの値がセットされているかを確認するために、codesignコマンドでEntitlementsを確認してみると…

$ codesign -d --entitlements :- MyApp.app 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>application-identifier</key>
    <string>BBBBBBBBBB.net.iseteki.myapp</string>
    <key>aps-environment</key>
    <string>development</string>
    <key>com.apple.developer.applesignin</key>
    <array>
        <string>Default</string>
    </array>
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:myapp.example.com</string>
        <string>activitycontinuation:myapp.example.com</string>
    </array>
    <key>com.apple.developer.team-identifier</key>
    <string>AAAAAAAAAA</string>
    <key>get-task-allow</key>
    <true/>
</dict>
</plist>

…おわかりいただけたであろうか?

application-identifierのprefixとteam-identifierが一致してない!

昔から作られているアプリはApp ID Prefixが異なる場合がある

昔、iOS Dev CenterでApp IDを作る際にApp ID Prefixを新規に生成することができた。現在はTeam ID固定となっていてこの設定をすることはできないようになっているが、昔から存在するApp IDの場合はこのBundle ID Prefixの指定が残っている。

App IDに指定されているApp ID PrefixはApp IDの設定画面で確認できる。

スクリーンショット 2020-01-15 13.21.34.png

App ID Prefixの末に (Team ID) と書かれていない場合は、Team IDとは異なるApp ID Prefixになっている。

apple-site-association に指定するBundle IdentifierのPrefixをここに表示されているApp ID Prefixに修正することで問題なく動作するようになった。

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

NeumorphismなUIをSwiftUIで作れるライブラリ、Neumorphismicを作ってみた

先日Neumorphism: 令和時代のスキューモーフィズムを読みました。

これが流行る頃にはSwiftUIも使えるようになってるだろう…ということでSwiftUIでNeumorphismのライブラリを作ってみました。Switchなどは作っていません。今後時間があれば作っていきたいですが、まだ形がコレと定まってもいないので難しそうですね。そもそも流行るのかもわかりませんし。

スターつけてくださると嬉しいです!

完成形

Demo view
Settings view

ModifierでShadowを実装する

Modifierについてはこの記事でひと通りわかると思います。簡単にいうと.fontとか.frameとかをまとめてViewに適合できるやつです。Buttonなどの場合はButtonStyleなどが使えればいいのですが、Appleさんが公開していないので諦めます。
SwiftUIでもshodowは1つしか追加できません。なのでZStackViewを2つ重ねてそれぞれに.shadowをつける必要があります。

struct ConvexModifier: ViewModifier {
    let lightShadowColor: Color
    let darkShadowColor: Color
    func body(content: Content) -> some View {
        ZStack {
            content
                .shadow(color: darkShadowColor, radius: 16, x: 9, y: 9)
            content
                .shadow(color: lightShadowColor, radius: 16, x: -9, y: -9)
        }
    }
}

しかし、これだけでは影の色をいちいち入力する必要があります。

色をEnvironmentで伝搬する

SwiftUIでは@Environmentを使うことでその子View全てに値を伝えることができます。詳しくはこの記事を読んでください。
そしてこれは自作することもできます。Neumorphismでは基本的にViewに1色しか使わないためこれが非常に有効です。さらに、ConvexModifierbaseColorを基準に影の色を決めればよくなります。自作する方法はこちらをご覧ください。

struct BaseColorKey: EnvironmentKey {
    static let defaultValue: Color = .gray
}

extension EnvironmentValues {
    var baseColor: Color {
        get { self[BaseColorKey.self] }
        set { self[BaseColorKey.self] = newValue }
    }
}

さて、色を変換したいわけだけど…

パッと見、SwiftUIのColorからはrgbやhueや取得できそうにありません。が、優しきAppleさんは.descriptionを用意してくれていました。#C1D2EBFFのようにカラーコードを返してくれます。ということでカラーコードからColorを生成(このリンクではUIColor)できるようにし、RGBとHSLRGBとHSBの変換コードを用意します。

Colorから色情報を取れるとわかったので、早速lighterColorを実装します。neumorphismPrimary(value:)は下に出てくるFloatingTabViewのラベルなどで使われています。

func getHSLA() -> (h: Double, s: Double, l: Double, a: Double) {
    let string = String(self.description.dropFirst())
    let v = Int(string, radix: 16) ?? 0

    let r = Double(v / Int(powf(256, 3)) % 256) / 255
    let g = Double(v / Int(powf(256, 2)) % 256) / 255
    let b = Double(v / Int(powf(256, 1)) % 256) / 255
    let a = Double(v / Int(powf(256, 0)) % 256) / 255

    let (h, s, l) = ColorTransformer.rgbToHsl(r: r, g: g, b: b)
    return (h, s, l, a)
}

func lighter(value: Double) -> Color {
    let (h, s, l, a) = getHSLA()
    let hsb = ColorTransformer.hslToHsb(h: h, s: s, l: l + value)
    return Color(hue: hsb.h, saturation: hsb.s, brightness: hsb.b, opacity: a)
}

func darker(value: Double) -> Color {
    let (h, s, l, a) = getHSLA()
    let hsb = ColorTransformer.hslToHsb(h: h, s: s, l: l - value)
    return Color(hue: hsb.h, saturation: hsb.s, brightness: hsb.b, opacity: a)
}

func primary(value: Double) -> Color {
    let (_, _, l, _) = getHSLA()
    return (l > 0.5) ? darker(value: value) : lighter(value: value)
}
struct ColorExtension_Previews: PreviewProvider {
    static var previews: some View {
        let color = Color(hex: "C1D2EB")

        return Group {
            ColorPreview(color)
            ColorPreview(color.lighter(value: 0.12))
            ColorPreview(color.darker(value: 0.18))
        }
        .previewLayout(.fixed(width: 200, height: 100))
    }
}

Colors
こうなりました。影として使わないとよくわかりませんね。

ConvexModifierを完成させる

材料は揃ったので合わせてみましょう。

struct ConvexModifier: ViewModifier {

    @Environment(\.baseColor) var baseColor: Color

    func body(content: Content) -> some View {
        ZStack {
            content
                .shadow(color: baseColor.darkerColor(value: 0.18), radius: 16, x: 9, y: 9)
            content
                .shadow(color: baseColor.lighterColor(value: 0.12), radius: 16, x: -9, y: -9)
        }
    }
}
struct ConvexModifier_Previews: PreviewProvider {
    static var previews: some View {
        ZStack {
            Color(hex: "C1D2EB")
                .edgesIgnoringSafeArea(.all)

            Circle()
                .fill(Color(hex: "C1D2EB"))
                .modifier(ConvexModifier())
                .frame(width: 300, height: 300)
        }
        .environment(\.baseColor, Color(hex: "C1D2EB"))
    }
}

.environmentbaseColorを伝えるの忘れないようにしましょう。
Image
いい感じですね!

environmentを使ったので当然

struct ConvexModifier_Previews: PreviewProvider {
    static var previews: some View {
        ZStack {
            Color(hex: "C1D2EB")
                .edgesIgnoringSafeArea(.all)

            Circle()
                .fill(Color(hex: "C1D2EB"))
                .frame(width: 300, height: 300)
                .modifier(ConvexModifier())
                // `environment`で影の色を変える
                .environment(\.baseColor, Color.red)
        }
        .environment(\.baseColor, Color(hex: "C1D2EB"))
    }
}

とできるだろうと思っていたのですができませんでした。.redの時は黒い影が出てきて、その他の場合は影がなくなりました。Color(hex:)を使ったら行けたのでよくわかりません。

Highlight時に表示を変えられるButtonも作ってみた

ハイライト時にNeumorphismではどうするのが正解なんだろうと考えるためにとりあえず作ってみたのですが、標準のButtonでもいい気がします。個人的に標準Buttonは ハイライト時に色が薄すぎる気がするのでこれを使ってます。押下時にボタンを小さくしたいときなどに使ってみてください。

struct HighlightableButton<Label>: View where Label: View {

    private let action: () -> Void
    private let label: (Bool) -> Label

    public init(
        action: @escaping () -> Void,
        label: @escaping (Bool) -> Label
    ) {
        self.action = action
        self.label = label
    }

    @State private var isHighlighted = false

    public var body: some View {
        label(isHighlighted)
            .animation(.easeOut(duration: 0.05))
            .gesture(DragGesture(minimumDistance: 0)
                .onChanged { _ in
                    withAnimation { self.isHighlighted = true }
                }
                .onEnded { _ in
                    self.action()
                    withAnimation { self.isHighlighted = false }
                }
        )
    }
}
struct ConvexModifier_HighlightableButton_ForPreviews: View {
    @State var isSelected = false
    var body: some View {
        HighlightableButton(action: {
            self.isSelected.toggle()
        }) { isH in
            Image(systemName: self.isSelected ? "house.fill" : "house")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: isH ? 54 : 60)
                .foregroundColor(Color(hex: "C1D2EB").darker(value: 0.18))
                .background(
                    Circle()
                        .fill(Color(hex: "C1D2EB"))
                        .frame(width: isH ? 90 : 100,
                               height: isH ? 90 : 100)
                        .modifier(ConvexModifier())
                )
                .opacity(isH ? 0.6 : 1)
        }
    }
}

.modifier(ConvexModifier())Buttonの中に書いてください。外に書くときちんと適用されません。ForPreviewsみたいな名前になっているのは、PreviewProviderの中には@Stateが置けないからです。
GIF

FloatingTabViewも作ってみた

@touyouもタブを作ってることだし作るかーみたいな感じで作ってみました。ViewBuilderを読み解こうとまでは思わなかったので、標準のTabViewのような綺麗さはありませんが、まあ使えるのではないでしょうか。タブの数は4つまでにしてみました。その他を実装する気力はなかったので切り捨てています。あとで多分何とかします。
使い方はこんな感じです。

struct FloatingTabView_ForPreviews: View {
    enum Season: String, CaseIterable {
        case spring, summer, fall, winter
        var color: Color {
            switch self {
            case .spring: return .pink
            case .summer: return .blue
            case .fall:   return .orange
            case .winter: return .white
            }
        }
    }
    @State var season: Season = .spring
    var body: some View {
        FloatingTabView(selection: $season, labelText: { s in
            s.rawValue
        }, labelImage: { _ in Image(systemName: "camera") }) { s in
            s.color.edgesIgnoringSafeArea(.all)
        }
    }
}

例を作るのが面倒なのが目に見えますね…。labelImageなんてカメラだけですし。この例では色を変えていますが、Neumorphismでは色を変えるのはご法度なので注意。(もちろん局所的にアクセントとして使うのはOKです)
Screen Shot 2020-01-12 at 20.34.46.png
ああ、影の色が汚い…。まあ色変えるとこうなるよっていう悪い例と思ってください。
ちなみにGeometryReaderのせいかLive PreviewにしていないとTabが下に落ちてしまいました。SwiftUIは7不思議どころじゃありません。そこら辺に穴が一杯です。全く理由がわからず何となくLive Previewにしてみたところ正しいことがわかりました。恐ろしや。

凹も作りたかったけど

凹凸どちらも作りたかったのですが、凹の方はいい案が思い浮かばず、凸だけになってしまいました。適合したいaViewよりひとまわり大きいbViewを作って、そこからaViewの大きさを切り抜いて、aViewの上にbViewを2つ置いて影をつければ行けそうだなとは思ったんですが、くり抜く方法がわかりませんでした。Pathを使えば何とかなりそうですが、Viewの形を取る方法もないですし…。
あと、SwiftUI製ですし、全プラットフォームに対応させたかったのですが、macOSにSF Symbolsがなかったり、tvOSにDragGestureがなかったりと面倒になってやめました() まあそこまで大変そうでもないのでいつかします。

使う上での諸注意

HighlightableButtonのところでも書きましたが、Buttonで使う際は.modifier(ConvexModifier())Buttonの中に書いてください。そうしないときれいに作れません。
またBinding系のコンポーネントは2つ同時にViewに存在すると使えなくなるようです。

TextField("C1D2EB", text: $model.userInput)
    .foregroundColor(baseColor.nmPrimary(value: 1))
    .padding(5)
    .background(
        RoundedRectangle(cornerRadius: 5, style: .continuous)
            .fill(baseColor)
            .modifier(NMConvexModifier(radius: 9))
    )

のようにbackgroundに設定したView.modifier(ConvexModifier())してください。Viewの量的にもこちらの方がパフォーマンスもいいです。

初OSS & 初Qiita?

SwiftUIなのでマルチプラットフォームに対応のOSSにしたかったわけですが、iOSのみとは作り方が違うようで。WWDCを参考に作らせていただきました。
Qiitaも初投稿ですが、そろそろ開発から離れて受験勉強しないと浪人する未来しか見えないので当分記事を書くことはないでしょう。1年後に戻って来れるように頑張ります。

いいねとスターつけてくださると嬉しいです!

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

Flutterで有名なアプリのUIを再現する(Twitter編①)

この記事を見るとここまでできます

Screenshot_1579017962.png

今回は初回なので、まだまだ形になっていません。

今回の記事のポイント(要点だけ知りたい方はここだけ読んでください)

画像を円形にして表示する

Twitterでは、左上に表示されている自身の画像や、投稿に表示される投稿者の画像が円形に加工されて表示されています。
この部分の実装は下記のようにしています。

Container(
 margin: EdgeInsets.all(8.0),
 decoration: BoxDecoration(
  shape: BoxShape.circle,
  image: DecorationImage(
   fit: BoxFit.fill,
   image: NetworkImage(iconImgUrl),
  ),
 ),
)

ContainerのdecorationプロパティにBoxDecorationを指定しています。
BoxDecorationのshapeプロパティでBoxShape.circleを指定することで円形に加工されます。
また、画像もDecorationImageのfitプロパティでBoxFit.fillを指定することで円形全体に画像が表示されます。

BottomNavigationBarに4つ以上のボタンを配置する

表示環境にもよるかもしれませんが、私の環境の場合だとBottomNavigationBarに4つ以上のボタンを配置すると表示位置が均等にならず、背景色の指定が無効化されてしまいましたが、最終的に下記のようにすることで解決しました。

BottomNavigationBar(
  type: BottomNavigationBarType.fixed,
  backgroundColor: Color.fromRGBO(30, 40, 54, 1.0),
  showSelectedLabels: false,
  showUnselectedLabels: false,
  items: <BottomNavigationBarItem>[
    _menuButton(Icons.home),
    _menuButton(Icons.search),
    _menuButton(Icons.notifications_none),
    _menuButton(Icons.mail_outline),
  ],
)

上記のようにtypeプロパティにBottomNavigationBarType.fixedを指定する事で解決しました。
typeはデフォルトではBottomNavigationBarType.shiftingが指定されており、固定化されていないようでした。

ここから本題です

このシリーズのゴール

UI周りの実装方法を学習するには有名なアプリを真似て作ればゴールも明確だし、完成した時の満足感も高いだろう、という事で今回はFlutterでTwitterアプリを再現しようと思います。

あくまでUI周りの実装を学習する過程を纏めた記事なので、Twitterクライアントを作るとか、機能を作りこむといったところまではせず、見た目と動作がそっくりになるところまで進めようと思います。

今のところ、5,6回くらいに分けて投稿する予定ですが、実装を進めながらの投稿になりますので、ハマったポイントがあった場合は番外記事も投稿するかもしれません。

続きが気になる方はフォローをお願いします。

今回のゴール

今回は初回なので、上下のメニュー部分を実装します。

コードと解説

アプリのメインは下記のようにしました。

main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Twitter',
      home: MainPage(),
    );
  }
}

ルートページとしてMainPageをインスタンス化していますので、次はMainPageのコードです。

少し長いのでメソッドごとに4つに分けて解説します。
実際のコードはMainPageクラス内に全て実装しています。

main.dart
class MainPage extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _twitterAppBar(
          'https://d1f5hsy4d47upe.cloudfront.net/79/791ba2a1c3245ae92712c393fe2b6408_w.jpg'),
      body: Container(
        color: Color.fromRGBO(30, 40, 54, 1.0),
      ),
      bottomNavigationBar: _twitterBottomBar(),
    );
  }
}

Twitterアプリのメイン画面は大きく分けると3つのセクションで構成されています。

  • アプリケーションバー:画面上部のアカウントアイコン、Twitterのアイコン、星アイコンが表示されている部分
  • 本文:画面中央のTwitterの投稿が表示される部分
  • メニュー:画面下部の各画面への遷移するためのメニュー部分

上記コードのbuildメソッドではScaffoldを返しています。
Scaffoldは、マテリアルデザイン用のウィジェットで、マテリアルデザインの各要素がプロパティとして定義されています。
その中で今回は、

  • appBarプロパティ:アプリケーションバーにあたる部分です。_twitterAppBarメソッドで中身を実装しています。
  • bodyプロパティ:本文にあたる部分です。今回は背景色を付けているだけですので、説明は割愛します。
  • bottomNavigationBarプロパティ:メニューにあたる部分です。_twitterBottomBarメソッドで中身を実装しています。

続いて、_twitterAppBarメソッドのコードです。

main.dart
  Widget _twitterAppBar(String iconImgUrl) {
    return AppBar(
      backgroundColor: Color.fromRGBO(30, 40, 54, 1.0),
      leading: Container(
        margin: EdgeInsets.all(8.0),
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          image: DecorationImage(
            fit: BoxFit.fill,
            image: NetworkImage(iconImgUrl),
          ),
        ),
      ),
      centerTitle: true,
      title: Text('Trial'),
      actions: <Widget>[
        Container(
          margin: EdgeInsets.all(8.0),
          child: Icon(
            Icons.star_border,
            color: Color.fromRGBO(76, 158, 235, 1.0),
            size: 45,
          ),
        ),
      ],
    );
  }

このメソッドではAppBarを返しています。
ポイントは下記の3点です。

  • leadingプロパティ

アプリケーションバーの左側に表示されるウィジェットを指定する部分です。
Twitterでは、ログインユーザーのアイコン画像が円状に表示されているので、BoxDecorationを使って画像を円状に変形させています。

  • titleプロパティ

アプリケーションバーの中央に表示されるウィジェットを指定する部分です。
Twitterでは、Twitterのアイコンが表示されますが勝手に拝借するわけにもいかないので、この部分はテキストを表示してあります。
centerTitleプロパティをtrueにしておかないと、Androidでは左寄せで表示されます。

  • actionsプロパティ

アプリケーションバーの右側に表示されるウィジェットを指定する部分です。
Twitterでは、表示順ルール変更用の星マークが表示されているので、代替となるアイコンを表示しています。

次は、_twitterBottomBarメソッドです。

main.dart
  Widget _twitterBottomBar() {
    return Container(
      margin: EdgeInsets.only(top: 0.1),
      child: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        backgroundColor: Color.fromRGBO(30, 40, 54, 1.0),
        showSelectedLabels: false,
        showUnselectedLabels: false,
        items: <BottomNavigationBarItem>[
          _menuButton(Icons.home),
          _menuButton(Icons.search),
          _menuButton(Icons.notifications_none),
          _menuButton(Icons.mail_outline),
        ],
      ),
    );
  }

このメソッドでは、BottomNavigationBarでメニューを実装しています。
(Containerで囲われていますが、これはbody部分との境界をつけるためのマージンを指定するためです)

ポイントは、typeプロパティに指定したBottomNavigationBarType.fixedの部分で、これを指定することでボタンの位置が固定化されます。
また、showSelectedLabelsプロパティとshowUnselectedLabelsプロパティでボタンのラベルを非表示にしています。

ボタンの実装は_menuButtonメソッドに実装しました。

main.dart
  BottomNavigationBarItem _menuButton(IconData icon) {
    return BottomNavigationBarItem(
      icon: Icon(
        icon,
        color: Color.fromRGBO(139, 152, 164, 1.0),
      ),
      activeIcon: Icon(
        icon,
        color: Color.fromRGBO(76, 158, 235, 1.0),
      ),
      title: Text(''),
    );
  }

非選択状態のアイコンをiconプロパティに、選択状態にアイコンをactiveIconプロパティに指定したBottomNavigationBarItemを返しています。

次回

自身とフォロワーの投稿が表示される本文を実装予定です。

あとがき

文字色や背景色を各箇所で指定していますが、themeを使ってまとめた方がよさそうですね。
ただ、themeで各プロパティにColor.fromRGBOを使うとエラーになってしまう部分が解決できず・・・。
このあたりも解決次第投稿予定です。

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