20191221のSwiftに関する記事は18件です。

SwiftUIでgit-blame-pr.plをMacアプリにしてみた

この記事は、ZOZOテクノロジーズ #4 AdventCalendar2019の記事です。
昨日は@satto_sannさんの「FastAPIの負荷実験環境を作ってみる」でした。

さて、あるコードが、どういった背景や意図で変更されたのかを知りたいことは、よくあることだと思います。そういった場合にgit-blame-pr.plを使うと、そのコードがどのプルリクエストで変更されたかを簡単に知ることができます。

これはCLIでそのまま使うだけでも十分便利です。
ただ使用頻度が高いこともあり、ユースケースに沿ったGUIが欲しくなりました。
そこで、git-blame-pr.plが出力した結果の行をクリックするだけで、該当のWebページに遷移できるMacアプリを作ってみました。

出来上がったものは下記のリポジトリで公開しています。
https://github.com/maoyama/GitBlamePR
Dec-20-2019 23-50-30

GitHubのリリースから最新のGitBlamePR.app.zipをダウンロード、解凍するだけで使えます。ぜひ試してみてください。
※macOS Catalinaが必要です。

実装内容の紹介

UIはAppKitではなくSwiftUIで実装しています。
以下のコードがUIにおいては全てです。SwiftUIを使っているので、Storyboardやxibファイルもありません。

struct GitBlamePRView: View {
    var model: GitBlamePRViewModel
    var textOnCommit: (String) -> Void

    @State private(set) var fullPath: String = ""

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            TextField(
                "Enter full path",
                text: $fullPath,
                onEditingChanged: {_ in },
                onCommit: {
                    self.textOnCommit(self.fullPath)
                }
            ).lineLimit(1)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            Divider()
            ScrollView(.vertical, showsIndicators: true) {
                VStack(alignment: .leading) {
                    if !model.error.isEmpty {
                        Text(model.error)
                    }
                    ForEach(model.lines, id: \.id) { line in
                        HStack(alignment: .top, spacing: 12) {
                            Text(line.message)
                                .font(Font.system(.caption, design: .monospaced))
                                .foregroundColor(.accentColor)
                                .frame(width: 80, height: nil, alignment: .trailing)
                                .onTapGesture {
                                    NSWorkspace.shared.open(line.url)
                                }
                            Text(line.code)
                                .font(Font.system(.caption, design: .monospaced))
                                .frame(width: nil, height: nil, alignment: .leading)
                        }
                    }
                    HStack {
                        Spacer()
                        EmptyView()
                    }
                }.padding()
            }.background(Color(NSColor.textBackgroundColor))
        }
    }
}

「Better apps. Less code.」とAppleが謳っている通り、とても少ないコードで実現できています。
またSwiftUIは、全てのAppleプラットフォームで使えます。そのため、僕のような普段はiOSアプリの開発をしているプログラマでも、macOSで動くアプリを作る敷居がとても低くなりました。

この記事は以上です。
明日は@alpha_gotoさんです。お楽しみに!

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

iOS13の設定アプリにつまずいた

はじめに

みなさんご存知かと思いますが iOS13 は設定アプリのクラッシュ祭りでした:confetti_ball:

設定アプリをクラッシュさせるためのアップデートといっても過言ではありません!!:expressionless:

某マッチングアプリの話

iOS13.2.3 で某マッチングアプリがインストールされていると設定アプリがクラッシュするという話もありました。

参考

【iPhone】設定が開けない、落ちる問題がiOS13.2.3で報告 Pairsの削除で改善する事例も

こちらは iOS13.2.3 から各アプリ情報を先読みするようになり某アプリの plist ファイルの値不正(型不正?)によって設定アプリを開くと数秒後にクラッシュするようになっていたようです。

私も調査のためインストールしてみたところ無事クラッシュしました:expressionless:

