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

【Swift】UITableViewのセルのタップでキーボードを表示する

1.はじめに
テーブルビューのセルにUITextFieldを設置し、セルのタップでテキストフィールドのキーボードを表示させます。
その際、テキストフィールドをタップしたことによるキーボードの表示は避けます。
なぜかというと、テキストフィールドをタップしてキーボードを表示した場合、セルが選択状態にならないからです。
あくまでもtableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) からキーボードを表示させるのが目的です。

セルの選択契機とテキストフィールドのタップ契機では微妙に見た目が違うので、セルの選択のみを契機としてキーボードを表示させます。

2.テキストフィールドの設定
isUserInteractionEnabledfalse にする。これはstoryboardから設定できます。この設定により、テキストフィールドのタップを無効化します。
canBecomeFirstRespondertrue にする。これによってテキストフィールドでキーボードを表示できるようします。ただ、このプロパティはget onlyのため、テキストフィールドを継承したサブクラスを定義し、そこで設定します。

class TextFieldSub: UITextField {
    override var canBecomeFirstResponder: Bool {
        return true
    }
}

3.セルの選択時にキーボードを表示する
あとはUITableViewDelegate のメソッドでセルのタップを捕まえてキーボードを表示するだけです。

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let cell = tableView.cellForRow(at: indexPath)  // 選択したセルの取得
    cell.textField.becomeFirstResponder()           // セルのテキストフィールドでキーボードを表示する
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CGImage.cropping()の注意点

はじめに

この前リリースしたAR Mini SketchというアプリでUIImageView上の画像をユーザーが指定した範囲で切り抜くという処理があるが、画像によっては指定通りに切り抜かれないという不具合があった。

原因を調べてみたところ、CGImage.cropping()の際の範囲指定には元画像の向きを考慮しなければいけないことが分かったため、注意点を備忘として残しておく。

CGImageクラスとは

CGImageクラスとはCore Graphics Imageの略。
bitmapイメージとして画像のマスク処理や切抜き処理を行うことができる。

尚、このクラスにはwidthとheightというプロパティはあるがorientationというプロパティはない。

UIImageクラスとは

iOSアプリ上で画像を操作する際の最も一般的なクラス。
リソース上の画像ファイルを読み込む際も大抵このクラスのインスタンスとして操作することが多い。
UIImage.cgImageでこの画像のGCImageを参照することができる。

尚、このクラスにはorientationというプロパティがあり画像の向き情報を保持している。

CGImage.cropping()とは

定義

func cropping(to rect: CGRect) -> CGImage?

rectで指定した範囲を切り抜いて、新しいCGImageのインスタンスを返してくれる

注意点

rect は切り抜きたいCGImageのスケールで指定する必要がある。
具体的な例を示すと、400x200のサイズのCGImageの丁度真ん中で50x50で画像を切り抜きたい場合、CGRectのパラメータは次の様になる。

let rect = CGRect(x: 200 - 25, y: 100 - 25, width: 50, height: 50)

また、大抵UIImageViewと表示するUIImageのサイズは一致しないため、UIImageView上で指定された範囲は実際のCGImageのスケールに直す必要がある。

スクリーンショット 2019-04-17 22.14.41.png

例えば上記の図のようにUIImageViewの表示上のサイズが400x1000で実際のUIImageのサイズが800x1200だったとした場合、選択範囲のRectは次のようにスケールを変更しなければならない。

let transform = CGAffineTransform(scaleX: 2.0, y: 1.2)
rect.applying(transform)

本題

じゃあ毎回、UIImageViewのサイズとUIImage(CGImage)のサイズを測って指定範囲のスケールを変更すれば良いのね、という訳ではなく一つ落とし穴がある。

ここのサイトの説明が分かり易いが画像にはorientationというプロパティが保持されており、UIImageView上で縦向きに画像が表示されていたとしてもCGImageの段階で横になっているということがある。

そうなると、下の図のように単純にスケールを変更しただけでは意図した場所から外れてしまうことになる。

