20200225のiOSに関する記事は10件です。

RealmのschemaVersionの管理方法について考える

RealmのschemaVersion

Realmのドキュメントのマイグレーション項目で言及されている通り、Realmを扱う際にはRealm.Configurationの引数であるschemaVersionにモデルにアップデートがあった際に以前のバージョンよりも大きい値を設定する必要があります。なお、この値はマイグレーションなどの処理を行う際に必要となり、Realm内部でoldSchemaVersion(初期値0)よりも最新のschemaVersionが大きいか否かで自動的にマイグレーションされます。(データ移行の際はmigrationBlockで移行処理を記述する)

schemaVersionをどのように管理すべきか

方法1. モデルのアップデートを行う際に手元でschemaVersionをインクリメントする

個人的にオススメしません。複数人で開発していた際に同時にアップデートすることもあるかと思います。その際にコンフリクトしたり、配信されているとschemaVersionが正しくインクリメントされておらずクラッシュも起きうると考えられます。

方法2. ビルド番号を直接入れる

アプリバージョンに関わらず、ビルド番号をGitのコミット数などでインクリメントしている場合、ビルド番号をschemaVersionに入れることも方法の1つかと思います。この場合だと実装者に関わらず常にインクリメントされていくのでschemaVersionも自動で上がってくれます。この方法がお手軽ですが、モデルにアップデートがないのにも関わらずインクリメントされるのは違和感に感じます。また、ビルド番号をどのように管理しているかにも依存するので開発方針に左右されます。

以上の方法を踏まえて実現したい理想

  1. schemaVersionはモデルのアップデートが行われた際のみインクリメントしたい。
  2. 複数人開発においても正しく管理されるようにGitに依存した値を注入したい

モデルのディレクトリ以下のgit logからschemaVersionを設定する

最終的にfastlaneのlaneとして定義できるようにします。

Step1. 特定のディレクトリ以下のマージコミットを取得する

git log --oneline --merges --first-parent master -- DIR_PATH | wc -l

上記のコマンドでDIR_PATH上の変更に対するマージコミットのカウントが取れます。
DIR_PATHはモデルが存在するディレクトリです。最終的にはこの数を最新のschemaVersionとして設定します。

Step2. 現在のブランチの最新マージコミット(コミットハッシュ)を取得する

git log --oneline --merges --pretty=format:"%h" -1

取得したコミットハッシュをHASH_STEP2とします。

Step3. 現在のブランチの最新コミット(コミットハッシュ)を取得する

git log --oneline --pretty=format:"%h" -1

取得したコミットハッシュをHASH_STEP3とします。

Step4. 最新のマージコミットと現在のコミット間で特定ディレクトリ上の変更のコミット数を取得する

git log --oneline HASH_STEP2..HASH_STEP3 -- DIR_PATH | wc -l

これによってDIR_PATHにおいて現在、変更を行ったかが0かどうかで判断することができます。

