20201125のiOSに関する記事は13件です。

CoreMLモデルのサイズを小さくする量子化でモデルの精度はどう変わるのか

モバイルに載せるなら、モデルのサイズは小さいほうがいいと思う。
量子化とは、重みパラメータを、例えばFloat_32からFloat_16にして早めの桁で切り上げるようにすること。

実際のモデルを量子化して、どれくらいサイズが小さくなるのか、精度はどう変わるのか、をやってみた。
スクリーンショット 2020-11-25 23.04.22.png

deeplabv3のxeptionバックボーンバージョンでセマンティックセグメンテーションを比較(背景ぼかし)。

xeptionバックボーンのdeeplabv3のmlmodelのサイズは165.1MB。
MobileNetバックボーンだとModelが8.6MBなので、xeptionの方が精度はずっといいとはいえ、さすがにもうちょっと小さくしたいところ。

CoreMLModelの量子化の方法

from coremltools.models.neural_network import quantization_utils

# 可能な値はnbits = 16, 8, 7, 6, ...., 1
nbits = 16
quantized_model = quantization_utils.quantize_weights(model, nbits)
coremltools.utils.save_spec(quantized_model, 'quantizedDeepLab.mlmodel')
# Tips: 量子化したmlmodelは、なぜか.save(Path)メソッドが使えないので、coremltools.utils.save_specで保存します。

結果

モデル サイズ(MB) 速度(s) 使用メモリ(MB)
Float32 165.1 315 477
Float16 82.6 2.72 477
Float8 41.8 2.77 477

モデルのサイズは見事に半分半分になった。
しかし、実行速度と使用メモリは変わらなかった。

さて、セグメンテーションの精度はというと・・・

左から【オリジナル】  【Float32】   【Float16】     【Float8】
            165MB    82MB       41MB
IMG_2906.JPGx.png16.png8.png

おわかりいただけたであろうか?
32と16ではほぼ全くセグメント精度は変わらない。
「8だとさすがに雑になるでしょ」と思いきや、あにはからんや、これもほとんど変わらない。

よく見ると右下の花をぼかしが少し侵食しているが、これくらいなら十分32と同じように使える。

バンドルするモデルのサイズを小さくしたければ、量子化は有効な選択肢になると思う。
他のタスクでどう違ってくるのかは試してないけど。
*今回はLinearというデフォルトの量子化方法を使った。Coremltoolsの引数で別の種類も選択できるらしい。

?


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium

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

2020-11-25時点でMongoDB RealmをiOSで使う場合の注意点

経緯

いま、iOS/Android(おそらくWebも)対応予定のアプリをprivateで書いています。
クロスプラットフォームでデータを同期するようなアプリとなる予定なので、記事タイトルにあるMongoDB Realmを利用することにしました。

というような経緯で、MongoDB RealmをiOSアプリで導入しようとしたら、2020-11-25時点でいくつか注意する点があったので、メモがてら残しておきます。

Be sure to select MongoDB version 4.4

見出しの通りなのですが、MongoDB Realmでは、MongoDB version 4.4(2020-11-25時点)である必要があります。

MongoDBが2019年にRealmを買収したため、2020年頃にRealmとMongoDBを統合したサービスに刷新され、以下の構成でworkするようになっています。
スクリーンショット 2020-11-25 20.39.07.png
https://www.mongodb.com/realm より

稲妻アイコンがRealmで葉っぱアイコンがMongoDBです。

MongoDB Realm利用者は、AWS / Google Cloud / Azure上にMongoDB用のクラスタを構築する必要があります。
このあたりは、MongoDB RealmがWeb UI(Realm UIと呼ばれています)を提供してくれているので、それ経由で簡単に構築できます。なのでRealm UIに従いサクサク進めていくと、見落としがちなのですが(私がそうでした :cry:)、MongoDBのバージョンは4.4である必要があります。
default versionが4.2になっていたり、いくつかのregionは4.2までしか利用できない、という状況なので、利用する際には気をつけてください :pray:

Choose your preferred provider and region, tier, and additional settings. As you build your cluster, Atlas displays the associated costs at the bottom of the page.

Be sure to select MongoDB version 4.4 for your cluster if you wish to use sync!

https://docs.mongodb.com/realm/get-started/create-realm-app/#b-create-an-atlas-cluster

(ドキュメントにはちゃんと書いてあります。sync機能を使う場合はと記載されているのですが、MongoDB Realmを採用するにも関わらず、sync機能を使わないケースはあまりない気がするので、実質必須と言ってしまって良い気がします。)

SwiftPM経由でRealmを導入できない?

こちらのドキュメントを読みつつiOSアプリにRealmの導入を進めていると、
こちらに記載されている通り、RealmはSwiftPM対応されているとのことなので、SwiftPM経由でRealmを依存に追加しました。

追加後もドキュメントに従い、import文を追加、

import RealmSwift

さらに下記を追加したのですが、Appは見つからない、というようなエラーが出てbuildできませんでした :cry:

let app = App(id: YOUR_REALM_APP_ID) // Replace YOUR_REALM_APP_ID with your Realm app ID

「import文が足りない?SwiftPMにおける依存の追加に誤りがある?」:thinking: などと自身の見落としを疑いドキュメントを改めて読んでみたのですが、特に問題はなさそうでした。
そもそもApp.swiftが存在していないのでは、ということで、realm-cocoaのPackage.swiftを見てみると、App.swiftexcludeされており、こちらが原因のように見えました :cry:
試しにCocoaPods経由で追加すると無事buildが通るようになりました。

ドキュメント上では、SwiftPM経由で追加した場合にはApp.swiftは無い(利用できない)、というようなことは何も記載されていないにも関わらずexcludeされている原因が気になったのでPRを追ってみました。

Package.swiftのexclude対象としてApp.swiftが追加されたのは、こちらのPRですが、このPRではRealmApp -> Appというrenameをしただけのようで、rename前のRealmApp.swiftがexclude対象になったのは、こちらのPRのようでした。
PRコメントとしてはDescription TK.と書かれているのみだったので、
CHANGELOG.mdを読んでみると、このPRでMongoDB統合をサポート開始したようでした。

Add support for next generation sync. Support for syncing to MongoDB instead of Realm Object Server. Applications must be created at realm.mongodb.com

ただ、知りたかったRealmApp.swiftPackage.swiftにてexcludeされるようになった理由については特に記載はなさそうでした。commit logもこれだけで、理由はクリアにならなかったので、issueで聞いてみるとことにしました。

