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

【Human Interface Guidelines】Modality機能 和訳

モバイルアプリを設計する上で、モーダルウィンドウ機能についてiOSの推奨を理解しましょう。
Human Interface Guidelines」のModalityの章について紹介します。
モバイルアプリは、ユーザーの実現したい目的のために画面が遷移していくことが基本です。
(AppStoreでは特定のアプリ詳細ページに遷移して、アプリをインストールする目的のためなど)
しかしユーザーにメッセージを伝えるために、一時的にユーザーストーリー(想定する画面遷移)から離脱したい場合があります。
その際に、ユーザーに不快感を与えることなく、実装することが求められます。

前提

モーダルウィンドウ機能は、ユーザーの現在開いているコンテンツとは独立しており、一時的に表示するためのデザイン手法です。
そのため、モーダルウィンドウを閉じるための明示的なアクションが必要となります。
コンテンツをモーダル表示することにより、以下が実現します。

  • ユーザーがモーダル画面で完結するタスクや関連した設定項目に集中することが可能
  • ユーザーが重要な情報を確実に受け取り、必要に応じて次の行動を促すことが可能

iOSは利用用途別に「Alerts」、「Activity Views (Share Sheets)」、「Action Sheets」を提供しています。
アプリで独自のモーダルコンテンツを表示するために、iOS13以降では以下の表示スタイルをサポートしています。
iphone-Modality.png

Sheet

シート表示形式は、カードのように前のコンテンツを部分的に覆うように表示されます。
そのため、覆われていない部分は暗くすることで、操作を無効化することを明確にします。
カードで覆われていない部分は、前のコンテンツを表示しておくことで、中断しているタスクを思い出せるようにします。
シートの閉じ方は以下の方法があります。

  • 画面の上部から下にスワイプする
  • シートのコンテンツが一番上にスクロールされた状態で、任意の箇所から下にスワイプする
  • 閉じるボタンを押す 単純なタスクのみを一時的に行わせるために、前の操作内容を想起させるための非没入型コンテンツ表示に適しています。

Full Screen

全画面表示形式は画面全体を覆います。
前のコンテンツが完全に覆われてしまっているため、意識を散漫させずたり、資格的な混乱を最小限に抑えます。
ユーザーはボタンをタップすることでモーダル画面を閉じることができます。
ビデオ、写真やカメラなどの没入型コンテンツや、資料や写真の編集などの複雑な操作が必要な場合に適しています。

1. モーダル機能を適切に使用する

現在実行しているタスクとは異なるタスクを、ユーザーに意識を集中させるためにモーダル機能を使用します。
モーダル機能はユーザーの現在のタスクを無理やり離脱させて、表示する必要があるため、適切な使用用途で用いることが不可欠です。

2. 必須情報を提供するためのアラート機能

通常、何かが間違っているためにアラートが表示されます。
アラートはユーザーのタスクを中断するため、閉じるための手段を明確にして、モーダル表示が正当だとユーザーに感じさせることが重要です。

3. モーダル機能は、単純、早く、少なく

アプリは一つの目標を達成するために設計されます。
モーダル画面上に別のアプリを作成しないでください。
モーダル機能が複雑であると、前の中断しているタスクが何であったか見失う可能性があります。
ビューが階層的に複数重なりあっている場合は特に注意が必要です。
モーダル画面上にサブビューがある場合は、タスクを完了するまでの手順を明確化してください。
タスクを完了する以外の目的で「完了」ボタンを使用してはいけません。

4. モーダル画面には必ず「閉じる」ボタンを用意する

「完了」や「キャンセル」ボタンも同義です。
ボタンを表示することで、ユーザーの操作性が向上されたり、スワイプなどの画面を閉じる操作の代替として提供ができます。

5. 必要に応じて、モーダル画面を閉じる前に確認を表示する