こちらは某アプリの値不正が原因でしたが、サードパーティ製のアプリが設定アプリに影響するというのは中々のバグなような気もします。(なんか色々悪いことできそう:smiling_imp:

某アプリのバグはすぐに修正され今はもうクラッシュしません:innocent:

みんな大好きLicensePlistの話

ライセンス表示がめっちゃ楽なみんな大好き LicensePlist も iOS13 の設定アプリでクラッシュするというのに遭遇していました:disappointed_relieved:(たぶん全部 iOS 側のバグ:speak_no_evil:)

13

押したらクラッシュ!!

とりあえずLicensePlistの各バージョンについて動作を確認してみました。Root.plist の記載は下記。

Root.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
    <key>PreferenceSpecifiers</key>
    <array>
        <dict>
            <key>Type</key>
            <string>PSGroupSpecifier</string>
            <key>FooterText</key>
            <string>Copyright</string>
        </dict>
        <dict>
            <key>Type</key>
            <string>PSChildPaneSpecifier</string>
            <key>Title</key>
            <string>Licenses</string>
            <key>File</key>
            <string>com.mono0926.LicensePlist</string>
        </dict>
        <dict>
            <key>Type</key>
            <string>PSTitleValueSpecifier</string>
            <key>DefaultValue</key>
            <string>1.0.0</string>
            <key>Title</key>
            <string>Version</string>
            <key>Key</key>
            <string>sbVersion</string>
        </dict>
    </array>
    <key>StringsTable</key>
    <string>Root</string>
    </dict>
</plist>

LicensePlist 2.6.0

これ 2.6.0: SwiftPM (Swift Package Manager) Support

2.7.1: iOS 13 support の前のバージョンです。(2.7.0入れたかったんですが pod install が失敗するのであきらめました:confused:

iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3
:o: :x: :o: :o: :o:

なんかたまにへん(gif わかりにくい場合この Issue の this behavior に動画がありました。)

260

LicensePlist 2.7.1

これ 2.7.1: iOS 13 support

iOS13 でのクラッシュを受けすぐに有志が動きました。(さすがは人気ライブラリ:clap:
生成される plist ファイルに Type: PSGroupSpecifier を加えたらいけるんじゃないか?とのこと。

iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3
:o: :x: :x: :x: :o:

13.1 と 13.2.2 はライセンス詳細から一覧に戻るとクラッシュ!!!

LicensePlist 2.10.0(2019/12/21時点で最新)

これ 2.10.0: Add --single-page option

iOS13 対応後もクラッシュの報告は続き対応されたのがこのバージョン。その後も有志が調査してどうやら3階層(Licenses -> ライブラリ一覧 -> ライセンス情報)push だとクラッシュするとのことでライブラリ一覧を削除した Licenses -> ライセンス情報の2階層にするオプション --single-page が追加されました。

こんな感じ

setting

--single-page なし

iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3
:o: :x: :o: :x: :o:

--single-page あり

iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3
:o: :x: :o: :o: :o:

クラッシュしていた原因の詳細はわかりませんが、クラッシュログにやたらと SwiftUI とあったのでそのへんなんだと思います...:neutral_face:

このあたりのバグは iOS13.3 で無事修正されたそうです:clap:

133

クラッシュしないけど一度表示して戻ると License を押しても画面遷移しない...(まあクラッシュしないし一回目はちゃんとライセンスがみれるのでOK:innocent:

さいごに

LicensePlist はもう iOS のアップデートを待つしかないと思ってましたが対応されてよかったです:laughing:
(某アプリは今後もクラッシュしないか調査を続行しようと思います:neutral_face:

ぎりぎりセーフ:mask:

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

Firebaseで超簡単にiOSアプリを配布する?

今回はFirebaseのApp Distributionを使って手動でアプリを配布する方法を紹介いたします

準備するもの

  • Firebaseプロジェクトの登録
  • Apple Developer Programの登録
  • 共有するメンバーのデバイスをDeveloperアカウントに登録
  • 共有するメンバーのメールアドレス
  • iOS DistributionタイプのCertificates登録
  • AdHoc用のProvisioning Profile登録

アプリのipaファイルを取得

アプリのアーカイブを生成

まずは、配布するアプリのビルドをipaとしてアーカイブします。既にアーカイブしている場合はOrganizerからアーカイブを選択します。
スクリーンショット 2019-12-21 21.04.18.png

アーカイブからipaファイルを生成

AdHocを選択し、手順にそってipaを作成していきます。Select Certificates&ProfileではiOS DistributionタイプのCertificatesとAdHoc用のProvisioning Profileを選択し作成します。

スクリーンショット 2019-12-21 21.09.42.png

ipaファイルをFirebaseにアップロード

ドラッグ&ドロップまたは参照を押してFirebaseにipaファイルをアップロードします.

スクリーンショット 2019-12-21 21.29.21.png

次に共有するメンバーのメールアドレスを登録します。なお、この共有するメンバーのデバイスはApple Developer ProgramのDevicesに登録しておく必要があります。
スクリーンショット 2019-12-21 21.31.36.png

リリースノートを記入して配信しましたボタンを押すとメンバーにFirebaseからメールが届きます。

スクリーンショット 2019-12-21 21.32.32.png

アプリをデバイスにインストール

Download tha latest buildをタップしてProfileをInstallします。

IMG_0640.jpg

Safariでリンクを開きDownloadをタップします

IMG_0641.jpg

次のようなポップアップダイアログが出るのでInstallを押してデバイスにProfileをインストールしていきます。

IMG_0642.jpg

下記の手順の通り設定に移動しProfileをインストールします。

IMG_0643.jpg

ProfileをインストールするとApp Distributionのアプリが追加させていることが確認できます。

IMG_0646.jpg

アプリをタップすると配信したVersionsの一覧が表示されるので任意のVersionをタップしアプリをダウンロードします。

IMG_0645.jpg

アプリがダウンロードできてるのを確認できましたね!
IMG_0647.jpg

最後に

今回はiOSをアプリを手動で配布する方法について紹介しましたが、FastlaneとFirebaseCLIでアップロードを行うと配布を自動化することができるので、次はFastlaneでの配信方法を紹介したいと思います?

公式: https://firebase.google.com/docs/app-distribution/ios/distribute-console?authuser=2

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

Firebase StorageでMetadataを使う

はじめに

こんにちは。
沖縄でエンジニアをしている @yuji_azama です。

Firebase Storage、いろんなデータを手軽に保存できて便利ですよね。

Firebase Storage にデータをアップロードするとき、Metadata を指定できることをご存知ですか?
この記事では、ちゃんと使うと便利な Firebase Storage の Metadata について説明します。

※サンプルコードは Swift で記述します。

Firebase Storage Metadata とは

Firebase Storage で扱うデータには、Metadata と呼ばれるそのデータに関する付加的な情報(ファイルサイズや contentType など)を含めることができます。

メタデータを設定することで、検索性が良くなったり、Firebase console 上での可視性(イメージプレビュー)が良くなったりします。

実際に使ってみるのがわかりやすいので、さっそく解説していきます。

Metadata を使わないとどうなるか

まずは Metadata を使用せずに JPEG 画像を保存してみます。

let storage = Storage.storage()
let storageRef = storage.reference()
let ref = storageRef.child("image.jpg")
let uploadImage = imageView.image!.jpegData(compressionQuality: 1.0)! as NSData

ref.putData(uploadImage as Data, metadata: nil) { _, error in
    if (error != nil) {
        print("upload error!")
    } else {
        print("upload successful!")
    }
}

上記を実行すると、Firebase console 上では以下のように確認できます。

image.png

ファイル名を選択することで、画面右側にファイルの詳細が表示できます。
先ほどアップロードした image.jpg のタイプが application/octet-stream となっており、JPEGのファイルとして認識されていないことがわかります。

ファイルを保存する目的だけであればこれでも問題はありませんが、ファイルの数が増えてくると console 上で一目で何のファイルなのか確認できないのは何かと不便です。

Metadata を使ってみる

それでは、Metadata を用いてファイルのタイプを指定してみます。
Metadata を使うには、StorageMetadata クラスを使用します。
StorageMetadata をインスタンス化したら、contentTypeimage/jpeg を設定し、putData メソッドの引数に指定します。

let storage = Storage.storage()
let storageRef = storage.reference()
let ref = storageRef.child("image.jpg")
let uploadImage = imageView.image!.jpegData(compressionQuality: 1.0)! as NSData

let metadata = StorageMetadata()
metadata.contentType = "image/jpeg"

ref.putData(uploadImage as Data, metadata: metadata) { _, error in
    if (error != nil) {
        print("upload error!")
    } else {
        print("upload successful!")
    }
}

上記を実行し、Firebase console 上で確認してみましょう。
以下の様に、ファイルの詳細を表示するとタイプに image/jpeg が設定されていることがわかります。
それにともなって、画像ファイルのプレビューも表示されているため、ファイルの可視性が向上したと思います。

image.png

おわりに

この様に、metadata を適切に設定することで Firebase Storage が少し使いやすくなります。
他にも様々な metadata があったり、カスタムメタデータを設定したりできます。
メタデータ利用についてはまだあまり情報がないので、効果的な使い方がどんどん出てくると良いですね。

参考

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

SwiftUIにおけるForEach内からのBindingオブジェクトの渡し方

はじめに

SwiftUIでリストコンテンツを表示するような画面を作る際に、ListForEach等のビューを使用する場面があるかと思います。

下のコードでは、RankingListというリストビューを持つ画面が、自身のviewModelmovies配列の値を基に、リスト内の各行のビューRankingRowを生成しています。

class RankingViewModel: ObservableObject {
    @Published var movies: [Movie]
    ...
}

struct RankingList: View {
    @ObservedObject var viewModel: RankingViewModel

    var body: some View {
        List {
            RankingHeader(&viewModel.genre)
            ForEach(viewModel.movies) { movie in
                RankingRow(movie: movie)
            }
        }
    }
}

この方法でRankingRowに渡しているのはmovies配列の要素のコピーとなります。

ForEach(viewModel.movies) { movie in
    RankingRow(movie: movie) // Movieオブジェクトのコピーを渡している
}

RankingRowMovie型の実体を受け取る設計であれば何ら問題ありませんが、もしRankingRowMovieBindingオブジェクトを受け取りたいとなった場合にはどうすればいいでしょうか。

ForEachのイニシャライザ

次のようにviewModel.moviesBindingオブジェクトをそのままForEachに渡したいところですが、残念ながらできません。

ForEach($viewModel.movies) {...} // NG: Binding<[Movie]>を渡そうとしている
ForEach(viewModel.$movies) {...} // NG: Published<[Movie]>.Publisherを渡そうとしている

定義を見てみると分かりますが、ForEachの3つのイニシャライザはRandomAccessCollectionに準拠したオブジェクトか、もしくはRange<Int>型の値しか受け取らないためです。

struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable {

    // 1
    init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content)

    // 2
    init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content)

    // 3
    init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content)
}
// ※ 一部抜粋

何らかの配列を渡す方法を別途考えてみます。

Array.indices

SwiftのArrayには、自身の要素のインデックス範囲を返すindicesプロパティがあります。

@inlinable public var indices: Range<Int> { get }

Range<Int>は上記3のinit(_, content:)に渡すことのできる型ですが、Range自体がRandomAccessCollectionに適合しているため、つまり上記1、2のイニシャライザにも渡すことができます。

今回はこの値を上記2のinit(_, id:, content:)に渡すことで当初の問題を解決します。

ForEach(viewModel.movies.indices, id: \.self) {...}

まとめ

class RankingViewModel: ObservableObject {
    @Published var movies: [Movie]
    ...
}

struct RankingList: View {
    @ObservedObject var viewModel: RankingViewModel

    var body: some View {
        List {
            RankingHeader(&viewModel.genre)
            ForEach(viewModel.movies.indices, id: \.self) { movie in
                RankingRow(movie: self.$viewModel.movies[index]) // OK: Binding<Movie>を渡している!
            }
        }
    }
}

ForEach内で各要素のindexを受け取ることができるようになった為、viewModel.movies配列の各要素を子ビューにバインディングして渡せるようになりました。


注意点として、ForEach生成時の引数に忘れずにid:を指定し、上記2のinit(_, id:, content:)を呼び出す必要があります。

引数id:を指定しない場合、上記3のinit(_, content:)を呼び出してしまうのですが、このイニシャライザはImmutableRangeオブジェクト向けとなっており、Rangeオブジェクトに変化があったとしてもSwiftUIはその変更をキャッチしてビューの更新を行ってくれません。

id:を指定せずにRange<Int>を渡した場合、初期化後のオブジェクトの更新時に以下のようなログが出力されます。

ForEach<Range<Int>, Int, RankingRow> count (3) != its initial count (0). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!

この警告が表示された際は、今一度id:を付け忘れていないか確認してみましょう。

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

DKImagePickerControllerで写真選択画面をカスタマイズしたい!

自己紹介

はじめまして!社会人1年目でiosアプリ開発を仕事で行っている駆け出しエンジニアです。勉強したことを発信したくて投稿を始めました。至らぬ部分が多々ありますが、参考にしていただけると幸いです笑

環境

  • macOS Catalina(10.15.1)
  • xcode(11.2.1)
  • Swift5
  • CocoaPods(1.8.4)

DKImagePickerControllerって何??

DKImagePickerControllerは簡単に言うと写真撮影写真選択が簡単に実装できる便利なライブラリです。撮影に関しては他にも標準のAVFoundation.framework、写真選択にはUIImagePickerControllerなどがあります。撮影に関しては標準のフレームワークの方が柔軟性がありますが、選択は複数選択できるメリットがあるのでこちらを利用すると便利です。

※事前にxcodeで新規のプロジェクトを作成しておいてください。

物は試し、早速簡単なアプリを作成しながら紹介していきます!

写真選択画面 写真選択画面のアラート表示
A583DF4F-E45A-4739-A06F-42B9324C580E_1_102_o.jpeg A921ACDA-B78F-4287-8E9C-6671F2B60742_1_102_o.jpeg

1.CocoaPodsでDKImagePickerControllerを導入

ターミナルで以下のように入力してください。(CocoaPodsが既にある方は読み飛ばしてください。)

sudo gem install cocoapods

インストールが完了したら、以下のように入力してください。

pod setup

次に、作成したプロジェクトまで移動して、以下のように入力してください。

pod init

ここまでで、podfileを作成することができました!このファイルは簡単にいうと、自分のプロジェクトにあるライブラリの一覧表みたいなものです。
次に、今作成したpodfileを開いてください。

target 'アプリ名' do
pod 'DKImagePickerController'  
end  
target 'アプリ名Tests' do  
end    
target 'アプリ名UITests' do  
end

1行目の「target 'アプリ名' do」の下に「pod 'DKImagePickerController'」と記述して、保存してください。(※わかりやすくするために、初期のpodfileとは少し変えてあります。)

最後に、ターミナルで以下のように入力してください。

pod install

これで、DKImagePickerControllerを自分のプロジェクトに導入することができました!!

2.写真選択画面の作成

次に写真選択画面の作成をします!

予め用意されているViewController.swiftを開き、DKImagePickerControllerをインポートし、継承してください。

ViewController.swift
import UIKit
import DKImagePickerController

class ViewController: DKImagePickerController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

実は、たったこれだけて写真選択画面が作れてしまうんです笑。
しかし、今回はカスタマイズすることが目的なので、もう少しコードを加えていきます!

ViewController.swift
class ViewController: DKImagePickerController {
    override func viewDidLoad() {
        super.viewDidLoad()

        //選択できる写真の最大数を指定
        self.maxSelectableCount = 3

        //カメラモード、写真モードの選択
        self.sourceType = .photo

        //キャンセルボタンの有効化
        self.showsCancelButton = true

        //UIのカスタマイズ
        self.UIDelegate = CustomUIDelegate()
    }
}
CustomUIDelegate.swift
import Foundation
import DKImagePickerController

class CustomUIDelegate: DKImagePickerControllerBaseUIDelegate {
    //右上のdoneボタンのカスタマイズ
    override func createDoneButtonIfNeeded() -> UIButton {
        let button = UIButton(type: UIButton.ButtonType.custom)
        button.setTitle("選択", for: .normal)
        button.setTitleColor(UINavigationBar.appearance().tintColor ?? self.imagePickerController.navigationBar.tintColor, for: .normal)
        return button
    }

    //写真選択超過時のアラートのカスタマイズ
    override func imagePickerControllerDidReachMaxLimit(_ imagePickerController: DKImagePickerController) {
        let alert = UIAlertController.init(title: "注意", message: "これ以上選択できません!", preferredStyle: .alert)
        let okAction = UIAlertAction(title: "OK", style: UIAlertAction.Style.cancel, handler: nil)
        alert.addAction(okAction)
        imagePickerController.present(alert, animated: true, completion: nil)
    }

    //cancelボタンのカスタマイズ
    override func imagePickerController(_ imagePickerController: DKImagePickerController, showsCancelButtonForVC vc: UIViewController) {
        let cancelButton = UIBarButtonItem()
        cancelButton.title = "戻る"
        cancelButton.style = .plain
        cancelButton.action = #selector(imagePickerController.dismiss as () -> Void)
        cancelButton.target = imagePickerController
        vc.navigationItem.leftBarButtonItem = cancelButton
    }
}

これらのコードで上記の写真のような写真選択画面が作れます!
アプリを実機で確認してもらいたいのですが、その前にカメラ機能に関する許可をinfo.plistに追加しなければいけません。以下のように項目を追加してください。valueの部分は実際表示される文言なのでなんでも構いません。

key value
Privacy - Camera Usage Description カメラ撮影を許可します。
Privacy - Photo Library Usage Description 写真アクセスを許可します。

補足

1点、難しいポイントとして以下のコードがあります。

//UIのカスタマイズ
self.UIDelegate = CustomUIDelegate()

このライブラリではUIDelegateの定義を以下のように行っています。

    /// Use UIDelegate to Customize the picker UI.
    @objc public var UIDelegate: DKImagePickerControllerBaseUIDelegate! {
        willSet {
            newValue?.imagePickerController = self
        }
    }

①DKImagePickerControllerBaseUIDelegateを継承したクラスを用意し、カスタマイズしたいメソッドをオーバーライドする。
②そのクラスのインスタンスをUIDelegateプロパティにセットする。

コメントにもあるように、UIDelegateをこのように使うことで写真選択画面のカスタマイズを行うことができます!
気になる方は、以下のURLを参考にしてみてください!(https://github.com/zhangao0086/DKImagePickerController/tree/develop/Example/DKImagePickerControllerDemo/CustomUIDelegate)

最後に

ここまで読んでいただきありがとうございます。これから積極的に記事を投稿していくので、みなさんのアプリ開発の参考に少しでもなるように努力していきます!

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

Swift MkMapViewで地図アプリ作成してみた(12)- ロングタップした位置と現在位置の標高差をピンに表示する

記事一覧

Swift MkMapViewで地図アプリ作成してみた(記事一覧)

国土地理院から現在位置の標高を取得する

国土地理院のWebAPIから標高を取得する方法の詳細は、「ロングタップした位置の標高を国土地理院から取得する」を参照してください。

  1. URLを生成する

    国土地理院から現在位置の標高をJSONで取得するためのURLを作成する。

    ViewController.swift:URLを生成する
    // 国土地理院のURL
    let baseUrl = "https://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?"
    // 現在位置でクエリを設定する
    let lonUrl = "&lon=" + mapView.userLocation.coordinate.longitude.description
    let latUrl = "&lat=" + mapView.userLocation.coordinate.latitude.description
    // アウトプット形式をJSONに設定する
    let outtypeUrl = "&outtype=JSON"
    // URLとクエリを連結
    let listUrl = baseUrl + lonUrl + latUrl + outtypeUrl        
    // URLを生成する
    guard let url = URL(string: listUrl) else { return }
    

    現在位置を取得するで現在位置受信を有効にしていれば、mapView.userLocationに現在位置が自動で設定されている。

  2. 標高を取得する

    URLSessionで生成したURLにアクセスして標高を取得する。

    ViewController.swift:標高を取得する
    // URLを生成する
    guard let url = URL(string: listUrl) else { return }
    URLSession.shared.dataTask(with: url) { (data, response, error) in
        if error != nil {
            print(error!.localizedDescription)
        }
        guard let data = data else { return }
        // JSONを取得する
        let json = try? JSONDecoder().decode(JsonElevation.self, from: data)
        if nil != json {
            // mainスレッドで処理する
            DispatchQueue.main.async {
                // JSONから標高を取得する
                self.currentElevation = (json?.elevation)!
            }
        }.resume()
    }
    

    サブスレッドからメインスレッドに切り替えるためには、DispatchQueue.main.asyncを使用する。

ロングタップした位置と現在位置の標高差をピンに表示する

  1. 標高差を計算する

    ロングタップした位置の標高を国土地理院から取得するで取得した標高との差を

    ViewController.swift:標高差を計算する
    self.lblDiffElevation = "高低差:" + (round(((self.longTapElevation - self.currentElevation)*10))/10).description + " m"
    // ロングタップしたアノテーション情報を更新する
    updateLongTapPointAno()
    

    少数第1桁となる様に、"*10))/10"としている。

  2. 標高差をピンに表示する

    ロングタップした位置にピンを立て距離を表示するで設定したピンのタイトルに連結する。

    ViewController.swift:標高差をピンに表示する
    // ロングタップしたアノテーション情報を更新する
    func updateLongTapPointAno() {
        // 現在位置とタッウプした位置の距離(m)を算出する
        let distance = calcDistance(mapView.userLocation.coordinate, pointAno.coordinate)
    
        // ピンに設定する文字列を生成する
        var str:String = Int(distance).description
        str = str + " m"
    
        // yard
        let yardStr = Int(distance * 1.09361)
        str = str + " / " + yardStr.description + " y"
    
        // 標高差を連結する
        str = str + "\n" + lblDiffElevation.description
    
        if pointAno.title != str {
            // ピンまでの距離に変化があればtitleを更新する
            pointAno.title = str
            mapView.addAnnotation(pointAno)
        }
    }
    

    シミュレータの実行結果は以下の様になる。

Qiita(12)-01.png

参考文書

国土地理院 ヘルプ

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

【Swift】Vision.frameworkでカメラ画像の顔認識を行う【iOS】

iOS11より、iOS標準フレームワーク Vision.framework を使うと、顔認識ができるらしいので今更ながら使ってみました。

概要

カメラ画像から顔を検出し、顔部分に矩形を表示します。

試した環境

  • Xcode 11.3
  • iOS 13.2
  • swift 5

実行サンプル

ぱくたそフリー素材で実験

IMG_3057.jpg
ディスプレイ画質の問題のせいもありそうですが、顔にちょっと髪がかかってたりすると少し認識が悪い。

Google画像検索「顔」で実験

IMG_3059.jpg
顔が沢山あっても、アップだと良く認識します。
(画像はぼかしてます

コード説明

VNImageRequestHandler
を利用して、 pixelBuffer から、顔情報を配列取得します。

結果は
VNDetectFaceRectanglesRequest
に非同期で戻されます。
顔情報は VNFaceObservation です。

    /// 顔認識情報の配列取得 (非同期)
    private func getFaceObservations(pixelBuffer: CVPixelBuffer, completion: @escaping (([VNFaceObservation])->())) {
        let request = VNDetectFaceRectanglesRequest { (request, error) in
            guard let results = request.results as? [VNFaceObservation] else {
                completion([])
                return
            }
            completion(results)
        }

        let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
        try? handler.perform([request])
    }

pixcelBuffer は カメラから取得した sampleBufferCMSampleBufferGetImageBuffer を使って変換します。
imageView には、 sampleBuffer から取得した生成をセット。

    /// カメラからの映像取得デリゲート
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return
        }
        getFaceObservations(pixelBuffer: pixelBuffer) { [weak self] faceObservations in
            guard let self = self else { return }
            let image = self.getFaceRectsImage(sampleBuffer: sampleBuffer, faceObservations: faceObservations)
            DispatchQueue.main.async { [weak self] in
                self?.previewImageView.image = image
            }
        }
    }

またその際、 VNFaceObservation から正規化された画像の位置が取得できるので、
その情報をもとに、矩形を画像に書き込みます。

        let imageSize = CGSize(width: width, height: height)
        let faseRects = faceObservations.compactMap {
            getUnfoldRect(normalizedRect: $0.boundingBox, targetSize: imageSize)
        }
        faseRects.forEach{ self.drawRect($0, context: newContext) }
    /// 正規化された矩形位置を指定領域に展開
    private func getUnfoldRect(normalizedRect: CGRect, targetSize: CGSize) -> CGRect {
        return CGRect(
            x: normalizedRect.minX * targetSize.width,
            y: normalizedRect.minY * targetSize.height,
            width: normalizedRect.width * targetSize.width,
            height: normalizedRect.height * targetSize.height
        )
    }
    /// コンテキストに矩形を描画
    private func drawRect(_ rect: CGRect, context: CGContext) {
        context.setLineWidth(4.0)
        context.setStrokeColor(UIColor.green.cgColor)
        context.stroke(rect)
    }

コード全体

import UIKit
import AVFoundation
import Vision

class FaceViewController: UIViewController {

    @IBOutlet weak var previewImageView: UIImageView!

    private let avCaptureSession = AVCaptureSession()

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

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        self.avCaptureSession.stopRunning()
    }

    /// カメラのセットアップ
    private func setupCamera() {
        self.avCaptureSession.sessionPreset = .photo

        let device = AVCaptureDevice.default(for: .video)
        let input = try! AVCaptureDeviceInput(device: device!)
        self.avCaptureSession.addInput(input)

        let videoDataOutput = AVCaptureVideoDataOutput()
        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String : Int(kCVPixelFormatType_32BGRA)]
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        videoDataOutput.setSampleBufferDelegate(self, queue: .global())

        self.avCaptureSession.addOutput(videoDataOutput)
        self.avCaptureSession.startRunning()
    }

    /// コンテキストに矩形を描画
    private func drawRect(_ rect: CGRect, context: CGContext) {
        context.setLineWidth(4.0)
        context.setStrokeColor(UIColor.green.cgColor)
        context.stroke(rect)
    }

    /// 顔認識情報の配列取得 (非同期)
    private func getFaceObservations(pixelBuffer: CVPixelBuffer, completion: @escaping (([VNFaceObservation])->())) {
        let request = VNDetectFaceRectanglesRequest { (request, error) in
            guard let results = request.results as? [VNFaceObservation] else {
                completion([])
                return
            }
            completion(results)
        }

        let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
        try? handler.perform([request])
    }

    /// 正規化された矩形位置を指定領域に展開
    private func getUnfoldRect(normalizedRect: CGRect, targetSize: CGSize) -> CGRect {
        return CGRect(
            x: normalizedRect.minX * targetSize.width,
            y: normalizedRect.minY * targetSize.height,
            width: normalizedRect.width * targetSize.width,
            height: normalizedRect.height * targetSize.height
        )
    }

    /// 顔検出位置に矩形を描画した image を取得
    private func getFaceRectsImage(sampleBuffer :CMSampleBuffer, faceObservations: [VNFaceObservation]) -> UIImage? {

        guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return nil
        }

        CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))

        guard let pixelBufferBaseAddres = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0) else {
            CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
            return nil
        }

        let width = CVPixelBufferGetWidth(imageBuffer)
        let height = CVPixelBufferGetHeight(imageBuffer)
        let bitmapInfo = CGBitmapInfo(rawValue:
            (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
        )

        guard let newContext = CGContext(
            data: pixelBufferBaseAddres,
            width: width,
            height: height,
            bitsPerComponent: 8,
            bytesPerRow: CVPixelBufferGetBytesPerRow(imageBuffer),
            space: CGColorSpaceCreateDeviceRGB(),
            bitmapInfo: bitmapInfo.rawValue
            ) else
        {
            CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
            return nil
        }

        let imageSize = CGSize(width: width, height: height)
        let faseRects = faceObservations.compactMap {
            getUnfoldRect(normalizedRect: $0.boundingBox, targetSize: imageSize)
        }
        faseRects.forEach{ self.drawRect($0, context: newContext) }

        CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))

        guard let imageRef = newContext.makeImage() else {
            return nil
        }
        let image = UIImage(cgImage: imageRef, scale: 1.0, orientation: UIImage.Orientation.right)

        return image
    }
}