issueを立てようとすると、下記のISSUE_TEMPLATEが存在し、

!!! MANDATORY TO FILL OUT !!!
<!---

**Questions**: If you have questions about HOW TO USE Realm, ask on
[StackOverflow](http://stackoverflow.com/questions/ask?tags=realm),
or in our [Swift Forum](https://forums.realm.io/c/swift) or [Obj-C Forum](https://forums.realm.io/c/objc).

**Feature Request**: Just fill in the first two sections below.

**Bugs**: To help you as fast as possible with an issue please describe your issue
and the steps you have taken to reproduce it in as many details as possible.

-->

## Goals
<!--- What do you want to achieve? -->

## Expected Results
<!--- What did you expect to happen? -->

## Actual Results
<!--- What happened instead?
e.g. the stack trace of a crash
-->

## Steps for others to Reproduce
<!--- What are steps OTHERS can follow to reproduce this issue? -->

## Code Sample
<!---
Provide a code sample or test case that highlights the issue.
If relevant, include your model definitions.
For larger code samples, links to external gists/repositories are preferred.
Alternatively share confidentially via mail to help@realm.io.
Full Xcode projects that we can compile ourselves are ideal!
-->

## Version of Realm and Tooling
<!---
[In the CONTRIBUTING guidelines](https://git.io/vgxJO), you will find a script,
which will help determining some of these versions.
-->
Realm framework version: ?

Realm Object Server version: ?

Xcode version: ?

iOS/OSX version: ?

Dependency manager + version: ?

今回の件だと、テンプレート中に記載があるSwift Forumのほうが適していそう、かつ、ぱっと見、同じような質問はなさそうだったので、そちらで質問してみることにしました。

で、いまWhy is App.swift exclude from Package.swift?というタイトルの投稿をしてみたのですが、どうやら承認が必要なようで、まだ表示されていません。

We've received your new post but it needs to be approved by a moderator before it will appear. Please be patient.

You have 1 post pending.

なにか返信など来たら追記したいと思います。

2020-11-25 22:43追記

当記事を公開していたら、早速返信してもらうことができました。(めちゃくちゃ早い)
https://developer.mongodb.com/community/forums/t/why-is-app-swift-exclude-from-package-swift/12160

スクリーンショット 2020-11-25 22.44.26.png
スクリーンショット 2020-11-25 22.45.30.png

ということで、Realmのsync機能はSPMをサポートしていないようです :pray:
素直にCocoapods or Carthage経由で利用しましょう。
(ドキュメントに書いておいてくれても良さそうな気もしますが、sync機能はβだったりするので、整備中なのかもしれません)

さいごに

なんというか、まとまりのない文章になってしまいましたが、言いたかったのは下記2点です。

  • Be sure to select MongoDB version 4.4
  • SwiftPM経由でRealmを導入できない?

以上です!

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

ProvisioningProfileの有効期限をチェックする

InHouse配布のiOSアプリは、ProvisioningProfileの有効期限が切れるとアプリが起動しなくなる。
アプリが起動しない状況を未然に防ぐため、
ProvisioningProfileの有効期限をチェックする機構を検討した。

前提条件

  • ProvisioningProfileはFastlane Matchで管理されていること

仕組み

主にFastlaneを利用する。
1. Fastlane MatchにてProvisioningProfileを取得する
2. OpenSSLにてProvisioningProfileをデコードし、Plistファイルを得る
3. Plistファイルから有効期限を取得する

サンプル

Fastlaneにて有効期限をチェックするサンプル

  lane :check_provisioningprofile do
    # Fastlane MatchにてProvisioningProfileを取得する
    match(
      readonly: true,
      git_url: #{matchリポジトリのURL},
      app_identifier: #{Bundle ID},
      output_path: "/tmp/check_provisioningprofile"
    )

    Dir.glob("/tmp/check_provisioningprofile/*.mobileprovision") do |mobileprovision|
      # OpenSSLにてProvisioningProfileをデコードし、Plistファイルを得る
      sh("openssl smime -inform der -verify -noverify -in #{mobileprovision} > /tmp/decoded.plist")

      # PlistファイルからProvisioningProfileの名称を取得する
      profile_name = get_info_plist_value(path: "/tmp/decoded.plist", key: "Name")

      # Plistファイルから有効期限を取得する
      expiration_date_value = get_info_plist_value(path: "/tmp/decoded.plist", key: "ExpirationDate")
      expiration_date = DateTime.parse(expiration_date_value.to_s)

      # 有効期限が30日後に迫っていたらエラーとする
      warning_threshold = DateTime.now + 30
      if warning_threshold > expiration_date
        UI.user_error!("It will expire soon.\n---> #{profile_name}")
      end
    end
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift5]containsを使って特定の文字列を検索する方法

containsメソッドとは

文字列の中にある特定の文字列を検索するメソッドです。

contains("検索したい文字列")

実用例

var firstText  = "2020年日本一はソフトバンク"
var secondText = "2021年は阪神タイガースが優勝"
var thirdText  = "baseball"

if firstText.contains("ソフトバンク") { //true
  print("含まれているので呼ばれる")
}

if secondText.contains("巨人") { //false
  print("含まれていないので呼ばれない")
}

//以下、lowercased()で大文字小文字を無視して検索

if thirdText.contains("BaseBall".lowercased()) { //true
  print("含まれているので呼ばれる")
}

if thirdText.contains("Soccer".lowercased()) { //false
  print("含まれていないので呼ばれない")
}

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

iOSアプリのサイズを軽量化したときの話

先週、iOSアプリのプロジェクトを見直し、35.8 MB から 17.3 MB まで 軽量化しました。
その方法とは「参照しなくなったファイルは、すぐプロジェクトから消しましょう」というだけのお話です。

iOSアプリのサイズを最適化する

Appleは、iOS11の公開にあわせて、モバイルデータ通信時にApp Storeから
アプリをダウンロードするときの容量制限を 100 MB から 150 MB に拡大しました。

https://developer.apple.com/news/?id=09192017b

さらに、iPhone端末自体の大容量化や、LTEや5G回線により
高速通信が可能になったことで、大きなサイズのアプリも不自由なく利用できるようになりました。
とは言え、アプリのサイズはもちろん今でも小さいほうがいいです!

私自身も、当初からアプリのサイズは小さくするように開発していました。
写真はその都度ダウンロードするため、アプリの中には5枚くらいしか入ってない...はずでした。

私がはじめに着目したのは、データベースファイルです。

SQLiteファイルを最適化する

私の使用しているSQLiteファイルには、テキストと写真のURLなどが保存されています。
調べてみると、SQLiteはテーブルからデータを削除したらすぐSQLiteファイルのサイズが小さくなる訳では無いようです。
ファイルの中で利用されていた領域はすぐに削除されるのではなく次にデータが追加された時に再利用されます。

https://www.dbonline.jp/sqlite/manage/index1.html

私のアプリではデータの削除はしていませんので、これは関係ありませんでした。

アプリサイズの内訳をチェックする

今回、ビンゴだった解決策はこちらです。
アプリサイズの内訳を確認してみたところ、不要ファイルがたくさん含まれていました。
(まさか、プロジェクトに含まれてたら使ってなくても一緒にコンパイルされてしまうなんて、、)

その方法は、まず以下の手順でOrganizerを開きます。

関連記事)https://qiita.com/kenkubomi/items/5e8b4aec981dfc9812c2

その後、確認したいアーカイブを選択後、
右クリックして「Show In Finder」を選択。ファインダーで開きます。

次に、開いたディレクトリにある .xcarchiveファイルを右クリックして、
「パッケージの内容を表示」を選択します。

さらに、Products > Applications の中にある、
.appファイルを右クリックして、先ほどと同様に「パッケージの内容を表示」を選択します。

この中のファイルをサイズの大きい順に並べたら一目瞭然ですね!
あまりにサイズの大きい画像などは、縮小や圧縮をしてみても良いかもしれません。

見事、ダイエットに成功!

グループ 1.png

アプリサイズが大きかった原因が分かってスッキリです!

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

Carview に導入したい iOS の最新技術

はじめに

Carview Advent Calender が始まりました! 1 日目の記事は、 iOS エンジニアの @zwtin が担当します。普段は iOS や Flutter1 の記事を書いています。今回はアプリエンジニア以外の方が見てくださる機会だと思うので嬉しいです!これを機会に、初日ですしそこまで踏み込んだコードの話は後日の方々におまかせし(決して手抜きではありません)、私は「 Carview に導入したい iOS の最新技術」と題して、 iOS では今こういう技術が流行ってます!というのをお伝えできればと思います。

導入したい技術

以下、第 1 位 〜 第 3 位まで発表します。

第 1 位: Core ML & Create ML

流行りの「機械学習・AI」の iOS 版です。 iOS 11 の発表とともに追加され、 iOS 用の機械学習として広まりました。 Carview では、写真を撮ると自動車のナンバープレート部分が自動で隠れるカメラをリリースしています(気になる方は、「みんカラ」で検索!)。ここをCore ML & Create ML に置き換えたいと思っています。というのも、Core ML は iOS 用の機械学習として謳っているだけあり、他の機械学習では CPU や GPU を使って処理するところをニューラルエンジン2 というものを使い、より高速に処理することができます。
下記に、私が個人的に実装してみたサンプルアプリの動画を貼ります。 PC で自動車の画像を表示しそれを iPhone のカメラで撮影している最中の動画ですが、常時ナンバープレートを隠せていることがわかります。
ezgif-2-b889be61d42b-min.gif

個人的には、この検知したナンバープレートエリアをさらに Core ML にかけることで、愛車登録時にナンバープレート情報を自動入力されるようにしたいと考えています。実際に、ナンバープレートの番号だけであれば Apple が文字を判定する機能3 を提供しているのであまり苦労せず実装することができそうです。ただこの機能は日本語に対応していないため、陸運支局の部分やひらがなの部分は独自で作る必要があり、さらにこの陸運支局は時間が経つごとに増えていく部分であるので、実装難易度がかなり高いです。

第 2 位: Swift UI

1 位がなかなか華々しい技術だったので期待された方には申し訳ありませんが、2 位は目玉らしい機能が実装できるというものではありません。 Swift UI とは iOS 13 とともに発表された、新しい画面の実装方法です。 iOS は今まで画面を作るときに、

  1. 画面上に部品を追加し、
  2. その部品をコード上に紐付けて、
  3. コード上で機能を実装する

ezgif-7-3dd6a1cf4937-min.gif

という実装を行う必要があります。これが Swift UI では 1 と 2 を行う必要がなく、3 のようなコードのみで完結する書き方です。ですので、実装時間を短縮することができコードレビューもしやすい、という特徴があります( 1 で作ってる画面上のコードレビューは本当に大変。。。)。

また個人的な話ですが、将来的には Apple は Swift UI しかサポートしなくなるのでは?という不安があります4。というのも、 iOS 14 から追加されている機能である Widget ですが、これは Swift UI でしか作れません。他にも Swift UI の発表と共に Objective-C からは使えないフレームワークが発表されたり5、いつ Apple が「古い作りのアプリは承認しないよ」といってもおかしくありません(といってもかなり先の話だと思いますが)。いつそう言われてもいいように、移行準備をしっかりしておきたいところですね。

第 3 位: ARKit

最新技術か?という声が聞こえなくもないですが、 iPhone 12 Pro が発売され LiDAR6 により精度が上がった ARKit を第 3 位としました。 iPhone をお持ちの方は、 Safari で「コウテイペンギン」や「ジャイアントパンダ」と検索すると、検索結果に「 3D 表示 」というボタンが出てきていることがわかると思います。

それを押すとカメラが起動し、カメラが平面を検知すると「コウテイペンギン」や「ジャイアントパンダ」が出現するようになります。このように、現実世界をカメラで撮影しながら、その世界に何か物体を置く技術を AR7 と呼びます。これを iOS で実現するための機能が、 ARKit と呼ばれるものになります。自分が気になる車種がカメラを向けたら出現し、それと一緒に撮影ができたら感動しそうですよね!

おわりに

というわけで、個人的に考えている「 Carview に導入したい iOS の最新技術」3 選をご紹介しました。iOS や iPhone について、今後の技術の進化を予感させられたり、わくわくできるような記事になっていましたら幸いです。
明日は @nbapps_dev さんの投稿で、全自動アプリ審査提出についてだそうですね!アプリエンジニアであれば審査提出作業の煩わしさを感じる機会は多いと思うので、どんな記事なのかとても楽しみです!
以上で、 1 日目の記事は終了しようと思います。ありがとうございました!!


  1. iOS と Android のアプリを一つのコードで実装できる、クロスプラットフォームフレームワーク。 

  2. Apple が開発した、機械学習関連の処理に特化した SoC の一部。 iPhone X 以降に搭載されている。 

  3. 正確には機械学習モデルを提供しています。 MNIST というモデルを使えば可能だと考えています。 

  4. 過激派による暴論の一つだと考えてください。サポートしなくなる、という根拠はないです。 

  5. Combine という非同期処理を扱うためのフレームワークが発表されましたが、これは Objective-C からは使用できません。 

  6. Light Detection and Ranging の略。iPhone では赤外線を発し反射して返ってくる時間を計測することで、部屋の奥行きなどを高精度で計測することができる。iPhone 12 Pro と iPhone 12 Pro Max に搭載されている。 

  7. Augmented Reality(拡張現実)の略。他にも Virtual Reality(仮想現実)や Mixed Reality(複合現実)などがある。 

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

FlutterのImagePickerでapp storeにリジェクトされた件。

ストアから以下のリジェクトをくらいました。

Guideline 5.1.1 - Legal - Privacy - Data Collection and Storage

We noticed that your app requests the user’s consent to access the camera and photos, but doesn’t sufficiently explain the use of the camera and photos in the purpose string.

To help users make informed decisions about how their data is used, all permission request alerts need to specify how your app will use the requested information.

Next Steps

Please revise the relevant purpose string in your app’s Info.plist file to specify why your app needs access to the user's camera and photos. Make sure the purpose string includes an example of how the user's data will be used.

You can modify your app's Info.plist file using the property list editor in Xcode.

原因と対策

下記ページを参考にImagePickerを導入してたのですが、
https://makulogsleep.com/flutter-image-store

下の部分をそのままInfo.plistに貼り付けてたら、「簡単すぎてわからんわ!」とお叱りを受けました。
スクリーンショット 2020-11-25 13.09.04.png

タグの中身を、具体的に「画像データをどう使うのか」と書き直したらOKでした。

ちなみに、どこで使われるのか。

以下のように、「カメラへのアクセスを求めています」の下に使われます。
スクリーンショット 2020-11-25 13.14.35.png

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

公式ドキュメントのAuto Layout記事にあるStackViewの例を作成してみた!

はじめに

https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/LayoutUsingStackViews.html#//apple_ref/doc/uid/TP40010853-CH11-SW12
この公式ドキュメントの例を元に、画像のようなレイアウト作成しました。
Stack ViewContent Hugging PriorityContent Compression Resistance Priorityの使い方や機能についてまとめてみようと思います。
スクリーンショット 2020-11-24 20.47.28.png

Stack Viewの使い方

いくつかやり方があるが、公式ドキュメントに載っていたやり方が分かりやすかったのでそのやり方を説明します。
LabeltextFieldStack Viewでまとめるには、
スクリーンショット 2020-11-24 20.57.39.png
二つを選択して
スクリーンショット 2020-11-24 20.58.53.png
[Editor]>[Embed in]>[Stack View]を選択!
スクリーンショット 2020-11-24 21.01.02.png
水平方向のStack Viewができました!
垂直に並べて同じ作業すれば垂直方向のStack Viewができます。
この作業を繰り返してレイアウトを作成していきます。

完成したもの

最初にも載せたこれです。
スクリーンショット 2020-11-24 20.47.28.png

Content Hugging PriorityとContent Compression Resistance Priority

この完成したレイアウトのContent Hugging PriorityContent Compression Resistance Priorityは以下の通りです。
スクリーンショット 2020-11-24 21.05.02.png

Content Hugging Priority

Stack View内でパーツを配置して、Stack Viewの方が水平または垂直方向に対して大きく、余白が発生する場合の余白を埋めるためのものです。このHuggingの値が小さい方が空白を埋めるために広がるイメージです。
以下の画像はLabelとtextFieldの間が空いていてどちらかに余白を埋めさせるとします。
textFieldのContent Hugging PriorityをLabelより低くして、Stack Viewの左右の幅を決めると、画像のようにtextFieldが余白を埋めてくれます。
スクリーンショット 2020-11-24 21.33.36.png
LabelのHorizontalのContent Hugging Priorityの値は251。(画面一番右の下の方)
スクリーンショット 2020-11-24 21.36.55.png
textFieldのContent Hugging Priorityの値は250
スクリーンショット 2020-11-24 21.39.14.png

Content Compression Resistance Priority

こちらはパーツがStack Viewからはみ出るときに、優先して表示する方の値を大きくします。
labelのContent Compression Resistance Priorityを751、textFieldを750にすると、labelがまず優先的に表示されてからtextFieldが表示されます。
スクリーンショット 2020-11-25 5.44.30.png
逆に、labelを750、textFieldを751にするとtextFieldが優先的に表示され、今回のようにtextFieldの内容が長ければlabelを隠してしまいます。
公式ドキュメントで作成したレイアウトで、labelのResistanceが750でtextFieldが749になっている理由もこれと同じでしょう。
スクリーンショット 2020-11-25 5.41.36.png

最後に

この辺りの知識は使いこなすことができれば、レイアウトの作成がやりやすくなり思うように動かせると思うので、しっかり使いこなしたいですね。

参考サイト

https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/LayoutUsingStackViews.html#//apple_ref/doc/uid/TP40010853-CH11-SW12
https://blog.officekoma.co.jp/2019/02/stackview-content-hugging.html

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

Swift Classのまとめ

class

swiftのクラスに関してまとめたのこちらに記事にいたします。

Class 参照型のデータ構造

  • 参照型、継承できる
  • 構造体と列挙体と違うは プロパティやメソッドの文法、初期化のフローなどが違う
  • 上記は継承を考慮するため
  • CocoaフレームワークのほとんどはClassで定義されている
class SomeClass {
    let id: Int
    let name: String

    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }

    func printName() {
        print(name)
    }
}