モーダル画面上のデータの損失を回避することができます。
スワイプ操作や閉じるボタンのタップ操作に関わらず、操作によってユーザーの生成したコンテンツが失われる可能性がある場合は、状況の説明と解決方法を提示するためのシートを提示することを推奨します。
(写真編集後保存されていないまま、画面を閉じようとした場合など)

6. FullScreenモーダル画面の上にSheetモーダル画面を表示しない

ポップオーバー(FullScreen)画面内にカード(Sheet)画面を表示することは可能ですが、上に重ねて表示はしてはいけません。(アラートの場合を除く)
ポップオーバー画面で操作中に、カード画面の表示が必要な場合は、一度ポップオーバー画面を閉じるようにしてください。

7. モーダル画面の内容が分かるタイトルを表示する

モーダル画面が表示されると、前のコンテンツから切り替わるため、新しいコンテンツを明確にすることを推奨します。
タスク内容をしっかり説明するか、案内を画面内に提供するなどで対応します。

8. モーダル画面の外観をアプリと調整する

例えば、モーダル画面にナビゲーションバーが含まれる場合は、アプリのナビゲーションバーと同じデザインを使用する必要があります。

9. 表示形式を適切に選択する

アプリ全体の遷移方式を考慮し、適切な画面切り替え方式を使用します。
デフォルトの切り替え方式は、モーダル画面が下部から上下にスライドし、閉じた時に元に戻ります。
アプリ全体と一貫した遷移方式を採用するようにしましょう。

まとめ

画面遷移はユーザーの予期しない場合に、かなりストレスに感じてしまいます。
モーダル画面上でPush遷移をさせたり、モーダル画面上に更にモーダル画面が表示されるなど、不適切な利用は避けて、UXを向上させるために適切に利用することを心がけてみてください。

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

【iOS】Swiftファイルのヘッダーコメントをカスタマイズする

概要

iOSの開発をする際にXcodeでファイルを追加すると、ファイルのヘッダーに以下のようなコメントが入るかと思います。

Sample.swift
//
//  Sample.swift
//  SampleProject
//  
//  Created by ~~~ on 2020/12/30
//  Copyright © 2020 ~~~. All rights reserved.
//

複数人で開発をする際に、上記コメントを合わせたかったので、コメントをカスタマイズする方法についてまとめてみました。

環境

Xcode: 11.2.1
Swift: Swift5

手順1

「IDETemplateMacros.plist」を生成します。
(生成したファイルの配置場所は手順3で説明します)

手順2

手順1で生成したファイルに以下を追加します。
(サンプルでは作成者・作成元を非表示としました)

IDETemplateMacros.plist
<?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>FILEHEADER</key>
  <string>
//  ___FILENAME___
//  ___TARGETNAME___
//</string>
</dict>
</plist>

手順3

出来上がった「IDETemplateMacros.plist」をプロジェクトに追加します。
今回は複数人で開発することを想定し、Gitの管理下となるよう以下のディレクトリにファイルを追加しました。
<ProjectName>.xcodeproj/xcshareddata/IDETemplateMacros.plist
※「/xcshareddata」は「.gitignore」に入れていない前提です!

確認

Xcodeからファイルを追加し、以下のようになっていれば設定完了です。

Sample.swift
//
//  Sample.swift
//  SampleProject
//  

参考

https://dev.classmethod.jp/etc/ios-xcode%E3%81%AE%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E3%81%AE%E3%83%98%E3%83%83%E3%83%80%E3%83%BC%E3%82%B3%E3%83%A1%E3%83%B3%E3%83%88%E3%81%AE%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC/
https://help.apple.com/xcode/mac/9.0/index.html?localePath=en.lproj#/dev91a7a31fc

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

初心者のためのswift、Xcodeで画面遷移をする方法(アウトプット& 復習用) 

今回はxcodeでの画面遷移についてです
この情報はよく出回っていることだと思いますが、自分なりに書きました!
「なんで遷移するのか?」や、「なんでこのコード必要なの?」、などの説明は
この記事で説明しておりません!
画面遷移方法の一つを紹介する記事となっています!