extension FaceViewController : AVCaptureVideoDataOutputSampleBufferDelegate{

    /// カメラからの映像取得デリゲート
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return
        }
        getFaceObservations(pixelBuffer: pixelBuffer) { [weak self] faceObservations in
            guard let self = self else { return }
            let image = self.getFaceRectsImage(sampleBuffer: sampleBuffer, faceObservations: faceObservations)
            DispatchQueue.main.async { [weak self] in
                self?.previewImageView.image = image
            }
        }
    }
}

github

becky3/face_detection: 【Swift】Vision.frameworkでカメラ画像の顔認識を行う【iOS】
https://github.com/becky3/face_detection

参考サイト

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

端末の画面サイズごとにレイアウトを変更できる"Vary for Traits"の使い方

はじめに

いきなりですが、XcodeのVary for Traits という機能をご存知ですか?

スクリーンショット 2019-12-21 13 05 31

InterfaceBuilderの下のバーにある、グレーのボタンで使うことができます。

一体どういうことができる機能かというと、コードの記述なしに端末の画面サイズや回転状況に応じて、AutoLayoutの制約を付け替えたり、制約の数値を変更、色の変更などのレイアウト周りの設定の変更を行うことができます。

それによってiPhoneとiPad間でのレイアウト崩れを防いだり、画面回転時に全く違うレイアウトを表示するといった活用ができます。

今回は、そんなVary for Traits機能を使用する方法を紹介します。

画面サイズの定義を知る

Vary for Traits機能は画面サイズの定義ごとにレイアウト設定を変更するので、使用する前に、まずは画面サイズの定義を知ることが必要不可欠です。
画面サイズの定義は、横幅と縦幅のw(Width)h(Height)、サイズの大小のR(Regular)C(Compact)、の二つの概念の組み合わせでできています。

以下の画像はInterfaceBuilderでiPhone 11 Pro Maxの縦向き状態でViewを表示した際のスクリーンショットです。
左下に、View as: iPhone 11 Pro MAX(wC hR)と書かれています。

スクリーンショット 2019-12-20 17 18 38

iPhone 11 Pro Maxを縦向きにしている状態の画面サイズはwC hR、つまり横幅が小さく、縦幅が大きい端末と定義されていることがわかります。

ちなみに、それぞれの端末がどのように定義されているかは以下の表に起こしたので、参考にしてください

w h
iPhone(縦) C R
4s・SE・8・11 Pro(横) C C
8Plus・11・11 Pro MAX(横) R C
iPad(縦横両方) R R

縦向きのiPhoneのwC hR、横向きのiPhoneがhC、iPadのwR hRなどはよく使うので覚えておくと良いでしょう。

Vary for Traitsを使う

それぞれの端末のサイズ定義がわかったところで実際にVary for Traits機能を使用してみましょう。

準備

例として、まずは画面の中心に横幅300×高さ400の制約を設定したViewを配置します。

スクリーンショット 2019-12-21 12 56 14

その状態で画面を横向きに変更すると、画面自体の高さが400以下になってしまうので、画面から高さ400の制約を設定したViewが見切れて表示されてしまいます。

スクリーンショット 2019-12-21 13 21 28

この状態からVary for Traits機能を使って、画面が横になった場合にレイアウトを変えていきます。

定義したい画面サイズを選択する

早速、Vary for Traitsのボタンをクリックすると、以下の画像の吹き出しが現れます。
WidthとHeightのチェックボックスは、現在表示しているInterfaceBuilderで選択されている画面のサイズ定義で個別に設定したいものにチェックを入れます。

スクリーンショット 2019-12-21 13 25 14

現在表示している画面はiPhone 11 Pro Maxの横向きなので、画面サイズの定義はwR hCです。

両方にチェックを入れるとwR hCの組み合わせのである、8Plus・11・11 Pro MAXの横向きで表示した際に、有効になるレイアウトを設定できます。

Widthのみにチェックを入れた場合はwRが指定されている、横向きの8Plus・11・11 Pro MAXとiPadで表示した際に、有効になるレイアウトを設定できます。

Heightのみにチェックを入れた場合はhCが指定されている、横向き状態のiPhoneで表示した際に、有効になるレイアウトを設定できます。

今回の場合、iPhoneの横向きで表示した際にViewが画面外にはみ出してしまうので、Heightの方にのみチェックを入れます。

スクリーンショット 2019-12-21 13 25 31

これでhCの画面サイズのみで有効になるレイアウトが設定できる状態になりました。

特定の画面サイズでレイアウトを設定する

1.制約の数値を変更する

まずは、Viewの高さの制約を変更してみましょう。
高さの制約の設定を確認すると以下のようになっています。

スクリーンショット 2019-12-21 13 46 11

この中のConstantの左側の+をクリックすると、以下の画像の吹き出しが出ます。
そのままAdd Variationをクリックします。

スクリーンショット 2019-12-21 14 23 47

そうするとConstantの入力欄の下にhCの欄が追加されるので、そこに250を設定してみます。

スクリーンショット 2019-12-21 14 27 11

そうすると、Viewの高さがちゃんと画面内に収まるようになりました。

スクリーンショット 2019-12-21 14 45 45

hCでの数値を変更したので、もちろん縦向きに変更しても影響はありません。

スクリーンショット 2019-12-21 14 50 15

2.制約の有効・無効を設定する

次は横向きの時には、赤いViewの中央揃えではなく、左上に配置するように設定してみましょう。

先ほどのように、View.Center Yの制約を選択し、今度はInstalledの横の+をクリックして、吹き出しのAdd Variationをクリックします。

スクリーンショット 2019-12-21 14 59 04

すると、Installedの下にhC用のチェックボックスが追加されるので、チェックを外すと、制約が無効化されます。

スクリーンショット 2019-12-21 15 00 20

同じようにView.Center Xの制約もhCのInstalledのチェックボックスを追加して、チェックを外しておきましょう。

スクリーンショット 2019-12-21 15 02 15

すると、以下の画像のような状態になるので、ここから新たに制約を設定していきましょう。

スクリーンショット 2019-12-21 15 04 14

今回は左上に配置するので、赤いViewのTopとLeadingに各10ずつの制約を設定します。

スクリーンショット 2019-12-21 15 06 20

設定したView.Leadingの制約の詳細を見てみると、Installedのチェックが外れていて、hCの方のInstalledはチェックが入っています。