Step5. Step1~4を用いてlaneを組む

  lane :bump_schema_version do
    latest_merges_commit_hash = sh(%Q[git log --oneline --merges --pretty=format:"%h" -1])
    latest_commit_hash = sh(%Q[git log --oneline --pretty=format:"%h" -1])
    latest_model_commit_count = sh(%Q[git log --oneline #{latest_merges_commit_hash}..#{latest_commit_hash} -- DIR_PATH | wc -l]).strip!.to_i
    latest_schema_version = sh("git log --oneline --merges --first-parent master -- DIR_PATH | wc -l").strip!.to_i

    if latest_model_commit_count > 0 
      latest_schema_version += 1
    end

    set_info_plist_value(path: PLIST_PATH, key: "schemaVersion", value: latest_schema_version)
  end

set_info_plist_valueでInfo.plist内のschemaVersionに書き込み、コードから取得することでRealm.Configurationに渡すことができます。

このlaneを実行することで現在のブランチで変更があるかを取得することができます。

latest_schema_versionをmasterへのマージコミット数としているのはCI上で実行したときにはモデルのディレクトリにおける変更のmasterマージコミット数schemaVersionとするためです。
まだマージしていないブランチ上で実行したときにはStep4で行っている現在変更しているかを加味してlatest_schema_versionがインクリメントされます。

前者と後者の値は一致するので「手元で実行してPR作成によってインクリメントする」or「CI上でフックして実行、PR作成する」などインクリメントする方法を選択することができます。

最後に

schemaVersionの管理方法をどのようにするかはプロジェクトによっても変わりますし、悩ましい部分だと思います。
他の方法もあればぜひ教えてください!

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

4LIFE Innovations is bracing up to acquire iOS 13 for iPhone App Development

4Life Innovations is one of the leading iPhone app development companies in India, has a team of techno-savvy enthusiasts, who are always keen to adopt the latest technologies and trends.

We always love to stay updated with the latest tech events and upcoming technologies released by Apple. This time, iOS developers at 4Life Innovations are ready to grab information about the latest release, iOS 13. We’re updating more and more about all the latest iOS 13 versions and learn how to implement these anticipated features for iPhone app development.

New features you need to know about iOS 13:

Apple introduced a lot of improvements on system and apps, such as advanced Siri, Apple Health, Memoji stickers, and a new Map experiences, but the most anticipated feature is “Dark mode” as iOS 13 itself has the tagline - “iOS now lives in the dark”

Let’s explore more -

System Features:

Dark Mode - This option gives iOS and apps a beautiful dark color scheme - manually and schedulable, customized wallpaper for dark mode
Camera Improvements - Adjustment on Portrait Lighting intensity and new high-key mono portrait lighting effect
Keyboard - The QuickType virtual keyboard includes QuickPath which allows the user to swipe their finger across the virtual keyboard to complete words and phrases
New Privacy control - Improvements on giving access to the current location, When an app requires access to location, the user can choose whether to grant access whenever they are using the app or only one time.
Accessibility - Rich text editing, Word and emoji suggestions, Add custom words, voice control
Sign in with Apple - It allows users to easily sign up accounts with 3rd-party services with a minimal amount of information i.e Google and Facebook.
Improved Siri - New voice system called “Neural Text to Speech, more functional & better sound control
Performance - 2X faster app launch, Face ID unlocks 30% faster, app downloads 50% smaller and updates 60% smaller
Battery improvements - OS 13 has a feature to limit the battery charging percentage to 80%, so this will reduce the battery aging and extend its lifespan

App Features:

Messages & Memoji - User profiles can be created and Memoji (new hairstyles, makeup, accessories, customizations) can be used as an iMessage profile picture
Maps - redesigned maps UI, with a detailed map and Look Around (similar to Google street view)
Reminders - revamped design, quick toolbar, Siri suggested reminders, smart list based on the reminder list
Photo - new redesigned UI, use of machine learning to auto-hide the clutter images such as screenshots as well as documents, All new photo tab (interface with users photos they took in the past year, month, week & day)

Wrapping up:

The iOS 13 is released by Apple on 19 September 2019. And this iOS 13 update is compatible with all the latest Apple devices including iPhone SE to iPhone 11 as well as the iPod touch 7th generation. For iPad devices, there is a separate iPodOS, which has the same features as iOS 13 OS with some more home screen and multitasking improvements.

With 5+ years of experience in iOS app development services, 4Life Innovations knows what it takes to deliver a successful iOS app that gets a good boost and good numbers of downloads. Let’s build for iOS 13!
https://www.4lifeinnovations.com/hire-iphone-app-developer/

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

iOSプロジェクト新規作成時によくやること一覧

自身のプロジェクト生成時によくやることを記載した備忘録です。
毎回お世話になるサイトをまとめました。

CocoaPodsの導入

iOSのライブラリ管理ツールです。高確率で使用することが多いです。
類似でCarthageというSwift製のライブラリ管理ツールもありますが、
今回はpodsについてのみです。
https://qiita.com/ShinokiRyosei/items/3090290cb72434852460

CocoaPodsのバージョン違いで予期せぬエラーが発生することも過去にあったため、
バージョンは開発チームで揃えておいた方が安心です。
任意のpodsバージョンのインストール/アンインストールは以下を参考にしています。
https://mushikago.com/i/?p=7298

CocoaPodsのファイルを編集する際にviコマンドも調べることが多いので、
以下のリンクにviコマンドについて記載されています。
https://qiita.com/pe-ta/items/0510bee10bcfd88afeee

Podsを用いてRealmSwiftの導入

RealmSwiftをPodsで入れる方法は以下です。
https://dev.classmethod.jp/smartphone/realmswift-introduction/

シミュレータでRealmの内容を確認したいときは、
こちらのサイトを参考にすると便利です!
https://qiita.com/i_nak/items/5d6062333b205275b85b

SwiftLintの導入

SwiftLintとは、Realmが開発しているSwiftのLintツールです。
Lintエラーの範囲は同プロジェクト内に配置する.swiftlint.ymlファイル内でカスタマイズできます。
導入方法は以下のサイト参照
https://qiita.com/OSR108/items/4b23b13bd23feada1921
エラーや警告が出た際は全ルールのまとめを書いてくれているサイトがあるので、
サイト内から原因を探して対処します。
https://qiita.com/uhooi/items/7f5d6cf2b240f60ba1ed

AlamofireとSwiftyJSONの導入

APIの呼び出し処理を行う時によく使うのがAlamofireです。
podで追加し、簡単な構文で呼び出すことができるので便利です。
レスポンスで受け取ったJSONで何か処理を行いたい場合はSwiftyJSONも便利です。
ただ構造体への洗い替え等であればSwiftyJSONでごにょごにょやらなくても、
Codableで容易に実装が可能です。
http://kayakuguri.github.io/blog/2016/05/26/alamofire-swiftyjson/

Generambaの導入

これは私も自力で導入したことは1度しかありません。
ただ使いこなすことができれば非常に便利なツールです。
テンプレートを用意することで、テンプレート通りのファイルを一気に生成することができます。
VIPERアーキテクチャのようにファイル生成数が必然的に
多くなってしまうようなものには特におすすめです。
https://qiita.com/negi0205/items/6853d43a91b58845b4eb

テンプレートファイルの編集方法は以下です。
https://qiita.com/tmyk110/items/de8f2aee315b47040c5e

最初にGeneramba導入する時に質問が全て英語でくるため、
最初に何をしているのかは少しわかりません。

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

PusherのChatKitを導入する

はじめに

チャットアプリを作ろうとすると、
Firebase Realtime Databaseというサービスを使うと素早く実装できるという記事がたくさんあるんで、
僕はPusherのChatKitというサービスを使ってチャットアプリを作ってみようかなと思います。
いつも通り元のDocはこちら

なぜPusher?

・ある程度なら無料で運用出来る
・有名どころが使用している
・自社サーバーとの連携が可能
主に上記の理由からPusherを選びました。

SDKのインストール

cocoapod

gem install cocoapods
target '<Your Target Name>' do
  pod 'PusherChatkit'
end
pod install

Carthage

$ brew update
$ brew install carthage
github "pusher/chatkit-swift"

公式はこちら

アカウント作成

こちらでアカウント作成
https://dashboard.pusher.com/accounts/sign_up

インスタンスを作成

スクリーンショット 2020-02-25 14.28.36.png

CredentialsでInstace Locatorを取得
スクリーンショット 2020-02-25 14.31.47.png

test用エンドポイントを取得
スクリーンショット 2020-02-25 14.31.56.png

認証用の設定

let provider = PCTokenProvider(url: "test用エンドポイント")
// ChatManagerの生成
let manager = ChatManager(instanceLocator: ChatKitInfo.instaceLocation,
            tokenProvider: provider,
            userID: )

Pusherとのコネクションを作る

self.manager.connect(delegate: ChatManagerDelegateImpl(delegate: self)) { (currentUser, error) in
            if let error = error {
                print("Error sending message: \(error.localizedDescription)")
                onError(error)
                return
            }

            self.currentUser = currentUser
            onSuccess()
        }

チャット部屋の購読

guard let currentUser = self.currentUser else {
    return
}

currentUser.subscribeToRoomMultipart(id: id, roomDelegate: self, messageLimit: 0) { error in
    if let error = error {
        print("Error subscribing to room: \(error.localizedDescription)")
        return
     }
    print("Successfully subscribed to the room! ?")
}

メッセージの送信

currentUser.sendSimpleMessage(roomID: room.id, text: message) { (messageId, error) in
            if let error = error {
                self.delegate?.onError(error: error)
                return
            }

            self.delegate?.onSuccessSendMessage(id: messageId)
        }

メッセージの受信

extension ChatKitManager: PCRoomDelegate {
    func onMultipartMessage(_ message: PCMultipartMessage) {}
}

これで基本的なチャットの機能が使えるようになります。

PCTokenProviderの生成

最後に認証用のプロバイダーですが、この記事ではテスト用のエンドポイントを使用しています。
しかし、実際に使用するときは自分でサーバーを構築して認証用のエンドポイントを作成する必要があります。
その場合の、PCTokenProviderの生成方法以下になります。

PCTokenProvider(url: endPoit,
                requestInjector: { request -> PCTokenProviderRequest in
                            // ヘッダー情報などを設定
                            request.addHeaders(headers)
                            return request
                }, retryStrategy: PCDefaultRetryStrategy(maxNumberOfAttempts: 6, maxTimeIntervalBetweenAttempts: 10))

Pusherではこれ以外にも既読を点けたり、過去のメッセージを取得したりも出来るのでチャットアプリを作ってみたい方は、Pusherというサービスもあるよとだけ覚えておいていただければと思います。

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

自分用メモ flutterのPageViewをほかのイベントからページ切り替えする方法

PageViewをプログラム側からページ切り替えしようと思ったら詰まった

やりたかったこと

ボタンとドロップダウンリストを使ってページ切り替え

説明に「PageControllerのinitialPage:で最初に表示するページを設定する」みたいなことが書いてあったので、なるほどPageControllerを再作成して切り替えたいページを指定するんだなと思って↓を書いた

//MyPageWidgetは省略

class MyPageState extends State<MyPageWidget>{
   var _currentPage = 0;
   @Override 
   Widget build(BuildContext context) {
      final controller = PageController(initialPage: _currentPage);
      return Column(
      children: <Widget>[
        Row( //ボタン用の行widget
          children: <Widget>[
            IconButton( //前ページボタン
              icon: Icon(Icons.navigate_before),
              onPressed: _currentPage > 0 ? () { //クリックイベント処理
                setState(){ _currentPage--;};
              } : null,
            ),
            Expanded( //ドロップダウンリストを余白いっぱいに引き伸ばす
              child: DropdownButton( //ドロップダウンリスト
                isExpanded: true,
                value: _current,
                items: List.generate(
                    12,
                    (index) => DropdownMenuItem(
                          value: index,
                          child: Text(
                            index.toString(),
                            textAlign: TextAlign.center,
                            textScaleFactor: 1.5,
                          ),
                        )),
                onChanged: (value) { //アイテム選択イベント処理
                  if (value is int) setState(){ _currentPage = value; };
                },
              ),
            ),
            IconButton( //次ページボタン
              icon: Icon(Icons.navigate_next),
              onPressed: _current < 11 ? (){ setState(){ _currentPage++ ;} : null,
            )
          ],
        ),
        Expanded( //PageViewはサイズが可変なので、Column Widgetに入れるときはこれに入れる
          child: PageView.builder(
              itemCount: 12,
              controller: _mController,
              onPageChanged: (index){ //ページ変更イベント処理
                setState(() {
                  _current = index;
                });
              },
              itemBuilder: (context, index){
                 return Center( child: Text((index + 1).toString() + "ページ",textScaleFactor: 3;
              }),
        )
      ],
    )

※動きません

flutterのPageViewリファレンスページをよく見ると

The PageController can also be used to control the PageController.initialPage, which determines which page is shown when the PageView is first constructed,

ページビューが最初に作成された時に表示されるページを決定する(雑訳)

つまりこれ、最初の一回だけページを指定するのであって、ページを移動するためには使えない、ということか!?

PageControllerのリファレンスを見ると……

METHODS

animateToPage
attach
createScrollPosition
jumpToPage
nextPage
previousPage
addListener

↑はい、この辺を使うんですね分かります。

//MyPageWidgetは省略

class MyPageState extends State<MyPageWidget>{
   var _currentPage = 0;
   var duration = Duration(milisecond: 200); //アニメーションする時間
   var curve = Curves.fastInSlowOut; //アニメーションの動き
   @Override 
   Widget build(BuildContext context) {
      final controller = PageController();

      //色々省略

      onPressed: _current > 0 ? (){ //前ページボタンイベント処理
           controller.previousPage(duration: duration,curve: curve);
         },
      //色々省略
                onChanged: (value) { //ドロップダウンリストアイテム選択イベント処理
                  if (value is int) {
                     controller.jumpToPage(value);
                  };
                },
       //色々省略
       onPressed: _current > 0 ? (){ //前ページボタンイベント処理
           controller.nextPage(duration: duration,curve: curve);
         },
   //色々省略
}

*これで動いた
*durationやcurveを変えるとアニメーションも変わる

気付かなかった理由

flutterのUIフレームワークになれすぎた。
何かを変更するときは値を変えてsetState()すれば終わりだと思ってた。
っていうかAnimatedContainerはsetState()で値変えたら自動でアニメーションするからさあ……(見苦しい言い訳)

androidStudioではずっとそうやって使ってきたのに完全に忘れちゃってたな。

おまけ

これらのメソッドはFutureクラスを返すので、

   controller.nextPage( duration: duration, curve:curve)
      .whenComplete((){
          //なにかの処理
       });

こうすれば、ページ切り替えが終わったあとに何らかの処理をする事ができる。

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

【iOS】UITextViewに貼り付けた画像をタップでプレビューする

UITextViewにはAttributedTextが使えるため、画像やEmojiを含んだウェブページ的な表現が出来ます。
「これで画像タップ時にプレビューとかもできたらな…」と思ったことはないですか?
僕はありました。

というわけで、以下のようにタップされた画像を開く方法を紹介します。
デフォルトではそのまんまの機能はなかったので、それを実現するコードと使い方の説明になります。
imageview.gif
※ タップ判定が概ね正しいことを示すため、TextViewをSelectableにしています

コードと使い方

このコードは、大半を以下の質問を参考にさせていただきました。
https://stackoverflow.com/questions/48498366/detect-tap-on-images-attached-in-nsattributedstring-while-uitextview-editing-is

import UIKit

class MemoViewController: UIViewController {
    @IBOutlet weak var textArea: UITextView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // [1] UITextViewにタップ判定を追加
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(textAreaTapped(sender:)))
        textArea.addGestureRecognizer(tapGesture)
    }

    @objc func textAreaTapped(sender: UITapGestureRecognizer) {
        guard case let senderView = sender.view, (senderView is UITextView) else { return }

        // [2] UITextView、LayoutManager、Locationを取得
        let textView = senderView as! UITextView
        let layoutManager = textView.layoutManager
        var location = sender.location(in: textView)
        // [3] LocationがInset分ずれるようなので訂正
        location.x -= textView.textContainerInset.left
        location.y -= textView.textContainerInset.top

        // [4] LayoutManagerを使ってタップした場所にあるAttributedTextのIndexを取得
        let textContainer = textView.textContainer
        let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        let textStorage = textView.textStorage
        guard characterIndex < textStorage.length else { return }

        // [5] タップ位置が対象AttributedText表示領域範囲内か検証。端にある場合など、判定領域が表示領域より大きいことがあるため
        let range = NSMakeRange(characterIndex, 1)
        let attributeBounds = layoutManager.boundingRect(forGlyphRange: range, in: textContainer)
        if attributeBounds.minX < location.x,
           attributeBounds.maxX > location.x,
           attributeBounds.minY < location.y,
           attributeBounds.maxY > location.y,
        // [6] 対象のAttributedTextのパーツを取り出し、画像ならプレビュー用ViewControllerに渡す
           let image = textView.getPartsOfRange(range).first as? UIImage {
            let viewController = ImagePreviewController(image: image) // ※プレビュー用VCは別途用意して下さい
            present(viewController, animated: true, completion: nil)
        }
    }
}

extension UITextView {
    func getPartsOfRange(_ range: NSRange) -> [AnyObject] {
        guard self.attributedText != nil else { return [] }
        var parts = [AnyObject]()

        let attributedString = self.attributedText
        attributedString?.enumerateAttributes(in: range, options: NSAttributedString.EnumerationOptions(rawValue: 0)) { (object, range, stop) in
            if object.keys.contains(NSAttributedString.Key.attachment) {
                if let attachment = object[NSAttributedString.Key.attachment] as? NSTextAttachment {
                    if let image = attachment.image {
                        parts.append(image)
                    } else if let image = attachment.image(forBounds: attachment.bounds, textContainer: nil, characterIndex: range.location) {
                        parts.append(image)
                    }
                }
            } else {
                let stringValue : String = attributedString!.attributedSubstring(from: range).string
                if (!stringValue.trimmingCharacters(in: .whitespaces).isEmpty) {
                    parts.append(stringValue as AnyObject)
                }
            }
        }
        return parts
    }
}

基本の使い方は以下の通りです。
1. タッププレビューを付けたいUITextViewに対してUITapGestureRecognizerを追加し、textAreaTappedを呼ぶようにする
2. UITextViewにextension等でgetPartsOfRangeを追加し、Rangeを指定してパーツを吐けるようにする
3. imageがとれたら、ViewControllerに渡してプレビュー。

※ このコードには画像閲覧用ViewControllerは付属しません

この例ではプレビューする画像はタップした1枚だけにしましたが、少し書き直して全ての画像を取得すればめくれるプレビューの実装も簡単と思います。応用してみてください。

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

GraphQLでタスク管理アプリを作る -フロントエンド編- [iOS+Apollo-iOS]

前書き

この記事はApollo-iOSを用いて、GraphQLを使用したTODOアプリのiOSの実装について紹介していくものです。
@ebkn さんの記事と @Climber22 さんの記事に触発されて作ったのでバックエンドの方はそちらを参考にしてもらえると

Qiita初投稿なので色々とガバガバだと思われますが何卒よろしくお願いします。

コードはこちら

Image from Gyazo
Image from Gyazo

主な技術要素

  • Apollo-iOS
  • Carthage
  • ReactorKit
  • RxSwift
  • RxDataSource
  • Swinject

Apollo

フロントエンドなので @Climber22 さんと書くことはそこまで変わりませんが
GraphQLのスキーマを元に通信部分のコードを型付きで自動生成してくれる便利なものです。
iOSだけではなくAndroid用のもあります。

開発体験

パースミスなどの簡単なミスから解放されるのとともに型があることで
コミュニケーションを綿密に取らなくても齟齬が生まれにくいのが良いところです。
またMoya等で書いていた部分が圧倒的に少なくなるのは工数がかけられない状況でも良いと感じました。

ディレクトリ構成

ディレクトリ構成は以下のようになっています。
多そうに見えますがそんなに大したことはないのと肝心なのは/Networking なので
そこだけ見てもらえば大丈夫です。

 $ tree -I "Carthage|GraphQLToDo.xcodeproj|Assets.xcassets"
.
├── Cartfile
├── Cartfile.resolved
├── CarthageInputFileList.xcfilelist
├── CarthageOutputFileList.xcfilelist
├── GraphQLToDo
│   ├── AppDelegate.swift
│   ├── Base.lproj
│   │   └── LaunchScreen.storyboard
│   ├── CellModel
│   │   ├── CellItem.swift
│   │   └── SectionModel.swift
│   ├── Component
│   │   ├── TaskCell.swift
│   │   ├── TaskCell.xib
│   │   └── TaskCellRector.swift
│   ├── Coordinator
│   │   ├── CoordinatorProtocol.swift
│   │   └── TaskCoordinator.swift
│   ├── DI
│   │   └── AppAssembly.swift
│   ├── Entities
│   │   └── Pagination.swift
│   ├── Extension
│   │   ├── Apollo+Rx.swift
│   │   ├── TaskFields+Equatable.swift
│   │   ├── UIScrolView+ReachBottom.swift
│   │   ├── UIScrolView+TouchBegan.swift
│   │   ├── UIViewController+instantiate.swift
│   │   └── UIViewControllerLifecycle+Rx.swift
│   ├── Foundation
│   │   └── DataFormatters.swift
│   ├── Info.plist
│   ├── Networking
│   │   ├── API.swift
│   │   ├── Repository
│   │   │   ├── ApiType.swift
│   │   │   ├── GraphQLApiType.swift
│   │   │   ├── GraphQLProviderType.swift
│   │   │   ├── GraphQLRepositoryType.swift
│   │   │   └── TaskRepository.swift
│   │   └── Schema
│   │       ├── Common.graphql
│   │       └── Task.graphql
│   ├── Presentable
│   │   └── TaskPresentable.swift
│   ├── Reactor
│   │   ├── CreateTaskViewReactor.swift
│   │   ├── TaskViewReactor.swift
│   │   └── TasksViewReactor.swift
│   ├── SceneDelegate.swift
│   ├── Service
│   │   └── TaskService.swift
│   ├── Store
│   │   └── AccessibleApolloStore.swift
│   ├── Storyboard
│   │   ├── CreateTask.storyboard
│   │   ├── Task.storyboard
│   │   └── Tasks.storyboard
│   ├── ViewController
│   │   ├── CreateTaskViewController.swift
│   │   ├── TaskViewController.swift
│   │   └── TasksViewController.swift
│   └── schema.json
├── GraphQLToDoTests
│   ├── GraphQLToDoTests.swift
│   └── Info.plist
├── GraphQLToDoUITests
│   ├── GraphQLToDoUITests.swift
│   └── Info.plist
├── Makefile
└── README.md

大まかな流れ

  • Apollo等のインストール
  • スキーマを書く
  • jsonの生成
  • ビルドでSwiftのコードの生成
  • 通信部分の実装
  • ViewとかViewModelとか という流れになります。 それでは最初のインストールからやってきます。

1. install

まずはインストールから
今回はライブラリの管理にCarthageを使用しているのでそれを使ってインストールします。
Apollo-iOSはCarthage/Cocoapods/Swift Package Managerの3つに対応しているので
どれを使ってインストールしても大丈夫ですが、最近盛り上がっているのとビルド速度でCarthageにしました
今回はMakefileを作ってあるので、makeしてもらえれば特に問題ないと思います。

2. スキーマを書く

次にバックエンドのスキーマを元にクライアント側のスキーマも書きます。
まずはバックエンドに対応しているQueryとMutationを叩くのを作ります
fragmentでレスポンスをまとめるのに関してはテストを考えるとできる限り書いた方がいいと思います。
可読性的にも向上すると思いますし。
また、コード生成をした時にstructでちゃんと精製してくれるので扱いやすいです。
ファイルの分け方としては意味単位で分けるのが良さげな気がします。
なので今回はTaskに関するものはTask.graphqlにPaginationなどの普遍的なものはCommon.graphqlに分離しています。

Task.graphql
fragment TaskFields on Task{
    id
    title
    notes
    completed
    due
}

query Tasks($input: TasksInput!, $orderBy: TaskOrderFields!, $page: PaginationInput!) {
    tasks(input: $input, orderBy: $orderBy, page: $page) {
        pageInfo {
            ...PageInfoFields
        }
        edges {
            cursor
            node {
                ...TaskFields
            }
        }
    }
}

mutation CreateTask($input: CreateTaskInput!) {
    createTask(input: $input) {
        ...TaskFields
    }
}

mutation UpdateTask($input: UpdateTaskInput!) {
    updateTask(input: $input) {
        ...TaskFields
    }
} 
Common.graphql
fragment PageInfoFields on PageInfo {
    endCursor
    hasNextPage
}

3. JSONの生成

Apollo-iOSだけではなくApolloClientにおいてはコード生成のためにapollo-toolingを使用しています。
これは先ほど作った.graphqlファイルからいろんな言語のコードを精製してくれたり、
サーバーからスキーマをダウンロードできたりするツールです。
しかしながらコード生成には先ほど書いたようなクライアントのスキーマとサーバーからダウンロードしてきたJSONの2つが必要なので
実際にapollo-toolingを使ってダウンロードしていきたいと思います。
まずはインストールからということで

$ npm install apollo

でインストールしてください。 -gはお好みで。
次は @ebkn さんが用意してくれた こちらをmakeして起動します。
そしたら/Networking下で次のコマンドを実行します。

$ apollo schema:download schema.json --endpoint http://localhost:3000/graphql

これでコード生成に必要なJSONがダウンロードされました。
このJSONには型定義がまとめられています
今回は認証がないのでこれでダウンロードできますが、認証がある場合は

$ apollo schema:download schema.json --endpoint http://localhost:3000/graphql --header "Authorization: Bearer <token>"

という形になります

4. ビルド設定

クライアント側のスキーマとコード生成に必要なJSONが揃ったので、Swiftのコードが生成されるようにビルドの設定をしていきます。
これはapolloのドキュメントのをコピペすれば大丈夫です、
ただ生成されるAPI.swiftのパスと、参照するスキーマとJSONのパスは自由に変えられるので
今回は以下のようにしています。

"${SCRIPT_PATH}"/run-bundled-codegen.sh codegen:generate --target=swift --includes=./Networking/Schema/*.graphql --localSchemaFile="./schema.json" ./Networking/API.swift

内部的にはコンパイルされる前にapokko-toolingSwiftのコードを生成するスクリプトを動かしています。
これで生成されるAPI.swiftは一番最初はXcodeに認識されてないのでAdd fileで追加しておいてください。

5. 通信部分の実装

5.1 ApolloClientの生成

ここから実際にswiftのコードを書いていきます。
まずは通信に必要なApolloClientのインスタンスを生成するところから
URLの文字列はBundleから呼び出すようにしています。

GraphQLProviderType.swift
    var baseURL: URL {
        guard
            let baseApiString = Bundle.main.object(forInfoDictionaryKey: "BaseAPI") as? String,
            let apiURL = URL(string: baseApiString)
            else {
                fatalError("BaseAPI is unavailable")
        }
        return apiURL
    }

    lazy var client: ApolloClient = {
        let networkTransport = HTTPNetworkTransport(url: baseURL)
        let client = ApolloClient(networkTransport: networkTransport)
        client.cacheKeyForObject = { $0["id"] }
        return client
    }()

クライアントの生成はコードを見てもらってもわかるように非常に簡単でヘッダーやURL、使用する通信プロトコル等を指定する
NetworkTransportをClientに渡すだけです。今回はHTTPNetworkTransportを使用していますが
他にもWebSocketを利用するWebSocketTransport
一部分にのみWebSocketを使うSplitNetworkTransportの2種類があります。
headerでtokenを渡す際には別のinitでこんな感じになります 

Example.swift
        let configuration = URLSessionConfiguration.default
        configuration.httpAdditionalHeaders = token
        let networkTransport = HTTPNetworkTransport(url: baseUrl,
                                                    session: URLSession(configuration: configuration),
                                                    sendOperationIdentifiers: false,
                                                    useGETForQueries: false,
                                                    enableAutoPersistedQueries: false,
                                                    useGETForPersistedQueryRetry: false,
                                                    delegate: nil,
                                                    requestCreator: ApolloRequestCreator())

さて、今説明したApolloClientを使って実際にQueryとMutationの部分を作っていきます。
まず基本的には以下のような形でfetch/mutationを実行します。

Example.swift
//Query
client.fetch(
    query: query,
    cachePolicy: cachePolicy,
    queue: queue
    )
{ result in
    switch result {
    case let .failure(error):
        // do something
    case let .success(response):
        if let errors = response.errors {
            // do something
        }
        else if let data = response.data {
            // do something
        }
    }
}
//Mutation
client.perform(
    mutation: mutation,
    queue: queue
    )
{ result in
    switch result {
    case let .failure(error):
        // do something
    case let .success(response):
        if let errors = response.errors {
            // do something
        }
        else if let data = response.data {
            // do something
        }
    }
}

レスポンスはCompletationHundlerにResult型で返ってきます。
.successの方にもエラーのチェックをしている部分がありますが、これは仕様としてQuery/Mutationでのインプットがおかしい場合はステータスコード200でエラーが返ってくるために必要になります。
引数にあるcachePolicyqueueについては前者はどのようにキャッシュを使いながらQueryを実行するかの指定で、後者はどのスレッドで実行するかを指定するものとなっています。
デフォルトではメインスレッドでの実行かつ、
CachePolicyは.returnCacheDataElseFetchというキャッシュがあるなら使うけど無いならサーバーからfetchするというものになっています。
今回は通信部分ということでバックグラウンドでの処理にしたいので.global()Queueを指定しています。
更に使いやすいように以下のようにRxで流せるように拡張しています。

Apollo+Rx.swift
    func fetch<Query: GraphQLQuery>(
        query: Query,
        cachePolicy: CachePolicy = .returnCacheDataElseFetch,
        queue: DispatchQueue = DispatchQueue.main)
        -> Single<Query.Data>
    {
        return Single.create { single in
            let cancellable = self.provider.client.fetch(
                query: query,
                cachePolicy: cachePolicy,
                queue: queue)
            { result in
                switch result {
                case let .failure(error):
                    single(.error(error))
                case let .success(response):
                    if let errors = response.errors {
                        single(.error(RxGraphQLError.graphQLErrors(errors)))
                    }
                    else if let data = response.data {
                        single(.success(data))
                    }
                }
            }
            return Disposables.create {
                cancellable.cancel()
            }
        }
    }

これによって実際にタスクをfetchしている部分はこのように簡潔に書くことができます。
今回はTaskServiceでレスポンスを扱いやすい形に直しています。

TaskRepository.swift
    func fetchTasks(
        input: TasksInput,
        orderBy: TaskOrderFields,
        page: PaginationInput,
        refetch: Bool
    )
        -> Single<TasksQuery.Data> {
            let cachePolicy: CachePolicy = refetch ? .fetchIgnoringCacheData : .returnCacheDataElseFetch
            return provider.rx.fetch(query: TasksQuery(input: input,
                                                       orderBy: orderBy,
                                                       page: page),
                                     cachePolicy: cachePolicy,
                                     queue: .global())
    }

あとはService層などで使いやすい形に整形すれば通信部分の実装は完了です。

6. Viewとか

ここからは実際にViewに表示したり画面遷移したりの部分ですが、
メインはApolloを使った通信部分なので詳しくは解説しません。
使っている技術と簡単な説明としては

  • Coordinator: 画面遷移をViewの外に分離
  • RxDatasources: TableViewとPickerViewのDatasouceとして使用
  • ReactorKit: ViewのStateの保持と通信処理の呼び出し
  • Swinject: これを使って先ほど説明したApolloClientなどをDI

というようになっています。
3つとも簡単なレイアウトなのでStoryboardで作成しています。
ReactorKitを使用しているのでViewは入力をreactorに対してbindしてるだけなので
主に実際に処理をしているreactorの説明になります。

6.1 タスク一覧の作成

レイアウトはタスク一覧を表示するためのTableViewとタスク作成画面を表示するためのボタン、
並べ替え用のPickerを表示するためのボタンとなっています。
Simulator Screen Shot - iPhone 11 Pro Max - 2020-02-24 at 20.45.28.png

この画面で必要な機能は

  • タスクの表示
  • スクロールでのページネーション
  • リロード
  • フィルタリング

の4つなのでこれを実装していきます。

6.1.1 タスク一覧の読み込み

ということでまずはタスク一覧の表示から、
これはreactorでTaskServiceにあるfetchTaskを呼び出してMutationに変換してStateにいれるだけです。

TasksViewReactor.swift
        case let .loadTask(reload):
            let startLoading: Observable<Mutation> = .just(.setIsLoading(true))
            let tasks = taskService.fetchTasks(completed: state.isCompleted,
                                               order: state.taskOrder,
                                               endCursor: "",
                                               hasNext: false,
                                               refetch: reload)
                .map { page in
                    let cellItem = page.pageElements.map { CellItem.task(
                        TaskCellReactor(taskService: self.taskService, task: $0)
                        )
                    }

                    return Pagination(pageElements: cellItem,
                                      hasNextPage: page.hasNextPage,
                                      endCursor: page.endCursor)
            }
            .map(Mutation.setTasks)
            let endLoading: Observable<Mutation> = .just(.setIsLoading(false))
            return .concat([startLoading, tasks, endLoading])

6.1.2 ページネーション

次にスクロールでのページネーションですが
ページネーション自体は、カーソルの指定と次のページが有るかないかを見るだけなので
以下のようにタスク一覧の表示とやってることは変わらないです。

TasksViewReactor.swift
            let nextPage = taskService.fetchTasks(completed: state.isCompleted,
                                                  order: state.taskOrder,
                                                  endCursor: state.tasks.endCursor,
                                                  hasNext: state.tasks.hasNextPage,
                                                  refetch: false)
                .map { page in
                    let cellItem = page.pageElements.map { CellItem.task(
                        TaskCellReactor(taskService: self.taskService, task: $0)
                        )
                    }

                    return Pagination(pageElements: cellItem,
                                      hasNextPage: page.hasNextPage,
                                      endCursor: page.endCursor)
            }

ページネーションの呼び出しはTableViewを以下のような形でExtensionさせて呼び出すようにしています。

UIScrolView+ReachBottom.swift
    var isReachedBottom: Observable<Void> {
        return contentOffset
            .filter { [weak base = self.base] _ in
                guard let base = base else { return false }
                return base.isReachedBottom(tolerance: base.bounds.height / 2)
            }
            .map { _ in }
    }

6.1.3 リロード

リロードについてなんですがApollo-iOSではrefetchの指定ができなく、
かつ基本的にキャッシュを参照してレスポンスを返してくるので
今回は以下のようにQueryに渡すcachePolicyをキャッシュを参照しないものに変更してfetchし直すことで対応しています。

let cachePolicy: CachePolicy = refetch ? .fetchIgnoringCacheData : .returnCacheDataElseFetch

6.1.4 フィルタリング

フィルタリングは以下のようにPickerで内容をbindしつつ、
リロードしている感じです。

TasksViewReactor.swift
        case let .selectCompletedAndOrder(row, component):
            switch state.menuOrderOptions[component][row] {
            case TaskOrderFields.latest.rawValue:
                return .just(.setTaskOrder(.latest))
            case TaskOrderFields.due.rawValue:
                return .just(.setTaskOrder(.due))
            case CompletedSelect.all.rawValue:
                return .just(.setIsCompleted(nil))
            case CompletedSelect.completed.rawValue:
                return .just(.setIsCompleted(true))
            case CompletedSelect.notCompleted.rawValue:
                return .just(.setIsCompleted(false))
            default:
                return .empty()
            }
TasksViewController.swift
        reactor.state.map { $0.taskOrder }
            .distinctUntilChanged()
            .map { _ in Reactor.Action.load }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

6.2 タスクページの作成

Simulator Screen Shot - iPhone 11 Pro Max - 2020-02-24 at 20.45.39.png

この画面で必要な機能は

  • タスクの表示
  • タスクの編集/アップデート

の3つなのでこれを実装していきます。

6.2.1 タスクの読み込み

GraphQLはその名前の由来として自動的にルートクエリからキャッシュを作る機構があります。
これはApollo-iOSでも利用でき、ApolloStoreというクラスからキャッシュにアクセスすることができます。
しかしながらデフォルトではfetchしてきたオブジェクトが何のキーにも紐づいていないので、
明示的にアクセスしたい場合にはキーを設定する必要があります。
ということで以下のようにcacheKeyForObjectにidを指定します。

GraphQLProviderType.swift
    lazy var client: ApolloClient = {
        let networkTransport = HTTPNetworkTransport(url: baseURL)
        let client = ApolloClient(networkTransport: networkTransport)
        client.cacheKeyForObject = { $0["id"] }
        return client
    }()

これによってfragmentに含まれているidをもとにApolloStoreから読み出せるようになったので、
実際に以下のようにStoreからの読み出しを実装していきます。

AccessibleApolloStore.swift
    func load <T: GraphQLSelectionSet>(
        identifier: String,
        fragmentType: T.Type
    )
        -> Observable<T>
    {
        return .create { observable in
            self.provider.client.store.withinReadTransaction(
                { transaction -> T in
                    let data = try transaction.readObject(ofType: fragmentType,
                                                          withKey: identifier)
                    return data
            },
                callbackQueue: .global(),
                completion: { result in
                    switch result {
                    case let .success(data):
                        observable.onNext(data)
                    case let .failure(error):
                        observable.onError(error)
                    }
            })
            return Disposables.create()
        }
    }

これでTask画面で表示するのは単純にStoreを呼び出すだけになります。

TaskViewReactor.swift
        case .load:
            let task = accessibleApolloStore.load(identifier: taskIdentifier,
                                                  fragmentType: TaskFields.self)
                .map(Mutation.setTask)
            return task

6.2.2 タスクのアップデート

次にアップデートの実装です
最初にキャッシュから読み出したTaskと同じかどうかを見て画面が非表示になる際に更新しています。
とても単純
このアップデート実行時に同時にApolloStoreにあるキャッシュもアップデートしています。

TaskViewReactor.swift
        case .updateTask:
            guard cachedTask != state.task else { return .empty() }
            taskService.updateTask(taskIdentifier: taskIdentifier,
                                   title: state.task.title,
                                   notes: state.task.notes,
                                   completed: state.task.completed,
                                   due: state.task.due)

                .do(onNext: {[weak self] task in
                    let _ = self?.accessibleApolloStore.update(identifier: task.id,
                                                               fragmentType: TaskFields.self,
                                                               newItem: task)
                    self?.taskService.onNextUpdateTaskStream(task: task)
                })
                .subscribe()
            return .empty()

6.3 タスク作成ページの作成

Simulator Screen Shot - iPhone 11 Pro Max - 2020-02-24 at 20.45.49.png

この画面で必要な機能は

  • タスクの作成

の1つだけなんで実装していきます。
各UIコンポーネントからreactorに対して値をbindするだけで、そこまで難しいものではありません。
タイトルはOptionalではないのでguardでタイトルがちゃんと入力されているかだけを見てcreateTaskを呼び出しています。

CreateTaskViewReactor.swift
case .createTask:
            guard state.isCreatable else { return .empty() }
            let due = state.due == nil ? nil : ISO8601DateFormatter().string(from: state.due!)
            let isCreated = taskService.createTask(title: state.title,
                                                   notes: state.notes,
                                                   completed: state.isCompleted,
                                                   due: due)
                .do(onNext: {[weak self] task in
                    self?.taskService.onNextCreateTaskStream(task: task)
                })
                .map { _ in true }
                .map(Mutation.setIsCreated)
            return isCreated

タスク作成時にはシングルトンのTaskServiceを通してタスク一覧画面に対して、リロードするようにしています。


今回はReactorKitとApollo-iOSを使った実装を紹介しました。
勝手に型付きで通信部分を自動生成してくれるのは負担も減るし開発体験として素晴らしいものがあります。
Apollo-iOSはどんどん変わっていくので追うのは大変ですがどんどん便利な機能も増えていくし、開発体験はとにかく最高なので皆さんもぜひやってみてください。

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

iOSアプリのアイコンをffmpegコマンドを使ってアルファチャンネルなしで各サイズを吐き出す

はじめに

iosアプリのアイコンでは異なるサイズの画像をたくさん作る必要があります。
でこれがめちゃくちゃめんどくさいので、そのためのツールが色々あるのでまずはこれらを試してみることをお勧めします。

一番簡単にWebアプリ上できるのがこちらで
appicon

Macのアプリでできるのがこちらです
Image2icon

遭遇した問題 〜 アルファチャンネルが追加されてしまう 〜

今回自分はillustratorでアイコンを描き画像としてでexportしました。
そして上記のツールでアイコンを作成して、App Store Connectにアップロードしようとすると、以下のようなエラーが出ました。

Error ITMS-90717: "Invalid App Store Icon. The App Store Icon in the asset catalog in 'YourApp.app' can't be transparent nor contain an alpha channel."

image.png

アルファチャンネルが含まれているから使えないよーとのことですね。
なるほど書き出したアイコンにアルファチャンネルが含まれているのねーということで確認してみると、含まれていません。

apole_pngの情報.png

色々試してみたのですが、どのツールを使っても書き出し後の画像にはアルファチャンネルが含まれてしまいました。
Icon-1024_pngの情報_と_ダウンロード.png

解決法 〜 ffmpegでリサイズする 〜

macのプレビューアプリで1つずつ、アルファチャンネルを除去することもできるのですが、そんなことはめんどくさすぎて絶対したくないので、ffmpegを使ったshell scriptを使います。

# Export ios app icons by ffmpeg scale command
# usage: sh export_ios_icons.sh {path_to_your_img}
# example: sh export_ios_icons.sh ./app_icon.png

# sizes of images
# you can get other size images by editing thisarray
size=(20 40 60 29 58 87 80 120 180 76 152 167 1024)
for i in "${size[@]}"
do
   : 
    ffmpeg -i $1 -vf scale=$i:$i output_$ix$i.png
done

gist

作りたい画像サイズを配列で保持しておいて、
ここでリサイズした画像を生成します。

ffmpeg -i $1 -vf scale=$i:$i output_$ix$i.png
# ex) ffmpeg -i $1 -vf scale=1024:1024 output_1024x1024.png

実行すると以下のように各画像サイズのアイコンが出力され、アルファチャンネルがない状態も見事キープされていますね!
ios_icon.png
output_1024_pngの情報.png

めでたしめでたし :confetti_ball:

(あとで気づいたがそもそもjpgでやれば、こんな問題全く起きなかったかも...)

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

【SwiftUI】1-1 Creating and Combining Views を噛み砕いていく【チュートリアル】

実行環境

  • MacOS 10.15.3
  • Xcode Version 11.3.1

最初の章「SwiftUI Essentials」は以下の3章からなってます
- Creating and Combining Views (今回)
- Building Lists and Navigation
- Handling User Input

スクリーンショット 2020-02-24 22.48.49.png

該当ページからファイルをダウンロードしないと、できない演習がありますので、毎回必ずダウンロードしてからチュートリアルを進めてください。

今回解説するCreating and Combining Viewsは全部で6編あります
  
1. 編集内容がリアルタイムに反映されるよ
2. 文字色の変え方
3. 縦横リスト、空白等の挿入方法
4. 画像の挿入方法や画像加工の方法
5. UIkitとMapViewを使ってみよう
6. 2〜6の知識をまとめて1つのUIを作ってみよう

という内容になってます。
実際にXcodeで動かしてみてください。

最初の章なので、まっさらなところから書き始めます。
次の章からはページの初めにあるResouseをダウンロードして、startのファイルからプロジェクト(チュートリアル)を開始します。

1-1 編集内容がリアルタイムに反映されるよ

要点…Resumeを押すとCanvasに変更内容がリアルタイムで反映される。

用語
・Project navigator...Xcodeの左端にあるファイル図が表示されているエリア。左上のファイルマークをクリックすると表示される(デフォルト)
・Resume…Canvasにコードの結果を表示するボタン。
・Canvas…コード結果を反映するエリアのこと(プレビューエリア)。上のEditorで色々いじれるっぽい
スクリーンショット 2020-02-24 23.22.12.png

1-2 文字色の変え方

要点…commandキー押しながらプレビューのテキストorコード内のText()をクリックするとそのエリアに対して様々なアクションを起こせる。
今回はShow Swift Inspect(チュートリアルではInspect)を押すことで、コードを書かなくても文字とその関連情報を一括で操作できることを確認する。(ショートカット有)

いろいろなところでコマンドキーを押しながらコードやらプレビューをクリックして慣れよう。

用語
・Inspect...text()内のフォント等を一括でGUI操作するアクション

1-3 縦横リスト、空白等の挿入方法

要点…CSS既習の人には見慣れたライブラリが多い。
Xcodeでは文字を打つだけでなく、左上の「+」ボタンを押すとライブラリがまとめて表示され、それをエディター部分にドラッグアンドドロップすると、コードが書き込まれるという話。

用語
・VStack…縦方向に要素や配列を揃える。HStackは横
・alignment: .leading (右)とか…CSSでお馴染み(?)のやつ。alignmentは「揃える」という意味。.hogehogeは右、中央、左等に揃える場所を選べる。
・spacer, padding…ライブラリ。前者はスペースを出力。後者はエリア周上に空白を出力
(次からライブラリは用語に書かない。興味ある人はライブラリの説明文と開発者向けドキュメントを読んでください)

今回のライブラリはpadding以外全部「+」のライブラリにありました。

1-4 画像の挿入方法や画像加工の方法

要点…Xcodeで画像ソース等を使う場合、「Preview content」フォルダの「Preview Assets.xcassets」にドラッグアンドドロップする。
その後、画像編集用のファイルを新規作成しライブラリを使って画像を加工していく。

コードを書いても可能、ライブラリのドラッグアンドドロップも可能。

用語
なし。ライブラリのみ

1-5 UIkitとMapViewを使ってみよう

要点…正直よくわからない。コピーアンドペーストで済ませた。詳細が発覚し次第、書こうと思います。ちなみにIDEあるあるですが、コーディングが完了前にエラーが出続けても、Appleさんは「かまわん、行け」と仰るのでそのまま書き続けましょう。

用語
なし

1-6 2〜6の知識をまとめて1つのUIを作ってみよう

要点…4と5で作ったファイルは他のファイルで呼び出せるので、組み合わせようという話。

用語
なし

まとめ

・なんかあったらcommandキーを押しながら操作してみよう。
・Xcodeの基本的な操作方法が習得できる。
・ファイルはfuncで呼び出せる。

そのうちXcode専用用語のまとめとか作れたらいいなーと思ってます

次回→


追記
関数じゃなくて構造体を呼び出せるってことだったっぽい

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

【ARKit】配置した3Dモデル(アニメーションつき)を削除する

はじめに

別記事で書いたアニメーションつきの3DモデルをARKitに追加するのに引き続き、ARKItに追加した3Dモデル(アニメーションつき)を削除するときにも躓いたので、そのメモ。

以下のサイトを参考に、長押しで3Dモデルを消そうとしたが削除できませんでした。このサイトではプリミティブなオブジェクト(cube)を配置しているので、今回使う3Dモデル(アニメーションつき)と何が違うのか?

実行環境

  • Xcode 11.2.1

3DモデルはMagicaVoxelで作って、Mixamo でアニメーションをつけたものを使用しました。

.scnファイルの構造

.scnファイル(sitting.scn)を開いてみると、直下に「sitting」ノードがあって、その中に3Dモデルとボーンの設定が含まれています。
cap01.png

アニメーション付き3Dモデルの配置時

配置するスクリプトは、直下の「sitting」のノードを取得して、self.mainSceneView.scene.rootNode に追加するようになっていますが、追加するノードに名前がないため、名前をつける処理(node.name = selectedItem)を追加しました。

※削除する際に、どのノードか判断するためです。

アニメーション付き3Dモデルの配置
    var selectedItem: String? = "sitting"

    //(中略)

    /// アイテム配置メソッド
    func addItem(hitTestResult: ARHitTestResult) {
        if let selectedItem = self.selectedItem {

            // .scnファイルから新しい3Dモデルのノードを作成
            let scene = SCNScene(named: "art.scnassets/\(selectedItem).scn")
            let node = (scene?.rootNode.childNode(withName: selectedItem, recursively: false))!

            // 現実世界の座標を取得
            let transform = hitTestResult.worldTransform
            let thirdColumn = transform.columns.3

            // 3Dモデルの配置
            node.position = SCNVector3(thirdColumn.x, thirdColumn.y, thirdColumn.z)

            // 3Dモデルのサイズを変更
            node.scale = SCNVector3(0.05, 0.05, 0.05)

            // 3Dモデルに名前をつける
            node.name = selectedItem

            // シーンに追加
            self.mainSceneView.scene.rootNode.addChildNode(node)
        }
    }

アニメーション付き3Dモデルの削除

.scnファイルの構造で見たように、配置したモデルは「sitting」というノード名で、その中に3Dモデル(unamed)とボーン(mixamorig_Hips)が含まれています。

長押しでのオブジェクトが存在するかどうかの判定は、ARSCNView.hitTest(_:types:)で行いますが、このときの検出対象は3Dモデル(unamed)になるため、ノード全体を削除するには親である「sitting」ノードを削除する必要があります。

削除の処理は以下のようになります。

アニメーション付き3Dモデルの削除
    // ロングプレスイベントハンドラの登録
     let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressView))
     self.mainSceneView.addGestureRecognizer(longPressGesture)

    //(中略)

    // 長押しでキャラクタを削除する
    @objc func longPressView(sender: UILongPressGestureRecognizer) {
        print("----長押し!")

        if sender.state == .began {
            let location = sender.location(in: self.mainSceneView)
            let hitTest  = self.mainSceneView.hitTest(location)

            if let result = hitTest.first  {

                // 3Dアニメーションモデルは、複数パーツで構成されるため、親ノードの名前で判定・削除する
                if result.node.parent!.name == selectedItem
                {
                    result.node.parent!.removeFromParentNode();
                }
            }
        }
    }

まとめ

ノードを扱う際には、常に階層構造を意識しないとダメってことですね(わかってみれば当然ですが)。

他にもっといい方法があれば、教えてください。

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