Modalによる遷移

まずは最初にMainStorybordに遷移先のViewcontrollerをおきます!
(右上の十字マークのところから持ってきます)

次にcommand + N を押してCocoa Touch Classを選択します
スクリーンショット 2019-12-29 20.36.28.png

その次に遷移先のviewcontolloer.swiftの名前を変更します。
私は適当に次の遷移先なのでNextViewControllerにしました!
SubClassは今回はUIViewControllerですので、そこも注意してください!

スクリーンショット 2019-12-29 20.18.51.png

今回はボタンを押したら遷移するものとして
mainstoryboardの最初のviewcontrollerにボタンを起きましょう
自分はボタンの名前をNEXTにしました!
そしたらボタンをControl を押しつつ次の画面に引っ張って持っていきます。

スクリーンショット 2019-12-29 20.47.07.png

このようなことを言われるので、Present modallyを選びます!
showなど他のものもありますので、そちらは調べてみてください!
スクリーンショット 2019-12-29 20.49.52.png

すると、先ほどはviewControllerの間になかった矢印が出てきました!
これによりとりあえずボタンを押すとNextViewControllerには
遷移することができるようになりました!ビルドして試してみてください!
スクリーンショット 2019-12-29 20.49.57.png

ですが、このままでは戻ることができないので
今回はボタンで最初の画面に戻れるように
NextViewcontrollerにボタンを置いてみましょう!
わたしはボタンの名前を戻るに変更しました!
スクリーンショット 2019-12-30 12.51.00.png

そしたら、戻るボタンをIBActionでNextViewcontroller.Swiftのこーどに紐付けます!
下のこんな感じになってればOKです!

スクリーンショット 2019-12-30 12.53.37.png
しかし、これではただ紐付けが、行われただけですので戻るボタンが押されても
元の画面に戻ることはできません!
なので、コードで少しこのように書いてみましょう。

スクリーンショット 2019-12-30 13.03.22.png

書き終わりましたら、一回ビルドして
次の画面に遷移するか、元の画面に戻るか確かめてみてください。
どうですか?以外にも簡単に画面遷移ができましたよね?

今回はswift, Xcodeでの画面遷移方法の一つを紹介しましたが
他にも遷移する方法は他にもありますので、調べてみてください!

最後までありがとうございました!

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

アダプターをSwift5で実装する

※この記事は「全デザインパターンをSwift5で実装する」https://qiita.com/satoru_pripara/items/3aa80dab8e80052796c6 の一部です。

The Adapter(アダプター)

0. アダプターの意義

レガシーコード(古いコード)や、他者製のコード(外部ライブラリなど)を使う際、自分の書いたコードと実装形式などが違う事がある。そういったコードを用いる際、いちいち自分のコードと適合する形に直さなければならないとすると非常に手間がかかる。

アダプターパターンはそういったコードを自分のコードから扱いやすい形に修正してくれるデザインパターンである。

元のコードがどういった実装をしていたか? などをいちいち意識する事なく自分のコードに適合させられるため、時間と労力の節約につながる。

注意点としては、外部のコード等がもともと期待する機能を持っていないのにアダプターパターンを使って無理に他のコードと統合しようとしてはいけないという事である。それはアダプターの能力を超えている。そういったケースは単純に外部のコードの選定ミスなので、違うライブラリなど目的にあったコードを選ぶようにする。

1.アダプターを用いない場合

  • プロトコルPaymentGatewayに準拠している自前のクラスPayPalStripe
Payment.swift
import Foundation

public protocol PaymentGateway {
    func receivePayment(amount: Double)
    var totalPayments: Double {get}
}

public class PayPal: PaymentGateway {
    private var total = 0.0

    public func receivePayment(amount: Double) {
        self.total += amount
    }

    public var totalPayments: Double {
        print("Total payments received via PayPal: \(self.total)")
        return self.total
    }

    public init() {}
}

public class Stripe: PaymentGateway {
    private var total = 0.0