つまり、Vary for Traits機能を使用している時に、新たに設定した制約はVary for Traitsボタンをクリックしたときに表示された吹き出しでチェックを入れた画面サイズでのみ有効になるということです。
(今回は、Heightに選択を入れていたので、hC時にのみ有効になる制約ということになります。)

スクリーンショット 2019-12-21 15 11 30

そして、制約の一覧で現在表示している画面サイズで有効な制約はアイコンが明るく表示され、無効な制約はアイコンが暗く表示されるので、それでチェックすることもできます。

スクリーンショット 2019-12-21 15 14 47

この設定が完了すれば、横向きの画面で以下のような表示になります。

スクリーンショット 2019-12-21 15 24 57

3.Viewの背景色の変更を設定する

次に横向きの時に、赤いViewの背景色を別の色にしてみましょう。

今度はViewを選択して、Bacgkroundの横の+をクリックしてAdd Variationをクリックします。
スクリーンショット 2019-12-21 15 30 59

hC用の背景色設定が表示されるので、適当な色を設定すると、横向きの時にのみその色になります。
スクリーンショット 2019-12-21 15 31 17

4.特定のViewを消去・配置する

次は、Viewの上のラベルを設定しましょう。

ラベルには、「300(w)×400(h)」と記載されていますが、横向きの時には、Viewのサイズが300(w)×250(h)なので、「300(w)×250(h)」に変更したいところですが、UILabelのTextは、Vary for Traits機能で差し替えることはできません。

スクリーンショット 2019-12-21 15 38 37

なので、今回の場合は「300(w)×400(h)」のラベルをhCの時に消去して、「300(w)×250(h)」と記載されたラベルをhCの時にのみ表示されるようにすれば望んだ通りの表示になりそうです。

まずは今までの手順と同じように「300(w)×400(h)」ラベルの詳細から、hCInstalledのチェックを外します。

スクリーンショット 2019-12-21 15 43 51

すると、ラベルがViewの上から消去されます。
スクリーンショット 2019-12-21 15 46 10

あとは、新たにラベルを追加します。

スクリーンショット 2019-12-21 15 49 04

制約を追加した時と同じように、新たに追加された「300(w)×250(h)」LabelはhCでのみInstalledになります。

スクリーンショット 2019-12-21 15 50 52

制約と同じようにViewもInstalledかどうかをアイコンの明暗で確認できます。

スクリーンショット 2019-12-21 15 50 01

5.Vary for Traitsを終了する

Done VryingをクリックするとVary for Traitsを終了することができます。

スクリーンショット 2019-12-21 15 17 37

6.確認する

InterfaceBuilderのOrientationを変えて確認してみましょう。
 VaryForTraits

まとめ

Vary for Traits機能を使うことで、縦画面と横画面それぞれに対応したレイアウトを作成する方法を紹介しました。

自分もこの機能を知るまでは、Viewの比率の制約や、制約のPriorityを細かく設定したり、コードで画面サイズや方向で制約を操作して、複数の画面サイズに対応していましたが、この機能を使うことで、コードを使うことなく、制約もシンプルな形で想定している表示を実現できたので、皆さんも使ってみてはいかがでしょうか?

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

SwiftUIにおけるForEach内からのBindingオブジェクトの渡し方

SwiftUIにてListビューを作る際に、多くのケースでForEachが用いられると思います。

下のコードでは、RankingListというリストビューを持つ画面が、自身のviewModelmovies配列の値を基に、リスト内の各行のビューRankingRowを生成しています。

class RankingViewModel: ObservableObject {
    @Published var movies: [Movie]
    ...
}

struct RankingList: View {
    @ObservedObject var viewModel: RankingViewModel

    var body: some View {
        List {
            ForEach(viewModel.movies) { movie in
                RankingRow(movie: movie)
            }
        }
    }
}

この方法でRankingRowに渡しているのはmovies配列の要素のコピーとなります。

ForEach(viewModel.movies) { movie in
    RankingRow(movie: movie) // Movieオブジェクトのコピーを渡している
}

RankingRowMovie型の実体を受け取る設計であれば何ら問題ありませんが、もしRankingRowMovieBindingオブジェクトを受け取りたいとなった場合にはどうすればいいでしょうか。


次のようにviewModel.moviesBindingオブジェクトをそのままForEachに渡したいところですが、残念ながらできません。

ForEach($viewModel.movies) {...} // NG: Binding<RankingViewModel>を渡そうとしている
ForEach(viewModel.$movies) {...} // NG: Published<[Movie]>.Publisherを渡そうとしている

ForEachの定義を見てみると分かりますが、RandomAccessCollectionに準拠したオブジェクトか、もしくはRange<Int>型の値しか受け取らないためです。

ForEach.swift
struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable {

    // ①
    init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content)

    // ②
    init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content)

    // ③
    init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content)
}
// ※ 一部抜粋

何らかの配列を渡す方法を別途考えてみます。


SwiftのArrayには、自身の要素のインデックス範囲を返すプロパティindicesがあります。

@inlinable public var indices: Range<Int> { get }

Range<Int>は上記③のinit(_, content:)に渡すことのできる型ですが、Range自体がRandomAccessCollectionに適合しているため、つまり上記①、②のイニシャライザにも渡すことができます。

今回は上記②のinit(_, id:, content:)に渡すことで当初の問題を解決します。

ForEach(viewModel.movies.indices, id: \.self) { index in
    RankingRow(movie: self.$viewModel.movies[index])
}

ForEach内で各要素のindexを受け取ることができるようになった為、viewModel.movies配列の各要素を子ビューにバインディングして渡せるようになりました。

注意点として、忘れずに引数にid:を指定して上記②のinit(_, id:, content:)を確実に呼び出す必要があります。

引数id:を指定しない場合、上記③のinit(_, content:)を呼び出してしまうのですが、このイニシャライザは静的なRangeオブジェクト向けとなっており、Rangeオブジェクトに変化があったとしてもSwiftUIはその変更をキャッチしてビューの更新を行ってくれません。

id:を指定せずForEachRange<Int>を渡した場合、初期化後のオブジェクトに変化があると、Xcodeに以下のようなログが出力されます。

ForEach<Range<Int>, Int, RankingRow> count (3) != its initial count (0). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!

この警告が表示された際は、今一度id:を付け忘れていないか確認してみましょう。

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

SwiftUIでiOSのAppStoreアプリのUIを表現してみる

Ateam Lifestyle Advent Calendar 2019の22日目は
株式会社エイチームライフスタイルでWebとiOSをメインに開発している @hytkgamiが担当します。

SwiftUIでアプリを作ってみようと思い、その制作過程を記事にまとめました。
設計やSwiftUIの実装等、至らない点が多々あるかと思いますが、編集リクエストやコメントをいただけますと幸いです!

想定読者

  • SwiftUIに興味がある方
  • iOSの開発に興味がある方

SwiftUIのViewについて、細かく説明はしていきません。
詳しく知りたい場合はドキュメントチュートリアルをご覧ください。

SwiftUIとは

WWDC 2019で発表された、新しいUI構築のフレームワークです。
iOS、macOS、watchOSなどすべてのAppleプラットフォーム上で動作します。

より優れたAppを、より少ないコードで。
(中略)
SwiftUIは宣言型シンタックスを使用しているため、ユーザーインターフェイスの動作をシンプルに記述することができます。たとえば、テキストフィールドからなるアイテムのリストを作成すると書いてから、各フィールドの配置、フォント、色を記述するといった具合です。これにより、コードがかつてないほどシンプルで読みやすくなり、時間の節約と保守作業の負担軽減につながります。

SwiftUIは宣言的な構文が特徴的で、Xcode - SwiftUI - Apple Developerでも上記のように紹介されています。
より詳しく知りたい方には以下の記事もおすすめです。

主なツール・環境など

開発環境

  • macOS Catalina 10.15.1
  • Swift version 5.1
  • Version 11.3 (11C29)

デザインツール

  • Figma

開発開始

今回はiOSのAppStoreアプリを開いてすぐに目に入るTodayタブのコンテンツを実装していきます。
イメージはこんな感じです。便宜的に、各コンポーネントに名前をつけました。

MockImage.png

ヘッダーの作成

まずは簡単にできそうな、ヘッダー部分から作っていきます。
ヘッダーの要素は、テキストラベルが2つと画像が1つです。
以下のように作ります。

struct HeaderView: View {
    let title: String
    init(title: String) {
        self.title = title
    }

    var body: some View {
        VStack {
            HStack {
                Text("12月22日 日曜日")
                    .foregroundColor(.gray)
                    .font(.system(size: 14))
                    .padding(16)
                Spacer()
            }.frame(height: 16, alignment: .topLeading)
            HStack {
                Text(title)
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .padding(16)
                Spacer()
                Image("avator")
                    .resizable() // 画像のサイズを変更可能にする
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 36, height: 36, alignment: .center)
                    .clipShape(Circle()) // 正円形に切り抜く
                    .padding(.trailing, 16)
            }
        }
    }
}

プレビューは次のとおりです。

HeaderView.png

タイトルとアイコンが水平方向に並び、日付がその上に積まれるようなUIになっていたため、HStackを2つVStackに積んでいます。
また、タイトルとアイコンはそれぞれ左端と右端に表示したいためSpacer()を噛ませてスペースを作っています。
なお、アイコンの画像は http://flat-icon-design.com/ から拝借しました。

日付の部分はフォーマットを作ってオブジェクトを渡して…が手間だったので固定値を入れています。

カードの作成

次にカードを作成します。
iOSのAppStoreアプリでは、カードをタップするとフルスクリーンで中身が表示される仕様になっています。
イメージはこちらです↓

Group 8.png

メインイメージの作成

struct ItemMainView: View {
    var body: some View {
        ZStack(alignment: .top) {
            Image("item_main_image")
                .resizable()
                .frame(height: 420)
            HStack {
                VStack(alignment: .leading, spacing: 0) {
                    Text("title")
                        .font(.headline)
                        .foregroundColor(.white)
                        .shadow(radius: 4.0)
                    Text("APP OF THE DAY")
                        .font(.largeTitle)
                        .foregroundColor(.white)
                        .shadow(radius: 4.0)
                }
                .padding()
                Spacer()
            }
        }
    }
}

プレビューは次のとおりです。

CardMainImage.png

画像にテキストが重なる構造のため、ZStackを利用しています。
引数にあるalignmentによって、上下左右中央のどこを基準として配置するか指定することができます。
画像は https://picsum.photos/ からダウンロードしたものをImageAssetsに追加して利用しています。

インストールバナーの作成

struct AppInstallBanner: View {
    var body: some View {
        HStack {
            Image("icon")
                .resizable()
                .frame(width: 48, height: 48)
                .padding()
            VStack(alignment: .leading) {
                Text("WeatherApp")
                    .font(.headline)
                    .lineLimit(1)
                Text("Deliver the weather forecast")
                    .font(.footnote)
                    .lineLimit(1)

            }
            Spacer()
            VStack(alignment: .center, spacing: 0) {
                Button(action: {
                    //
                }) {
                    Text("GET")
                        .bold()
                        .foregroundColor(Color.blue)
                }
                .padding(.vertical, 4)
                .padding(.horizontal, 16)
                .background(Color.white)
                .clipShape(Capsule())
                Text("In-app purchase")
                    .lineLimit(1)
                    .font(.caption)
            }
            .fixedSize()
            .padding()
        }
        .foregroundColor(Color.white)
        .background(Color("gray3"))
    }
}

プレビューは次のとおりです。

AppInstallBanner.png

HStackVStackを使った構造はだいぶ見慣れてきたかと思います。
ここでのポイントはButtonです。ボタン押下時のアクションと見た目を一度に定義します。
今回インストール機能までは実装しないため、コメントアウトするだけにしています。
アプリアイコンはFigmaで適当に作りました。

紹介文の作成

struct ItemIntroduceTextView: View {
    let description: String
    init(with description: String) {
        self.description = description
    }

    var body: some View {
        Text(description)
            .lineLimit(nil)
            .fixedSize(horizontal: false, vertical: true)
            .padding()
    }
}

スクリーンショット 2019-12-18 4.12.17.png

長文が入る箇所なので、.lineLimit(nil)としています。
また、.fixedSize(horizontal: false, vertical: true)で、要素に応じて垂直方向にViewのサイズが変わるようにしています。

ここまでのコンポーネントをまとめてカードを作成する

struct ItemDetailView: View {
    var body: some View {
        ScrollView(.vertical, showsIndicators: false) {
            VStack(alignment: .center, spacing: 0) {
                ItemMainView()
                AppInstallBanner()
                Divider()
                    .background(Color.gray)
                ItemIntroduceTextView()
            }
            .background(Color("gray3"))
        }.edgesIgnoringSafeArea(.all)
    }
}

ItemDetailView.png