スクリーンショット 2019-04-17 22.42.40.png

したがって、UIImageView上で指定された範囲でCGImage.cropping()を行う際には次のロジックで処理を行う必要がある。

1. UIImageViewと表示しているUIImageの縦横のスケール比率を取得する

2. UIImageのorientationを取得し、画像の向きを判定する

3. 画像の向きに応じて範囲の回転並びに、平行移動を行う

4. 先ほど計算したスケール比率で範囲の拡大/縮小を行う

実際に書くとこんな感じ

extension UIImageView {
    func transformByImage(rect : CGRect) -> CGRect? {
        guard let image = self.image else { return nil }
        let imageSize = image.size
        let imageOrientation = image.imageOrientation
        let selfSize = self.frame.size

        let scaleWidth = imageSize.width / selfSize.width
        let scaleHeight = imageSize.height / selfSize.height

        var transform: CGAffineTransform

        switch imageOrientation {
        case .left:
            transform = CGAffineTransform(rotationAngle: .pi / 2).translatedBy(x: 0, y: -image.size.height)
        case .right:
            transform = CGAffineTransform(rotationAngle: -.pi / 2).translatedBy(x: -image.size.width, y: 0)
        case .down:
            transform = CGAffineTransform(rotationAngle: -.pi).translatedBy(x: -image.size.width, y: -image.size.height)
        default:
            transform = .identity
        }

        transform = transform.scaledBy(x: scaleWidth, y: scaleHeight)

        return rect.applying(transform)
    }
}

こんな感じで使います。

let cropRect = self.cropRect
let imageView = self.imageView

guard let image = imageView?.image else { return }
guard let rectByImage = imageView?.transformByImage(rect : cropRect) else { return }

let croppedImage = image.cgImage.cropping(to: rectByImage)
let newImage = UIImage(cgImage: croppedImage!, scale: imageView.scale, orientation: image.imageOrientation)

もし誰かのお役に立てば嬉しいです。

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

React Native 0.57.xにおけるPodfileのsubspecsとpost_installの例

うまくいったPodfile

0.57.6で通ったPodfileの例。

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'appname' do
  pod 'React', :path => '../node_modules/react-native', :subspecs => [
    'Core',
    'CxxBridge', # RN >= 0.47では必要
    'DevSupport', # RN >= 0.43で開発関連の機能を使う場合
    'RCTText',
    'RCTNetwork',
    'RCTAnimation'
  ]
  # RN >= 0.42.0では明示が必要
  pod 'yoga', :path => '../node_modules/react-native/ReactCommon/yoga'

  # 追加のpod
  pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    if target.name == "React"
      target.remove_from_project
    end
    target.build_configurations.each do |config|
        config.build_settings['DYLIB_COMPATIBILITY_VERSION'] = ''
    end
  end
end

正直何が何だかわからないけどこんな感じでビルドできた。

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

Ionic × FCMでiOSのPUSH配信ができなかった時の解決策メモ

この記事について

Ionic を用いたアプリ開発をしており、PUSH配信に FirebaseCloudMessaging を使用しているのですが、iOSへのPUSH配信が出来なかったのでその時の解決方法です。

実装時の環境

  • Ionic : 3.9.4
  • Cordova: 8.0.0
  • cordova-plugin-firebase: 2.0.5
  • Java11 (APIサーバー)
  • Firebase Admin SDK

現象

Ionic側

import { Firebase } from "@ionic-native/firebase/ngx";