    public func receivePayment(amount: Double) {
        self.total += amount
    }

    public var totalPayments: Double {
        print("Total payments received via Venmo: \(self.total)")
        return self.total
    }

    public init() {}
}
  • PaymentGatewayに準拠していない外部のコード由来(という設定)のAmazonPaymentsクラス
ThirdPartyPayment.swift
import Foundation

//Third-party class that does not conform to PaymentGateway protocol.
public class AmazonPayments {
    public var payments = 0.0

    public func paid(value: Double, currency: String) {
        self.payments += value
        print("Paid \(currency)\(value) via Amazon Payments")
    }

    public func fulfilledTransactions() -> Double {
        return self.payments
    }

    public init() {}
}
  • AmazonPaymentsクラスだけは、他のクラスと一緒に処理する事ができない
PaymentGateways.playgorund
let paypal = PayPal()
paypal.receivePayment(amount: 100)
paypal.receivePayment(amount: 200)
paypal.receivePayment(amount: 499)

let stripe = Stripe()
stripe.receivePayment(amount: 5.99)
stripe.receivePayment(amount: 25)
stripe.receivePayment(amount: 9.99)

let amazonPayments = AmazonPayments()
amazonPayments.paid(value: 120, currency: "USD")
amazonPayments.paid(value: 74.99, currency: "USD")

var paymentGateways: [PaymentGateway] = [paypal, stripe]//AmazonPaymentsクラスは配列に追加する事ができない

var total = 0.0
paymentGateways.forEach {total += $0.totalPayments}
print(total)

2. アダプターの実装

  • PaymentGatewayプロトコルに準拠したAmazonPaymentsAdapterクラスを作成し、AmazonPaymentsクラスを処理させる
Adapter.swift
import Foundation

public class AmazonPaymentsAdapter: PaymentGateway {
    public func receivePayment(amount: Double) {
        self.amazonPayments.paid(value: amount, currency: "USD")
    }

    public var totalPayments: Double {
        let total = self.amazonPayments.payments
        print("Total payments received via Amazon Payments: \(total)")
        return total
    }

    //AmazonPaymentsクラスを変数として持ち、扱いやすいように処理する。
    //外部からのアクセスを防ぐため、private修飾子をつける。
    private let amazonPayments = AmazonPayments()

    public init() {}
}
  • 使用例
PaymentGateways.playground
let paypal = PayPal()
paypal.receivePayment(amount: 100)
paypal.receivePayment(amount: 200)
paypal.receivePayment(amount: 499)

let stripe = Stripe()
stripe.receivePayment(amount: 5.99)
stripe.receivePayment(amount: 25)
stripe.receivePayment(amount: 9.99)

//アダプターを使用する事で、AmazonPaymentsクラスには直接触らなくて済む
let amazonPaymentsAdapter = AmazonPaymentsAdapter()
amazonPaymentsAdapter.receivePayment(amount: 120)
amazonPaymentsAdapter.receivePayment(amount: 74.99)

var paymentGateways: [PaymentGateway] = [paypal, stripe, amazonPaymentsAdapter]//アダプターを介して、AmazonPaymentsクラスを自作クラスと同じ型として扱えるようになった。

3. extensionを用いたアダプター

Swiftの機能としてextensionというものがある。これは、既存のクラスに自作の変数や関数などを追加できるというSwiftの機能である。

※詳細は
https://www.sejuku.net/blog/33334
https://docs.swift.org/swift-book/LanguageGuide/Extensions.html
などを参照

さらには、extensionを使って、今まで準拠していなかったプロトコルに準拠させるということもできる。

アダプターパターンの実現に当たって、より直感的な実装が可能となる。

本プログラムにおいては、PaymentGatewayプロトコルに準拠していなかったため扱いづらかったAmazonPaymentsクラスを、extensionにより準拠させる事ができる。

AmazonPaymentsAdapterというアダプタークラスを別に作る必要がないため、非常に便利と言える。