フルスクリーンを表現するために、.edgesIgnoringSafeArea(.all)を利用しています。
本来Viewの端はセーフエリアとの境界までとして描画されるのですが、.edgesIgnoringSafeArea(.all)によってその制約を無視します。
全画面に拡張したカードはスクロール可能なため、ScrollViewで囲います。
クローズボタンはフルスクリーン時にのみ出現するため、後ほど実装します。

コレクションの作成

まずはカードを一つ置いてみます。

struct RecommendCollectionView: View {
    var body: some View {
        ScrollView(.vertical, showsIndicators: true) {
            HeaderView(title: "Today")
            ItemDetailView()
                .frame(
                    width: 380,
                    height: 400,
                    alignment: .top)
                .cornerRadius(20)
                .disabled(true)
        }
    }
}

RecommendCollectionView.png

ポイントは.disabled(true)です。ScrollViewの中にScrollViewがある構造なので、
親のスクロールビューを操作したいのに、子のスクロールビューがスクロールされてしまう…といった状況を起こさないようにします。
それっぽい見た目にはなってきましたが、タップしてフルスクリーン表示される挙動がまだ実装できていません。
ここで@Stateを使います。

@Stateを使ってフルスクリーン表示を実装する

struct RecommendCollectionView: View {
    @ObservedObject var store = RecommendItemStore()
    @State private var presentationMode = false
    let item = RecommendItem(id: 1, appName: "appName", title: "title", caption: "caption", recommendReason: "reason", imageUrl: "https://picsum.photos/id/1000/474/520", description: "description")

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                ScrollView(.vertical, showsIndicators: true) {
                    if !self.presentationMode {
                        HeaderView(title: "Today")
                    }
                    ItemDetailView(item: item, presentationMode: self.$presentationMode)
                        .frame(width: 340, height: 380, alignment: .top)
                        .cornerRadius(20)
                        .disabled(true)
                        .onTapGesture {
                            self.presentationMode = true
                    }.padding()
                }
                if self.presentationMode {
                    ItemDetailView(item: item, presentationMode: self.$presentationMode)
                        .background(Color("gray3"))
                        .edgesIgnoringSafeArea(.all)
                }
            }
        }
    }
}

struct ItemDetailView: View {
    let item: RecommendItem
    @Binding var presentationMode: Bool
    var body: some View {
        ScrollView(.vertical, showsIndicators: false) {
            VStack(alignment: .center, spacing: 0) {
                ZStack(alignment: .topTrailing) {
                    ItemMainView(with: item)
                    if presentationMode {
                        Button(action: {
                            self.presentationMode = false
                        }) {
                            Image("ic_close")
                        }.padding()
                    }
                }
                AppInstallBanner(with: item)
                Divider()
                    .background(Color.gray)
                ItemIntroduceTextView(with: item.description)
            }
            .background(Color("gray3"))
        }.edgesIgnoringSafeArea(.all)
    }
}

いくつか新しい要素が出てきます。

  • GeometryReader
    • 親レイアウトのサイズと、親に対して相対的な自身の座標を持つコンテナ
  • @State
    • データバインディングのために用いる修飾子
  • $
    • @StateプロパティをBindingに変換するためにつけるプレフィクス

presentationModeという変数をステートとして管理し、それをItemDetailViewにバインディング変換して渡します。
RecommendCollectionViewではpresentationModetrueの場合にフルスクリーンでアイテムの詳細画面を表示します。

本来フルスクリーンのモーダルで表示するべきかと思いますが、SwiftUIのリファレンスを見てもそのようなメソッドは見当たらず、ZStackで実現しました。

こちらの記事にも記載されていますが、他の手段としてはUIKitとの組み合わせでも実現できるようです。

ItemDetailViewでは新たにボタンを追加し、アクションの中でバインドされたpresentationModeに対して変更を加えています。
こうすることで、presentationModeへの変更がRecommendCollectionViewにも伝わりフルスクリーンを解除することが可能です。

仕上げ APIを通してカードを生成する

カードが1つでは寂しいので、複数表示したいところですが
すべて固定値では手間がかかるので、APIからデータを取得し、モデルに変換して表示するようにします。

モデルの準備

struct RecommendItem: Identifiable, Codable {
    let id: Int
    let appName: String
    let title: String
    let caption: String
    let recommendReason: String
    let imageUrl: String
    let description: String
}

単純なCodableオブジェクトです。ForEachでループさせるためにIdentifiableにも準拠させます。

APIの用意

ちょうどいいダミーデータをjson形式で秒で生成してくれる「faker」を紹介 を参考にして、ローカルにAPIサーバを用意します。
ほとんどコピペですが、画像URLの部分やテキストの長さを調整したかったので、ダミーデータ生成用のプログラムを以下のようにしています。

let faker = require("faker")

let db = {
 products: []
}

for(let i = 0;i < 20; ++i) {
 db.products.push({
   id: i+1,
   app_name: faker.lorem.word(),
   title: faker.lorem.words(),
   caption: faker.lorem.lines(),
   recommend_reason: faker.lorem.word(),
   image_url: `https://picsum.photos/id/${1000 + i}/474/520`,
   description: faker.lorem.sentences(),
 })
}

console.log(JSON.stringify(db))

URLからImageを生成できるようにする

UIKit向けのライブラリはたくさんありますが、SwiftUIに対応したものはほとんどありませんでした。
自前で書くか、UIKit向けのライブラリを使ってUIImageを取得したあとにImage(uiImage: UIImage())のように生成するか迷っていましたが、KingfisherのREADMEにSwiftUIの文字が…!

import KingfisherSwiftUI

var body: some View {
    KFImage(URL(string: "https://example.com/image.png")!)
}

ということで、SwiftPackageManagerを使ってインストールします。

API呼び出し処理の実装

先ほど作成したローカルAPIサーバに向けてリクエストを送る処理を作ります。
RecommendItemStoreクラスを作成します。

class RecommendItemStore: ObservableObject {
    @Published var items: [RecommendItem] = []

    init() {
        self.fetch()
    }

    private func fetch() {
        guard let url = URL(string: "http://localhost:3001/products") else { return }
        URLSession.shared.dataTask(with: url) { (data, _, err) in
            if err != nil {
                print(err.debugDescription)
            }
            let jsonDecoder = JSONDecoder()
            jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
            guard let data = data,
                let items = try? jsonDecoder.decode([RecommendItem].self, from: data) else { return }
            DispatchQueue.main.async {
                self.items = items
            }
        }.resume()
    }
}

@Publishedをプロパティに付与することで、値の更新時に通知が行われるようになります。
つまり、上記のクラスではfetch()によってAPIから値がセットされたときに通知が発行されます。

APIのデータを受け取って描画する

通知を受け取る側では以下のように実装します。

struct RecommendCollectionView: View {
    @ObservedObject var store = RecommendItemStore()
    @State private var presentationMode = false
    @State private var selection: RecommendItem?
    // 省略
    var body: some View {
        // 省略
            ForEach(self.store.items, id: \.id) { item in
                ItemDetailView(item: item, presentationMode: self.$presentationMode)
                    .frame(width: 340, height: 380, alignment: .top)
                    .cornerRadius(20)
                    .disabled(true)
                    .onTapGesture {
                        self.selection = item
                        self.presentationMode = true
                    }.padding()
                }
            }
            if self.presentationMode {
                ItemDetailView(item: item, presentationMode: self.$presentationMode)
                    .background(Color("gray3"))
                    .edgesIgnoringSafeArea(.all)
                }
            }
        }
    }
}

sample.gif

ForEachを使ってRecommendItemStoreに保持しているアイテムのリストを描画していきます。
フルスクリーン表示に対応するため、現在選択されているアイテムをselection変数に保持し、それをフルスクリーンに表示する要素として渡します。

SwiftUIで実装してみての所感

本当はTabViewを使って実装していたのですが、フルスクリーンモーダルを実装しようとすると
どうしても最前面にタブバーが表示されてしまいうまく実現できませんでした。
今回は複雑な要件とは言い難いかもしれませんが、そうした要件の実現のため自由にカスタマイズしながら使う場合は、まだUIKitのほうが効率が良いように思えます。(もちろんSwiftUIに対する理解不足も大きいです。)

とはいえ、普段のStoryBoardでの開発に比べると圧倒的にスムーズに実装&検証が進みますし、Viewの組み立てもやりやすくなったと思います。
また、イベント処理がしやすくなったことによる恩恵は大きいのではないでしょうか。
今回はまだSwiftUIのほんの一部にしか触れられていませんが、これを機にもっと色んなものを作ってみたいと思っています。

終わりに

Ateam Lifestyle Advent Calendar 2019 の23日目は、@masatomasato1224がお送りします。お楽しみに!
“挑戦”を大事にするエイチームグループでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
https://www.a-tm.co.jp/recruit/

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

Swift MkMapViewで地図アプリ作成してみた(11)- 国土地理院からロングタップした位置の標高を取得する

記事一覧

Swift MkMapViewで地図アプリ作成してみた(記事一覧)

国土地理院のサーバ

国土地理院に、標高を取得するWebAPI が公開されています。
使用方法は上記のリンクを参照してください。

使用例

https://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?lon=140.08531&lat=36.103543&outtype=JSON

ブラウザで上記のURLにアクセスしてみてください。

{"elevation":25.3,"hsrc":"5m\uff08\u30ec\u30fc\u30b6\uff09"}

上記の情報が表示されるWebページが表示されます。
elevationが標高ですので、指定した位置は標高25.3mとなります。

国土地理院のWebAPIをコールするコードを組んでみる

ViewController.swift:ロングタップ開始時に古いピンを削除する
    // 国土地理院のURL
    let baseUrl = "https://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?"
    // ロングタップ地点の標高を取得する
    let lonUrl = "&lon=" + pointAno.coordinate.longitude.description
    let latUrl = "&lat=" + pointAno.coordinate.latitude.description
    // アウトプットの形式
    let outtypeUrl = "&outtype=JSON"
    // URLとクエリを連結
    let listUrl = baseUrl + lonUrl + latUrl + outtypeUrl

    // http:は「Info.plis」に「App Transport Security Settings」を設定しないとエラーになる
    guard let url2 = URL(string: listUrl) else { return }

    URLSession.shared.dataTask(with: url2) { (data, response, error) in
        if error != nil {
            print(error!.localizedDescription)
        }

        guard let data = data else { return }

        let json = try? JSONDecoder().decode(JsonElevation.self, from: data)
        if nil != json {
            // mainスレッドで処理する
            DispatchQueue.main.async {
                self.longTapElevation = (json?.elevation)!

            }
        }.resume()
    }

参考文書

国土地理院 ヘルプ > 標高を求めるプログラム > サーバサイドで経緯度から標高を求めるプログラム
補足:標高意外にも素晴らしい技術情報があります。

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

Swift MkMapViewで地図アプリ作成してみた(11)- ロングタップした位置の標高を国土地理院から取得する

記事一覧

Swift MkMapViewで地図アプリ作成してみた(記事一覧)

国土地理院のサーバ

国土地理院に、標高を取得するWebAPI が公開されています。
使用方法は上記のリンクを参照してください。

使用例

https://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?lon=140.08531&lat=36.103543&outtype=JSON

ブラウザで上記のURLにアクセスしてみてください。

{"elevation":25.3,"hsrc":"5m\uff08\u30ec\u30fc\u30b6\uff09"}

上記の情報が表示されるWebページが表示されます。
elevationが標高ですので、指定した位置は標高25.3mとなります。

国土地理院からロングタップした位置の標高を取得する

  1. URLを生成する

    ロングタップした位置にピンを立て距離を表示するで設定したピンの標高を取得するURLを生成する。

    ViewController.swift:URLを生成する
    // 国土地理院のURL
    let baseUrl = "https://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?"
    // ロングタップ地点でクエリを設定する
    let lonUrl = "&lon=" + pointAno.coordinate.longitude.description
    let latUrl = "&lat=" + pointAno.coordinate.latitude.description
    // アウトプット形式をJSONに設定する
    let outtypeUrl = "&outtype=JSON"
    // URLとクエリを連結
    let listUrl = baseUrl + lonUrl + latUrl + outtypeUrl        
    // URLを生成する
    guard let url = URL(string: listUrl) else { return }
    
  2. 標高を取得する

    URLSessionで生成したURLにアクセスして標高を取得する。

    ViewController.swift:標高を取得する
    // URLを生成する
    guard let url = URL(string: listUrl) else { return }
    URLSession.shared.dataTask(with: url) { (data, response, error) in
        if error != nil {
            print(error!.localizedDescription)
        }
        guard let data = data else { return }
        // JSONを取得する
        let json = try? JSONDecoder().decode(JsonElevation.self, from: data)
        if nil != json {
            // mainスレッドで処理する
            DispatchQueue.main.async {
                // JSONから標高を取得する
                self.longTapElevation = (json?.elevation)!
            }
        }.resume()
    }
    

    サブスレッドからメインスレッドに切り替えるためには、DispatchQueue.main.asyncを使用する。

余談:HTTPを使用する場合に設定すること(HTTPSは不要)