let instane = SomeClass(id: 1, name: "name")
instane.printName()

継承

  • 型の構成要素の引き継ぎ
  • 継承とは、新たなクラスを定義する時、他のクラスのプロパティ、メソッド、イニシャライザなどの型を再利用する仕組み
  • 継承先のクラスでは、継承元のクラスと共通する動作を改めて定義する必要がなく、継承元のクラスとの差分のみを定義知れば済む
  • 継承元のクラスのサブクラス、継承元のクラスはクラスのスーパークラスといいます
  • Swiftでは複数のクラスから継承する多重継承は禁止されている
class ClassName: 継承 {
    クラスの定義
}

class User {
    let id: Int

    var massage: String {
        return "Hello"
    }

    init(id: Int) {
        self.id = id
    }

    func printProfile() {
        print("id: \(id)")
        print("massage: \(massage)")
    }
}

// Userを継承したクラス
class RegisteredUser: User {

    let name: String

    init(id: Int, name: String) {
        self.name = name
        super.init(id: id)
    }
}

let registeredUser = RegisteredUser(id: 1, name: "tanaka takuya")

let id = registeredUser.id
let massage = registeredUser.massage // Userを継承しているのでmassageが呼べる
registeredUser.printProfile()

オーバーライド

  • 型の構成要素の再定義
  • スーパークラスで定義されているプロパティやメソッドなどの定義は、サブクラスで再定義することもできる
  • オーバーライド可能なプロパティはインスタンスプロパティと後述するクラスプロパティのみで、すタティックプロパティはオーバーライドできない
  • オーバーライドを行うには、overrideキーワードを使用してスーパークラスで定義されている要素を再定義します。