ThirdPartyPayment.swift
//extensionによりPaymentGatewayプロトコルに準拠させる。
extension AmazonPayments: PaymentGateway {
    public func receivePayment(amount: Double) {
        self.paid(value: amount, currency: "USD")
    }

    public var totalPayments: Double {
        let total = self.payments
        print("Total payments received via Amazon Payments: \(total)")
        return total
    }
}
PaymentGateways
let paypal = PayPal()
paypal.receivePayment(amount: 100)
paypal.receivePayment(amount: 200)
paypal.receivePayment(amount: 499)

let stripe = Stripe()
stripe.receivePayment(amount: 5.99)
stripe.receivePayment(amount: 25)
stripe.receivePayment(amount: 9.99)

let amazonPayments = AmazonPayments()
amazonPayments.receivePayment(amount: 120)
amazonPayments.receivePayment(amount: 74.99)

//AmazonPayments型を自作クラスと同じ型の配列に入れられるようになった
var paymentGateways: [PaymentGateway] = [paypal, stripe, amazonPayments]

https://github.com/Satoru-PriChan/AdapterPaymentGatewayDemo

参考文献: https://www.amazon.com/Design-Patterns-Swift-implement-Improve-ebook/dp/B07MDD3FQJ

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

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Facade~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Facadeパターン概要

  • 煩雑なモノの窓口(=Facade、ファサード)を作ることでシンプルに扱えるようにするパターンです。
  • Facadeクラスは複雑な実装は持たず、(大抵は複数の)モジュールに対する問合せ窓口としての機能だけを提供します。
  • GoFのデザインパターンでは構造に関するパターンに分類されます。

使い所

アプリ開発の実務で適用できるケースはたくさんあると思いますが、例えば

  • UserDefaultsの読み込み・書き込み とか
  • KeyChainの読み込み・書き込み とか

サンプルコード

Swiftバージョンは 5.1 です。

// Example
final class Defaults {

    private let defaults: UserDefaults

    init(defaults: UserDefaults = .standard) {
        self.defaults = defaults
    }

    subscript(key: String) -> String? {
        get {
            return defaults.string(forKey: key)
        }

        set {
            defaults.set(newValue, forKey: key)
        }
    }
}

// Usage
let storage = Defaults()

// Store
storage["Bishop"] = "Disconnect me. I’d rather be nothing"

// Read
storage["Bishop"]

引用:
https://github.com/ochococo/Design-Patterns-In-Swift#-fa%C3%A7ade

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

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Abstract Factory~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Abstract Factoryパターン概要

  • オブジェクトの生成を抽象化することにより、関連/依存する複数のオブジェクト生成を一括して提供するためのパターンです。
  • Factory Methodと類似していますが、主な相違点は上記の太字部分です。
  • オブジェクト生成の責務を担う側を「抽象的な工場」に見立てて、Abstract Factoryと呼びます。
  • 利用側は「工場」に対して生成条件を提示するわけですが、Swiftの場合はenumを上手く使うと良さそうです。
  • GoFのデザインパターンでは生成に関するパターンに分類されます。

使い所

  • テスト時はDBの処理をモック化するとか
  • (iOSアプリではあまりないと思いますが)マルチデータベース対応 とか

サンプルコード

Swiftバージョンは 5.0 です。

// Protocols
protocol DatabaseConnection {
    var connection: String { get }
}

protocol DatabaseAccessible {
    func select()
    func insert()
    func delete()
    func update()
}

protocol DatabaseProvider {
    func makeConnection() -> DatabaseConnection
    func makeAccessor() -> DatabaseAccessible
}

// Class
final class MockConnection: DatabaseConnection {
    let connection = "MockConnection"
}

final class MockAccessor: DatabaseAccessible {
    func select() { print("ダミーを返す") }
    func insert() { print("何もしない") }
    func delete() { print("何もしない") }
    func update() { print("何もしない") }
}

final class ProductionConnection: DatabaseConnection {
    let connection = "ProductionConnection"
}