HTTP通信が非推奨のためHTTP通信する場合は、Info.plistを設定しないと実行時エラーとなる。

Qiita(11)-01.png

App Transport Security Settingsを追加して、Allow Arbitrary LoadsにYESを設定する。
これでHTTP通信が可能となる。

参考文書

国土地理院 ヘルプ

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

Apple Watch で名言表示アプリを作る

 この記事は 、ユアマイスター Advent Calendar 2019 の21日目の記事です。

 こんにちは!ユアマイスターでエンジニアインターンをしている土佐鰹です。
 年末にPRIDE、K1などで盛り上がっていた時代から10年飛んで、RIZINが年末の格闘技放送を定例化してきて嬉しい限りでございます。

 弊社には格闘技のように、長く愛されるサービスを作りたいエンジニアがたくさんいます。その中の1人に、息を吐くように名言を生み出す方がおられます。(以降、Fさん)
 そんなFさんが吐き出す名言を忘れたくないと、過去に弊社では全精力をかけて名言を返してくれるSlackBotを作成しました。
スクリーンショット 2019-12-21 0.42.53.png

 しかし、Slackを使うのは仕事モードの時がほとんどですよね。。。
 「遊んでいる時にも時々思い出したいな。。」
 「ジムで自分を追い込んで疲れてる時に癒されたいな。。」
 「エンジニアチームだけずるいな。。」
 (※弊社ではエンジニアチーム以外はSlack以外のツールをメインで使っています)

 そのようなお客様の思いを解決するために、このアプリを作成しました!!

実装

 まず、Xcodeで新しいProjectを作り Watch App を選択します。
スクリーンショット 2019-12-20 23.42.34.png

Inerface.storyboardでAppleWatchのUIを作っています。
今回はsimple is the bestの精神で、LabelButtonのみ追加します。

スクリーンショット 2019-12-20 23.50.06.png

 作成したLabelから controlを押しながらInterfaceControllerにスワイプしていきます。
 するとLabelのNameを設定する画面が出てきますので適当に入力します。今回は名言表示部分なのでmaximumLabelとしました。(Buttonに関しても同様。)
スクリーンショット 2019-12-20 23.55.57.png

 以上の作業を行うと、以下のようなコードがInterfaceController.swiftに追加されています。

  @IBOutlet weak var maximLabel: WKInterfaceLabel!

    @IBAction func onTapButton() {
    }

 このonTapButtonでボタンが押された際の処理を書いていきます。

 まず、classの先頭で以下のコードを追加します。

    let maxim_words = [
        "プログラミング言語界のノンシリコンシャンプー",
        "食は質より量",
        "俺の辞書の中にカロリーはない",
        "夕方の12時",
        "もうそろそろグルコサミンの時期かな",
        "1の3乗は1だから"
    ]

 これは名言の中でも、自分が選び抜いた精鋭たちになります。
 この名言たちから、"技術を追い求めている人なのかな"、"太っているのかな"、"足腰が弱いのかな" 等の想像ができると思います。
 謎に包まれたFさんのベールも少しずつ剥がれてきましたね!!

 そして、onTapButtonの関数でランダムに名言を選んでLabelに表示する処理を書きます。

    let maximum_word = self.maxim_words.randomElement()
    maximLabel.setText(maximum_word)

これで完成です!!!!!!!!!

View

完成したアプリの起動後画面は以下です。
スクリーンショット 2019-12-21 1.14.50.png

ボタンを押すと次々に名言を吐いてくれます。
スクリーンショット 2019-12-21 0.26.18.png
スクリーンショット 2019-12-21 0.26.32.png

最後に

 これで仕事モードでないOFFの時も名言を思い出すことができます。
 Fさんも自分の名言がみんなに見てもらえて嬉しいことこの上ないはず。

 自分はARKitでARの開発を行なっているのですが、AppleWatchのフレームワークもさほど変わらないことがわかったので、最近買ったAppleWatchで暇があったらアプリを作ってみようかなと思いました。

参考記事

初心者向け】はじめてのApple Watchアプリ

 

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

10分で作成!名言表示アプリ for Apple Watch

 この記事は 、ユアマイスター Advent Calendar 2019 の21日目の記事です。

 こんにちは!ユアマイスターでエンジニアインターンをしている土佐鰹です。
 年末にPRIDE、K1などで盛り上がっていた時代から10年飛んで、RIZINが年末の格闘技放送を定例化してきて嬉しい限りでございます。

 弊社には格闘技のように、長く愛されるサービスを作りたいエンジニアがたくさんいます。その中の1人に、息を吐くように名言を生み出す方がおられます。(以降、Fさん)
 そんなFさんが吐き出す名言を忘れたくないと、過去に弊社では全精力をかけて名言を返してくれるSlackBotを作成しました。
スクリーンショット 2019-12-21 0.42.53.png

 しかし、Slackを使うのは仕事モードの時がほとんどですよね。。。
 「遊んでいる時にも時々思い出したいな。。」
 「ジムで自分を追い込んで疲れてる時に癒されたいな。。」
 「エンジニアチームだけずるいな。。」
 (※弊社ではエンジニアチーム以外はSlack以外のツールをメインで使っています)

 そのようなお客様の思いを解決するために、このアプリを作成しました!!

実装

 まず、Xcodeで新しいProjectを作り Watch App を選択します。
スクリーンショット 2019-12-20 23.42.34.png

Inerface.storyboardでAppleWatchのUIを作っています。
今回はsimple is the bestの精神で、LabelButtonのみ追加します。

スクリーンショット 2019-12-20 23.50.06.png

 作成したLabelから controlを押しながらInterfaceControllerにスワイプしていきます。
 するとLabelのNameを設定する画面が出てきますので適当に入力します。今回は名言表示部分なのでmaximumLabelとしました。(Buttonに関しても同様。)
スクリーンショット 2019-12-20 23.55.57.png

 以上の作業を行うと、以下のようなコードがInterfaceController.swiftに追加されています。

  @IBOutlet weak var maximLabel: WKInterfaceLabel!

    @IBAction func onTapButton() {
    }

 このonTapButtonでボタンが押された際の処理を書いていきます。

 まず、classの先頭で以下のコードを追加します。

    let maxim_words = [
        "プログラミング言語界のノンシリコンシャンプー",
        "食は質より量",
        "俺の辞書の中にカロリーはない",
        "夕方の12時",
        "もうそろそろグルコサミンの時期かな",
        "1の3乗は1だから"
    ]

 これは名言の中でも、自分が選び抜いた精鋭たちになります。
 この名言たちから、"技術を追い求めている人なのかな"、"太っているのかな"、"足腰が弱いのかな" 等の想像ができると思います。
 謎に包まれたFさんのベールも少しずつ剥がれてきましたね!!

 そして、onTapButtonの関数でランダムに名言を選んでLabelに表示する処理を書きます。

    let maximum_word = self.maxim_words.randomElement()
    maximLabel.setText(maximum_word)

これで完成です!!!!!!!!!

View

完成したアプリの起動後画面は以下です。
スクリーンショット 2019-12-21 1.14.50.png

ボタンを押すと次々に名言を吐いてくれます。
スクリーンショット 2019-12-21 0.26.18.png
スクリーンショット 2019-12-21 0.26.32.png

最後に

 これで仕事モードでないOFFの時も名言を思い出すことができます。
 Fさんも自分の名言がみんなに見てもらえて嬉しいことこの上ないはず。

 自分はARKitでARの開発を行なっているのですが、AppleWatchのフレームワークもさほど変わらないことがわかったので、最近買ったAppleWatchで暇があったらアプリを作ってみようかなと思いました。

参考記事

初心者向け】はじめてのApple Watchアプリ

 

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

[入門] 10分で開発!名言表示アプリ for Apple Watch

 この記事は 、ユアマイスター Advent Calendar 2019 の21日目の記事です。

 こんにちは!ユアマイスターでエンジニアインターンをしている土佐鰹です。
 年末にPRIDE、K1などで盛り上がっていた時代から10年飛んで、RIZINが年末の格闘技放送を定例化してきて嬉しい限りでございます。

 弊社には格闘技のように、長く愛されるサービスを作りたいエンジニアがたくさんいます。その中の1人に、息を吐くように名言を生み出す方がおられます。(以降、Fさん)
 そんなFさんが吐き出す名言を忘れたくないと、過去に弊社では全精力をかけて名言を返してくれるSlackBotを作成しました。
スクリーンショット 2019-12-21 0.42.53.png

 しかし、Slackを使うのは仕事モードの時がほとんどですよね。。。
 「遊んでいる時にも時々思い出したいな。。」
 「ジムで自分を追い込んで疲れてる時に癒されたいな。。」
 「エンジニアチームだけずるいな。。」
 (※弊社ではエンジニアチーム以外はSlack以外のツールをメインで使っています)

 そのようなお客様の思いを解決するために、このアプリを作成しました!!

実装

 まず、Xcodeで新しいProjectを作り Watch App を選択します。
スクリーンショット 2019-12-20 23.42.34.png

Inerface.storyboardでAppleWatchのUIを作っています。
今回はsimple is the bestの精神で、LabelButtonのみ追加します。

スクリーンショット 2019-12-20 23.50.06.png

 作成したLabelから controlを押しながらInterfaceControllerにスワイプしていきます。
 するとLabelのNameを設定する画面が出てきますので適当に入力します。今回は名言表示部分なのでmaximumLabelとしました。(Buttonに関しても同様。)
スクリーンショット 2019-12-20 23.55.57.png

 以上の作業を行うと、以下のようなコードがInterfaceController.swiftに追加されています。

@IBOutlet weak var maximLabel: WKInterfaceLabel!

@IBAction func onTapButton() {
}

 このonTapButtonでボタンが押された際の処理を書いていきます。

 まず、classの先頭で以下のコードを追加します。

let maxim_words = [
    "プログラミング言語界のノンシリコンシャンプー",
    "食は質より量",
    "俺の辞書の中にカロリーはない",
    "夕方の12時",
    "もうそろそろグルコサミンの時期かな",
    "1の3乗は1だから"
]

 これは名言の中でも、自分が選び抜いた精鋭たちになります。
 この名言たちから、"技術を追い求めている人なのかな"、"太っているのかな"、"足腰が弱いのかな" 等の想像ができると思います。
 謎に包まれたFさんのベールも少しずつ剥がれてきましたね!!

 そして、onTapButtonの関数でランダムに名言を選んでLabelに表示する処理を書きます。

@IBAction func onTapButton() {
    let maximum_word = self.maxim_words.randomElement()
    maximLabel.setText(maximum_word)
}

これで完成です!!!!!!!!!

View

完成したアプリの起動後画面は以下です。
スクリーンショット 2019-12-21 1.14.50.png

ボタンを押すと次々に名言を吐いてくれます。
スクリーンショット 2019-12-21 0.26.18.png
スクリーンショット 2019-12-21 0.26.32.png

最後に

 これで仕事モードでないOFFの時も名言を思い出すことができます。
 Fさんも自分の名言がみんなに見てもらえて嬉しいことこの上ないはず。

 自分はARKitでARの開発を行なっているのですが、AppleWatchのフレームワークもさほど変わらないことがわかったので、最近買ったAppleWatchで暇があったらアプリを作ってみようかなと思いました。

参考記事

初心者向け】はじめてのApple Watchアプリ

 

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

swiftの勉強をはじめた人へ

はじめに

みなさんdelegateとかのイメージはついてますか?
この記事では、私が勉強を始めて1,2ヶ月目で理解するのに苦労した部分(今回は7選)を中心に、
こんな解説があったらよかったなぁ〜と思っていたシリーズを集めて、
初心者がひっかかりやすいであろうところ、基本事項をQ&A方式でまとめました!
確認の意味でやりたい場合は、Questionだけみて答えを考えてください!
全くわからない人は、読みたいところを読んでみてください!☺️

①letとvarの違い

まずは、一番初めの宣言の部分から!簡単な人には簡単です!
(ちなみに私は始めた頃さっぱりわかってませんでした、、?)

Question
let  var の違いは?

Answer
letは定数、varは変数です。
varietyのvarですね!

varは、1度データを入れても、後でデータを書き換えることができます。
一方、letは1度入れたデータを書き換えることができません。

ここで多くのみなさんはこう思うはず。
「全てvarで良いのでは、、?」
実際当時のわたしも、こう思っていました!笑

確かに、できないことはないのですが、もしletで良いところをvarで書いてしまうと、
変更するつもりのない変数を間違えて、変更してしまった時に気づかず、余計なエラーを自ら引き起こしてしまう、、という懸念があるので(複雑になるほどそうなるかもです)、しっかり使い分けていきましょう!

var number : Int = 0 //最初は0(初期値)
number = 2 //上書き保存されます

let number : Int = 0
number = 2 //これはアウト letで宣言したので初期値0から書き換え不可能です

②オプショナル型