class ClassMame: superClass {
    override func methodName(引数) -> 戻り値 {
    メソッド呼び出し時に実行される文
    }
}

override var プロパティ名型名 {
    get {
        return文によって値を返す処理
        superキーワードでスーパークラスの実装に利用できる
    }

    set {
        値を更新する処理
        superキーワードでスーパークラスの実装を利用できる
    }
}
  • オーバーライドしたプロパティやメソッドの中でsuperキーワードを使用することで、スーパークラスの実装を呼び出すこともできる
  • User1で定義されているmassageとprintProfileをRegisteredUser1でオーバーライドしている
  • printProfileではsuper.printProfileによって、スパークラスの実装を読んでいる
class User1 {
    let id: Int

    var massage: String {
        return "Hello"
    }

    init(id: Int) {
        self.id = id
    }

    func printProfile() {
        print("id: \(id)")
        print("massage: \(massage)")
    }
}

class RegisteredUser1: User1 {
    let name: String

    override var massage: String {
        return "Hello, my name is \(name)"
    }

    init(id: Int, name: String) {
        self.name = name
        super.init(id: id)
    }

    override func printProfile() {
        super.printProfile()
        print("name: \(name)")
    }
}

let user1 = User1(id: 1)
user1.printProfile()

print("--")

let registeredUser1 = RegisteredUser1(id: 2, name: "tanaka takuya")
registeredUser1.printProfile()

  • Userクラスのprofileではidとmessageのみを出力していましたが、ResisteredUser1ではこれに加えてnameの値も出力している