final class ProductionAccessor: DatabaseAccessible {
    func select() { print("実際に読み込む") }
    func insert() { print("実際に追加する") }
    func delete() { print("実際に削除する") }
    func update() { print("実際に更新する") }
}

// Abstract factory
enum DatabaseFactoryType: DatabaseProvider {
    case mock
    case production

    func makeConnection() -> DatabaseConnection {
        switch self {
        case .mock:
            return MockConnection()
        case .production:
            return ProductionConnection()
        }
    }

    func makeAccessor() -> DatabaseAccessible {
        switch self {
        case .mock:
            return MockAccessor()
        case .production:
            return ProductionAccessor()
        }
    }
}

// Usage
let mockFactory = DatabaseFactoryType.mock
let mockConnection = mockFactory.makeConnection()
print(mockConnection.connection)   // "MockConnection"
let mockAccessor = mockFactory.makeAccessor()
mockAccessor.select()  // "ダミーを返す"

let productionFactory = DatabaseFactoryType.production
let productionConnection = productionFactory.makeConnection()
print(productionConnection.connection)   // "ProductionConnection"
let productionAccessor = productionFactory.makeAccessor()
productionAccessor.select()  // "実際に読み込む"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Factory Method~

Factory Methodパターン概要

  • 生成するインスタンスを条件に応じて柔軟に切り替えるためのパターンです。
  • Javaなどの場合は抽象クラスで振る舞いとインスタンス生成の雛形を定義し、サブクラスでそれらをオーバーライドします。(参照:Wikipedia
  • Swiftの場合は抽象クラスという概念がありませんし、enumの表現力が強力なので、enumをうまく使うと良さそうです。
  • GoFのデザインパターンでは生成に関するパターンに分類されます。

使い所

アプリ開発の実務で適用できるケースはたくさんあると思いますが、例えば

  • 料金体系の違う「通常会員」と「優待会員」のオブジェクト生成を切り替える、とか
  • テスト用targetの場合はモックオブジェクトを生成する、とか

サンプルコード

Swiftバージョンは 5.0 です。

protocol CurrencyDescribing {
    var symbol: String { get }
    var code: String { get }
}

final class Euro: CurrencyDescribing {
    var symbol: String {
        return "€"
    }

    var code: String {
        return "EUR"
    }
}

final class UnitedStatesDolar: CurrencyDescribing {
    var symbol: String {
        return "$"
    }

    var code: String {
        return "USD"
    }
}

enum Country {
    case unitedStates
    case spain
    case uk
    case greece
}

enum CurrencyFactory {
    static func currency(for country: Country) -> CurrencyDescribing? {
        switch country {
            case .spain, .greece:
                return Euro()
            case .unitedStates:
                return UnitedStatesDolar()
            default:
                return nil
        }
    }
}

// Usage
print(CurrencyFactory.currency(for: .greece)?.symbol ?? "")   // "€"
print(CurrencyFactory.currency(for: .spain)?.symbol ?? "")    // "€"
print(CurrencyFactory.currency(for: .unitedStates)?.symbol ?? "") // "$"

引用:
https://github.com/ochococo/Design-Patterns-In-Swift#-factory-method

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

【Carthage】failed with exit code 128: fatal: could not read Username for 'https://github.com': terminal prompts disabled

概要

Carthageでレポジトリを指定して、

carthage update --platform ios

を実行すると

A shell task (/usr/bin/env git fetch --prune --quiet https://github.com/Qiuck/Nimble.git refs/tags/*:refs/tags/* +refs/heads/*:refs/heads/* 
(launched in /Users/[user_name]/Library/Caches/org.carthage.CarthageKit/dependencies/Nimble)) failed with exit code 128:
fatal: could not read Username for 'https://github.com': terminal prompts disabled

というエラーが出た。

解決策

解決策ってほどのものでもなく、 Cartfileのレポジトリ名がtypoってた だけでした。

もうちょっと分かりやすいエラーメッセージにしてもらえませんかね...。

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