次に、非オプショナル型・オプショナル型・暗黙的オプショナル型についてです。
宣言のところで "!" をよく見かけませんか?
これってなんだろう、、とりあえずよくわからないけど、書いとこう!って人多いと思います!
そこで、以下の例題を踏まえ考えていきましょう!

Question それぞれのprint関数は何が出力されるでしょうか?

//非オプショナル型
var number0 : Int = 2
print(number0)

//オプショナル型
var number1 : Int?
number1 = 2
print(number1)
print(number1!)

//暗黙的オプショナル型
var number2 : Int!
number2 = 2
print(number2)


print(number1 + number2)
print(number1 + number2!)

以下、答え合わせです。

Answer
//非オプショナル型
var number0 : Int = 2
print(number0) //2 

↑これは非オプショナル型といって、nilを代入できないデータ型です。
そのため、宣言の時点ですでに初期値が入っているので、print関数にはそのまま初期値の2が出力されます。

Answer

//オプショナル型
var number1 : Int?
number1 = 2
print(number1)//Optional(2)
print(number1!) //2

オプショナル型はnilを代入できるデータ型です。
宣言した段階では、値は何も入っていないnilの状態です。
普通、nilだとクラッシュしますが、オプショナル型にすることで、(型の最後に!や?をつけることで)、number1という変数は包み紙(ラップ)で覆われている状態となるので、中身がnilでもクラッシュしません。
ここで、もし初期値を代入しないのに、!や?をつけないとnilが宙に浮いている状態となってクラッシュします。

次に、出力の部分です。
今回のnumber1は2を途中で代入しています。
しかしそのままprint(number1)と出力すると、先ほど説明した包み紙がついた状態なので、Optional(2)と出力されます。
この包み紙(ラップ)を外して数字の"2"として扱いたい場合は、オプショナル型の変数の後ろに!をつけてあげます。
このことをアンラップ(unwrap)と言います。

Answer

//暗黙的オプショナル型
var number2 : Int!
number2 = 2
print(number2) //2

暗黙的に宣言する(宣言の時に型の最後に"!"をつける)と、
その変数を次使う時に、包み紙(ラップ)が外れている状態になるので、先ほどのようにアンラップ(number2!とすること)する必要がありません。

以上のことを踏まえて、最後の足し算の部分をみてみます。
答えの1つ目は違う型同士の足し算になってしまうのでエラーとなります。
そのため、number1は2つ目のようにアンラップしてから使いましょう!
オプショナル型で宣言したときは、使用時にアンラップをお忘れなく!

Answer
print(number1 + number2) //Optional(2)+2
print(number1! + number2)//4

そもそもなんでOptional型いるの??について
例えばSNSでは、最初にユーザー登録をしますよね!
Facebookなどでは、名前は初めから必要ですが、プロフィール画像は初めから追加しなくてもアプリとして使えるはずです。
つまり、プロフィール画像はユーザーが写真を追加するまで、値が代入されない"nil"の状態を維持します。
このように全てのものに初期値があるわけではないのでOptional型が必要ってわけです!

③引数

引数って言葉聞いたことありますか?
"いんすう"ではなく、"ひきすう"って読みます!

Question ()は何を表してますか?

//パターン1
func sample(){
//ここに何か処理をかく
}

Answer
()は、引数(ひきすう)です!
引数とは、関数やプログラムなどに渡す値のこと。処理が行われるためのトリガーのような存在です。

例えば、2進数⇄10進数に変換してくれる機械があったとします。
その機械に"1010"(2進数)と入力すると、"10"(10進数)と変換されます。
この場合、最初に入れた"1010"が引数、処理後に出てきた"10"が戻り値となります。

パターン1の場合、
()←これは、引数がnil(空)であるという意味です。
つまり、この関数が発動して、中に書いた処理が読み込まれるには、この関数がどこかで呼ばれる必要があります。
(トリガーとしての役割を担う引数が存在しないケースですからね!)
例えば、画面を開いた瞬間に、この関数の中身の処理が行われて欲しい場合は、viewDidLoadの中などに書いてあげましょう!以下のように書きます!

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

こんな感じです。

もう一つみてみましょう!

//パターン2
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
}

パターン2に書いたtableViewのカッコ内
(_ tableView: UITableView, numberOfRowsInSection section: Int)
この部分には色々と書かれていますね!これも引数です!
これは元から引数が存在するので、先ほどのパターン1のように、この関数をどこかに書く必要はありません!きちんと読み込まれます!

④delegate

実際に、tableViewの画面遷移を扱うあたりから登場しますよね!
最初は、とりあえず書いとけ!みたいなノリで書いてるかもしれませんが、他のところでも色々お世話になると思うので理解しておきましょう!
tableViewを例にdelegateについて考えてみます。

Question 
ViewController の上に TableView を配置した場合delegateはどうやってかくでしょうか?
//delegate周りで押さえておきたい3つのポイントを意識しよう!
Answer
//TableViewの場合
class ViewController: UIViewController,UITableViewDelegate { //①ここにUITableViewDelegate(protocol)をかく

@IBOutlet var TableView : UITableView!

 override func viewDidLoad() {
        super.viewDidLoad()
        TableView.delegate = self //②上で追加したプロトコルの実装をお任せ
}

//③delegateを元にした実装
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // セルがタップされたら行われる処理

}

詳しく説明していきます。

まず、delegateを実装するには、3つの要素が必要になります。
1.protocol
2.処理を依頼するクラス(この場合はTableView)
3.依頼された処理を実行するクラス(この場合はViewController)

ViewControllerの上にtableViewを配置している構造を、わかりやすく、例えて
1階がViewController、2階がtableViewのマンションがあるとします。
この1,2階は繋がっていません。(という設定)
delegateが書かれていないと、2階にあるものを1階に届けたいけどどうしよう繋がってないし、、っていう状況です。

この状況打破のため、2階のtableViewが実はわたしハシゴ持ってるよ!っていう手段を提示します。
これがprotocolです。(上でいう①)

次に、tableView自身が
「ハシゴは持ってるけど、設置はできないから、1階のViewControllerがやってね!実装は任せたよ!」
っていうのがprotocolの実装を任せている上の②の部分です。

③はハシゴが設置された結果できるようになったこと。つまり今回は、tableViewがタップされると、その下のViewcontrollerが"タップされたこと"を認識できて、タップされたらどうしたいかの処理を(コーディングしたものを)ViewControllerがやってくれることになります。

それではtextfieldで練習です!

Question

//ViewControllerの上にtextfieldを配置した場合

textfield.delegate = self
これはなんのために書いてるのでしょうか?

Answer
これも先ほどの説明同様に、textfieldのprotocolをViewControllerに任せているって意味です!

皆さんはtextfield(例えばSNSのログインでユーザーIDとかパスワードとか)で文字を打ち込んだ後、enterを押してキーボードを閉じていませんか?これができているのはこのdelegateのおかげなのです!
textfieldだけではキーボードを閉じることができないので、delegateを使ってViewControllerにその処理をお任せして閉じてもらっています!

1.protocol
→UITextFieldDelegate
2.処理を依頼するクラス(この場合はTextField)
→enter押されたら、押されたタイミングをViewControllerに知らせてキーボード閉じる処理を依頼
3.依頼された処理を実行するクラス(この場合はViewController)
→ViewControllerが2の依頼を実行(キーボードを閉じる)

delegate周りの3点は覚えておきましょう!

⑤append

英語のappendって意味知ってますか?
appendはaddの派生語ですね!つまり"追加"って感じです。
以下で、配列にappendするコードをみていきます。

Question

var memoArray = ["りんご","ゴール","ルール"]

@IBAction func sample(){
      memoArray.append("日本")
      print(memoArray)//ここで出力されるものは?
}

Answer
["りんご","ゴール","ルール","日本"]
となります。

このように普通にappend(追加)した場合は1番最後に追加されます!
では、複数追加したい場合や、元の配列の途中に追加したい場合はどうなるでしょうか、、?

Question
var memoArray = ["りんご","ゴール","ルール"]

@IBAction func sample(){
      //①"ルビー"と"ビー玉"を配列の最後に追加するコードを書いてください→["りんご","ゴール","ルール","ルビー","ビー玉"]となればおけ

      //②"うり"をmemoArrayの最初に追加してください→["うり","りんご","ゴール","ルール"]となればおけ
}

Answer
①memoArray.append(contentsOf: ["ルビー","ビー玉"])
②memoArray.insert("うり", at: 0)

それぞれ上記のようにかけば対応できます!
②で、配列の一番初めは0番目になるので、1にしないようにそこだけ注意しましょう!

追加されるもの.append(追加したいもの)
となります!

⑥protocol

先ほど④のところで少しだけ登場したprotocolです!
④では、UITableViewDelegateなど1ワードで登場しましたが、今回は少し長いprotocolの登場です。

TimelineTableViewCell.swift
Question 
Twitterのいいね機能(セルの上のボタンをタップ)
はどのような仕組みになっているでしょうか?
ViewController  TimelineTableViewCell のカスタムセルを配置した場合を考えます!

protocol TimelineTableViewCellDelegate{
    func didTapButton(timelineTableviewCell: UITableViewCell, button: UIButton)

}

class TimelineTableViewCell: UITableViewCell {

 var delegate: TimelineTableViewCellDelegate?

 @IBAction func Button(button: UIButton){
        self.delegate?.didTapButton(timelineTableviewCell: self ,button: button)
   }
}
ViewController.swift
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, TimelineTableViewCellDelegate {

func didTapButton(timelineTableviewCell: UITableViewCell, button: UIButton) {
   //ここにボタンをタップした後の処理

 }

}

Answer
protocolは④のdelegateで書いた通り、delegate処理に必要な要素の1つです。
今回は④とは違い、デフォルトに存在しないものを使いたいので、protocol宣言をします。

TimelineTableViewCell.swift
//protocol宣言で、これが使えますっていう手の内を公開
protocol TimelineTableViewCellDelegate{
    func didTapButton(timelineTableviewCell: UITableViewCell, button: UIButton)

}

delegate周りの時に登場する3つの役割は、今回の場合こんな感じです!

1.protocol
→TimelineTableViewCellDelegate(宣言して自分で置いたもの)
2.処理を依頼するクラス(この場合はTimelineTableViewCell)
→ボタン押されたら、押されたタイミングをViewControllerに知らせる
3.依頼された処理を実行するクラス(この場合はViewController)
→2の依頼を受けて、ボタンが押された後の処理を実行

⑦for文とwhile文

for文とwhile文は繰り返しのループ文ですよね!
それぞれ使い分けしていますか?
一度わかれば簡単です見ていきましょう!

Question それぞれprint関数には何が出力されるでしょうか?

for i in 0...9{
  number = Int(arc4random_uniform(20))
  print(number)
}

while number < 10{
  number = Int(arc4random_uniform(20))
  print(number)
}

Answer
1つ目は、乱数発生を10回繰り返すので、0~19までの20個の数字で発生した乱数が10個出力されます。
2つ目は、0~19までの中で乱数が発生して、その発生した数字が10未満だったら、while文が繰り返されますが、10以上の乱数が表示された場合はそこで繰り返し文の読み込みは終了します。なので、出力される個数は一定ではありません。

回数が決まってる時はfor文を使います。
今回は10回、回したいっていう繰り返し文なので
for i in 0...9
としました!

一方、ある条件に達するまで繰り返したい場合はwhile文を使います。
今回の条件は number < 10 ということ。
numberの値が0~9の場合は永遠にwhile文が繰り返されるという条件が前提です。

ちなみにfor文は配列と組み合わせるのに最適です!

let memoArray = ["りんご","ゴール","ルール"]

for i in memoArray{

}

このように書くと、宣言した配列memoArrayのそれぞれの単語にアクセスすることが可能になります!

また、配列の何番がなんの言葉か、というindexも取得できます

let memoArray = ["りんご","ゴール","ルール"]

for (index, memo) in memoArray.enumerated() {
    print(index, memo)
    // 0 りんご
    // 1 ゴール
    // 2 ルール
}

以上のように、print関数に出力されるので、indexを取得すれば、配列の何番目以上はこうしたい、みたいな条件分岐などをすることができます!

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

swift はじめの一歩

はじめに

みなさんdelegateとかのイメージはついてますか?
この記事では、私が勉強を始めて1,2ヶ月目で理解するのに苦労した部分(今回は7選)を中心に、
こんな解説があったらよかったなぁ〜と思っていたシリーズを集めて、
初心者がひっかかりやすいであろうところ、基本事項をQ&A方式でまとめました!
確認の意味でやりたい場合は、Questionだけみて答えを考えてください!
全くわからない人は、読みたいところを読んでみてください!☺️

①letとvarの違い

まずは、一番初めの宣言の部分から!簡単な人には簡単です!
(ちなみに私は始めた頃さっぱりわかってませんでした、、?)

Question
let  var の違いは?

Answer
letは定数、varは変数です。
variableのvarですね!