final 継承とオーバーライドの禁止

  • オーバーライド可能な要素の前にfinalをつけることによって、その要素がオーバーライドされることを禁止できる
class SuperClass {
    func overrideMethod() {}

    final func finalMethod() {}
}

class SubClass: SuperClass {
    override func overrideMethod() {}

    // コンパイルerror
    // Instance method overrides a 'final' instance method
    // override func finalMethod() {}
}

// classにfinalをつけることによって、そのクラスを定義することを禁止する
class AAA {}

class BBB: AAA {}

final class CCC {}

// コンパイルerror
// nheritance from a final class 'CCC'
// class DDD: CCC {}

クラスに紐づく要素

  • クラスのインスタンスではなく、クラス自身に紐ずく要素として、クラスプロパティとクラスメソッドがある
  • クラスプロパティとすタティックプロパティと、クラスメソッドはスタティックメソッドと、それぞれ似た性質を持っている
両者の違い
  • スタティックプロパティとスタティックメソッドはオーバーライドできない
  • クラスプロパティとクラスメソッドはオーバーライドできる

クラスプロパティ

  • クラス自身に紐ずくプロパティ
  • クラスプロパティはクラスのインスタンスではなく、クラス自身に紐ずくプロパティで、インスタンスに依存しない値を扱う場合に利用する
  • クラスプロパティを定義するには、プロパティ宣言の先頭にclassをつける
  • アクセス方法は型名に . とクラスプロパティ名をつけて、型名.クラスプロパティのように書く
class A {
    class var className: String {
        return "A"
    }
}

class B: A {
    override class var className: String {
        return "B"
    }
}

A.className // "A"
B.className // "B"

クラスメソッド

  • クラスに紐ずくメソッド
  • クラスメソッドはクラスのインスタンスではなく、クラス自身に紐ずくメソッドで、インスタンスに依存しない処理を実装する際に利用します
  • 使い方は上と同じ
class A1 {
    class func inheritanceHierarchy() -> String {
        return "A"
    }
}

class B1: A1 {
    override class func inheritanceHierarchy() -> String {
        return super.inheritanceHierarchy() + "<-B"
    }
}

class C1: B1 {
    override class func inheritanceHierarchy() -> String {
        return super.inheritanceHierarchy() + "<-C"
    }
}

A1.inheritanceHierarchy() // A
B1.inheritanceHierarchy() // A<-B
C1.inheritanceHierarchy() // A<-B<-C

イニシャライザの種類と初期化のプロセス

  • イニシャライザの役割は型のインスタンス化の完了までに全てのプロパティを初期化し、型の生合成を保つことである
  • クラスには継承関係があるため、様々な階層で定義されたプロパティが初期化されることを保証する必要がある
  • 上記を保証するためにクラスには二段階初期化の仕組みが導入されている
  • 二段階初期化を実現するためにクラスのイニシャライザは指定イニシャライザとコンビニエンスイニシャライザの二種類がある

指定イニシャライザ 主となるイニシャライザ

  • クラスの主となるイニシャライザ
  • このイニシャライザの中で全てのストアドプロパティが初期化される必要がある
class Mail {
    let form: String
    let to: String
    let title: String

    // 指定イニシャライザ
    init(form: String, to: String, title: String) {
        self.form = form
        self.to = to
        self.title = title
    }
}

コンビニエンスイニシャライザ

  • 指定イニシャライザをラップするイニシャライザ
  • 指定イニシャライザを中継するイニシャライザで、内部で引数を組み立てて指定イニシャライザを呼び出す必要がある
class Mail1 {
    let from: String
    let to: String
    let title: String

    init(from: String, to: String, title: String) {
        self.from = from
        self.to = to
        self.title = title
    }

    convenience init(from: String, to: String) {
        self.init(from: from, to: to, title: "Hello, \(from)")
    }
}

let mail = Mail(form: "aaa", to: "bbb", title: "ccc")
let mail1 = Mail1(from: "aaa", to: "bbb")
print(mail.form, mail.to, mail.title)
print(mail1.from, mail1.to, mail1.title)