export class Sample {
  constructor(private firebase: Firebase) {}

public sample() {
    // 通知許可の設定
    this.firebase.hasPermission().then( data => {
      console.log("data.isEnabled:" + data.isEnabled)
      if(data.isEnabled != true) {
        this.firebase.grantPermission().then( () => {
          console.log("grantPermission:")
        }).catch((error) => {
          console.log("grantPermission Error:" + JSON.stringify(error))
        })
      }
    })
}

上記のコードで トークン の取得は正常に行えてるように見えました。

APIサーバー側

Firebase Admin SDK を用いて、
PUSH配信を行おうとすると、下記の例外が発生しました。

com.google.firebase.messaging.FirebaseMessagingException: Request contains an invalid argument.
    at com.google.firebase.messaging.FirebaseMessaging.handleSendHttpError(FirebaseMessaging.java:291)
    at com.google.firebase.messaging.FirebaseMessaging.access$600(FirebaseMessaging.java:55)
    at com.google.firebase.messaging.FirebaseMessaging$1.execute(FirebaseMessaging.java:260)
    at com.google.firebase.messaging.FirebaseMessaging$1.execute(FirebaseMessaging.java:240)
    at com.google.firebase.internal.CallableOperation.call(CallableOperation.java:36)

Caused by: com.google.api.client.http.HttpResponseException: 400 Bad Request
{
  "error": {
    "code": 400,
    "message": "Request contains an invalid argument.",
    "status": "INVALID_ARGUMENT",
    "details": [
      {
        "@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError",
        "errorCode": "INVALID_ARGUMENT"
      },
      {
        "@type": "type.googleapis.com/google.firebase.fcm.v1.ApnsError",
        "statusCode": 400,
        "reason": "BadDeviceToken"
      },
      {
        "@type": "type.googleapis.com/google.rpc.BadRequest",
        "fieldViolations": [
          {
            "field": "message.token",
            "description": "Invalid registration token"
          }
        ]
      }
    ]
  }
}

解決策

xcodeでのビルド時に、下記の設定をする必要がありました。
Xcode > File > Project Settings > Build System [Lagacy Build System]

FCMのその他の設定について

こちら の記事を参考にさせて頂きました。

最後に

結構ハマってしまったので、誰かの助けになれば幸いです・・・!

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

iOS標準のCLGeocoderを使って郵便番号から住所を取得する

郵便番号から住所を取得する

CLGeocoderには標準で

func geocodeAddressString(_ addressString: String, completionHandler: @escaping CLGeocodeCompletionHandler)

こちらの郵便番号から住所を取得するメソッドがあるのですが、これで取得するとCLPlacemarkのsubLocalityが取得できませんでした。
そのため、一度このメソッドを使って取得した住所の緯度経度を使ってCLLocationを作り

func reverseGeocodeLocation(_ location: CLLocation, completionHandler: @escaping CLGeocodeCompletionHandler)

こちらのCLLocationから住所を取得するメソッドを使うことでsubLocalityが取得することができます。

扱いやすいようにCLGeocoderにextensionで上記の方法で郵便番号から住所を取得するメソッドを追加すると良いかと思います。

extension CLGeocoder {

    struct Address {
        var administrativeArea: String? // 都道府県 例) 東京都
        var locality: String? // 市区町村 例) 墨田区
        var subLocality: String? // 地名 例) 押上
    }