varは、1度データを入れても、後でデータを書き換えることができます。
一方、letは1度入れたデータを書き換えることができません。

ここで多くのみなさんはこう思うはず。
「全てvarで良いのでは、、?」
実際当時のわたしも、こう思っていました!笑

確かに、できないことはないのですが、もしletで良いところをvarで書いてしまうと、
変更するつもりのない変数を間違えて、変更してしまった時に気づかず、余計なエラーを自ら引き起こしてしまう、、という懸念があるので(複雑になるほどそうなるかもです)、しっかり使い分けていきましょう!

var number : Int = 0 //最初は0(初期値)
number = 2 //上書き保存されます

let number : Int = 0
number = 2 //これはアウト letで宣言したので初期値0から書き換え不可能です

②オプショナル型

次に、非オプショナル型・オプショナル型・暗黙的オプショナル型についてです。
宣言のところで "!" をよく見かけませんか?
これってなんだろう、、とりあえずよくわからないけど、書いとこう!って人多いと思います!
そこで、以下の例題を踏まえ考えていきましょう!

Question それぞれのprint関数は何が出力されるでしょうか?

//非オプショナル型
var number0 : Int = 2
print(number0)

//オプショナル型
var number1 : Int?
number1 = 2
print(number1)
print(number1!)

//暗黙的オプショナル型
var number2 : Int!
number2 = 2
print(number2)


print(number1 + number2)
print(number1 + number2!)

以下、答え合わせです。

Answer
//非オプショナル型
var number0 : Int = 2
print(number0) //2 

↑これは非オプショナル型といって、nilを代入できないデータ型です。
そのため、宣言の時点ですでに初期値が入っているので、print関数にはそのまま初期値の2が出力されます。

Answer

//オプショナル型
var number1 : Int?
number1 = 2
print(number1)//Optional(2)
print(number1!) //2

オプショナル型はnilを代入できるデータ型です。
宣言した段階では、値は何も入っていないnilの状態です。
普通、nilだとクラッシュしますが、オプショナル型にすることで、(型の最後に!や?をつけることで)、number1という変数は包み紙(ラップ)で覆われている状態となるので、中身がnilでもクラッシュしません。
ここで、もし初期値を代入しないのに、!や?をつけないとnilが宙に浮いている状態となってクラッシュします。

次に、出力の部分です。
今回のnumber1は2を途中で代入しています。
しかしそのままprint(number1)と出力すると、先ほど説明した包み紙がついた状態なので、Optional(2)と出力されます。
この包み紙(ラップ)を外して数字の"2"として扱いたい場合は、オプショナル型の変数の後ろに!をつけてあげます。
このことをアンラップ(unwrap)と言います。

Answer

//暗黙的オプショナル型
var number2 : Int!
number2 = 2
print(number2) //2

暗黙的に宣言する(宣言の時に型の最後に"!"をつける)と、
その変数を次使う時に、包み紙(ラップ)が外れている状態になるので、先ほどのようにアンラップ(number2!とすること)する必要がありません。

以上のことを踏まえて、最後の足し算の部分をみてみます。
答えの1つ目は違う型同士の足し算になってしまうのでエラーとなります。
そのため、number1は2つ目のようにアンラップしてから使いましょう!
オプショナル型で宣言したときは、使用時にアンラップをお忘れなく!

Answer
print(number1 + number2) //Optional(2)+2
print(number1! + number2)//4

そもそもなんでOptional型いるの??について
例えばSNSでは、最初にユーザー登録をしますよね!
Facebookなどでは、名前は初めから必要ですが、プロフィール画像は初めから追加しなくてもアプリとして使えるはずです。
つまり、プロフィール画像はユーザーが写真を追加するまで、値が代入されない"nil"の状態を維持します。
このように全てのものに初期値があるわけではないのでOptional型が必要ってわけです!

③引数

引数って言葉聞いたことありますか?
"いんすう"ではなく、"ひきすう"って読みます!

Question ()は何を表してますか?

//パターン1
func sample(){
//ここに何か処理をかく
}

Answer
()は、引数(ひきすう)です!
引数とは、関数やプログラムなどに渡す値のこと。処理が行われるためのトリガーのような存在です。

例えば、2進数⇄10進数に変換してくれる機械があったとします。
その機械に"1010"(2進数)と入力すると、"10"(10進数)と変換されます。
この場合、最初に入れた"1010"が引数、処理後に出てきた"10"が戻り値となります。

パターン1の場合、
()←これは、引数がnil(空)であるという意味です。
つまり、この関数が発動して、中に書いた処理が読み込まれるには、この関数がどこかで呼ばれる必要があります。
(トリガーとしての役割を担う引数が存在しないケースですからね!)
例えば、画面を開いた瞬間に、この関数の中身の処理が行われて欲しい場合は、viewDidLoadの中などに書いてあげましょう!以下のように書きます!

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

こんな感じです。

もう一つみてみましょう!

//パターン2
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
}

パターン2に書いたtableViewのカッコ内
(_ tableView: UITableView, numberOfRowsInSection section: Int)
この部分には色々と書かれていますね!これも引数です!
これは元から引数が存在するので、先ほどのパターン1のように、この関数をどこかに書く必要はありません!きちんと読み込まれます!

④delegate

実際に、tableViewの画面遷移を扱うあたりから登場しますよね!
最初は、とりあえず書いとけ!みたいなノリで書いてるかもしれませんが、他のところでも色々お世話になると思うので理解しておきましょう!
tableViewを例にdelegateについて考えてみます。

Question 
ViewController の上に TableView を配置した場合delegateはどうやってかくでしょうか?
//delegate周りで押さえておきたい3つのポイントを意識しよう!
Answer
//TableViewの場合
class ViewController: UIViewController,UITableViewDelegate { //①ここにUITableViewDelegate(protocol)をかく

@IBOutlet var TableView : UITableView!

 override func viewDidLoad() {
        super.viewDidLoad()
        TableView.delegate = self //②上で追加したプロトコルの実装をお任せ
}

//③delegateを元にした実装
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // セルがタップされたら行われる処理

}

詳しく説明していきます。

まず、delegateを実装するには、3つの要素が必要になります。
1.protocol
2.処理を依頼するクラス(この場合はTableView)
3.依頼された処理を実行するクラス(この場合はViewController)

ViewControllerの上にtableViewを配置している構造を、わかりやすく、例えて
1階がViewController、2階がtableViewのマンションがあるとします。
この1,2階は繋がっていません。(という設定)
delegateが書かれていないと、2階にあるものを1階に届けたいけどどうしよう繋がってないし、、っていう状況です。

この状況打破のため、2階のtableViewが実はわたしハシゴ持ってるよ!っていう手段を提示します。
これがprotocolです。(上でいう①)

次に、tableView自身が
「ハシゴは持ってるけど、設置はできないから、1階のViewControllerがやってね!実装は任せたよ!」
っていうのがprotocolの実装を任せている上の②の部分です。

③はハシゴが設置された結果できるようになったこと。つまり今回は、tableViewがタップされると、その下のViewcontrollerが"タップされたこと"を認識できて、タップされたらどうしたいかの処理を(コーディングしたものを)ViewControllerがやってくれることになります。

それではtextfieldで練習です!

Question

//ViewControllerの上にtextfieldを配置した場合

textfield.delegate = self
これはなんのために書いてるのでしょうか?

Answer
これも先ほどの説明同様に、textfieldのprotocolをViewControllerに任せているって意味です!

皆さんはtextfield(例えばSNSのログインでユーザーIDとかパスワードとか)で文字を打ち込んだ後、enterを押してキーボードを閉じていませんか?これができているのはこのdelegateのおかげなのです!
textfieldだけではキーボードを閉じることができないので、delegateを使ってViewControllerにその処理をお任せして閉じてもらっています!

1.protocol
→UITextFieldDelegate
2.処理を依頼するクラス(この場合はTextField)
→enter押されたら、押されたタイミングをViewControllerに知らせてキーボード閉じる処理を依頼
3.依頼された処理を実行するクラス(この場合はViewController)
→ViewControllerが2の依頼を実行(キーボードを閉じる)

delegate周りの3点は覚えておきましょう!

⑤append

英語のappendって意味知ってますか?
appendはaddの派生語ですね!つまり"追加"って感じです。
以下で、配列にappendするコードをみていきます。

Question

var memoArray = ["りんご","ゴール","ルール"]

@IBAction func sample(){
      memoArray.append("日本")
      print(memoArray)//ここで出力されるものは?
}

Answer
["りんご","ゴール","ルール","日本"]
となります。

このように普通にappend(追加)した場合は1番最後に追加されます!
では、複数追加したい場合や、元の配列の途中に追加したい場合はどうなるでしょうか、、?

Question
var memoArray = ["りんご","ゴール","ルール"]

@IBAction func sample(){
      //①"ルビー"と"ビー玉"を配列の最後に追加するコードを書いてください→["りんご","ゴール","ルール","ルビー","ビー玉"]となればおけ

      //②"うり"をmemoArrayの最初に追加してください→["うり","りんご","ゴール","ルール"]となればおけ
}

Answer
①memoArray.append(contentsOf: ["ルビー","ビー玉"])
②memoArray.insert("うり", at: 0)

それぞれ上記のようにかけば対応できます!
②で、配列の一番初めは0番目になるので、1にしないようにそこだけ注意しましょう!

追加されるもの.append(追加したいもの)
となります!

⑥protocol

先ほど④のところで少しだけ登場したprotocolです!
④では、UITableViewDelegateなど1ワードで登場しましたが、今回は少し長いprotocolの登場です。

TimelineTableViewCell.swift
Question 
Twitterのいいね機能(セルの上のボタンをタップ)
はどのような仕組みになっているでしょうか?
ViewController  TimelineTableViewCell のカスタムセルを配置した場合を考えます!

protocol TimelineTableViewCellDelegate{
    func didTapButton(timelineTableviewCell: UITableViewCell, button: UIButton)

}

class TimelineTableViewCell: UITableViewCell {

 var delegate: TimelineTableViewCellDelegate?

 @IBAction func Button(button: UIButton){
        self.delegate?.didTapButton(timelineTableviewCell: self ,button: button)
   }
}
ViewController.swift
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, TimelineTableViewCellDelegate {

func didTapButton(timelineTableviewCell: UITableViewCell, button: UIButton) {
   //ここにボタンをタップした後の処理

 }

}

Answer
protocolは④のdelegateで書いた通り、delegate処理に必要な要素の1つです。
今回は④とは違い、デフォルトに存在しないものを使いたいので、protocol宣言をします。

TimelineTableViewCell.swift
//protocol宣言で、これが使えますっていう手の内を公開
protocol TimelineTableViewCellDelegate{
    func didTapButton(timelineTableviewCell: UITableViewCell, button: UIButton)

}

delegate周りの時に登場する3つの役割は、今回の場合こんな感じです!

1.protocol
→TimelineTableViewCellDelegate(宣言して自分で置いたもの)
2.処理を依頼するクラス(この場合はTimelineTableViewCell)
→ボタン押されたら、押されたタイミングをViewControllerに知らせる
3.依頼された処理を実行するクラス(この場合はViewController)
→2の依頼を受けて、ボタンが押された後の処理を実行

⑦for文とwhile文

for文とwhile文は繰り返しのループ文ですよね!
それぞれ使い分けしていますか?
一度わかれば簡単です見ていきましょう!

Question それぞれprint関数には何が出力されるでしょうか?

for i in 0...9{
  number = Int(arc4random_uniform(20))
  print(number)
}

while number < 10{
  number = Int(arc4random_uniform(20))
  print(number)
}

Answer
1つ目は、乱数発生を10回繰り返すので、0~19までの20個の数字で発生した乱数が10個出力されます。
2つ目は、0~19までの中で乱数が発生して、その発生した数字が10未満だったら、while文が繰り返されますが、10以上の乱数が表示された場合はそこで繰り返し文の読み込みは終了します。なので、出力される個数は一定ではありません。

回数が決まってる時はfor文を使います。
今回は10回、回したいっていう繰り返し文なので
for i in 0...9
としました!

一方、ある条件に達するまで繰り返したい場合はwhile文を使います。
今回の条件は number < 10 ということ。
numberの値が0~9の場合は永遠にwhile文が繰り返されるという条件が前提です。

ちなみにfor文は配列と組み合わせるのに最適です!

let memoArray = ["りんご","ゴール","ルール"]

for i in memoArray{

}

このように書くと、宣言した配列memoArrayのそれぞれの単語にアクセスすることが可能になります!

また、配列の何番がなんの言葉か、というindexも取得できます

let memoArray = ["りんご","ゴール","ルール"]

for (index, memo) in memoArray.enumerated() {
    print(index, memo)
    // 0 りんご
    // 1 ゴール
    // 2 ルール
}

以上のように、print関数に出力されるので、indexを取得すれば、配列の何番目以上はこうしたい、みたいな条件分岐などをすることができます!

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