二段階初期化

  • 型の生合成を保った初期化を実現するため、クラスのイニシャライザには3つのルールがある
  • 指定イニシャライザは、スーパークラスの指定イニシャライザを呼ぶ
  • コンビニエンスイニシャライザは同一クラスのイニシャライザを呼ぶ
  • コンビニエンスイニシャライザは、最終的に指定イニシャライザを呼ぶ
上記のルールを満たしている場合
  • 継承関係にある全てのクラスの指定イニシャライザがかなさず実行され、各クラスで定義されたプロパティが全て初期化されることが保証される
  • 一つでも満たせないルールがある場合、型の生合成を保てない可能性があるのでコンパイルエラーになる
  • またスーパークラスとサブクラスのプロパティの初期化順序を守ため、指定イニシャライザによるクラスの初期化は二段回に分けて行われる
  • クラス内で新たに定義された全てのストアドプロパティを初期化し、スーパークラスの指定イニシャライザを実行する
  • スーパークラスでも同様の初期化を行い、大元のクラスまで遡る
  • ストアドプロパティ以外の初期化を行う
class User2 {
    let id: Int

    init(id: Int) {
        self.id = id
    }

    func printProfile() {
        print("id: \(id)")
    }
}

class RegisteredUser2: User2 {
    let name: String

    init(id: Int, name: String) {
        // 第一段階
        self.name = name
        super.init(id: id)
        // 第二段階
        super.printProfile()
    }
}

デフォルトイニシャライザ

  • プロパティの初期化が不要な場合に定義されるイニシャライザ
  • プロパティが存在しない場合や、全てのプロパティが初期値を持っている場合、指定イニシャライザ内で初期化する必要があるプロパティはない
  • 暗黙的にデフォルトの指定イニシャライザが定義される
class User3 {
    let id = 0
    let name = "kenta"

    // 以下と同等のイニシャライザが自動的に定義されている
    // init() {}
}

let user3 = User3()

// 一つでも指定イニシャライザ内で初期化が必要なプロパティが存在する場合、デフォルトイニシャライザがなくなり、指定イニシャライザを定義する必要があるます!
class User4 {
    let id: Int
    let name: String
    // これがないとコンパイルエラー
    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
}

let user4 = User4(id: 0, name: "Taro")

デイニシャライザ インスタンスの終了処理

  • ARCによってインスタンスが破棄されるタイムングでは、クラスのデイニシャライザが実行されます、デイ二シャライザの逆で、クリーンアップなどの終了処理を行うもの - デイニシャライザは、deinitキーワードを使用して、クラス内に次のように定義する
class ClassName {
//    deinit {
//        クリーンアップなどの処理
//    }
//}
// デイニシャライザは継承関係の下位クラスから自動的に実行されるため、
// スーパークラスのデイ二シャライザを呼び出す必要はありません
// 値の比較と参照の比較
// 参照型の比較は、参照先の値同士の比較と、参照先の比較の2つに分けられる
// 参照先の値同士の比較はこれまでに登場してきたものと同様に==演算子で行い、参照自体の比較は===演算子で行う
class SomeClassA: Equatable {
    static func ==(lhs: SomeClassA, rhs: SomeClassA) -> Bool {
        return true
    }
}

let a = SomeClassA()
let b = SomeClassA()

let c = a

// 同じ
a == b // true
// 参照は異なる
a === b // false
// 参照先は同じ
a === c // true

まとめ

個人的にまとめたので、記事にしてみました。
完全に個人メモをそのまま書いたので、見辛いかも知れませんがご了承ください。

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

swift 構造体まとめ

構造体

Swiftの構造体に関してまとめたので、記事として残しておきます。

構造体とは?

  • 値型
  • ストアドプロパティ の組み合わせによって、一つの値を表す
  • 標準ライブラリで提供されている多くは構造体である
    • Bool String Int Array Dictionary
    • オプショナルは列挙型 タプルは型の組み合わせ Anyはプロトコル

定義

struct 構造体名 {
  構造体の定義
}


struct Article {
    let id: Int
    let title: String
    let body: String

    init(id: Int, title: String, body: String) {
        self.id = id
        self.title = title
        self.body = body
    }

    func printBody() {
        print(body)
    }
}

let article = Article(id: 1, title: "title", body: "body")
article.printBody() // body

  • ストアドプロパティ の変更による値の変更
  • 構造体はストアドプロパティの組み合わせで一つの値を表す値型
  • 構造体のストアドプロパティを変更することは、構造体を別の値に変更することであり、構造体が入っている変数や、定数への再代入を必要とします
  • 値型の変更に関する仕様は構造体のストアドプロパティ の変更にも適用される

  • 定数のストアドプロパティは変更できない

  • 構造体のストアドプロパティの変更は再代入を必要とするため、定数に代入された構造体のストアドプロパティは変更できない

struct SomeStruct {
    var id: Int

    init(id: Int) {
        self.id = id
    }
}

var variable = SomeStruct(id: 1)
variable.id = 2
let constant = SomeStruct(id: 1)
constant.id = 2 // error
  • メソッド内のストアドプロパティの変更にはmutatingが必要
  • 構造体のストアドプロパティの変更は再代入が必要なので、ストアドプロパティを含むメソッドにはmutatingが必要
struct SomeStruct2 {
    var id: Int

    init(id: Int) {
        self.id = id
    }
//  mutating をつけないとerror
// Cannot assign to property: 'self' is immutable
// 自動でつけてくれるようになる
    mutating func someMethod() {
        id = 4
    }
}

var some2 = SomeStruct2(id: 1)
some2.someMethod()
some2.id // 4 変更されている
  • メンバーワイズイニシャライザ デフォルトで用意されているイニシャライザ
  • 型のインスタンスは初期化後に全てのプロパティ が初期化される必要がある
  • 独自にイニシャライザを定義して初期化の処理を行うこともできます、
  • 構造体では自動的に定義される メンバーワイズイニシャライザというイニシャライザがある
  • 型が持っている各ストアドプロパティと同名の引数をとるイニシャライザである
struct Article2 {
    var id: Int
    var title: String
    var boby: String

// 以下と同等のイニシャライザが自動的に定義される
//    init(id: Int, title: String, body: String) {
//        self.id = id
//        self.title = title
//        self.boby = body
//    }
}

let article2 = Article2(id: 1, title: "Hello", boby: "this is body")
article2.id      // 1
article2.title   // Hello
article2.boby    // this is body
  • メンバーワイズイニシャライザのデフォルト引数
  • ストアドプロパティ が初期化式とともに定義されている場合
  • そのプロパティに対するメンバーワイズイニシャライザの引数はデフォで引数を持ち、呼び出し時の引数の指定の省略ができる
struct Mail {
    var subject: String = "(No Subject)"
    var body: String

// 以下と同等のイニシャライザが自動的に定義される
//    init(subject: String = "(No Subject)", body: String ) {
//        self.subject = subject
//        self.body = body
//    }
}

let noSubject = Mail(body: "Hello")
noSubject.subject // "(No Subject)"
noSubject.body    // Hello
let greeting = Mail(subject: "Greeting", body: "Hello!")
greeting.subject // Greeting
greeting.body    // Hello!

まとめ

簡単にまとめたのでこちらに記載しました。
何かあればご指摘ください

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

Swift 構造体まとめ

構造体

Swiftの構造体に関してまとめたので、記事として残しておきます。

構造体とは?