    func convertAddress(from postalCode: String, completion: @escaping (Address?, Error?) -> Void) {
        CLGeocoder().geocodeAddressString(postalCode) { (placemarks, error) in
            if let error = error {
                completion(nil, error)
                return
            }
            if let placemark = placemarks?.first {
                let location = CLLocation(
                    latitude: (placemark.location?.coordinate.latitude)!,
                    longitude: (placemark.location?.coordinate.longitude)!
                )
                 CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
                    guard let placemark = placemarks?.first, error == nil else {
                        completion(nil, error)
                        return
                    }
                    var address: Address = Address()
                    address.administrativeArea = placemark.administrativeArea
                    address.locality = placemark.locality
                    address.subLocality = placemark.subLocality
                    completion(address, nil)
                }
            }
        }
    }
}
// 郵便番号はハイフンなしでも取得可能です
CLGeocoder().convertAddress(from: "131-0045") { (address, error) in
    if let error = error {
        print(error)
        return
    }
    print(address?.administrativeArea) // → 東京都
    print(address?.locality) // → 墨田区
    print(address?.subLocality) // → 押上
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

XVim2をinstallする

XVim2/SIGNING_Xcode.md

1. 自己証明書の作成

※ 自己証明書の期限は1年なので、期限切れしたら再作成すること

2. 作成した証明書でXcodeに再署名

$ sudo codesign -f -s XcodeSigner /Applications/Xcode.app

3. XVim2をBuildする

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

Xamarin 最近どうよ?

「ネイティブアプリ開発者は絶滅危惧種なのか?」への感想文 - ナカザンドットネット

Office 365, MS teams, Skype, @code, and the edge debug protocol are being rewritten in js instead of C++ with special MS tooling : programming

への反応で、「最近 Xamarin 生きてんの?」という声がちらほら聞こえたので書いてみました。

もともとの 「ネイティブアプリ開発者は絶滅危惧種なのか?」 という問いには、「緩やかにそうなっていくでしょうね」 という冒頭の記事を書かれた @Nkzn さんとほぼ同じアンサー1 なので、この話題を Xamarin と絡めたらというネタで書きます。

マイクロソフトなのになんで React 使ってんの?Xamarin は?

Office も Teams も Skype も、全て Web アプリも含めての rewrite 構想だったのでしょうね。
Webを含めるという戦略も当然だし、Xamarin は Webアプリはカバーしない ので、至極当然の選択です、以上です。

近年のマイクロソフト

ここ数年のマイクロソフトは、軸足を完全に Azure、つまりサーバーサイドに移していて、クライアントサイドのプラットフォーム(Windows)で覇権を取ることはあきらめたように見えます。

フロントエンド技術への注力で目立つものは、

  • TypeScript 2
  • Xamarin
  • HoloLens

くらいで、いわゆる「Windows向けアプリケーション」を開発するための、

  • WPF(Windows Platform Foundation)
  • UWP(Universal Windows Platform)

といった領域の新機能はあまり目立ちません。

クロスプラットフォームな .NET 開発/実行環境である .NET Core

一方でマイクロソフトは、サーバーサイドというか、 Windows 以外のプラットフォームで .NET 資産を使えるようにする ことに注力し続けていて、それが .NET Core です。

.NET Core は 本当にクロスプラットフォーム で、 Linux や mac で同じバイナリが動作します。
AWS の Lambda も .NET Core をサポートしているし、Dockerコンテナ も普通に用意されています。

「は?オレ今まで .NET 使ってないから関係ねーし」と言われればそれまでですが、この先ずっと同じ言語を使って働いていくとも限らないですし、選択肢が増えるのは良いことです。

マイクロソフト自身が「脱Windows」している

個人的にもっと象徴的だなあと思ったのが SQL Server on Linux です。

MS の RDBサーバーである SQL Server は当然 Windows OS 専用でしたが、それすら Linux 版を作ってしまいました。

このように、マイクロソフト自身が Windows を捨てる準備を着々としているように感じられます。

Xamarin は最近どうなのよ?

さて、ネイティブアプリ開発が PWA に喰われ、クロスプラットフォームなネイティブアプリ開発は Flutter に押されと、オワコン説もささやかれる Xamarin ですが、Xamarin の最近の動向を書いてみます。

そもそもブランドとしての Xamarin とは、

  • Android や iOS の API を .NET 向けにラップした Xamarin Native(Xamarin.Android や Xamarin.iOS)
  • ↑を利用した、Android/iOS/etc共通のUI/アプリケーションフレームワークである Xamarin.Forms 3

に大別されます。

Xamarin Native は縁の下の力持ちというか、Android,iOSそれぞれのSDKのバージョンアップに追従して粛々とバージョンアップをし続けていて、それだけでとても価値があります。逆に言えば、ここでは皆が驚くような新機能は今後も出てこないでしょう。

Xamarin.Forms が、Flutter や ReactNative と直接の競合関係になる箇所です。
Flutter の「Materialで美麗なアプリを爆速で」とか、ReactNativeの「WebもAndroidもiOSも」に比べると弱いですが、機能追加はしています。最近だと、

  • FlexLayout - FlexBox みたいなやつ、というかそれ
  • Xamarin.Forms Visual - Flutter よろしく Android/iOS で Material な見た目を実現できるやつ

Xamarin.Forms Visual は実現方法は違えど、対Flutterとしては効果が期待できそうな雰囲気を感じます(目的はそれかは知らない)。

image.png

Visual Challenge Conquered! | Xamarin Blog より

今後私は Xamarin と、あるいはネイティブアプリとどう向き合って行くのか?(持論)

Xamarin に関しては、徐々にシェアは減っていくのでしょうが、ぽんぽこ風に言えば「それでもどっこい生きている」でしょうか。意外としぶといんですよ、.NET が死ななければ Xamarin も死なないし。

私自身は、「PWAファースト」な考えをしています。
最近は Glide という Spreadsheet から PWA が自動生成できる サービスに注目していて、そう言えばこれは Xamarin からスピンアウトした人達が作っているサービスでした。

スピンアウトした彼らは、ネイティブアプリからPWA移行の急先鋒と言えるのかも知れないですが、この Glide に注目している理由を、冒頭の @Nkzn さんのブログから引用します。

BtoBでお客さんが「アプリが欲しい」と言った場合に、実はそれは素朴なWebアプリでいい要件だったり、少し込み入ってもPWAの範囲でなんとかなるものは、意外と多いのではないでしょうか(肌感覚です)。

こういう肌感覚を私も持っていて、Glide ってまさにこういうニーズにぶっ刺さるんじゃないかな、と感じるからです。
Salesforce みたいなツールを導入するコストは無い、業務は EXCEL で回っているみたいな中小企業さん向けのソリューションですね。
Glide がそのまま使えるケースは限られるでしょうが、開発の参考にする価値は大いにあるなあと。

PWA の次の選択肢として検討するのが「ガワネイティブ」ですね。
DroidKaigi 2019 の私の発表 で、「再考!ガワネイティブ」という章を設けました。

image.png

これの意図するところは、

  • プレゼンテーション層は PWA(というか Web)
  • デバイス連携はネイティブ

とすることで、現状 Web では苦手な機能を部分的にネイティブにやらせることで、いいとこ取りが簡単にできるじゃん?というものです。

で、このデバイス連携箇所をネイティブで作る際、Xamarin(Xamarin Native)が活躍します。
例えば 「カメラで画像を撮影して Android の TensorFlow Lite や iOS の CoreML で解析してその結果を返す」 という一連の処理をそれぞれ実装して、抽象化されたAPIを定義し、それを JavaScript と連携させて PWA から呼び出す、ということは、現状もっとも作りやすいのは Xamarin と .NET です。
Flutter や ReactNative や Cordova などはどうしてもネイティブとのブリッジが面倒です。その面倒さは API をラップしている Xamarin Native にはほぼありません(何度言っているのだろうこのセリフ)。
ただ、Kotlin Multiplatform がもっと進化したら置き換えられる可能性はあります、期待しています。

おわりに

まとめると、

  • モバイルネイティブアプリ開発者は徐々に絶滅危惧種になっていくと思う
  • Xamarin はそれでもどっこい生きている(く)
  • 10年後のことは誰にもわからない

モバイル "ネイティブ" アプリ開発案件の数は減っていくと思っていて、その中のクロスプラットフォームアプリ開発ツールと領域でも Flutter や RN、Kotlin MPP とパイを取り合う構図なので、Xamarin へのニーズも徐々に減っていくと思います(クロスプラットフォームはモバイルに限った話ではない、という話ならそれは Xamarin ではなく ".NET" ですし)。

一方で .NET の利用シーンは増えている(Azure, ゲーム(Unity), HoloLens など)ので、「周りがほとんど .NET だった時にモバイルアプリが必要になったら Xamarin」という選択にはなると思います。JavaScript はほぼどこでも使えるようになっているし、Kotlin もそうなりつつあります。Dart はまだ未知数という感じでしょうか。

Android Dev Phone 1 を $400 で買って、自分で書いたアプリケーションがその端末で動いたとき、めちゃくちゃ感動したし、なんだかものすごい未来が来る予感がして、実際(概ね)そうなったわけですが、その10年前と同じ感動と予感が得られる「何か」をずっと探していて、そのために、スマホアプリに限らずいろいろなテクノロジーに手を出しています。たぶんこれからもそうして生きていくと思います、それが楽しいです。


  1. ポジションも React を Xamarin に置き換えればほぼ同じだわw 

  2. 言語という意味では C# もガンガン進化してます 

  3. もはやこちらが「Xamarin」として認知されているのが現状ですが、そうでは無いことをご理解いただきたく。 

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

Glideで個人間で使う旅行計画PWAアプリをつくってみた

はじめに

ネイティブアプリデベロッパーからしたら驚愕のWebサービスが出てきました。
当方もネイティブアプリ開発をしていますが、15分足らずでPWAアプリが作れるのは脅威的だと思います。

本記事では、ざっくり作り方、Glideでできることできないことを明確にしていきたいと思います。
(記事執筆時 2019/04/17 時点)

作成したアプリになります。ご査収ください。

スポット一覧

スクリーンショット 2019-04-16 23.53.19.png

スケジュール一覧

スクリーンショット 2019-04-17 0.21.56.png

GWにタイ旅行に行くので、知り合いとスポットを管理できたら便利だなぁと思い作成しました。

アプリ前提

  • データ作成は、本アプリからユーザが行う(PC使えない場合も踏まえて)
  • 少人数での利用(2-3人)

GlideでのPWAアプリ作り方

ぶっちゃけある程度はポチポチしてるだけで作れるので説明はあまりいらないかと思いますが、一応つけときます。

  1. Glide に登録し「New App」 スクリーンショット 2019-04-16 23.55.30.png
  2. Google Spreadsheetでデータの作成 スクリーンショット 2019-04-16 23.57.21.png
  3. Glideでデータを選択 スクリーンショット 2019-04-16 23.58.34.png
  4. Glide画面のNavigation, Screen, Settings で入力項目や表示項目を設定していく スクリーンショット 2019-04-17 0.01.48.png

めちゃくちゃ簡単っ!!

Glideでの操作

  • Navigation : Spreadsheetのシートに応じてメニューを増やすことができます スクリーンショット 2019-04-17 0.06.17.png
  • Screen : 画面の表示項目や入力項目を追加できます
    スクリーンショット 2019-04-17 0.06.29.png

  • Settings: アプリ名、ロゴを決めたり、PWAリンクを設定できます

ドメインを登録するには課金しないとみたいです
http://help.glideapps.com/faq/setting-a-custom-domain-for-your-app

スクリーンショット 2019-04-17 0.05.32.png

Glideでできなかったこと、注意すること

正直ここまで簡単にアプリが作れるので、文句はないのですが、これができればなぁという一覧です。まあアップデートでなんとでもなると思うので今後も期待ですね。

  • 写真はURLの直アドレスのみ

  • アプリ上から画像の保存は不可

    • 手入力で画像アドレスをはっつけるのはめんどい!と思い、ないかなと期待しましたが、残念ながらできない様子
  • データ入力項目はテキストのみのため、日付項目などは選択できない

    • (今のところ)データの入力項目はNote、テキスト、Switchのみです。
  • Spreadsheetの計算式は利用できない?

  • Navigation(アプリメニュー項目)を増やすと表示項目などの設定が初期化される(バグ?)

    • 途中まで作成してあとからメニューを増やそうとしたら表示項目などが初期化されてしまい、また最初からになりました。(簡単だからいいけど、さすがにめんどくさい。。。)
  • Setting(入力項目や表示項目)のエクスポートができない

    • 入力項目や表示項目のエクスポートはできず、同様にアプリをコピーとかもできないようです。

さいごに

無料かつ短時間でノンプラグラミングでここまでのアプリができてしまうGlide。個人、企業で利用しない手はないですね!
個人的にはもっと細かく設定できたり、設定をエクスポートできたらなぁという思いがありますが、今後有料化したりして機能が増えていけばなぁと思う次第です。

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