  • 値型
  • ストアドプロパティ の組み合わせによって、一つの値を表す
  • 標準ライブラリで提供されている多くは構造体である
    • Bool String Int Array Dictionary
    • オプショナルは列挙型 タプルは型の組み合わせ Anyはプロトコル

定義

struct 構造体名 {
  構造体の定義
}


struct Article {
    let id: Int
    let title: String
    let body: String

    init(id: Int, title: String, body: String) {
        self.id = id
        self.title = title
        self.body = body
    }

    func printBody() {
        print(body)
    }
}

let article = Article(id: 1, title: "title", body: "body")
article.printBody() // body

ストアドプロパティ の変更による値の変更

  • 構造体はストアドプロパティの組み合わせで一つの値を表す値型
  • 構造体のストアドプロパティを変更することは、構造体を別の値に変更することであり、構造体が入っている変数や、定数への再代入を必要とします
  • 値型の変更に関する仕様は構造体のストアドプロパティ の変更にも適用される

  • 定数のストアドプロパティは変更できない

  • 構造体のストアドプロパティの変更は再代入を必要とするため、定数に代入された構造体のストアドプロパティは変更できない

struct SomeStruct {
    var id: Int

    init(id: Int) {
        self.id = id
    }
}

var variable = SomeStruct(id: 1)
variable.id = 2
let constant = SomeStruct(id: 1)
constant.id = 2 // error
  • メソッド内のストアドプロパティの変更にはmutatingが必要
  • 構造体のストアドプロパティの変更は再代入が必要なので、ストアドプロパティを含むメソッドにはmutatingが必要
struct SomeStruct2 {
    var id: Int

    init(id: Int) {
        self.id = id
    }
//  mutating をつけないとerror
// Cannot assign to property: 'self' is immutable
// 自動でつけてくれるようになる
    mutating func someMethod() {
        id = 4
    }
}

var some2 = SomeStruct2(id: 1)
some2.someMethod()
some2.id // 4 変更されている

メンバーワイズイニシャライザ

  • デフォルトで用意されているイニシャライザ
  • 型のインスタンスは初期化後に全てのプロパティ が初期化される必要がある
  • 独自にイニシャライザを定義して初期化の処理を行うこともできます、
  • 構造体では自動的に定義される メンバーワイズイニシャライザというイニシャライザがある
  • 型が持っている各ストアドプロパティと同名の引数をとるイニシャライザである
struct Article2 {
    var id: Int
    var title: String
    var boby: String

// 以下と同等のイニシャライザが自動的に定義される
//    init(id: Int, title: String, body: String) {
//        self.id = id
//        self.title = title
//        self.boby = body
//    }
}

let article2 = Article2(id: 1, title: "Hello", boby: "this is body")
article2.id      // 1
article2.title   // Hello
article2.boby    // this is body
  • メンバーワイズイニシャライザのデフォルト引数
  • ストアドプロパティ が初期化式とともに定義されている場合
  • そのプロパティに対するメンバーワイズイニシャライザの引数はデフォで引数を持ち、呼び出し時の引数の指定の省略ができる
struct Mail {
    var subject: String = "(No Subject)"
    var body: String

// 以下と同等のイニシャライザが自動的に定義される
//    init(subject: String = "(No Subject)", body: String ) {
//        self.subject = subject
//        self.body = body
//    }
}

let noSubject = Mail(body: "Hello")
noSubject.subject // "(No Subject)"
noSubject.body    // Hello
let greeting = Mail(subject: "Greeting", body: "Hello!")
greeting.subject // Greeting
greeting.body    // Hello!

まとめ

簡単にまとめたのでこちらに記載しました。
何かあればご指摘ください

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

【iOS14】CollectionViewのscrollToItemが機能しない場合の対処法

iOS14において、CollectionViewのscrollToItemが機能しないバグが報告されています(執筆時、2020年11月25日現在)。AppleのDeveloper Forumsでも話題になっており、iOS14.2でも修正されていないようです。私もOS依存のバグとは気づかず、仕事で10時間くらい時間を溶かしました(ずっとiOS14のシミュレータでなんでスクロールできないんだろうと四苦八苦してた)

https://developer.apple.com/forums/thread/663156

scrollToItemの説明(読み飛ばし可)

scrollToItemは指定したindexPathまでcellをスクロールさせることができるものです。アプリ初回起動時のウォークスルー、チュートリアル画面などで、スワイプではなくボタンタップでcellをスクロールさせるような場合に使ったりすると思います。

例えば上記のような画面で「次へ」を押した時にページをスクロールするような場合は、以下のように書けます。

collectionView.scrollToItem(at: IndexPath(item: newPage, section: 0), at: .init(), animated: true)

しかしiOS14では、「次へ」を押してもなぜかスクロールされない状態となっています。

解決方法

フォーラムでは下記のようにscrollToItem前後でisPagingEnableのBool値を切り替えるような手法が提案されています。

self.collectionView.isPagingEnabled = false
self.collectionView.scrollToItem(at: indexPath, at: .left, animated: false)
self.collectionView.isPagingEnabled = true

実際試すとこれでもうまくいくのですがこれよりも、冒頭のようなチュートリアル画面を作りたい場合は、同じくcollectionViewのsetContentOffsetで代用する方が良いのではないかなと思いました。

let cellWidth = collectionView.frame.width * CGFloat(newPage)
let cellHeight = CollectionView.frame.minY
collectionView.setContentOffset(CGPoint(x: cellWidth, y: cellHeight), animated: true)

チュートリアル画面ではなくとも今までscrollToItemを使っていた実装は、同じように座標を取得する方法が有効かと思います。

おそらくすぐにAppleが対応してくれる問題だとは思うのですが、もしお困りの方がいらっしゃったら試してみてください〜

参考

UICollectionViewでチュートリアル画面を作る

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

【SwiftUI】Likeボタンタップ時のLike数増減とFirebaseの更新機能の実装

はじめに

SwiftUIでカード内のLikeボタンを押した場合にLike数を+1し、再度押した場合に-1する機能を実装することと、その場合にFirebaseのデータベースを更新できるようにすることを目的とする。

今回はカード内のLikeボタンの実装を行います。

前回までの記事は以下を参考ください。

参考記事
【SwiftUI】Likeボタンとリスト内セルにボタンを実装する場合の注意点

開発環境

OSX 10.15.7 (Catalina)
Xcode 12.2.0
CocoaPods 1.10.0

実装したコード

ContentRowView_swift.png

主な変更点
・Likeボタン内にif文を設定
→ボタンを押した場合はLike数を+1、再度押した場合は-1になるようにしました。また、押した場合はFirebaseのデータベースが更新されるようにしています。

struct ContentRowView: View {
    var id  = ""
    var user = ""
......
    static let formatter = RelativeDateTimeFormatter()

    // 以下を追記
    @State var isLiked = false

    var body: some View {

......

            // Likes Number
            HStack {
                Image(systemName: isLiked ? "heart.fill": "heart").font(.headline).foregroundColor(Color("pinkColor"))
                Text(likes).font(.headline).foregroundColor(Color("pinkColor"))
                Spacer()
            }.padding(.top, 5)

            // Info
            HStack {
......

                // Like Button push Image to add like's Number.
                Image(systemName: isLiked ? "heart.fill": "heart")
                    .font(.title)
                    .foregroundColor(Color("pinkColor"))
                    .onTapGesture {
                        self.isLiked.toggle()
                        if isLiked == true {
                            // push button
                            print("true. likes+1")
                            // update likes...

                            let db = Firestore.firestore()

                            let like = Int.init(self.likes)!
                            db.collection("ikku").document(id).updateData(["likes": "\(like + 1)"]) { (err) in

                                if err != nil{

                                    print((err) as Any)
                                    return
                                }
                                print("updated....")
                            }

                        } else {
                            // And push Button
                            print("false. likes-1")
                            // update likes...

                            let db = Firestore.firestore()

                            let like = Int.init(self.likes)!
                            db.collection("ikku").document(id).updateData(["likes": "\(like - 1)"]) { (err) in

                                if err != nil{

                                    print((err) as Any)
                                    return
                                }

                                print("updated....")
                            }
                        }
                    }
                    .frame(width: 30, height: 30)
            }.padding(5)
        }.padding(.top, 8).frame(height: 391)
    }
}

・getDataクラス内に.modifiedを実装
→Likeボタンを押すと、Like数も変更するようにしました。実装にあたって、ボタンを押すと、Like数左にあるハートマークも変更できるようにしています。

class getData: ObservableObject {

    @Published var datas = [dataType]()

    init() {

        let db = Firestore.firestore()

        db.collection("ikku").order(by: "now", descending: true).addSnapshotListener{ (snap, err) in

            if err != nil{

                print((err?.localizedDescription)!)
                return
            }

            for i in snap!.documentChanges{

                if i.type == .added{

                    let id = i.document.documentID
                    let user = i.document.get("userName") as! String
                    let haiku = i.document.get("haiku") as! String
                    let place = i.document.get("place") as! String
                    let likes = i.document.get("likes") as! String
                    let mapImage = i.document.get("mapImage") as! String
                    let userImage = i.document.get("userImage") as! String

                    let now = i.document.get("now") as! Timestamp
                    let stamp = i.document.get("createdDate") as! Timestamp

                    let formatterDate = DateFormatter()
                    formatterDate.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
                    let createdDate = formatterDate.string(from: stamp.dateValue())

                    let isLiked = false

                    DispatchQueue.main.async {
                        self.datas.append(dataType(id: id, user: user, haiku: haiku, place: place, likes: likes, mapImage: mapImage, userImage: userImage, stamp: stamp.dateValue(), createdDate: createdDate, now: now.dateValue(), isLiked: isLiked ))

                    }
                }

                // 以下を追加
                if i.type == .removed{

                    let id = i.document.documentID

                    for j in 0 ..< self.datas.count {

                        if self.datas[j].id == id{

                            self.datas.remove(at: j)
                            return
                        }
                    }
                }

                if i.type == .modified{

                    let id = i.document.documentID
                    let likes = i.document.get("likes") as! String

                    for j in 0 ..< self.datas.count {

                        if self.datas[j].id == id{

                            self.datas[j].likes = likes
                            return
                        }
                    }
                }
            }
        }
    }
}

実装後のイメージ

タップ前
iPhone_11_–_14_2.png

Haiku_-_Cloud_Firestore_-_Firebase_コンソール.png

タップ後
iPhone_11_–_14_2.png

Haiku_-_Cloud_Firestore_-_Firebase_コンソール.png

再度、タップするとFirebaseのLike数は-1になり、Likeの見た目が元に戻ります。

ハートをアニメーションで動かす

Likeボタンをタップした場合に、ハートにアニメーションがかかるようにしました。

実装方法

ezgif-7-f0350d0318d1.gif

import SwiftUI

struct ContentView: View {

    @State private var isLiked = false

    var body: some View {
        VStack {
            Text(isLiked ? "Liked!" : "unLiked!")
            HeartButton(isLiked: $isLiked)
        }
    }
}

struct HeartButton: View {

    @Binding var isLiked : Bool

    private let animationDuration : Double = 0.1
    private var animationScale : CGFloat {
        isLiked ? 0.7 : 1.3
    }

    @State private var animate = false

    var body: some View {

        Button(action: {
            self.animate = true

            DispatchQueue.main.asyncAfter(deadline: .now() + self.animationDuration, execute: {
                self.animate = false
                self.isLiked.toggle()
            })


            if isLiked == true {
                // push button
                print("true")
            } else {
                // And push Button
                print("false")
            }
        }, label: {
            Image(systemName: isLiked ? "heart.fill": "heart")
                .font(.largeTitle)
                .foregroundColor(Color.red)
                .frame(width: 100, height: 100)
        })
        .scaleEffect(animate ? animationScale : 1)
        .animation(.easeIn(duration: animationDuration))
    }
}

実際に実装した後のイメージ

かわいい感じになりました。
ezgif-7-eca8634e67ec.gif

以上です。

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